Skip to content

Accept only confirmed Square refund outcomes - #1911

Merged
stefan-burke merged 22 commits into
mainfrom
split/confirmed-square-refunds
Jul 25, 2026
Merged

Accept only confirmed Square refund outcomes#1911
stefan-burke merged 22 commits into
mainfrom
split/confirmed-square-refunds

Conversation

@stefan-burke

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

Copy link
Copy Markdown
Member

Square refunds previously reported success the moment the POST to /v2/refunds returned an OK response, even when the refund was still PENDING. That counted a half-finished refund as done. This PR makes the contract honest and addresses all three merge blockers from the independent review.

What changed

refundPayment now parses the refund object Square returns through a Valibot boundary schema and reports success only for a Square-documented COMPLETED status:

  • COMPLETEDtrue (the money has been returned to the buyer).
  • PENDING, REJECTED, FAILED, or any other status → false.
  • A 200 response that is missing its refund object, has an empty or non-string id/statusthrows loudly (Valibot v.parse runs outside withClient, so the malformed response is visible instead of silently normalized to false). The schema matches Square's own PaymentRefund docs: id is Required with Min Length 1, status is a string.

APPROVED was removed — it is in Square's RefundStatus enum page but NOT the PaymentRefund object documentation (which lists PENDING/COMPLETED/REJECTED/FAILED). SUCCEEDED was also removed (not a Square status).

Blocker 1 — PENDING placeholder stranding (fixed, not deferred)

Traced store-refund.tsrefund-ledger.tsattendees-edit.ts: a PENDING refund returned false, which made storeRefundedBooking terminal with only the payment leg (no refund_cash). The operator could not refresh because loadRefreshContext filtered quantity > 0 (excluding quantity-0 placeholders), and computeAttendeeRefund skipped them as unbalanced. Fixed:

  • loadRefreshContext: NOW prefers quantity > 0 but falls back to quantity-0 placeholders via ORDER BY (quantity > 0) DESC — so a placeholder can be refreshed. Uses listing_id from the booking row (no requireListingWithCount call), so a deleted listing does not 404 the refresh.
  • computeAttendeeRefund: NOW allows reversal for placeholder accounts (payment legs only, no sale legs) regardless of balance — the refund_cash leg simply returns the payment, balance zeroes cleanly.
  • handleRefreshPayment: refreshed to use listingId (from the booking) instead of the full listing object.

When a PENDING Square refund later settles (COMPLETED), the operator clicks "refresh payment" → isPaymentRefunded returns true (money moved) → recordAttendeeRefundcomputeAttendeeRefund detects the placeholder (no sale legs) → posts the refund_cash leg → ledger reconciled.

Stable idempotency + downstream

Uses refundIdempotencyKey("square", paymentId) from #1912 (the stable-refund-keys sibling job). Downstream, a false is handled by the existing tryRefund fallback (isPaymentRefunded); the stable key prevents a double payout on redelivery. The PENDING-as-pending union (carrying the refund id instead of collapsing to false) is the staged-checkout/callback work recorded in TODO.md against #1853 and #1905.

Tests

Split retrieve-refund.test.ts (481→240 lines) and rest-transport.test.ts (429→372) by extracting:

  • refund-payment.test.ts (247 lines) — status matrix (COMPLETED→true, PENDING/REJECTED/FAILED/APPROVED/SUCCEEDED/unknown→false), Valibot boundary throws (missing refund, empty id/status, non-string fields, minLength boundary cases).
  • refund-transport.test.ts (123 lines) — raw response return for the boundary validator.
  • test/lib/square/mock-fetch.ts — shared jsonResponse/installMockFetch helper.

