Skip to content

Save each booking completely or not at all - #1833

Merged
stefan-burke merged 7 commits into
mainfrom
split/atomic-booking-recovery
Jul 15, 2026
Merged

Save each booking completely or not at all#1833
stefan-burke merged 7 commits into
mainfrom
split/atomic-booking-recovery

Conversation

@stefan-burke

@stefan-burke stefan-burke commented Jul 15, 2026

Copy link
Copy Markdown
Member

What changed

  • The app saves each attendee, all ticket lines, ledger entries, modifier use, contact activity, and payment finalization in one database transaction.
  • Free, admin, servicing, and paid bookings use the same atomic write path.
  • If any guarded write fails, the whole booking rolls back.
  • Paid bookings use one stable ticket token before the write starts.
  • If the database reply is lost, the app checks the primary database before deciding whether to recover the booking, refund it, or stop on conflicting state.
  • A redirect that arrives after the atomic commit can replay the finalized attendee and exact ticket token while the first request is still finishing.
  • Errors before the atomic write starts are stored as no-quantity records and refunded instead of leaving the payment session held.

Why

A multi-ticket booking could previously fail after saving only part of its data. A successful database write could also look like a failure when its reply was lost. That could leave a partial booking or refund a payment for a ticket that exists.

This change makes the booking write all-or-nothing and makes uncertain write results safe to recover. It does not add staged checkout tables or seat holds.

Checks

  • deno task test:files test/lib/webhook-price-signature/post-commit-recovery.test.ts
  • deno task precommit
  • Full lint, typecheck, duplicate-code, copy, edge-build, test, and coverage gates passed after merging current main.

Required follow-up

Paid work after the database commit is not yet resumable. If saving answers, logging promo-code use, or sending registration work fails after commit, a later replay can return success without retrying that work.

A full durable outbox or completion checkpoint is required before staged payment runtime work starts. It must track each effect, prevent duplicate delivery, resume unfinished work, and block pruning while work remains. This high-priority prerequisite is recorded in TODO.md. This PR does not add a partial retry.

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f5032444-3519-4c66-a5e5-161bba45a637

📥 Commits

Reviewing files that changed from the base of the PR and between b870080 and 5449c55.

📒 Files selected for processing (2)
  • scripts/mutation/equivalent-mutants.txt
  • test/lib/webhook-price-signature/post-commit-recovery.test.ts

📝 Walkthrough

Walkthrough

This PR refactors attendee creation and payment processing around stable ticket tokens, atomic batch writes, guarded payment finalization, explicit recovery decisions, and centralized completion. It removes post-create completeness checks and adds rollback, replay, concurrency, and finalize-guard coverage.

Changes

Atomic attendee creation and payment-processing refactor

