Skip to content

Keep existing payments working when new sales are off - #2020

Merged
stefan-burke merged 49 commits into
mainfrom
payment-aggregate/01-safety-behavior
Aug 4, 2026
Merged

Keep existing payments working when new sales are off#2020
stefan-burke merged 49 commits into
mainfrom
payment-aggregate/01-safety-behavior

Conversation

@stefan-burke

@stefan-burke stefan-burke commented Aug 1, 2026

Copy link
Copy Markdown
Member

What changed

Turning off new sales now stops new checkouts without cutting off payment work that already exists.

  • Refunds, payment callbacks, checkout completion, cancellation pages, and dashboard links use the provider for existing payments.
  • The site remembers the last active provider when new sales are turned off.
  • Older sites recover automatically when only one provider has saved credentials.
  • When several providers could own old payments, the operator must choose one before enabling sales or changing a domain.
  • Saving provider credentials keeps a newer provider choice and does not turn sales back on.
  • Provider choices, credential saves, and domain changes share the settings task lock.
  • Settings forms carry their loaded revision, so stale requests cannot replace a newer provider or domain choice.

Operator impact

An operator can pause new online sales and still refund or finish payments already in progress. If an older site has an unclear provider, the settings page asks for an explicit choice while keeping new sales off. A stale settings page asks the operator to reload instead of overwriting a newer choice.

Known limitation

Each payment still stores only its provider reference, not the provider name. If a site took payments through more than one provider, older payments may still need per-payment provider tracking. That follow-up is recorded in TODO.md and docs/payment-aggregate-acceptance.md.

The requested splits for the oversized settings and webhook modules are also recorded in TODO.md. They were not included here because this PR has a strict source-change limit.

Verification

  • Source churn against current main: 556 additions + 158 deletions = 714 lines.
  • Focused form, provider, settings database, and payment-provider scenario suites passed.
  • CI completed 21,315 tests and 227 scenarios with full coverage.
  • Targeted typecheck and lint passed.
  • jscpd passed with 0% duplication.
  • Every review thread is answered and resolved.

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 23 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 68031203-41c8-4df7-ad00-baf45f5de574

📥 Commits

Reviewing files that changed from the base of the PR and between a2676cc and adab54e.

📒 Files selected for processing (111)
  • TODO.md
  • scripts/mutation/equivalent-mutants/features.txt
  • scripts/mutation/equivalent-mutants/shared-a-l.txt
  • scripts/mutation/equivalent-mutants/shared-m-z.txt
  • src/features/admin/attendees-edit.ts
  • src/features/admin/refunds/provider.ts
  • src/features/admin/settings-general.ts
  • src/features/admin/settings-helpers.ts
  • src/features/api/payment-processing/classify.ts
  • src/features/api/payment-processing/refunds.ts
  • src/features/api/webhooks.ts
  • src/locales/en/payment.json
  • src/locales/en/settings.json
  • src/shared/currency.ts
  • src/shared/db/payment-references.ts
  • src/shared/db/settings.ts
  • src/shared/db/settings/current-task.ts
  • src/shared/payment-helpers.ts
  • src/shared/payment-signature.ts
  • src/shared/payment/money.ts
  • src/shared/payment/refund-state.ts
  • src/shared/payment/resource-id.ts
  • src/shared/payment/validated-session.ts
  • src/shared/payments.ts
  • src/shared/square-provider.ts
  • src/shared/square.ts
  • src/shared/stripe-provider.ts
  • src/shared/stripe/schemas.ts
  • src/shared/sumup-provider.ts
  • src/shared/sumup.ts
  • src/shared/validation/money.ts
  • src/ui/templates/admin/settings-advanced.tsx
  • src/ui/templates/admin/settings.tsx
  • src/ui/templates/admin/settings/payment.tsx
  • test/features/admin/refunds/provider.test.ts
  • test/features/admin/refunds/provider/batch.test.ts
  • test/features/admin/settings-general/payment-provider.test.ts
  • test/features/api/payment-processing/cancel.test.ts
  • test/features/api/payment-processing/classify.test.ts
  • test/features/api/payment-processing/committed-entries.test.ts
  • test/features/api/payment-processing/create.test.ts
  • test/features/api/payment-processing/index/helpers.ts
  • test/features/api/payment-processing/metadata.test.ts
  • test/features/api/payment-processing/recovery.test.ts
  • test/features/api/payment-processing/refunds.test.ts
  • test/features/api/payment-processing/refunds/rejected-charge.test.ts
  • test/features/api/webhooks/callbacks.test.ts
  • test/integration/balance-helpers.ts
  • test/integration/server-balance-webhook.test.ts
  • test/integration/server/attendees/payment.test.ts
  • test/integration/server/attendees/placeholder-refresh.test.ts
  • test/integration/server/balance-payment-replay.test.ts
  • test/integration/server/package-children.test.ts
  • test/integration/server/payments-success-basic.test.ts
  • test/integration/server/payments-success-refunds.test.ts
  • test/integration/server/payments-success-replay.test.ts
  • test/integration/server/payments-success-tokens.test.ts
  • test/integration/server/payments/cancel.test.ts
  • test/integration/server/payments/success.test.ts
  • test/integration/server/webhooks/concurrent-processing.test.ts
  • test/integration/server/webhooks/extract-intent-redirect.test.ts
  • test/integration/server/webhooks/item-validation.test.ts
  • test/integration/server/webhooks/price-paid-calculation.test.ts
  • test/integration/server/webhooks/refund-helper-functions.test.ts
  • test/integration/server/webhooks/refund-skip-conditions.test.ts
  • test/integration/server/webhooks/single-ticket-refunds.test.ts
  • test/integration/server/webhooks/sumup.test.ts
  • test/integration/server/webhooks/unrecognized-sessions.test.ts
  • test/integration/webhook-price-signature/helpers.ts
  • test/scripts/deno-command.test.ts
  • test/shared/currency.test.ts
  • test/shared/db/payment-references.test.ts
  • test/shared/db/settings/public-api.test.ts
  • test/shared/db/settings/raw-writes.test.ts
  • test/shared/merge/attendee-merge/repoint.test.ts
  • test/shared/payment-helpers/dispatch.test.ts
  • test/shared/payment-signature.test.ts
  • test/shared/payment/money.test.ts
  • test/shared/payment/refund-state.test.ts
  • test/shared/payment/resource-id.test.ts
  • test/shared/payment/validated-session.test.ts
  • test/shared/refund-ledger/helpers.ts
  • test/shared/square-provider.test.ts
  • test/shared/square-provider/provider.test.ts
  • test/shared/square-provider/webhook.test.ts
  • test/shared/square/refund-payment.test.ts
  • test/shared/square/refund-transport.test.ts
  • test/shared/square/rest-transport.test.ts
  • test/shared/square/retrieve-refund.test.ts
  • test/shared/stripe-provider.test.ts
  • test/shared/stripe-provider/operations.test.ts
  • test/shared/stripe-provider/webhook.test.ts
  • test/shared/stripe/schemas.test.ts
  • test/shared/sumup-provider.test.ts
  • test/shared/sumup-provider/webhook.test.ts
  • test/shared/sumup.test.ts
  • test/shared/sumup/connection.test.ts
  • test/shared/sumup/create-checkout.test.ts
  • test/shared/sumup/transactions.test.ts
  • test/shared/validation/money.test.ts
  • test/specs/support/money-drivers.ts
  • test/test-utils/payment-session.ts
  • test/test-utils/processed-payments.ts
  • test/test-utils/session.ts
  • test/test-utils/square/fixtures.ts
  • test/test-utils/stripe/fixtures.ts
  • test/test-utils/stripe/responses.ts
  • test/test-utils/sumup.ts
  • test/test-utils/webhook-verify-helpers.ts
  • test/test-utils/webhooks.ts
  • test/ui/templates/admin/settings/payment.test.ts
📝 Walkthrough

Walkthrough

This PR separates provider resolution for new checkouts from provider resolution for existing payments. It adds a LAST_ACTIVE_PAYMENT_PROVIDER setting, transactional provider state updates, an existing-payment-provider resolution module, an admin recovery route and form, domain-change gating on recovery status, settings-version-based task locking, and extensive tests and documentation.

Changes

Payment provider safety

