feat(expenses): add transactional expense case events

This commit is contained in:
caoxiaozhu
2026-07-13 11:58:48 +08:00
parent 9a84e125d0
commit 661990b27b
21 changed files with 1973 additions and 63 deletions

View File

@@ -6,6 +6,7 @@ from decimal import Decimal, InvalidOperation
from typing import Any
from app.api.deps import CurrentUserContext
from app.models.financial_record import ExpenseClaim
from app.services.budget import BudgetService
from app.services.expense_claim_workflow_constants import (
APPLICATION_LINK_STATUS_STAGE,
@@ -45,6 +46,7 @@ class ExpenseClaimApprovalFlowMixin:
next_budget_manager = None
merged_budget_approval = False
route_decision_flag: dict[str, Any] | None = None
generated_draft = None
if previous_stage == DIRECT_MANAGER_APPROVAL_STAGE:
if not self._access_policy.can_approve_claim(current_user, claim):
raise ValueError("只有当前直属领导审批人可以审批通过该单据。")
@@ -254,6 +256,48 @@ class ExpenseClaimApprovalFlowMixin:
business_stage=business_stage,
)
correlation_id = str(approval_flag.get("approval_event_id") or uuid.uuid4())
structured_event_type = "approval_stage_completed"
if is_application_claim and next_status == "approved":
structured_event_type = "application_approved"
elif not is_application_claim and next_status == PAYMENT_PENDING_STATUS:
structured_event_type = "claim_approved"
expense_case, _event = self._expense_cases.record_claim_event(
claim,
event_type=structured_event_type,
actor_id=current_user.username,
tenant_id=getattr(current_user, "tenant_id", None),
correlation_id=correlation_id,
idempotency_key=correlation_id,
previous_status=str(before_json.get("status") or ""),
previous_approval_stage=previous_stage,
extra_payload={
"workflow_event_type": event_type,
"opinion": approval_opinion,
"route_requires_budget_review": bool(
route_decision_flag and route_decision_flag.get("requires_budget_review")
),
},
)
if generated_draft is not None:
self._expense_cases.record_claim_event(
generated_draft,
event_type="reimbursement_draft_generated",
actor_id="system",
tenant_id=getattr(current_user, "tenant_id", None),
correlation_id=correlation_id,
idempotency_key=correlation_id,
causation_id=_event.id,
previous_status="",
previous_approval_stage="",
extra_payload={
"application_claim_id": claim.id,
"application_claim_no": claim.claim_no,
},
expense_case=expense_case,
relation_type="generated_reimbursement",
)
self.db.commit()
self.db.refresh(claim)
self._access_policy.attach_budget_approval_snapshot(claim)
@@ -326,6 +370,47 @@ class ExpenseClaimApprovalFlowMixin:
claim.approval_stage = PAYMENT_PAID_STAGE
claim.risk_flags_json = [*list(claim.risk_flags_json or []), payment_flag]
payment_correlation_id = str(payment_flag.get("payment_event_id") or uuid.uuid4())
expense_case, payment_event = self._expense_cases.record_claim_event(
claim,
event_type="payment_completed",
actor_id=current_user.username,
tenant_id=getattr(current_user, "tenant_id", None),
correlation_id=payment_correlation_id,
idempotency_key=payment_correlation_id,
previous_status=str(before_json.get("status") or ""),
previous_approval_stage=previous_stage,
extra_payload={"archived_applications": archived_applications},
)
for archived_application in archived_applications:
application_claim = self.db.get(
ExpenseClaim,
str(archived_application.get("application_claim_id") or ""),
)
if application_claim is None:
raise RuntimeError("付款关联的申请单已不存在,无法记录归档事件。")
archive_event_id = str(archived_application.get("archive_event_id") or "").strip()
self._expense_cases.record_claim_event(
application_claim,
event_type="application_archived",
actor_id=current_user.username,
tenant_id=getattr(current_user, "tenant_id", None),
correlation_id=payment_correlation_id,
idempotency_key=archive_event_id,
causation_id=payment_event.id,
previous_status=str(archived_application.get("previous_status") or ""),
previous_approval_stage=str(
archived_application.get("previous_approval_stage") or ""
),
extra_payload={
"reimbursement_claim_id": claim.id,
"reimbursement_claim_no": claim.claim_no,
},
expense_case=expense_case,
relation_type="application",
update_case_state=False,
)
self.db.commit()
self.db.refresh(claim)