138 lines
5.2 KiB
Python
138 lines
5.2 KiB
Python
import sqlite3
|
|
from decimal import Decimal
|
|
from pathlib import Path
|
|
|
|
from fin_hub.services.billing import build_billing_basis
|
|
from fin_hub.services.exchange import ingest_planning_evidence
|
|
from fin_hub.services.ledger import import_csv, record_engagement_price
|
|
|
|
CLIENT_KEY = "client:acme|app:portal|instance:prod-01"
|
|
|
|
|
|
def _seed_billing_ledger(tmp_path: Path):
|
|
ledger = tmp_path / "ledger.db"
|
|
direct = tmp_path / "direct.csv"
|
|
direct.write_text(
|
|
"product,amount,currency,invoice_date,environment,client_id,application_id,app_instance_id\n"
|
|
"Dedicated service,20.00,EUR,2026-07-01,production,acme,portal,prod-01\n",
|
|
encoding="utf-8",
|
|
)
|
|
shared = tmp_path / "shared.csv"
|
|
shared.write_text(
|
|
"product,amount,currency,invoice_date,environment\n"
|
|
"Shared cluster,100.00,EUR,2026-07-01,production\n",
|
|
encoding="utf-8",
|
|
)
|
|
import_csv(direct, "hosteurope", ledger_path=ledger)
|
|
import_csv(shared, "hosteurope", ledger_path=ledger)
|
|
with sqlite3.connect(ledger) as conn:
|
|
shared_fact_id = conn.execute(
|
|
"SELECT financial_fact_id FROM ledger_entries "
|
|
"WHERE cost_attribution_key IS NULL"
|
|
).fetchone()[0]
|
|
price = record_engagement_price(
|
|
client_id="acme",
|
|
application_id="portal",
|
|
app_instance_id="prod-01",
|
|
period_month="2026-07",
|
|
amount="200.00",
|
|
currency="EUR",
|
|
source="agreement-v1",
|
|
ledger_path=ledger,
|
|
)
|
|
allocation = {
|
|
"schema_version": "0.1",
|
|
"record_type": "allocation",
|
|
"record_id": "allocation:shared:2026-07:v1",
|
|
"revision_of": None,
|
|
"resource_id": "resource:shared_cluster",
|
|
"service_id": "shared-cluster",
|
|
"workload_id": None,
|
|
"tenant_id": None,
|
|
"environment": "production",
|
|
"cost_attribution_key": "platform:shared-cluster",
|
|
"period_start": "2026-07-01",
|
|
"period_end": "2026-07-31",
|
|
"currency": "EUR",
|
|
"source_evidence": ["resource-control:namespace-cpu-v1"],
|
|
"created_at": "2026-08-11T10:00:00Z",
|
|
"financial_fact_ids": [shared_fact_id],
|
|
"method": "namespace-cpu-v1",
|
|
"allocated_amount": "100.00",
|
|
"shares": [{"target_key": CLIENT_KEY, "share": "0.80"}],
|
|
"residual_share": "0.20",
|
|
}
|
|
ingest_planning_evidence(allocation, ledger_path=ledger)
|
|
return ledger, direct, price, allocation
|
|
|
|
|
|
def test_billing_basis_combines_direct_and_allocated_cost_idempotently(tmp_path: Path):
|
|
ledger, _direct, price, allocation = _seed_billing_ledger(tmp_path)
|
|
|
|
first = build_billing_basis(ledger_path=ledger)
|
|
second = build_billing_basis(ledger_path=ledger)
|
|
|
|
assert first == second
|
|
assert first.artifact_type == "billing_basis_report"
|
|
assert "not an invoice" in first.disclaimer
|
|
record = first.records[0]
|
|
assert record.price_id == price.id
|
|
assert record.revenue == Decimal("200.00")
|
|
assert record.direct_cost == Decimal("20.00")
|
|
assert record.allocated_cost == Decimal("80.00")
|
|
assert record.total_cost == Decimal("100.00")
|
|
assert record.margin == Decimal("100.00")
|
|
assert record.allocation_ids == (allocation["record_id"],)
|
|
assert record.financial_fact_ids
|
|
assert "agreement-v1" in record.provenance
|
|
assert first.exceptions[0].reason == "unattributed_allocation_residual"
|
|
assert first.exceptions[0].amount == Decimal("20.00")
|
|
|
|
|
|
def test_billing_basis_id_changes_and_exposes_correction_and_price_revision(tmp_path: Path):
|
|
ledger, direct, price, _allocation = _seed_billing_ledger(tmp_path)
|
|
before = build_billing_basis(ledger_path=ledger).records[0]
|
|
direct.write_text(
|
|
"product,amount,currency,invoice_date,environment,client_id,application_id,app_instance_id\n"
|
|
"Dedicated service,25.00,EUR,2026-07-01,production,acme,portal,prod-01\n",
|
|
encoding="utf-8",
|
|
)
|
|
import_csv(direct, "hosteurope", ledger_path=ledger, force=True)
|
|
revised_price = record_engagement_price(
|
|
client_id="acme",
|
|
application_id="portal",
|
|
app_instance_id="prod-01",
|
|
period_month="2026-07",
|
|
amount="210.00",
|
|
currency="EUR",
|
|
source="agreement-v2",
|
|
revision_of=price.id,
|
|
ledger_path=ledger,
|
|
)
|
|
|
|
after = build_billing_basis(ledger_path=ledger).records[0]
|
|
|
|
assert after.billing_basis_id != before.billing_basis_id
|
|
assert after.price_id == revised_price.id
|
|
assert after.price_revision_of == price.id
|
|
assert after.financial_fact_corrections
|
|
assert after.direct_cost == Decimal("25.00")
|
|
assert after.margin == Decimal("105.00")
|
|
|
|
|
|
def test_client_cost_without_price_is_an_explicit_exception(tmp_path: Path):
|
|
ledger = tmp_path / "ledger.db"
|
|
source = tmp_path / "direct.csv"
|
|
source.write_text(
|
|
"product,amount,currency,invoice_date,client_id,application_id,app_instance_id\n"
|
|
"Dedicated service,20.00,EUR,2026-07-01,acme,portal,prod-01\n",
|
|
encoding="utf-8",
|
|
)
|
|
import_csv(source, "hosteurope", ledger_path=ledger)
|
|
|
|
export = build_billing_basis(ledger_path=ledger)
|
|
|
|
assert export.records == ()
|
|
assert export.exceptions[0].reason == "missing_price_basis"
|
|
assert export.exceptions[0].cost_attribution_key == CLIENT_KEY
|
|
assert export.exceptions[0].amount == Decimal("20.00")
|