Layer / File(s) Summary
Persist remembered provider state
src/shared/db/settings.ts, src/shared/db/settings/raw-writes.ts, src/shared/db/settings/current-task.ts, src/shared/settings/keys.ts, src/shared/settings/registry.ts, src/shared/forms/csrf-form.tsx, src/features/admin/settings-helpers.ts, src/features/admin/settings-stripe.ts, scripts/mutation/..., test/shared/db/settings/*, test/shared/stripe/endpoints/connection.test.ts, test/integration/server/settings/sensitive-field-masking.test.ts, test/test-utils/session.ts
Adds LAST_ACTIVE_PAYMENT_PROVIDER, transactional provider-state updates, version-aware task locking, renames Stripe activate to configure, and adds settings_version to forms and requests.
Resolve providers for existing payments
src/shared/payments.ts, src/shared/existing-payment-provider.ts, src/shared/payment-dashboard.ts, src/features/admin/require-provider.ts, src/features/api/payment-processing/classify.ts, src/features/api/payment-processing/refunds.ts, test/shared/existing-payment-provider.test.ts, test/shared/payments.test.ts, test/features/admin/require-provider.test.ts
Adds getPaymentProviderForExistingPayments, existingPaymentProviderState, and updates refund, classification, and dashboard resolution accordingly.
Use resolution across payment flows
src/features/api/webhooks.ts, test/integration/server/payments/sales-off-safety.test.ts, test/features/api/webhooks/*, test/features/api/payment-processing/refunds.test.ts, test/integration/server/webhooks/refund-skip-conditions.test.ts, test/test-utils/webhook-verify-helpers.ts, test/test-utils/debug-log.ts, test/test-utils/refund-routes.ts
Webhooks, cancellations, and refunds resolve existing-payment providers, add failureDetail, and gain expanded integration test coverage for sales-off scenarios.
Expose provider recovery in settings
src/features/admin/settings-general.ts, src/features/admin/settings.ts, src/features/admin/settings-page.ts, src/ui/templates/admin/settings*, src/locales/en/settings.json, test/features/admin/settings-*, test/ui/templates/admin/settings*, test/test-utils/csrf.ts
Adds a recovery POST route, ExistingPaymentProviderForm, updated settings state shapes, SubmitButton disabled support, and localization strings.
Gate domain changes on recovery
src/features/admin/settings-domains.ts, specs/payments/payment-provider-choice.feature, test/specs/steps/payment-provider-choice.ts, test/features/admin/settings-domains/*
Custom-domain and host-subdomain routes require settings versions and block changes until provider recovery is complete, verified with Cucumber and integration tests.
Record payment aggregate acceptance rules
docs/payment-aggregate-acceptance.md, TODO.md
Documents provider-resolution semantics, refund limitations, and deferred refactoring plans.

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

Sequence Diagram(s)

sequenceDiagram
  participant AdminSettingsPage
  participant SettingsGeneralRoute
  participant Settings
  participant ExistingPaymentProvider

  AdminSettingsPage->>SettingsGeneralRoute: POST /admin/settings/payment-provider-recovery
  SettingsGeneralRoute->>ExistingPaymentProvider: existingPaymentProviderState()
  ExistingPaymentProvider-->>SettingsGeneralRoute: recovery choices
  SettingsGeneralRoute->>Settings: withCurrentTask(recoverPaymentProvider)
  Settings-->>SettingsGeneralRoute: updated provider state
  SettingsGeneralRoute-->>AdminSettingsPage: redirect with success
Loading
sequenceDiagram
  participant Webhook
  participant GetPaymentProviderForExistingPayments
  participant ExistingPaymentProvider
  participant Settings
  participant PaymentProvider

  Webhook->>GetPaymentProviderForExistingPayments: resolve provider
  GetPaymentProviderForExistingPayments->>ExistingPaymentProvider: existingPaymentProviderType()
  ExistingPaymentProvider->>Settings: read active/remembered provider
  ExistingPaymentProvider->>PaymentProvider: infer from stored credentials
  ExistingPaymentProvider-->>GetPaymentProviderForExistingPayments: resolved provider
  GetPaymentProviderForExistingPayments-->>Webhook: provider or null
Loading

Possibly related PRs

  • chobbledotcom/tickets#1841: The main PR refactors payment-provider selection logic by renaming stripe.activate() to stripe.configure() and moving provider selection to separate recovery and fallback logic.
  • chobbledotcom/tickets#1925: Both PRs modify payment-provider choice/recovery behavior and its Cucumber step definitions in specs/payments/payment-provider-choice.feature and test/specs/steps/payment-provider-choice.ts.
  • chobbledotcom/tickets#2021: The PRs directly overlap in payment-processing and refund/webhook handling, including shared changes to refunds.ts, classify.ts, webhooks.ts, and payments.ts.
🚥 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 clearly summarizes the primary change: preserving existing payment operations when new sales are disabled.
✨ 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 payment-aggregate/01-safety-behavior
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch payment-aggregate/01-safety-behavior

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

@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 `@src/features/api/payment-processing/refunds.ts`:
- Around line 40-50: Define and export a shared ExistingPaymentProviderResult
type from `#shared/payments.ts`, then update getPaymentProviderOrLog in
src/features/api/payment-processing/refunds.ts#L40-L50 to use it instead of the
inline Awaited<ReturnType<...>> expression. Also update authenticateWebhook in
src/features/api/webhooks.ts#L321-L329 to use
NonNullable<ExistingPaymentProviderResult>, preserving its existing non-nullable
return contract.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: beec80eb-fa11-41a1-a2b5-fac7fdeca1e3

📥 Commits

Reviewing files that changed from the base of the PR and between 09e47fd and 2e4f452.

📒 Files selected for processing (18)
  • TODO.md
  • docs/payment-aggregate-acceptance.md
  • scripts/mutation/equivalent-mutants/shared-m-z.txt
  • src/features/admin/require-provider.ts
  • src/features/api/payment-processing/classify.ts
  • src/features/api/payment-processing/refunds.ts
  • src/features/api/webhooks.ts
  • src/features/settings-bundles.ts
  • src/shared/db/settings.ts
  • src/shared/payments.ts
  • src/shared/settings/keys.ts
  • src/shared/settings/registry.ts
  • test/integration/server/payments/sales-off-safety.test.ts
  • test/integration/server/webhooks/refund-skip-conditions.test.ts
  • test/shared/db/settings/public-api.test.ts
  • test/shared/payments.test.ts
  • test/shared/settings/keys.test.ts
  • test/shared/settings/registry.test.ts

Comment thread src/features/api/payment-processing/refunds.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: 2e4f452de1

ℹ️ 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/settings.ts Outdated
Comment thread src/shared/payments.ts Outdated
Switching new sales off (saving the payment provider as "none") used to strand
every payment already captured: refunds, replayed callbacks, redirect
completion, the cancel page, and operator refunds all resolved the provider
through the new-sales gate, so they broke the moment a buyer could no longer
start a checkout -- even though the provider's credentials stay stored.

Separate the two questions. New sales keep using getActivePaymentProvider /
isPaymentsEnabled. Refunds, provider reconciliation, replayed callbacks, and
completion of already-started payment work use a new resolver,
getPaymentProviderForExistingPayments, which falls back to the last provider the
operator activated when new sales are off. That provider is remembered in a new
last_active_payment_provider setting, captured when a provider is switched on
and again when new sales are switched off (a second "none" save keeps it).

The seven accepted safety rules the payment aggregate must satisfy --
including the owner-review, queued owner email, and aggregate-activation
behaviour not yet on main -- are recorded as acceptance constraints in
docs/payment-aggregate-acceptance.md, not built ahead of their time.

Regression tests cover each path: a captured payment refunded while sales are
off, an in-flight payment completing and issuing the ticket, a handled failure
replaying on a redelivered callback, and an operator refunding an existing paid
booking -- all written to fail before the fix and pass after.
@stefan-burke
stefan-burke force-pushed the payment-aggregate/01-safety-behavior branch from 2e4f452 to dc137e9 Compare August 1, 2026 00:49

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

ℹ️ 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/payments.ts Outdated
CodeRabbit (CHANGES_REQUESTED): export a named ExistingPaymentProvider type
from #shared/payments.ts and use it in place of the duplicated
Awaited<ReturnType<typeof getPaymentProviderForExistingPayments>> expression
in getPaymentProviderOrLog and authenticateWebhook.

Codex P2: persist PAYMENT_PROVIDER and LAST_ACTIVE_PAYMENT_PROVIDER in one
writeRawBatch transaction (mirroring the existing Stripe activation path), so a
failure between the two writes cannot leave new sales enabled against a stale
remembered provider. The snapshot mirrors the committed values only after the
batch succeeds. A second 'none' save keeps the remembered provider.

Codex P1 (per-charge provider) and P1 (pre-existing none sites): recorded as
known gaps in docs/payment-aggregate-acceptance.md and TODO.md rather than
implemented here — per-charge provider tracking is the reference a charge lives
in and is future aggregate work; a site already on 'none' with no recorded
last-active provider is not regressed by this PR and recovers via a one-click
provider re-select.

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

ℹ️ 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/settings.ts Outdated
Comment thread src/shared/db/settings.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: 4

Caution

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

⚠️ Outside diff range comments (2)
TODO.md (2)

34-35: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix the Markdown reference to PR #1995.

Line 35 starts with #1995.. This triggers MD018 and renders as a malformed heading. Keep the reference in the sentence, such as shipped on PR #1995..

🤖 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 `@TODO.md` around lines 34 - 35, Update the Finding 1 sentence in TODO.md so
the PR reference remains inline, ensuring it uses “PR `#1995`” rather than
starting the line with “#1995.” and triggering MD018.

Source: Linters/SAST tools


25-32: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Replace local scan references with durable references.

The TODO records an absolute /home/user/... path and local worktree names. These values are not available to other contributors and will become stale. Use stable ticket, PR, or repository-relative references.

🤖 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 `@TODO.md` around lines 25 - 32, Update the TODO entry describing Findings 2
and 4 to remove the absolute local scan path and local worktree names, replacing
them with durable ticket, pull-request, or repository-relative references while
preserving the findings’ descriptions.
🤖 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/settings.ts`:
- Around line 464-480: Update setPaymentProviderNone so keepLastActive is
derived from the current database state or the provider transition is
serialized, preventing a concurrent activation from persisting a stale
LAST_ACTIVE_PAYMENT_PROVIDER alongside payment_provider = "none". Preserve the
behavior of retaining the last active provider on repeated disable operations,
and add a regression test covering the disable/activation interleaving.

In `@test/integration/server/payments/sales-off-safety.test.ts`:
- Around line 93-125: Replace the manual try/finally disposal at
test/integration/server/payments/sales-off-safety.test.ts lines 93-125 and lines
195-207 with using declarations for the stub results: use using mockVerify for
the verifyWebhookSignature stub and using mockRefund for the refund stub, then
keep each test body directly after its declaration and remove the corresponding
restore calls and try/finally wrappers.

In `@TODO.md`:
- Around line 492-494: The existing-payment guarantee is too broad because
fallback does not track the provider that captured each charge. In TODO.md lines
492-494 and docs/payment-aggregate-acceptance.md lines 21-25, replace the
unconditional claim with wording that guarantees recovery only for payments
captured by the remembered provider, while explicitly retaining the per-charge
provider identity gap.
- Around line 513-516: Update the payment persistence and dispatch flow so each
payment charge retains its capture-time provider using the existing
payment_charges.provider and reference_index fields, rather than storing
provider identity only on processed_payments. Backfill legacy charges only when
capture-time evidence identifies one unambiguous provider; mark unresolved
charges non-dispatchable and expose them for operator review, then dispatch
existing-payment work from each charge’s provider instead of a global fallback.

---

Outside diff comments:
In `@TODO.md`:
- Around line 34-35: Update the Finding 1 sentence in TODO.md so the PR
reference remains inline, ensuring it uses “PR `#1995`” rather than starting the
line with “#1995.” and triggering MD018.
- Around line 25-32: Update the TODO entry describing Findings 2 and 4 to remove
the absolute local scan path and local worktree names, replacing them with
durable ticket, pull-request, or repository-relative references while preserving
the findings’ descriptions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ee66fda4-586e-4c45-8d73-c697b958b976

📥 Commits

Reviewing files that changed from the base of the PR and between 2e4f452 and bc4a6dc.

📒 Files selected for processing (18)
  • TODO.md
  • docs/payment-aggregate-acceptance.md
  • scripts/mutation/equivalent-mutants/shared-m-z.txt
  • src/features/admin/require-provider.ts
  • src/features/api/payment-processing/classify.ts
  • src/features/api/payment-processing/refunds.ts
  • src/features/api/webhooks.ts
  • src/features/settings-bundles.ts
  • src/shared/db/settings.ts
  • src/shared/payments.ts
  • src/shared/settings/keys.ts
  • src/shared/settings/registry.ts
  • test/integration/server/payments/sales-off-safety.test.ts
  • test/integration/server/webhooks/refund-skip-conditions.test.ts
  • test/shared/db/settings/public-api.test.ts
  • test/shared/payments.test.ts
  • test/shared/settings/keys.test.ts
  • test/shared/settings/registry.test.ts

Comment thread src/shared/db/settings.ts
Comment thread test/integration/server/payments/sales-off-safety.test.ts Outdated
Comment thread TODO.md Outdated
Comment thread TODO.md Outdated
The shared type refactor shifted the payments.ts ?? equivalent from line
379 to 387; record the same equivalent in settings.ts:471 (data.payment_provider
?? "" — a provider type or null, never falsy-but-non-null).
CodeRabbit: tighten 'never stranded' to 'payments captured by that provider
stay refundable and completable' in the acceptance doc and TODO, since the
fallback only resolves the remembered provider (not per-charge).

CodeRabbit: replace manual try/finally stub disposal with using declarations
in sales-off-safety.test.ts (the established pattern in
test/integration/server/webhooks/).
Comment thread src/shared/db/settings.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: 3fbb3d6d9a

ℹ️ 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/settings.ts Outdated
Comment thread src/shared/payments.ts Outdated
Codex: throw on a corrupt non-empty last_active_payment_provider value
(offensive programming — a stored garbage string is surfaced at read time
rather than silently treated as null). Covered by a regression test.

Codex: paymentDashboardUrl and the domain-change webhook warning both read
settings.paymentProvider (the new-sales gate) and returned null when sales
were off, hiding dashboard links and the domain-change warning for existing
payments. Both now fall back to settings.lastActivePaymentProvider.

Remove the broken CODEX_SECURITY_PYTHON export from flake.nix — the
codexSecurityPython variable was never defined (landed on main in commit
09e47fd), breaking nix develop and every nix-wrapped check runner.
Pre-existing none sites: the resolver now falls back to the sole provider
with stored credentials when exactly one is configured (unambiguous
evidence). When zero or multiple providers have credentials, it returns null
(genuine ambiguity — operator must re-select, no guessing).

Concurrency race: setPaymentProviderNone now reads the current provider from
the database via a SQL subquery inside the batch statement (not from the
request-start snapshot), so a concurrent activation landing before the
batch is correctly reflected. Uses a single INSERT OR REPLACE ... SELECT
that evaluates subqueries against pre-statement state, making read and write
atomic without withTransaction's cache invalidation (which resets the
session private key).

Regression tests: single-credential recovery, multi-credential no-guess,
sumup sole-credential, concurrency race (stale snapshot vs DB state).
Raw-cache mirroring assertions kill the syncStoredSetting removal mutant.
Mirror-located tests added for require-provider, refunds, webhooks,
settings-page, custom-domain, subdomain.

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

Caution

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

⚠️ Outside diff range comments (2)
src/shared/db/settings/raw-writes.ts (1)

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

Export syncWrittenBatch and reuse it in setPaymentProviderNone to fix the doc comment and remove duplicated cache-sync logic. The comment on syncWrittenBatch claims it is "Shared by every batch write path," but setPaymentProviderNone bypasses it, manually calling syncStoredSetting twice with the same key/value-mirroring logic that syncWrittenBatch already implements in one syncCache call.

  • src/shared/db/settings/raw-writes.ts#L95-105: export syncWrittenBatch, or correct the comment to state it is shared only by writeRawBatch-based paths.
  • src/shared/db/settings.ts#L507-512: once known values are available (after the write completes), call syncWrittenBatch([[CONFIG_KEYS.PAYMENT_PROVIDER, "none"], [CONFIG_KEYS.LAST_ACTIVE_PAYMENT_PROVIDER, lastActive]]) instead of two separate syncStoredSetting calls.
🤖 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/settings/raw-writes.ts` at line 1, The syncWrittenBatch
helper’s documented shared usage is incomplete and setPaymentProviderNone
duplicates its cache synchronization. Export syncWrittenBatch from
raw-writes.ts, then update setPaymentProviderNone to call it after the write
completes with both payment-provider key/value pairs, replacing the two
syncStoredSetting calls.
test/shared/db/settings/public-api.test.ts (1)

133-141: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Reload after the second none save.

This test only checks the current snapshot. A second setPaymentProviderNone() call could clear the stored remembered provider while this assertion still passes.

Invalidate the cache, reload CONFIG_KEYS.LAST_ACTIVE_PAYMENT_PROVIDER, and assert that the value remains "stripe".

Proposed test extension
       await settings.update.setPaymentProviderNone();
       await settings.update.setPaymentProviderNone();
       expect(settings.paymentProvider).toBeNull();
       expect(settings.lastActivePaymentProvider).toBe("stripe");
+
+      settings.invalidateCache();
+      await settings.loadKeys([CONFIG_KEYS.LAST_ACTIVE_PAYMENT_PROVIDER]);
+      expect(settings.lastActivePaymentProvider).toBe("stripe");

As per coding guidelines, “Every bug fix must include a regression test that reproduces the exact defect and fails before the fix.”

🤖 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 `@test/shared/db/settings/public-api.test.ts` around lines 133 - 141, The test
“a second none save keeps the remembered provider” only verifies the in-memory
snapshot; invalidate the settings cache after the second
setPaymentProviderNone() call, reload CONFIG_KEYS.LAST_ACTIVE_PAYMENT_PROVIDER,
and assert the persisted value remains "stripe" alongside the existing
assertions.

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.

Inline comments:
In `@scripts/mutation/equivalent-mutants/shared-m-z.txt`:
- Around line 29-33: Remove the three ?? → || entries for custom-domain.tsx and
subdomain.tsx from the equivalent-mutant list, since empty-string
paymentProvider values make them behaviorally different. Correct the
accompanying falsy-value statement so it does not claim that "none" is falsy; do
not narrow the state type or alter advancedDefaultState unless choosing that
alternative instead.

In `@src/shared/db/settings.ts`:
- Around line 473-516: Update setPaymentProviderNone to use
executeBatchWithResults, add RETURNING key, value to the INSERT statement, and
extract the returned last_active_payment_provider row instead of calling
requireOne. Protect the subsequent syncStoredSetting calls from a concurrent
activation so this operation cannot overwrite newer cached provider state with
the stale "none" state; use the existing cache synchronization mechanism.

In `@src/shared/payment-dashboard.ts`:
- Around line 33-36: Add a regression test for paymentDashboardUrl that sets
payment_provider to null and last_active_payment_provider to "stripe", then
asserts paymentDashboardUrl("pi_123") returns the expected Stripe dashboard URL.
Place it alongside the existing dashboard tests and preserve the current
direct-provider cases.

---

Outside diff comments:
In `@src/shared/db/settings/raw-writes.ts`:
- Line 1: The syncWrittenBatch helper’s documented shared usage is incomplete
and setPaymentProviderNone duplicates its cache synchronization. Export
syncWrittenBatch from raw-writes.ts, then update setPaymentProviderNone to call
it after the write completes with both payment-provider key/value pairs,
replacing the two syncStoredSetting calls.

In `@test/shared/db/settings/public-api.test.ts`:
- Around line 133-141: The test “a second none save keeps the remembered
provider” only verifies the in-memory snapshot; invalidate the settings cache
after the second setPaymentProviderNone() call, reload
CONFIG_KEYS.LAST_ACTIVE_PAYMENT_PROVIDER, and assert the persisted value remains
"stripe" alongside the existing assertions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: bd6eada2-ebfb-4da1-a0da-abcec4751031

📥 Commits

Reviewing files that changed from the base of the PR and between bc4a6dc and 5345a26.

📒 Files selected for processing (26)
  • TODO.md
  • docs/payment-aggregate-acceptance.md
  • flake.nix
  • scripts/mutation/equivalent-mutants/shared-db.txt
  • scripts/mutation/equivalent-mutants/shared-m-z.txt
  • src/features/admin/settings-page.ts
  • src/shared/db/settings.ts
  • src/shared/db/settings/raw-writes.ts
  • src/shared/payment-dashboard.ts
  • src/shared/payments.ts
  • src/ui/templates/admin/settings-advanced.tsx
  • src/ui/templates/admin/settings.tsx
  • src/ui/templates/admin/settings/custom-domain.tsx
  • src/ui/templates/admin/settings/subdomain.tsx
  • test/features/admin/require-provider.test.ts
  • test/features/admin/settings-page/last-active-provider.test.ts
  • test/features/api/payment-processing/refunds.test.ts
  • test/features/api/webhooks.test.ts
  • test/integration/server/payments/sales-off-safety.test.ts
  • test/integration/server/webhooks/refund-skip-conditions.test.ts
  • test/shared/db/settings/public-api.test.ts
  • test/shared/payments.test.ts
  • test/ui/templates/admin/settings-advanced/state.ts
  • test/ui/templates/admin/settings-state.ts
  • test/ui/templates/admin/settings/custom-domain.test.ts
  • test/ui/templates/admin/settings/subdomain.test.ts
💤 Files with no reviewable changes (1)
  • flake.nix

Comment thread scripts/mutation/equivalent-mutants/shared-m-z.txt Outdated
Comment thread src/shared/db/settings.ts
Comment thread src/shared/payment-dashboard.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: 5345a26401

ℹ️ 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/settings.ts Outdated
Comment thread src/ui/templates/admin/settings/custom-domain.tsx Outdated
Comment thread src/shared/payments.ts Outdated
Comment thread src/shared/payments.ts Outdated
Comment thread scripts/mutation/equivalent-mutants/shared-m-z.txt Outdated
Comment thread src/shared/payments.ts Outdated
Comment thread TODO.md Outdated
Comment thread src/shared/payment-dashboard.ts Outdated
Finding (1): Replace the lagging-replica readOne after the INSERT...SELECT
with RETURNING value — the computed last_active is read from the write's own
batch result, never from a replica.

Finding (2): Unify paymentDashboardUrl and settings-page.ts on the shared
existingPaymentProviderType() from payments.ts — one provider-resolution
mechanism, no duplicate credential scans. Templates restored to main
(untouched); the advanced settings-page state supplies the resolved provider
via s.paymentProvider, so DomainPaymentWebhookWarning sees it without
template changes.

Finding (3): Remove the invalid shared-db equivalent entry for none->empty.
Finding (4): Remove the invalid UI ??->|| equivalent entries (string|null).
Finding (6): Document INSERT...SELECT (not withTransaction) in TODO.md.

Template files (.tsx) reverted to main to reduce mutation scope. Two
settings-state test fixtures reverted to match.
…tActive

Replace rawWritesApi + manual branch checks + results[0]! with a single
valibot tupleWithRest schema that validates the full batch result shape:
exactly one result set with exactly one {value: string} row, plus any
remaining result sets. v.parse(results)[0].rows[0].value is fully
type-safe — no local invalid-shape branches, no non-null assertions, no
test-only exports.

Restore distinct lastActivePaymentProvider in template state (not
overloaded into paymentProvider which controls the sales radio).
Templates use s.paymentProvider ?? s.lastActivePaymentProvider so the
domain-change warning shows when sales are off while the radio stays
'none'. Template test fixtures and types updated to match.

Remove all jscpd:ignore markers outside import blocks; extract shared
test helpers (log-spy, webhook-verify-helpers, withRefundRedirect).

@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: 17a9483505

ℹ️ 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/payments.ts Outdated
Comment thread src/shared/db/settings/raw-writes.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: 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/settings.ts (1)

250-260: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep provider selection in one atomic path. lastActivePaymentProvider correctly maps "" to null and throws for unsupported values. Square and SumUp use the shared setter, but Stripe activate() writes PAYMENT_PROVIDER before the shared setter writes LAST_ACTIVE_PAYMENT_PROVIDER. Include both keys in one atomic activation path.

🤖 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/settings.ts` around lines 250 - 260, The Stripe activation flow
must update PAYMENT_PROVIDER and LAST_ACTIVE_PAYMENT_PROVIDER atomically. Modify
Stripe’s activate() path to use the shared setter or atomic mechanism that
writes both settings together, removing the separate PAYMENT_PROVIDER write
while preserving lastActivePaymentProvider’s empty-value and validation
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.

Inline comments:
In `@test/features/api/webhooks.test.ts`:
- Around line 33-36: Update the “rejects a success callback with only bad token
params” test to store the response text and assert it contains the specific
invalid-payment-callback message used by the sibling test, replacing the
non-empty-length assertion.

In `@test/shared/db/settings/raw-writes.test.ts`:
- Around line 66-79: Add a negative test alongside the existing
“executeSettingsBatchReturningValue” test that exercises a RETURNING result with
zero or multiple rows and asserts the validation rejects it. Cover the newly
added batchReturningValue parse-rejection branch directly while preserving the
existing successful single-row test.
- Around line 33-37: Update the “throws on an empty batch” test callback to be
async and await the rejects assertion for writeRawBatch([]), ensuring the test
waits for and correctly reports the expected rejection.

In `@test/test-utils/log-spy.ts`:
- Around line 17-18: Remove the useDebugLogSpy re-export from this helper module
and update all callers to import it directly from its defining ./debug-log.ts
module; do not move or duplicate the implementation, and do not retain a
compatibility import path.
- Around line 20-22: Extract the duplicated call-matching logic from errorLogged
and debugLogged into one private logLogged helper that accepts the spy and
needle, preserves the explicit boolean return type, and has both exported
functions delegate to it without changing behavior.

In `@test/test-utils/webhook-verify-helpers.ts`:
- Around line 9-23: The webhook test helper does not expose the HTTP status, so
the deleted-listing refund test cannot verify its promised 404 response. In
test/test-utils/webhook-verify-helpers.ts lines 9-23, update withWebhookVerify’s
assertions callback to receive res.status and pass it through; in
test/features/api/payment-processing/refunds.test.ts lines 164-182, update the
deleted-listing refund test callback to assert that status is 404 while
preserving the existing JSON assertion.

In `@test/ui/templates/admin/settings/custom-domain.test.ts`:
- Around line 19-20: Strengthen warning assertions in
test/ui/templates/admin/settings/custom-domain.test.ts:19-20 and
test/ui/templates/admin/settings/subdomain.test.ts:19-20 by asserting the
complete provider warning structure, including its linked provider settings
target. At test/ui/templates/admin/settings/custom-domain.test.ts:32 and
test/ui/templates/admin/settings/subdomain.test.ts:32, assert that both the
complete warning element and provider link are absent, using structural or exact
observable assertions rather than independent presence-only checks.

---

Outside diff comments:
In `@src/shared/db/settings.ts`:
- Around line 250-260: The Stripe activation flow must update PAYMENT_PROVIDER
and LAST_ACTIVE_PAYMENT_PROVIDER atomically. Modify Stripe’s activate() path to
use the shared setter or atomic mechanism that writes both settings together,
removing the separate PAYMENT_PROVIDER write while preserving
lastActivePaymentProvider’s empty-value and validation behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 5616ebe7-d126-4c16-86d4-71ab49e89259

📥 Commits

Reviewing files that changed from the base of the PR and between 5345a26 and 17a9483.

📒 Files selected for processing (16)
  • TODO.md
  • scripts/mutation/equivalent-mutants/shared-m-z.txt
  • src/features/admin/settings-page.ts
  • src/shared/db/client.ts
  • src/shared/db/settings.ts
  • src/shared/db/settings/raw-writes.ts
  • src/shared/payment-dashboard.ts
  • src/shared/payments.ts
  • src/ui/templates/admin/settings.tsx
  • test/features/api/payment-processing/refunds.test.ts
  • test/features/api/webhooks.test.ts
  • test/shared/db/settings/raw-writes.test.ts
  • test/test-utils/log-spy.ts
  • test/test-utils/webhook-verify-helpers.ts
  • test/ui/templates/admin/settings/custom-domain.test.ts
  • test/ui/templates/admin/settings/subdomain.test.ts

Comment thread test/features/api/webhooks.test.ts Outdated
Comment thread test/shared/db/settings/raw-writes.test.ts Outdated
Comment thread test/shared/db/settings/raw-writes.test.ts Outdated
Comment thread test/test-utils/log-spy.ts Outdated
Comment thread test/test-utils/log-spy.ts Outdated
Comment thread test/test-utils/webhook-verify-helpers.ts
Comment thread test/ui/templates/admin/settings/custom-domain.test.ts
Add comprehensive render-snapshot tests for custom-domain.tsx and
subdomain.tsx that assert every form field, action, label, CSS class,
and i18n-rendered text — killing ~46 string-literal and operator mutants.

Add a check-form test for subdomain.tsx covering the no-subdomain render
path (type=text, muted, Check button, suffix).

Remove stale equivalent-mutant entries: settings-page.ts:128:46 (line no
longer has ??) and settings-bundles.ts:247:40 (superseded by 248:40 in
shared-m-z.txt).

@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 `@test/ui/templates/admin/settings/custom-domain.test.ts`:
- Around line 47-63: Strengthen the assertions at
test/ui/templates/admin/settings/custom-domain.test.ts:47-63 by matching the
custom_domain input value tickets.example.com and verifying it is associated
with the custom-domain form. In
test/ui/templates/admin/settings/subdomain.test.ts:47-63, assert the hidden
subdomain control contains my-sub and belongs to the registration form. In
test/ui/templates/admin/settings/subdomain.test.ts:76-79, bind the
availability-check input and its control to the check form, replacing
independent presence-only assertions with form-scoped observable checks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: df80243a-a347-49a9-8b09-5debd683ebcf

📥 Commits

Reviewing files that changed from the base of the PR and between 17a9483 and 9c67112.

📒 Files selected for processing (4)
  • scripts/mutation/equivalent-mutants/shared-a-l.txt
  • scripts/mutation/equivalent-mutants/shared-m-z.txt
  • test/ui/templates/admin/settings/custom-domain.test.ts
  • test/ui/templates/admin/settings/subdomain.test.ts
💤 Files with no reviewable changes (2)
  • scripts/mutation/equivalent-mutants/shared-m-z.txt
  • scripts/mutation/equivalent-mutants/shared-a-l.txt

Comment thread test/ui/templates/admin/settings/custom-domain.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: 9c67112c19

ℹ️ 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 scripts/mutation/equivalent-mutants/shared-m-z.txt Outdated
refunds.ts (16/16 killed): direct unit tests for tryRefund,
validationFailure, refuseMismatch, refundSpec, refundedNoteText —
table-driven for REFUND_REASONS.

webhooks.ts (22/47 killed): Square orderId redirect, 409 concurrent
reservation, refunded-with-failed-refund, augmented log assertions.

custom-domain.tsx (8/8 killed): CSS-class, whitespace, and ??→||
assertions via regex-based render checks.

subdomain.tsx (15/15 killed): i18n keys, CSS classes, ??→||, &&→||,
!→∅, false→true, value→ via register+check form assertions.

raw-writes.ts (4/6 killed): recordSettingsLoaded and loaded.add
removal via audit-wrapped assertions.

Remaining 29 survivors: webhooks.ts complex processSessionAndRedirect
paths (multi-token URLs, thank-you), raw-writes loaded.add (not read
by getCachedRaw), client.ts false→true (transient-error retry).

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

🤖 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 `@test/features/api/payment-processing/refunds.test.ts`:
- Around line 314-322: Strengthen the non-404 case in the validationFailure test
by asserting an observable refund attempt outcome, not only refunded: false.
Reuse the existing errorLogged/errorSpy pattern for the unconfigured provider,
or configure a stub via stubStripeRefund and assert its specific result, so the
test distinguishes an attempted refund from skipping the non-404 branch.

In `@test/shared/db/settings/raw-writes.test.ts`:
- Around line 56-67: The test currently verifies only cache state and does not
confirm persisted deletion. Split the deletion behavior from the
audit-registration case, then in the focused `writeOrDelete` test invalidate the
cache, reload `CONFIG_KEYS.SQUARE_LOCATION_ID`, and assert
`settings.square.locationId` is empty while retaining the existing write/delete
setup.

In `@test/ui/templates/admin/settings/custom-domain.test.ts`:
- Around line 33-35: Remove the validation-action assertion from the
warning-suppression test and place it in the existing form-rendering test, or
create a dedicated test for it. Keep the current test focused exclusively on
verifying warning suppression, while preserving the validation action
expectation separately.
- Line 36: Replace the syntax-focused comments at
test/ui/templates/admin/settings/custom-domain.test.ts lines 36-36 and
test/ui/templates/admin/settings/subdomain.test.ts lines 33-34 with brief
behavior-focused statements that an empty configured provider suppresses the
warning; replace the comment at
test/ui/templates/admin/settings/subdomain.test.ts lines 44-45 with a brief
statement that the preview rendering path suppresses the warning.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 092d3bcf-ecdc-4709-9bf3-3fa7955060a5

📥 Commits

Reviewing files that changed from the base of the PR and between 9c67112 and b6dbcd7.

📒 Files selected for processing (5)
  • test/features/api/payment-processing/refunds.test.ts
  • test/features/api/webhooks.test.ts
  • test/shared/db/settings/raw-writes.test.ts
  • test/ui/templates/admin/settings/custom-domain.test.ts
  • test/ui/templates/admin/settings/subdomain.test.ts

Comment thread test/features/api/payment-processing/refunds.test.ts
Comment thread test/shared/db/settings/raw-writes.test.ts Outdated
Comment thread test/ui/templates/admin/settings/custom-domain.test.ts Outdated
Comment thread test/ui/templates/admin/settings/custom-domain.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: b6dbcd7b09

ℹ️ 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/payment-dashboard.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: aab800bcda

ℹ️ 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/payments.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: bdefe2d718

ℹ️ 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/admin/settings-helpers.ts Outdated
Comment thread src/features/admin/settings-domains.ts Outdated
…safety-behavior

# Conflicts:
#	flake.nix
#	test/shared/stripe/endpoints/cleanup.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: ff70865d8d

ℹ️ 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/webhooks.ts
Comment thread src/shared/db/client.ts Outdated
Comment thread src/ui/templates/admin/settings/payment.tsx
@stefan-burke stefan-burke changed the title Keep existing payments refundable when new sales are off Keep existing payments working when new sales are off Aug 4, 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: 909b396027

ℹ️ 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/settings/current-task.ts Outdated
Comment thread src/shared/db/settings.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: a2676cc71b

ℹ️ 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/settings.ts
Comment thread src/shared/db/settings.ts
Comment thread src/shared/db/settings.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: 8

🤖 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/features/admin/settings-helpers.ts`:
- Around line 143-150: The repeated settings-task execution and error mapping
should be centralized in a shared helper. In
src/features/admin/settings-helpers.ts lines 143-150, define the helper to
accept taskName, formId, errorPage, expected version, and the operation, then
return either the operation result or the form error page; update
src/features/admin/settings-helpers.ts lines 408-423 and
src/features/admin/settings-general.ts lines 104-117 to call it instead of
duplicating withCurrentTask handling. In src/features/admin/settings-domains.ts
lines 45-58, build runGuardedTask on the shared helper and retain only its
recovery-guard behavior.

In `@src/locales/en/settings.json`:
- Line 28: Update the rejection path in the database settings logic near the
payment-provider activation validation to use the existing
error.payment_provider_activation_requires_recovery catalog key through t("key")
instead of the duplicated hardcoded sentence, keeping the English catalog entry
as the single source of user-facing copy.

In `@src/shared/db/settings.ts`:
- Around line 216-218: Update the call to syncLastActivePaymentProvider in the
settings flow to pass values[CONFIG_KEYS.LAST_ACTIVE_PAYMENT_PROVIDER] directly,
removing the ?? "" fallback. Preserve the existing behavior that allows a
missing required row or value to throw instead of silently treating it as no
remembered provider.
- Around line 183-201: Update the SQL construction and argument assembly near
the settings update flow to use explicit named or indexed bindings for all 24
placeholders instead of relying on the flattened positional arrays in args.
Ensure every repeated value and credential key maps to the correct placeholder,
and alias repeated settings subqueries with descriptive AS names where
applicable. Preserve the existing query behavior and conflict-update semantics.

In `@src/shared/db/settings/current-task.ts`:
- Around line 51-57: Remove the unreachable Number.isInteger guard after
executeWithoutCacheInvalidation in the current settings-version flow, and read
the guaranteed first row value directly via rows[0].value when computing
currentVersion. Preserve the existing query and version handling without adding
defensive fallbacks.

In `@test/features/admin/settings-helpers/provider-credentials.test.ts`:
- Around line 128-143: The test hardcodes the string "Another task is already in
progress" in the expectFlash call in the "does not save credentials while
another settings task runs" test function when it should import this message
constant from the source. Export the error message constant (and the STALE_TASK
constant) from src/shared/db/settings/current-task.ts, then import and use these
exported constants in the expectFlash assertion here and also fix the duplicate
hardcoded literal in test/features/admin/settings-domains/custom-save.test.ts
line 110 to use the same imported constant, ensuring test failures when source
messages change.
- Around line 43-59: Add a test for the credential-save route using the post
helper with a stale settings_version, and assert routes.save returns the
stale-version message without calling saveSecret or saveFields. Keep the
existing current-version behavior and target /admin/settings/test-provider
rather than the payment-provider route.

In `@test/test-utils/csrf.ts`:
- Around line 27-36: Update inputTagWithValue to throw when no matching input is
found instead of returning an empty string, and include both value and name in
the error message. Preserve the existing matching behavior for successful
lookups.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 3d9b3446-afa0-47cb-87ba-dcfaa5554690

📥 Commits

Reviewing files that changed from the base of the PR and between bdefe2d and a2676cc.

📒 Files selected for processing (25)
  • TODO.md
  • src/features/admin/settings-domains.ts
  • src/features/admin/settings-general.ts
  • src/features/admin/settings-helpers.ts
  • src/features/admin/settings-stripe.ts
  • src/features/api/payment-processing/refunds.ts
  • src/locales/en/settings.json
  • src/shared/db/settings.ts
  • src/shared/db/settings/current-task.ts
  • src/shared/db/settings/raw-writes.ts
  • src/shared/existing-payment-provider.ts
  • src/shared/forms/csrf-form.tsx
  • test/features/admin/settings-domains/custom-save.test.ts
  • test/features/admin/settings-general/payment-provider.test.ts
  • test/features/admin/settings-helpers/provider-credentials.test.ts
  • test/features/admin/settings-page/last-active-provider.test.ts
  • test/features/admin/settings-stripe.test.ts
  • test/integration/server/settings/sensitive-field-masking.test.ts
  • test/shared/db/client/upstream-errors.test.ts
  • test/shared/db/settings/public-api.test.ts
  • test/shared/stripe/endpoints/connection.test.ts
  • test/specs/steps/payment-provider-choice.ts
  • test/test-utils/csrf-and-forms.test.ts
  • test/test-utils/csrf.ts
  • test/test-utils/session.ts
💤 Files with no reviewable changes (2)
  • test/shared/db/client/upstream-errors.test.ts
  • src/shared/db/settings/raw-writes.ts

Comment thread src/features/admin/settings-helpers.ts
Comment thread src/locales/en/settings.json
Comment thread src/shared/db/settings.ts
Comment thread src/shared/db/settings.ts
Comment thread src/shared/db/settings/current-task.ts Outdated
Comment thread test/features/admin/settings-helpers/provider-credentials.test.ts
Comment thread test/features/admin/settings-helpers/provider-credentials.test.ts
Comment thread test/test-utils/csrf.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: 550b6ce92f

ℹ️ 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/settings/registry.ts
Comment thread src/shared/payment-dashboard.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: 8fba391d7e

ℹ️ 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 on lines 403 to 404
path: string,
data: Record<string, string> = {},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Split the expanded session test helper below 400 lines

Adding the settings-version setup grows test/test-utils/session.ts from 519 to 529 lines, extending an already oversized test helper instead of moving adminFormPost and its related form-session helpers into a focused module. The repository applies its approximately 400-line limit to test files specifically to keep test and mutation scopes manageable.

AGENTS.md reference: AGENTS.md:L82-L82

Useful? React with 👍 / 👎.

@stefan-burke
stefan-burke added this pull request to the merge queue Aug 4, 2026
@stefan-burke
stefan-burke removed this pull request from the merge queue due to a manual request Aug 4, 2026
…t we cannot read (#2021)

* Reject malformed money and blank provider resource ids at the payment boundary

The provider boundary now refuses a charge whose amount is not a non-negative minor-unit integer or whose currency is not three letters, so every webhook and success callback reads a well-formed charge. A blank or whitespace-only provider resource id is refused at tryRefund, the one path every refund shares; a captured charge is still kept and surfaced, never dropped. Refund state is typed (none/completed/unknown), with "unknown" valid only for a legacy charge whose refund was never observed.

* Reject blank resource IDs and non-matching currencies at the boundary

Codex review on PR #2021 flagged two gaps in the provider boundary:

1. A paid session with a blank payment reference was accepted and persisted, but the reference was then excluded by getRefundPaymentReferences and refused by tryRefund, stranding the captured money. The boundary now rejects a paid session whose resource id is blank, rather than persisting an unrefundable charge.

2. A missing currency was defaulted to the site's currency (??), and a different valid currency was silently accepted. Both are now refused: a missing expected field is a hard no, and a non-matching currency would put the amount in the wrong unit for the site-currency proof.

* Address CodeRabbit findings on the provider boundary

- Fix the stale comment on Stripe session currency: the boundary rejects a missing currency, it does not settle it to the site's.
- Assert, rather than default to empty, the SumUp checkout currency; a missing expected field is a hard no.
- Validate the SumUp checkout reference before loading staged metadata, and handle a missing staged row instead of asserting non-null.
- Deduplicate the refund-state documentation (one concise statement of when unknown is valid).
- Annotate exported payment-session test helpers with explicit return types.

* Purge the codexSecurity python env var from the dev shell and fix the webhook fixture default

- Remove the undefined codexSecurityPython reference from flake.nix (upstream main added the export without defining the binding, which broke nix develop) and delete the env var entirely; the dev shell no longer exports a codexSecurity python path.
- checkoutSessionEvent now defaults payment_intent to a non-blank pi_<sessionId> so an omitted value still yields a processable paid session; pass null explicitly to exercise the boundary blank-reference rejection. Direct fixture tests live in dispatch.test.ts (a new file would reshuffle the suite's test groups and surface pre-existing isolation flakiness in guide schema and mutation-isolation coverage).

* Keep payment schemas private, prove the refund guard, and extract the session boundary

Codex re-review (P1) on PR #2021:

- CurrencySchema/MoneySchema and ResourceIdSchema were exported only for their own tests; they are now module-private and the tests exercise the money() and isResourceId() production interfaces.
- The resource-id module doc no longer compares the current guard with the old per-provider parsing.
- The tryRefund blank-reference test now configures a provider and spies on refundPayment/retrievePaymentIntent, so it proves the guard fires before any provider call rather than merely returning false because no provider was resolvable.
- validatedPaymentSession moves from the 800-line payment-helpers.ts into src/shared/payment/validated-session.ts; its test moves to the mirror location.

* Validate the raw SumUp amount before rounding to minor units

toSumupCheckout previously passed the provider's major-unit amount through toMinorUnits (which rounds) before the payment boundary validated it, so an over-precise charge could round to a value that matches the signed total. The raw amount is now checked against the currency's precision first and fails loudly. Regression test: a GBP checkout with a three-decimal amount resolves to no session.

* Bind the SumUp staged record to the webhook id and address CodeRabbit re-review

- resolveWebhookSession now requires the staged record's sumupId to equal the webhook id, so a provider response naming a different staged checkout cannot process the wrong booking; regression test stages two checkouts and a cross-reference.
- RefundState is defined from a valibot picklist schema, matching the repository's schema-first value-type pattern.
- The balanceSession fixture drops its unchecked double cast (created/url filled in, meta narrowed to string values).
- The blank-reference webhook test asserts no processed-payment row is persisted.
- The createdAt assertion now checks the constructed session directly; the fixture-only checkoutSessionEvent tests are removed (the default is exercised through the production webhook path).

* Route wrong-currency paid charges into the existing mismatch refund path

Codex P1: a paid charge in a different valid currency with a usable reference was rejected at the boundary, so the webhook acked it as unrecognized and the captured money was neither booked nor refunded.

The session now carries its validated currency, and classifySession treats a non-site currency as a price mismatch, so the charge flows through the existing mismatch -> refund -> alert machinery instead of being dropped.

* Refund paid charges the boundary cannot read, via a typed rejection

A paid charge in an unusable shape (wrong currency, over-precise SumUp amount, malformed amount/currency) previously made resolveWebhookSession return null, so the webhook acked it as unrecognized and the captured money was stranded.

validatedPaymentSession now returns a typed rejection that carries the payment reference and whether it is refundable; the providers surface it, and the webhook, redirect, and cancel paths refund a refundable rejected charge through tryRefund before acknowledging. SumUp flags over-precise amounts instead of throwing, so those reach the refund path too.

Also: RefundStateSchema is private, the resource-id test comment no longer compares with old parsing, and the sumup-provider ?? -> || equivalents were removed (falsy non-string ids differ) and are now pinned by a test.

* Convert SumUp charges in the checkout's own currency

toSumupCheckout converted amounts and checked precision against the site currency, so a valid foreign-currency checkout (e.g. JPY, no minor places) was rounded or flagged against the wrong decimal places. The checkout currency is now normalized once and passed to toMinorUnits and exceedsCurrencyPrecision; both helpers default to the site currency for existing callers. Regression test: a one-decimal JPY amount converts and flags against JPY, not GBP.

* Retry failed rejection refunds, verify ownership, and validate SumUp currency presence

CodeRabbit + Codex re-review (9 findings):

- refundRejectedCharge now returns the refund outcome, and a rejected-charge refund the provider refuses is answered 503 (retryable) by the webhook and a retryable error by the redirect/cancel paths instead of a 200 ack that strands the charge.
- The malformed_charge rejection carries the session metadata, and the refund is issued only when its price proof verifies, so a session belonging to another instance sharing the provider account is not refunded.
- isSessionRejection validates the exact variants and fields.
- SumUp: a missing or blank checkout currency is carried as null (never asserted away inside withClient) so the boundary refuses it as a refundable malformed charge; currency conversion and precision checks take the checkout currency.
- SessionBuild is module-private; stale providerRefunded comment updated; asSession(result!) simplified; the accidental npm:biome lock resolution (566 lines) removed and the bare specifier removed from the deno-command test.

* Keep a malformed SumUp currency out of the currency helpers

SumUp answering with a currency that is not a real three-letter code
(say "GB") made the checkout fetch throw: the code went straight to
Intl.NumberFormat for the minor-unit conversion and the precision check,
and Intl rejects it. The client wrapper caught that throw and turned it
into "no session", so the webhook acknowledged a paid charge as
unrecognized and the captured money was neither booked nor refunded.

The adapter now converts with the site's currency whenever the provider
code is not well-formed, and carries the raw code through so the payment
boundary refuses the charge and the callback refunds it — the same route
a missing currency already took. The guard is the boundary's own
currency schema, exposed as isCurrency, so a well-formed code has one
definition.

Also strengthens two assertions review asked for: the webhook 503 test
proves the refund was attempted on the captured charge, and the
currency-fallback tests assert the whole normalized checkout.

Splits the two SumUp suites, which were over the 400-line guide, into
mirror directories, with their fake client, fixtures, and suite setup in
one #test-utils module.

* Cover the currency guard directly

isCurrency is a new exported guard; give it its own table of well-formed
and malformed codes rather than leaning on the SumUp adapter's tests.

* Carry a currency on the merged webhook fixture

The base branch added a webhookEvent test fixture after this branch had
updated the Stripe and Square fixtures to carry a currency, so every
session it built was refused at the payment boundary once the two met.
Give it the site currency, lower-cased the way Stripe sends it.

The base's redirect-path refund suite and this branch's rejected-charge
unit suite both landed at test/features/api/payment-processing/refunds
.test.ts; they test different things, so the unit one moves to
refunds/rejected-charge.test.ts and both stay under the line guide.

* Close mutation gaps on the payment sources this layer touches

Adds a direct table for parsePriceProof (moved into payment-signature.ts
by this layer but only covered indirectly), a '-0' row proving the
non-negative money parser refuses a signed zero, the sub-minor-unit sign
cases for formatSignedCurrency, and the SumUp provider's webhook contract
assertion its Stripe and Square siblings already had.

Records two proven-equivalent mutants: the canonical key sort's -1/1
return (sort() only tests for a strictly negative result — checked
against all 8! permutations and 3,000 random arrays) and the positive
money schema's signed flag (a leading minus only admits values at or
below zero, which minValue(1) rejects either way). Refreshes the webhook
and SumUp ignore-list line numbers the merge shifted.

* Refresh provider ignore-list line numbers the merge shifted

* Bring every source this layer touches to a 100% mutation kill

Stripe's provider adapter now hands the boundary the currency exactly as
Stripe sent it, rather than turning an absent one into null on the way in;
the boundary already refuses absent, blank, and malformed alike, so the
extra defaulting only hid which of them arrived.

New tests for the gaps the runs found: a paid Stripe session with no
payment intent is refused as a blank reference, a one-character session id
is still looked up, both webhook-endpoint states parse and an unknown one
does not, a normal booking does not pick up the quantity-0 placeholder's
note, and an unrelated system note survives the stale-note cleanup.

Splits the Stripe provider's webhook tests into a mirror directory, and
folds the two 503 webhook posts in the refund-skip suite onto one helper.

* Stop three more captured charges dying inside the client wrapper

Same fault as the malformed SumUp currency, in three more places. Each
reached into provider data that may not be there, from inside the wrapper
that turns any error into "no session" — so the webhook acknowledged a
paid charge as one it had never heard of and the money stayed taken.

- A SumUp checkout with no amount: the precision check called toFixed on
  undefined. The amount is carried as null for the boundary to refuse.
- A Square order with no money object: retrieveOrder asserted its way
  into it. Both halves are carried as null instead, and the provider
  keeps a missing amount missing rather than letting Number(null) read as
  a real free order.
- A Square rejection carried the packed wire record. The price proof is
  signed over the unpacked shape, so the rejection's own ownership check
  could never pass and a real Square charge would never be refunded. The
  rejection now carries the same canonical metadata the success path does.

Also stubs the Stripe refund in the rejected-charge tests instead of
spying it, so those refunds are answered here rather than leaving for
Stripe, and folds their shared scaffolding onto one curried helper.

Records the remaining review point — writing a durable "refused and
refunded" outcome for the session — in TODO.md with why it is not a live
fault today.

* Close the mutation gaps the Square money change opened up

square.ts is in this branch's diff now, so its survivors are ours. Four
were in the refund response's boundary schema, which no test could
distinguish because every malformed fixture was missing several fields at
once: a blank refund id, a blank payment id, and a blank currency are now
each refused on their own, and a zero refund amount is shown to parse and
be caught by the amount check instead. A fifth was the log written when
Square refuses a refund — the only record of it, since the caller just
sees false.

The two new nulls needed their own cover: a zero total and a blank
currency are Square's real values and are carried through untouched, so
only a wholly absent money object reads as null.

* Say plainly when a buyer's money went back

Two things were reading the same for a charge we refunded and a charge
there was never anything to refund.

The operator log said "refunded: true" either way, including for a
blank-reference rejection where no provider call was ever made. The
outcome now carries both facts apart: settled (nothing left owing, which
decides the retry) and refunded (money actually moved, which is what gets
logged).

The buyer got "Payment session not found" even when they had really been
charged and we had just sent the money back — an invitation to wait for a
ticket that will never come, or to pay a second time. They are now told
what happened, and that it takes a few days.

Both callbacks answer a rejected session the same way, so they share one
helper rather than two copies of refund-log-respond.

Separately, a SumUp checkout that comes back under a reference we never
staged is now raised as an error instead of returning quietly. The
booking is encrypted under that reference, so an unmatched one leaves us
unable to read it or prove the charge is ours to refund — which is
exactly when someone needs to know.

* Configure Stripe's key, not just its name, in the refund tests

The base branch made an existing payment resolve only through a provider
this site still holds credentials for, so naming stripe as the current
provider no longer reaches it — every refund in this suite silently found
none and returned false. The helper now sets the key alongside the choice.

* Refuse a provider resource id with space around it

" pi_123 " contains text, so the guard accepted it — and the id is stored
and sent back to the provider exactly as it arrived, so the session was
booked as refundable and then matched no charge on every refund attempt.
The id must have no space around it; padding is refused rather than
trimmed, since trimming guesses at what the provider meant.

Records the other review point — telling a buyer whose money was taken
and not given back, which the refunded page does not yet cover — in
TODO.md, with the three outcomes it applies to and the one it must not.

* Use the payment id Square signed, not just the order's tenders

A payment.updated webhook carries a payment id Square has verified and
reported COMPLETED, and the session build threw it away — it read the
reference off order.tenders instead. Square's tenders can lag the payment,
so a completed payment on an order whose tenders had not caught up read as
unpaid: acknowledged as pending with the money taken, or, once its money
was also unreadable, refused as a charge with nothing to refund.

retrieveSession now takes that already-known payment as an optional second
argument, on the shared provider interface rather than Square's copy of
it: any provider whose session lags its payment has the same problem, and
the ones that do not simply ignore it.

* Let the webhook's payment win over an earlier tender

Preferring the order's first tender only helped when there was no tender
at all. An order can already carry a tender for an earlier payment, and
the webhook names the one Square has just reported COMPLETED — so that is
the charge this session records, and the one a refund has to reach.

Also asserts the payment lookup happened before reading its arguments, so
a regression reports the missing call rather than a TypeError, and notes
on the deferred buyer-message work that a 503 only asks for another
*webhook* delivery: the redirect and cancel paths have no retry behind
them, so an unresolved refund has to be written down before anyone is
told it is in hand.

* Work through the review comments I had closed unread

Ten threads had been resolved by a sweep without ever being read. Going
back through them:

- refundRejectedSession and RejectionOutcome had no production caller
  outside their own module, so they were public only for tests. Both are
  private now, and those tests go through answerRejectedSession — which
  asserts the buyer's page and status rather than an internal shape.
- The "not found" wording is a catalog entry rather than a literal
  repeated across three call sites.
- The refund copy is active voice: "we did not make a booking", "we have
  sent your money back".
- The SumUp normalization comment was thirteen lines narrating each
  branch; it is four, keeping only the withClient constraint that is not
  visible from the code.
- The refund-schema tests assert which field Valibot refused, not just
  that something threw — an unrelated fault would have passed before.
- Non-null assertions I added are early-return narrows, a stale comment
  about "no session" describes the blank_reference rejection it actually
  asserts, two call-argument reads assert the call happened first, and
  the classify test pins the reference the refund was issued for.
- square-provider.test.ts was 718 lines; its webhook cases move to the
  mirror directory, with the fixtures and suite hooks both files were
  about to duplicate lifted into the shared Square test utils.

* Refuse whitespace anywhere in a provider resource id

"pi 123" was accepted, and a test of mine asserted it should be. It never
can be: the id goes back to the provider exactly as it arrived, so an id
with a space in it books the session as refundable and then fails every
refund — leaving the webhook retrying for good, which is worse than
refusing it at the boundary. The whole id must be unbroken text now.

Also from review: the payment-session boundary's eighteen-line comment
narrated the branches below it and is four lines saying what the code
cannot; "payment session not found" is a full sentence in the buyer's
voice; the two callback tests hand their result back from the helper
instead of a definite-assignment binding; and the Square webhook test
asserts the whole order-lookup call list rather than its first entry.

* Validate what Square took, and retry when it cannot be read back

Two faults in the same lookup, both leaving a captured charge stranded.

The webhook already saw Square report a payment COMPLETED, but completion
was then re-derived solely from a second retrievePayment call. When that
call failed transiently the status fell back to "unpaid" and the webhook
acknowledged the charge as pending with a 200, so Square never delivered
it again. A failed read-back of a payment the webhook verified now throws,
which answers the caller retryably instead of going quiet.

The money came from order.totalMoney — what was asked for, not what was
taken. A partial payment therefore matched the order's signed price and
booked as paid in full. Square's own figure for the payment is used
whenever it names one, so a short charge is refused by the boundary and
refunded.

Simplifies to stay inside the source budget, none of it cosmetic: the
SessionBuild wrapper and sessionOrRejection did the job isSessionRejection
already does, so validatedPaymentSession returns the session or the
rejection directly and three provider adapters stop unwrapping it;
refundRejectedSession was a one-line private wrapper with a single caller
and is inlined; and four comment blocks that narrated the code below them
are cut to what the code cannot say.

* Assert the flat session shape in the dispatch test too

A second validatedPaymentSession test lived in the payment-helpers suite
and still expected the { ok, session } wrapper that this branch removed.
CI caught it; my targeted run had not reached that file.

* Close three boundary gaps the contract audit found

Auditing all three adapters against one written contract — trusted signed
facts versus observed provider facts, and what a completed result must
carry — turned up two providers each missing a guard the other already
had, plus one place expected money still stood in for observed money.

Square never checked that the payment it fetched belongs to the order it
is about to book. Square's two records disagreeing meant booking one
order's signed metadata against another payment's money. It now skips,
the way SumUp already does when a checkout comes back under a reference
that is not the one staged for it. The payment's own order is left alone,
so its real webhook still books it.

SumUp acknowledged a staged checkout it could not fetch. The staging row
had already proved the checkout was ours, so a failed read is SumUp being
unreachable, not a checkout we never made — and a 200 is terminal, so a
paid one was stranded. It now throws, the way Square already does for a
payment it cannot read back.

A completed Square payment that named no amount fell back to the order
total, which is what was asked for rather than what was taken — the same
hazard as a partial charge, reached a different way. Observed money is
now required exactly when money was observed to move; before that the
order total is all there is and nothing has been captured against it.

The COMPLETED payment fixtures gain the amountMoney real Square always
sends, and one test named for the order total is renamed for what it
now proves.

* Fold SumUp's over-precise flag into an unreadable amount

overPrecise was an optional field that changed what amountMinor meant:
when set, the number beside it was a lie. It also gave the SumUp adapter
its own route to a rejection, in parallel with the boundary that decides
this for every other provider.

An amount finer than its currency can hold is simply an amount we cannot
read, so it is now carried as null like an absent one, and the one
boundary refuses both. Behaviour is identical — same reference, same paid
flag, same refundable rejection — and the refusal is now logged, which the
adapter's own path never did.

Drops the field, its branch, and a table row that had become a duplicate
of the no-amount case.

* Retry a Square payment whose order contradicts the signed event

I had this one wrong. The mismatch was treated as "this payment belongs to
another order, whose own webhook will book it", so it skipped — and skip is
a 200, which is terminal. But the signed event that sent us here had
already linked the payment to this order, so a lookup naming a different
one is Square disagreeing with itself about money it has taken, not a
routine delivery for somebody else. There may be no second webhook coming.

Neither answer available at that point is safe: booking uses metadata the
payment may not belong to, and acknowledging strands a captured charge.
Throwing keeps it retryable until Square answers consistently, which is
what the other two contradictions in this file already do.

* Retry a Square payment that does not read back as completed

A signed webhook reports a payment COMPLETED, but the follow-up read can come
back missing or still saying APPROVED. Either way the session was built as
unpaid, and the webhook then acknowledged captured money as pending, so Square
had no reason to send it again.

Both readings are Square lagging its own signed event, so they now share one
guard and throw, keeping the delivery retryable until Square agrees with itself.

Also condenses the money builder's comment to the part the code does not
already say, and records the foreign-currency money history finding in TODO.md:
filing it honestly needs a currency on the accounting store itself.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Retry a SumUp checkout that comes back under the wrong reference

The staging row already proved this checkout is ours, so a reference SumUp
answers with that is blank, unknown, or another booking's is SumUp
contradicting itself. The code logged that and returned null, which the webhook
acknowledged as unrecognized, so a paid checkout was left with the money taken,
no booking, and no refund. The comment beside it already said to raise it.

It now throws, keeping the delivery retryable. The booking is encrypted under
that reference, so without a match there is nothing we can read or prove is
ours to refund — there is no safe way to answer this one quietly.

Also drops the export from malformedChargeRejection, whose only caller is in
its own module, refreshes the SumUp header now that a checkout converts in the
currency SumUp returned, and condenses the resource-id comment.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Condense the rejected-session callback comment

Keeps the retry constraint, which the code does not say for itself, and drops
the narration of the refund, the log line, and the buyer's message.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@stefan-burke
stefan-burke enabled auto-merge August 4, 2026 17:56
@stefan-burke
stefan-burke added this pull request to the merge queue Aug 4, 2026
Merged via the queue into main with commit a3f8dec Aug 4, 2026
3 checks passed
@stefan-burke
stefan-burke deleted the payment-aggregate/01-safety-behavior branch August 4, 2026 18:04

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

ℹ️ 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 on lines +76 to +77
if (rejection.reason === "blank_reference" || !rejection.refundable) {
return NOTHING_TO_REFUND;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep paid sessions retryable when the charge reference is blank

When SumUp reports a checkout as PAID before returning its optional transaction ID, validatedPaymentSession produces blank_reference, but this branch marks the charge settled. The webhook then acknowledges it at webhooks.ts:426-427, permanently leaving the buyer charged without a booking or refund. A blank reference on this rejection means the expected provider field is unavailable, not that no money was captured; keep it unsettled so the webhook returns 503 and retries until the reference can be read.

AGENTS.md reference: AGENTS.md:L118-L127

Useful? React with 👍 / 👎.

},
);
});
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Split the Square webhook suite below 400 lines

This newly added test file is 434 lines, already exceeding the repository's approximately 400-line limit for tests. Split the webhook session, payment readback, and event-shape cases into narrower suites so Square mutants do not need to run against another oversized test file.

AGENTS.md reference: AGENTS.md:L82-L82

Useful? React with 👍 / 👎.

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