Exhaustive mutation on square.ts is 100% (284 killed, 2 genuine equivalents suppressed). Verified with nix develop -c deno task precommit (typecheck, lint, cpd, build, full test suite) on a branch rebased onto main (#1909/#1910/#1912).

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Square refund handling now parses refund responses and confirms success only for approved statuses with required fields. Tests cover response mapping, status outcomes, and malformed data; mutation records, assertions, fixtures, and follow-up TODOs are also updated.

Changes

Square refund confirmation

Layer / File(s) Summary
Refund response contract and transport
src/shared/square.ts, test/shared/square/rest-transport.test.ts
Square refund responses are typed and returned with refund identifiers and statuses, including null when absent; REST tests verify these shapes.
Refund confirmation behavior and contract tests
src/shared/square.ts, test/shared/square-provider/provider.test.ts, test/shared/square/retrieve-refund.test.ts
Refund operations validate required refund data and confirmed statuses, log malformed responses, and cover successful, pending, failed, unknown, and incomplete outcomes.
Square test precision and mutation audit records
TODO.md, scripts/mutation/equivalent-mutants.txt, test/shared/square/{client,payment-link-validation,webhook}.test.ts
Exact test expectations, signature fixtures, equivalent-mutant entries, and stale-entry remediation notes are updated.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

🚥 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 main change: Square refunds now count as successful only for confirmed outcomes.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch split/confirmed-square-refunds
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch split/confirmed-square-refunds

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

@stefan-burke
stefan-burke force-pushed the split/confirmed-square-refunds branch from e6d96be to 5a795fa Compare July 24, 2026 13:56

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

ℹ️ 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/square.ts Outdated
Comment thread src/shared/square.ts Outdated
stefan-burke added a commit that referenced this pull request Jul 24, 2026
Codex review pointed out that returning false for a refund response missing its object/id/status silently swallowed a Square contract break as an ordinary not-yet-refunded payment — the codebase's offensive-programming rule says a missing documented field is a hard no that must fail at the boundary. refundPayment now throws when the 200 response lacks a refund object, id, or status; withClient contains the throw into a logged false, so the bad response is diagnosable instead of silently false. PENDING/FAILED/unknown statuses still return false silently (only a present-but-not-confirmed status is a normal not-yet-refunded outcome). Adds a test asserting the boundary error log; the existing fails-safely tests still hold (the throw is contained to the same false outcome). The pending-result union Codex also suggested (propagate a pending refund id instead of false) is the staged-checkout/callback work this PR was told not to introduce and is recorded in TODO.md against #1853/#1905.

Refreshed the surviving equivalent-mutant line numbers in equivalent-mutants.txt (the helper removal shifted result ?? false and locations ?? []).

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

ℹ️ 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/square.ts Outdated
stefan-burke added a commit that referenced this pull request Jul 24, 2026
Codex follow-up (thread 3): the boundary throw lived inside withClient, which catches non-PaymentUserError exceptions and returns null, so it was architecturally the same as a logged false — an indirect throw-and-catch rather than a real loud failure. Replaced it with an explicit logError + return false, matching the missing-amount case just above it: the malformed response is still diagnosed at the boundary under E_SQUARE_REFUND and still fails safely to false (never reports a refund that was never confirmed), now without the swallowed-throw indirection. Propagating the throw outside withClient (a loud/errored failure) would change the contract from fail-safely-contained to fail-loudly, which is the per-path staged-checkout/callback resolution this PR was told not to introduce and is recorded in TODO.md against #1853 and #1905. Exhaustive mutation on square.ts is now 100% (286 killed, 3 genuine equivalents suppressed).

@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 (1)
src/shared/square.ts (1)

638-675: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Do not treat an accepted PENDING refund as a retryable failure.

Line 675 returns false for PENDING; downstream callers interpret that as failure. The documented flow then releases the reservation, returns 503, and retries the full refund with a new idempotency key rather than resolving the existing refund ID. Preserve a distinct pending outcome or otherwise make redelivery query/reuse the accepted refund before this behavior ships.

🤖 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/square.ts` around lines 638 - 675, Update refundPayment and its
callers so an accepted Square PENDING refund is represented distinctly from
FAILED or malformed refunds; ensure redelivery queries or reuses the existing
refund ID rather than releasing the reservation and retrying with a new
idempotency key. Preserve true only for CONFIRMED_REFUND_STATUSES and keep
malformed responses fail-safe.
🤖 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.txt`:
- Line 897: Update the equivalence rationale for the src/shared/square.ts
callback mutation to mention that pending, unknown, and malformed refunds now
return false, and that false ?? false and false || false both evaluate to false.
Preserve the existing rationale for true results and null error or
missing-client cases.

In `@src/shared/square.ts`:
- Around line 235-242: Update the SquareRefundResponse definition and refund
POST handling to use a Valibot schema as the source of truth, with an optional
refund object whose id and status fields are optional strings. Parse and
validate the response before mapping or applying the unconfirmed-refund
fallback, rejecting malformed JSON and invalid field types rather than relying
on the post<SquareRefundResponse> cast.

In `@test/shared/square/retrieve-refund.test.ts`:
- Around line 421-422: Update the comment preceding the malformed-response
assertion in the refund test to state that the implementation logs the malformed
response and returns false. Remove the inaccurate claim that withClient throws
or that the error is silently handled, while preserving the existing assertion
and test behavior.

---

Outside diff comments:
In `@src/shared/square.ts`:
- Around line 638-675: Update refundPayment and its callers so an accepted
Square PENDING refund is represented distinctly from FAILED or malformed
refunds; ensure redelivery queries or reuses the existing refund ID rather than
releasing the reservation and retrying with a new idempotency key. Preserve true
only for CONFIRMED_REFUND_STATUSES and keep malformed responses fail-safe.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a17e7d78-23d3-49ef-b290-64e078b98bc2

📥 Commits

Reviewing files that changed from the base of the PR and between e6d96be and 71c42ba.

📒 Files selected for processing (9)
  • TODO.md
  • scripts/mutation/equivalent-mutants.txt
  • src/shared/square.ts
  • test/shared/square-provider/provider.test.ts
  • test/shared/square/client.test.ts
  • test/shared/square/payment-link-validation.test.ts
  • test/shared/square/rest-transport.test.ts
  • test/shared/square/retrieve-refund.test.ts
  • test/shared/square/webhook.test.ts

Comment thread scripts/mutation/equivalent-mutants.txt Outdated
Comment thread src/shared/square.ts Outdated
Comment thread test/shared/square/retrieve-refund.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: 71c42ba0b5

ℹ️ 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/square.ts Outdated
Comment thread src/shared/square.ts Outdated
Comment thread src/shared/square.ts Outdated
stefan-burke added a commit that referenced this pull request Jul 24, 2026
…1911 review)

