feat(platform): close AI expense value loop
Add tenant-safe value, telemetry, connector, commercial, and production-readiness foundations.
This commit is contained in:
386
server/tests/savings_migration_assertions.py
Normal file
386
server/tests/savings_migration_assertions.py
Normal file
@@ -0,0 +1,386 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import inspect, text
|
||||
from sqlalchemy.engine import Engine
|
||||
from sqlalchemy.exc import DBAPIError, IntegrityError
|
||||
|
||||
from app.models.savings import (
|
||||
ProfileBaselineSnapshot,
|
||||
SavingsEvent,
|
||||
SavingsEvidenceLink,
|
||||
SavingsOpportunity,
|
||||
SavingsRealization,
|
||||
)
|
||||
|
||||
|
||||
def _assert_savings_head_schema(engine: Engine) -> None:
|
||||
inspector = inspect(engine)
|
||||
for model in (
|
||||
ProfileBaselineSnapshot,
|
||||
SavingsOpportunity,
|
||||
SavingsRealization,
|
||||
SavingsEvidenceLink,
|
||||
SavingsEvent,
|
||||
):
|
||||
table = model.__table__
|
||||
live_columns = {
|
||||
str(column["name"]): bool(column["nullable"])
|
||||
for column in inspector.get_columns(table.name, schema="public")
|
||||
}
|
||||
declared_columns = {
|
||||
column.name: bool(column.nullable) for column in table.columns
|
||||
}
|
||||
assert live_columns == declared_columns
|
||||
|
||||
live_constraint_names = {
|
||||
str(item["name"])
|
||||
for loader in (
|
||||
inspector.get_unique_constraints,
|
||||
inspector.get_check_constraints,
|
||||
inspector.get_foreign_keys,
|
||||
)
|
||||
for item in loader(table.name, schema="public")
|
||||
if item.get("name")
|
||||
}
|
||||
declared_constraint_names = {
|
||||
str(constraint.name)
|
||||
for constraint in table.constraints
|
||||
if constraint.name is not None
|
||||
}
|
||||
assert live_constraint_names == declared_constraint_names
|
||||
|
||||
live_index_names = {
|
||||
str(item["name"])
|
||||
for item in inspector.get_indexes(table.name, schema="public")
|
||||
if not item.get("duplicates_constraint")
|
||||
}
|
||||
declared_index_names = {str(index.name) for index in table.indexes}
|
||||
assert live_index_names == declared_index_names
|
||||
|
||||
for table_name in ("savings_opportunities", "savings_realizations"):
|
||||
constrained_columns = {
|
||||
str(column)
|
||||
for foreign_key in inspector.get_foreign_keys(table_name, schema="public")
|
||||
for column in foreign_key["constrained_columns"]
|
||||
}
|
||||
assert "claim_id" not in constrained_columns
|
||||
assert "claim_item_id" not in constrained_columns
|
||||
|
||||
with engine.connect() as connection:
|
||||
canonical_index = str(
|
||||
connection.scalar(
|
||||
text(
|
||||
"SELECT indexdef FROM pg_indexes "
|
||||
"WHERE schemaname = 'public' "
|
||||
"AND tablename = 'savings_realizations' "
|
||||
"AND indexname = "
|
||||
"'uq_savings_realizations_actual_canonical_benefit'"
|
||||
)
|
||||
)
|
||||
or ""
|
||||
).lower()
|
||||
trigger_count = int(
|
||||
connection.scalar(
|
||||
text(
|
||||
"SELECT COUNT(*) FROM pg_trigger trigger "
|
||||
"JOIN pg_class relation ON relation.oid = trigger.tgrelid "
|
||||
"WHERE relation.relname = 'savings_events' "
|
||||
"AND trigger.tgname = 'trg_savings_events_append_only' "
|
||||
"AND NOT trigger.tgisinternal"
|
||||
)
|
||||
)
|
||||
or 0
|
||||
)
|
||||
assert "unique index" in canonical_index
|
||||
assert "realization_type" in canonical_index
|
||||
assert "actual" in canonical_index
|
||||
assert "dedupe_status" in canonical_index
|
||||
assert "canonical" in canonical_index
|
||||
assert trigger_count == 1
|
||||
|
||||
|
||||
def _assert_savings_runtime_invariants(engine: Engine) -> None:
|
||||
opportunity_insert = text(
|
||||
"""
|
||||
INSERT INTO savings_opportunities (
|
||||
id, tenant_id, opportunity_key, benefit_key, expense_case_id,
|
||||
claim_id, claim_no_snapshot, source_type, source_id, category,
|
||||
value_kind, title, description, exposure_amount,
|
||||
baseline_snapshot_id, baseline_amount, target_amount,
|
||||
estimated_gross, estimated_cost, estimated_net,
|
||||
estimated_low, estimated_high, confidence, currency,
|
||||
reporting_currency, attribution_method, suggested_action,
|
||||
owner_id, owner_name, owner_role
|
||||
) VALUES (
|
||||
:id, :tenant_id, :opportunity_key, :benefit_key, :expense_case_id,
|
||||
'claim-soft-ref', 'CLM-SNAPSHOT', 'policy_adjustment', :id, 'lodging',
|
||||
'cash', '住宿标准优化', '服务器政策反事实', 20,
|
||||
:baseline_snapshot_id, 100, 80, 20, 0, 20, 15, 25, 1,
|
||||
'CNY', 'CNY', 'policy_counterfactual', '按职级标准重算',
|
||||
'finance-owner', '财务负责人', 'finance'
|
||||
)
|
||||
"""
|
||||
)
|
||||
realization_insert = text(
|
||||
"""
|
||||
INSERT INTO savings_realizations (
|
||||
id, tenant_id, realization_key, opportunity_id, expense_case_id,
|
||||
claim_id, realization_type, reversal_of_realization_id, realized_at,
|
||||
recorded_by_id, recorded_by_name, actual_gross, incremental_cost,
|
||||
actual_net, original_currency, reporting_amount, reporting_currency,
|
||||
fx_rate, fx_source, fx_date, fx_version, attribution_method,
|
||||
attribution_ratio, benefit_key, dedupe_status,
|
||||
canonical_realization_id, status
|
||||
) VALUES (
|
||||
:id, 'tenant-a', :id, :opportunity_id, 'case-a', 'claim-soft-ref',
|
||||
:realization_type, :reversal_of_realization_id, now(),
|
||||
'recorder-a', '填报人', :actual_gross, 0, :actual_net,
|
||||
'CNY', :reporting_amount, 'CNY', 1, 'system-fixed', current_date,
|
||||
'fx-cny-v1', 'direct', 1, :benefit_key, :dedupe_status,
|
||||
:canonical_realization_id, 'pending_confirmation'
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
def execute_rejected(
|
||||
connection: Any,
|
||||
statement: Any,
|
||||
parameters: dict[str, Any],
|
||||
error_type: type[DBAPIError] = IntegrityError,
|
||||
) -> None:
|
||||
savepoint = connection.begin_nested()
|
||||
try:
|
||||
with pytest.raises(error_type):
|
||||
connection.execute(statement, parameters)
|
||||
finally:
|
||||
if savepoint.is_active:
|
||||
savepoint.rollback()
|
||||
|
||||
with engine.connect() as connection:
|
||||
transaction = connection.begin()
|
||||
try:
|
||||
connection.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO expense_cases (
|
||||
id, tenant_id, case_no, scene_code, title, current_stage, status
|
||||
) VALUES (
|
||||
'case-a', 'tenant-a', 'CASE-A', 'expense_reimbursement',
|
||||
'迁移约束探针', 'payment', 'active'
|
||||
)
|
||||
"""
|
||||
)
|
||||
)
|
||||
connection.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO profile_baseline_snapshots (
|
||||
id, tenant_id, baseline_key, baseline_type, dimension_type,
|
||||
dimension_id, metric_key, unit, original_currency,
|
||||
baseline_value, sample_count, method, query_fingerprint,
|
||||
data_quality_status, data_quality_score, algorithm_version,
|
||||
policy_version, policy_effective_from, target_resource_type,
|
||||
target_resource_id, frozen_at, frozen_by
|
||||
) VALUES (
|
||||
'baseline-a', 'tenant-a', 'baseline-a',
|
||||
'policy_counterfactual', 'claim_item', 'item-soft-ref',
|
||||
'approved_amount', 'currency', 'CNY', 100, 0,
|
||||
'server_policy', 'query-fingerprint-a', 'complete', 1,
|
||||
'policy-calculator-v1', 'policy-v1', current_date,
|
||||
'claim_item', 'item-soft-ref', now(), 'system'
|
||||
)
|
||||
"""
|
||||
)
|
||||
)
|
||||
connection.execute(
|
||||
opportunity_insert,
|
||||
{
|
||||
"id": "opportunity-a",
|
||||
"tenant_id": "tenant-a",
|
||||
"opportunity_key": "opportunity-key-a",
|
||||
"benefit_key": "benefit-a",
|
||||
"expense_case_id": "case-a",
|
||||
"baseline_snapshot_id": "baseline-a",
|
||||
},
|
||||
)
|
||||
connection.execute(
|
||||
opportunity_insert,
|
||||
{
|
||||
"id": "opportunity-b",
|
||||
"tenant_id": "tenant-a",
|
||||
"opportunity_key": "opportunity-key-b",
|
||||
"benefit_key": "benefit-b",
|
||||
"expense_case_id": "case-a",
|
||||
"baseline_snapshot_id": "baseline-a",
|
||||
},
|
||||
)
|
||||
execute_rejected(
|
||||
connection,
|
||||
opportunity_insert,
|
||||
{
|
||||
"id": "cross-tenant-opportunity",
|
||||
"tenant_id": "tenant-b",
|
||||
"opportunity_key": "cross-tenant-opportunity",
|
||||
"benefit_key": "cross-tenant-benefit",
|
||||
"expense_case_id": "case-a",
|
||||
"baseline_snapshot_id": "baseline-a",
|
||||
},
|
||||
)
|
||||
|
||||
common_actual = {
|
||||
"opportunity_id": "opportunity-a",
|
||||
"realization_type": "actual",
|
||||
"reversal_of_realization_id": None,
|
||||
"actual_gross": 20,
|
||||
"actual_net": 20,
|
||||
"reporting_amount": 20,
|
||||
"canonical_realization_id": None,
|
||||
}
|
||||
connection.execute(
|
||||
realization_insert,
|
||||
{
|
||||
**common_actual,
|
||||
"id": "realization-a",
|
||||
"benefit_key": "benefit-a",
|
||||
"dedupe_status": "pending_review",
|
||||
},
|
||||
)
|
||||
connection.execute(
|
||||
realization_insert,
|
||||
{
|
||||
**common_actual,
|
||||
"id": "realization-canonical",
|
||||
"benefit_key": "benefit-canonical",
|
||||
"dedupe_status": "canonical",
|
||||
},
|
||||
)
|
||||
execute_rejected(
|
||||
connection,
|
||||
realization_insert,
|
||||
{
|
||||
**common_actual,
|
||||
"id": "realization-canonical-duplicate",
|
||||
"benefit_key": "benefit-canonical",
|
||||
"dedupe_status": "canonical",
|
||||
},
|
||||
)
|
||||
execute_rejected(
|
||||
connection,
|
||||
realization_insert,
|
||||
{
|
||||
**common_actual,
|
||||
"id": "realization-wrong-canonical",
|
||||
"benefit_key": "other-benefit",
|
||||
"dedupe_status": "duplicate",
|
||||
"canonical_realization_id": "realization-a",
|
||||
},
|
||||
)
|
||||
execute_rejected(
|
||||
connection,
|
||||
realization_insert,
|
||||
{
|
||||
"id": "cross-opportunity-reversal",
|
||||
"opportunity_id": "opportunity-b",
|
||||
"realization_type": "reversal",
|
||||
"reversal_of_realization_id": "realization-a",
|
||||
"actual_gross": -5,
|
||||
"actual_net": -5,
|
||||
"reporting_amount": -5,
|
||||
"benefit_key": "benefit-a",
|
||||
"dedupe_status": "pending_review",
|
||||
"canonical_realization_id": None,
|
||||
},
|
||||
)
|
||||
execute_rejected(
|
||||
connection,
|
||||
text(
|
||||
"""
|
||||
UPDATE savings_realizations
|
||||
SET status = 'finance_confirmed',
|
||||
finance_confirmer_id = recorded_by_id,
|
||||
finance_confirmer_name = '同一填报人',
|
||||
confirmed_at = now(), confirmation_note = '不应通过'
|
||||
WHERE id = 'realization-canonical'
|
||||
"""
|
||||
),
|
||||
{},
|
||||
)
|
||||
connection.execute(
|
||||
text(
|
||||
"""
|
||||
UPDATE savings_realizations
|
||||
SET status = 'finance_confirmed',
|
||||
finance_confirmer_id = 'finance-b',
|
||||
finance_confirmer_name = '独立财务',
|
||||
confirmed_at = now(), confirmation_note = '证据完整'
|
||||
WHERE id = 'realization-canonical'
|
||||
"""
|
||||
)
|
||||
)
|
||||
assert connection.scalar(
|
||||
text(
|
||||
"SELECT status FROM savings_realizations "
|
||||
"WHERE id = 'realization-canonical'"
|
||||
)
|
||||
) == "finance_confirmed"
|
||||
connection.execute(
|
||||
realization_insert,
|
||||
{
|
||||
"id": "valid-reversal",
|
||||
"opportunity_id": "opportunity-a",
|
||||
"realization_type": "reversal",
|
||||
"reversal_of_realization_id": "realization-a",
|
||||
"actual_gross": -5,
|
||||
"actual_net": -5,
|
||||
"reporting_amount": -5,
|
||||
"benefit_key": "benefit-a",
|
||||
"dedupe_status": "canonical",
|
||||
"canonical_realization_id": None,
|
||||
},
|
||||
)
|
||||
connection.execute(
|
||||
text(
|
||||
"""
|
||||
UPDATE savings_realizations
|
||||
SET status = 'finance_confirmed',
|
||||
finance_confirmer_id = recorded_by_id,
|
||||
finance_confirmer_name = '冲回财务',
|
||||
confirmed_at = now(), confirmation_note = '全额冲回'
|
||||
WHERE id = 'valid-reversal'
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
connection.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO savings_events (
|
||||
id, tenant_id, aggregate_type, aggregate_id,
|
||||
opportunity_id, action, actor_id, actor_name, actor_type,
|
||||
request_id, expected_version, result_version,
|
||||
payload_fingerprint
|
||||
) VALUES (
|
||||
'event-a', 'tenant-a', 'opportunity', 'opportunity-a',
|
||||
'opportunity-a', 'created', 'system', '系统', 'system',
|
||||
'request-a', 0, 1, 'fingerprint-a'
|
||||
)
|
||||
"""
|
||||
)
|
||||
)
|
||||
execute_rejected(
|
||||
connection,
|
||||
text("UPDATE savings_events SET action = 'tampered' WHERE id = 'event-a'"),
|
||||
{},
|
||||
DBAPIError,
|
||||
)
|
||||
execute_rejected(
|
||||
connection,
|
||||
text("DELETE FROM savings_events WHERE id = 'event-a'"),
|
||||
{},
|
||||
DBAPIError,
|
||||
)
|
||||
finally:
|
||||
transaction.rollback()
|
||||
Reference in New Issue
Block a user