- Shipped
- 10 Agosti 2026, 23:54 UTC
- Author
- Kamo
- Commit
- c54414c
Five defects that all moved real money, plus the sub-resource IDOR beneath them. Billing cycles. ANNUAL prices were sent to Stripe at the stored per-month rate on a YEAR interval, so a plan seeded as "$28/mo annual" billed $28 once a year — a 12x undercharge. QUARTERLY and SEMI_ANNUAL mapped to a MONTH interval and never set interval_count, despite a comment claiming otherwise, billing the full period amount every month. BillingCycles now owns the conversion and documents the two price conventions the codebase genuinely has: SubscriptionPlanPrice carries a per-month rate, **************** a total. Pricing model. Every add-on was billed at quantity = seats regardless of its pricingModel, so the org-wide storage add-on cost 25x on a 25-seat org, and because Stripe rejects a quantity on a metered price, including it made the whole subscription create throw. Quantity now follows the model: seats for PER_SEAT, 1 for FLAT, absent for USAGE_BASED, skipped for ONE_TIME. Plan changes. The console posts create() for "Change Plan"; create() called Subscription.create unconditionally and overwrote the stored id, leaving the previous Stripe subscription live and unreferenced — billed twice, forever. It now amends the existing subscription when there is one. update() gained the plan change it had been silently discarding, and reconciles every line item rather than only items[0], which had left add-on quantities frozen at whatever the seat count was on the day the subscription opened. Add-on authorization. update() wrote addonCodes straight from the request body, the exact self-grant hole create() was hardened against: a FREE-tier caller could send {"addonCodes":["MLOS"]} and entitle the mortgage app, uninvoiced. Both paths now go through resolveAddonCodes. Seats. assign() only asked whether an UNASSIGNED row existed, and rows were never destroyed when the seat count shrank, so an org that cut 10 seats to 3 could re-assign all ten. It now checks capacity on both the fresh and the reactivation branch, refuses to issue seats on a lapsed subscription, and ensureSeatRows reclaims spare slots. A cancelled or unpaid Stripe subscription now lapses its licenses — the webhook wrote the status and stopped, so members kept paid features indefinitely. Authorization. Proving ownership of {accountUid} was the entire check; handlers then loaded subscriptions and licenses by their own id and never compared, so pairing your own account id with a foreign UUID cancelled someone else's paid subscription or revoked its seat. Bound centrally in the interceptor so routes added later are covered by default. Adds the first tests over price conversion, which had none.