feat(platform): close AI expense value loop
Add tenant-safe value, telemetry, connector, commercial, and production-readiness foundations.
This commit is contained in:
591
server/tests/test_expense_financial_value_chain_e2e.py
Normal file
591
server/tests/test_expense_financial_value_chain_e2e.py
Normal file
@@ -0,0 +1,591 @@
|
||||
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",
|
||||
)
|
||||
Reference in New Issue
Block a user