/** * Pure helpers for pairing-lifecycle status transitions. Extracted so * the rules are unit-testable without spinning up Baileys / Postgres. * * Key invariant the tests guard: * - A failed or abandoned pair MUST NOT leave the row stuck in * `pending`. It transitions to `unpaired` so the operator can see * the account on the list with a Re-pair affordance. * - Successful pairing transitions to `connected` (set by the * session-manager on the `open` event — not this helper's job). * - Auto-reconnect for transient drops only applies to accounts * that have been linked at least once (`lastConnectedAt` set). */ export type AccountStatus = | "pending" | "unpaired" | "connected" | "disconnected" | "logged_out" | "banned"; export interface PairCloseInput { /** Status of the account row at the moment the close event fires. */ current: AccountStatus; /** Did Baileys signal a logged-out close (vs an ephemeral close)? */ loggedOut: boolean; } export interface StatusUpdate { next: AccountStatus; /** Wipe the cached QR PNG when the pair window closes. */ clearQrPng: boolean; } /** * Decide the status transition when the Baileys session closes during * a pairing attempt (i.e. before the user has scanned the QR). * * - logged_out close → terminal: `logged_out`. * - ephemeral close (refs exhausted, network blip, etc.) → park as * `unpaired` so the row stays visible and the user can retry. */ export function decideOnPairClose({ current, loggedOut }: PairCloseInput): StatusUpdate { if (loggedOut) { return { next: "logged_out", clearQrPng: true }; } // Whatever transient state we were in (most often `pending`), park // the row as `unpaired` — anything else hides it from the operator. return { next: "unpaired", clearQrPng: true }; } /** Whether the session-manager should auto-reconnect after a non-loggedOut close. */ export function shouldAutoReconnect(args: { loggedOut: boolean; /** True if the account row has `last_connected_at` set (has been linked before). */ hasEverConnected: boolean; }): boolean { if (args.loggedOut) return false; return args.hasEverConnected; } /** Decide what happens when the 5-min pair-window timeout fires. */ export function decideOnPairTimeout({ current }: { current: AccountStatus }): StatusUpdate | null { // Only the still-pending rows need cleanup. Anything else has already // moved on (connected, unpaired by an earlier close, etc.). if (current !== "pending") return null; return { next: "unpaired", clearQrPng: true }; }