HEL-157: 审核中心接入三类待办完整队列
将待复核账户、待审手工单、匹配异常统一渲染到审核中心列表, 处置走既有 decision API,并补三类种子一致性与处置减一集成测试。 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
7fb97119ba
commit
e04f47b9d2
@@ -313,6 +313,167 @@ class DashboardApiTests(unittest.TestCase):
|
||||
after["total"], after["high"] + after["medium"] + after["low"]
|
||||
)
|
||||
|
||||
def test_three_queue_audit_parity_and_dispose_sync(self) -> None:
|
||||
"""Seed account+manual+exception: list sizes == dashboard.audit; dispose syncs -1."""
|
||||
from bank_importer.db import connect as db_connect
|
||||
from bank_importer import auth, manual_records, master_data as md
|
||||
|
||||
status, _, raw = self.admin.get("/api/admin/companies")
|
||||
company_id = as_json(raw)["companies"][0]["id"]
|
||||
|
||||
# Second company for manual counterparty.
|
||||
status, _, raw = self.admin.post_json(
|
||||
"/api/admin/companies", {"name": "乙公司", "username": "cashier-b-hel157"}
|
||||
)
|
||||
self.assertEqual(200, status, raw)
|
||||
company_b = as_json(raw).get("company_id") or as_json(raw).get("id")
|
||||
self.assertIsNotNone(company_b)
|
||||
|
||||
connection = db_connect(self.db_path)
|
||||
try:
|
||||
account = md.submit_bank_account(
|
||||
connection,
|
||||
company_id=company_id,
|
||||
bank_name="工行",
|
||||
account_type="一般户",
|
||||
account_number="6222020000000157",
|
||||
start_date="2026-01-01",
|
||||
actor=None,
|
||||
)
|
||||
account_id = account["id"]
|
||||
|
||||
cashier = connection.execute(
|
||||
"SELECT * FROM users WHERE username = 'cashier-a'"
|
||||
).fetchone()
|
||||
if cashier is None:
|
||||
auth.create_user(
|
||||
connection,
|
||||
"cashier-a",
|
||||
"CashierPass123",
|
||||
"company",
|
||||
company_id=company_id,
|
||||
)
|
||||
cashier = connection.execute(
|
||||
"SELECT * FROM users WHERE username = 'cashier-a'"
|
||||
).fetchone()
|
||||
manual = manual_records.submit(
|
||||
connection,
|
||||
company_id=company_id,
|
||||
counterparty_company_id=int(company_b),
|
||||
occurred_at="2026-02-01T09:00:00",
|
||||
direction="incoming",
|
||||
amount="100.00",
|
||||
currency="CNY",
|
||||
funding_source="other",
|
||||
requested_subject="receivable",
|
||||
request_key="hel157-queue-manual",
|
||||
actor=cashier,
|
||||
)
|
||||
|
||||
now = md.utc_now()
|
||||
event_id = connection.execute(
|
||||
"INSERT INTO canonical_transfer_events (lifecycle, created_at) VALUES ('active', ?)",
|
||||
(now,),
|
||||
).lastrowid
|
||||
decision_id = connection.execute(
|
||||
"""
|
||||
INSERT INTO transfer_match_decisions (
|
||||
event_id, revision, effective_at, amount, currency, classification,
|
||||
pairing, locked, mode, rule_version, created_at
|
||||
) VALUES (?, 1, '2026-07-05T10:00:00', '100000.00', 'CNY', 'unresolved',
|
||||
'single', 0, 'manual', 'test', ?)
|
||||
""",
|
||||
(event_id, now),
|
||||
).lastrowid
|
||||
connection.execute(
|
||||
"INSERT INTO current_transfer_decisions (event_id, decision_id) VALUES (?, ?)",
|
||||
(event_id, decision_id),
|
||||
)
|
||||
connection.commit()
|
||||
manual_id = manual["id"]
|
||||
manual_decision_id = manual["decision_id"]
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
def queue_sizes():
|
||||
st, _, body = self.admin.get("/api/admin/accounts?status=pending")
|
||||
self.assertEqual(200, st, body)
|
||||
accounts_n = len(as_json(body)["accounts"])
|
||||
st, _, body = self.admin.get("/api/admin/manual-records?state=pending")
|
||||
self.assertEqual(200, st, body)
|
||||
manuals_n = len(as_json(body)["records"])
|
||||
st, _, body = self.admin.get("/api/admin/match-exceptions")
|
||||
self.assertEqual(200, st, body)
|
||||
exceptions_n = len(as_json(body)["exceptions"])
|
||||
return accounts_n, manuals_n, exceptions_n, accounts_n + manuals_n + exceptions_n
|
||||
|
||||
def assert_parity(expected_total: int) -> dict:
|
||||
st, _, body = self.admin.get("/api/admin/dashboard")
|
||||
self.assertEqual(200, st, body)
|
||||
audit = as_json(body)["audit"]
|
||||
accounts_n, manuals_n, exceptions_n, list_total = queue_sizes()
|
||||
self.assertEqual(expected_total, audit["total"])
|
||||
self.assertEqual(expected_total, list_total)
|
||||
self.assertEqual(
|
||||
audit["total"], audit["high"] + audit["medium"] + audit["low"]
|
||||
)
|
||||
self.assertGreaterEqual(accounts_n, 1 if expected_total >= 3 else 0)
|
||||
return {
|
||||
"audit": audit,
|
||||
"accounts": accounts_n,
|
||||
"manuals": manuals_n,
|
||||
"exceptions": exceptions_n,
|
||||
}
|
||||
|
||||
before = assert_parity(3)
|
||||
self.assertEqual(1, before["accounts"])
|
||||
self.assertEqual(1, before["manuals"])
|
||||
self.assertEqual(1, before["exceptions"])
|
||||
self.assertEqual(1, before["audit"]["high"])
|
||||
self.assertEqual(2, before["audit"]["medium"])
|
||||
|
||||
# Dispose account → total 2
|
||||
st, _, body = self.admin.post_json(
|
||||
f"/api/admin/accounts/{account_id}/review",
|
||||
{"decision": "approve", "reason": "HEL-157 three-queue dispose account"},
|
||||
)
|
||||
self.assertEqual(200, st, body)
|
||||
after_account = assert_parity(2)
|
||||
|
||||
# Dispose manual → total 1
|
||||
st, _, body = self.admin.post_json(
|
||||
f"/api/admin/manual-records/{manual_id}/decisions",
|
||||
{
|
||||
"action": "approve_new",
|
||||
"reason": "HEL-157 three-queue dispose manual",
|
||||
"expected_decision_id": manual_decision_id,
|
||||
"request_key": "hel157-dispose-manual",
|
||||
},
|
||||
)
|
||||
self.assertEqual(200, st, body)
|
||||
after_manual = assert_parity(1)
|
||||
self.assertEqual(0, after_manual["manuals"])
|
||||
|
||||
# Dispose match exception via reverse → total 0
|
||||
st, _, body = self.admin.get("/api/admin/match-exceptions")
|
||||
exceptions = as_json(body)["exceptions"]
|
||||
self.assertEqual(1, len(exceptions))
|
||||
target = exceptions[0]
|
||||
st, _, body = self.admin.post_json(
|
||||
f"/api/admin/transfer-events/{target['event_id']}/decisions",
|
||||
{
|
||||
"action": "reverse",
|
||||
"reason": "HEL-157 three-queue dispose match",
|
||||
"expected_revision": target["revision"],
|
||||
"request_key": "hel157-dispose-match",
|
||||
},
|
||||
)
|
||||
self.assertEqual(200, st, body)
|
||||
after_match = assert_parity(0)
|
||||
self.assertEqual(0, after_match["exceptions"])
|
||||
self.assertEqual(after_account["audit"]["total"] - 1, after_manual["audit"]["total"])
|
||||
self.assertEqual(after_manual["audit"]["total"] - 1, after_match["audit"]["total"])
|
||||
|
||||
def test_company_detail_missing(self) -> None:
|
||||
status, _, raw = self.admin.get(
|
||||
"/api/admin/dashboard/companies/999999?from=2026-01-01&cutoff=2026-08-20"
|
||||
|
||||
Reference in New Issue
Block a user