import type { SubscriptionDunning } from '../../services/BillingContext.tsx'; // What a failing card SAYS, and who sees it. // // Spencer, 2026-08-14: *"a warning message at the top of admin view... if the card has already failed // as well as 1 retry then a warning should appear at the [top] of all staff views so everyone is aware // the subscription is about to expire and the system will stop working."* // // THE COPY IS THE FEATURE, so it is a pure function. The two audiences need different sentences and // the difference is not tone - it is what the reader can DO. An admin is told to fix a card. A // receptionist cannot fix a card, so telling them to would be useless; they are told what is about to // happen to their work and who to tell. export interface SubscriptionWarning { /** `error` once the whole practice is being told - this is no longer a routine reminder. */ severity: 'warning' | 'error'; message: string; /** FALSE at the all-staff stage: a banner about the system stopping is not one to wave away. */ dismissible: boolean; /** Only the people who can act on it get a link to act. */ actionHref?: string; actionLabel?: string; } const daysPhrase = (days: number | undefined): string => // No invented number. "expires in 7 days" made up from nothing is a false statement a practice // will act on - the costliest form of this codebase's recurring defaulting mistake, because it is // acted on rather than merely read. days === undefined ? 'soon' : days === 0 ? 'today' : days === 1 ? 'tomorrow' : `in ${days} days`; /** * A practice whose subscription a contractor pays, and who owns it as read for this viewer: the viewer, someone * else, nobody yet, or not known. Only an accepted owner may take billing back. */ export interface SponsoredBilling { owner: 'viewer' | 'other' | 'none' | 'unknown'; } /** Where the contractor-access panel, and its existing Take billing back control, opens. */ export const TAKE_BILLING_BACK_HREF = '#/dashboard/organization/account?panel=agency'; const PRACTICE_ACCOUNT_HREF = '#/dashboard/organization/account'; /** * An administrator of a contractor-paid practice cannot fix the contractor's card, so they are told who handles it * and given the remedy they actually have. Nothing here buys anything: the link only opens the existing control. */ function sponsoredWarning(dunning: SubscriptionDunning, sponsorship: SponsoredBilling): SubscriptionWarning { const lead = 'The contractor-paid subscription for this practice needs billing attention'; const consequence = dunning.stage === 'stopped' ? `${lead}, so paid add-ons are off. The core system keeps working.` : `${lead}, and paid add-ons switch off ${daysPhrase(dunning.daysRemaining)} if it isn’t settled.`; const remedy = { viewer: { text: 'You own this practice, so you can take billing back from the contractor.', actionHref: TAKE_BILLING_BACK_HREF, actionLabel: 'Take billing back' }, other: { text: 'Contact the practice owner or the contractor.' }, none: { text: 'This practice has no owner yet, and only an owner can take billing back. Set one up under Practice account.', actionHref: PRACTICE_ACCOUNT_HREF, actionLabel: 'Practice account' }, unknown: { text: 'The contractor handles this practice’s billing.' }, }[sponsorship.owner]; return { severity: dunning.stage === 'admin-only' ? 'warning' : 'error', message: `${consequence} ${remedy.text}`, dismissible: dunning.stage === 'admin-only', ...('actionHref' in remedy ? { actionHref: remedy.actionHref, actionLabel: remedy.actionLabel } : {}), }; } /** * The banner for this viewer, or null when there is nothing to say to them. * * `isAdmin` decides only whether the ADMIN-ONLY stage is shown; once the stage widens, everyone sees it. * `sponsorship`, for an administrator of a contractor-paid practice, replaces the payment-method remedy. */ export function resolveSubscriptionWarning( dunning: SubscriptionDunning | null | undefined, isAdmin: boolean, sponsorship?: SponsoredBilling, ): SubscriptionWarning | null { if (!dunning || dunning.stage === 'none') return null; if (isAdmin && sponsorship) return sponsoredWarning(dunning, sponsorship); if (dunning.stage === 'stopped') { // Stopped is also a live subscription whose payment is overdue, so it never claims the subscription ended. return { severity: 'error', message: isAdmin ? 'Your subscription needs attention, so paid add-ons are off. The core system keeps working. Update your payment method to restore them.' : 'This practice’s subscription needs attention, so paid add-ons are off. The core system keeps working. Please let an administrator know.', dismissible: false, ...(isAdmin ? { actionHref: '#/dashboard/subscription', actionLabel: 'Update payment method' } : {}), }; } if (dunning.stage === 'admin-only') { // Nobody but an admin. A receptionist can do nothing about a card, and a banner they cannot // act on is one they learn to ignore - which is precisely the banner we need them to READ at the // next stage. Spending their attention here is what makes the real warning worthless. if (!isAdmin) return null; return { severity: 'warning', message: `We couldn’t take payment for your subscription. We’ll try again, but paid add-ons switch off ${daysPhrase(dunning.daysRemaining)} if it isn’t settled.`, // Dismissible at this stage: it is a reminder to one person who can act on it today. dismissible: true, actionHref: '#/dashboard/subscription', actionLabel: 'Update payment method', }; } // all-staff - the card has failed and a retry has failed too. return { severity: 'error', message: isAdmin ? `Payment has failed more than once. This practice’s paid add-ons switch off ${daysPhrase(dunning.daysRemaining)} — update your payment method to keep them.` // Says what stops, not that a payment failed; whose card it is, a receptionist does not need. : `This practice’s subscription payment has failed and paid add-ons switch off ${daysPhrase(dunning.daysRemaining)}. The core system keeps working. Please tell an administrator.`, // NOT dismissible. Everyone is being told because features they use are about to stop; // a warning you can wave away is one that is gone by the morning it matters. dismissible: false, ...(isAdmin ? { actionHref: '#/dashboard/subscription', actionLabel: 'Update payment method' } : {}), }; }