CodeRabbit noted the recorded proof for the result ?? false equivalent mutant predated the new false return path. The callback now returns true (confirmed), false (pending/failed/unknown status, or a malformed response), or null (missing client or a caught error); true stays true and both false and null fall through to false under ?? and ||, so the operators still produce the same boolean. The rationale now states that.
stefan-burke added a commit that referenced this pull request Jul 24, 2026
CodeRabbit and Codex flagged that Square's RefundStatus enum includes APPROVED (the current terminal success state for a refund), which CONFIRMED_REFUND_STATUSES omitted — a refund Square approved would have been wrongly treated as not-confirmed. Added APPROVED alongside COMPLETED and SUCCEEDED (kept per the job spec as defensive aliases), with a regression test. Confirmed APPROVED against Square's current API reference. Also corrected the now-stale comment in the malformed-response test (it no longer throws; it logs under E_SQUARE_REFUND and returns false).

Exhaustive mutation on square.ts is still 100% (288 killed, 3 genuine equivalents suppressed; line numbers refreshed).
coderabbitai[bot]
coderabbitai Bot previously requested changes Jul 24, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (2)
src/shared/square.ts (2)

643-649: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Document APPROVED as a confirmed refund status.

The implementation returns true for APPROVED, but this JSDoc lists only COMPLETED and SUCCEEDED. Update the comment so the documented contract matches the allowlist and tests.

🤖 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/square.ts` around lines 643 - 649, Update the JSDoc for the refund
method near the full-refund implementation to include APPROVED alongside
COMPLETED and SUCCEEDED as confirmed statuses, keeping the existing descriptions
of pending, failed, unknown, and malformed responses unchanged.

Source: Coding guidelines


661-680: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Do not collapse an accepted PENDING refund into plain false without updating downstream handling.

Line 680 makes an unsettled refund indistinguishable from failure. The supplied TODO states that webhook/admin paths release the reservation, return 503, and retry with a fresh idempotency key instead of polling the existing refund id. Propagate a pending outcome carrying refund.id, or update those callers in the same change.

🤖 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/square.ts` around lines 661 - 680, Update the refund handling
around the refund-status check in the payment refund method so an accepted
PENDING refund is propagated as a distinct pending outcome carrying refund.id,
rather than returned as plain false. Update all downstream webhook/admin callers
to recognize that outcome, retain the reservation, return 503, and retry using
the existing refund id instead of treating it as a failed refund.
🤖 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.txt`:
- Around line 896-898: Resolve the target-branch content conflict in
scripts/mutation/equivalent-mutants.txt, preserving the correct
equivalent-mutant entries from both branches without duplicate or stale records.
Then rerun the merge check and mutation audit to confirm the file is clean and
consistent.

---

Outside diff comments:
In `@src/shared/square.ts`:
- Around line 643-649: Update the JSDoc for the refund method near the
full-refund implementation to include APPROVED alongside COMPLETED and SUCCEEDED
as confirmed statuses, keeping the existing descriptions of pending, failed,
unknown, and malformed responses unchanged.
- Around line 661-680: Update the refund handling around the refund-status check
in the payment refund method so an accepted PENDING refund is propagated as a
distinct pending outcome carrying refund.id, rather than returned as plain
false. Update all downstream webhook/admin callers to recognize that outcome,
retain the reservation, return 503, and retry using the existing refund id
instead of treating it as a failed refund.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 9ecc072a-c318-4241-a6a8-3f73ec99c0a9

📥 Commits

Reviewing files that changed from the base of the PR and between d75c7dd and 99c3d5a.

📒 Files selected for processing (4)
  • TODO.md
  • scripts/mutation/equivalent-mutants.txt
  • src/shared/square.ts
  • test/shared/square/retrieve-refund.test.ts

Comment thread scripts/mutation/equivalent-mutants.txt Outdated
stefan-burke added a commit that referenced this pull request Jul 24, 2026
Codex review pointed out that returning false for a refund response missing its object/id/status silently swallowed a Square contract break as an ordinary not-yet-refunded payment — the codebase's offensive-programming rule says a missing documented field is a hard no that must fail at the boundary. refundPayment now throws when the 200 response lacks a refund object, id, or status; withClient contains the throw into a logged false, so the bad response is diagnosable instead of silently false. PENDING/FAILED/unknown statuses still return false silently (only a present-but-not-confirmed status is a normal not-yet-refunded outcome). Adds a test asserting the boundary error log; the existing fails-safely tests still hold (the throw is contained to the same false outcome). The pending-result union Codex also suggested (propagate a pending refund id instead of false) is the staged-checkout/callback work this PR was told not to introduce and is recorded in TODO.md against #1853/#1905.

Refreshed the surviving equivalent-mutant line numbers in equivalent-mutants.txt (the helper removal shifted result ?? false and locations ?? []).
stefan-burke added a commit that referenced this pull request Jul 24, 2026
Codex follow-up (thread 3): the boundary throw lived inside withClient, which catches non-PaymentUserError exceptions and returns null, so it was architecturally the same as a logged false — an indirect throw-and-catch rather than a real loud failure. Replaced it with an explicit logError + return false, matching the missing-amount case just above it: the malformed response is still diagnosed at the boundary under E_SQUARE_REFUND and still fails safely to false (never reports a refund that was never confirmed), now without the swallowed-throw indirection. Propagating the throw outside withClient (a loud/errored failure) would change the contract from fail-safely-contained to fail-loudly, which is the per-path staged-checkout/callback resolution this PR was told not to introduce and is recorded in TODO.md against #1853 and #1905. Exhaustive mutation on square.ts is now 100% (286 killed, 3 genuine equivalents suppressed).
stefan-burke added a commit that referenced this pull request Jul 24, 2026
CodeRabbit and Codex flagged that Square's RefundStatus enum includes APPROVED (the current terminal success state for a refund), which CONFIRMED_REFUND_STATUSES omitted — a refund Square approved would have been wrongly treated as not-confirmed. Added APPROVED alongside COMPLETED and SUCCEEDED (kept per the job spec as defensive aliases), with a regression test. Confirmed APPROVED against Square's current API reference. Also corrected the now-stale comment in the malformed-response test (it no longer throws; it logs under E_SQUARE_REFUND and returns false).

Exhaustive mutation on square.ts is still 100% (288 killed, 3 genuine equivalents suppressed; line numbers refreshed).
@stefan-burke
stefan-burke force-pushed the split/confirmed-square-refunds branch from 99c3d5a to dccfb50 Compare July 24, 2026 18:19

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

ℹ️ 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/square.ts Outdated
Comment thread src/shared/square.ts Outdated
@stefan-burke
stefan-burke dismissed coderabbitai[bot]’s stale review July 24, 2026 18:29

All concerns from this review are addressed — every thread is resolved, and the branch was rebased onto main (absorbing #1909/#1910/#1912, including #1912 stable Square refund idempotency keys). Dismissing because this changes-requested was against the pre-rebase commit 99c3d5a, which is no longer in the branch; CodeRabbit already re-reviewed the rebased tip as COMMENTED with no new findings.

@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/shared/square.ts`:
- Around line 567-576: Limit CONFIRMED_REFUND_STATUSES in
src/shared/square.ts:567-576 to COMPLETED and SUCCEEDED by removing APPROVED.
Update test/shared/square/retrieve-refund.test.ts:410-414 to remove or change
the APPROVED success expectation so tests enforce only the declared confirmation
statuses.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 6dba3257-9e88-40ad-a69f-66f0720a1522

