Ingest resource-control's August technical usage, omit nulls, and reconcile forecast + usage + booked facts. Commissioning month reports missing_booked_fact instead of 0.00. T05 still waits on the first invoice.
415 lines
14 KiB
Python
415 lines
14 KiB
Python
import json
|
|
from datetime import date, datetime, timezone
|
|
from decimal import Decimal
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from fin_hub.models.engagement_price import EngagementPrice, _validate_engagement_price
|
|
from fin_hub.schemas.exchange import (
|
|
AllocationEvidence,
|
|
BookedCostEvidence,
|
|
ForecastEvidence,
|
|
)
|
|
from fin_hub.services.exchange import (
|
|
booked_cost_projection,
|
|
exchange_health,
|
|
financial_constraint_projection,
|
|
ingest_planning_evidence,
|
|
ingest_resource_forecast,
|
|
ingest_resource_usage,
|
|
reconcile_resource_period,
|
|
)
|
|
from fin_hub.services.ledger import import_csv
|
|
|
|
|
|
def _common_planning() -> dict:
|
|
return {
|
|
"schema_version": "0.1",
|
|
"record_id": "forecast:platform-audit-storage:2026-08",
|
|
"revision_of": None,
|
|
"resource_id": "resource:platform_audit_storage",
|
|
"service_id": "object-storage",
|
|
"workload_id": "platform-pg",
|
|
"tenant_id": None,
|
|
"environment": "production",
|
|
"cost_attribution_key": "platform:audit-storage",
|
|
"period_start": "2026-09-01",
|
|
"period_end": "2027-08-31",
|
|
"currency": "eur",
|
|
"source_evidence": ["resource-control:data/forecasts/platform-audit-storage"],
|
|
"created_at": "2026-08-10T17:10:00Z",
|
|
}
|
|
|
|
|
|
def test_booked_cost_money_relationships_and_decimal_json():
|
|
record = BookedCostEvidence(
|
|
financial_fact_id="fact:1",
|
|
correction_of=None,
|
|
adjustment_kind="charge",
|
|
source_type="provider_invoice",
|
|
source_document_id="invoice:1",
|
|
source_line_id="invoice:1:line:1",
|
|
content_fingerprint="sha256:abc",
|
|
provider="provider",
|
|
accounting_period="2026-07",
|
|
currency="eur",
|
|
net_amount="10.005",
|
|
discount_amount="1.00",
|
|
tax_status="known",
|
|
tax_amount="1.90",
|
|
gross_amount="10.90",
|
|
adjustment_amount="0",
|
|
effective_amount="10.90",
|
|
source_evidence_ref="invoice:1",
|
|
recorded_at=datetime.now(timezone.utc),
|
|
)
|
|
|
|
assert record.net_amount == Decimal("10.00")
|
|
assert record.currency == "EUR"
|
|
assert '"gross_amount":"10.90"' in record.model_dump_json()
|
|
|
|
|
|
def test_booked_cost_rejects_invalid_relationships():
|
|
with pytest.raises(ValidationError, match="gross_amount"):
|
|
BookedCostEvidence(
|
|
financial_fact_id="fact:1",
|
|
adjustment_kind="charge",
|
|
source_type="invoice",
|
|
source_document_id="doc",
|
|
source_line_id="line",
|
|
content_fingerprint="hash",
|
|
provider="provider",
|
|
accounting_period="2026-07",
|
|
currency="EUR",
|
|
net_amount="10",
|
|
discount_amount="0",
|
|
tax_status="known",
|
|
tax_amount="1.90",
|
|
gross_amount="12",
|
|
effective_amount="12",
|
|
source_evidence_ref="doc",
|
|
recorded_at=datetime.now(timezone.utc),
|
|
)
|
|
|
|
|
|
def test_planning_forecast_is_typed_and_idempotent(tmp_path: Path):
|
|
payload = {
|
|
**_common_planning(),
|
|
"record_type": "forecast",
|
|
"scenario": "base",
|
|
"forecast_version": "2026-08-base",
|
|
"costs": {
|
|
"infrastructure": "2.89",
|
|
"internal_labor": "60",
|
|
"external_services": "0",
|
|
"setup": "0",
|
|
"other": "0",
|
|
},
|
|
"uncertainty": "provider price excludes unknown tax",
|
|
"assumptions": ["180 GB stored"],
|
|
}
|
|
ledger = tmp_path / "ledger.db"
|
|
|
|
first = ingest_planning_evidence(payload, ledger_path=ledger)
|
|
second = ingest_planning_evidence(payload, ledger_path=ledger)
|
|
|
|
assert isinstance(first, ForecastEvidence)
|
|
assert first.costs.total == Decimal("62.89")
|
|
assert second.record_id == first.record_id
|
|
|
|
|
|
def test_resource_control_backup_forecast_enters_planning_store(tmp_path: Path):
|
|
payload = {
|
|
"schema_version": "0.1",
|
|
"record_type": "forecast",
|
|
"workload": "platform-pg",
|
|
"cost_attribution_key": "platform:audit-storage",
|
|
"provider_id": "scaleway-standard-multi-az",
|
|
"created_at": "2026-08-10T17:10:00Z",
|
|
"scenario": "base",
|
|
"forecast_ref": None,
|
|
"rows": [
|
|
{
|
|
"period": "2026-09",
|
|
"database_gb": 5,
|
|
"stored_gb": 180,
|
|
"wal_gb": 30,
|
|
"restore_egress_gb": 5,
|
|
"write_requests": 2500,
|
|
"read_requests": 1000,
|
|
"infrastructure_eur": 2.89,
|
|
"internal_labor_hours": 1,
|
|
"internal_labor_eur": 60,
|
|
"total_eur": 62.89,
|
|
}
|
|
],
|
|
}
|
|
records = ingest_resource_forecast(
|
|
payload,
|
|
resource_id="resource:platform_audit_storage",
|
|
source_evidence_ref="resource-control:data/forecasts/platform-audit-storage",
|
|
ledger_path=tmp_path / "ledger.db",
|
|
)
|
|
|
|
assert len(records) == 1
|
|
assert isinstance(records[0], ForecastEvidence)
|
|
assert records[0].costs.total == Decimal("62.89")
|
|
|
|
|
|
def test_allocation_requires_shares_and_residual_to_reconcile():
|
|
payload = {
|
|
**_common_planning(),
|
|
"record_type": "allocation",
|
|
"record_id": "allocation:1",
|
|
"financial_fact_ids": ["fact:1"],
|
|
"method": "namespace-cpu-v1",
|
|
"allocated_amount": "10.00",
|
|
"shares": [{"target_key": "client:acme", "share": "0.75"}],
|
|
"residual_share": "0.20",
|
|
}
|
|
with pytest.raises(ValidationError, match="must equal 1"):
|
|
AllocationEvidence.model_validate(payload)
|
|
|
|
|
|
def test_booked_cost_projection_uses_current_corrected_fact(tmp_path: Path):
|
|
source = tmp_path / "cost.csv"
|
|
ledger = tmp_path / "ledger.db"
|
|
source.write_text(
|
|
"product,amount,currency,invoice_date,environment\n"
|
|
"Server,10.00,EUR,2026-07-01,production\n",
|
|
encoding="utf-8",
|
|
)
|
|
import_csv(source, "hosteurope", ledger_path=ledger)
|
|
source.write_text(
|
|
"product,amount,currency,invoice_date,environment\n"
|
|
"Server,12.00,EUR,2026-07-01,production\n",
|
|
encoding="utf-8",
|
|
)
|
|
import_csv(source, "hosteurope", ledger_path=ledger, force=True)
|
|
|
|
records = booked_cost_projection(ledger_path=ledger)
|
|
|
|
assert len(records) == 1
|
|
assert records[0].effective_amount == Decimal("12.00")
|
|
assert records[0].correction_of is not None
|
|
assert records[0].environment == "production"
|
|
|
|
|
|
def test_hosteurope_fact_joins_external_resource_by_stable_fact_id(tmp_path: Path):
|
|
source = tmp_path / "hosteurope.csv"
|
|
ledger = tmp_path / "ledger.db"
|
|
source.write_text(
|
|
"product,amount,currency,invoice_date,environment\n"
|
|
"Server,49.00,EUR,2026-07-01,production\n",
|
|
encoding="utf-8",
|
|
)
|
|
import_csv(source, "hosteurope", ledger_path=ledger)
|
|
fact = booked_cost_projection(ledger_path=ledger)[0]
|
|
|
|
joined = booked_cost_projection(
|
|
ledger_path=ledger,
|
|
fact_resource_ids={
|
|
fact.financial_fact_id: "resource:hosteurope:railiance01"
|
|
},
|
|
)
|
|
|
|
assert joined[0].resource_id == "resource:hosteurope:railiance01"
|
|
assert joined[0].provider == "hosteurope"
|
|
with pytest.raises(ValueError, match="unknown current financial facts"):
|
|
booked_cost_projection(
|
|
ledger_path=ledger,
|
|
fact_resource_ids={"fact:unknown": "resource:hosteurope:railiance01"},
|
|
)
|
|
|
|
|
|
def test_sqlalchemy_price_uses_shared_invariants():
|
|
price = EngagementPrice(
|
|
client_id="acme",
|
|
application_id="portal",
|
|
app_instance_id="prod-01",
|
|
cost_attribution_key="wrong",
|
|
effective_from=date(2026, 7, 1),
|
|
amount=Decimal("100.001"),
|
|
currency="eur",
|
|
source="agreement",
|
|
)
|
|
with pytest.raises(ValueError, match="does not match"):
|
|
_validate_engagement_price(None, None, price)
|
|
|
|
|
|
def test_financial_constraints_are_bounded_typed_and_idempotent():
|
|
kwargs = {
|
|
"domain_slug": "financials",
|
|
"period_start": date(2026, 8, 1),
|
|
"period_end": date(2026, 8, 31),
|
|
"currency": "eur",
|
|
"budget_ceiling": "100.00",
|
|
"active_commitment": "20.00",
|
|
"spent": "85.00",
|
|
"monthly_burn": "30.00",
|
|
"runway_months": "2.5",
|
|
"runway_threshold_months": "3",
|
|
"resource_id": "resource:hosteurope:railiance01",
|
|
"source_evidence": ("fin-hub:budget:2026-08", "fin-hub:runway:2026-08"),
|
|
"generated_at": datetime(2026, 8, 11, 13, 0, tzinfo=timezone.utc),
|
|
}
|
|
|
|
first = financial_constraint_projection(**kwargs)
|
|
second = financial_constraint_projection(**kwargs)
|
|
|
|
assert first == second
|
|
assert [signal.signal_kind for signal in first.signals] == [
|
|
"budget_ceiling",
|
|
"active_commitment",
|
|
"burn_pressure",
|
|
"runway_pressure",
|
|
]
|
|
assert first.signals[0].classification == "policy_constraint"
|
|
assert first.signals[2].classification == "informational_warning"
|
|
assert first.signals[3].metric_value == Decimal("2.5")
|
|
assert all(signal.currency == "EUR" for signal in first.signals)
|
|
assert "Not payment authority" in first.disclaimer
|
|
|
|
|
|
def test_financial_constraints_do_not_invent_missing_authoritative_values():
|
|
export = financial_constraint_projection(
|
|
domain_slug="financials",
|
|
period_start=date(2026, 8, 1),
|
|
period_end=date(2026, 8, 31),
|
|
currency="EUR",
|
|
source_evidence=("fin-hub:budget:none",),
|
|
generated_at=datetime(2026, 8, 11, 13, 0, tzinfo=timezone.utc),
|
|
)
|
|
|
|
assert export.signals == ()
|
|
|
|
|
|
def test_financial_constraint_rejects_invalid_period_or_unbounded_provenance():
|
|
with pytest.raises(ValueError, match="period_end"):
|
|
financial_constraint_projection(
|
|
domain_slug="financials",
|
|
period_start=date(2026, 9, 1),
|
|
period_end=date(2026, 8, 31),
|
|
currency="EUR",
|
|
source_evidence=("fin-hub:budget",),
|
|
)
|
|
with pytest.raises(ValueError, match="source_evidence"):
|
|
financial_constraint_projection(
|
|
domain_slug="financials",
|
|
period_start=date(2026, 8, 1),
|
|
period_end=date(2026, 8, 31),
|
|
currency="EUR",
|
|
source_evidence=(),
|
|
)
|
|
|
|
|
|
def test_exchange_health_exposes_rejection_stale_forecast_and_unattributed_cost(
|
|
tmp_path: Path,
|
|
):
|
|
ledger = tmp_path / "ledger.db"
|
|
source = tmp_path / "hosteurope.csv"
|
|
source.write_text(
|
|
"product,amount,currency,invoice_date,environment\n"
|
|
"Shared service,10.00,EUR,2026-07-01,production\n",
|
|
encoding="utf-8",
|
|
)
|
|
import_csv(source, "hosteurope", ledger_path=ledger)
|
|
stale = {
|
|
**_common_planning(),
|
|
"record_type": "forecast",
|
|
"record_id": "forecast:stale",
|
|
"period_start": "2026-01-01",
|
|
"period_end": "2026-01-31",
|
|
"scenario": "base",
|
|
"forecast_version": "v1",
|
|
"costs": {},
|
|
"assumptions": [],
|
|
}
|
|
ingest_planning_evidence(stale, ledger_path=ledger)
|
|
invalid = {**stale, "record_id": "forecast:rejected", "currency": "EURO"}
|
|
with pytest.raises(ValidationError):
|
|
ingest_planning_evidence(invalid, ledger_path=ledger)
|
|
with pytest.raises(ValidationError):
|
|
ingest_planning_evidence(invalid, ledger_path=ledger)
|
|
|
|
health = exchange_health(ledger_path=ledger, as_of=date(2026, 8, 11))
|
|
|
|
assert health.booked_fact_count == 1
|
|
assert health.current_planning_record_count == 1
|
|
assert health.rejected_delivery_count == 2
|
|
assert {issue.code for issue in health.issues} == {
|
|
"rejected_planning_delivery",
|
|
"stale_forecast",
|
|
"unattributed_booked_cost",
|
|
}
|
|
|
|
|
|
FIXTURES = Path(__file__).parent / "fixtures"
|
|
AUDIT_STORAGE = "resource:platform:audit-storage"
|
|
|
|
|
|
def test_audit_storage_usage_omits_nulls_and_does_not_invent_zero_spend(tmp_path: Path):
|
|
ledger = tmp_path / "ledger.db"
|
|
payload = json.loads(
|
|
(FIXTURES / "platform-audit-storage-usage-2026-08.json").read_text()
|
|
)
|
|
records = ingest_resource_usage(
|
|
payload,
|
|
resource_id=AUDIT_STORAGE,
|
|
source_evidence_ref="resource-control:data/actuals/2026-08.json",
|
|
ledger_path=ledger,
|
|
)
|
|
assert len(records) == 1
|
|
names = {measure.name: measure for measure in records[0].measures}
|
|
assert set(names) == {
|
|
"database_gb",
|
|
"stored_gb",
|
|
"backup_success_pct",
|
|
"restore_rto_minutes",
|
|
}
|
|
assert "infrastructure_eur" not in names
|
|
assert names["stored_gb"].value == Decimal("0.0066")
|
|
assert any("gap:infrastructure_eur" in item for item in records[0].source_evidence)
|
|
|
|
forecast = {
|
|
"schema_version": "0.1",
|
|
"record_type": "forecast",
|
|
"workload": "platform-pg",
|
|
"cost_attribution_key": "platform:audit-storage",
|
|
"provider_id": "scaleway-standard-multi-az",
|
|
"created_at": "2026-08-10T17:10:00Z",
|
|
"scenario": "base",
|
|
"forecast_ref": None,
|
|
"rows": [
|
|
{
|
|
"period": "2026-09",
|
|
"database_gb": 5,
|
|
"stored_gb": 180,
|
|
"wal_gb": 30,
|
|
"restore_egress_gb": 5,
|
|
"write_requests": 2500,
|
|
"read_requests": 1000,
|
|
"infrastructure_eur": 2.89,
|
|
"internal_labor_hours": 1,
|
|
"internal_labor_eur": 60,
|
|
"total_eur": 62.89,
|
|
}
|
|
],
|
|
}
|
|
ingest_resource_forecast(
|
|
forecast,
|
|
resource_id=AUDIT_STORAGE,
|
|
source_evidence_ref="resource-control:data/forecasts/platform-audit-storage",
|
|
ledger_path=ledger,
|
|
)
|
|
august = reconcile_resource_period(
|
|
resource_id=AUDIT_STORAGE, period="2026-08", ledger_path=ledger
|
|
)
|
|
assert august.usage_record_ids
|
|
assert august.missing_booked_fact is True
|
|
assert august.booked_effective_amount is None
|
|
assert august.invented_zero is False
|
|
health = exchange_health(ledger_path=ledger)
|
|
assert "usage_without_booked_fact" in {issue.code for issue in health.issues}
|