B-44: intercompany ledger events, subject review and drill-down evidence

- migration 6: manual_records, ledger_event_revisions chain, current
  projections, source claims, subject suggestions, eligible_position_events
- ledger_events.py: bank-event reconciliation, reversal/adjustment/reopen,
  append-only revision chain and rebuildable current projection
- subjects.py: fixed subject mirror, draft suggestion dictionary, explicit
  administrator subject confirmation with expected_revision + idempotency
- manual_records.py: submit, approve new/link, return/exception/reverse,
  candidate hints, idempotent replay and concurrency-safe claims
- positions.py: Decimal aggregation, both-perspective conservation asserts,
  cutoff window, unresolved gross buckets, keyset pagination, evidence
  visibility (visible/masked/missing)
- server.py: admin + company intercompany APIs with tenant isolation (404 on
  cross-tenant reads, 403 on company writes) and auto reconcile wiring
- admin/company portals: balance directory, pair drill-down drawer, evidence
  drawer, subject/manual audit queue, company balance summary
- tests: ledger events, subjects, manual records, positions, HTTP API and
  migration persistence (233 total, all green)
This commit is contained in:
腾讯WorkBuddy
2026-08-19 11:58:25 +08:00
parent f99917321b
commit 85293b79df
18 changed files with 6902 additions and 142 deletions
+105 -5
View File
@@ -41,7 +41,7 @@ class PersistenceTestCase(unittest.TestCase):
class MigrationTests(PersistenceTestCase):
def test_migrate_creates_schema_and_is_idempotent(self) -> None:
first = applied_versions(self.connection)
self.assertEqual([1, 2, 3, 4, 5], first)
self.assertEqual([1, 2, 3, 4, 5, 6], first)
self.assertEqual([], migrate(self.connection))
self.assertEqual(first, applied_versions(self.connection))
tables = {
@@ -73,19 +73,28 @@ class MigrationTests(PersistenceTestCase):
"transfer_match_candidates",
"current_transfer_decisions",
"transfer_observation_claims",
"manual_records",
"manual_record_decisions",
"current_manual_record_decisions",
"ledger_events",
"ledger_event_revisions",
"current_ledger_event_revisions",
"ledger_event_bank_sources",
"ledger_event_manual_sources",
"ledger_subject_suggestions",
"schema_migrations",
):
self.assertIn(table, tables)
def test_rollback_removes_schema_and_forward_rebuilds_it(self) -> None:
self.assertEqual([5, 4, 3, 2, 1], rollback(self.connection, 0))
self.assertEqual([6, 5, 4, 3, 2, 1], rollback(self.connection, 0))
self.assertEqual([], applied_versions(self.connection))
remaining = self.connection.execute(
"SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'source_rows'"
).fetchone()
self.assertIsNone(remaining)
self.assertEqual([1, 2, 3, 4, 5], migrate(self.connection))
self.assertEqual([1, 2, 3, 4, 5], applied_versions(self.connection))
self.assertEqual([1, 2, 3, 4, 5, 6], migrate(self.connection))
self.assertEqual([1, 2, 3, 4, 5, 6], applied_versions(self.connection))
def test_rollback_to_4_keeps_bank_evidence_and_drops_event_layer(self) -> None:
self.import_sample()
@@ -93,7 +102,7 @@ class MigrationTests(PersistenceTestCase):
"SELECT COUNT(*) AS n FROM source_rows"
).fetchone()["n"]
self.assertGreater(row_count, 0)
self.assertEqual([5], rollback(self.connection, 4))
self.assertEqual([6, 5], rollback(self.connection, 4))
# The pre-migration evidence and schema are untouched.
self.assertEqual(
row_count,
@@ -109,6 +118,10 @@ class MigrationTests(PersistenceTestCase):
"SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'transfer_match_decisions'"
).fetchone()
self.assertIsNone(remaining)
ledger_remaining = self.connection.execute(
"SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'ledger_event_revisions'"
).fetchone()
self.assertIsNone(ledger_remaining)
def test_event_layer_views_exist_after_migration(self) -> None:
view = self.connection.execute(
@@ -116,6 +129,93 @@ class MigrationTests(PersistenceTestCase):
).fetchone()
self.assertIsNotNone(view)
def test_ledger_layer_views_exist_after_migration(self) -> None:
view = self.connection.execute(
"SELECT name FROM sqlite_master WHERE type = 'view' AND name = 'eligible_position_events'"
).fetchone()
self.assertIsNotNone(view)
table = self.connection.execute(
"SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'ledger_event_revisions'"
).fetchone()
self.assertIsNotNone(table)
def test_ledger_revision_log_is_immutable(self) -> None:
with self.connection:
self.connection.execute(
"INSERT INTO companies (name, created_at, updated_at) VALUES ('甲公司', ?, ?)",
(utc_now(), utc_now()),
)
self.connection.execute(
"INSERT INTO companies (name, created_at, updated_at) VALUES ('乙公司', ?, ?)",
(utc_now(), utc_now()),
)
company_a, company_b = [
row["id"]
for row in self.connection.execute("SELECT id FROM companies ORDER BY id")
]
self.connection.execute(
"INSERT INTO ledger_events (lifecycle, created_at) VALUES ('active', ?)",
(utc_now(),),
)
event_id = self.connection.execute(
"SELECT id FROM ledger_events LIMIT 1"
).fetchone()["id"]
self.connection.execute(
"""
INSERT INTO ledger_event_revisions (
ledger_event_id, revision, state, effective_at, amount,
amount_scale, currency, payer_company_id, payee_company_id,
source_kind, posting_kind, created_at
) VALUES (?, 1, 'pending_subject', '2026-01-01T00:00:00', '1.00', 2,
'CNY', ?, ?, 'bank', 'normal', ?)
""",
(event_id, company_a, company_b, utc_now()),
)
for statement in (
"UPDATE ledger_event_revisions SET amount = '9.99'",
"DELETE FROM ledger_event_revisions",
"UPDATE ledger_events SET created_at = '2020-01-01T00:00:00'",
"DELETE FROM ledger_events",
):
with self.subTest(statement=statement):
with self.assertRaises(sqlite3.IntegrityError):
self.connection.execute(statement)
self.connection.rollback()
def test_manual_record_facts_are_immutable(self) -> None:
with self.connection:
self.connection.execute(
"INSERT INTO companies (name, created_at, updated_at) VALUES ('甲公司', ?, ?)",
(utc_now(), utc_now()),
)
self.connection.execute(
"INSERT INTO companies (name, created_at, updated_at) VALUES ('乙公司', ?, ?)",
(utc_now(), utc_now()),
)
company_a, company_b = [
row["id"]
for row in self.connection.execute("SELECT id FROM companies ORDER BY id")
]
self.connection.execute(
"""
INSERT INTO manual_records (
company_id, counterparty_company_id, occurred_at, direction,
amount, amount_scale, currency, funding_source, requested_subject,
request_key, created_at
) VALUES (?, ?, '2026-01-01T00:00:00', 'incoming', '1.00', 2,
'CNY', 'other', 'receivable', 'k1', ?)
""",
(company_a, company_b, utc_now()),
)
for statement in (
"UPDATE manual_records SET amount = '9.99'",
"DELETE FROM manual_records",
):
with self.subTest(statement=statement):
with self.assertRaises(sqlite3.IntegrityError):
self.connection.execute(statement)
self.connection.rollback()
def test_decision_log_immutability_triggers(self) -> None:
self.import_sample()
source_row_id = self.connection.execute(