feat(platform): close AI expense value loop
Add tenant-safe value, telemetry, connector, commercial, and production-readiness foundations.
This commit is contained in:
193
server/tests/test_savings_value_e2e.py
Normal file
193
server/tests/test_savings_value_e2e.py
Normal file
@@ -0,0 +1,193 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from datetime import UTC, date, datetime, timedelta
|
||||
from decimal import Decimal
|
||||
|
||||
from sqlalchemy import create_engine, 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.expense_case import BusinessEvent
|
||||
from app.models.financial_record import ExpenseClaim, ExpenseClaimItem
|
||||
from app.models.savings import SavingsEvidenceLink, SavingsRealization
|
||||
from app.schemas.cfo_value import CfoValueFiltersRead
|
||||
from app.schemas.savings import SavingsRealizationActionCreate
|
||||
from app.services.cfo_value_analytics import CfoValueAnalyticsService
|
||||
from app.services.expense_claims import ExpenseClaimService
|
||||
from app.services.savings_discovery import SavingsDiscoveryService
|
||||
from app.services.savings_realization import SavingsRealizationService
|
||||
|
||||
|
||||
def test_standard_adjustment_payment_confirmation_and_reversal_value_e2e() -> 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_value_e2e(db)
|
||||
finally:
|
||||
Base.metadata.drop_all(engine)
|
||||
engine.dispose()
|
||||
|
||||
|
||||
def _run_value_e2e(db: Session) -> None:
|
||||
payer = _finance_user("finance-payer")
|
||||
confirmer = _finance_user("finance-confirmer")
|
||||
claim = ExpenseClaim(
|
||||
id=str(uuid.uuid4()),
|
||||
claim_no="BX-SAVINGS-E2E-001",
|
||||
employee_name="测试员工",
|
||||
department_name="销售部",
|
||||
project_code="PROJECT-E2E",
|
||||
expense_type="hotel",
|
||||
reason="上海客户现场差旅",
|
||||
location="上海",
|
||||
amount=Decimal("66.00"),
|
||||
currency="CNY",
|
||||
invoice_count=1,
|
||||
occurred_at=datetime.now(UTC),
|
||||
submitted_at=datetime.now(UTC),
|
||||
status="pending_payment",
|
||||
approval_stage="待付款",
|
||||
risk_flags_json=[],
|
||||
)
|
||||
item = ExpenseClaimItem(
|
||||
id=str(uuid.uuid4()),
|
||||
claim=claim,
|
||||
item_date=date.today(),
|
||||
item_type="hotel",
|
||||
item_reason="上海住宿",
|
||||
item_location="上海",
|
||||
item_note="",
|
||||
item_amount=Decimal("100.00"),
|
||||
)
|
||||
db.add(claim)
|
||||
db.flush()
|
||||
opportunity = SavingsDiscoveryService(db).discover_standard_adjustments(
|
||||
claim=claim,
|
||||
items_by_id={item.id: item},
|
||||
adjustment_flags=[
|
||||
{
|
||||
"item_id": item.id,
|
||||
"message": "服务端按已发布住宿政策从 100 元调整为 66 元",
|
||||
"original_amount": "100.00",
|
||||
"reimbursable_amount": "66.00",
|
||||
"employee_absorbed_amount": "34.00",
|
||||
"policy_rule_version": "hotel-policy-e2e-v1",
|
||||
"policy_rule_version_source": "published",
|
||||
"policy_grade": "P6",
|
||||
"policy_matched_city": "上海",
|
||||
"calculation_fingerprint": "sha256:" + "d" * 64,
|
||||
}
|
||||
],
|
||||
current_user=payer,
|
||||
request_id="e2e-discovery-001",
|
||||
)[0]
|
||||
db.commit()
|
||||
|
||||
ExpenseClaimService(db).mark_claim_paid(
|
||||
claim.id,
|
||||
payer,
|
||||
request_id="e2e-payment-001",
|
||||
expected_status="pending_payment",
|
||||
expected_approval_stage="待付款",
|
||||
)
|
||||
realization = db.scalar(
|
||||
select(SavingsRealization).where(
|
||||
SavingsRealization.opportunity_id == opportunity.id,
|
||||
SavingsRealization.realization_type == "actual",
|
||||
)
|
||||
)
|
||||
assert realization is not None
|
||||
assert realization.status == "pending_confirmation"
|
||||
assert realization.actual_net == Decimal("34.0000")
|
||||
assert _verified_cash(db, confirmer) == {}
|
||||
|
||||
confirmed = SavingsRealizationService(db).execute_action(
|
||||
realization.id,
|
||||
SavingsRealizationActionCreate(
|
||||
action="confirm",
|
||||
request_id="e2e-confirm-001",
|
||||
expected_version=1,
|
||||
comment="独立复核政策基线、付款事件、归因和去重键。",
|
||||
),
|
||||
confirmer,
|
||||
)
|
||||
confirmed_as_of = confirmed.response.event.occurred_at
|
||||
assert _verified_cash(db, confirmer) == {"CNY": Decimal("34.0000")}
|
||||
evidence = list(
|
||||
db.scalars(
|
||||
select(SavingsEvidenceLink).where(
|
||||
SavingsEvidenceLink.realization_id == realization.id
|
||||
)
|
||||
).all()
|
||||
)
|
||||
assert evidence and all(item.verification_status == "verified" for item in evidence)
|
||||
|
||||
SavingsRealizationService(db).execute_action(
|
||||
realization.id,
|
||||
SavingsRealizationActionCreate(
|
||||
action="reverse",
|
||||
request_id="e2e-reverse-001",
|
||||
expected_version=2,
|
||||
comment="员工申诉后完成补付,全额冲回原节省。",
|
||||
reversal_amount=Decimal("34.00"),
|
||||
),
|
||||
confirmer,
|
||||
)
|
||||
assert _verified_cash(db, confirmer) == {"CNY": Decimal("0.0000")}
|
||||
assert _verified_cash(db, confirmer, as_of=confirmed_as_of) == {
|
||||
"CNY": Decimal("34.0000")
|
||||
}
|
||||
event_types = set(
|
||||
db.scalars(
|
||||
select(BusinessEvent.event_type).where(
|
||||
BusinessEvent.expense_case_id == opportunity.expense_case_id
|
||||
)
|
||||
).all()
|
||||
)
|
||||
assert {
|
||||
"saving_opportunity_created",
|
||||
"payment_completed",
|
||||
"saving_action_completed",
|
||||
"saving_confirmed",
|
||||
"saving_reversed",
|
||||
} <= event_types
|
||||
|
||||
|
||||
def _verified_cash(
|
||||
db: Session,
|
||||
current_user: CurrentUserContext,
|
||||
*,
|
||||
as_of: datetime | None = None,
|
||||
) -> dict[str, Decimal]:
|
||||
now = datetime.now(UTC)
|
||||
dashboard = CfoValueAnalyticsService(db).build_dashboard(
|
||||
current_user,
|
||||
start=now - timedelta(days=1),
|
||||
end=now + timedelta(days=1),
|
||||
as_of=as_of or now,
|
||||
filters=CfoValueFiltersRead(),
|
||||
)
|
||||
return {
|
||||
item.currency: item.amount for item in dashboard.kpis.verified_cash.values
|
||||
}
|
||||
|
||||
|
||||
def _finance_user(username: str) -> CurrentUserContext:
|
||||
return CurrentUserContext(
|
||||
username=username,
|
||||
name=username,
|
||||
role_codes=["finance"],
|
||||
is_admin=False,
|
||||
tenant_id="default",
|
||||
employee_id=username,
|
||||
)
|
||||
Reference in New Issue
Block a user