📥 Commits

Reviewing files that changed from the base of the PR and between 99c3d5a and dccfb50.

📒 Files selected for processing (9)
  • TODO.md
  • scripts/mutation/equivalent-mutants.txt
  • src/shared/square.ts
  • test/shared/square-provider/provider.test.ts
  • test/shared/square/client.test.ts
  • test/shared/square/payment-link-validation.test.ts
  • test/shared/square/rest-transport.test.ts
  • test/shared/square/retrieve-refund.test.ts
  • test/shared/square/webhook.test.ts

Comment thread src/shared/square.ts Outdated
stefan-burke added a commit that referenced this pull request Jul 24, 2026
…iew)

Codex follow-ups: the refundPayment docstring still listed only COMPLETED/SUCCEEDED after APPROVED was added (now references CONFIRMED_REFUND_STATUSES), and the malformed-response guard only checked truthiness, so a truthy non-string id (e.g. id: 1) with an allow-listed status would have been recorded as a confirmed refund — the guard now requires string id/status (a missing or non-string field logs at the boundary under E_SQUARE_REFUND and returns false). Exhaustive mutation on square.ts is 100% (301 killed, 3 equivalents suppressed).

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

ℹ️ 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/square.ts Outdated
stefan-burke added a commit that referenced this pull request Jul 24, 2026
…#1911 review)

Codex P2 with Square doc sources: SUCCEEDED is not a Square refund status. Square documents a settled refund as COMPLETED (PaymentRefund object) or APPROVED (current RefundStatus enum); SUCCEEDED appears in neither, so accepting it would record a contract-drift response as a confirmed refund. Removed SUCCEEDED from CONFIRMED_REFUND_STATUSES (now APPROVED and COMPLETED only, both Square-documented across versions); the SUCCEEDED test now asserts false (treated as contract drift). This matches the job intent (accept only confirmed Square refund outcomes) and is the inverse of adding APPROVED: align the allow-list with Square documentation rather than the literal job spec, which predated Square current enum. Exhaustive mutation on square.ts is 100% (299 killed, 3 equivalents suppressed).

@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: 76e79fc792

ℹ️ 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/square.ts Outdated
Comment thread test/shared/square/retrieve-refund.test.ts Outdated
Comment thread test/shared/square/rest-transport.test.ts Outdated
Comment thread src/shared/square.ts Outdated
stefan-burke added a commit that referenced this pull request Jul 24, 2026
…review)