Layer / File(s) Summary
Stable-token batch writes
src/shared/db/attendee-types.ts, src/shared/db/attendees/*, src/shared/seeds.ts
Attendee creation carries stable ticket tokens through prepared batch, ledger, activity, and seed writes, with guarded atomic booking strategies.
Payment completion and recovery
src/features/api/payment-processing/*, src/shared/db/payment-finalize.ts, src/shared/db/processed-payments.ts, src/features/api/webhooks.ts
Payment finalization uses guarded statement batches; successful and uncertain creates route through centralized completion or recovery decisions.
Caller and database helper updates
src/features/admin/attendee-form-routes.ts, src/features/public/ticket-payment.ts, src/shared/db/attendees/servicing.ts, src/shared/db/contact-tokens.ts, src/shared/db/contact-preferences.ts, src/shared/db/modifier-usage.ts, src/shared/db/client.ts
Callers use atomic result status directly, while booking activity, contact visits, modifier stock checks, and SQL condition composition are updated.
Rollback and replay coverage
test/features/*, test/integration/*, test/shared/db/attendees/*, test/lib/webhook-price-signature/*
Tests cover failed writes, capacity rollback, post-commit recovery, refund idempotency, ambiguous errors, stable-token replay, and concurrent webhook/redirect handling.
Test utilities and fixture migration
test/test-utils/*, test/lib/*, test/shared/db/*, test/routes/*
Test setup uses guarded finalization helpers, direct activity and modifier seeding, contact-visit upserts, and shared attendee assertions.
Documentation and mutation metadata
TODO.md, docs/importer-plan.md, scripts/mutation/equivalent-mutants.txt
Documentation and known-equivalent mutant references are updated for the refactored flows.

Estimated code review effort: 4 (Complex) | ~75 minutes

Sequence Diagram(s)

sequenceDiagram
  participant processPaymentSession
  participant createAttendeeForSession
  participant createBookingAtomic
  participant completePaidBooking
  participant recoverOrRefundUnexpectedCreate
  participant Database

  processPaymentSession->>createAttendeeForSession: create with stable ticket token
  createAttendeeForSession->>createBookingAtomic: atomic attendee and booking write
  createBookingAtomic->>Database: guarded batch and payment finalization
  alt successful create
    createAttendeeForSession-->>processPaymentSession: created entries
    processPaymentSession->>completePaidBooking: complete booking
  else unexpected create result
    processPaymentSession->>recoverOrRefundUnexpectedCreate: inspect recovery facts
    recoverOrRefundUnexpectedCreate->>Database: query committed attendee and session state
    alt recover
      recoverOrRefundUnexpectedCreate->>completePaidBooking: complete committed entries
    else refund
      recoverOrRefundUnexpectedCreate->>Database: store unexpected-error refund
    end
  end
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: bookings are now saved atomically or rolled back completely on failure.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch split/atomic-booking-recovery

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 60f18b0aeb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/features/api/payment-processing/index.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/shared/db/attendees/create.ts (1)

43-59: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Tighten AttendeeOrderFields

  • kind should reuse AttendeeKind instead of string.
  • remainingBalance is not written by buildAttendeeInsert and attendee rows no longer store remaining_balance; drop it from this insert-only shape and keep it only where the returned attendee is built.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/shared/db/attendees/create.ts` around lines 43 - 59, Update
AttendeeOrderFields to type kind as AttendeeKind and remove remainingBalance
from the insert-only shape, since buildAttendeeInsert does not write it.
Preserve remainingBalance only in the separate returned-attendee construction
path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/importer-plan.md`:
- Around line 1263-1267: Update the importer plan’s references to the per-source
booking count and visit counts to explicitly use the resolved counter name
admin_booking_count, including the corresponding section around the later
booking-counter instructions. Ensure the steps clearly distinguish incrementing
visits from incrementing admin_booking_count.

In `@src/features/admin/attendee-form-routes.ts`:
- Around line 367-368: Update the failure handling around createAttendeeAtomic
so an encryption_error is not mapped to t("attendee_form.error_capacity").
Preserve the capacity message only for the capacity failure reason, and for
encryption_error propagate the failure or return the established generic save
error; do not suppress unexpected failures with a default.

In `@src/features/api/payment-processing/completion.ts`:
- Around line 23-33: Update the post-commit flow containing saveSessionAnswers,
logPromoCodeModifiers, and logAndNotifyRegistration to persist an idempotent
completion checkpoint or outbox before executing effects, record each effect as
completed, and resume any unfinished effects when the booking is replayed.
Ensure retries skip completed operations while reliably retrying rejected
session-answer, promo-code, or registration-notification work.

In `@src/shared/db/attendees/create.ts`:
- Around line 139-157: Update the caller around buildCapacityCheckedInsert to
pass enc.ticketTokenIndex as its attendeeIdArg parameter, then use the returned
statement.args directly without manually splicing the token index or relying on
statement.args.slice. Preserve the existing extraCondition SQL and argument
handling.

In `@test/lib/webhook-price-signature/post-commit-recovery.test.ts`:
- Around line 312-317: Make the regression test deterministically exercise the
booking-completion race by adding a barrier-controlled stub or hook at the
critical-section boundary, pausing the first request until the other request
reaches that same boundary. Update the test around webhookRequest and
redirectRequest so both requests are released together, then retain the existing
assertions for processed status and redirect response.

In `@test/shared/db/attendees/create-rollback.test.ts`:
- Around line 277-284: Strengthen the rollback test around createBookingAtomic
by fetching the contact record and asserting visits and publicBookingCount are
both 1 immediately after the atomic creation, before calling seedOrderActivity.
Keep the existing post-replay assertions and verify those counts remain
unchanged after replay, ensuring the test proves activity was written by the
original operation.

In `@test/test-utils/atomic-booking.ts`:
- Around line 5-47: Extract the shared trigger lifecycle from
withRejectedBookingWrite and withSkippedBookingWrite into one private helper
that accepts the trigger name, trigger SQL, and callback, creates the trigger,
executes the callback, and always drops the trigger in cleanup. Keep both
exported wrappers and have each delegate to this helper with its existing
trigger behavior.

---

Outside diff comments:
In `@src/shared/db/attendees/create.ts`:
- Around line 43-59: Update AttendeeOrderFields to type kind as AttendeeKind and
remove remainingBalance from the insert-only shape, since buildAttendeeInsert
does not write it. Preserve remainingBalance only in the separate
returned-attendee construction path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 42cd9723-ebce-4484-9273-3cb79e5b5c39

📥 Commits

Reviewing files that changed from the base of the PR and between cb29db7 and 60f18b0.

📒 Files selected for processing (69)
  • TODO.md
  • docs/importer-plan.md
  • scripts/mutation/equivalent-mutants.txt
  • src/docs/database.ts
  • src/features/admin/attendee-form-routes.ts
  • src/features/api/payment-processing/committed-entries.ts
  • src/features/api/payment-processing/completion.ts
  • src/features/api/payment-processing/create.ts
  • src/features/api/payment-processing/index.ts
  • src/features/api/payment-processing/recovery-decision.ts
  • src/features/api/payment-processing/recovery.ts
  • src/features/api/payment-processing/store-refund.ts
  • src/features/api/webhooks.ts
  • src/features/public/ticket-payment.ts
  • src/shared/db/attendee-types.ts
  • src/shared/db/attendees/create-batch.ts
  • src/shared/db/attendees/create.ts
  • src/shared/db/attendees/pii.ts
  • src/shared/db/attendees/servicing.ts
  • src/shared/db/client.ts
  • src/shared/db/contact-preferences.ts
  • src/shared/db/contact-tokens.ts
  • src/shared/db/modifier-usage.ts
  • src/shared/db/payment-finalize.ts
  • src/shared/db/processed-payments.ts
  • src/shared/seeds.ts
  • test/features/admin/attendee-create-atomic.test.ts
  • test/features/api/payment-processing/recovery-decision.test.ts
  • test/features/public/free-reservation-atomic.test.ts
  • test/features/public/ticket-payment.test.ts
  • test/integration/servicing/create-atomic.test.ts
  • test/lib/code-quality.test.ts
  • test/lib/db/attendees/create-attendee-atomic.test.ts
  • test/lib/db/attendees/delete-attendee.test.ts
  • test/lib/db/settle-balance.test.ts
  • test/lib/processed-payments/locking.test.ts
  • test/lib/server-attendee-refresh-payment.test.ts
  • test/lib/server-attendees/delete-incomplete.test.ts
  • test/lib/server-bulk-email/notes-and-history.test.ts
  • test/lib/server-privacy.test.ts
  • test/lib/server-public/ticket-additional-coverage.test.ts
  • test/lib/server-refunds-balance-payments.test.ts
  • test/lib/server-reservation/helpers.ts
  • test/lib/server-reservation/public-default-modifiers.test.ts
  • test/lib/server-webhooks/already-processed-rollback.test.ts
  • test/lib/server-webhooks/concurrent-processing.test.ts
  • test/lib/server-webhooks/modifier-refunds.test.ts
  • test/lib/test-utils/factories.test.ts
  • test/lib/webhook-price-signature/post-commit-recovery.test.ts
  • test/routes/unsubscribe.test.ts
  • test/shared/db/attendees/create-rollback.test.ts
  • test/shared/db/attendees/create.test.ts
  • test/shared/db/contact-preferences.test.ts
  • test/shared/db/contact-tokens.test.ts
  • test/shared/db/listing-overview-stats.test.ts
  • test/shared/db/listings/delete.test.ts
  • test/shared/db/modifier-resolve.test.ts
  • test/shared/db/modifier-usage.test.ts
  • test/shared/db/payment-references.test.ts
  • test/shared/db/processed-payments.test.ts
  • test/shared/db/processed-payments/finalize-guard.test.ts
  • test/shared/db/processed-payments/staleness.test.ts
  • test/shared/merge/attendee-merge/repoint.test.ts
  • test/test-utils/atomic-booking.ts
  • test/test-utils/contact-preferences.ts
  • test/test-utils/contact-tokens.ts
  • test/test-utils/db-helpers/attendees.ts
  • test/test-utils/modifiers.ts
  • test/test-utils/processed-payments.ts
💤 Files with no reviewable changes (5)
  • TODO.md
  • test/shared/db/attendees/create.test.ts
  • src/docs/database.ts
  • test/lib/code-quality.test.ts
  • src/shared/db/contact-preferences.ts

Comment thread docs/importer-plan.md
Comment thread src/features/admin/attendee-form-routes.ts
Comment thread src/features/api/payment-processing/completion.ts
Comment thread src/shared/db/attendees/create.ts
Comment thread test/lib/webhook-price-signature/post-commit-recovery.test.ts Outdated
Comment thread test/shared/db/attendees/create-rollback.test.ts
Comment thread test/test-utils/atomic-booking.ts Outdated
@stefan-burke

Copy link
Copy Markdown
Member Author

Addressed the CodeRabbit review-body comment about AttendeeOrderFields in b870080. The insert shape now uses AttendeeKind, drops the unused remainingBalance field, and both callers pass only fields that buildAttendeeInsert writes. Returned attendee balances still come from BuildAttendeeInput. The full precommit gate passed.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/importer-plan.md`:
- Around line 1751-1755: Align the phase-5 list item beginning “Increment visits
and admin_booking_count” with its sibling items by reducing its indentation to
the surrounding list level, including its wrapped lines.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e4f09591-a088-420e-9b6e-b08b63443a3a

📥 Commits

Reviewing files that changed from the base of the PR and between d2e5dc5 and a47f5da.

📒 Files selected for processing (9)
  • TODO.md
  • docs/importer-plan.md
  • src/features/admin/attendee-form-routes.ts
  • src/shared/db/attendees/capacity/checks.ts
  • src/shared/db/attendees/create.ts
  • test/features/admin/attendee-create-atomic.test.ts
  • test/lib/webhook-price-signature/post-commit-recovery.test.ts
  • test/shared/db/attendees/create-rollback.test.ts
  • test/test-utils/atomic-booking.ts

Comment thread docs/importer-plan.md
@stefan-burke
stefan-burke added this pull request to the merge queue Jul 15, 2026
Merged via the queue into main with commit 9214215 Jul 15, 2026
3 checks passed
@stefan-burke
stefan-burke deleted the split/atomic-booking-recovery branch July 15, 2026 13:04
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.

1 participant