HEL-282: 写库路径事务提交与审计留痕自查修复
同类未提交即 close 回滚、业务与审计拆成两笔事务的路径一并收进可嵌套事务边界。 Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
co-authored by
Cursor
multica-agent
parent
9e0e4a103a
commit
9193a3fce0
@@ -349,6 +349,66 @@ class CoverageGapTests(CalculationBase):
|
||||
).fetchone()
|
||||
self.assertEqual("closed_attested", closed["status"])
|
||||
|
||||
def test_attestation_and_audit_survive_connection_close(self) -> None:
|
||||
"""HEL-282: attestation writes used to skip the change log; review also
|
||||
nested-committed coverage recalculation before the overlap close."""
|
||||
from bank_importer.db import connect as db_connect
|
||||
|
||||
self.add_confirmed_row(
|
||||
self.company_a,
|
||||
account_id=self.account_a["id"],
|
||||
own_account="6222000000000001",
|
||||
at="2026-06-21T10:00:00",
|
||||
)
|
||||
calculation.recalculate_coverage_gaps(self.connection)
|
||||
gap = self.connection.execute(
|
||||
"SELECT * FROM coverage_gaps WHERE status = 'open'"
|
||||
).fetchone()
|
||||
cashier_id = auth.create_user(
|
||||
self.connection, "cashier-close", "CashierA123", "company", self.company_a
|
||||
)
|
||||
cashier = self.connection.execute(
|
||||
"SELECT * FROM users WHERE id = ?", (cashier_id,)
|
||||
).fetchone()
|
||||
att = calculation.submit_no_business_attestation(
|
||||
self.connection,
|
||||
company_id=self.company_a,
|
||||
bank_account_id=self.account_a["id"],
|
||||
gap_start=gap["gap_start"],
|
||||
gap_end=gap["gap_end"],
|
||||
reason="当日账户无资金往来",
|
||||
evidence=None,
|
||||
actor=cashier,
|
||||
)
|
||||
calculation.review_no_business_attestation(
|
||||
self.connection, att["id"], "approve", "审核通过", self.admin
|
||||
)
|
||||
att_id = att["id"]
|
||||
self.connection.close()
|
||||
fresh = db_connect(self.db_path)
|
||||
try:
|
||||
row = fresh.execute(
|
||||
"SELECT status FROM no_business_attestations WHERE id = ?", (att_id,)
|
||||
).fetchone()
|
||||
actions = [
|
||||
item["action"]
|
||||
for item in fresh.execute(
|
||||
"""
|
||||
SELECT action FROM audit_log
|
||||
WHERE action LIKE 'attestation_%'
|
||||
ORDER BY id
|
||||
"""
|
||||
).fetchall()
|
||||
]
|
||||
closed = fresh.execute(
|
||||
"SELECT status FROM coverage_gaps WHERE id = ?", (gap["id"],)
|
||||
).fetchone()
|
||||
finally:
|
||||
fresh.close()
|
||||
self.assertEqual("approved", row["status"])
|
||||
self.assertEqual(["attestation_submit", "attestation_approve"], actions)
|
||||
self.assertEqual("closed_attested", closed["status"])
|
||||
|
||||
|
||||
class BalanceBasisTests(CalculationBase):
|
||||
def setUp(self) -> None:
|
||||
|
||||
@@ -257,6 +257,56 @@ class ProjectionTests(LedgerBase):
|
||||
self.assertEqual("confirmed", revision["state"])
|
||||
self.assertEqual("receivable", revision["subject_code"])
|
||||
|
||||
def test_reopen_subject_survives_connection_close(self) -> None:
|
||||
"""HEL-282: create_event used to commit the replacement event while
|
||||
the bank-source re-claim stayed uncommitted; close() dropped the claim."""
|
||||
from bank_importer.db import connect as db_connect
|
||||
|
||||
self.pair(self.company_a, self.company_b, "100.00")
|
||||
ledger_events.reconcile_bank_events(self.connection, actor=self.admin)
|
||||
original_id = self.ledger_events()[0]["id"]
|
||||
subjects.confirm_subject(
|
||||
self.connection, original_id,
|
||||
perspective_company_id=self.company_a, subject_code="receivable",
|
||||
reason="确认应收", expected_revision=1, request_key="k1",
|
||||
actor=self.admin,
|
||||
)
|
||||
new_id, _ = ledger_events.reopen_subject(
|
||||
self.connection, original_id,
|
||||
reason="科目复核更正为其他应收", actor=self.admin,
|
||||
)
|
||||
self.connection.close()
|
||||
fresh = db_connect(self.db_path)
|
||||
try:
|
||||
claim = fresh.execute(
|
||||
"SELECT ledger_event_id FROM ledger_event_bank_sources"
|
||||
).fetchone()
|
||||
new_state = fresh.execute(
|
||||
"""
|
||||
SELECT r.state FROM current_ledger_event_revisions c
|
||||
JOIN ledger_event_revisions r ON r.id = c.revision_id
|
||||
WHERE c.ledger_event_id = ?
|
||||
""",
|
||||
(new_id,),
|
||||
).fetchone()
|
||||
suggestions = fresh.execute(
|
||||
"SELECT COUNT(*) AS n FROM ledger_subject_suggestions WHERE ledger_event_id = ?",
|
||||
(new_id,),
|
||||
).fetchone()["n"]
|
||||
reversal = fresh.execute(
|
||||
"""
|
||||
SELECT COUNT(*) AS n FROM ledger_event_revisions
|
||||
WHERE posting_kind = 'reversal' AND reverses_ledger_event_id = ?
|
||||
""",
|
||||
(original_id,),
|
||||
).fetchone()["n"]
|
||||
finally:
|
||||
fresh.close()
|
||||
self.assertEqual(new_id, claim["ledger_event_id"])
|
||||
self.assertEqual("pending_subject", new_state["state"])
|
||||
self.assertGreaterEqual(suggestions, 1)
|
||||
self.assertEqual(1, reversal)
|
||||
|
||||
|
||||
class SubjectSuggestionTests(LedgerBase):
|
||||
def test_mirror_mapping_is_symmetric(self) -> None:
|
||||
|
||||
@@ -210,6 +210,40 @@ class MasterDataUnitTests(unittest.TestCase):
|
||||
)
|
||||
|
||||
|
||||
class MasterDataCommitTests(unittest.TestCase):
|
||||
"""File-database checks that business rows and audit share one commit."""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.temp_dir = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.temp_dir.cleanup)
|
||||
self.db_path = Path(self.temp_dir.name) / "app.db"
|
||||
self.connection = connect(self.db_path)
|
||||
self.addCleanup(self.connection.close)
|
||||
migrate(self.connection)
|
||||
|
||||
def test_create_company_and_audit_survive_connection_close(self) -> None:
|
||||
company_id = master_data.create_company(
|
||||
self.connection, "丁公司", None, None, actor=None
|
||||
)
|
||||
self.connection.close()
|
||||
fresh = connect(self.db_path)
|
||||
try:
|
||||
company = fresh.execute(
|
||||
"SELECT name FROM companies WHERE id = ?", (company_id,)
|
||||
).fetchone()
|
||||
change = fresh.execute(
|
||||
"""
|
||||
SELECT action, entity_id FROM master_data_changes
|
||||
WHERE entity_type = 'company'
|
||||
"""
|
||||
).fetchone()
|
||||
finally:
|
||||
fresh.close()
|
||||
self.assertEqual("丁公司", company["name"])
|
||||
self.assertEqual("create", change["action"])
|
||||
self.assertEqual(company_id, change["entity_id"])
|
||||
|
||||
|
||||
class MasterDataApiTests(unittest.TestCase):
|
||||
"""Live-server workflow tests for account registration and review."""
|
||||
|
||||
|
||||
@@ -943,6 +943,38 @@ class ProjectionRebuildTests(MatchingBase):
|
||||
self.assertEqual(sorted(before), sorted(after))
|
||||
self.assertEqual(sorted(claims_before), sorted(claims_after))
|
||||
|
||||
def test_rebuild_clears_stale_projection_and_survives_close(self) -> None:
|
||||
"""HEL-282: DELETEs used to stay uncommitted when nothing was restored."""
|
||||
from bank_importer.db import connect as db_connect
|
||||
|
||||
row_a = self.add_row(
|
||||
self.company_a, own_account="6222000000000001",
|
||||
cp_account="6222000000000002", expense="100.00",
|
||||
)
|
||||
row_b = self.add_row(
|
||||
self.company_b, own_account="6222000000000002",
|
||||
cp_account="6222000000000001", income="100.00",
|
||||
)
|
||||
matching.reconcile_rows(self.connection, [row_a, row_b])
|
||||
with self.connection:
|
||||
self.connection.execute(
|
||||
"UPDATE canonical_transfer_events SET lifecycle = 'superseded'"
|
||||
)
|
||||
matching.rebuild_current_projection(self.connection)
|
||||
self.connection.close()
|
||||
fresh = db_connect(self.db_path)
|
||||
try:
|
||||
remaining = fresh.execute(
|
||||
"SELECT COUNT(*) AS n FROM current_transfer_decisions"
|
||||
).fetchone()["n"]
|
||||
claims = fresh.execute(
|
||||
"SELECT COUNT(*) AS n FROM transfer_observation_claims"
|
||||
).fetchone()["n"]
|
||||
finally:
|
||||
fresh.close()
|
||||
self.assertEqual(0, remaining)
|
||||
self.assertEqual(0, claims)
|
||||
|
||||
|
||||
class ConcurrentReconcileTests(MatchingBase):
|
||||
def test_concurrent_reconcile_creates_one_event(self) -> None:
|
||||
|
||||
@@ -189,6 +189,42 @@ class PeriodCloseTests(LedgerBase):
|
||||
self.assertEqual(1, versions[0]["version"])
|
||||
self.assertEqual(2, versions[-1]["version"])
|
||||
|
||||
def test_close_and_reopen_request_survive_connection_close(self) -> None:
|
||||
"""HEL-282: monthly close / reopen request must persist with audit."""
|
||||
from bank_importer.db import connect as db_connect
|
||||
|
||||
self._cover_month()
|
||||
closed = self._close()
|
||||
req = period_close.request_reopen(
|
||||
self.connection, self.MONTH, self.admin,
|
||||
reason="补录金牛煤业七月运输费并核对金额",
|
||||
)
|
||||
report_no = closed["report_no"]
|
||||
request_id = req["id"]
|
||||
self.connection.close()
|
||||
fresh = db_connect(self.db_path)
|
||||
try:
|
||||
run = fresh.execute(
|
||||
"SELECT status, report_no FROM period_close_runs WHERE year_month = ?",
|
||||
(self.MONTH,),
|
||||
).fetchone()
|
||||
reopen = fresh.execute(
|
||||
"SELECT status FROM period_reopen_requests WHERE id = ?",
|
||||
(request_id,),
|
||||
).fetchone()
|
||||
actions = {
|
||||
row["action"]
|
||||
for row in fresh.execute(
|
||||
"SELECT action FROM period_audit_events"
|
||||
).fetchall()
|
||||
}
|
||||
finally:
|
||||
fresh.close()
|
||||
self.assertEqual("closed", run["status"])
|
||||
self.assertEqual(report_no, run["report_no"])
|
||||
self.assertEqual("pending", reopen["status"])
|
||||
self.assertTrue({"close_execute", "reopen_request"} <= actions)
|
||||
|
||||
def test_wal_on_file_database(self) -> None:
|
||||
mode = self.connection.execute("PRAGMA journal_mode").fetchone()[0]
|
||||
self.assertEqual("wal", str(mode).lower())
|
||||
|
||||
Reference in New Issue
Block a user