Sell the three-year term, and stop four copies of the cycle drifting

Featurekamo-internal
Shipped
August 31, 2026 at 2:13 AM UTC
Author
Kamo
Commit
58d2459

The billing cycles were typed out by hand in five places. One of them, the plan-pricing editor, offered "BIENNIAL" — a value the server has never had — so choosing it sent a string BillingCycle.valueOf throws on and saving a price 500'd with nothing to suggest the option itself was the problem. posApi now owns BILLING_CYCLES, BILLING_CYCLE_MONTHS and BILLING_CYCLE_LABELS, and a vitest guard reads the Java enum source and fails if the two ever disagree on constants, order or month counts. Two computeMRR copies (the subscription detail page and the pipeline signups tab) divided the order subtotal by the cycle length. That subtotal is ALREADY monthly — calculatePrice builds it from a per-seat-per-month unit price — so an annual subscriber was reported at a twelfth of their real MRR, and a three-year one would have been reported at a thirty-sixth. Both now use the subtotal directly, matching the server-side fix. planQuote gains the third term. A missing term falls back to the next SHORTER one rather than straight to monthly: quoting month-to-month against a three-year commitment undercharges by the whole commitment discount. chargesPerYear becomes monthsPerCharge, because 12/36 is 0.33 and "billed 0.33 times per year" is not a sentence an interface can use. The billing console's monthly/annual Switch becomes a three-way ToggleButtonGroup — a switch cannot express three states, and the third is the cheapest rate we sell, so it has to be visible rather than reachable by toggling twice. Its saving badge now fires on any committed term, not the annual one alone.

All changes

Like what you see shipping?

Every one of these updates lands in your workspace automatically. Start free and watch it grow week after week.

Start Free ForeverView Pricing