Harden resource cost evidence contract
This commit is contained in:
parent
080f756fff
commit
00343307fd
22 changed files with 1623 additions and 130 deletions
|
|
@ -21,8 +21,11 @@ def test_build_service_cost_report_groups_by_service():
|
|||
report = build_service_cost_report(lines)
|
||||
assert report["signal"] == "service_cost_attribution"
|
||||
assert len(report["services"]) == 2
|
||||
assert report["totals_by_month"]["2026-06"] == pytest.approx(99.8)
|
||||
assert report["totals_by_period_currency"] == [
|
||||
{"period_month": "2026-06", "currency": "EUR", "total": pytest.approx(99.8)}
|
||||
]
|
||||
assert report["attributions"][0]["cost_attribution_key"] is None
|
||||
assert report["unattributed"][0]["total"] == pytest.approx(99.8)
|
||||
|
||||
|
||||
def test_build_service_cost_report_groups_by_client_attribution():
|
||||
|
|
@ -49,6 +52,7 @@ def test_build_service_cost_report_groups_by_client_attribution():
|
|||
"client_id": "acme",
|
||||
"application_id": "portal",
|
||||
"app_instance_id": "prod-01",
|
||||
"environment": "production",
|
||||
"currency": "EUR",
|
||||
"months": {"2026-07": 42.0},
|
||||
"total": 42.0,
|
||||
|
|
@ -77,3 +81,31 @@ def test_client_attribution_totals_do_not_mix_currencies():
|
|||
("EUR", 42.0),
|
||||
("USD", 50.0),
|
||||
}
|
||||
|
||||
|
||||
def test_service_totals_do_not_mix_currency_or_environment():
|
||||
common = {
|
||||
"service_id": "cluster",
|
||||
"period_month": "2026-07",
|
||||
"source": "fixture",
|
||||
}
|
||||
report = build_service_cost_report(
|
||||
[
|
||||
ServiceCostLine(amount=10.0, currency="EUR", environment="production", **common),
|
||||
ServiceCostLine(amount=20.0, currency="USD", environment="production", **common),
|
||||
ServiceCostLine(amount=30.0, currency="EUR", environment="development", **common),
|
||||
]
|
||||
)
|
||||
|
||||
assert {
|
||||
(row["environment"], row["currency"], row["total"])
|
||||
for row in report["services"]
|
||||
} == {
|
||||
("production", "EUR", 10.0),
|
||||
("production", "USD", 20.0),
|
||||
("development", "EUR", 30.0),
|
||||
}
|
||||
assert report["totals_by_period_currency"] == [
|
||||
{"period_month": "2026-07", "currency": "EUR", "total": 40.0},
|
||||
{"period_month": "2026-07", "currency": "USD", "total": 20.0},
|
||||
]
|
||||
|
|
|
|||
207
tests/test_exchange.py
Normal file
207
tests/test_exchange.py
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
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,
|
||||
ingest_planning_evidence,
|
||||
ingest_resource_forecast,
|
||||
)
|
||||
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_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)
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
import sqlite3
|
||||
import os
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
|
@ -11,6 +13,7 @@ from fin_hub.services.ledger import (
|
|||
monthly_burn_series,
|
||||
monthly_summary,
|
||||
record_engagement_price,
|
||||
reverse_financial_fact,
|
||||
set_opening_balance,
|
||||
)
|
||||
|
||||
|
|
@ -44,6 +47,86 @@ def test_ledger_import_skips_unchanged_file(tmp_path: Path):
|
|||
assert second.rows_imported == 0
|
||||
|
||||
|
||||
def test_ledger_import_is_idempotent_when_file_is_renamed_or_touched(tmp_path: Path):
|
||||
ledger = tmp_path / "ledger.db"
|
||||
source = tmp_path / "costs.csv"
|
||||
shutil.copy(FIXTURES / "cloud-costs.csv", source)
|
||||
renamed = tmp_path / "renamed.csv"
|
||||
|
||||
first = import_csv(source, "cloud", ledger_path=ledger)
|
||||
os.utime(source, (source.stat().st_atime, source.stat().st_mtime + 1))
|
||||
touched = import_csv(source, "cloud", ledger_path=ledger)
|
||||
shutil.copy(source, renamed)
|
||||
delivered_again = import_csv(renamed, "cloud", ledger_path=ledger)
|
||||
|
||||
assert first.rows_imported == 3
|
||||
assert touched.rows_imported == 0
|
||||
assert delivered_again.rows_imported == 0
|
||||
assert monthly_summary(ledger_path=ledger)[0].entry_count == 3
|
||||
|
||||
|
||||
def test_changed_fact_requires_and_applies_one_correction(tmp_path: Path):
|
||||
ledger = tmp_path / "ledger.db"
|
||||
source = tmp_path / "costs.csv"
|
||||
source.write_text(
|
||||
"service,amount,currency,period_month\ncompute,10.00,EUR,2026-07\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
import_csv(source, "cloud", ledger_path=ledger)
|
||||
source.write_text(
|
||||
"service,amount,currency,period_month\ncompute,12.00,EUR,2026-07\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="explicit correction"):
|
||||
import_csv(source, "cloud", ledger_path=ledger)
|
||||
corrected = import_csv(source, "cloud", ledger_path=ledger, force=True)
|
||||
duplicate = import_csv(source, "cloud", ledger_path=ledger, force=True)
|
||||
|
||||
assert corrected.rows_imported == 1
|
||||
assert duplicate.rows_imported == 0
|
||||
rollup = monthly_summary(ledger_path=ledger)[0]
|
||||
assert rollup.total == 12.0
|
||||
assert rollup.entry_count == 1
|
||||
with sqlite3.connect(ledger) as conn:
|
||||
facts = conn.execute(
|
||||
"SELECT correction_of, is_current FROM ledger_entries ORDER BY id"
|
||||
).fetchall()
|
||||
assert facts[0][1] == 0
|
||||
assert facts[1][0] is not None
|
||||
assert facts[1][1] == 1
|
||||
|
||||
|
||||
def test_financial_fact_reversal_is_append_only_and_zeroes_effective_total(tmp_path: Path):
|
||||
ledger = tmp_path / "ledger.db"
|
||||
source = tmp_path / "costs.csv"
|
||||
source.write_text(
|
||||
"service,amount,currency,period_month\ncompute,10.00,EUR,2026-07\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
import_csv(source, "cloud", ledger_path=ledger)
|
||||
with sqlite3.connect(ledger) as conn:
|
||||
fact_id = conn.execute(
|
||||
"SELECT financial_fact_id FROM ledger_entries"
|
||||
).fetchone()[0]
|
||||
|
||||
reversal_id = reverse_financial_fact(
|
||||
fact_id, source="provider-credit-note", ledger_path=ledger
|
||||
)
|
||||
|
||||
rollup = monthly_summary(ledger_path=ledger)[0]
|
||||
assert rollup.total == 0.0
|
||||
with sqlite3.connect(ledger) as conn:
|
||||
rows = conn.execute(
|
||||
"SELECT financial_fact_id, correction_of, adjustment_kind, is_current "
|
||||
"FROM ledger_entries ORDER BY id"
|
||||
).fetchall()
|
||||
assert rows == [
|
||||
(fact_id, None, "charge", 0),
|
||||
(reversal_id, fact_id, "reversal", 1),
|
||||
]
|
||||
|
||||
|
||||
def test_ledger_import_preserves_client_attribution(tmp_path: Path):
|
||||
source = tmp_path / "attributed.csv"
|
||||
source.write_text(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
"""Smoke tests for fin-hub model registration."""
|
||||
|
||||
from datetime import date
|
||||
from decimal import Decimal
|
||||
|
||||
import pytest
|
||||
|
||||
from fin_hub.models import (
|
||||
Budget,
|
||||
BurnRate,
|
||||
|
|
@ -9,6 +14,8 @@ from fin_hub.models import (
|
|||
ServiceCost,
|
||||
TokenSpend,
|
||||
)
|
||||
from fin_hub.models.engagement_price import _validate_engagement_price
|
||||
from fin_hub.models.service_cost import _normalize_service_cost_attribution
|
||||
from hub_core.models.base import Base
|
||||
|
||||
|
||||
|
|
@ -39,3 +46,41 @@ def test_service_cost_has_external_attribution_seam():
|
|||
assert columns["application_id"].nullable is True
|
||||
assert columns["app_instance_id"].nullable is True
|
||||
assert columns["cost_attribution_key"].nullable is True
|
||||
|
||||
|
||||
def test_service_cost_orm_uses_shared_money_and_period_invariants():
|
||||
cost = ServiceCost(
|
||||
service_id="cluster",
|
||||
environment="production",
|
||||
period_month="2026-07",
|
||||
amount=Decimal("10.005"),
|
||||
currency="eur",
|
||||
source=" invoice ",
|
||||
)
|
||||
_normalize_service_cost_attribution(None, None, cost)
|
||||
assert cost.amount == Decimal("10.00")
|
||||
assert cost.currency == "EUR"
|
||||
assert cost.source == "invoice"
|
||||
|
||||
cost.period_month = "2026-13"
|
||||
with pytest.raises(ValueError, match="YYYY-MM"):
|
||||
_normalize_service_cost_attribution(None, None, cost)
|
||||
|
||||
|
||||
def test_engagement_price_orm_normalizes_same_basis_as_sqlite():
|
||||
price = EngagementPrice(
|
||||
client_id="acme",
|
||||
application_id="portal",
|
||||
app_instance_id="prod-01",
|
||||
cost_attribution_key=None,
|
||||
period_month="placeholder",
|
||||
effective_from=date(2026, 7, 15),
|
||||
amount=Decimal("100.005"),
|
||||
currency="eur",
|
||||
source=" agreement ",
|
||||
)
|
||||
_validate_engagement_price(None, None, price)
|
||||
assert price.period_month == "2026-07"
|
||||
assert price.amount == Decimal("100.00")
|
||||
assert price.currency == "EUR"
|
||||
assert price.source == "agreement"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue