Inkress ADRsarchitecture decisions
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_holds row 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.
← 0034 Orders are immutable commercial records; money movement lives in separate ledger transactions 0036 Media is stored in S3 behind cdn.inkress.com, uploaded via the API with content-addressed dedup →