import type { OwnerState } from '../../services/OrganizationOwnerService.tsx'; // - what a practice is told about who their organization belongs to. // // THE STATE THIS COPY EXISTS FOR IS "nobody", and the support case it prevents begins with nobody // having noticed. An agency installs the plugin, becomes the founding admin, configures everything, // pays, and vanishes - and the practice discovers they have no authority over their own account at the // worst possible moment. A notice they see every day is the cheapest possible fix. // // AND A PENDING NOMINATION IS NOT "handled". It is the state that LOOKS handled, so it gets its own // wording rather than being folded in with an accepted owner. /** * `info` is NOT a quieter warning - it is a different claim. `warning` says something is wrong; * `info` says something is true and this reader is the one it concerns. Ownership hand-off uses it * because a practice mid-transfer has an owner throughout, and borrowing the alarm tone for a healthy * state is how the real "this practice has no owner" warning stops being read. */ export type OwnerNoticeTone = 'warning' | 'action' | 'info' | 'none'; export interface OwnerNotice { tone: OwnerNoticeTone; title: string; body: string; /** The label on the button, when there is something this person can do about it. */ action?: string; } export interface OwnerNoticeContext { /** Whether the person reading this is the one who has been nominated. */ viewerIsNominee: boolean; /** Whether the person reading this can nominate - the notice must not offer what they cannot do. */ viewerCanNominate: boolean; /** Whether the reader currently holds the designation. The only person offered a transfer. */ viewerIsOwner?: boolean; /** Whether the reader is the person a pending transfer is being handed to. */ viewerIsPendingTransferee?: boolean; } export function ownerNotice(state: OwnerState | undefined, context: OwnerNoticeContext): OwnerNotice { // THE NO-OWNER WARNING COMES FIRST AND IS NOT DISPLACEABLE. Everything below it describes a // healthy practice; if this one is due, it is the serious one and nothing may push it aside. if (state && state.needsOwner) { return ownerlessNotice(state, context); } if (!state) { return { tone: 'none', title: '', body: '' }; } // A hand-off in flight // Only the two people it concerns hear about it. A receptionist learning that ownership is // changing has nothing to do with the information only exceptions earn a word. if (state.pendingTransferToPractitionerId) { if (context.viewerIsPendingTransferee) { return { tone: 'action', title: 'You have been asked to take over as owner of this practice', body: 'The owner is the one person nobody else — including an outside platform manager — can ' + 'remove from this account. Nothing changes until you accept it yourself.', action: 'Review and accept', }; } if (context.viewerIsOwner) { // The failure this prevents: an Owner who started a hand-off, heard nothing more, and // believes they have handed over. They have not - and they are still the one person who // cannot be removed from this account. return { tone: 'info', title: 'You are handing this practice over', body: 'You are still the owner until they accept it themselves. Until then nothing has ' + 'moved.', action: 'Change who you are handing it to', }; } return { tone: 'none', title: '', body: '' }; } // Settled, and the reader is the owner // THE ONLY PLACE THE DESIGNATION IS VISIBLE AT ALL, and the only door to a transfer. Without it // the exit exists on the server with nothing calling it, and an owner who leaves the practice has // no way to hand over. if (context.viewerIsOwner) { return { tone: 'info', title: 'You own this practice', body: 'You are the one person nobody else — including an outside platform manager — can remove from ' + 'this account.', action: 'Hand ownership over', }; } return { tone: 'none', title: '', body: '' }; } function ownerlessNotice(state: OwnerState, context: OwnerNoticeContext): OwnerNotice { // THE NOMINEE'S OWN VIEW COMES FIRST, because for them this is not a warning - it is a task, and // only they can complete it. Showing them the generic "this practice has no owner" would tell the // one person who can fix it that somebody ought to. if (state.status === 'nominated' && context.viewerIsNominee) { return { tone: 'action', title: 'You have been named the owner of this practice', body: 'The owner is the one person nobody else can remove from this account. ' + 'Nothing takes effect until you accept it yourself: with the Accept button in the email ' + 'we sent you, or here.', action: 'Review and accept', }; } if (state.status === 'nominated') { return { tone: 'warning', title: 'This practice still has no owner', body: 'Someone has been nominated, but until they accept it themselves this practice has ' + 'nobody who cannot be removed from its account.', ...(context.viewerCanNominate ? { action: 'Change who is nominated' } : {}), }; } if (!context.viewerCanNominate) { return { tone: 'warning', title: 'Confirm the practice owner', body: `Confirm ownership with a code sent to the practice's own contact details. ` + `If you are a contractor helping a client, ask the client's authorized representative to complete this step.`, action: 'Get a verification code', }; } return { tone: 'warning', title: 'Confirm the practice owner', body: `Choose the practice's authorized representative as its account owner. If you are a ` + `contractor helping a client, choose the client's representative.`, action: 'Choose an owner', }; } /** * What the nominator is told about whether the nominee actually heard. * * Silence is not success. A nomination nobody was told about is exactly the pending state that looks * handled and is not, so an unreachable nominee produces an instruction rather than nothing. */ export function nomineeNotifiedMessage( nomineeNotified: boolean | undefined, reason?: 'no-email' | 'send-failed', ): string | undefined { if (nomineeNotified === undefined) return undefined; if (nomineeNotified) return 'We have emailed them. They press Accept in that email to take it on.'; const why = reason === 'no-email' ? 'there is no email address on their record or their account' : 'the email could not be sent just now'; return 'We could not email them — ' + why + '. ' + 'Please tell them in person: they need to sign in and accept it themselves.'; }