docs: partner provisioning API contract, routing context, Track P
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>
This commit is contained in:
sdarbinyan
2026-08-18 11:22:24 +04:00
parent 551a22a245
commit 71da5a8d80
11 changed files with 560 additions and 10 deletions

View File

@@ -0,0 +1,72 @@
---
id: ADR-0003
title: Build partner merchant-provisioning as a generic API, not a per-partner integration
status: active
date: 2026-08-18
supersedes: []
tags: [architecture, api, payments, multi-tenant, security, decision]
---
# ADR-0003: Build partner merchant-provisioning as a generic API, not a per-partner integration
## Context
A partner asked (2026-08-18) for an API to programmatically manage a merchant hierarchy — Company → Project → Store → PaymentPoint — with idempotent provisioning, `externalReference` lookup, TEST/LIVE separation, public-key credentials with scoped authority and rotation, and payment/callback fields that route a payment unambiguously to one store.
Their request arrived written in their own vocabulary. Building against that vocabulary directly would produce a partner-shaped API, and the next partner asking for the same capability with different level names would either get a second parallel surface or force a rename through our schema.
Three facts about our current model made the ask non-trivial:
1. Nothing exists above `Marketplace` ([Phase 9](../../backend/PHASE-9-TENANT-REGISTRY-DOMAINS-CONTRACT.md)). No company, no project.
2. Payments carry no store dimension ([Phase 1](../../backend/PHASE-1-MONEY-FX-PAYMENTS-CONTRACT.md) §6). Reconciliation can reconstruct *why* an amount was charged but not *who for*.
3. We have no partner-facing write API at all. [Phase 4](../../backend/PHASE-4-CONNECTOR-FRAMEWORK-CONTRACT.md) is outbound/ingest — the opposite direction.
## Decision
Build one generic partner provisioning API. Contract: [PARTNER-PROVISIONING-API-CONTRACT.md](../../backend/PARTNER-PROVISIONING-API-CONTRACT.md).
### 1. Partner-specific behaviour is config, never schema
No partner name appears in any entity, field, endpoint, or status value. Everything partner-varying lives in a `PartnerProfile` row: which levels are required, level name aliases, routing field names, rate-limit tier, key rotation window, webhook field map. **Onboarding a partner is a config row, not a deployment.**
Deliberately *not* configurable, because configurability there breaks reconciliation or safety: status values and transitions, idempotency semantics, environment partitioning, signature scheme, the four-level ceiling.
### 2. Fixed four levels with optional middles, not a free-form tree
`company → project → store → payment_point`. Middle levels are omittable per partner profile; depth is never partner-defined. An arbitrary-depth tree would push every downstream consumer — routing, reconciliation, settlement, audit — into handling shapes no partner actually has.
### 3. Credentials are node-scoped
The partner asked us to choose between per-company, per-project, and per-store credentials. We answer all three with one mechanism: a credential binds to **any single node**, and its authority is that node's subtree. Partner keypairs are partner-generated; we hold only the public key. Rotation runs with a bounded overlap; revocation is immediate and irreversible.
### 4. Level mapping onto our model
| Partner level | Our entity |
|---|---|
| `company` | new, thin |
| `project` | new, thin — a product line (e.g. `marketplaces`) |
| `store` | `Marketplace` (Phase 9), gains `companyId`/`projectId`/`externalReference` |
| `payment_point` | new — one payment method accepted at one marketplace (`qr`, `card`; both ship today) |
`PaymentPoint` is an acceptance channel, not a physical till and not a settlement account. Registering one never enables real money — financial enablement is a separate approved flow that sets `providerAccountRef`.
### 5. Seller is excluded from the hierarchy
`Seller` ([Phase 5](../../backend/PHASE-5-SELLER-PORTAL-CONTRACT.md)) is orthogonal. A payment routes to one payment point, is reconciled there, and only then splits across the sellers whose lines the order contains ([Phase 7](../../backend/PHASE-7-PAYMENTS-RECONCILIATION-CONTRACT.md) §3.1). Putting `Seller` in the partner hierarchy would force every partner to model our multi-seller concept, which most do not have.
### 6. RoutingContext lands in Phase 1 before implementation, not after
`RoutingContext` (companyId, routingPath, leafNodeId, environment, merchantReference, providerPaymentId) is required on `CheckoutSession`, `PaymentIntent`, `Payment`, `Refund`, `ReconciliationRecord`. Frozen at checkout-session creation, immutable thereafter.
## Consequences
**Cost now:** two new entities (`Company`, `Project`), one new leaf (`PaymentPoint`), three amended contracts (Phases 1, 7, 9) plus Track S §4.1, and a backfill for existing marketplaces (Phase 9 §1.2).
**Cost avoided:** retrofitting a routing dimension onto a populated payments table after launch; a second parallel provisioning surface for partner number two.
**Accepted limits:**
- A partner needing more than four levels cannot be served without a contract change. Judged unlikely enough to be worth the simplicity.
- Backfilled rows carry a synthetic company and project. `externalReference` stays null for them.
- Partners cannot create companies through the API — company creation stays a commercial, out-of-band action.
**Unaffected:** the `@marketplaces/auth` / `@marketplaces/payment` package split ([ADR-0001](ADR-0001-extract-auth-and-payment-into-shared-marketplaces-packages.md)). The provisioning API is backend-side; nothing about it belongs in a frontend package.

