Skip to content

feat: expire stale pending booking-intents - #736

Open
islarmeeyah-cyber wants to merge 1 commit into
Chronopay-Org:mainfrom
islarmeeyah-cyber:feat/expire-pending-booking-intents
Open

feat: expire stale pending booking-intents#736
islarmeeyah-cyber wants to merge 1 commit into
Chronopay-Org:mainfrom
islarmeeyah-cyber:feat/expire-pending-booking-intents

Conversation

@islarmeeyah-cyber

Copy link
Copy Markdown

Overview

This PR adds a scheduled background worker that finds booking intents stuck in pending (awaiting payment/confirmation) for longer than the configured TTL (default 30 minutes), cancels them, releases the reserved slot inventory back to the marketplace, and emits a durable booking_intent_expired event.

Related Issue

Closes #588

Changes

⏰ Expire Pending Booking-Intents Worker

  • [ADD] src/scheduler/expireBookingIntents.ts

  • Scans for stale pending intents (oldest first) every 60s by default.

  • Claims rows with FOR UPDATE SKIP LOCKED so multiple worker instances never double-process an intent.

  • Re-verifies each intent is still pending immediately before expiring, preventing double-cancels (e.g. buyer confirmed/completed in the meantime).

  • Trips a safety brake and skips the sweep when candidate count exceeds the threshold (default 10,000).

  • Marks the intent expired, releases the slot inventory, and emits the booking_intent_expired event.

  • Configurable via env vars: EXPIRE_BOOKING_INTENTS_TTL_MS, _BATCH_SIZE, _SAFETY_THRESHOLD, _INTERVAL_MS; disable with EXPIRE_BOOKING_INTENTS_DISABLED=true.

  • [ADD] src/scheduler/__tests__/expireBookingIntents.test.ts

  • 24 tests covering edge cases: two workers running (no double-cancel), expiry exactly at the 30-minute boundary, slot already completed, safety brake, event-emission failures, async repositories, env config parsing, and graceful shutdown.

  • [MODIFY] Repository layer

  • booking-intent-repository.ts — added findStalePendingIntents to the interface + in-memory implementation.

  • pg-booking-intent-repository.ts — PostgreSQL implementation using FOR UPDATE SKIP LOCKED for concurrent-safety.

  • [MODIFY] src/metrics.ts

  • Added booking_intents_expired_total Prometheus counter + safety-brake counter.

  • [MODIFY] src/index.ts

  • Registers the worker with transactional-outbox event emission (booking_intent_expired) and a shutdown hook.

  • [MODIFY] src/modules/booking-intents/__tests__/pg-booking-intent-repository.test.ts

  • Coverage for the new findStalePendingIntents query.

Verification Results

node --experimental-vm-modules node_modules/jest-cli/bin/jest.js src/scheduler/__tests__/expireBookingIntents.test.ts --runInBand
✅ 24/24 passed

All affected suites (worker, PG repo, service, lifecycle, scheduling):
✅ 124/124 passed

Worker coverage:
✅ 98.5% statements, 94.44% branches, 100% functions
Acceptance Criteria Status
Two workers running (no double-cancel) ✅ Covered by re-verification guard + FOR UPDATE SKIP LOCKED
Expiry exactly at 30-min boundary ✅ Tested (createdAt === cutoff)
Slot already-completed intent skipped ✅ Tested
Releases slot inventory releaseSlot sets slot back to bookable
Emits booking_intent_expired event ✅ Durable via transactional outbox
Prometheus metric booking_intents_expired_total

Notes

  • The issue suggested src/workers/expireBookingIntents.ts and src/scheduler/registry.ts; this repo keeps all workers under src/scheduler/ and registers them in src/index.ts, so the worker follows the existing codebase conventions (same pattern as holdAutoRefundWorker).
  • The domain equivalent of cancelled_expired is the expired status in the booking_intent_status enum (migration 004).

Adds a scheduled worker that sweeps booking intents stuck in `pending`
beyond a 30-minute TTL, expires them, releases the reserved slot
inventory, and emits a durable `booking_intent_expired` outbox event.

- src/scheduler/expireBookingIntents.ts: new background worker with
  safety-brake threshold, re-verification guard against double-cancels,
  and env-configurable TTL/batch/interval
- PgBookingIntentRepository.findStalePendingIntents: claims stale rows
  with FOR UPDATE SKIP LOCKED so concurrent worker instances never
  process the same intent twice
- metrics: booking_intents_expired_total counter
- index.ts: registers the worker with transactional-outbox event emission

Closes Chronopay-Org#588
@drips-wave

drips-wave Bot commented Jul 31, 2026

Copy link
Copy Markdown

@islarmeeyah-cyber Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add background job that scans for booking-intents stuck in "pending_payment" > 30 min and cancels them

1 participant