Files
marketplaces/docs/context/adrs/ADR-0003-generic-partner-provisioning-api.md
sdarbinyan 71da5a8d80
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
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

5.3 KiB

id, title, status, date, supersedes, tags
id title status date supersedes tags
ADR-0003 Build partner merchant-provisioning as a generic API, not a per-partner integration active 2026-08-18
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). No company, no project.
  2. Payments carry no store dimension (Phase 1 §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 is outbound/ingest — the opposite direction.

Decision

Build one generic partner provisioning API. Contract: 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) 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 §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). The provisioning API is backend-side; nothing about it belongs in a frontend package.