- Shipped
- September 6, 2026 at 5:42 PM UTC
- Author
- Kamo
- Commit
- a782fef
GET /api/billing/self/plan answers everything /myplan renders: the plan, what the organization can actually use, the catalogue to compare against, seats and their roster, storage, renewal, and any negotiated agreement. One request rather than six, for a reason beyond the loading states: the access tier cannot honestly be decided in the browser. Deciding it there means listing the Account's members, and a member may only list them if they already hold the access being decided. So it is decided here, twice — the client gate decides what to OFFER, this decides what may be read — and a SELF_PAY caller receives a payload the seat roster, organization totals and agreement were never put into. Withholding beats hiding: someone reading the network tab of a page they are allowed to open should learn nothing they were not allowed to be told. The trial is the trap worth naming. A trial subscription sits on plan_code = FREE, because TrialAdminService converts the free-default row in place, while EntitlementService grants the entire catalogue unlimited. Serving the plan's own feature rows during a trial would tell a customer they do not have AI, e-signatures or SSO while they are in the middle of using all three — in the week they are deciding whether to buy. So an active trial reports the union of the catalogue as included. A PENDING trial deliberately does not: its clock has not started, and promising otherwise is a lie the product contradicts on first click. The agreement is returned derived, never transcribed. The prose is customerVisibleNote and only that; internal notes never leave the service. And the next seat's cost is a number from SetupFees.forAddedSeats — the same code that raises the charge — rather than a sentence describing the rule, because a sentence is a second implementation that will eventually disagree with the first.