Skip to content

fix(payment-service): recover payments stuck in PROCESSING (#18) - #195

Merged
Yousefa7medmaher merged 1 commit into
devfrom
fix/payment-processing-recovery
Oct 15, 2026
Merged

Yousefa7medmaher merged 1 commit into
devfrom
fix/payment-processing-recovery

Conversation

@Yousefa7medmaher

Copy link
Copy Markdown
Member

Summary

Payments can no longer get stuck in PROCESSING (#18).

Closes #18.

Changes

  • Claim stamp and owner. Claiming a payment (PENDING → PROCESSING) stamps processingClaimedAt (database clock) and a fresh processingToken. Migration 004 adds both columns as nullable.
  • Active claims stay protected. A retry within PAYMENT_PROCESSING_TIMEOUT_MS (default 60 s) gets 409, as before.
  • Expired claims are taken over. After the timeout, a retry takes the claim over with one atomic UPDATE … WHERE "processingClaimedAt" < now() - timeout and settles the payment. Of many concurrent takeovers exactly one wins.
  • Settlement is fenced. It needs the current token (WHERE "processingToken" = <own>). A request whose claim was taken over writes nothing and returns what the new holder settled, so a payment is settled exactly once.
  • Abandoned payments are recovered. If the customer never retries, the existing payment sweep settles payments still PROCESSING after PAYMENT_PROCESSING_ABANDON_MS (default 15 min) as FAILED: "Payment processing was interrupted, so you were not charged." Then payment.failed is published, the order moves to FAILED and the customer is notified.
  • Deploy-safe. Rows already stuck have no claim time and count as abandoned, so they become recoverable right after deploy.
  • Simulator only. Re-deciding the outcome on a takeover, and "not charged", are right because the simulator charges nothing until it settles. The docs state that a real gateway must look the charge up by its idempotency key at those two points.

Docs: docs/services/payment-service.md ("Stuck PROCESSING recovery"), .project-context/04-database-design.md and .project-context/05-event-driven-design.md.

Lease ownership for side effects is the next issue, #19.

Test plan

  • Unit tests: 66 service tests, including:
    • a fresh claim gets 409;
    • an abandoned claim is taken over and settled once;
    • a legacy row with no claim time is recoverable;
    • of two concurrent takeovers, one settles;
    • a request that was taken over can't settle, and returns the new holder's result;
    • the sweep fails a long-abandoned payment and leaves recent claims alone.
  • PostgreSQL 16 integration (PAYMENT_TEST_DATABASE_URL), 21 tests, including:
    • 10 concurrent takeovers of an expired claim → exactly 1 winner;
    • the old holder's token can't settle; the new holder settles and the token is cleared;
    • a legacy PROCESSING row is listed as abandoned and reclaimable;
    • the legacy-upgrade test now also applies migration 004 and still matches the entity.
  • Migrations on a fresh database add processingClaimedAt timestamptz and processingToken uuid.
  • tsc; lint shows 0 errors.
  • CI

🤖 Generated with Claude Code

Claims stamp processingClaimedAt and a processingToken. A claim older than
PAYMENT_PROCESSING_TIMEOUT_MS (or with no claim time) is taken over
atomically by the next retry; only the token holder can settle, so a
taken-over request writes nothing. The payment sweep settles payments
abandoned for PAYMENT_PROCESSING_ABANDON_MS as FAILED, not charged
(simulated gateway). Rows stuck before this deploy are recoverable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Yousefa7medmaher
Yousefa7medmaher merged commit a34c515 into dev Oct 15, 2026
24 checks passed
@Yousefa7medmaher
Yousefa7medmaher deleted the fix/payment-processing-recovery branch October 15, 2026 18:37
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.

fix: recover payments stuck in PROCESSING state

1 participant