from __future__ import annotations from datetime import datetime from typing import Any, Literal from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator RiskDispositionAction = Literal[ "confirm", "false_positive", "request_supplement", "start_remediation", "resolve", "request_waiver", ] RiskAdjudication = Literal["unreviewed", "confirmed", "false_positive"] RiskLifecycleStatus = Literal[ "open", "supplement_requested", "remediation_in_progress", "waiver_requested", "resolved", ] class RiskDispositionActionCreate(BaseModel): action: RiskDispositionAction expected_version: int = Field(ge=0) request_id: str = Field(min_length=8, max_length=120) comment: str | None = Field(default=None, max_length=1000) assignee: str | None = Field(default=None, max_length=120) due_at: datetime | None = None resolution: str | None = Field(default=None, max_length=2000) @field_validator("request_id", "comment", "assignee", "resolution", mode="before") @classmethod def normalize_text(cls, value: Any) -> Any: if value is None: return None normalized = str(value).strip() return normalized or None @model_validator(mode="after") def validate_action_fields(self) -> RiskDispositionActionCreate: if self.action == "resolve" and not self.resolution: raise ValueError("resolve 动作必须填写 resolution") if self.action in {"false_positive", "request_supplement", "request_waiver"} and not ( self.comment ): raise ValueError(f"{self.action} 动作必须填写 comment") if self.action in {"confirm", "false_positive"} and any( value is not None for value in (self.assignee, self.due_at, self.resolution) ): raise ValueError("裁决动作不能同时修改负责人、截止时间或解决说明") return self class RiskDispositionEventRead(BaseModel): model_config = ConfigDict(from_attributes=True) id: str tenant_id: str disposition_id: str observation_id: str version: int action: RiskDispositionAction actor_id: str actor_name: str request_id: str comment: str | None before_json: dict[str, Any] after_json: dict[str, Any] created_at: datetime class RiskDispositionRead(BaseModel): model_config = ConfigDict(from_attributes=True) id: str tenant_id: str observation_id: str adjudication: RiskAdjudication lifecycle_status: RiskLifecycleStatus version: int assignee: str | None due_at: datetime | None resolution: str | None created_at: datetime updated_at: datetime events: list[RiskDispositionEventRead] = Field(default_factory=list) class RiskDispositionMutationRead(BaseModel): disposition: RiskDispositionRead event: RiskDispositionEventRead replayed: bool = False