/** * 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; /** Was it the post-pair "restart required" close (status 515)? */ restartRequired?: boolean; } export type StatusUpdate = { next: AccountStatus; /** Wipe the cached QR PNG when the pair window closes. */ clearQrPng: boolean; } | null; /** * Decide the status transition when the Baileys session closes during * a pairing attempt. * * - logged_out close → terminal: `logged_out`. * - restart-required close → null (this is a SUCCESS signal that triggers * a reconnect; the row stays in its current state until `open` fires). * - 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, restartRequired }: PairCloseInput): StatusUpdate { if (loggedOut) { return { next: "logged_out", clearQrPng: true }; } if (restartRequired) { // Post-pair-success reconnect — the next `open` event finishes the // job. Don't touch DB state and don't tear the listener down. return null; } // 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; restartRequired?: boolean; /** True if the account row has `last_connected_at` set (has been linked before). */ hasEverConnected: boolean; }): boolean { if (args.loggedOut) return false; // Status 515 is the post-pair-success reconnect — always do it, // regardless of whether the account has ever connected before. if (args.restartRequired) return true; 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 }; }