fin-hub/tests/test_ai_plan.py
tegwick ce4f791a63 feat: book monthly AI-plan invoices as facts and commitments
Add ai-plan ingest that stores one current booked fact per
provider-account × plan × period. Subscriptions become recurring
commitments; usage top-ups do not. Session-level rows are rejected.
2026-08-15 19:09:14 +02:00

147 lines
6 KiB
Python

from pathlib import Path
import pytest
from fin_hub.ingest.ai_plan import parse_ai_plan_csv
from fin_hub.services.evaluate import evaluate_runway
from fin_hub.services.exchange import booked_cost_projection
from fin_hub.services.ledger import (
import_csv,
list_current_commitments,
monthly_summary,
set_opening_balance,
)
FIXTURES = Path(__file__).parent / "fixtures"
def test_parse_ai_plan_fixture_keeps_subscription_and_topup_separate():
rows = parse_ai_plan_csv(FIXTURES / "ai-plans.csv")
assert len(rows) == 2
assert rows[0].provider == "anthropic"
assert rows[0].charge_kind == "subscription"
assert rows[0].ongoing is True
assert rows[0].provider_account is None
assert rows[1].provider == "openai"
assert rows[1].charge_kind == "usage_topup"
assert rows[1].ongoing is False
assert rows[1].provider_account == "org-demo"
assert rows[0].incurred_on.isoformat() == "2026-08-01"
def test_parse_ai_plan_rejects_session_level_rows(tmp_path: Path):
source = tmp_path / "sessions.csv"
source.write_text(
"provider,plan,charge_kind,amount,currency,period_month,session_id\n"
"anthropic,claude-max,subscription,200.00,EUR,2026-08,sess-1\n",
encoding="utf-8",
)
with pytest.raises(ValueError, match="session-level"):
parse_ai_plan_csv(source)
def test_import_ai_plans_books_once_and_commits_only_subscriptions(tmp_path: Path):
ledger = tmp_path / "ledger.db"
first = import_csv(FIXTURES / "ai-plans.csv", "ai-plan", ledger_path=ledger)
second = import_csv(FIXTURES / "ai-plans.csv", "ai-plan", ledger_path=ledger)
assert first.rows_imported == 2
assert second.skipped is True
rollup = monthly_summary(ledger_path=ledger)[0]
assert rollup.currency == "EUR"
assert rollup.period_month == "2026-08"
assert rollup.total == pytest.approx(240.0)
assert rollup.entry_count == 2
commitments = list_current_commitments(ledger_path=ledger)
assert len(commitments) == 1
assert commitments[0].provider == "anthropic"
assert commitments[0].plan == "claude-max"
assert commitments[0].amount == 200
assert commitments[0].financial_fact_id is not None
def test_ai_plan_correction_is_append_only(tmp_path: Path):
ledger = tmp_path / "ledger.db"
source = tmp_path / "plans.csv"
source.write_text(
"provider,plan,charge_kind,amount,currency,period_month,ongoing\n"
"anthropic,claude-max,subscription,200.00,EUR,2026-08,true\n",
encoding="utf-8",
)
import_csv(source, "ai-plan", ledger_path=ledger)
source.write_text(
"provider,plan,charge_kind,amount,currency,period_month,ongoing\n"
"anthropic,claude-max,subscription,220.00,EUR,2026-08,true\n",
encoding="utf-8",
)
with pytest.raises(ValueError, match="explicit correction"):
import_csv(source, "ai-plan", ledger_path=ledger)
corrected = import_csv(source, "ai-plan", ledger_path=ledger, force=True)
assert corrected.rows_imported == 1
assert monthly_summary(ledger_path=ledger)[0].total == pytest.approx(220.0)
commitments = list_current_commitments(ledger_path=ledger)
assert len(commitments) == 1
assert commitments[0].amount == 220
assert commitments[0].revision_of is not None
def test_ending_a_subscription_closes_the_commitment(tmp_path: Path):
ledger = tmp_path / "ledger.db"
source = tmp_path / "plans.csv"
source.write_text(
"provider,plan,charge_kind,amount,currency,period_month,ongoing\n"
"anthropic,claude-max,subscription,200.00,EUR,2026-08,true\n",
encoding="utf-8",
)
import_csv(source, "ai-plan", ledger_path=ledger)
source.write_text(
"provider,plan,charge_kind,amount,currency,period_month,ongoing\n"
"anthropic,claude-max,subscription,200.00,EUR,2026-09,false\n",
encoding="utf-8",
)
import_csv(source, "ai-plan", ledger_path=ledger)
assert list_current_commitments(ledger_path=ledger) == []
months = {row.period_month: row.total for row in monthly_summary(ledger_path=ledger)}
assert months["2026-08"] == pytest.approx(200.0)
assert months["2026-09"] == pytest.approx(200.0)
def test_ai_plan_facts_project_without_invented_resource_id(tmp_path: Path):
ledger = tmp_path / "ledger.db"
import_csv(FIXTURES / "ai-plans.csv", "ai-plan", ledger_path=ledger)
facts = booked_cost_projection(ledger_path=ledger)
by_provider = {fact.provider: fact for fact in facts}
assert set(by_provider) == {"anthropic", "openai"}
assert by_provider["anthropic"].service_id == "claude-max"
assert by_provider["anthropic"].resource_id is None
assert by_provider["anthropic"].tax_status == "unknown"
assert by_provider["openai"].service_id == "api-prepaid"
assert by_provider["openai"].effective_amount == 40
def test_ai_plan_commitment_appears_on_runway_when_no_other_burns(tmp_path: Path):
ledger = tmp_path / "ledger.db"
set_opening_balance(ledger, 12000.0, currency="EUR")
source = tmp_path / "future-only.csv"
source.write_text(
"provider,plan,charge_kind,amount,currency,period_month,ongoing\n"
"xai,grok-heavy,subscription,300.00,USD,2026-09,true\n",
encoding="utf-8",
)
import_csv(source, "ai-plan", ledger_path=ledger)
empty_eur = evaluate_runway(ledger_path=ledger, currency="EUR")
assert empty_eur["commitments"] == []
usd = evaluate_runway(ledger_path=ledger, currency="USD")
assert usd["runway"]["monthly_burn"] == pytest.approx(300.0)
assert usd["commitments"][0]["plan"] == "grok-heavy"
def test_booked_ai_plan_invoices_drive_runway_without_double_counting(tmp_path: Path):
ledger = tmp_path / "ledger.db"
set_opening_balance(ledger, 12000.0)
import_csv(FIXTURES / "ai-plans.csv", "ai-plan", ledger_path=ledger)
report = evaluate_runway(ledger_path=ledger)
assert report["runway"]["monthly_burn"] == pytest.approx(240.0)
assert len(report["commitments"]) == 1
assert report["commitments"][0]["amount"] == 200