// server/supabase/functions/send-push/dispatch-response.ts // Pure presenter: turns a processed push dispatch into the HTTP body/status. // // Provider transport codes (webpush_send_timeout, fcm_send_failed, …) and // their distinct HTTP statuses describe what happened on the function's own // network. Echoing them to an end-user caller turns the dispatch endpoint into // a reachability oracle, so user callers only ever see one generic failure. // Trusted system callers (service role, drain worker) keep the detailed code // for operations. export type DispatchAudience = 'user' | 'system' export interface DispatchOutcome { status: 'pending' | 'processing' | 'succeeded' | 'partial' | 'failed' complete: boolean attempted: number delivered: number stale: number retryableFailed: number permanentFailed: number nextRetryAt: string | null transientError: string | null transientStatus: number | null } export interface DispatchResponse { body: Record status: number } export const DELIVERY_FAILURE_CODE = 'push_delivery_failed' const DELIVERY_FAILURE_STATUS = 502 export function presentDispatchOutcome( audience: DispatchAudience, eventType: string, resourceId: string, outcome: DispatchOutcome, ): DispatchResponse { const body: Record = { event_type: eventType, resource_id: resourceId, duplicate: false, status: outcome.status, complete: outcome.complete, attempted: outcome.attempted, sent: outcome.delivered, stale_removed: outcome.stale, retryable_failed: outcome.retryableFailed, permanent_failed: outcome.permanentFailed, next_retry_at: outcome.nextRetryAt, } const failed = outcome.status === 'partial' || outcome.status === 'failed' if (outcome.transientError) { if (audience === 'user') { return { body: { error: DELIVERY_FAILURE_CODE, ...body }, status: DELIVERY_FAILURE_STATUS, } } return { body: { error: outcome.transientError, ...body }, status: outcome.transientStatus ?? DELIVERY_FAILURE_STATUS, } } if (failed) { return { body: { error: DELIVERY_FAILURE_CODE, ...body }, status: DELIVERY_FAILURE_STATUS, } } return { body, status: outcome.complete ? 200 : 202 } }