Skip to content

Payment initiation and idempotent, low-latency transaction submission - #1282

Merged
mftee merged 1 commit into
CodeGirlsInc:mainfrom
AbdulmujibOladayo:feature/1276-payment-initiation-fund-escrow
Aug 22, 2026
Merged

Payment initiation and idempotent, low-latency transaction submission#1282
mftee merged 1 commit into
CodeGirlsInc:mainfrom
AbdulmujibOladayo:feature/1276-payment-initiation-fund-escrow

Conversation

@AbdulmujibOladayo

Copy link
Copy Markdown
Contributor

Summary

  • Adds POST /shipments/:id/payment (shipper only): validates the shipment is ACCEPTED and the requester is its shipper, then builds and simulates (but does not sign) a fund_escrow transaction via StellarContractService.buildFundEscrowTransaction, returning unsigned XDR for the shipper's own wallet to sign — the platform never holds shipper keys (non-custodial deposit model from [PAY-02] Soroban integration layer — signer, RPC client, and escrow contract bridge #1275/ADR).
  • Adds POST /shipments/:id/payment/:paymentId/submit (shipper only): accepts the shipper-signed XDR and submits it via a new StellarContractService.submitSignedTransaction.
  • Adds a FUNDING payment status, claimed atomically (PENDING -> FUNDING via a conditional UPDATE ... WHERE status = 'pending') immediately before the chain call in submit, so two concurrent duplicate submit requests can never both reach the network — the loser gets a structured 409 (PaymentAlreadyInFlightError) instead of a second chain call. On failure the claim is released back to PENDING so a legitimate retry is possible.
  • Maps StellarContractService failures (simulation failure incl. insufficient balance/missing token approve allowance, submission failure, typed escrow-contract rejection) to structured 4xx PaymentFlowError subclasses instead of a generic 500.
  • Shipment.price (fiat decimal) converts to the contract's i128 base units via a documented fixed 7-decimal shift (settlement-asset.util.ts) — an MVP simplification (no live FX), matching the 7-decimal convention already used in the escrow contract's own tests.
  • Handles the "carrier changed after a failed funding attempt" edge case: an existing PENDING payment row has its carrier/shipper wallet binding refreshed in place rather than reused stale, if the shipment's carrier has changed since the row was created.
  • Adds a test-only shipper-signing path (POST .../test-sign-and-submit, gated by ALLOW_TEST_SIGNING, disabled by default) so the funding flow is verifiable end-to-end on testnet before the frontend wallet-signing UI (a later issue) exists.
  • Adds a migration for the new FUNDING enum value and stellar_tx_hash / failure_reason columns (production relies on migrations; synchronize is dev/test only).

Test plan

  • npm run build
  • npm run lint (scoped to changed files — 0 errors; the full-repo run shows thousands of pre-existing CRLF-as-error results across untouched files, confirmed unrelated to this change)
  • npm run test — all suites pass, including new/updated specs:
    • payments.service.spec.ts (forbidden/not-accepted/missing-wallet validation, first-funding-attempt row creation, already-funded / already-in-flight guards, stale-carrier-binding refresh, simulation-error mapping, the atomic claim race for concurrent submit — affected: 0 losing path, claim-release-on-failure, and the test-signing path)
    • stellar-contract.service.spec.ts (new buildFundEscrowTransaction / submitSignedTransaction methods, plus all 18 pre-existing tests still passing after extracting the shared submitPrepared helper from invoke)
    • settlement-asset.util.spec.ts

Closes #1276

@vercel

vercel Bot commented Aug 22, 2026

Copy link
Copy Markdown

@wumibals is attempting to deploy a commit to the Mftee's projects Team on Vercel.

A member of the Team first needs to authorize it.

Adds POST /shipments/:id/payment (builds+simulates an unsigned fund_escrow
transaction for the shipper to sign client-side, non-custodial) and
POST /shipments/:id/payment/:paymentId/submit (accepts the signed XDR and
submits it). A new FUNDING payment status is claimed atomically
(PENDING -> FUNDING) before the chain call, so concurrent duplicate submit
requests can never both reach the network — the loser gets a structured
409 instead. Stellar/escrow contract failures (insufficient balance,
missing token allowance, contract rejection) map to typed 4xx errors
instead of a generic 500. Shipment.price converts to the contract's i128
base units via a documented fixed 7-decimal shift (MVP, no live FX).

Also adds a test-only shipper-signing path (ALLOW_TEST_SIGNING, disabled
by default) so the flow is verifiable end-to-end on testnet before the
frontend wallet-signing UI exists.

Closes CodeGirlsInc#1276
@AbdulmujibOladayo
AbdulmujibOladayo force-pushed the feature/1276-payment-initiation-fund-escrow branch from 948f8e1 to 8f9b636 Compare August 22, 2026 09:22

@mftee mftee 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.

Reviewed the payment initiation and submission PR (closes #1276, building on the Soroban bridge from #1275).

  • The two-endpoint split (initiate/build-and-simulate unsigned XDR, then separately submit the shipper-signed XDR) correctly preserves the non-custodial model from the ADR — the platform never touches shipper keys, only ever unsigned or shipper-signed envelopes.
  • The atomic PENDING -> FUNDING claim via a conditional UPDATE ... WHERE status = 'pending' immediately before the chain call is the right way to prevent double-submission — it means the race is resolved at the DB layer before any network call happens, not after. The loser getting a structured 409 instead of a second chain call, and the claim being released back to PENDING on failure so a legitimate retry isn't permanently blocked, are both the correct behaviors for this kind of guard.
  • Mapping StellarContractService failures (simulation, submission, typed contract rejection) into structured PaymentFlowError 4xx subclasses instead of letting them surface as generic 500s continues the pattern established in the earlier Stellar bridge work — good consistency.
  • The stale-carrier-binding refresh (an existing PENDING row gets its carrier/shipper wallet refreshed in place if the shipment's carrier changed since the row was created, instead of reusing stale wallet data) is a real edge case that's easy to miss and is explicitly tested.
  • The 7-decimal fiat-to-i128 conversion being documented as an MVP simplification, and specifically matching the convention already used in the escrow contract's own tests rather than picking an arbitrary precision, shows the tradeoff was made deliberately rather than by accident.
  • The test-only signing path is appropriately gated behind ALLOW_TEST_SIGNING and disabled by default — a reasonable way to get end-to-end testnet verification before the real wallet-signing frontend exists, without it being reachable in a normal deployment.
  • Test coverage matches the risk surface: the atomic claim race (including the losing affected: 0 path), claim-release-on-failure, stale-carrier refresh, and simulation-error mapping are all explicitly covered, plus the pre-existing 18 StellarContractService tests still pass after extracting the shared submitPrepared helper.

CI is green across Backend, Frontend, and Contracts. Vercel's FAILURE is the usual unauthorized deployment integration link, unrelated to the code.

Approving — careful handling of the funding flow's concurrency and failure edges.

@mftee
mftee merged commit c1ec7d4 into CodeGirlsInc:main Aug 22, 2026
3 of 4 checks passed
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.

[PAY-03] Payment initiation — fund escrow on bid acceptance, with idempotency

2 participants