fix(approval): replay immutable action responses
This commit is contained in:
@@ -3,12 +3,17 @@ from __future__ import annotations
|
||||
from collections.abc import Callable, Mapping
|
||||
from typing import Any
|
||||
|
||||
from pydantic import ValidationError
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.api.deps import CurrentUserContext
|
||||
from app.models.approval_action import ApprovalActionLedger
|
||||
from app.models.financial_record import ExpenseClaim
|
||||
from app.services.approval_action_protocol import ApprovalActionProtocol
|
||||
from app.schemas.reimbursement import ExpenseClaimRead
|
||||
from app.services.approval_action_protocol import (
|
||||
ApprovalActionConflictError,
|
||||
ApprovalActionProtocol,
|
||||
)
|
||||
from app.services.expense_claim_tenant_scope import ExpenseClaimTenantScopeMixin
|
||||
|
||||
ClaimActionExecutor = Callable[
|
||||
@@ -29,7 +34,7 @@ class ExpenseClaimActionProtocolMixin:
|
||||
expected_approval_stage: str | None,
|
||||
payload: Mapping[str, Any],
|
||||
executor: ClaimActionExecutor,
|
||||
) -> ExpenseClaim | None:
|
||||
) -> ExpenseClaim | ExpenseClaimRead | None:
|
||||
protocol = ApprovalActionProtocol(self.db)
|
||||
tenant_id = ExpenseClaimTenantScopeMixin.normalize_tenant_id(current_user.tenant_id)
|
||||
actor_id = str(current_user.username or "").strip().casefold()
|
||||
@@ -51,18 +56,16 @@ class ExpenseClaimActionProtocolMixin:
|
||||
claim_id,
|
||||
current_user,
|
||||
),
|
||||
replay_claim_loader=lambda: self._load_claim_for_replay(
|
||||
claim_id,
|
||||
current_user,
|
||||
),
|
||||
)
|
||||
if started.replayed:
|
||||
if started.ledger is None: # pragma: no cover - defensive invariant
|
||||
raise RuntimeError("审批动作重放账本缺失。")
|
||||
response = self._response_from_ledger(started.ledger)
|
||||
self.db.commit()
|
||||
return response
|
||||
if started.claim is None:
|
||||
self.db.rollback()
|
||||
return None
|
||||
if started.replayed:
|
||||
self.db.commit()
|
||||
self.db.refresh(started.claim)
|
||||
return self._access_policy.attach_approval_snapshot(started.claim)
|
||||
if started.ledger is None: # pragma: no cover - defensive invariant
|
||||
raise RuntimeError("审批动作账本初始化失败。")
|
||||
|
||||
@@ -71,14 +74,16 @@ class ExpenseClaimActionProtocolMixin:
|
||||
started.ledger,
|
||||
normalized_request_id,
|
||||
)
|
||||
self.db.flush()
|
||||
self.db.refresh(claim)
|
||||
response = ExpenseClaimRead.model_validate(
|
||||
self._access_policy.attach_approval_snapshot(claim)
|
||||
)
|
||||
response_json = response.model_dump(mode="json")
|
||||
protocol.complete(
|
||||
started.ledger,
|
||||
claim,
|
||||
response_json={
|
||||
"claim_id": claim.id,
|
||||
"status": str(claim.status or "").strip(),
|
||||
"approval_stage": str(claim.approval_stage or "").strip(),
|
||||
},
|
||||
response_json=response_json,
|
||||
)
|
||||
self.db.commit()
|
||||
self.db.refresh(claim)
|
||||
@@ -108,13 +113,22 @@ class ExpenseClaimActionProtocolMixin:
|
||||
self._repair_duplicate_budget_approval_stage(claim)
|
||||
return claim
|
||||
|
||||
def _load_claim_for_replay(
|
||||
self,
|
||||
claim_id: str,
|
||||
current_user: CurrentUserContext,
|
||||
) -> ExpenseClaim | None:
|
||||
stmt = select(ExpenseClaim).where(
|
||||
ExpenseClaim.id == claim_id,
|
||||
ExpenseClaimTenantScopeMixin.build_claim_tenant_condition(current_user.tenant_id),
|
||||
)
|
||||
return self.db.scalar(stmt)
|
||||
@staticmethod
|
||||
def _response_from_ledger(ledger: ApprovalActionLedger) -> ExpenseClaimRead:
|
||||
try:
|
||||
response = ExpenseClaimRead.model_validate(ledger.response_json)
|
||||
except (TypeError, ValueError, ValidationError) as error:
|
||||
raise ApprovalActionConflictError(
|
||||
"该审批动作缺少可验证的原始响应快照,请刷新单据后使用新的 request_id。"
|
||||
) from error
|
||||
if str(response.id) != str(ledger.claim_id):
|
||||
raise ApprovalActionConflictError("审批动作原始响应快照与单据不一致。")
|
||||
if str(response.status or "").strip().lower() != str(
|
||||
ledger.result_status or ""
|
||||
).strip().lower():
|
||||
raise ApprovalActionConflictError("审批动作原始响应快照状态校验失败。")
|
||||
if str(response.approval_stage or "").strip() != str(
|
||||
ledger.result_approval_stage or ""
|
||||
).strip():
|
||||
raise ApprovalActionConflictError("审批动作原始响应快照节点校验失败。")
|
||||
return response
|
||||
|
||||
Reference in New Issue
Block a user