Codex P2: the malformed-response guard checked typeof refund.id === string but not non-empty, so a malformed refund: { id: "", status: "APPROVED" } would have been recorded as confirmed (an empty id is not a usable provider refund reference). Added a non-empty id check (and an empty-id regression test). Also resynced the refundPayment docstring — it still listed SUCCEEDED after that status was dropped from CONFIRMED_REFUND_STATUSES; it now names only APPROVED and COMPLETED and the malformed-shape clause covers missing, empty, and non-string. The two ~400-line test-file splits Codex flagged are recorded in TODO.md (pure test-organisation, separate from the refund contract). Exhaustive mutation on square.ts is 100% (302 killed, 3 equivalents suppressed).

@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: 8c0bebdec8

ℹ️ 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/refund-ledger.ts Outdated
Comment thread src/features/admin/attendees-edit.ts
Comment thread src/features/admin/attendees-edit.ts
Comment thread TODO.md Outdated
stefan-burke added a commit that referenced this pull request Jul 24, 2026
…ean TODOs (PR #1911 review)

Codex P1-1 (regression test): added a test that creates a quantity-0-only placeholder attendee with a payment leg (no sale, no refund_cash), stubs isPaymentRefunded to return true, calls the refresh-payment endpoint, and verifies the refund_cash leg was posted (ledger reconciled). Also extracted a postPaymentLeg helper to dedup the mapBooking/postTransfers pattern.

Codex P1-2 (stale manual-refund note): handleRefreshPayment now adds a resolving system note ('Refund confirmed: the payment provider reported this refund as settled. No manual refund is needed.') when the refresh confirms and records the refund, so the operator does not follow the stale 'refund manually' instruction left by storeRefundedBooking.

Codex P1-3 (stale TODOs): removed the completed 'Validate Square REST responses' and 'Split the Square refund tests' TODO entries. Replaced with a single note about the remaining orders/payments validation work; the refund validation and test splits are done.

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

ℹ️ 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/attendees-edit.ts Outdated
Comment thread src/features/admin/attendees-edit.ts Outdated
Comment thread test/features/admin/attendees-edit.test.ts Outdated
stefan-burke added a commit that referenced this pull request Jul 24, 2026
…ing test (PR #1911 review)

Codex P1 (restrict balance bypass): isPaymentOnlyPlaceholder now checks every leg is a provider-payment leg (no sale/fee/adjustment), so a surcharge-only order (payment + fee, no sale) is NOT misclassified as a placeholder and the balance guard is retained for it.

Codex P2 (limit resolving note): the system note is now only created for a quantity-0-only placeholder (no quantity > 0 booking), not for every refreshed refund. Also moved the text to the locale catalog (attendees.json: note.placeholder_refund_confirmed) via t(). Extracted recordConfirmedRefund helper to reduce handleRefreshPayment cognitive complexity.

Codex P1 (deleted-listing test): added a test that deletes the listing before refreshing, proving the loadRefreshContext fallback (using listing_id from the booking row, not requireListingWithCount) works for deleted-listing placeholders. Extracted setupPlaceholderForRefresh + refreshAndVerifyRefundCash helpers to dedup the two placeholder tests.
Codex follow-up (thread 3): the boundary throw lived inside withClient, which catches non-PaymentUserError exceptions and returns null, so it was architecturally the same as a logged false — an indirect throw-and-catch rather than a real loud failure. Replaced it with an explicit logError + return false, matching the missing-amount case just above it: the malformed response is still diagnosed at the boundary under E_SQUARE_REFUND and still fails safely to false (never reports a refund that was never confirmed), now without the swallowed-throw indirection. Propagating the throw outside withClient (a loud/errored failure) would change the contract from fail-safely-contained to fail-loudly, which is the per-path staged-checkout/callback resolution this PR was told not to introduce and is recorded in TODO.md against #1853 and #1905. Exhaustive mutation on square.ts is now 100% (286 killed, 3 genuine equivalents suppressed).
CodeRabbit and Codex flagged that Square's RefundStatus enum includes APPROVED (the current terminal success state for a refund), which CONFIRMED_REFUND_STATUSES omitted — a refund Square approved would have been wrongly treated as not-confirmed. Added APPROVED alongside COMPLETED and SUCCEEDED (kept per the job spec as defensive aliases), with a regression test. Confirmed APPROVED against Square's current API reference. Also corrected the now-stale comment in the malformed-response test (it no longer throws; it logs under E_SQUARE_REFUND and returns false).

Exhaustive mutation on square.ts is still 100% (288 killed, 3 genuine equivalents suppressed; line numbers refreshed).
… rebase

After rebasing onto main (absorbing #1909 test hardening, #1910 equivalent-registry cleanup, and #1912 stable Square refund idempotency keys), the three square.ts equivalent-mutant entries moved to 294/684/754 and the Square refundPayment now uses #1912 stable refundIdempotencyKey instead of crypto.randomUUID. Updated the entries to match and the TODO note for #1912 landing (the stable key mitigates the double-pay half of the PENDING-refund redelivery risk). Exhaustive mutation on square.ts is 100% (290 killed, 3 equivalents suppressed).
…iew)

Codex follow-ups: the refundPayment docstring still listed only COMPLETED/SUCCEEDED after APPROVED was added (now references CONFIRMED_REFUND_STATUSES), and the malformed-response guard only checked truthiness, so a truthy non-string id (e.g. id: 1) with an allow-listed status would have been recorded as a confirmed refund — the guard now requires string id/status (a missing or non-string field logs at the boundary under E_SQUARE_REFUND and returns false). Exhaustive mutation on square.ts is 100% (301 killed, 3 equivalents suppressed).
…#1911 review)

Codex P2 with Square doc sources: SUCCEEDED is not a Square refund status. Square documents a settled refund as COMPLETED (PaymentRefund object) or APPROVED (current RefundStatus enum); SUCCEEDED appears in neither, so accepting it would record a contract-drift response as a confirmed refund. Removed SUCCEEDED from CONFIRMED_REFUND_STATUSES (now APPROVED and COMPLETED only, both Square-documented across versions); the SUCCEEDED test now asserts false (treated as contract drift). This matches the job intent (accept only confirmed Square refund outcomes) and is the inverse of adding APPROVED: align the allow-list with Square documentation rather than the literal job spec, which predated Square current enum. Exhaustive mutation on square.ts is 100% (299 killed, 3 equivalents suppressed).
…review)

Codex P2: the malformed-response guard checked typeof refund.id === string but not non-empty, so a malformed refund: { id: "", status: "APPROVED" } would have been recorded as confirmed (an empty id is not a usable provider refund reference). Added a non-empty id check (and an empty-id regression test). Also resynced the refundPayment docstring — it still listed SUCCEEDED after that status was dropped from CONFIRMED_REFUND_STATUSES; it now names only APPROVED and COMPLETED and the malformed-shape clause covers missing, empty, and non-string. The two ~400-line test-file splits Codex flagged are recorded in TODO.md (pure test-organisation, separate from the refund contract). Exhaustive mutation on square.ts is 100% (302 killed, 3 equivalents suppressed).
Blocker 1 (PENDING placeholder stranding): traced store-refund.ts → refund-ledger.ts → attendees-edit.ts. A PENDING Square refund returns false from refundPayment, which made storeRefundedBooking terminal with only the payment leg (no refund_cash). The operator could not refresh because loadRefreshContext filtered quantity > 0 (excluding placeholders), and computeAttendeeRefund skipped them as unbalanced. Fixed: loadRefreshContext now prefers quantity > 0 but falls back to quantity-0 placeholders (ORDER BY (quantity > 0) DESC); computeAttendeeRefund now allows reversal for placeholder accounts (no sale legs) regardless of balance, so a settled PENDING refund can be reconciled via the refresh-payment path. Also dropped the ListingWithCount requirement (uses listing_id from the booking row, so a deleted listing does not 404 the refresh).

Blocker 2 (APPROVED): verified against the Square PaymentRefund object documentation — its status field is PENDING/COMPLETED/REJECTED/FAILED (NOT APPROVED). APPROVED is only in the separate RefundStatus enum page, not the PaymentRefund object that /v2/refunds returns. Removed APPROVED; CONFIRMED_REFUND_STATUSES is now [COMPLETED] only.

Blocker 3 (Valibot boundary): replaced the type cast with a Valibot schema (SquareRefundSchema with v.string() + v.minLength(1) for id and status, matching Square docs: id is Required with Min Length 1). v.parse runs OUTSIDE withClient so a malformed response (missing refund, empty id/status, non-string fields) throws loudly instead of normalizing to false. The client returns raw unknown; the boundary validation is in squareApi.refundPayment.

Also split retrieve-refund.test.ts (481→240) and rest-transport.test.ts (429→372) by extracting refund-payment.test.ts (247 lines) and refund-transport.test.ts (123 lines) with a shared mock-fetch helper. Added minLength boundary tests and a REJECTED status test. Exhaustive mutation on square.ts is 100% (284 killed, 2 equivalents suppressed).
…ean TODOs (PR #1911 review)

Codex P1-1 (regression test): added a test that creates a quantity-0-only placeholder attendee with a payment leg (no sale, no refund_cash), stubs isPaymentRefunded to return true, calls the refresh-payment endpoint, and verifies the refund_cash leg was posted (ledger reconciled). Also extracted a postPaymentLeg helper to dedup the mapBooking/postTransfers pattern.

Codex P1-2 (stale manual-refund note): handleRefreshPayment now adds a resolving system note ('Refund confirmed: the payment provider reported this refund as settled. No manual refund is needed.') when the refresh confirms and records the refund, so the operator does not follow the stale 'refund manually' instruction left by storeRefundedBooking.

Codex P1-3 (stale TODOs): removed the completed 'Validate Square REST responses' and 'Split the Square refund tests' TODO entries. Replaced with a single note about the remaining orders/payments validation work; the refund validation and test splits are done.
…ing test (PR #1911 review)

Codex P1 (restrict balance bypass): isPaymentOnlyPlaceholder now checks every leg is a provider-payment leg (no sale/fee/adjustment), so a surcharge-only order (payment + fee, no sale) is NOT misclassified as a placeholder and the balance guard is retained for it.

Codex P2 (limit resolving note): the system note is now only created for a quantity-0-only placeholder (no quantity > 0 booking), not for every refreshed refund. Also moved the text to the locale catalog (attendees.json: note.placeholder_refund_confirmed) via t(). Extracted recordConfirmedRefund helper to reduce handleRefreshPayment cognitive complexity.

Codex P1 (deleted-listing test): added a test that deletes the listing before refreshing, proving the loadRefreshContext fallback (using listing_id from the booking row, not requireListingWithCount) works for deleted-listing placeholders. Extracted setupPlaceholderForRefresh + refreshAndVerifyRefundCash helpers to dedup the two placeholder tests.
…1911 review)

Codex P2 (picklist): the Valibot schema now validates status with v.picklist([PENDING, COMPLETED, REJECTED, FAILED]) — the four documented PaymentRefund statuses. An undocumented status (APPROVED, SUCCEEDED, WAT, etc.) throws at the boundary instead of silently becoming an ordinary false outcome. Updated tests: undocumented statuses now expect a throw via expectMalformedThrows.

Codex P1 (resolve warning via ledger): recordConfirmedRefund now checks the ledger (transfersByAccount + KIND.sale) for whether the attendee has sale legs, not the booking quantities. If the operator added a real booking to a placeholder attendee before the refund settled, the ledger would show sale legs and the resolving note would NOT be created (correctly), because the original manual-refund warning from storeRefundedBooking only fires for quantity-0-only placeholders with no sale. The ledger is the authoritative check.
… (PR #1911 review)

Codex P2 (docstring): the refundPayment docstring now distinguishes documented non-success statuses (PENDING/REJECTED/FAILED → false) from undocumented/malformed statuses (not in the picklist → throws), reflecting the v.picklist validation.

Codex P2 (stale TODO line numbers): removed the concrete line numbers (293, 678, 748) from the stale-equivalent-mutants TODO section — they drift as the source shifts lines. The text now references the audit command's own output (which lists every stale file:line:col) without embedding numbers that immediately go stale.
…#1911 review)

Codex P1 (deleted-listing test): the test callback now receives the actual listing ID (not a hardcoded 0), so it deletes the correct listing row. The test comment clarifies that deleteListing would cascade to the attendee — this test exercises the case where only the listing row is gone (the real-world scenario for a storeRefundedBooking placeholder whose listing was already removed).

Codex P2 (surcharge-only order): the placeholder check now uses legMatches({ from: WORLD, kind: KIND.payment }) (via sameAccount) instead of === (which compared object references for AccountRef). A surcharge-only order (payment + fee, no sale) is NOT classified as a placeholder because the fee leg is not a provider-payment leg.
Codex P2: refundPayment now gets the Square client directly (not through withClient) so a successful HTTP 200 response — even a null or invalid-JSON body — always reaches the Valibot parse. withClient would catch a JSON.parse throw or a JSON null return and normalize to null → false. With the direct approach: client-unconfigured returns false; HTTP/network/JSON errors are caught by a targeted try/catch (logged → false, retryable); a 200 body always reaches v.parse (malformed → throws loudly). Added a test for the client-unavailable-after-retrieval edge case.
Codex P2: the try/catch now re-throws SyntaxError (JSON.parse failure from a 200 body) so invalid JSON in a successful response propagates — it fails loudly at the boundary instead of being caught as a generic error → false. HTTP errors and network errors still return false (graceful, retryable). Added a test: a mock rejecting with SyntaxError causes refundPayment to reject (not resolve to false).
…review)

Codex P2: the Valibot schema now requires payment_id (non-empty string) and amount_money (non-negative number + non-empty currency) on the PaymentRefund object, and refundPayment verifies the refund's payment_id matches the paymentId it was called with — a COMPLETED refund for a different payment throws loudly. Updated all test mocks to include the required fields. Added a test for the wrong-payment-id case.
…alformed throws (PR #1911 review)

Codex P2: the docstring now correctly documents three failure modes: (1) HTTP errors and network failures → logged and returned as false (graceful, retryable); (2) a 200 response is always validated by v.parse — a malformed body throws loudly; (3) client-unconfigured returns false. The previous docstring incorrectly said HTTP and parsing errors propagate.
…te; fix coverage

E2e: added afterPaidBooking hook to the Square provider using the shared exerciseAdminRefund helper — the nightly Square sandbox run now exercises the real POST /v2/refunds, the Valibot boundary schema, the COMPLETED status check, the payment_id/amount verification, and the ledger posting. Extracted the shared admin refund exercise from exerciseStripeRefund into shared.ts so Stripe and Square share one implementation.

Validation: the Valibot schema now requires payment_id and amount_money on the PaymentRefund object; refundPayment verifies the refund is for the correct payment AND the amount matches before accepting COMPLETED. Added tests for wrong-payment-id, wrong-amount, and wrong-currency (all throw).

Note cleanup: recordConfirmedRefund deletes the stale 'could NOT be refunded' system note from storeRefundedBooking before creating the resolution note. Added a test: creates the stale note, refreshes, verifies the stale note is deleted and the confirmation note is present.

Equivalent-mutant line numbers refreshed (312/783). Deduped the getRefundPaymentReferences call pattern via getRefundPaymentReferencesForAttendee.
@stefan-burke
stefan-burke force-pushed the split/confirmed-square-refunds branch from 90aa5d3 to 99d3860 Compare July 25, 2026 05:14

@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: 99d3860b42

ℹ️ 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 +70 to 72
ORDER BY (listingAttendee.quantity > 0) DESC,
listingAttendee.start_at, listingAttendee.listing_id
LIMIT 1`,

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 Recover refunds without a surviving booking row

When the placeholder's listing is deleted through the application, deleteListing removes its listing_attendees rows before deleting the listing (src/shared/db/listings/delete.ts:8-13), so this booking-row query returns nothing and the refresh route responds with 404. A Square refund that settles afterward therefore cannot be recorded, leaving the stale manual-refund warning and risking another refund attempt. Fresh evidence after the earlier thread is that the final regression test explicitly avoids deleteListing and directly deletes only the listing row, preserving the condition this query requires; load the context without requiring a booking row and cover the real deletion path.

AGENTS.md reference: AGENTS.md:L83-L83

Useful? React with 👍 / 👎.

Comment on lines +131 to +132
const isPlaceholder =
legsBeforeRefund.length > 0 && legsBeforeRefund.every(isProviderPayment);

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 Remove stale warnings after the account gains other legs

When a pending-refund placeholder later gains a paid booking before all refunds settle—for example, by keeping the placeholder as the target of an attendee merge—its account contains sale and payment legs, so this every(isProviderPayment) check is false. Once every provider payment is refunded, recordAttendeeRefund can still reverse the fully paid account successfully, but this branch leaves the original red note telling the operator to refund manually, risking a second refund. Fresh evidence after the earlier thread is that the final code gates warning deletion on the account still being payment-only rather than on the warning's existence; delete the matching warning after confirmed recovery regardless of later account shape and cover that exact sequence.

AGENTS.md reference: AGENTS.md:L83-L83

Useful? React with 👍 / 👎.

Comment on lines +108 to +112
await requirePageText(
session,
"Refund issued",
"refund-failed",
'Expected the app page to contain "Refund issued"',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Wait for accepted Square refunds in the e2e flow

When the Square sandbox returns the documented PENDING outcome, this assertion fails even though the refund request was accepted: squareApi.refundPayment now returns false for that status, and the inspected admin path in src/features/admin/refunds/provider.ts:39-56 performs only one immediate isPaymentRefunded check before rendering “Refund failed.” The new Square afterPaidBooking hook therefore makes the nightly journey depend on the refund becoming COMPLETED synchronously; poll through the refresh flow until the accepted refund settles before requiring the success flash.

Useful? React with 👍 / 👎.

…; add e2e Square refund

Split test/features/admin/attendees-edit.test.ts (410→297 lines) by extracting the placeholder refresh tests into test/features/admin/placeholder-refresh.test.ts (115 lines). Both files now stay under the ~400-line target. Extracted the shared postPaymentLeg helper into test/test-utils/db-helpers/payment-leg.ts.

E2e: the nightly Square sandbox run now exercises the real refund path (afterPaidBooking → exerciseAdminRefund) via the shared shared.ts helper, so the POST /v2/refunds → Valibot schema → COMPLETED check → payment_id/amount verification → ledger posting round-trip is exercised against the real Square sandbox API every night. Extracted exerciseAdminRefund into shared.ts so Stripe and Square share one implementation.

@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: 23cae697ce

ℹ️ 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/attendees-edit.ts Outdated
…1911 review)

Codex P1: the stale-note cleanup now runs on every successful refresh, regardless of whether attendee.refunded was already true. Previously, if the note cleanup failed after the ledger posted, attendee.refunded became true and the next refresh skipped recordConfirmedRefund entirely (because of the !attendee.refunded condition), stranding the stale manual-refund note forever. Fixed by: (1) always calling recordConfirmedRefund when all references are provider-refunded (the ledger post is idempotent — already-posted returns {posted: true} as a no-op); (2) moving cleanupStaleManualRefundNote outside the isPlaceholder check (after the first refresh, the account has a refund_cash leg so isPlaceholder is false, but the stale note could still be there); (3) extracted loadRefreshState to keep handleRefreshPayment under cognitive complexity 12. Added a test: first refresh posts the ledger, re-creates the stale note, second refresh cleans it up.

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

ℹ️ 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/attendees-edit.ts Outdated
Codex P2: the activity log ('Payment marked as refunded') now only fires when attendee.refunded was false before the refresh. On retry when the ledger was already posted, the activity log is skipped (no duplicate audit entries), but the stale-note cleanup still runs unconditionally.
@stefan-burke
stefan-burke added this pull request to the merge queue Jul 25, 2026
Merged via the queue into main with commit 9c70f03 Jul 25, 2026
3 checks passed
@stefan-burke
stefan-burke deleted the split/confirmed-square-refunds branch July 25, 2026 10:17
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