import { HttpError } from '../../../services/HttpService'; // // S4d Phase 2 slice 2 - why a send or a schedule was refused. // // THE SERVER'S MESSAGES ALREADY NAME THEIR REMEDY, so this module RELAYS them rather than // carrying a second copy that drifts. What it adds is the two BEHAVIOURAL bits a message cannot // express: may the client retry, and is the remedy "narrow the segment"? // // acknowledgedAudience.ts::AcknowledgedAudienceRefusal is the source of this vocabulary. // export type RefusalReason = | 'not-previewed' | 'preview-expired' | 'preview-incomplete' | 'preview-corrupt' | 'preview-truncated' | 'not-acknowledged' | 'audience-diverged' | 'unknown'; export interface CampaignRefusal { reason: RefusalReason; /** The server's own sentence. Never rewritten here. */ message: string; /** The remedy is to make the segment smaller. Re-counting unchanged cannot help. */ mustNarrow: boolean; /** Re-counting and trying again is a sensible next action. */ retryable: boolean; } const KNOWN: Record, { mustNarrow: boolean; retryable: boolean }> = { 'not-previewed': { mustNarrow: false, retryable: true }, 'preview-expired': { mustNarrow: false, retryable: true }, 'preview-incomplete': { mustNarrow: false, retryable: true }, 'preview-corrupt': { mustNarrow: false, retryable: true }, // NOT retryable. Re-running the identical segment truncates identically - a retry loop here // cannot terminate, which is why the server gives this its own reason instead of "expired". 'preview-truncated': { mustNarrow: true, retryable: false }, 'not-acknowledged': { mustNarrow: false, retryable: true }, 'audience-diverged': { mustNarrow: false, retryable: true }, }; const GENERIC = 'We could not send this campaign. Please count the audience again and retry.'; const readBody = (error: unknown): { reason?: unknown; message?: unknown } => { if (!(error instanceof HttpError)) return {}; const body = error.body; if (typeof body !== 'object' || body === null) return {}; return body as { reason?: unknown; message?: unknown }; }; export function readRefusal(error: unknown): CampaignRefusal { const body = readBody(error); const serverMessage = typeof body.message === 'string' && body.message !== '' ? body.message : (error instanceof HttpError && error.message !== '' ? error.message : GENERIC); const rawReason = typeof body.reason === 'string' ? body.reason : ''; if (rawReason !== '' && rawReason in KNOWN) { const reason = rawReason as Exclude; return { reason, message: serverMessage, ...KNOWN[reason] }; } // DEFAULT IS "we do not know why", never a guessed reason. Naming a cause we cannot establish // is what fixed on the unlock page and fixed on the receptionist search. return { reason: 'unknown', message: serverMessage, mustNarrow: false, retryable: true }; }