- Shipped
- September 6, 2026 at 9:39 PM UTC
- Author
- Kamo
- Commit
- 5cfc969
An audit of the Plans & Billing console against live Stripe keys found the seam between the screen and Stripe broken in every direction at once, and silent in all of them. No organization had ever completed a paid subscription, so every one of these would have fired on the first real customer. WHAT THE CUSTOMER WAS CHARGED The advertised offer was never applied. Promotions were a display concept: resolved by PlanCatalogService, carried to the browser, struck through on the plan cards, counted down in the banner — and then the subscription was created with no coupon, no discount and no promotion code. A grep for any of the three across the service found nothing. StripePromotionService mirrors the live promotion as a real Stripe coupon, scoped with applies_to.products to the plan's own product so it discounts the seats and NOT the add-ons riding alongside them, which is what **************** has always done. The coupon id encodes the percentage and duration, so editing the catalogue mints a new coupon instead of silently reusing the old rate. The startup fee was never charged. Both paid plans carry $99 per seat, so a five-seat order owes $495 before a month is billed. calculatePrice quotes it, generateInvoice bills it, ConsumerCheckoutService bills it — and this path, the one an organization actually buys through, billed it nowhere, so the same plan cost $495 less depending on which door the customer came through. SetupFeeBiller posts it as a pending invoice item before the subscription opens, so it lands on that subscription's own first invoice, and asks SetupFees for the amount so a negotiated agreement (WAIVED, FLAT, PER_SEAT_OVERRIDE) is honoured. Once for the account on the first order, and per person as the team grows. Stripe prices had drifted to roughly half the catalogue. ensurePlanPrices short-circuits on a mapping existing, so a re-price changed every surface except the charge: Business monthly quoted $119 and billed $72. The only remedy was a platform-admin endpoint nobody had called. Purchases now check the mapping against the catalogue and repoint before billing, and a nightly sweep reports drift across the estate. The reconciler still refuses to move live subscribers, because that is a commercial decision rather than a reconciliation. WHAT HAPPENED WHEN IT FAILED No card was required and none was collected. Stripe opened the subscription with nothing to charge, answered `incomplete` — a normal response, not an exception — and everything downstream carried on: the row was stamped PAID, the trial was erased, and the customer was told their plan was updated over a subscription Stripe would delete within the day. A paid subscription now requires a payment method, opens with **************** so a decoder throws and the transaction rolls back with the trial intact, and asserts a live status afterwards. Buying a plan unassigned every seat in the organization. create() deleted every licence and rebuilt them unassigned, and nothing put anyone back — including on a purchase that succeeded. rebalanceSubscription already reconciles seat rows in both directions, so the delete was never reconciling anything; it is gone. An add-on with no Stripe price was granted free. desiredItems logged "it will not be billed" and continued, while the code alone grants the app. It now refuses. WHAT THE CUSTOMER WAS TOLD Every refusal read "Internal Server Error". The service had no @ControllerAdvice, so a dozen carefully worded, customer-facing sentences — the Enterprise refusal, the seat ceiling, the mail add-on guard — became Spring's default 500 body. BillingExceptionHandler answers 400 with the message for a rule, 402 for a decoder, and a generic sentence plus a traceable reference for anything else, which is never echoed. No dunning mail could be delivered. DunningNotifier posted to **************** which does not exist; it now calls /api/email/templates/send, which does and is behind InternalAuthFilter, with the shared secret wired through the manifests. The templates land in kamo-shared-library 941c2421. Nothing told an owner their organization's bill had failed. /self/alerts raised alerts only for a member's own seat and groups they manage — sound for a member, who cannot act, but the owner is exactly who can. Scoped to the owner, so the original rule still holds for everyone else. Enterprise carried a firm price. Its terms are negotiated and EnterprisePlanGuard refuses any request naming it, but the catalogue served 119/109/99 while the public site said "Custom" — one company, two answers. A CUSTOM plan now arrives with no rate and no setup fee unless the caller may actually sell it. Also: add-on quantities are on the wire, so a console draft can start from what the subscription actually holds rather than from an empty map that reset storage blocks to one on save. 282 existing tests pass; 15 added covering coupon derivation, the setup fee including negotiated agreements, and what a refusal looks like to a customer.