// ── 3DS finalize HTTP client ──────────────────────────────────── // // POSTs the 3DS flow outcome back to the host's finalize endpoint // (e.g., WordPress: `/wp-json/weeconnectpay/v1/woocommerce/3ds/finalize`; // dashboard: `/api/v1/clover/charges/3ds/finalize`). // // Headers are pluggable so each host can inject nonces / auth tokens. // Response parsing handles three outcomes: terminal success, escalation // (method→challenge or vice versa), or terminal failure. import type { HeadersProvider, ThreeDsInterim } from './types'; export interface FinalizeRequest { readonly url: string; readonly headers?: HeadersProvider; readonly body: Record; } export interface FinalizeResponseEnvelope { readonly ok: boolean; readonly data: FinalizeResponseData | null; } export interface FinalizeResponseData { readonly success?: boolean; readonly redirect?: string; readonly message?: string; /** When Clover escalates the flow, the host returns the next interim payload here. */ readonly weeconnectpay_threeds_interim?: ThreeDsInterimRaw; } /** Wire shape from Clover / the host before normalization to ThreeDsInterim. */ export interface ThreeDsInterimRaw { readonly state: 'method_required' | 'challenge_required'; readonly charge_id: string | null; readonly threeds: { readonly acs_url?: string; readonly acs_transaction_id?: string; readonly method_url?: string; readonly method_notification_url?: string; readonly threeds_server_transaction_id?: string; readonly threeds_protocol_version?: string; /** Older Clover docs used `message_version` — both supported. */ readonly message_version?: string; }; } export async function postFinalize(request: FinalizeRequest): Promise { const headers: Record = { 'Content-Type': 'application/json', Accept: 'application/json', }; if (request.headers) { Object.assign(headers, await request.headers()); } let response: Response; try { response = await fetch(request.url, { method: 'POST', headers, body: JSON.stringify(request.body), }); } catch (error) { throw new Error(`Network error during 3DS finalize: ${(error as Error).message}`); } let data: FinalizeResponseData | null = null; try { data = (await response.json()) as FinalizeResponseData; } catch { data = null; } return { ok: response.ok, data }; } /** * Normalize the wire shape into the SDK's `ThreeDsInterim`. Returns null when * required fields (charge_id) are missing — the caller should treat as a * failure rather than entering a malformed state. */ export function parseInterim(raw: ThreeDsInterimRaw): ThreeDsInterim | null { if (!raw.charge_id) { return null; } return { state: raw.state, chargeId: raw.charge_id, fields: { acsUrl: raw.threeds.acs_url, acsTransactionId: raw.threeds.acs_transaction_id, methodUrl: raw.threeds.method_url, methodNotificationUrl: raw.threeds.method_notification_url, threeDsServerTransactionId: raw.threeds.threeds_server_transaction_id, protocolVersion: raw.threeds.threeds_protocol_version ?? raw.threeds.message_version, }, }; }