Skip to content

Stage paid bookings until payment succeeds - #1764

Closed
stefan-burke wants to merge 29 commits into
mainfrom
fix/committed-booking-refunds
Closed

stefan-burke wants to merge 29 commits into
mainfrom
fix/committed-booking-refunds

Conversation

@stefan-burke

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

Copy link
Copy Markdown
Member

What changed

  • Create the Stripe session first, then store the whole order as quantity-zero checkout rows before sending the customer to Stripe.
  • Keep staged rows out of capacity, stock, booking totals, the money ledger, and contact history. They are not reservations or holds.
  • Remove unpaid staged attendee data when checkout is cancelled, and prune abandoned pending stages after seven days. A payment already being processed blocks cleanup.
  • When Stripe confirms payment, check current capacity and modifier stock again, then activate the same attendee and booking rows in one database transaction.
  • Record the payment, ledger entries, modifier use, contact history, and completed checkout stage through the shared booking path.
  • If the order can no longer be booked, leave its rows at quantity zero, keep the provider payment reference for operator follow-up, mark the checkout as failed, and refund the payment.
  • Recover bookings from committed payment state after a late processing error without issuing a refund for a live ticket.
  • Make free orders and multi-item writes atomic, so a failed order cannot leave a partial booking behind.
  • Preserve existing servicing booking rows and their check-in, logistics, and attachment state when an operator edits the event.
  • Add the checkout-stages database migration and regression tests for successful claims, cancellation, stale cleanup, capacity and stock changes, package paths, retries, refunds, servicing edits, and transaction rollback.

Why

A paid order must not claim a place before payment succeeds. It must also never refund a payment while leaving a live ticket behind. The previous flow could depend on cleanup after part of an order had already been written. The new flow stores a non-booking checkout record first, then claims every place together only after Stripe confirms payment. Cancelled and abandoned checkouts no longer keep customer data forever.

Verification

  • deno task precommit
  • 100% line and branch coverage
  • Zero duplicated code reported by the precommit checks

@coderabbitai

coderabbitai Bot commented Jul 11, 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
📝 Walkthrough

Walkthrough

Payment processing now preserves payment ticket tokens before booking commits, carries payment references through ledger replay, and conditionally recovers unexpected booking failures. Database behavior and webhook, replay, locking, PII, and token tests were updated accordingly.

Changes

Payment processing safety

