2026-08-17 23:04:11 +04:00
# Phase 7 Backend Contract — Refunds + Reconciliation
Companion to [PRODUCT-PLAN-v3.1-DELIVERY-PLAN.md ](../PRODUCT-PLAN-v3.1-DELIVERY-PLAN.md ) Phase 7 (Sprints 7.1– 7.3). Extends [Phase 1 ](PHASE-1-MONEY-FX-PAYMENTS-CONTRACT.md ) §6 (payment state machine).
**Status: ready to build.** `requestRefund(id)` exists today only as a mock gateway method; `reconcil*` and `settlement*` return zero hits anywhere in the codebase.
---
## 1. Refunds
```ts
interface Refund {
id: string;
orderId: string;
orderLineIds: string[]; // which lines this refund covers - partial refunds must specify
amount: Money;
reason: string;
actor: string; // user id who initiated it, never anonymous
status: 'requested' | 'approved' | 'processing' | 'completed' | 'failed';
requestedAt: string;
completedAt?: string;
docs: partner provisioning API contract, routing context, Track P
A partner integration request landed for programmatic merchant-hierarchy
management (Company/Project/Store/PaymentPoint). Built the answer generically:
partner-specific behaviour is a PartnerProfile config row, and no partner name
appears in any entity, field, endpoint or status value.
New:
- docs/backend/PARTNER-PROVISIONING-API-CONTRACT.md - hierarchy, idempotency,
node-scoped public-key credentials, TEST/LIVE partition, routing context
- docs/context/adrs/ADR-0003-generic-partner-provisioning-api.md
Amended, because the schema impact must land before Phase 1 is implemented:
- Phase 1 gains RoutingContext on CheckoutSession/PaymentIntent/Payment,
frozen at checkout-session creation and immutable after
- Phase 7 gains routing on Refund/ReconciliationRecord, plus the rule that
seller settlement splits happen after routing, never as a hierarchy level
- Phase 9 gains Company/Project above Marketplace and PaymentPoint below it,
with a backfill sequence for existing marketplaces
- Track S gains partner credentials: public key only, node-scoped authority,
rotation with overlap, immediate revoke, audit coverage
Also: Track P (P1-P10) in the delivery plan, and backend ownership closed as
answered across the contract set.
Card payment was checked, not added - qr and card both already ship in
cart.component.ts with separate create paths and status pollers.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 11:22:24 +04:00
routing: RoutingContext; // copied verbatim from the original Payment, never recomputed
2026-08-17 23:04:11 +04:00
}
```
docs: partner provisioning API contract, routing context, Track P
A partner integration request landed for programmatic merchant-hierarchy
management (Company/Project/Store/PaymentPoint). Built the answer generically:
partner-specific behaviour is a PartnerProfile config row, and no partner name
appears in any entity, field, endpoint or status value.
New:
- docs/backend/PARTNER-PROVISIONING-API-CONTRACT.md - hierarchy, idempotency,
node-scoped public-key credentials, TEST/LIVE partition, routing context
- docs/context/adrs/ADR-0003-generic-partner-provisioning-api.md
Amended, because the schema impact must land before Phase 1 is implemented:
- Phase 1 gains RoutingContext on CheckoutSession/PaymentIntent/Payment,
frozen at checkout-session creation and immutable after
- Phase 7 gains routing on Refund/ReconciliationRecord, plus the rule that
seller settlement splits happen after routing, never as a hierarchy level
- Phase 9 gains Company/Project above Marketplace and PaymentPoint below it,
with a backfill sequence for existing marketplaces
- Track S gains partner credentials: public key only, node-scoped authority,
rotation with overlap, immediate revoke, audit coverage
Also: Track P (P1-P10) in the delivery plan, and backend ownership closed as
answered across the contract set.
Card payment was checked, not added - qr and card both already ship in
cart.component.ts with separate create paths and status pollers.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 11:22:24 +04:00
A refund always carries the routing context of the payment it reverses. It is copied, not re-resolved — a store suspended after the payment must still be refundable.
2026-08-17 23:04:11 +04:00
```
POST /api/admin/v2/orders/{orderId}/refunds { orderLineIds, amount, reason }
GET /api/admin/v2/orders/{orderId}/refunds
```
A `Refund` updates `Payment.status` to `refunded` or `partially_refunded` (Phase 1 §6.1) and emits `refund.requested` /`refund.completed` on the Phase 2 event bus.
## 2. Reconciliation
```ts
interface ReconciliationRecord {
id: string;
orderId: string;
providerPaymentId?: string;
internalAmount: Money;
providerAmount?: Money;
matchStrategy: 'provider_payment_id' | 'merchant_reference' | 'amount_currency_fallback';
result: 'matched' | 'unmatched' | 'duplicate' | 'amount_mismatch' | 'status_mismatch';
resolvedBy?: string;
resolvedAt?: string;
resolutionNote?: string;
docs: partner provisioning API contract, routing context, Track P
A partner integration request landed for programmatic merchant-hierarchy
management (Company/Project/Store/PaymentPoint). Built the answer generically:
partner-specific behaviour is a PartnerProfile config row, and no partner name
appears in any entity, field, endpoint or status value.
New:
- docs/backend/PARTNER-PROVISIONING-API-CONTRACT.md - hierarchy, idempotency,
node-scoped public-key credentials, TEST/LIVE partition, routing context
- docs/context/adrs/ADR-0003-generic-partner-provisioning-api.md
Amended, because the schema impact must land before Phase 1 is implemented:
- Phase 1 gains RoutingContext on CheckoutSession/PaymentIntent/Payment,
frozen at checkout-session creation and immutable after
- Phase 7 gains routing on Refund/ReconciliationRecord, plus the rule that
seller settlement splits happen after routing, never as a hierarchy level
- Phase 9 gains Company/Project above Marketplace and PaymentPoint below it,
with a backfill sequence for existing marketplaces
- Track S gains partner credentials: public key only, node-scoped authority,
rotation with overlap, immediate revoke, audit coverage
Also: Track P (P1-P10) in the delivery plan, and backend ownership closed as
answered across the contract set.
Card payment was checked, not added - qr and card both already ship in
cart.component.ts with separate create paths and status pollers.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 11:22:24 +04:00
routing: RoutingContext; // from the Payment; makes every row attributable to one payment point
2026-08-17 23:04:11 +04:00
}
```
Process (per plan §7.3):
```
1. Collect internal paid orders for a period.
2. Fetch provider transactions/events for the same period.
3. Match by providerPaymentId, falling back to merchant reference, falling back to amount+currency.
4. Classify: matched / unmatched / duplicate / amount_mismatch / status_mismatch.
5. Surface the non-matched set in backoffice with controlled, audited resolution.
```
```
docs: partner provisioning API contract, routing context, Track P
A partner integration request landed for programmatic merchant-hierarchy
management (Company/Project/Store/PaymentPoint). Built the answer generically:
partner-specific behaviour is a PartnerProfile config row, and no partner name
appears in any entity, field, endpoint or status value.
New:
- docs/backend/PARTNER-PROVISIONING-API-CONTRACT.md - hierarchy, idempotency,
node-scoped public-key credentials, TEST/LIVE partition, routing context
- docs/context/adrs/ADR-0003-generic-partner-provisioning-api.md
Amended, because the schema impact must land before Phase 1 is implemented:
- Phase 1 gains RoutingContext on CheckoutSession/PaymentIntent/Payment,
frozen at checkout-session creation and immutable after
- Phase 7 gains routing on Refund/ReconciliationRecord, plus the rule that
seller settlement splits happen after routing, never as a hierarchy level
- Phase 9 gains Company/Project above Marketplace and PaymentPoint below it,
with a backfill sequence for existing marketplaces
- Track S gains partner credentials: public key only, node-scoped authority,
rotation with overlap, immediate revoke, audit coverage
Also: Track P (P1-P10) in the delivery plan, and backend ownership closed as
answered across the contract set.
Card payment was checked, not added - qr and card both already ship in
cart.component.ts with separate create paths and status pollers.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 11:22:24 +04:00
GET /api/admin/v2/reconciliation/queue?marketplaceId=&companyId=&projectId=&leafNodeId=&result=
2026-08-17 23:04:11 +04:00
POST /api/admin/v2/reconciliation/{id}/resolve { note }
```
docs: partner provisioning API contract, routing context, Track P
A partner integration request landed for programmatic merchant-hierarchy
management (Company/Project/Store/PaymentPoint). Built the answer generically:
partner-specific behaviour is a PartnerProfile config row, and no partner name
appears in any entity, field, endpoint or status value.
New:
- docs/backend/PARTNER-PROVISIONING-API-CONTRACT.md - hierarchy, idempotency,
node-scoped public-key credentials, TEST/LIVE partition, routing context
- docs/context/adrs/ADR-0003-generic-partner-provisioning-api.md
Amended, because the schema impact must land before Phase 1 is implemented:
- Phase 1 gains RoutingContext on CheckoutSession/PaymentIntent/Payment,
frozen at checkout-session creation and immutable after
- Phase 7 gains routing on Refund/ReconciliationRecord, plus the rule that
seller settlement splits happen after routing, never as a hierarchy level
- Phase 9 gains Company/Project above Marketplace and PaymentPoint below it,
with a backfill sequence for existing marketplaces
- Track S gains partner credentials: public key only, node-scoped authority,
rotation with overlap, immediate revoke, audit coverage
Also: Track P (P1-P10) in the delivery plan, and backend ownership closed as
answered across the contract set.
Card payment was checked, not added - qr and card both already ship in
cart.component.ts with separate create paths and status pollers.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 11:22:24 +04:00
Step 3's `merchant_reference` strategy matches on `RoutingContext.merchantReference` — the partner-supplied value, stored verbatim (Phase 1 §6.5). The queue is filterable at every hierarchy level so an unmatched set can be narrowed to one payment point without a join the backoffice has to build itself.
2026-08-17 23:04:11 +04:00
## 3. Settlements
```ts
interface Settlement {
id: string;
sellerId: string;
periodStart: string;
periodEnd: string;
grossAmount: Money;
commission: Money;
refunds: Money;
netPayout: Money;
status: 'pending' | 'paid';
}
```
```
GET /api/seller/v1/finance/settlements
docs: partner provisioning API contract, routing context, Track P
A partner integration request landed for programmatic merchant-hierarchy
management (Company/Project/Store/PaymentPoint). Built the answer generically:
partner-specific behaviour is a PartnerProfile config row, and no partner name
appears in any entity, field, endpoint or status value.
New:
- docs/backend/PARTNER-PROVISIONING-API-CONTRACT.md - hierarchy, idempotency,
node-scoped public-key credentials, TEST/LIVE partition, routing context
- docs/context/adrs/ADR-0003-generic-partner-provisioning-api.md
Amended, because the schema impact must land before Phase 1 is implemented:
- Phase 1 gains RoutingContext on CheckoutSession/PaymentIntent/Payment,
frozen at checkout-session creation and immutable after
- Phase 7 gains routing on Refund/ReconciliationRecord, plus the rule that
seller settlement splits happen after routing, never as a hierarchy level
- Phase 9 gains Company/Project above Marketplace and PaymentPoint below it,
with a backfill sequence for existing marketplaces
- Track S gains partner credentials: public key only, node-scoped authority,
rotation with overlap, immediate revoke, audit coverage
Also: Track P (P1-P10) in the delivery plan, and backend ownership closed as
answered across the contract set.
Card payment was checked, not added - qr and card both already ship in
cart.component.ts with separate create paths and status pollers.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 11:22:24 +04:00
GET /api/admin/v2/finance/settlements?sellerId=&companyId=&projectId=&storeId=&period=
```
### 3.1 Seller split happens after routing
Added 2026-08-18. `Seller` is deliberately **not ** a level in the partner hierarchy ([PARTNER-PROVISIONING-API-CONTRACT.md §10.2 ](PARTNER-PROVISIONING-API-CONTRACT.md )). Order of operations:
2026-08-17 23:04:11 +04:00
```
docs: partner provisioning API contract, routing context, Track P
A partner integration request landed for programmatic merchant-hierarchy
management (Company/Project/Store/PaymentPoint). Built the answer generically:
partner-specific behaviour is a PartnerProfile config row, and no partner name
appears in any entity, field, endpoint or status value.
New:
- docs/backend/PARTNER-PROVISIONING-API-CONTRACT.md - hierarchy, idempotency,
node-scoped public-key credentials, TEST/LIVE partition, routing context
- docs/context/adrs/ADR-0003-generic-partner-provisioning-api.md
Amended, because the schema impact must land before Phase 1 is implemented:
- Phase 1 gains RoutingContext on CheckoutSession/PaymentIntent/Payment,
frozen at checkout-session creation and immutable after
- Phase 7 gains routing on Refund/ReconciliationRecord, plus the rule that
seller settlement splits happen after routing, never as a hierarchy level
- Phase 9 gains Company/Project above Marketplace and PaymentPoint below it,
with a backfill sequence for existing marketplaces
- Track S gains partner credentials: public key only, node-scoped authority,
rotation with overlap, immediate revoke, audit coverage
Also: Track P (P1-P10) in the delivery plan, and backend ownership closed as
answered across the contract set.
Card payment was checked, not added - qr and card both already ship in
cart.component.ts with separate create paths and status pollers.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 11:22:24 +04:00
payment -> routed to exactly one payment point (Phase 1 §6.5, frozen at checkout)
-> reconciled at that payment point
-> split across the sellers whose lines the order contains (this phase)
```
- A `Settlement` belongs to one seller **within one store ** . A seller trading in two stores gets two settlements per period, never one merged row.
- Splitting never rewrites `RoutingContext` . The money arrived at one payment point; the split decides who is owed from it.
- `grossAmount` summed across a store's settlements for a period must reconcile against that store's matched reconciliation rows for the same period. A mismatch is a reconciliation defect, not a rounding tolerance.
2026-08-17 23:04:11 +04:00
## 4. Provider breadth (open business question)
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.
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>
2026-08-21 11:12:05 +04:00
## 5. Idempotency belongs in the schema, not in a handler
Added 2026-08-21 (FH-2.2). [Phase 1 §6.3– 6.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
2026-08-17 23:04:11 +04:00
- 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.
- Reconciliation-queue resolution UI with full audit trail.