Overview / Disputes
ADR 0035
Disputes: a thin hold projection and internal triad in commerce-api; the chargeback books in the worker
AcceptedDisputes
Context
When a card payment is disputed, three things must happen: the disputed funds must be kept out of payouts while the case is open, the actual chargeback debit must eventually post, and some system must own the dispute case. These land in different trust zones — commerce-risk owns risk/cases (ADR 0024), commerce-api owns the ledger and balances, and background jobs run in commerce-worker (ADR 0014/0019).
Decision
Split dispute handling across services by responsibility, with commerce-api holding only a thin projection and a narrow internal triad.
- A dispute hold is a balance overlay, not a ledger entry. An open hold is a
dispute_holdsrow that is subtracted at compute time from available balance (available = running balance − pending payouts − Σ open dispute holds, ADR 0031) and never writes a ledger entry or touches the running balance — so contested funds cannot be paid out while a dispute is open, without polluting the immutable ledger with a reversible reservation. - A three-endpoint internal triad. commerce-api exposes dispute txn-lookup, hold/release, and settle on an internal pipeline, authenticated by the shared
DISPUTE_LOOKUP_TOKEN(ADR 0028). commerce-risk drives it and remains the dispute case source of truth. - The money moves in the worker. The real chargeback debit and the order status flip run in commerce-worker (commerce-api runs no Oban queues — it still runs legacy Verk, ADR 0014), consistent with the job split (ADR 0019).
Consequences
- Positive: disputed funds are protected the instant a hold is placed, with no reversible ledger bookkeeping; each service does its part (risk = case, api = balance/holds, worker = the debit).
- Positive: the cross-service surface is a small, explicitly-authenticated triad rather than a shared dispute database.
- Current state: this is largely built end-to-end — the commerce-api hold overlay + triad, the worker-side chargeback debit, and the commerce-risk case-ingestion half (email/IMAP intake, an Anthropic dispute classifier, a case spine + portal/dashboard, cron-scheduled) are all shipped; only prod auto-match and outbound bank replies remain. (Earlier notes calling ingestion "planned" are stale.)
- Neutral: distinct from ADR 0024 — that is risk scoring/policy; this is the money-movement and service split for a confirmed dispute.
Alternatives considered
- Book the hold as a ledger entry (hold/release pair): rejected — it puts reversible reservations in the permanent ledger; a compute-time overlay (ADR 0031) keeps holds out of the record of settled money.
- Handle the entire dispute in one service: rejected — it would recouple the trust zones (ADR 0019); risk owns the case, commerce-api owns balances, the worker owns the async debit.
- Trust the internal dispute calls on the private network: rejected — the triad is explicitly authenticated (ADR 0028) even though it is internal.