Functional Weave
Code in TypeScript

subscriptions.churn@1.0.0

vectors.json

5,604 bytes · view raw

[
  {"name": "a typical month: expansion exactly offsets losses, so net churn is zero and NRR 100%", "args": [200, 7, {"minor": 1000000, "currency": "GBP"}, {"minor": 50000, "currency": "GBP"}, {"minor": 20000, "currency": "GBP"}, {"minor": 30000, "currency": "GBP"}], "expect": {"logoChurnBasisPoints": 350, "grossRevenueChurnBasisPoints": 500, "netRevenueChurnBasisPoints": 0, "grossRevenueRetentionBasisPoints": 9500, "netRevenueRetentionBasisPoints": 10000}},
  {"name": "ChartMogul's example: 100 + 10 - 10 - 10 is 90% net retention", "args": [50, 5, {"minor": 10000, "currency": "GBP"}, {"minor": 1000, "currency": "GBP"}, {"minor": 1000, "currency": "GBP"}, {"minor": 1000, "currency": "GBP"}], "expect": {"logoChurnBasisPoints": 1000, "grossRevenueChurnBasisPoints": 2000, "netRevenueChurnBasisPoints": 1000, "grossRevenueRetentionBasisPoints": 8000, "netRevenueRetentionBasisPoints": 9000}},
  {"name": "negative churn: expansion beats losses, NRR above 100%", "args": [40, 1, {"minor": 100000, "currency": "GBP"}, {"minor": 15000, "currency": "GBP"}, {"minor": 2000, "currency": "GBP"}, {"minor": 3000, "currency": "GBP"}], "expect": {"logoChurnBasisPoints": 250, "grossRevenueChurnBasisPoints": 500, "netRevenueChurnBasisPoints": -1000, "grossRevenueRetentionBasisPoints": 9500, "netRevenueRetentionBasisPoints": 11000}},
  {"name": "one lost out of three is 33.33%, rounded to a whole basis point", "args": [3, 1, {"minor": 30000, "currency": "GBP"}, {"minor": 0, "currency": "GBP"}, {"minor": 0, "currency": "GBP"}, {"minor": 10000, "currency": "GBP"}], "expect": {"logoChurnBasisPoints": 3333, "grossRevenueChurnBasisPoints": 3333, "netRevenueChurnBasisPoints": 3333, "grossRevenueRetentionBasisPoints": 6667, "netRevenueRetentionBasisPoints": 6667}},
  {"name": "two lost out of three rounds up to 66.67%", "args": [3, 2, {"minor": 30000, "currency": "GBP"}, {"minor": 0, "currency": "GBP"}, {"minor": 0, "currency": "GBP"}, {"minor": 20000, "currency": "GBP"}], "expect": {"logoChurnBasisPoints": 6667, "grossRevenueChurnBasisPoints": 6667, "netRevenueChurnBasisPoints": 6667, "grossRevenueRetentionBasisPoints": 3333, "netRevenueRetentionBasisPoints": 3333}},
  {"name": "half a basis point of net churn rounds away from zero", "args": [10, 0, {"minor": 20000, "currency": "GBP"}, {"minor": 1, "currency": "GBP"}, {"minor": 0, "currency": "GBP"}, {"minor": 0, "currency": "GBP"}], "expect": {"logoChurnBasisPoints": 0, "grossRevenueChurnBasisPoints": 0, "netRevenueChurnBasisPoints": -1, "grossRevenueRetentionBasisPoints": 10000, "netRevenueRetentionBasisPoints": 10001}},
  {"name": "everyone lost: 100% churn and nothing retained", "args": [10, 10, {"minor": 5000, "currency": "GBP"}, {"minor": 0, "currency": "GBP"}, {"minor": 0, "currency": "GBP"}, {"minor": 5000, "currency": "GBP"}], "expect": {"logoChurnBasisPoints": 10000, "grossRevenueChurnBasisPoints": 10000, "netRevenueChurnBasisPoints": 10000, "grossRevenueRetentionBasisPoints": 0, "netRevenueRetentionBasisPoints": 0}},
  {"name": "no customers and no MRR at the start: every rate is null, not zero", "args": [0, 0, {"minor": 0, "currency": "GBP"}, {"minor": 0, "currency": "GBP"}, {"minor": 0, "currency": "GBP"}, {"minor": 0, "currency": "GBP"}], "expect": {"logoChurnBasisPoints": null, "grossRevenueChurnBasisPoints": null, "netRevenueChurnBasisPoints": null, "grossRevenueRetentionBasisPoints": null, "netRevenueRetentionBasisPoints": null}},
  {"name": "free customers only: logo churn exists, revenue rates do not", "args": [5, 1, {"minor": 0, "currency": "GBP"}, {"minor": 0, "currency": "GBP"}, {"minor": 0, "currency": "GBP"}, {"minor": 0, "currency": "GBP"}], "expect": {"logoChurnBasisPoints": 2000, "grossRevenueChurnBasisPoints": null, "netRevenueChurnBasisPoints": null, "grossRevenueRetentionBasisPoints": null, "netRevenueRetentionBasisPoints": null}},
  {"name": "a quiet month: nobody left, nothing changed", "args": [120, 0, {"minor": 600000, "currency": "USD"}, {"minor": 0, "currency": "USD"}, {"minor": 0, "currency": "USD"}, {"minor": 0, "currency": "USD"}], "expect": {"logoChurnBasisPoints": 0, "grossRevenueChurnBasisPoints": 0, "netRevenueChurnBasisPoints": 0, "grossRevenueRetentionBasisPoints": 10000, "netRevenueRetentionBasisPoints": 10000}},
  {"name": "more customers lost than there were is an error", "args": [5, 6, {"minor": 1000, "currency": "GBP"}, {"minor": 0, "currency": "GBP"}, {"minor": 0, "currency": "GBP"}, {"minor": 0, "currency": "GBP"}], "expectError": "customers lost must not exceed customers at start"},
  {"name": "a negative customer count is an error", "args": [-1, 0, {"minor": 1000, "currency": "GBP"}, {"minor": 0, "currency": "GBP"}, {"minor": 0, "currency": "GBP"}, {"minor": 0, "currency": "GBP"}], "expectError": "customer counts must be whole numbers of 0 or more"},
  {"name": "losing more MRR than there was is an error", "args": [5, 1, {"minor": 1000, "currency": "GBP"}, {"minor": 0, "currency": "GBP"}, {"minor": 600, "currency": "GBP"}, {"minor": 500, "currency": "GBP"}], "expectError": "contraction and churned MRR must not exceed MRR at start"},
  {"name": "a negative expansion is an error", "args": [5, 1, {"minor": 1000, "currency": "GBP"}, {"minor": -10, "currency": "GBP"}, {"minor": 0, "currency": "GBP"}, {"minor": 0, "currency": "GBP"}], "expectError": "MRR amounts must not be negative"},
  {"name": "amounts in different currencies are an error", "args": [5, 1, {"minor": 1000, "currency": "GBP"}, {"minor": 10, "currency": "EUR"}, {"minor": 0, "currency": "GBP"}, {"minor": 0, "currency": "GBP"}], "expectError": "currency mismatch"}
]