feat(platform): close AI expense value loop

Add tenant-safe value, telemetry, connector, commercial, and production-readiness foundations.
This commit is contained in:
caoxiaozhu
2026-07-17 14:14:08 +08:00
parent 242d68c36f
commit 787bc3a481
507 changed files with 82072 additions and 6344 deletions

View File

@@ -0,0 +1,335 @@
from __future__ import annotations
from datetime import date, datetime
from decimal import Decimal
from typing import Any, Literal
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
SavingsValueType = Literal["cash", "labor"]
SavingsOpportunityStatus = Literal[
"identified",
"accepted",
"in_progress",
"realized",
"verified",
"reversed",
"rejected",
"expired",
]
SavingsOpportunityAction = Literal["accept", "start", "reject", "expire"]
SavingsOpportunityAvailableAction = Literal[
"accept",
"start",
"reject",
"expire",
"record_realization",
]
SavingsRealizationAction = Literal["confirm", "reject", "reverse"]
class ProfileBaselineSnapshotRead(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: str
tenant_id: str
baseline_key: str
baseline_type: Literal["historical_cohort", "policy_counterfactual", "manual"]
dimension_type: str
dimension_id: str
metric_key: str
unit: str
original_currency: str | None = None
baseline_value: Decimal
window_start: datetime | None = None
window_end: datetime | None = None
sample_count: int
method: str
query_fingerprint: str
data_quality_status: Literal["complete", "partial", "insufficient", "invalid"]
data_quality_score: Decimal
quality_issues_json: list[dict[str, Any]] = Field(default_factory=list)
algorithm_version: str
policy_version: str | None = None
policy_effective_from: date | None = None
policy_effective_to: date | None = None
target_resource_type: str | None = None
target_resource_id: str | None = None
frozen_at: datetime
frozen_by: str
valid_until: datetime | None = None
version: int
created_at: datetime
class SavingsEvidenceLinkRead(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: str
tenant_id: str
evidence_key: str
entity_type: Literal["baseline", "opportunity", "realization"]
entity_id: str
evidence_role: str
resource_type: str
resource_id: str
source_system: str
external_event_id: str | None = None
content_hash: str
occurred_at: datetime
collected_at: datetime
verification_status: Literal["unverified", "verified", "rejected", "unavailable"]
verified_by: str | None = None
verified_at: datetime | None = None
metadata_json: dict[str, Any] = Field(default_factory=dict)
created_at: datetime
class SavingsEventRead(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: str
tenant_id: str
aggregate_type: Literal["baseline", "opportunity", "realization"]
aggregate_id: str
action: str
actor_id: str
actor_name: str
actor_type: Literal["user", "system", "agent", "service"]
request_id: str
expected_version: int
result_version: int
payload_fingerprint: str
payload_json: dict[str, Any] = Field(default_factory=dict)
before_json: dict[str, Any] = Field(default_factory=dict)
after_json: dict[str, Any] = Field(default_factory=dict)
response_json: dict[str, Any] = Field(default_factory=dict)
correlation_id: str | None = None
causation_id: str | None = None
occurred_at: datetime
class SavingsRealizationRead(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: str
tenant_id: str
realization_key: str
opportunity_id: str
expense_case_id: str
claim_id: str
claim_item_id: str | None = None
business_event_id: str | None = None
realization_type: Literal["actual", "reversal"]
reversal_of_realization_id: str | None = None
realized_at: datetime
recorded_by_id: str
recorded_by_name: str
actual_gross: Decimal
incremental_cost: Decimal
actual_net: Decimal
original_currency: str
reporting_amount: Decimal
reporting_currency: str
fx_rate: Decimal
fx_source: str
fx_date: date
fx_version: str
attribution_method: str
attribution_ratio: Decimal
benefit_key: str
dedupe_status: Literal["pending_review", "canonical", "duplicate", "excluded"]
canonical_realization_id: str | None = None
status: Literal["pending_confirmation", "finance_confirmed", "rejected", "reversed"]
finance_confirmer_id: str | None = None
finance_confirmer_name: str | None = None
confirmed_at: datetime | None = None
confirmation_note: str | None = None
rejected_by_id: str | None = None
rejected_by_name: str | None = None
rejected_at: datetime | None = None
rejection_reason: str | None = None
reversed_by_id: str | None = None
reversed_by_name: str | None = None
reversed_at: datetime | None = None
reversal_reason: str | None = None
baseline_snapshot_json: dict[str, Any] = Field(default_factory=dict)
final_snapshot_json: dict[str, Any] = Field(default_factory=dict)
evidence_json: list[dict[str, Any]] = Field(default_factory=list)
version: int
created_at: datetime
updated_at: datetime
available_actions: list[SavingsRealizationAction] = Field(default_factory=list)
read_only_reason: str = ""
class SavingsOpportunityRead(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: str
tenant_id: str
opportunity_key: str
expense_case_id: str
claim_id: str
claim_item_id: str | None = None
discovery_business_event_id: str | None = None
source_type: str
source_id: str
category: str
claim_no_snapshot: str
title: str
description: str
value_kind: SavingsValueType
exposure_amount: Decimal
baseline_snapshot_id: str
baseline_amount: Decimal
target_amount: Decimal
estimated_gross: Decimal
estimated_cost: Decimal
estimated_net: Decimal
estimated_low: Decimal
estimated_high: Decimal
confidence: Decimal
currency: str
reporting_currency: str
attribution_method: str
ai_decision_id: str | None = None
owner_id: str
owner_name: str
owner_role: str
due_at: datetime | None = None
status: SavingsOpportunityStatus
version: int
benefit_key: str
suggested_action: str
dimension_json: dict[str, Any] = Field(default_factory=dict)
baseline_snapshot_json: dict[str, Any] = Field(default_factory=dict)
evidence_json: list[dict[str, Any]] = Field(default_factory=list)
accepted_at: datetime | None = None
started_at: datetime | None = None
realized_at: datetime | None = None
verified_at: datetime | None = None
closed_at: datetime | None = None
created_at: datetime
updated_at: datetime
available_actions: list[SavingsOpportunityAvailableAction] = Field(default_factory=list)
read_only_reason: str = ""
baseline: ProfileBaselineSnapshotRead | None = None
realizations: list[SavingsRealizationRead] = Field(default_factory=list)
evidence: list[SavingsEvidenceLinkRead] = Field(default_factory=list)
events: list[SavingsEventRead] = Field(default_factory=list)
class SavingsOpportunityListRead(BaseModel):
items: list[SavingsOpportunityRead] = Field(default_factory=list)
total: int = 0
page: int = 1
page_size: int = 20
total_pages: int = 0
generated_at: datetime
class SavingsActionBase(BaseModel):
request_id: str = Field(min_length=8, max_length=120)
expected_version: int = Field(ge=1)
comment: str = Field(min_length=2, max_length=1000)
@field_validator("request_id", "comment", mode="before")
@classmethod
def normalize_text(cls, value: Any) -> str:
return str(value or "").strip()
class SavingsOpportunityActionCreate(SavingsActionBase):
action: SavingsOpportunityAction
class SavingsEvidenceCreate(BaseModel):
evidence_key: str = Field(min_length=1, max_length=160)
evidence_role: str = Field(min_length=1, max_length=50)
resource_type: str = Field(min_length=1, max_length=50)
resource_id: str = Field(min_length=1, max_length=160)
source_system: str = Field(min_length=1, max_length=60)
external_event_id: str | None = Field(default=None, max_length=160)
content_hash: str = Field(min_length=16, max_length=80)
occurred_at: datetime
verification_status: Literal["unverified", "unavailable"] = "unverified"
metadata_json: dict[str, Any] = Field(default_factory=dict)
@field_validator(
"evidence_key",
"evidence_role",
"resource_type",
"resource_id",
"source_system",
"external_event_id",
"content_hash",
mode="before",
)
@classmethod
def normalize_optional_text(cls, value: Any) -> Any:
if value is None:
return None
normalized = str(value).strip()
return normalized or None
class SavingsRealizationCreate(SavingsActionBase):
actual_gross: Decimal = Field(gt=0, max_digits=16, decimal_places=2)
incremental_cost: Decimal = Field(
default=Decimal("0.00"),
ge=0,
max_digits=16,
decimal_places=2,
)
currency: str = Field(default="CNY", min_length=3, max_length=10)
realized_at: datetime
attribution_method: str = Field(min_length=2, max_length=80)
attribution_ratio: Decimal = Field(default=Decimal("1.0000"), gt=0, le=1)
evidence_level: Literal["business_state", "external_document", "external_verified"]
evidence: list[SavingsEvidenceCreate] = Field(default_factory=list, max_length=30)
@field_validator("currency", mode="before")
@classmethod
def normalize_currency(cls, value: Any) -> str:
return str(value or "CNY").strip().upper()
@field_validator("attribution_method", mode="before")
@classmethod
def normalize_method(cls, value: Any) -> str:
return str(value or "").strip()
@model_validator(mode="after")
def require_result_evidence(self) -> SavingsRealizationCreate:
if not self.evidence:
raise ValueError("实际节省结果必须至少提供一条可追溯证据。")
if self.incremental_cost > self.actual_gross:
raise ValueError("执行成本不能高于实际毛节省。")
return self
class SavingsRealizationActionCreate(SavingsActionBase):
action: SavingsRealizationAction
reversal_amount: Decimal | None = Field(default=None, gt=0, max_digits=16, decimal_places=2)
evidence: list[SavingsEvidenceCreate] = Field(default_factory=list, max_length=30)
@model_validator(mode="after")
def validate_action_payload(self) -> SavingsRealizationActionCreate:
if self.action == "reverse" and self.reversal_amount is None:
raise ValueError("冲回动作必须提供冲回金额。")
if self.action != "reverse" and self.reversal_amount is not None:
raise ValueError("只有冲回动作可以提供冲回金额。")
return self
class SavingsOpportunityMutationRead(BaseModel):
opportunity: SavingsOpportunityRead
event: SavingsEventRead
replayed: bool = False
class SavingsRealizationMutationRead(BaseModel):
realization: SavingsRealizationRead
opportunity: SavingsOpportunityRead
event: SavingsEventRead
replayed: bool = False