# 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`.