examples/storefront Public
// Unpaid orders hold stock (checkout takes it), so they cannot wait forever. Every five minutes// the cron trigger cancels orders still pending_payment after PAYMENT_WINDOW_MS, cancelling the// PaymentIntent first so a late payment cannot land on a cancelled order.import type { OrderEvent } from "./events.ts";import { cancelOrder } from "./orders.ts";import type { Payments } from "./payments.ts";export const PAYMENT_WINDOW_MS = 30 * 60 * 1000;const BATCH = 50;export type ExpiryReport = { cancelled: string[]; left_to_webhook: string[] };export async function expireOrders(db: D1Database, events: Queue<OrderEvent>, payments: Payments, now: number): Promise<ExpiryReport> { const { results } = await db .prepare("SELECT id, payment_intent_id FROM orders WHERE status = 'pending_payment' AND created_at < ? ORDER BY created_at LIMIT ?") .bind(now - PAYMENT_WINDOW_MS, BATCH) .all<{ id: string; payment_intent_id: string | null }>(); const report: ExpiryReport = { cancelled: [], left_to_webhook: [] }; for (const o of results) { // The intent succeeded or is processing: the payment won the race, and the webhook will mark // the order paid. Leave it alone. if (o.payment_intent_id && !(await payments.cancelIntent(o.payment_intent_id))) { report.left_to_webhook.push(o.id); continue; } if ((await cancelOrder(db, events, o.id, now)).changed) report.cancelled.push(o.id); } return report;}