import type { AgreementBlock, InlineRun } from "../components/FirstSignInGate/agreementCopy"; // The practice's standing against the Master Subscription Agreement, as the server computes it. The MSA and the BAA // keep separate revisions: an MSA update never makes the BAA unsigned, and no agreement expires with time. export type MsaEdition = { version: string; editionHash: string }; export type MsaStanding = | { state: "not-published" } | { state: "satisfied"; current: MsaEdition } | { state: "grace"; current: MsaEdition; deadline: string } | { state: "outstanding"; current: MsaEdition } | { state: "unavailable" }; export type MsaOrganizationFacts = { orgName: string; orgAddress: string; orgPhone: string }; export type MsaPreview = { version: string; editionHash: string; title: string; blocks: AgreementBlock[]; facts: MsaOrganizationFacts; signDate: string; factsDigest: string; }; export type MsaSummary = { standing: MsaStanding; selected: { version: string; acceptedAt: string; operationId: string } | null; preview: MsaPreview | null; }; export type MsaExecutionFacts = MsaOrganizationFacts & { signerName: string; signerTitle: string; signDate: string }; const TOKENS = ["ORG_NAME", "SIGN_DATE", "SIGNER_NAME", "SIGNER_TITLE", "ORG_ADDRESS", "ORG_PHONE"] as const; type Token = (typeof TOKENS)[number]; const PLACEHOLDER = new RegExp("\{\{(" + TOKENS.join("|") + ")\}\}", "g"); const valuesOf = (facts: MsaExecutionFacts): Record => ({ ORG_NAME: facts.orgName, SIGN_DATE: facts.signDate, SIGNER_NAME: facts.signerName, SIGNER_TITLE: facts.signerTitle, ORG_ADDRESS: facts.orgAddress, ORG_PHONE: facts.orgPhone, }); const fill = (text: string, values: Record) => text.replace(PLACEHOLDER, (_match, token: Token) => values[token]); const fillRuns = (runs: InlineRun[] | undefined, text: string, values: Record): InlineRun[] | undefined => { if (!runs) return undefined; const filled = runs.map((run) => ({ ...run, text: fill(run.text, values) })); return filled.map((run) => run.text).join("") === text ? filled : undefined; }; /** The edition with its six placeholders filled, exactly as the server fills the executed copy. */ export function resolveMsaBlocks(blocks: readonly AgreementBlock[], facts: MsaExecutionFacts): AgreementBlock[] { const values = valuesOf(facts); return blocks.map((block): AgreementBlock => { if (block.type === "title") return { type: "title", text: fill(block.text, values) }; if (block.type === "list") { const items = block.items.map((item) => fill(item, values)); const itemRuns = block.itemRuns?.map((runs, n) => fillRuns(runs, items[n], values)); const keptRuns = itemRuns && itemRuns.every((runs) => runs !== undefined) ? (itemRuns as InlineRun[][]) : undefined; return { type: "list", items, ...(keptRuns ? { itemRuns: keptRuns } : {}), ...(block.ordered ? { ordered: true as const } : {}) }; } const text = fill(block.text, values); const runs = fillRuns(block.runs, text, values); return { type: block.type, text, ...(runs ? { runs } : {}) }; }); } /** The execution lines printed under the agreement. */ export const msaClosingLines = (facts: MsaExecutionFacts): string[] => [ "I have read and agree to this Master Subscription Agreement on behalf of the Customer named below.", "Customer: " + facts.orgName, "Address: " + facts.orgAddress, "Phone: " + facts.orgPhone, "Accepted by: " + [facts.signerName, facts.signerTitle].filter((part) => part.trim()).join(", "), "Date: " + facts.signDate, ]; /** Setup counts the MSA done when nothing is owed now; a grace period is an update to answer, not unfinished setup. * An answer without an MSA part comes from a server that does not know one, and asks nothing. */ export const msaSetupFact = (summary: MsaSummary | undefined): boolean | "none" | undefined => { if (!summary) return "none"; const state = summary.standing.state; if (state === "unavailable") return undefined; if (state === "not-published") return "none"; return state !== "outstanding"; }; /** * What the owner's practice entry does: open, a notice with the date an update must be accepted by, restricted until * acceptance, or a retry notice when standing could not be read. Only the owner's entry is ever restricted. */ export type MsaEntry = { kind: "open" } | { kind: "notice"; deadline: string } | { kind: "restricted" } | { kind: "retry" }; export const msaEntry = (summary: MsaSummary | undefined, viewerIsOwner: boolean | undefined): MsaEntry => { if (viewerIsOwner !== true || !summary) return { kind: "open" }; const { standing } = summary; if (standing.state === "unavailable") return { kind: "retry" }; if (standing.state === "grace") return { kind: "notice", deadline: standing.deadline }; // A practice that has never accepted meets the MSA in setup, before its first BAA; only an update is enforced here. if (standing.state === "outstanding" && summary.selected) return { kind: "restricted" }; return { kind: "open" }; }; export const MSA_ROUTE = "/dashboard/practice-agreement";