View File

@@ -5,3 +5,9 @@
{"id":"PV-20260713T000000Z-0005","subject":"admin-app","predicate":"is-isolated-from","object":"marketplace storefront bundle: admin code never ships to storefront and vice versa, though they may share a domain","src":["docs/context/adrs/ADR-0001-marketplace-platform-vision.md"],"status":"active","kind":"constraint","updated_at":"2026-07-13T00:00:00Z","confidence":"high","tags":["admin","security"]}
{"id":"PV-20260713T000000Z-0006","subject":"widgets","predicate":"must-not-own","object":"page spacing or page width; the renderer owns sections, spacing, and page width, widgets own only their internal layout","src":["docs/context/adrs/ADR-0001-marketplace-platform-vision.md"],"status":"active","kind":"constraint","updated_at":"2026-07-13T00:00:00Z","confidence":"high","tags":["widgets","layout"]}
{"id":"PV-20260818T001500Z-a1f3","subject":"auth-and-payment-client-logic","predicate":"is-decided-to-extract-into","object":"standalone versioned npm packages @marketplaces/auth and @marketplaces/payment, installed as dependencies rather than edited in-repo","src":["docs/context/adrs/ADR-0001-extract-auth-and-payment-into-shared-marketplaces-packages.md"],"status":"active","kind":"decision","updated_at":"2026-08-18T00:15:00Z","confidence":"high","tags":["architecture","auth","payment","decision"]}
{"id":"PV-20260818T104000Z-c7d1","subject":"partner-merchant-provisioning","predicate":"is-decided-to-build-as","object":"one generic inbound API where all partner-specific behaviour is a PartnerProfile config row (required levels, level aliases, routing field names, rate tier); no partner name appears in any entity, field, endpoint or status value","src":["docs/context/adrs/ADR-0003-generic-partner-provisioning-api.md","docs/backend/PARTNER-PROVISIONING-API-CONTRACT.md"],"status":"active","kind":"decision","updated_at":"2026-08-18T10:40:00Z","confidence":"high","tags":["architecture","api","partner","decision"]}
{"id":"PV-20260818T104100Z-e2b8","subject":"partner-hierarchy-levels","predicate":"map-onto","object":"company and project are new thin entities above Marketplace; store IS Marketplace (Phase 9); payment_point is new and equals one payment method accepted at one marketplace (qr, card)","src":["docs/context/adrs/ADR-0003-generic-partner-provisioning-api.md","docs/backend/PHASE-9-TENANT-REGISTRY-DOMAINS-CONTRACT.md"],"status":"active","kind":"decision","updated_at":"2026-08-18T10:41:00Z","confidence":"high","tags":["architecture","multi-tenant","payments","decision"]}
{"id":"PV-20260818T104200Z-f5a9","subject":"Seller","predicate":"is-excluded-from","object":"the partner provisioning hierarchy; a payment routes to exactly one payment point, is reconciled there, and only then splits across sellers in Phase 7 settlement","src":["docs/context/adrs/ADR-0003-generic-partner-provisioning-api.md","docs/backend/PHASE-7-PAYMENTS-RECONCILIATION-CONTRACT.md"],"status":"active","kind":"constraint","updated_at":"2026-08-18T10:42:00Z","confidence":"high","tags":["payments","reconciliation","sellers"]}
{"id":"PV-20260818T104300Z-b3c4","subject":"RoutingContext","predicate":"is-required-on","object":"CheckoutSession, PaymentIntent, Payment, Refund and ReconciliationRecord; frozen at checkout-session creation and immutable thereafter, so a payment is always attributable to exactly one payment point","src":["docs/backend/PHASE-1-MONEY-FX-PAYMENTS-CONTRACT.md","docs/backend/PHASE-7-PAYMENTS-RECONCILIATION-CONTRACT.md"],"status":"active","kind":"constraint","updated_at":"2026-08-18T10:43:00Z","confidence":"high","tags":["payments","reconciliation","contract"]}
{"id":"PV-20260818T104400Z-d9e2","subject":"partner-api-credentials","predicate":"are-scoped-by","object":"a single node whose subtree defines authority; we hold only the partner-generated public key, rotation runs on a bounded overlap window and revocation is immediate and irreversible","src":["docs/backend/PARTNER-PROVISIONING-API-CONTRACT.md","docs/backend/TRACK-S-SECURITY-RBAC-CONTRACT.md"],"status":"active","kind":"decision","updated_at":"2026-08-18T10:44:00Z","confidence":"high","tags":["security","credentials","partner"]}
{"id":"PV-20260818T104500Z-a6f7","subject":"checkout-payment-methods","predicate":"already-support","object":"both qr and card end to end in src/app/pages/cart/cart.component.ts (separate create paths and separate status pollers); card is not an outstanding gap","src":["src/app/pages/cart/cart.component.ts","src/app/services/api.service.ts"],"status":"active","kind":"implemented","updated_at":"2026-08-18T10:45:00Z","confidence":"high","tags":["payments","frontend"]}