Reference
referenceReserves a persistent row per merchant and key, and returns 409 while that charge is in flight.
01 · Payments API
A payment endpoint must charge a card once per Idempotency-Key, no matter how the client retries.
referenceReserves a persistent row per merchant and key, and returns 409 while that charge is in flight.
alt_validTransaction-wide advisory lock and response on payment rows; reference reserves persistent request rows.
wrong_inmemory_cacheApp-local cache disappears at restart.
wrong_retry_raceChecks before charging; concurrent requests each charge before insertion.
wrong_uncached_declinePersists replay state only for successful charges, so retrying a declined payment calls the gateway again.
rawbody_fingerprintExpect: rejectObserved: rejectedHashes raw JSON bytes, violating equality of the three validated fields regardless of JSON member ordering.
key_limit_64Expect: rejectObserved: rejectedRejects keys longer than 64 characters, violating the inclusive 1-255 key length rule.
key_limit_254Expect: rejectObserved: rejectedRejects a 255-character key, violating the inclusive 1-255 key length rule.
process_local_lockExpect: rejectObserved: rejectedSerializes keys only within one app, violating single-charge concurrency across app instances.
alt_poll_replayExpect: acceptObserved: acceptedPolls reserved rows until completion and replays the receipt instead of returning an in-progress response.
alt_schema_in_lifespanExpect: rejectObserved: rejectedCreates its schema only in a lifespan handler, violating PROBLEM.md lines 34-35 requiring schema readiness when create_app returns (original review label: correct; F-402 false_fail clarification).
Each row groups a test’s parameterized cases; any failing case marks the row as Fail. * marks a wrong solution’s declared must_fail test.
| Test | Starter | Reference | Alternative | inmemory cache | retry race | uncached decline |
|---|---|---|---|---|---|---|
| Sequential retry | Fail | Pass | Pass | Pass | Pass | Pass |
| Concurrent duplicates charge once | Fail | Pass | Pass | Pass | Fail * | Pass |
| Same key different merchants independent | Pass | Pass | Pass | Pass | Pass | Pass |
| Body mismatch | Fail | Pass | Pass | Pass | Pass | Pass |
| Decline replayed | Fail | Pass | Pass | Pass | Pass | Fail * |
| Body mismatch while in flight | Fail | Pass | Pass | Pass | Fail | Pass |
| Replay survives app restart | Fail | Pass | Pass | Fail * | Pass | Pass |
| Required key | Fail | Pass | Pass | Pass | Pass | Pass |
| Valid key lengths and reordered body | Fail | Pass | Pass | Pass | Pass | Pass |
| Invalid payment fields and merchant | Pass | Pass | Pass | Pass | Pass | Pass |
| Concurrent duplicates across app instances | Fail | Pass | Pass | Fail | Fail | Pass |
| Test | Starter | Reference | Alternative | inmemory cache | retry race | uncached decline |
|---|---|---|---|---|---|---|
| Create | Pass | Pass | Pass | Pass | Pass | Pass |
| Retry | Fail | Pass | Pass | Pass | Pass | Pass |
| Missing key | Fail | Pass | Pass | Pass | Pass | Pass |
Our synthetic payment service already charges a declared gateway and returns a receipt. Clients retry requests after losing a response. Implement the following observable contract.
Public interface: app.main.create_app(database_url: str, gateway: Gateway) -> FastAPI; app.gateway.Gateway.charge(merchant_id, amount_minor, currency, source_token) -> ChargeResult. ChargeResult(charge_id: str, status: "succeeded"|"declined") is a frozen dataclass. The starter also exports app.gateway.FakeGateway for visible and naive smoke tests; its .calls property is a tuple of charging calls. Hidden final tests inject their own Gateway. POST /v1/payments takes X-Merchant-Id, Idempotency-Key, and JSON {amount_minor, currency, source_token}. Money is positive integer minor units, never bool. Currencies supported: CAD/USD/EUR/GBP (2 dp), JPY (0), KWD/BHD (3). No conversion occurs.
idempotency_key_required. Merchant is a nonempty string; missing: 400 merchant_required.invalid_payment. Ignore JSON member ordering. Body equality means equality of the three validated fields; no extra fields are accepted.card_declined) also replay exactly.idempotency_key_reuse, no charge, whether the first request is complete or in flight.idempotency_request_in_progress. A later retry must replay.payment_id, charge_id, status ("succeeded"), amount_minor, currency, and merchant_id. The payment id is nonempty and unique for distinct payments. Do not compare an id to any particular UUID format.{"error":{"code":"<snake_case>","message":"..."}}; message text is free. No gateway exceptions or crash recovery are in scope. A charge returns normally with either status.Preserve the factory and gateway protocol. Store state in the supplied PostgreSQL database; the application may create its own schema. The schema must exist when create_app() returns; HTTP requests must work without running application lifespan/startup handlers. Tests inspect only the declared HTTP surface and gateway calls. Timestamps in stored receipts use aware UTC; no timestamp field is required in JSON. Run visible tests from starter: python -m pytest tests -q with DATABASE_URL set to a disposable database. No customer data or real payment network is involved.