/** * Soft-hold service for the booking-journey wizard. * * Per `docs/architecture/booking-journey-architecture.md` §5.7 + * §6. Decrements `availability_slots.remainingPax` while a hold is * live, restores it on release. The booking-journey reaper job * calls `releaseExpiredHolds` to clean up abandoned drafts. * * Atomicity: every operation runs inside a transaction that * locks the slot row, so concurrent placeHold attempts can't * over-allocate. */ import { type AvailabilityHold } from "@voyant-travel/availability/schema"; import type { PostgresJsDatabase } from "drizzle-orm/postgres-js"; export interface PlaceAvailabilityHoldInput { draftId: string; productId: string; slotId: string; paxCount: number; ttlMs: number; /** Caller-supplied hold token; defaults to a fresh typeid. */ holdToken?: string; } export type PlaceAvailabilityHoldOutcome = { status: "ok"; hold: AvailabilityHold; } | { status: "slot_not_found"; } | { status: "slot_unlimited"; holdToken: string; expiresAt: Date; } | { status: "insufficient_capacity"; remaining: number; needed: number; }; /** * Place a soft hold on a slot. When the slot is `unlimited`, no * capacity decrement is needed but a hold row is still written for * audit + later release. The bridge returns a token the caller * stores on the draft (typically as `draft.id` for journey * convenience). */ export declare function placeAvailabilityHold(db: PostgresJsDatabase, input: PlaceAvailabilityHoldInput): Promise; export interface ExtendAvailabilityHoldInput { holdToken: string; ttlMs: number; } export type ExtendAvailabilityHoldOutcome = { status: "ok"; expiresAt: Date; } | { status: "hold_not_found"; } | { status: "already_released"; }; export declare function extendAvailabilityHold(db: PostgresJsDatabase, input: ExtendAvailabilityHoldInput): Promise; /** * Release a hold by token. Restores capacity. Idempotent — calling * twice is a no-op on the second call. */ export declare function releaseAvailabilityHold(db: PostgresJsDatabase, holdToken: string): Promise; /** * Reaper helper — releases all holds past `expires_at` that haven't * already been released. Returns the count of newly-released holds. * * Locks slot-before-holds, matching `placeAvailabilityHold`. Taking the hold * lock first (the obvious shape, since the holds are what the sweep selects) * inverts that order, and a checkout placing a hold on the same slot while this * runs deadlocks — Postgres then aborts one of them, either the sweep or a * customer's checkout. The slot ids are therefore collected with an unlocked * read, and each slot's holds are re-selected under its slot lock. */ export declare function releaseExpiredHolds(db: PostgresJsDatabase, cutoff?: Date): Promise; /** * Looks up the hold(s) for a draft id. Multiple holds per draft * are possible (e.g. a multi-day product touching several slots); * the journey reaper releases them all at once. */ export declare function findHoldsByDraft(db: PostgresJsDatabase, draftId: string): Promise;