Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Collapses the entire docs/backend/ set - Phase 1-10, Track A/S, the partner API, the two handoffs, the frontend surface inventory, and the harvest requirements - into a single source of truth, docs/backend/BACKEND-INTEGRATION.md. Every contract's entities, endpoints, and invariants are preserved, reorganised by domain rather than by sprint. The nine release invariants, the FH-* harvest mechanisms, the RBAC/audit/secrets cross-cutting rules, the tenant-routing infra contract, the 15 acceptance tests, build order, dev setup, and open decisions are all in the one file, with a change log (§14) at the bottom. The file opens with the maintenance rule: any new backend need, contract change, or shipped item updates this file in the same change - the affected section and the change log. No new backend .md files. Inbound links from BACKEND-API-REFERENCE, the ADRs, the fork docs, DEPLOYMENT, PACKAGES-USAGE, the delivery plan, and e2e/README are repointed at the single doc (section anchors collapse to the file; the prose section refs remain as context). Also recorded the rule in the repo CLAUDE.md. 17 backend docs removed, 1 added. No implementation changes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
73 lines
5.2 KiB
Markdown
73 lines
5.2 KiB
Markdown
---
|
|
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/BACKEND-INTEGRATION.md)). No company, no project.
|
|
2. Payments carry no store dimension ([Phase 1](../../backend/BACKEND-INTEGRATION.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/BACKEND-INTEGRATION.md) is outbound/ingest — the opposite direction.
|
|
|
|
## Decision
|
|
|
|
Build one generic partner provisioning API. Contract: [../../backend/BACKEND-INTEGRATION.md](../../backend/BACKEND-INTEGRATION.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/BACKEND-INTEGRATION.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/BACKEND-INTEGRATION.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.
|