- Shipped
- August 31, 2026 at 2:12 AM UTC
- Author
- Kamo
- Commit
- a47805a
Adds BillingCycle.TRIENNIAL so a subscriber can commit for three years. The column is varchar with no CHECK constraint anywhere, so this needs no migration — but every service that READS such a row must be running this enum first, because valueOf throws on an unknown name and getCatalog loads every price in one query. Three defects had to be fixed for the term to be safe to add: 1. unitPrice is a per-seat-per-MONTH rate on every cycle — BillingCycles in BillingService says so and multiplies by the month count before it reaches Stripe. SubscriptionService did the opposite: calculateMrr DIVIDED an annual rate by 12 (reporting a $59 seat as $4.92 of MRR) and generateInvoice billed one month's rate for a whole year of service. At 36 months those become 36x errors. The month count now lives on the enum, where a new constant cannot be added without answering for it, and invoicing multiplies by it. 2. calculatePrice checked nothing about a promotion but isActive. Applicable plans, applicable billing cycles, the start and end dates, the seat minimum and the redemption cap were all persisted and none were read — a code written for two tiers discounted all four, and an expired code never stopped working. validatePromoCode checked the dates and the cap but not the plan or the cycle, so the endpoint that exists to answer "does this code apply" disagreed with the arithmetic that applied it. Both now ask SubscriptionPromotions. 3. FLAT_PER_SEAT returned the bare discount value without multiplying by seats, making it a silent synonym for FLAT_AMOUNT: an "$18 per seat" code took $18 off a 25-seat order instead of $450. A percentage promotion now comes off the plan's per-seat subtotal after the term discount, not off plan+addons — "33% off the base per seat cost", applied to the rate the customer actually committed to.