Some checks failed
Architecture Governance / architecture (push) Has been cancelled
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>
113 lines
4.8 KiB
Markdown
113 lines
4.8 KiB
Markdown
# 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;
|
||
routing: RoutingContext; // copied verbatim from the original Payment, never recomputed
|
||
}
|
||
```
|
||
|
||
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.
|
||
|
||
```
|
||
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;
|
||
routing: RoutingContext; // from the Payment; makes every row attributable to one payment point
|
||
}
|
||
```
|
||
|
||
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.
|
||
```
|
||
|
||
```
|
||
GET /api/admin/v2/reconciliation/queue?marketplaceId=&companyId=&projectId=&leafNodeId=&result=
|
||
POST /api/admin/v2/reconciliation/{id}/resolve { note }
|
||
```
|
||
|
||
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.
|
||
|
||
## 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
|
||
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:
|
||
|
||
```
|
||
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.
|
||
|
||
## 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.
|
||
|
||
## 5. 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.
|
||
- Reconciliation-queue resolution UI with full audit trail.
|