Skip to content

fix(payments): orchestrate a payment automatically after it is initiated - #319

Merged
tiana-code merged 1 commit into
mainfrom
feat/E-02-auto-orchestrate
Jun 19, 2026
Merged

fix(payments): orchestrate a payment automatically after it is initiated#319
tiana-code merged 1 commit into
mainfrom
feat/E-02-auto-orchestrate

Conversation

@tiana-code

Copy link
Copy Markdown
Owner

fix(payments) #317 - orchestrate a payment automatically after initiate.

The defect

PaymentOrchestrator.process() (INITIATED -> SCREENING -> bank submit -> SUBMITTED) had NO production caller - only the retry path used the orchestrator, and only for payments already in SCREENING (its scheduler is off by default). So a payment created via POST /v1/payments stayed in INITIATED forever. Found during #236 scoping by source verification; the e2e demo (#236) is precisely the test that would have caught it.

Fix

  • Publish PaymentInitiatedEvent inside createPayment (within the reserveOrRun transaction; invariant noted in a comment).
  • PaymentOrchestrationTrigger: @async + @TransactionalEventListener(AFTER_COMMIT) -> orchestrator.process(id), so screening and the external bank call run off the request thread, after the row is durably committed, never inside a transaction (CLAUDE.md 8.10). A BankProviderException leaves the payment in SCREENING for the existing retry; the listener catches and logs so the async worker survives.
  • PaymentAsyncConfig: @EnableAsync + a bounded ThreadPoolTaskExecutor (core 2 / max 8 / queue 100) that drains on graceful shutdown.
  • Idempotent replay returns the existing payment without re-publishing, so it does not re-trigger. A second process() is a safe no-op (the state machine rejects the transition).

Tests

Unit: the trigger processes on the event and swallows a failure; the service publishes the event on initiate. The full after-commit -> async -> SUBMITTED path is exercised by the #236 compose-smoke demo on a live stack (a JVM @DataJpaTest slice cannot load the async listener). detekt + spotless + unit tests green locally; integrationTest runs on CI.

Gate chain

critic: GO-WITH-CHANGES. code-reviewer + security-auditor reviewed. Applied: bounded @async executor (was the HIGH), invariant comment. Verified: process() is non-transactional by design (each transition is its own short tx). Durability backstop (the retry sweeper should also recover stuck INITIATED, not only SCREENING) tracked as follow-up #318; the bounded executor's graceful drain already covers the shutdown case, leaving only a hard-crash window. evaluator: PASS (0.85).

Closes #317

A payment created through the API stayed in INITIATED forever: the
orchestrator that screens and submits it had no production caller (only
the retry path touched it, and only for payments already in SCREENING).
Publish an in-process event when a payment is persisted and drive
orchestration from an after-commit, asynchronous listener on a bounded
executor, so screening and bank submission run off the request thread,
after the row is durably committed, and never inside a transaction. A
bank failure still leaves the payment in SCREENING for the scheduled
retry. Idempotent replays do not re-trigger.

Closes #317
@tiana-code tiana-code added this to the v0.2.0 milestone Jun 19, 2026
@tiana-code
tiana-code merged commit 1b8450d into main Jun 19, 2026
8 checks passed
@tiana-code
tiana-code deleted the feat/E-02-auto-orchestrate branch June 19, 2026 22:45
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(payments): no production trigger advances an initiated payment to SUBMITTED

1 participant