592 lines
20 KiB
Python
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",
|
||
|
|
)
|