HEL-168: 公司端工作台待办按权威单边集合同步
确认成功后重拉 /api/company/workspace,去确认数字与本月待办共用同一口径; 新增公司端确认接口与 3→2→1→0 集成测试,失败/重复确认不误减。 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
eefdc92ab6
commit
bf5754ee09
@@ -115,6 +115,9 @@ class AppHandler(SimpleHTTPRequestHandler):
|
||||
if path == "/api/company/match-exceptions":
|
||||
self._handle_company_match_exceptions(query)
|
||||
return
|
||||
if path == "/api/company/workspace":
|
||||
self._handle_company_workspace()
|
||||
return
|
||||
company_event_match = re.fullmatch(r"/api/company/transfer-events/(\d+)", path)
|
||||
if company_event_match:
|
||||
self._handle_company_transfer_event_detail(int(company_event_match.group(1)))
|
||||
@@ -277,6 +280,12 @@ class AppHandler(SimpleHTTPRequestHandler):
|
||||
if path == "/api/company/manual-records":
|
||||
self._handle_company_manual_records_submit()
|
||||
return
|
||||
company_confirm = re.fullmatch(
|
||||
r"/api/company/transfer-events/(\d+)/confirm", path
|
||||
)
|
||||
if company_confirm:
|
||||
self._handle_company_transfer_confirm(int(company_confirm.group(1)))
|
||||
return
|
||||
self._send_json(404, {"status": "error", "message": "接口不存在。"})
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
@@ -2195,6 +2204,8 @@ class AppHandler(SimpleHTTPRequestHandler):
|
||||
)
|
||||
return {
|
||||
"event_id": row["event_id"],
|
||||
"decision_id": row["decision_id"],
|
||||
"revision": row["revision"],
|
||||
"classification": row["classification"],
|
||||
"pairing": row["pairing"],
|
||||
"status": matching.exposed_status(row),
|
||||
@@ -2209,6 +2220,20 @@ class AppHandler(SimpleHTTPRequestHandler):
|
||||
"evidence_count": row["evidence_count"],
|
||||
}
|
||||
|
||||
def _handle_company_workspace(self) -> None:
|
||||
connection = connect(DB_PATH)
|
||||
try:
|
||||
user = self._require_user(connection)
|
||||
if user is None:
|
||||
return
|
||||
if user["role"] != "company":
|
||||
self._send_json(403, {"status": "error", "message": "该操作仅限公司用户。"})
|
||||
return
|
||||
payload = matching.company_workspace_payload(connection, user["company_id"])
|
||||
self._send_json(200, {"status": "ok", **payload})
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
def _handle_company_match_exceptions(self, query: dict[str, list[str]]) -> None:
|
||||
connection = connect(DB_PATH)
|
||||
try:
|
||||
@@ -2218,13 +2243,7 @@ class AppHandler(SimpleHTTPRequestHandler):
|
||||
if user["role"] != "company":
|
||||
self._send_json(403, {"status": "error", "message": "该操作仅限公司用户。"})
|
||||
return
|
||||
params: list[object] = [user["company_id"], user["company_id"]]
|
||||
rows = connection.execute(
|
||||
self._event_base_sql(True)
|
||||
+ "AND d.classification IN ('unresolved', 'needs_review') "
|
||||
"ORDER BY d.id DESC LIMIT 500",
|
||||
params,
|
||||
).fetchall()
|
||||
rows = matching.company_pending_unilaterals(connection, user["company_id"])
|
||||
items = [
|
||||
self._company_event_list_item(row, user["company_id"]) for row in rows
|
||||
]
|
||||
@@ -2232,6 +2251,109 @@ class AppHandler(SimpleHTTPRequestHandler):
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
def _handle_company_transfer_confirm(self, event_id: int) -> None:
|
||||
"""Company confirms a pending unilateral using existing assign_participant rules."""
|
||||
connection = connect(DB_PATH)
|
||||
try:
|
||||
user = self._require_user(connection)
|
||||
if user is None:
|
||||
return
|
||||
if user["role"] != "company":
|
||||
self._send_json(403, {"status": "error", "message": "该操作仅限公司用户。"})
|
||||
return
|
||||
company_id = int(user["company_id"])
|
||||
data = self._read_json_body()
|
||||
if data is None:
|
||||
return
|
||||
request_key = str(data.get("request_key") or "").strip() or None
|
||||
# Idempotent replay: same request_key returns current workspace without re-decrement.
|
||||
if request_key:
|
||||
existing = connection.execute(
|
||||
"""
|
||||
SELECT id, actor_username
|
||||
FROM transfer_match_decisions
|
||||
WHERE idempotency_key = ? AND event_id = ?
|
||||
""",
|
||||
(request_key, event_id),
|
||||
).fetchone()
|
||||
if existing is not None:
|
||||
if existing["actor_username"] != user["username"]:
|
||||
self._send_json(404, {"status": "error", "message": "待确认单边流水不存在或已处理。"})
|
||||
return
|
||||
workspace = matching.company_workspace_payload(connection, company_id)
|
||||
payload = matching._decision_payload(connection, event_id, existing["id"])
|
||||
self._send_json(
|
||||
200,
|
||||
{"status": "ok", "decision": payload, "workspace": workspace},
|
||||
)
|
||||
return
|
||||
pending = matching.company_pending_unilaterals(connection, company_id)
|
||||
target = next((row for row in pending if int(row["event_id"]) == event_id), None)
|
||||
if target is None:
|
||||
self._send_json(404, {"status": "error", "message": "待确认单边流水不存在或已处理。"})
|
||||
return
|
||||
try:
|
||||
expected_revision = (
|
||||
int(data["expected_revision"])
|
||||
if data.get("expected_revision") is not None
|
||||
else None
|
||||
)
|
||||
except (TypeError, ValueError):
|
||||
self._send_json(400, {"status": "error", "message": "expected_revision 参数无效。"})
|
||||
return
|
||||
try:
|
||||
counterparty_company_id = int(data["counterparty_company_id"])
|
||||
except (KeyError, TypeError, ValueError):
|
||||
self._send_json(
|
||||
400, {"status": "error", "message": "counterparty_company_id 必须是有效的公司 id。"}
|
||||
)
|
||||
return
|
||||
if counterparty_company_id == company_id:
|
||||
self._send_json(400, {"status": "error", "message": "对方公司不能是本公司。"})
|
||||
return
|
||||
# Own side is already on the decision; assign the opposite role.
|
||||
own_is_payer = target["payer_company_id"] == company_id
|
||||
role = "payee" if own_is_payer else "payer"
|
||||
if target["payer_company_id"] is None and target["payee_company_id"] is None:
|
||||
self._send_json(400, {"status": "error", "message": "单边流水缺少本方参与方,无法确认。"})
|
||||
return
|
||||
if not own_is_payer and target["payee_company_id"] != company_id:
|
||||
self._send_json(404, {"status": "error", "message": "待确认单边流水不存在或已处理。"})
|
||||
return
|
||||
reason = str(data.get("reason") or "").strip() or "公司端确认单边流水"
|
||||
try:
|
||||
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,
|
||||
},
|
||||
)
|
||||
except matching.MatchConflictError as exc:
|
||||
self._send_json(409, {"status": "error", "message": str(exc)})
|
||||
return
|
||||
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
|
||||
workspace = matching.company_workspace_payload(connection, company_id)
|
||||
self._send_json(
|
||||
200,
|
||||
{"status": "ok", "decision": payload, "workspace": workspace},
|
||||
)
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
def _handle_company_transfer_event_detail(self, event_id: int) -> None:
|
||||
connection = connect(DB_PATH)
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user