Skip to content

feat(payments): add scheduled retry for stuck payments - #227

Merged
tiana-code merged 2 commits into
mainfrom
feat/E-02-payments-retry
Jun 18, 2026
Merged

feat(payments): add scheduled retry for stuck payments#227
tiana-code merged 2 commits into
mainfrom
feat/E-02-payments-retry

Conversation

@tiana-code

Copy link
Copy Markdown
Owner

What

Eighth slice of E-02. An opt-in @Scheduled job that recovers payments stuck in SCREENING (left there when a bank submission failed transiently in #213).

  • PaymentOrchestrator.resume(payment): re-route a SCREENING payment to the bank (re-submit, outside any transaction).
  • PaymentRetryService (not @Transactional): find SCREENING payments older than stuck-after; per payment, re-route via resume, or - if older than max-age - markFailed("retry deadline exceeded"). Per-payment try/catch so one failure does not abort the batch.
  • Bounded: the [stuck-after, max-age] window bounds total attempts; PaymentRetryProperties.init validates stuck-after <= max-age. Age-based (no retry_count column / migration).
  • PaymentRetryScheduler: @Configuration @EnableScheduling @ConditionalOnProperty(...enabled, matchIfMissing=false) - off by default (mirrors the ledger cleanup scheduler Cleanup/TTL job for idempotency_keys (24h) and outbox (30d) #67).
  • Partial index idx_payments_status_created_at on the live statuses backs the query.

§8.10: the bank call runs outside any transaction (service + orchestrator non-transactional; transitions are separate-bean short transactions). Single-scheduler-instance + idempotent-submit is documented as a load-bearing contract; a SCREENING -> SUBMITTED transition removes a payment from the retry set once accepted.

Gates

Local (-x integrationTest): :services:payments:{test,detekt,detektTest,spotlessCheck,assemble,compileIntegrationTestKotlin} green. critic GO (single-instance contract + stuck-after<=max-age guard applied), security-auditor PASS (§8.10 + bounded-retry + §5.3, 5/5 ACs), evaluator 0.914 (a prior eval pass hallucinated a §8.10 violation referencing a non-existent status; a fresh grep-verified re-eval confirmed PASS). The migration runs on CI.

Closes #215

Add an opt-in scheduled job that re-routes payments stuck in screening (left
there by a transient bank failure) back to the bank within a bounded age
window, and fails them once past the deadline. The job is not transactional:
it resumes a stuck payment through the orchestrator, which re-submits to the
bank outside any transaction and settles the outcome in its own short
transaction; each payment is attempted independently so one failure does not
abort the batch. The retry window is bounded by stuck-after and max-age
(validated so stuck-after never exceeds max-age), and the scheduler is off by
default. A partial index on the live statuses backs the stuck-payment query.

Closes #215
@tiana-code tiana-code added this to the v0.2.0 milestone Jun 18, 2026
Apply the service interface+impl rule (CLAUDE.md 8.3): PaymentRetryService is
now an interface and the logic moves to PaymentRetryServiceImpl with @service.
No behaviour change.
@tiana-code
tiana-code merged commit a7c069d into main Jun 18, 2026
8 checks passed
@tiana-code
tiana-code deleted the feat/E-02-payments-retry branch June 18, 2026 15:22
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.

F-02.8 Scheduled payment retry job

1 participant