Layer / File(s) Summary
Payment ticket token lifecycle
src/shared/payment-ticket-token.ts, src/shared/db/attendees/pii.ts, src/features/api/payment-processing/index.ts, src/shared/db/payment-finalize.ts, src/shared/db/processed-payments.ts, test/shared/**
Async-scoped payment ticket tokens are generated or reused, stored before booking creation, encrypted into attendee PII, and preserved during finalization.
Ledger replay and unexpected-create recovery
src/features/api/payment-processing/index.ts, src/features/api/payment-processing/recovery.ts, src/features/api/payment-processing/completion.ts
Ledger replay accepts payment references and distinguishes recorded outcomes from unrecorded sessions; unexpected creation failures recover finalized bookings or refund unresolved reservations.
Processed-payment state validation
src/shared/db/processed-payments.ts, test/shared/db/**, test/lib/processed-payments/**, test/lib/server-payments/confirm.test.ts
Processed-payment helpers and tests cover unresolved reservations, payment-reference healing, token preservation, removed attendee lookup, deterministic staleness, and replay setup.
Webhook and replay regression coverage
test/lib/webhook-price-signature/**, test/lib/server-*.test.ts, TODO.md
Webhook, balance replay, refund, concurrency, rollback, and post-commit failure tests assert recovery outcomes, refund suppression, response content, and persisted booking state; the related TODO entry is shortened.

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

Sequence Diagram(s)

sequenceDiagram
  participant Webhook
  participant processReservedSession
  participant replaySessionFromLedgerOrNull
  participant recoverOrRefundUnexpectedCreate
  participant processed_payments

  Webhook->>processReservedSession: process payment session
  processReservedSession->>replaySessionFromLedgerOrNull: replay payment reference
  replaySessionFromLedgerOrNull->>processed_payments: inspect recorded outcome
  processed_payments-->>processReservedSession: result or null
  processReservedSession->>recoverOrRefundUnexpectedCreate: recover creation failure
  recoverOrRefundUnexpectedCreate->>processed_payments: inspect finalized or unresolved reservation
  processed_payments-->>Webhook: recovered success, refund, or original error
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately reflects the main change: staging paid bookings through the payment-success flow.
✨ 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 fix/committed-booking-refunds

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: ce199cc597

ℹ️ 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

@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: 6848c5ce09

ℹ️ 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 Outdated
@stefan-burke
stefan-burke added this pull request to the merge queue Jul 11, 2026

@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.

Caution

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

⚠️ Outside diff range comments (1)
src/features/api/payment-processing/index.ts (1)

137-156: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Sentinel choice diverges from *OrNull convention.

replaySessionFromLedger uses a literal false to signal "unrecorded" instead of null, and the function/type aren't named to signal this (e.g. no OrNull/documented absence marker). Per the repo guideline on optional absence, null + explicit naming is preferred so callers can't confuse the sentinel with a legitimate falsy PaymentResult field.

♻️ Suggested naming/sentinel alignment
-const replaySessionFromLedger = async (
+/** Returns null when the session is not yet recorded in the ledger. */
+const replaySessionFromLedgerOrNull = async (
   sessionId: string,
   listingId: number,
   paymentReference: string,
-): Promise<PaymentResult | false> => {
+): Promise<PaymentResult | null> => {
   const disposition = await bookingLedgerDisposition(sessionId);
   switch (disposition.status) {
     case "unrecorded":
-      return false;
+      return null;

As per coding guidelines, "For expected optional absence, use explicit nullable naming such as *OrNull, document the expected absence, and reserve defaults, optional chaining, and nullable returns for genuinely meaningful optional values."

🤖 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/features/api/payment-processing/index.ts` around lines 137 - 156, Update
replaySessionFromLedger to return null instead of false for the "unrecorded"
disposition, change its return type accordingly, and rename the function with an
OrNull suffix to make the expected absence explicit. Update all callers to use
the renamed function and handle the nullable result without treating false as
the sentinel.

Source: Coding guidelines

🤖 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.

Outside diff comments:
In `@src/features/api/payment-processing/index.ts`:
- Around line 137-156: Update replaySessionFromLedger to return null instead of
false for the "unrecorded" disposition, change its return type accordingly, and
rename the function with an OrNull suffix to make the expected absence explicit.
Update all callers to use the renamed function and handle the nullable result
without treating false as the sentinel.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 668c9032-84ad-4868-9617-fd6b1c1c0999

📥 Commits

Reviewing files that changed from the base of the PR and between ce199cc and 6848c5c.

📒 Files selected for processing (2)
  • src/features/api/payment-processing/index.ts
  • test/lib/webhook-price-signature/trusted-and-mismatch.test.ts

@stefan-burke
stefan-burke removed this pull request from the merge queue due to a manual request Jul 11, 2026
@stefan-burke

Copy link
Copy Markdown
Member Author

@coderabbitai Resolved the nullable-sentinel review note in ee33cd8. replaySessionFromLedgerOrNull now returns null for an unrecorded session, its type and name state that contract, and the caller checks replay !== null. The final payment-processor mutation run detected all 70/70 mutants.

@stefan-burke stefan-burke changed the title Never refund a booking that already completed Keep completed bookings and their ticket links Jul 11, 2026
@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
git log --oneline -5 ee33cd8e 2>/dev/null || echo "commit not found locally"
git show ee33cd8e --stat 2>/dev/null | head -50

Length of output: 1663


🏁 Script executed:

#!/bin/bash
git show ee33cd8e -- src/features/api/payment-processing/index.ts | head -150

Length of output: 6097


@stefan-burke Confirmed — I checked commit ee33cd8. The renamed replaySessionFromLedgerOrNull now returns Promise<PaymentResult | null>, the "unrecorded" disposition case returns null, and the doc comment explicitly states "Returns null when the ledger has not recorded this session yet." The caller in processReservedSession correctly checks replay !== null before short-circuiting. This is a clean resolution of the nullable-sentinel ambiguity — much clearer than the previous boolean false sentinel that could be confused with a falsy PaymentResult. Nice work, and good to hear the mutation suite still detects all mutants.

(=^・ω・^=) ノ

@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.

Caution

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

⚠️ Outside diff range comments (1)
src/features/api/payment-processing/index.ts (1)

76-82: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Remove the now-ignored storeTokens option.

The API still accepts { storeTokens: false }, but _options is discarded and Line 267 always persists a token. Remove the option so existing callers cannot silently rely on behavior that no longer exists.

Proposed cleanup
-type SessionProcessorOptions = { storeTokens?: boolean };
-
 type SessionProcessor = (
   sessionId: string,
   data: ValidatedSession,
-  options?: SessionProcessorOptions,
 ) => Promise<PaymentResult>;

 export const processPaymentSession: SessionProcessor = async (
   sessionId,
   data,
-  _options,
 ) => {

Also applies to: 321-325

🤖 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/features/api/payment-processing/index.ts` around lines 76 - 82, Remove
the storeTokens property from SessionProcessorOptions and eliminate the
now-unused options parameter from SessionProcessor and its implementations,
including the call sites around the payment session processing flow. Update
callers to stop passing storeTokens while preserving the existing unconditional
token persistence behavior.
🤖 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.

Outside diff comments:
In `@src/features/api/payment-processing/index.ts`:
- Around line 76-82: Remove the storeTokens property from
SessionProcessorOptions and eliminate the now-unused options parameter from
SessionProcessor and its implementations, including the call sites around the
payment session processing flow. Update callers to stop passing storeTokens
while preserving the existing unconditional token persistence behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 7a73416c-3ac1-4a80-89f8-5524603345c5

📥 Commits

Reviewing files that changed from the base of the PR and between 6848c5c and ee33cd8.

📒 Files selected for processing (13)
  • src/features/api/payment-processing/index.ts
  • src/features/api/payment-processing/recovery.ts
  • src/shared/db/attendees/pii.ts
  • src/shared/db/payment-finalize.ts
  • src/shared/db/processed-payments.ts
  • src/shared/payment-ticket-token.ts
  • test/lib/code-quality.test.ts
  • test/lib/processed-payments/locking.test.ts
  • test/lib/server-payments/confirm.test.ts
  • test/lib/webhook-price-signature/post-commit.test.ts
  • test/shared/db/attendees/pii.test.ts
  • test/shared/db/processed-payments.test.ts
  • test/shared/payment-ticket-token.test.ts
💤 Files with no reviewable changes (2)
  • test/lib/processed-payments/locking.test.ts
  • test/lib/code-quality.test.ts

@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: ee33cd8ea3

ℹ️ 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/recovery.ts Outdated
Comment thread src/features/api/payment-processing/recovery.ts Outdated

@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: d4640c37c7

ℹ️ 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/recovery.ts Outdated
Comment thread src/shared/db/payment-finalize.ts Outdated
@stefan-burke

Copy link
Copy Markdown
Member Author

Addressed the final CodeRabbit storeTokens finding in 2fd68ed by restoring the option’s real contract instead of leaving it ignored: successful redirect processing clears the pre-stored database token after returning it for the URL, while direct-render and webhook processing retain it for a later redirect or reload. The redirect/direct-render tests and the payment processor mutation suite cover both paths.

@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: 6ccf91c68a

ℹ️ 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/recovery.ts Outdated
Comment thread src/shared/db/processed-payments.ts Outdated

@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: 2

🤖 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 `@src/shared/db/processed-payments.ts`:
- Around line 304-308: Update the pre-store flow around encryptTicketTokens and
execute so it validates that the UPDATE matched a row for the supplied
sessionId. Use execute’s returned affected-row count or equivalent result shape,
and throw a descriptive error when no row is updated; preserve the existing
successful update behavior and let the failure propagate.

In `@test/lib/webhook-price-signature/post-commit.test.ts`:
- Around line 237-238: Update the assertion around isSessionProcessed in the
post-commit test to establish that processed is non-null before checking its
contents, using the TypeScript non-null assertion if the record is guaranteed to
exist. Then assert failure_data directly against the expected non-empty value
without optional chaining.
🪄 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: f86e890e-a184-45c4-a159-be029d185f5a

📥 Commits

Reviewing files that changed from the base of the PR and between d4640c3 and 6ccf91c.

📒 Files selected for processing (4)
  • src/features/api/payment-processing/recovery.ts
  • src/shared/db/processed-payments.ts
  • test/lib/webhook-price-signature/post-commit.test.ts
  • test/shared/db/processed-payments/staleness.test.ts

Comment thread src/shared/db/processed-payments.ts Outdated
Comment thread test/lib/webhook-price-signature/post-commit.test.ts Outdated

@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: af47a1400f

ℹ️ 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/recovery.ts Outdated

@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: 3eaebc6c2e

ℹ️ 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/recovery.ts Outdated

@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: 1aac4e0116

ℹ️ 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 Outdated
@stefan-burke stefan-burke changed the title Keep completed bookings and their ticket links Stage paid bookings until payment succeeds Jul 12, 2026

@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: b02e9f1920

ℹ️ 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/shared/db/checkout-stages.ts
Comment thread src/features/api/payment-processing/store-refund.ts
Comment thread src/shared/db/attendees/servicing.ts
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