# ADR-0002: Dependency direction — `settlement_core → dispute_core`, never the reverse

## Status

Accepted

## Context

Extracting chargeback code into `apps/dispute_core` (ADR-0001) runs into a cross-domain coupling problem:

- `SettlementCore.SettlementMisGenerator` reads `ChargebackCase` directly and calls its `mark_recovered_changeset/2` to auto-create a debit `MerchantAdjustment` when a case resolves as `recovered`.
- `ChargebackCase.CsvImporter` (the chargeback intake path) writes `chargeback_hold = true` onto the matching `SettlementCore.CoreTransaction` row when a new case is imported.

If `dispute_core` depended on `settlement_core` (for `CoreTransaction`) *and* `settlement_core` depended on `dispute_core` (for the case schema), the umbrella would have a circular compile-time dependency between the two apps, which Mix does not support.

## Decision

Make the dependency strictly one-directional: `settlement_core → dispute_core`.

- `dispute_core` depends on nothing but `platform_core` (same as `risk_core`) — no dependency on `settlement_core`.
- The CSV importer's chargeback-hold write is done via a raw parameterized SQL `UPDATE` against `core_transactions` (`Ecto.Adapters.SQL.query!/3`) instead of importing the `SettlementCore.CoreTransaction` schema module. A single boolean-column flag flip does not need a full schema dependency.
- `settlement_core` takes the new umbrella dependency on `dispute_core`; `SettlementMisGenerator` is updated to alias `DisputeCore.Case` and call `DisputeCore.mark_recovered/2`.
- `risk_core`'s existing raw-SQL queries against the `chargeback_cases` table (used by two chargeback-frequency risk rules) require **no changes** — they query the table directly by name, not through the Ecto schema module, so they are unaffected by which app owns that module.
- `platform_web` adds `{:dispute_core, in_umbrella: true}` alongside its existing `settlement_core` dependency; the chargeback controller, router, and LiveView switch their aliases from `SettlementCore.*` to `DisputeCore.*`.

## Alternatives considered

- **PubSub-decoupled bidirectional events** (dispute_core broadcasts lifecycle events, settlement_core subscribes without a compile dependency in either direction). Rejected for now — only one narrow write (`chargeback_hold`) crosses the boundary today, and introducing a second coordination mechanism (PubSub) alongside direct function calls adds indirection without a matching payoff at current scope. Revisit this if more cross-domain reactions accumulate (e.g. multiple apps needing to react to case-state changes) — at that point routing all cross-domain reactions to `dispute_core` events through PubSub, rather than growing `settlement_core`'s single direct-call dependency, may become the better trade-off.

## Consequences

- `dispute_core` stays a low-level, dependency-free domain app, easy to reason about and reuse.
- `settlement_core` carries one new umbrella dependency; this is acceptable since `settlement_core` already sits "above" several other apps in practice (nothing currently depends on `settlement_core`'s chargeback code except `platform_web` and `risk_core`'s raw SQL).
- The raw-SQL `UPDATE` in the CSV importer is a deliberate, narrow exception to the "always use the Ecto schema" convention — call this out in the importer's moduledoc so it isn't "cleaned up" into a schema dependency later, which would reintroduce the cycle this ADR avoids.
