import type { Address, Organization } from 'fhir/r4'; // --------------------------------------------------------------------------------------------- // - which physical postal address a marketing email will carry (CAN-SPAM). // // THIS IS A SECOND IMPLEMENTATION OF A SERVER RULE, AND THAT IS A KNOWN COST. The authoritative // copy is `utils/campaign/sendingIdentity.ts` in akashic-infrastructure: it decides what actually // goes into a message, and it is what refuses a send. After every save the UI renders the // `effectiveAddress` the SERVER returned, not this. // // This exists for the one question the client cannot ask the server - "what will be used, BEFORE // anything is saved?" - so the settings section can show a practice where its address is coming // from on first render. Keep the two in step; the mirror is pinned by tests on both sides so a // divergence surfaces as a red test rather than as a campaign that silently refuses to send while // the UI reports everything is fine. // --------------------------------------------------------------------------------------------- export const CAMPAIGN_MAILING_ADDRESS_EXTENSION_URL = 'https://akashichealthrecords.com/fhir/StructureDefinition/organization-campaign-mailing-address'; /** Where the address being used came from - drives what the section tells the practice. */ export type CampaignMailingAddressSource = 'override' | 'primary' | 'none'; export interface ResolvedCampaignMailingAddress { address: Address | null; source: CampaignMailingAddressSource; } /** * NOT A TRUTHINESS CHECK, mirroring the server exactly. An `Address` object with nothing in it is * truthy, so `if (org.address?.[0])` would tell a practice it is ready to send while the server * refuses - the confusing half-state this card exists to remove. */ const isDeliverable = (address: Address | undefined): address is Address => { if (!address) return false; const filled = (value: string | undefined): boolean => typeof value === 'string' && value.trim().length > 0; const hasLine = (address.line ?? []).some((line) => filled(line)); return hasLine && filled(address.city) && filled(address.state) && filled(address.postalCode); }; export const resolveCampaignMailingAddress = ( organization: Organization | undefined | null, ): ResolvedCampaignMailingAddress => { if (!organization) return { address: null, source: 'none' }; const override = organization.extension ?.find((extension) => extension.url === CAMPAIGN_MAILING_ADDRESS_EXTENSION_URL) ?.valueAddress; if (isDeliverable(override)) return { address: override, source: 'override' }; // address[0] SPECIFICALLY - index 0 is the primary organization address by the product's own // convention (the Locations page writes `[primary,...rest]`). Scanning for "the first complete // one" would silently point at a satellite clinic. const primary = organization.address?.[0]; if (isDeliverable(primary)) return { address: primary, source: 'primary' }; return { address: null, source: 'none' }; }; // phase 2 - the sending identity, read for display only // // The SERVER is authoritative for whether a practice may send: `resolveCampaignSender` refuses // unless the domain is verified. This reader exists so the settings screen can show the current // state on first render without a round trip. Never gate anything on it. export const CAMPAIGN_SENDING_IDENTITY_EXTENSION_URL = 'https://akashichealthrecords.com/fhir/StructureDefinition/organization-campaign-sending-identity'; export type SendingIdentityStatus = 'pending' | 'verified' | 'failed'; export interface CampaignSendingIdentity { domain: string; status: SendingIdentityStatus; dkimTokens: string[]; mailFromDomain: string | null; /** * The MAIL FROM verifies SEPARATELY from DKIM and BOTH are required to send. Carried * separately so the screen can say which half is outstanding - "pending" alone sends a practice * back to re-read three CNAMEs that are already correct while the MX is the thing missing. */ mailFromStatus: SendingIdentityStatus | null; verifiedAt: string | null; } export interface DnsRecord { type: 'CNAME' | 'MX' | 'TXT'; host: string; value: string } export interface CampaignSendingIdentitySettings { identity: CampaignSendingIdentity | null; dnsRecords: DnsRecord[]; } export type CampaignSetupStepState = 'not-started' | 'issued' | 'unknown' | 'confirmed'; /** A new or changed sending domain on its way to replacing the selection. Its records come only from confirmed steps. */ export interface CampaignSendingSetup { attemptId: string; domain: string; phase: 'admitted' | 'steps-running' | 'provider-ready'; /** Why it cannot continue on its own: provider-mismatch, provider-unconfirmed, uncertain-call, configuration, superseded. */ held?: string; steps: Record<'identity' | 'dkim' | 'mailFrom', CampaignSetupStepState>; dnsRecords: DnsRecord[]; } /** The sending identity as the server stores it: each owner with its own records, and the revision every action names. */ export interface CampaignSendingState { revision: number; selected: { identity: CampaignSendingIdentity; selectionId: string; dnsRecords: DnsRecord[] } | null; setup: CampaignSendingSetup | null; } export interface CampaignSendingAnswer { organization?: Organization; sending: CampaignSendingState; /** Present when SES could not be read; the stored status stands. */ provider?: 'unavailable'; } export type CampaignSendingRequest = | { domain: string; basisRevision: number; replaceAttemptId?: string } | { clear: 'selected'; selectionId: string; basisRevision: number } | { clear: 'setup'; attemptId: string; basisRevision: number }; export type CampaignSendingCheck = { selectionId: string } | { attemptId: string }; export const resolveCampaignSendingIdentity = ( organization: Organization | undefined | null, ): CampaignSendingIdentity | null => { const container = organization?.extension ?.find((extension) => extension.url === CAMPAIGN_SENDING_IDENTITY_EXTENSION_URL); if (!container) return null; const sub = (url: string) => container.extension?.find((e) => e.url === url)?.valueString; const domain = sub('domain'); const status = sub('status'); // Mirrors the server: a half-written record reads as ABSENT, never as a partial, so the UI // cannot tell a practice it is ready when the sender will refuse. if (!domain?.trim() || !status || !['pending', 'verified', 'failed'].includes(status)) return null; return { domain: domain.trim(), status: status as SendingIdentityStatus, dkimTokens: (container.extension ?? []) .filter((e) => e.url === 'dkimToken') .map((e) => e.valueString) .filter((v): v is string => !!v?.trim()), mailFromDomain: sub('mailFromDomain')?.trim() || null, mailFromStatus: (['pending', 'verified', 'failed'] as const) .find((s) => s === sub('mailFromStatus')) ?? null, verifiedAt: sub('verifiedAt') || null, }; }; /** The same single line the server renders into the message footer. */ export const formatCampaignPostalAddress = (address: Address): string => { const lines = (address.line ?? []).map((line) => line.trim()).filter((line) => line.length > 0); const cityState = [address.city?.trim(), address.state?.trim()].filter(Boolean).join(', '); const tail = [cityState, address.postalCode?.trim()].filter(Boolean).join(' '); return [...lines, tail].filter((part) => part.length > 0).join(', '); };