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
@@ -18,7 +18,7 @@ from bank_importer import (
|
||||
manual_records, master_data, matching, multipart, period_close, personal_transit,
|
||||
positions, reminders, settings, subjects,
|
||||
)
|
||||
from bank_importer.db import connect, migrate, utc_now
|
||||
from bank_importer.db import connect, migrate, transaction, utc_now
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
@@ -2615,22 +2615,22 @@ class AppHandler(SimpleHTTPRequestHandler):
|
||||
)
|
||||
return
|
||||
try:
|
||||
result = matching.reconcile_rows(connection, writable, actor=user)
|
||||
with transaction(connection):
|
||||
result = matching.reconcile_rows(connection, writable, actor=user)
|
||||
ledger_events.reconcile_bank_events(connection, actor=user)
|
||||
result["late_arrivals"] = late
|
||||
auth.audit(
|
||||
connection, "transfer_reconcile", actor=user,
|
||||
target=f"rows:{len(writable)}",
|
||||
detail=(
|
||||
f"created:{result['created_events']};"
|
||||
f"updated:{result['updated_events']};late:{late}"
|
||||
),
|
||||
ip=self._client_ip,
|
||||
)
|
||||
except Exception as exc:
|
||||
self._send_json(500, {"status": "error", "message": f"重跑匹配失败:{exc}"})
|
||||
return
|
||||
try:
|
||||
ledger_events.reconcile_bank_events(connection, actor=user)
|
||||
except Exception as exc:
|
||||
self._send_json(500, {"status": "error", "message": f"同步往来事件失败:{exc}"})
|
||||
return
|
||||
result["late_arrivals"] = late
|
||||
auth.audit(
|
||||
connection, "transfer_reconcile", actor=user,
|
||||
target=f"rows:{len(writable)}",
|
||||
detail=f"created:{result['created_events']};updated:{result['updated_events']};late:{late}",
|
||||
ip=self._client_ip,
|
||||
)
|
||||
self._send_json(200, {"status": "ok", "matching": result, "late_arrivals": late})
|
||||
finally:
|
||||
connection.close()
|
||||
@@ -2660,19 +2660,21 @@ class AppHandler(SimpleHTTPRequestHandler):
|
||||
return
|
||||
try:
|
||||
period_close.assert_event_writable(connection, event_id)
|
||||
payload = matching.apply_manual_decision(
|
||||
connection,
|
||||
event_id,
|
||||
action,
|
||||
reason=str(data.get("reason") or ""),
|
||||
expected_revision=expected_revision,
|
||||
request_key=str(data.get("request_key") or "") or None,
|
||||
actor=user,
|
||||
source_row_ids=[int(item) for item in source_row_ids]
|
||||
if source_row_ids
|
||||
else None,
|
||||
participant=data.get("participant"),
|
||||
)
|
||||
with transaction(connection):
|
||||
payload = matching.apply_manual_decision(
|
||||
connection,
|
||||
event_id,
|
||||
action,
|
||||
reason=str(data.get("reason") or ""),
|
||||
expected_revision=expected_revision,
|
||||
request_key=str(data.get("request_key") or "") or None,
|
||||
actor=user,
|
||||
source_row_ids=[int(item) for item in source_row_ids]
|
||||
if source_row_ids
|
||||
else None,
|
||||
participant=data.get("participant"),
|
||||
)
|
||||
ledger_events.reconcile_bank_events(connection, actor=user)
|
||||
except period_close.PeriodLockedError as exc:
|
||||
self._send_json(409, {"status": "error", "message": str(exc), "year_month": exc.year_month})
|
||||
return
|
||||
@@ -2682,8 +2684,6 @@ class AppHandler(SimpleHTTPRequestHandler):
|
||||
except matching.MatchInputError as exc:
|
||||
self._send_json(400, {"status": "error", "message": str(exc)})
|
||||
return
|
||||
try:
|
||||
ledger_events.reconcile_bank_events(connection, actor=user)
|
||||
except Exception as exc:
|
||||
self._send_json(500, {"status": "error", "message": f"同步往来事件失败:{exc}"})
|
||||
return
|
||||
@@ -2976,19 +2976,21 @@ class AppHandler(SimpleHTTPRequestHandler):
|
||||
reason = str(data.get("reason") or "").strip() or "公司端确认单边流水"
|
||||
try:
|
||||
period_close.assert_event_writable(connection, event_id)
|
||||
payload = matching.apply_manual_decision(
|
||||
connection,
|
||||
event_id,
|
||||
"assign_participant",
|
||||
reason=reason,
|
||||
expected_revision=expected_revision,
|
||||
request_key=request_key,
|
||||
actor=user,
|
||||
participant={
|
||||
"role": role,
|
||||
"company_id": counterparty_company_id,
|
||||
},
|
||||
)
|
||||
with transaction(connection):
|
||||
payload = matching.apply_manual_decision(
|
||||
connection,
|
||||
event_id,
|
||||
"assign_participant",
|
||||
reason=reason,
|
||||
expected_revision=expected_revision,
|
||||
request_key=request_key,
|
||||
actor=user,
|
||||
participant={
|
||||
"role": role,
|
||||
"company_id": counterparty_company_id,
|
||||
},
|
||||
)
|
||||
ledger_events.reconcile_bank_events(connection, actor=user)
|
||||
except period_close.PeriodLockedError as exc:
|
||||
self._send_json(409, {"status": "error", "message": str(exc), "year_month": exc.year_month})
|
||||
return
|
||||
@@ -2998,8 +3000,6 @@ class AppHandler(SimpleHTTPRequestHandler):
|
||||
except matching.MatchInputError as exc:
|
||||
self._send_json(400, {"status": "error", "message": str(exc)})
|
||||
return
|
||||
try:
|
||||
ledger_events.reconcile_bank_events(connection, actor=user)
|
||||
except Exception as exc:
|
||||
self._send_json(500, {"status": "error", "message": f"同步往来事件失败:{exc}"})
|
||||
return
|
||||
@@ -3417,51 +3417,56 @@ class AppHandler(SimpleHTTPRequestHandler):
|
||||
period_close.assert_ledger_writable(connection, event_id)
|
||||
if action in ("adjust", "reverse") and data.get("effective_at"):
|
||||
period_close.assert_date_writable(connection, str(data.get("effective_at")))
|
||||
if action == "reverse":
|
||||
event_id, _revision_id = ledger_events.create_reversal(
|
||||
connection, event_id,
|
||||
source_kind="adjustment",
|
||||
source_revision_token=None,
|
||||
effective_at=data.get("effective_at") or None,
|
||||
reason=reason, actor=user, idempotency_key=request_key,
|
||||
)
|
||||
outcome: dict[str, object] = {
|
||||
"action": "reverse", "ledger_event_id": event_id,
|
||||
}
|
||||
elif action == "adjust":
|
||||
try:
|
||||
effective_at = str(data.get("effective_at") or "")
|
||||
amount = str(data.get("amount") or "")
|
||||
currency = str(data.get("currency") or "")
|
||||
payer = int(data["payer_company_id"])
|
||||
payee = int(data["payee_company_id"])
|
||||
perspective = int(data["perspective_company_id"])
|
||||
subject_code = str(data.get("subject_code") or "")
|
||||
except (KeyError, TypeError, ValueError):
|
||||
self._send_json(
|
||||
400, {"status": "error", "message": "adjust 参数不完整或无效。"}
|
||||
)
|
||||
return
|
||||
event_id, _revision_id = ledger_events.create_adjustment(
|
||||
connection, event_id,
|
||||
effective_at=effective_at, amount=amount, currency=currency,
|
||||
payer_company_id=payer, payee_company_id=payee,
|
||||
perspective_company_id=perspective, subject_code=subject_code,
|
||||
reason=reason, actor=user, idempotency_key=request_key,
|
||||
)
|
||||
outcome = {"action": "adjust", "ledger_event_id": event_id}
|
||||
elif action == "reopen":
|
||||
event_id, _revision_id = ledger_events.reopen_subject(
|
||||
connection, event_id, reason=reason, actor=user,
|
||||
idempotency_key=request_key,
|
||||
)
|
||||
outcome = {"action": "reopen", "ledger_event_id": event_id}
|
||||
else:
|
||||
if action not in ("reverse", "adjust", "reopen"):
|
||||
self._send_json(
|
||||
400,
|
||||
{"status": "error", "message": "action 必须是 reverse、adjust 或 reopen。"},
|
||||
)
|
||||
return
|
||||
with transaction(connection):
|
||||
if action == "reverse":
|
||||
event_id, _revision_id = ledger_events.create_reversal(
|
||||
connection, event_id,
|
||||
source_kind="adjustment",
|
||||
source_revision_token=None,
|
||||
effective_at=data.get("effective_at") or None,
|
||||
reason=reason, actor=user, idempotency_key=request_key,
|
||||
)
|
||||
outcome = {
|
||||
"action": "reverse", "ledger_event_id": event_id,
|
||||
}
|
||||
elif action == "adjust":
|
||||
try:
|
||||
effective_at = str(data.get("effective_at") or "")
|
||||
amount = str(data.get("amount") or "")
|
||||
currency = str(data.get("currency") or "")
|
||||
payer = int(data["payer_company_id"])
|
||||
payee = int(data["payee_company_id"])
|
||||
perspective = int(data["perspective_company_id"])
|
||||
subject_code = str(data.get("subject_code") or "")
|
||||
except (KeyError, TypeError, ValueError):
|
||||
self._send_json(
|
||||
400, {"status": "error", "message": "adjust 参数不完整或无效。"}
|
||||
)
|
||||
return
|
||||
event_id, _revision_id = ledger_events.create_adjustment(
|
||||
connection, event_id,
|
||||
effective_at=effective_at, amount=amount, currency=currency,
|
||||
payer_company_id=payer, payee_company_id=payee,
|
||||
perspective_company_id=perspective, subject_code=subject_code,
|
||||
reason=reason, actor=user, idempotency_key=request_key,
|
||||
)
|
||||
outcome = {"action": "adjust", "ledger_event_id": event_id}
|
||||
else:
|
||||
event_id, _revision_id = ledger_events.reopen_subject(
|
||||
connection, event_id, reason=reason, actor=user,
|
||||
idempotency_key=request_key,
|
||||
)
|
||||
outcome = {"action": "reopen", "ledger_event_id": event_id}
|
||||
auth.audit(
|
||||
connection, f"ledger_{action}", actor=user,
|
||||
target=f"ledger_event:{event_id}", detail=reason, ip=self._client_ip,
|
||||
)
|
||||
except period_close.PeriodLockedError as exc:
|
||||
self._send_json(409, {"status": "error", "message": str(exc), "year_month": exc.year_month})
|
||||
return
|
||||
@@ -3471,10 +3476,6 @@ class AppHandler(SimpleHTTPRequestHandler):
|
||||
except ledger_events.LedgerInputError as exc:
|
||||
self._send_json(400, {"status": "error", "message": str(exc)})
|
||||
return
|
||||
auth.audit(
|
||||
connection, f"ledger_{action}", actor=user,
|
||||
target=f"ledger_event:{event_id}", detail=reason, ip=self._client_ip,
|
||||
)
|
||||
self._send_json(200, {"status": "ok", **outcome})
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
Reference in New Issue
Block a user