Files
X-Financial/server/tests/savings_migration_assertions.py

387 lines
15 KiB
Python
Raw Permalink Normal View History

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()