97 lines
3.4 KiB
Python
97 lines
3.4 KiB
Python
|
|
"""Pure, offline tests for target_revenue.metrics (WP-0006-T05).
|
||
|
|
|
||
|
|
No Docker/Postgres required — metrics.compute_metrics is a pure function
|
||
|
|
of (manifest, entries, as_of), like fold.py (TSD §6.1).
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from datetime import datetime, timezone
|
||
|
|
|
||
|
|
from conftest import golden_entries, golden_manifest
|
||
|
|
|
||
|
|
from target_revenue import metrics
|
||
|
|
|
||
|
|
|
||
|
|
def test_mandatory_facts_present_for_golden_phase():
|
||
|
|
manifest = golden_manifest()
|
||
|
|
entries = golden_entries()
|
||
|
|
as_of = datetime(2026, 12, 1, tzinfo=timezone.utc)
|
||
|
|
|
||
|
|
result = metrics.compute_metrics(manifest, entries, as_of)
|
||
|
|
|
||
|
|
facts = result["facts"]
|
||
|
|
assert facts["initial_target_amount"] == manifest["phase"]["initial_target"]["amount"]
|
||
|
|
assert facts["initial_target_currency"] == manifest["phase"]["initial_target"]["currency"]
|
||
|
|
assert facts["last_ledger_entry_id"] == entries[-1]["id"]
|
||
|
|
assert facts["longstop_at"] == manifest["phase"]["longstop_at"]
|
||
|
|
assert "cumulative_development_credit" in facts
|
||
|
|
assert "cumulative_remission_credit" in facts
|
||
|
|
assert "outstanding_target" in facts
|
||
|
|
assert "is_converted" in facts
|
||
|
|
|
||
|
|
|
||
|
|
def test_calculations_and_forecast_are_separately_labeled():
|
||
|
|
manifest = golden_manifest()
|
||
|
|
entries = golden_entries()
|
||
|
|
as_of = datetime(2026, 12, 1, tzinfo=timezone.utc)
|
||
|
|
|
||
|
|
result = metrics.compute_metrics(manifest, entries, as_of)
|
||
|
|
|
||
|
|
assert set(result.keys()) == {"phase", "as_of", "facts", "calculations", "forecasts"}
|
||
|
|
assert "target_satisfaction_percentage" in result["calculations"]
|
||
|
|
assert "projected_conversion_date" in result["forecasts"]
|
||
|
|
# A forecast must never appear inside the facts block.
|
||
|
|
assert "projected_conversion_date" not in result["facts"]
|
||
|
|
|
||
|
|
|
||
|
|
def test_target_satisfaction_percentage_matches_fold():
|
||
|
|
manifest = golden_manifest()
|
||
|
|
entries = golden_entries()
|
||
|
|
as_of = datetime(2026, 12, 1, tzinfo=timezone.utc)
|
||
|
|
|
||
|
|
result = metrics.compute_metrics(manifest, entries, as_of)
|
||
|
|
facts = result["facts"]
|
||
|
|
initial = facts["initial_target_amount"]
|
||
|
|
expected_pct = round(
|
||
|
|
100.0
|
||
|
|
* min(
|
||
|
|
1.0,
|
||
|
|
(facts["cumulative_development_credit"] + facts["cumulative_remission_credit"])
|
||
|
|
/ initial,
|
||
|
|
),
|
||
|
|
4,
|
||
|
|
)
|
||
|
|
assert result["calculations"]["target_satisfaction_percentage"] == expected_pct
|
||
|
|
|
||
|
|
|
||
|
|
def test_no_material_entries_yields_no_velocity_or_forecast():
|
||
|
|
manifest = golden_manifest()
|
||
|
|
as_of = datetime(2026, 12, 1, tzinfo=timezone.utc)
|
||
|
|
|
||
|
|
result = metrics.compute_metrics(manifest, [], as_of)
|
||
|
|
|
||
|
|
assert result["calculations"]["development_credit_velocity_per_day"] is None
|
||
|
|
assert result["calculations"]["remission_credit_velocity_per_day"] is None
|
||
|
|
assert result["calculations"]["days_since_last_material_progress"] is None
|
||
|
|
assert result["forecasts"]["projected_conversion_date"] is None
|
||
|
|
assert result["facts"]["outstanding_target"] == manifest["phase"]["initial_target"]["amount"]
|
||
|
|
|
||
|
|
|
||
|
|
def test_converted_phase_has_no_forecast():
|
||
|
|
manifest = golden_manifest()
|
||
|
|
entries = golden_entries()
|
||
|
|
as_of = datetime(2026, 12, 1, tzinfo=timezone.utc)
|
||
|
|
result = metrics.compute_metrics(manifest, entries, as_of)
|
||
|
|
if result["facts"]["is_converted"]:
|
||
|
|
assert result["forecasts"]["projected_conversion_date"] is None
|
||
|
|
|
||
|
|
|
||
|
|
def test_as_of_must_be_timezone_aware():
|
||
|
|
manifest = golden_manifest()
|
||
|
|
entries = golden_entries()
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
with pytest.raises(ValueError):
|
||
|
|
metrics.compute_metrics(manifest, entries, datetime(2026, 12, 1))
|