// - what to tell someone the BAA route just refused. // // THESE ARE NOT FAILURES TO RETRY, which is what makes a generic message actively harmful here. The // modal's old catch said *"Failed to sign BAA. Please try again."* for everything - and against the new // refusals that sends an admin round a loop that cannot end: trying again will refuse again, forever, // because the problem is who they are or what the practice has not done yet. // // MATCHED ON THE SERVER'S `reason` CODE, NEVER ON ITS PROSE. The message copy is allowed to change; a // client that matched on sentences would break silently the first time it did, and "silently" is the whole // problem with this class of bug. // // AND THE SERVER'S OWN MESSAGE IS PREFERRED WHEN IT SENDS ONE. It is written to name the next action - // *"assign an owner first, then come back to this step"* - so overriding it with a local paraphrase would // throw away the more useful sentence. These are the fallbacks for when it does not. import { relayServerFailure } from '../../../../utils/fetchFailureMessage'; // The form re-reads and shows the current text itself, so this replaces the server's "Reload" sentence. const AGREEMENT_CHANGED = 'The agreement changed while this page was open. Read the current version below, then confirm and sign again.'; type RefusalReason = | 'no-owner' | 'not-the-owner' | 'attestation-missing' | 'attestation-stale' | 'attestation-not-signer' | 'missing-version' | 'stale-version' | 'msa-required' | 'msa-unavailable'; const FALLBACK: Record = { 'no-owner': 'This practice has not said who owns it yet, and only the owner can sign this agreement. ' + 'Set an owner on the Organization page, then come back here.', 'not-the-owner': 'Only the person designated as this practice\'s owner can sign this agreement. ' + 'If that should be you, ownership has to be transferred to you first.', 'attestation-missing': 'You have to confirm you are authorized to sign for this organization before signing.', 'attestation-stale': 'The wording has changed since this page was opened. Reload and read it again before signing.', 'attestation-not-signer': 'The confirmation has to be made by the person signing.', 'missing-version': AGREEMENT_CHANGED, 'stale-version': AGREEMENT_CHANGED, 'msa-required': 'The practice owner accepts the Master Subscription Agreement before the first BAA. Accept it above, then sign.', 'msa-unavailable': 'The Master Subscription Agreement could not be checked just now. Try again in a moment.', }; /** The first BAA waits for the owner's MSA acceptance; the screen then shows the MSA first. */ export const isMsaRequired = (error: unknown): boolean => reasonOf(error) === 'msa-required'; /** True when the agreement in force is not the one this page showed; the form then shows the current one. */ export const isAgreementChanged = (error: unknown): boolean => { const reason = reasonOf(error); return reason === 'missing-version' || reason === 'stale-version'; }; /** * READ STRUCTURALLY, NOT VIA `instanceof HttpError`, and that is deliberate rather than lazy. Importing * `HttpService` here would pull `environment.ts` - and therefore `window` - into what is otherwise a pure * function, making this module unusable outside a browser and untestable without a DOM. What this actually * needs is one field, so it asks for one field. */ const bodyOf = (error: unknown): { reason?: string; message?: string } | undefined => { const body = (error as { body?: unknown } | null)?.body; return body && typeof body === 'object' ? (body as { reason?: string; message?: string }) : undefined; }; const reasonOf = (error: unknown): RefusalReason | undefined => { const reason = bodyOf(error)?.reason; return reason && reason in FALLBACK ? (reason as RefusalReason) : undefined; }; /** * True when the practice has no designated owner - the one refusal that is about the ORGANIZATION rather * than about the caller, and the one a screen may want to act on by pointing somewhere else. * * Keyed on the code, so it stays true if the sentence is reworded. */ export const isMissingOwner = (error: unknown): boolean => reasonOf(error) === 'no-owner'; export const baaSignErrorMessage = (error: unknown): string => { const reason = reasonOf(error); if (reason === 'missing-version' || reason === 'stale-version') return FALLBACK[reason]; if (reason) { // Prefer the server's sentence: it is the one written to name the next action. return bodyOf(error)?.message?.trim() || FALLBACK[reason]; } // Everything else keeps the old behaviour - a genuine failure IS worth retrying, and this must not // start telling people a network blip is a permissions problem. return relayServerFailure(error, 'Failed to sign BAA. Please try again.'); };