# 006: Intercompany positions, subject review and drill-down evidence (B-44)

Status: accepted (2026-08-19)

## Decision

The intercompany ledger adds one canonical layer above B-43's eligible events:

- **Append-only ledger events.** A canonical `ledger_event` carries an
  append-only `ledger_event_revisions` chain. A B-43 eligible bank event first
  becomes a `pending_subject` revision; an administrator confirms the subject
  into a `confirmed` revision. Corrections are never in-place edits — a
  reversal or adjustment is a *new* ledger event with its own effective date,
  and the original event keeps its history so earlier cutoffs are not
  rewritten.
- **Manual records as immutable submitted facts.** Companies submit
  `manual_records`; only an administrator-approved record becomes a confirmed
  ledger event (`approve_new`) or joins one (`approve_link`). Returned,
  exception and pending records never affect a balance and never leak to the
  counterparty. Approved facts change only through `reverse` (a new opposite
  event) — the original is never edited.
- **One perspective, fixed mirror.** Subjects are stored from one participating
  company's perspective (`receivable/payable/other_receivable/other_payable`);
  the other side is the fixed mirror (应收<->应付, 其他应收<->其他应付), so the
  two companies can never book conflicting subjects.
- **Subjects are confirmed, never auto-posted.** Bank summary/purpose text only
  feeds a deterministic *suggestion* dictionary (`subject-suggest-draft-v1`,
  not group-approved). Without an approved trade dictionary every bank event
  stays in subject review until an administrator confirms.
- **Decimal-only aggregation.** All money is stored as TEXT decimal strings and
  aggregated with Python `Decimal`. SQLite `SUM`, JavaScript `Number` and
  Python `float` never touch financial math. Different currencies are
  aggregated and displayed separately; nothing is converted to a group total.
- **Conservation is asserted.** For every company pair and currency the two
  perspectives must mirror exactly (`C_A == -C_B`); a violation raises a
  calculation exception instead of rendering an unbalanced number.
- **Unresolved is absolute gross.** Unresolved amounts are summed by absolute
  value per currency (never netted), broken down by reason
  (`subject_review`, `unmatched_single`, `manual_pending`), with count and
  gross amount exposed on every balance response.
- **B-45 boundary.** Until the B-45 opening balance exists, every response
  returns `opening.status=unavailable`, `opening.amount=null` and
  `result.kind=period_net_change`; the UI labels this "期间净变动", never
  "期末余额".

## Background

B-43 produces `eligible_intercompany_events` as the only bank-event entry
point. Before B-44 there was no canonical financial event, no statutory
subject, no manual-record approval, and no server-side balance API. The
revision-chain design is inherited from `transfer_match_decisions` in
migration 5 and from the append-only audit posture of the rest of the system.

## Consequences

- **Positive:** balances are deterministic, conservable, auditable and
  drillable from a group directory down to bank source rows; manual records
  cannot double count; corrections never mutate evidence; company portals are
  tenant-scoped on the server.
- **Negative:** pending bank events and returned/exception manual records are
  intentionally invisible to counterparties, which can surprise cashiers who
  expect symmetric disclosure; subject confirmation is manual until a
  group-approved dictionary exists.
- **Operational:** migration 6 is forward-only for production once
  approvals/revisions exist; pre-production it can be rolled back with
  `--rollback-to 5`. Read aggregation runs against the rebuildable current
  projection (no day snapshots yet); if B-45 monthly close needs them, immutable
  monthly snapshots can be added behind the same API contract.

## Files

- `src/bank_importer/ledger_events.py` — event lifecycle, revision chain,
  bank reconciliation, reversal/adjustment/reopen, projection rebuild.
- `src/bank_importer/subjects.py` — subject constants/mirror, suggestion
  dictionary, `confirm_subject`.
- `src/bank_importer/manual_records.py` — submission, approval
  (new/link), return/exception/reverse, candidate hints, idempotency.
- `src/bank_importer/positions.py` — Decimal aggregation, directories, pairs,
  events, evidence visibility, unresolved buckets, keyset pagination.
- `server.py` — `/api/admin/intercompany/*` and `/api/company/intercompany/*`
  plus `/api/admin/subject-reviews` and `/api/admin/manual-records`.
- `db.py` migration 6 — `manual_records`, `manual_record_decisions`,
  `ledger_events`, `ledger_event_revisions`, current-pointer projections,
  source claim tables, `ledger_subject_suggestions`, `eligible_position_events`.
- Tests: `test_ledger_events.py`, `test_manual_records.py`,
  `test_positions.py`, `test_positions_api.py`, extended
  `test_persistence.py`.
