Files
X-Financial/server/tests/test_expense_financial_value_chain_e2e.py
caoxiaozhu 787bc3a481 feat(platform): close AI expense value loop
Add tenant-safe value, telemetry, connector, commercial, and production-readiness foundations.
2026-07-17 14:14:08 +08:00

592 lines
20 KiB
Python

from __future__ import annotations
import uuid
from datetime import UTC, date, datetime, timedelta
from decimal import Decimal
from sqlalchemy import create_engine, func, select
from sqlalchemy.orm import Session, sessionmaker
from sqlalchemy.pool import StaticPool
import app.models # noqa: F401 - 注册完整 metadata
from app.api.deps import CurrentUserContext
from app.db.base_class import Base
from app.models.budget import BudgetAllocation
from app.models.employee import Employee
from app.models.expense_case import BusinessEvent
from app.models.financial_connector import (
FinancialConnectorConfig,
FinancialConnectorEvent,
PaymentReconciliationCase,
)
from app.models.financial_record import ExpenseClaim
from app.models.organization import OrganizationUnit
from app.models.savings import SavingsEvidenceLink, SavingsRealization
from app.schemas.commercial import (
CommercialCostEventCreate,
CommercialPlanCreate,
CommercialPricingScenarioWrite,
CommercialSubscriptionCreate,
)
from app.schemas.financial_connector import FinancialEventEnvelope
from app.schemas.reimbursement import ExpenseClaimItemCreate
from app.schemas.savings import SavingsRealizationActionCreate
from app.services.commercial_admin import CommercialAdminService
from app.services.commercial_analytics import CommercialAnalyticsService
from app.services.commercial_metering import CommercialMeteringService
from app.services.commercial_pricing import CommercialPricingService
from app.services.expense_claims import ExpenseClaimService
from app.services.financial_connector_auth import (
FinancialConnectorSecretResolver,
sign_financial_event,
)
from app.services.financial_connector_ingestion import FinancialConnectorIngestionService
from app.services.savings_discovery import SavingsDiscoveryService
from app.services.savings_realization import SavingsRealizationService
def test_expense_application_to_refund_and_value_evidence_chain() -> None:
"""正式服务贯穿申请、审批、外部财务事实和商业价值口径。"""
engine = create_engine(
"sqlite+pysqlite:///:memory:",
connect_args={"check_same_thread": False},
poolclass=StaticPool,
)
Base.metadata.create_all(engine)
factory = sessionmaker(bind=engine, expire_on_commit=False)
try:
with factory() as db:
_run_chain(db)
finally:
Base.metadata.drop_all(engine)
engine.dispose()
def _run_chain(db: Session) -> None:
now = datetime.now(UTC)
users, application = _seed_application_context(db, now=now)
claims = ExpenseClaimService(db)
submitted_application = claims.submit_claim(
application.id,
users["employee"],
correlation_id="value-chain-application-submit",
)
assert submitted_application is not None
assert (submitted_application.status, submitted_application.approval_stage) == (
"submitted",
"直属领导审批",
)
approved_application = claims.approve_claim(
application.id,
users["manager"],
opinion="差旅必要且预算充足,同意生成报销草稿。",
request_id="value-chain-application-approve",
expected_status="submitted",
expected_approval_stage="直属领导审批",
)
assert approved_application is not None
assert (approved_application.status, approved_application.approval_stage) == (
"approved",
"关联单据状态",
)
reimbursement = _generated_reimbursement(db, approved_application)
reimbursement = claims.create_claim_item(
claim_id=reimbursement.id,
payload=ExpenseClaimItemCreate(
item_date=date.today(),
item_type="hotel",
item_reason="上海客户现场住宿",
item_location="上海",
item_amount=Decimal("66.00"),
invoice_id="invoice-value-chain-001",
),
current_user=users["employee"],
)
assert reimbursement is not None
reimbursable_item = next(
item for item in reimbursement.items if item.invoice_id == "invoice-value-chain-001"
)
opportunity = SavingsDiscoveryService(db).discover_standard_adjustments(
claim=reimbursement,
items_by_id={reimbursable_item.id: reimbursable_item},
adjustment_flags=[
{
"item_id": reimbursable_item.id,
"message": "已发布住宿政策将原始 100 元锁定为可报 66 元。",
"original_amount": "100.00",
"reimbursable_amount": "66.00",
"employee_absorbed_amount": "34.00",
"policy_rule_version": "value-chain-hotel-v1",
"policy_rule_version_source": "published",
"policy_grade": "P6",
"policy_matched_city": "上海",
"calculation_fingerprint": "sha256:" + "a" * 64,
}
],
current_user=users["employee"],
request_id="value-chain-saving-discovery",
)[0]
db.commit()
submitted = claims.submit_claim(
reimbursement.id,
users["employee"],
correlation_id="value-chain-reimbursement-submit",
)
assert submitted is not None and submitted.approval_stage == "直属领导审批"
manager_approved = claims.approve_claim(
reimbursement.id,
users["manager"],
opinion="业务真实,同意进入财务审核。",
request_id="value-chain-manager-approve",
expected_status="submitted",
expected_approval_stage="直属领导审批",
)
assert manager_approved is not None and manager_approved.approval_stage == "财务审批"
finance_approved = claims.approve_claim(
reimbursement.id,
users["finance"],
opinion="票据、政策调整和预算均已复核。",
request_id="value-chain-finance-approve",
expected_status="submitted",
expected_approval_stage="财务审批",
)
assert finance_approved is not None
assert (finance_approved.status, finance_approved.approval_stage) == (
"pending_payment",
"待付款",
)
_seed_commercial_account(db, now=now)
_seed_connector(db)
db.commit()
timestamp = 1_800_000_000
before_payment_events = _event_count(db, "payment_completed")
mismatch = _financial_event(
reimbursement,
event_id="value-chain-settlement-mismatch",
extra={"amount": "999.00"},
)
mismatch_result = _ingest(db, mismatch, timestamp)
db.commit()
db.refresh(reimbursement)
assert mismatch_result.error_code == "amount_mismatch"
assert reimbursement.status == "pending_payment"
assert _event_count(db, "payment_completed") == before_payment_events
assert (
db.scalar(
select(func.count(SavingsRealization.id)).where(
SavingsRealization.opportunity_id == opportunity.id
)
)
== 0
)
settlement = _financial_event(reimbursement, event_id="value-chain-settlement-001")
settled = _ingest(db, settlement, timestamp + 1)
db.commit()
replay = _ingest(db, settlement, timestamp + 1)
db.commit()
db.refresh(reimbursement)
db.refresh(application)
assert settled.processing_status == "processed" and replay.replayed is True
assert settled.verification_level == "production_verified"
assert settled.evidence_classification == "external_cash"
assert reimbursement.status == "paid"
assert application.approval_stage == "申请归档"
assert _event_count(db, "payment_completed") == before_payment_events + 1
realization = db.scalar(
select(SavingsRealization).where(
SavingsRealization.opportunity_id == opportunity.id,
SavingsRealization.realization_type == "actual",
)
)
assert realization is not None and realization.actual_net == Decimal("34.0000")
confirmation = SavingsRealizationActionCreate(
action="confirm",
request_id="value-chain-saving-confirm",
expected_version=1,
comment="独立财务已复核生产银行回执、政策基线和归因键。",
)
confirmed = SavingsRealizationService(db).execute_action(
realization.id,
confirmation,
users["confirmer"],
)
confirmed_replay = SavingsRealizationService(db).execute_action(
realization.id,
confirmation,
users["confirmer"],
)
assert confirmed.response.replayed is False
assert confirmed_replay.response.replayed is True
evidence = db.scalar(
select(SavingsEvidenceLink).where(
SavingsEvidenceLink.realization_id == realization.id,
SavingsEvidenceLink.resource_type == "financial_connector_event",
)
)
assert evidence is not None and evidence.verification_status == "verified"
erp = _financial_event(
reimbursement,
event_id="value-chain-erp-001",
event_type="erp_posted",
extra={
"origin_external_event_id": settlement.external_event_id,
"erp_document_number": "ERP-VALUE-CHAIN-000001",
"accounting_period": "2026-07",
},
)
erp_result = _ingest(db, erp, timestamp + 2)
db.commit()
case = db.get(PaymentReconciliationCase, settled.reconciliation_case_id)
assert erp_result.processing_status == "processed"
assert case is not None and case.erp_status == "posted"
assert case.erp_document_tail == "N-000001"
window = {
"start": now - timedelta(days=2),
"end": now + timedelta(days=2),
"as_of": now + timedelta(days=1),
}
before_refund = CommercialAnalyticsService(db).build("default", **window)
assert _money(before_refund.verified_cash_savings) == {"CNY": Decimal("34.0000")}
pricing = CommercialPricingService(db).build(
"default",
CommercialPricingScenarioWrite(
**window,
target_contribution_margin_rate=Decimal("0.50"),
max_verified_savings_share=Decimal("0.50"),
),
)
assert pricing.recommended_model == "hybrid"
assert pricing.scenarios[0].minimum_sustainable_charge == Decimal("10.0000")
assert pricing.scenarios[0].maximum_value_aligned_charge == Decimal("17.0000")
refund = _financial_event(
reimbursement,
event_id="value-chain-refund-001",
event_type="payment_refunded",
extra={"origin_external_event_id": settlement.external_event_id},
)
refunded = _ingest(db, refund, timestamp + 3)
db.commit()
refund_replay = _ingest(db, refund, timestamp + 3)
db.commit()
db.refresh(reimbursement)
assert refunded.processing_status == "processed" and refund_replay.replayed is True
assert reimbursement.status == "pending_payment"
realizations = list(
db.scalars(
select(SavingsRealization).where(SavingsRealization.opportunity_id == opportunity.id)
).all()
)
assert len(realizations) == 2
assert sum((row.reporting_amount for row in realizations), Decimal("0")) == Decimal("0")
duplicate_reversal = _financial_event(
reimbursement,
event_id="value-chain-reversal-after-refund",
event_type="payment_reversed",
extra={"origin_external_event_id": settlement.external_event_id},
)
duplicate_result = _ingest(db, duplicate_reversal, timestamp + 4)
db.commit()
assert duplicate_result.processing_status == "exception"
assert duplicate_result.error_code == "claim_not_paid"
assert (
db.scalar(
select(func.count(SavingsRealization.id)).where(
SavingsRealization.opportunity_id == opportunity.id
)
)
== 2
)
after_refund = CommercialAnalyticsService(db).build("default", **window)
assert _money(after_refund.verified_cash_savings) == {"CNY": Decimal("0.0000")}
pricing_after_refund = CommercialPricingService(db).build(
"default",
CommercialPricingScenarioWrite(
**window,
target_contribution_margin_rate=Decimal("0.50"),
max_verified_savings_share=Decimal("0.50"),
),
)
assert pricing_after_refund.recommended_model == "optimize_unit_economics"
assert pricing_after_refund.scenarios[0].maximum_value_aligned_charge == Decimal("0.0000")
assert (
CommercialAnalyticsService(db)
.build(
"tenant-other",
**window,
)
.verified_cash_savings.values
== []
)
assert (
db.scalar(
select(func.count(FinancialConnectorEvent.id)).where(
FinancialConnectorEvent.external_event_id == refund.external_event_id
)
)
== 1
)
def _seed_application_context(
db: Session,
*,
now: datetime,
) -> tuple[dict[str, CurrentUserContext], ExpenseClaim]:
department = OrganizationUnit(
unit_code="VALUE-CHAIN-DEPT",
name="价值链试点部",
unit_type="department",
)
manager = Employee(
employee_no="VALUE-MANAGER",
name="价值链经理",
email="value-manager@example.com",
organization_unit=department,
)
employee = Employee(
employee_no="VALUE-EMPLOYEE",
name="价值链员工",
email="value-employee@example.com",
organization_unit=department,
manager=manager,
)
db.add_all([department, manager, employee])
db.flush()
db.add(
BudgetAllocation(
budget_no="BUD-VALUE-CHAIN-2026Q3",
fiscal_year=2026,
period_type="quarter",
period_key="2026Q3",
department_id=department.id,
department_name=department.name,
subject_code="travel",
subject_name="差旅",
original_amount=Decimal("50000.00"),
adjusted_amount=Decimal("0.00"),
status="active",
warning_threshold=Decimal("80.00"),
control_action="block",
)
)
application = ExpenseClaim(
id=str(uuid.uuid4()),
claim_no="APP-VALUE-CHAIN-20260716",
employee_id=employee.id,
employee_name=employee.name,
department_id=department.id,
department_name=department.name,
expense_type="travel_application",
reason="上海客户现场差旅",
location="上海",
amount=Decimal("66.00"),
currency="CNY",
invoice_count=0,
occurred_at=now,
status="draft",
approval_stage="待提交",
risk_flags_json=[],
)
db.add(application)
db.commit()
return (
{
"employee": _user(
employee.email,
name=employee.name,
employee_id=employee.id,
),
"manager": _user(
manager.email,
name=manager.name,
employee_id=manager.id,
roles=["manager"],
),
"finance": _user("value-finance@example.com", roles=["finance"]),
"confirmer": _user("value-confirmer@example.com", roles=["finance"]),
},
application,
)
def _generated_reimbursement(
db: Session,
application: ExpenseClaim,
) -> ExpenseClaim:
draft_id = next(
str(flag.get("generated_draft_claim_id"))
for flag in application.risk_flags_json
if isinstance(flag, dict) and flag.get("generated_draft_claim_id")
)
draft = db.get(ExpenseClaim, draft_id)
assert draft is not None and draft.status == "draft"
return draft
def _seed_commercial_account(db: Session, *, now: datetime) -> None:
admin = CommercialAdminService(db)
plan = admin.create_plan(
"default",
CommercialPlanCreate(
plan_code="value-pilot",
name="价值证明试点",
pricing_model="hybrid",
billing_interval="monthly",
currency="CNY",
base_fee=Decimal("8.00"),
included_seats=10,
effective_from=now - timedelta(days=30),
),
actor_id="platform-admin",
)
admin.activate_plan("default", plan.id, expected_version=plan.version)
subscription = admin.create_subscription(
"default",
CommercialSubscriptionCreate(
subscription_key="default-value-pilot-2026",
plan_id=plan.id,
starts_at=now - timedelta(days=10),
current_period_start=now - timedelta(days=1),
current_period_end=now + timedelta(days=29),
seats=5,
),
actor_id="platform-admin",
)
CommercialMeteringService(db).record_cost(
"default",
CommercialCostEventCreate(
subscription_id=subscription.id,
cost_category="connector",
quantity=Decimal("1"),
unit="workflow",
unit_cost=Decimal("5.00"),
original_currency="CNY",
reporting_currency="CNY",
fx_rate=Decimal("1"),
allocation_key="value-chain-workflow",
occurred_at=now,
source_system="cost-ledger",
idempotency_key="value-chain-cost-001",
),
)
def _seed_connector(db: Session) -> None:
db.add(
FinancialConnectorConfig(
id=str(uuid.uuid4()),
tenant_id="default",
provider="value-bank",
environment="production",
key_version="v1",
secret_ref="connector/value-chain",
allowed_event_types_json=[
"payment_settled",
"payment_failed",
"erp_posted",
"erp_posting_failed",
"payment_refunded",
"payment_reversed",
],
clock_skew_seconds=300,
status="active",
created_by="platform-admin",
)
)
def _financial_event(
claim: ExpenseClaim,
*,
event_id: str,
event_type: str = "payment_settled",
extra: dict[str, str] | None = None,
) -> FinancialEventEnvelope:
return FinancialEventEnvelope(
tenant_id="default",
external_event_id=event_id,
event_type=event_type,
occurred_at=datetime.now(UTC),
correlation_id=f"corr-{event_id}",
payload={
"claim_id": claim.id,
"claim_reference": claim.claim_no,
"amount": str(claim.amount),
"currency": claim.currency,
"external_payment_reference": f"PAY-{claim.claim_no}",
**dict(extra or {}),
},
)
def _ingest(
db: Session,
envelope: FinancialEventEnvelope,
timestamp: int,
):
secret = "value-chain-server-secret"
signature = sign_financial_event(
envelope,
timestamp=timestamp,
secret=secret,
tenant_id="default",
provider="value-bank",
key_version="v1",
)
return FinancialConnectorIngestionService(
db,
secrets=FinancialConnectorSecretResolver({"connector/value-chain": secret}),
now_epoch=timestamp,
).ingest(
envelope,
tenant_header="default",
provider_header="value-bank",
key_version_header="v1",
timestamp_header=str(timestamp),
signature_header=signature,
)
def _event_count(db: Session, event_type: str) -> int:
return int(
db.scalar(
select(func.count(BusinessEvent.id)).where(BusinessEvent.event_type == event_type)
)
or 0
)
def _money(metric) -> dict[str, Decimal]:
return {item.currency: item.amount for item in metric.values}
def _user(
username: str,
*,
name: str | None = None,
employee_id: str = "",
roles: list[str] | None = None,
) -> CurrentUserContext:
return CurrentUserContext(
username=username,
name=name or username,
employee_id=employee_id,
role_codes=list(roles or []),
is_admin=False,
tenant_id="default",
)