import { formatMoney } from './money'; import type { SeatStanding } from './seatStanding'; /** * What to tell an admin BEFORE enabling a clinician takes their money. * * §3 * The settled ruling is that **enabling a clinician IS the purchase** - the charge is the consequence, * not a separate act - and it carries one named requirement: *a confirmation that names the money.* * **Never a silent charge on a toggle.** * * THE MONEY ALREADY MOVES. Measured, not assumed: the clinician-seat sweep has reconciled hourly for * at least seven days with the $50 Price wired and `failed: 0`. So the card's own claim that "nothing * updates the Stripe subscription quantity" is out of date - which makes the missing confirmation the * SHARPER defect rather than a smaller one. Ticking a checkbox and pressing Save bills the practice * $50/month within the hour, and the only warning is the phrase "a paid seat". It never says fifty * dollars. * * A PURE FUNCTION because the SENTENCE is the product. What an admin is told immediately before money * moves is the thing worth pinning; a dialog test proves a dialog opened, never what it said. */ /** * The OTHER thing a seat does, and the half that had no sentence anywhere. * * A seat was described only as something you buy. What it actually DOES - make somebody appear in the * booking picker for patients and the front desk - was never said on the screen that grants it. That is * how a Receptionist at Great River came to hold one: the admin was told about a subscription line, not * about who could now be booked. * * A CONSTANT, NOT A BRANCH, and that is deliberate. It is equally true of the FREE first seat, so * folding it into the pricing function would make the one grant nobody is warned about the one grant * that says least. The money varies; what the seat permits never does. */ export const CLINICIAN_SEAT_PERMITS = 'They become bookable as a clinician — patients and the front desk can book visits with them.'; export interface ClinicianSeatChargeInput { /** * Clinicians already enabled at this organization, BEFORE this change. * * `undefined` when the count could not be read. It must never be treated as 0: zero would * promise the seat is FREE, which is the most expensive wrong answer available here - a promise * about money made immediately before taking some. */ enabledClinicians: number | undefined; /** The seat Price in cents. `undefined` when no Stripe Price is configured in this environment. */ seatPriceCents: number | undefined; /** * Does this practice already have a subscription to put the seat on? * * `undefined` when we have not been told, and that is a THIRD answer rather than a shrug: the * sentence hedges instead of promising either that the bill changes or that it does not. * * WHY IT IS HERE AT ALL. Spencer ruled on 2026-09-14 that a chargeable seat founds the * subscription, at checkout. So on a free practice this save does not add a line to a bill - it * asks for a card. The old sentence said "this adds $50.00 per month to your subscription" to a * practice with no subscription, which was measured on staging as a modal promising a charge that * no mechanism delivered. * * OPTIONAL, AND THE OMISSION IS THE SAFE ANSWER. A caller that does not pass it gets the hedge, * never "you have one" - so the way to get this wrong by forgetting is the way that cannot promise * somebody a charge, or the absence of one, that we were never told about. */ hasSubscription?: boolean | undefined; } export interface ClinicianSeatCharge { /** What this change adds to the monthly bill. `undefined` when the price is not known. */ chargeCents: number | undefined; /** The seat line's monthly total after the change. `undefined` when either input is unknown. */ newSeatTotalCents: number | undefined; /** One sentence, and it is the thing the confirmation exists to say. */ headline: string; /** * True when pressing through this confirmation leads to a PAYMENT SCREEN rather than to a line on * an existing bill. * * The button reads differently for the two, and it has to: "Enable and add to my bill" in front of * a Stripe checkout tells somebody the money is already settled when they are about to be asked * for a card. */ startsSubscription: boolean; } /** * Whether saving this form is about to start a charge, and therefore must be confirmed first. * * Extracted rather than left inline in the component so the RULE is checkable. The two ways to get * this wrong are opposites and both are quiet: never confirming is the silent charge §3 forbids, and * confirming on EVERY save trains admins to dismiss the dialog - which is how a confirmation stops * protecting anything while still appearing to. */ export function shouldConfirmClinicianSeatCharge({ draftEnabled, savedEnabled, canManageSeats, }: { draftEnabled: boolean; savedEnabled: boolean; canManageSeats: boolean; }): boolean { // Only on the way ON. Re-saving an already-enabled clinician charges nothing new, and an actor who // cannot manage seats never sends the flag at all - the server would discard it, so confirming a // charge that cannot happen would be a lie in the other direction. return canManageSeats && draftEnabled && !savedEnabled; } /** * Whether saving this form is about to take a seat AWAY, and therefore must deal with that clinician's * booked patients first. * * TWO ROUTES REACH THE SAME PLACE, AND THE SECOND IS THE ONE THAT WOULD HAVE BEEN MISSED. The obvious * one is unticking the seat. The other is unticking the PRACTITIONER WORKSPACE while the seat stays * ticked: a seat only ever belongs to a practitioner, so the save drops it either way. Testing the * checkbox alone would let that second route unseat somebody silently - which is the whole failure this * confirmation exists to prevent, reachable from a control that does not mention seats at all. * * A DROPPED WORKSPACE IS NOT A DROPPED SEAT ON ITS OWN. `draftEnabled` false with Practitioner still * ticked, on somebody who never had a seat, is an ordinary save - `savedEnabled` is what makes this a * removal rather than a no-op. */ export function shouldConfirmClinicianSeatRemoval({ draftEnabled, practitionerWorkspaceSelected, savedEnabled, canManageSeats, }: { draftEnabled: boolean; practitionerWorkspaceSelected: boolean; savedEnabled: boolean; canManageSeats: boolean; }): boolean { // An actor without the permission does not send the flag at all, so nothing is being removed and a // confirmation would describe a change that cannot happen - the same reasoning as the grant side. return canManageSeats && savedEnabled && !(draftEnabled && practitionerWorkspaceSelected); } /** * May the removal actually be committed yet? * * NOTHING HAPPENS BY DEFAULT. Every false below is a silent outcome if it is allowed through, and each * one has a different shape: * * - `impactKnown` false covers the calendar not read yet AND the read having FAILED. A failed look is * not an empty calendar, and the caller must leave the count unset rather than treat a failure as * zero - otherwise the most dangerous case reads as the safest one. `impactUnavailable` then tells * those two apart, because they deserve different answers: still looking waits, a look that FAILED * offers a way through. * - that way through is `acknowledgedUnknownCalendar`, and it is the reader's statement rather than * ours. Refusing outright looks like the careful choice and is the one-way door: whoever may grant * the seat and add the charge is then unable to take the charge off again, because a courtesy read * they are not permitted to make failed. A permission that is not needed to ADD a cost must not be * what prevents REMOVING it. What must never happen is us reporting an empty calendar we could not * see, which is why the count stays unset here rather than the failure being read as a zero. * - a real count with no plan is the admin having been shown the patients and answered nothing. * - "move them" is not an answer until it says WHERE and that colleague's calendar has actually been * assessed. Committing on a chosen-but-unassessed target unseats with nothing moved. * * Extracted rather than left inline in the dialog for the same reason the charge rule is: this is the * rule, and a rule sitting in a `disabled=` expression is one nobody can test. */ export function clinicianSeatRemovalIsReady({ impactKnown, impactUnavailable, acknowledgedUnknownCalendar, affectedCount, plan, reassignReady, }: { impactKnown: boolean; impactUnavailable: boolean; acknowledgedUnknownCalendar: boolean; affectedCount: number; plan: 'cancel' | 'reassign' | undefined; reassignReady: boolean; }): boolean { if (!impactKnown) return impactUnavailable && acknowledgedUnknownCalendar; if (affectedCount > 0 && plan === undefined) return false; if (plan === 'reassign' && !reassignReady) return false; return true; } export function describeClinicianSeatCharge({ enabledClinicians, seatPriceCents, hasSubscription, }: ClinicianSeatChargeInput): ClinicianSeatCharge { // The first clinician at a practice is free by DESIGN, not by promotion - a true solo runs the whole // platform for nothing. Only claimable when the count is actually known. const isFirst = enabledClinicians === 0; if (isFirst) { return { chargeCents: 0, newSeatTotalCents: 0, // Not "$0.00/month". A price of zero reads as a discount that could end; this line is // free because the model says so. headline: 'Your first clinician is free — this will not change your bill.', startsSubscription: false, }; } /* * A CHARGEABLE SEAT ON A PRACTICE WITH NO SUBSCRIPTION IS A CHECKOUT, NOT A BILL LINE. * * Spencer, 2026-09-14: "the 2nd clinician seat starts a checkout and founds a subscription * because the 2nd clinician seat is not free like the first." * * The server refuses this save with a 402 and the billing modal takes them to Stripe. So the * sentence says what is about to happen instead of describing a subscription that does not * exist - and it says the thing somebody about to be asked for a card most wants to know, * which is that nothing has been charged yet. * * IT DOES NOT SAY "this will not change your bill" while the charge is missing. Spencer * refused that wording explicitly: it is true only because the mechanism was absent, and it * invites a practice to enable ten seats it will later owe for. */ if (hasSubscription === false) { // An unreadable count cannot claim this one is chargeable, only that it may be. The same // rule as the price below: on a screen that precedes money, hedge rather than guess. const chargeable = enabledClinicians === undefined ? 'If this clinician is a paid seat, enabling them starts your subscription' : 'Your first clinician is free and this one is not, so enabling them starts your subscription'; return { chargeCents: seatPriceCents, // NOT a seat-line total. There is no seat line yet, and quoting one would be the same // invented figure the price branch below refuses to print. newSeatTotalCents: undefined, headline: `${chargeable}${seatPriceCents === undefined ? '' : ` at ${formatMoney(seatPriceCents)} per month`}. ` + 'You will be asked for a card, and nothing is charged until you have entered one.', startsSubscription: true, }; } if (seatPriceCents === undefined) { // No invented figure. On a confirmation that precedes a charge, saying the wrong number is // worse than admitting we do not have it. return { chargeCents: undefined, newSeatTotalCents: undefined, headline: 'This adds a paid clinician seat to your subscription.', startsSubscription: false, }; } // With an unknown count we still know what a billable seat costs, and that this one may be billable. // The wording must not promise it IS billable either - it hedges rather than guesses. if (enabledClinicians === undefined) { return { chargeCents: seatPriceCents, newSeatTotalCents: undefined, headline: `This adds a clinician seat at ${formatMoney(seatPriceCents)} per month.`, startsSubscription: false, }; } // Already at least one clinician this one is billable, and the seat line grows by exactly one. const newSeatTotalCents = seatPriceCents * enabledClinicians; /* * AND IT SAYS WHEN, which is the half this sentence was missing while its sibling had it. * * THE MEASUREMENT, on staging: enabling a third clinician moved the subscription page's ROWS * immediately and left the header reading the old total for 53 minutes. The rows come from * entitlements, derived at read time; the header comes from the live Stripe subscription, which * only the hourly seat sweep moves. Two totals sat on one screen with nothing saying which was * which, and the page's own settling guard covers a different lag - a minute or two, not an hour. * * The remove dialog fifteen pixels away has always said it: "your subscription stops being charged * for the seat within the hour." One half of a pair telling the truth about the hour and the other * saying nothing is what left the practice to work it out from two numbers. */ return { chargeCents: seatPriceCents, newSeatTotalCents, headline: `This adds ${formatMoney(seatPriceCents)} per month to your subscription, taking clinician seats to ${formatMoney(newSeatTotalCents)} per month. Your bill shows it within the hour.`, startsSubscription: false, }; } /** * Whether the clinician seat control is offered to this actor at all. * * Starting a subscription is completed only in the Admin workspace: the billing refusal offers the * upgrade to Admin alone and tells everybody else to contact an administrator. So a seat that would START * a subscription is not offered outside Admin - the form must never promise a card it cannot take. * A seat that joins an existing bill, the free first clinician, and a seat the member already holds stay * with whoever the practice granted `clinicians: manage`. An unanswered billing read is not a yes. * * Narrow is the safe direction: it can hide a control that would have worked, never offer one that * cannot. The server still decides. */ export function mayOfferClinicianSeat({ canManageClinicians, isAdminActor, alreadySeated, billingKnown, seatCharge, }: { canManageClinicians: boolean; isAdminActor: boolean; alreadySeated: boolean; billingKnown: boolean; seatCharge: Pick; }): boolean { if (!canManageClinicians) return false; if (isAdminActor || alreadySeated) return true; return billingKnown && !seatCharge.startsSubscription; } /** The server refused a seat the screen showed as the included one, because it no longer is. */ export const isSeatPriceChanged = (error: unknown): boolean => { const candidate = error as { status?: number; body?: { reason?: string } } | null; return candidate?.status === 409 && candidate.body?.reason === 'seat-price-changed'; }; /** * What removing a seat does to the bill, from the billing this page has confirmed. Only a live paid subscription is * reconciled within the hour. Free Basic, ended or never paid, has no seat charge to stop, and a lapsed subscription * is sent no seat changes until it is paid again. Undefined when either answer is not known: nothing is said about * money that was not read. */ export function describeSeatRemovalBilling(billingStatus: string | null | undefined, seat: SeatStanding | null): string | undefined { if (!billingStatus) return undefined; if (billingStatus === 'free_basic') { return 'Your practice is on Free Basic with no paid subscription, so removing the seat changes nothing on your bill.'; } if (seat?.standing === 'lapsed') { return 'Your subscription has lapsed, so removing the seat changes nothing on your bill now. It will not be billed when you pay again.'; } if (seat?.standing === 'paid') return 'Your subscription stops being charged for the seat within the hour.'; return undefined; }