Build ChartMogul

KINDAback to the verdict

0%0 of 17 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

A subscription analytics layer over billing data whose difficulty is definitional rather than computational: the arithmetic is easy, the disagreement about what counts is not, and every number has to carry its own definition to be worth anything.

estimated effort multi-daythe files for this build are in the project pack

SourceRead-only import from the billing system, never a second source of truthAmountsInteger minor units with currency and the rate at the timeDefinitionsEvery metric stored as an explicit rule set with a versionMovementsRevenue changes classified into new, expansion, contraction, churn and reactivationTraceabilityEvery figure openable to the subscriptions and invoices behind it

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. installfree

    Why Runs the import and the metric computation.

    Get it nodejs.org, or nvm install 22. open ↗

    Verify node --version prints v22 or higher

  2. installfree locally

    Why Movements, definitions and exact integer arithmetic over billing history.

    Get it Docker or your package manager. open ↗

    Verify psql connects and integer sums are exact

  3. free with an existing account

    Why Failed payments, retries, mid-period changes and refunds are the cases that break naive imports, and none of them appear in generated data.

    Get it Read-only API access to a billing account with at least a year of history including refunds and failed payments.

    Verify Imported totals reconcile against the system's own report

  4. decidefree, and it takes a conversation

    Why Annual spreading, discounts, refunds, set-up fees and usage each change the figures materially, and deciding them after seeing the numbers means choosing whichever looks best.

    Get it Write down how each is treated and why, and agree it with whoever will quote the figures.

    Verify A written definition covers all five and appears in the configuration

  5. have readyfree

    Why Movement classification is only testable where a real customer downgraded then upgraded, or churned and returned, which generated data never contains.

    Get it Use the real history rather than a fixture, and identify the awkward customers before computing anything.

    Verify At least three customers with mid-month changes or returns are identified

Data model

Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.

- customers(id, external_id, first_paid_at, churned_at, segments_json)
- subscriptions(id, customer_id, plan_id, started_at, ended_at, interval, amount_minor, currency, discount_json)
- invoices(id, customer_id, issued_at, paid_at, refunded_at, lines_json, amount_minor, currency, fx_rate)
- metric_definitions(id, name, version, rules_json, note, effective_from) · what counts, written down
- movements(month, customer_id, kind, amount_minor, definition_version) · kind is new, expansion, contraction, churn or reactivation
- metric_values(metric_id, definition_version, period, value_minor, components_json, computed_at)
- A movement records the definition version that classified it, so a series computed under two definitions is visibly two series rather than one that jumped.

Environment variables

These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.

VariableNeededExampleWhere the value comes from
DATABASE_URLsecretrequiredpostgres://localhost:5432/metricsImported billing history for a real business.
BILLING_API_KEYsecretrequireda read-only keyRead only. This never writes to the billing system.
DEFINITIONS_PATHrequired./config/definitions.jsonThe five decisions, versioned, displayed beside every figure.
REPORTING_CURRENCYrequiredUSDFor conversion. Source amounts keep their own currency and rate.
FX_POLICYrequiredrate_at_invoicePresent as a constant. Re-converting rewrites history every morning.
REACTIVATION_WINDOW_DAYSrequired90A returning customer within this is reactivation rather than new.

The build, in order

  1. Importing billing without becoming billing

    Every charge, refund and change is imported and reconciles to the source.

    1. Failures and retries, not only successful charges.

      Files src/import.ts

    2. Exactly. A difference is a defect rather than a rounding matter.

      Files tests/reconcile.test.ts

    3. Re-running must change nothing.

      Files src/import.ts

    done when · tick each as it passes
    watch out
    • Importing only successful invoices misses the failed payment that succeeded three days later, so a customer appears churned and then reactivated in the same month, which corrupts every movement figure. Import the full event history including failures and retries, and derive state from it.
  2. Definitions as data

    Every metric has a written rule set you can read.

    1. Annual spreading, discounts, refunds, fees, usage. Five decisions minimum.

      Files config/definitions.json

    2. So a disagreement is about the rule rather than the arithmetic.

      Files src/report.ts

    3. A changed definition changes history and must be visible.

      Files src/definitions.ts

    done when · tick each as it passes
    watch out
    • Encoding the definitions in code means the answer to what counts is spread across functions nobody reads, and when an investor asks how you compute recurring revenue the honest answer is that you would have to go and look. Store the rules as data, print them beside the number, and version them.
  3. Movements, where definitions bite

    Every change in revenue is classified consistently and traceably.

    1. Recording which version classified each movement.

      Files src/movements.ts

    2. If they do not, one of the categories is wrong.

      Files tests/movement-sum.test.ts

    3. A customer returning after two months is a different event from one after two years.

      Files src/movements.ts

    done when · tick each as it passes
    watch out
    • A customer who downgrades on the first of the month and upgrades on the twentieth can be recorded as contraction then expansion, as a single net change, or as neither, and the three produce very different churn figures from identical facts. Decide the rule, record which was applied, and make it visible per movement.
  4. More than one currency

    Historic revenue does not change when exchange rates move.

    1. Re-converting rewrites history every morning.

      Files src/currency.ts

    2. So the figure can be reproduced by someone else.

      Files src/report.ts

    3. It happens and it should not corrupt the customer's history.

      Files src/currency.ts

    done when · tick each as it passes
    watch out
    • Converting all historic revenue at today's rate means last year's figures change every day, so a report run this morning disagrees with the one you sent an investor last week and neither is wrong. Fix the rate at the invoice and never re-convert.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more analytics.