docs(backend): harvest platform mechanisms into the contracts (Wave 2, FH-E.1-E.4)

Writes the 14 harvested mechanisms from FORK-ANALYSIS-2026-08-21.md into
the backend contracts. Each section is dated 2026-08-21 and tagged FH-*
so any wording traces back to why it is worded that way.

The through-line: several contracts stated correctness as behaviour
("the webhook must be idempotent"). Behaviour written as an if-statement
gets deleted by a refactor and the failure mode is a double charge. These
sections restate it as schema and mechanism.

PHASE-3  3.1 conditional-write reservation, 409 on zero rows, cart-wide
             rollback, 15 min TTL
         3.2 InventoryMovement append-only journal with resultingAvailable
         6   bulk import idempotent by SKU, rollback while unsold
         6a  digital code pools, revealed only when paid
PHASE-7  5   unique constraints for payment idempotency and webhook
             replay, insert-first handling, signature over raw body,
             24h poll as reconciliation not primary
TRACK-S  2.1 session model - 32 bytes stored as SHA-256 only, HttpOnly,
             one cookie per contour, Argon2id params, mandatory TOTP
         2.2 origin allowlist ahead of routing on every cookie mutation
         4.2 AES-256-GCM envelope for stored secrets, HMAC fingerprints
         8a  order manager as a separate contour, scoped by membership
             rows rather than by configuration
PHASE-9  5.1 revision immutability, version = max+1, pointer flipped
             in-transaction, operational state does not travel
         5.2 clone carry / no-carry list, inventory to zero
         5.3 signed read-only preview, non-GET 404s while previewing
         6   host normalization, verifiedAt required, cache invalidation
PHASE-10 3a  server re-runs the editor's validation, clamp-and-fallback
PHASE-2  3.1 order publicToken, snapshot completeness, never updated

FH-2.12 rejected on the merits: our marketplace lifecycle state machine
is richer than theirs, adopting it would be a downgrade. Recorded in the
TODO so it is not raised again.

Also adds BACKEND-HANDOFF.md sections 0 and 0a - nine falsifiable
invariants as a release gate, each cross-referenced to the contract that
specifies it, plus PR and release discipline. And ADR-0006 recording what
we take, what we reject, what we keep because ours is better, and the
organizational question it deliberately does not settle.

No implementation changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
sdarbinyan
2026-08-21 11:12:05 +04:00
parent e8fc8480fe
commit f9e09b1757
9 changed files with 353 additions and 38 deletions

View File

@@ -105,7 +105,26 @@ payment -> routed to exactly one payment point (Phase 1 §6.5, frozen at checkou
Current flow supports QR and card only, via one custom provider integration. Adding wallets/BNPL is an explicit open business decision (not answered in Sprint 0.1) — this contract's `PaymentIntent`/`Payment` shapes from Phase 1 §6 are provider-agnostic already, so a new provider is a new adapter behind the same state machine, not a schema change. No action needed here until that business decision is made.
## 5. What the frontend will start doing once this ships
## 5. Idempotency belongs in the schema, not in a handler
Added 2026-08-21 (FH-2.2). [Phase 1 §6.36.4](PHASE-1-MONEY-FX-PAYMENTS-CONTRACT.md) already require that a replayed webhook be a no-op and that order creation be idempotent. Both are stated as *behaviour*. Behaviour written as an `if` gets deleted by someone refactoring in eighteen months, and the failure mode is a double charge. Make the database refuse instead:
```
UNIQUE (payment.idempotency_key)
UNIQUE (payment_webhook_event.provider, payment_webhook_event.event_key)
```
Handling:
- **Payment creation.** `Idempotency-Key` is required on the create call. If a payment already exists for that key: same order and marketplace → return the existing payment unchanged; different order or marketplace → `409`, never silently create a second one.
- **Webhook receipt.** Insert the event row *first*. A unique-violation is the duplicate signal — respond `{ accepted: true, duplicate: true }` and stop. Only a successful insert proceeds to apply the status change. Mark `processedAt` after applying, so a crash between insert and apply is visible as an unprocessed row rather than a lost event.
- **`event_key`** is the provider's event id where one exists, and `sha256(rawBody)` where it does not. A provider that sends no event id must still be replay-safe.
- **Signature verification runs against the raw request body**, before any parsing or re-serialization. Verify-after-parse is verify-nothing.
- **Poll as reconciliation, not as primary.** A scheduled job re-checks provider status for payments still `pending` within the last 24 hours and applies the result through the same state-machine path as the webhook. Transient provider failures are swallowed; the next tick retries. Never a fixed delay, never a UI-driven poll standing in for a missed webhook.
**Acceptance:** the same provider event delivered twice completes the order once, moves stock once, and emits one notification. This is scenario 10 of the acceptance list in [FORK-HARVEST-TODO.md](../FORK-HARVEST-TODO.md) and is a required e2e test.
## 6. What the frontend will start doing once this ships
- Wire the mock `requestRefund(id)` to a real endpoint.
- Build the backoffice **Payments & Finance** section (missing from admin nav today): payments, refunds, reconciliation queue, unmatched events, settlements.