Add shared cost allocation reporting
This commit is contained in:
parent
a28a40dfe9
commit
b75ad06b3a
9 changed files with 348 additions and 15 deletions
|
|
@ -2,7 +2,7 @@
|
||||||
# Custodian Brief — fin-hub
|
# Custodian Brief — fin-hub
|
||||||
|
|
||||||
**Domain:** financials
|
**Domain:** financials
|
||||||
**Last synced:** 2026-08-11 08:37 UTC
|
**Last synced:** 2026-08-11 09:04 UTC
|
||||||
**State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)*
|
**State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)*
|
||||||
|
|
||||||
## Active Workstreams
|
## Active Workstreams
|
||||||
|
|
@ -16,12 +16,11 @@ Progress: 6/9 done | workplan_id: `67b6de6c-4820-4478-9789-f50260204c27`
|
||||||
- · T06 — Generalize and operate the contract `a1309d51`
|
- · T06 — Generalize and operate the contract `a1309d51`
|
||||||
|
|
||||||
### Client attribution and billing basis
|
### Client attribution and billing basis
|
||||||
Progress: 3/6 done | workplan_id: `ebc1d2de-ae11-4cde-b860-047922fc74b9`
|
Progress: 4/6 done | workplan_id: `ebc1d2de-ae11-4cde-b860-047922fc74b9`
|
||||||
|
|
||||||
**Open tasks:**
|
**Open tasks:**
|
||||||
- ! Export a per-client billing basis `b5886131`
|
|
||||||
- ! Select the external invoicing system `d30b606f`
|
- ! Select the external invoicing system `d30b606f`
|
||||||
- · Define shared-infrastructure allocation `899a5c29`
|
- · Export a per-client billing basis `b5886131`
|
||||||
|
|
||||||
---
|
---
|
||||||
## MCP Orientation (when available)
|
## MCP Orientation (when available)
|
||||||
|
|
|
||||||
10
README.md
10
README.md
|
|
@ -24,6 +24,7 @@ uv run finhub runway --balance 12000 --monthly-burn 2100,2200,2000
|
||||||
uv run finhub ledger import cloud tests/fixtures/cloud-costs.csv
|
uv run finhub ledger import cloud tests/fixtures/cloud-costs.csv
|
||||||
uv run finhub ledger set-price --client acme --application portal --instance prod-01 --period 2026-07 --amount 100 --source agreement-2026-01
|
uv run finhub ledger set-price --client acme --application portal --instance prod-01 --period 2026-07 --amount 100 --source agreement-2026-01
|
||||||
uv run finhub ledger margins
|
uv run finhub ledger margins
|
||||||
|
uv run finhub ledger allocations
|
||||||
uv run finhub evaluate
|
uv run finhub evaluate
|
||||||
uv run finhub evidence --seed-fixtures
|
uv run finhub evidence --seed-fixtures
|
||||||
uv run finhub serve
|
uv run finhub serve
|
||||||
|
|
@ -68,6 +69,15 @@ allocations, optimization cases, and commitment candidates outside booked
|
||||||
spend. `ingest_resource_forecast` adapts resource-control's v0.1 monthly backup
|
spend. `ingest_resource_forecast` adapts resource-control's v0.1 monthly backup
|
||||||
forecast without inventing resource identity.
|
forecast without inventing resource identity.
|
||||||
|
|
||||||
|
Shared-infrastructure allocation remains authoritative in resource-control.
|
||||||
|
Fin-hub's `ledger allocations` command consumes current `AllocationEvidence`,
|
||||||
|
requires every referenced financial fact to be current and uniquely claimed,
|
||||||
|
checks period/environment/currency and booked totals, and calculates target and
|
||||||
|
residual amounts in integer minor units. The producer-supplied versioned method
|
||||||
|
and provenance remain visible. Deterministic largest-remainder rounding makes
|
||||||
|
all target amounts plus the explicit unattributed residual reconcile exactly
|
||||||
|
to booked cost.
|
||||||
|
|
||||||
## Related Workplans
|
## Related Workplans
|
||||||
|
|
||||||
- `the-custodian/workplans/CUST-WP-0025-fos-hub-bootstrap.md` — umbrella
|
- `the-custodian/workplans/CUST-WP-0025-fos-hub-bootstrap.md` — umbrella
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,8 @@
|
||||||
| task | FIN-WP-0002-T00 | done | — | workplans/FIN-WP-0002-client-attribution-and-billing-basis.md |
|
| task | FIN-WP-0002-T00 | done | — | workplans/FIN-WP-0002-client-attribution-and-billing-basis.md |
|
||||||
| task | FIN-WP-0002-T01 | done | — | workplans/FIN-WP-0002-client-attribution-and-billing-basis.md |
|
| task | FIN-WP-0002-T01 | done | — | workplans/FIN-WP-0002-client-attribution-and-billing-basis.md |
|
||||||
| task | FIN-WP-0002-T02 | done | — | workplans/FIN-WP-0002-client-attribution-and-billing-basis.md |
|
| task | FIN-WP-0002-T02 | done | — | workplans/FIN-WP-0002-client-attribution-and-billing-basis.md |
|
||||||
| task | FIN-WP-0002-T03 | todo | — | workplans/FIN-WP-0002-client-attribution-and-billing-basis.md |
|
| task | FIN-WP-0002-T03 | done | — | workplans/FIN-WP-0002-client-attribution-and-billing-basis.md |
|
||||||
| task | FIN-WP-0002-T04 | wait | — | workplans/FIN-WP-0002-client-attribution-and-billing-basis.md |
|
| task | FIN-WP-0002-T04 | todo | — | workplans/FIN-WP-0002-client-attribution-and-billing-basis.md |
|
||||||
| task | FIN-WP-0002-T05 | wait | — | workplans/FIN-WP-0002-client-attribution-and-billing-basis.md |
|
| task | FIN-WP-0002-T05 | wait | — | workplans/FIN-WP-0002-client-attribution-and-billing-basis.md |
|
||||||
| task | FIN-WP-0003-T01 | todo | — | workplans/FIN-WP-0003-fabric-authority-boundary.md |
|
| task | FIN-WP-0003-T01 | todo | — | workplans/FIN-WP-0003-fabric-authority-boundary.md |
|
||||||
| task | FIN-WP-0003-T02 | todo | — | workplans/FIN-WP-0003-fabric-authority-boundary.md |
|
| task | FIN-WP-0003-T02 | todo | — | workplans/FIN-WP-0003-fabric-authority-boundary.md |
|
||||||
|
|
@ -36,7 +36,7 @@
|
||||||
| task | FIN-WP-0004-T02 | done | — | workplans/FIN-WP-0004-resource-cost-evidence-contract.md |
|
| task | FIN-WP-0004-T02 | done | — | workplans/FIN-WP-0004-resource-cost-evidence-contract.md |
|
||||||
| task | FIN-WP-0004-T03 | done | — | workplans/FIN-WP-0004-resource-cost-evidence-contract.md |
|
| task | FIN-WP-0004-T03 | done | — | workplans/FIN-WP-0004-resource-cost-evidence-contract.md |
|
||||||
| task | FIN-WP-0004-T04 | todo | — | workplans/FIN-WP-0004-resource-cost-evidence-contract.md |
|
| task | FIN-WP-0004-T04 | todo | — | workplans/FIN-WP-0004-resource-cost-evidence-contract.md |
|
||||||
| task | FIN-WP-0004-T05 | todo | — | workplans/FIN-WP-0004-resource-cost-evidence-contract.md |
|
| task | FIN-WP-0004-T05 | wait | — | workplans/FIN-WP-0004-resource-cost-evidence-contract.md |
|
||||||
| task | FIN-WP-0004-T06 | todo | — | workplans/FIN-WP-0004-resource-cost-evidence-contract.md |
|
| task | FIN-WP-0004-T06 | todo | — | workplans/FIN-WP-0004-resource-cost-evidence-contract.md |
|
||||||
| task | FIN-WP-0004-T07 | done | — | workplans/FIN-WP-0004-resource-cost-evidence-contract.md |
|
| task | FIN-WP-0004-T07 | done | — | workplans/FIN-WP-0004-resource-cost-evidence-contract.md |
|
||||||
| task | FIN-WP-0004-T08 | done | — | workplans/FIN-WP-0004-resource-cost-evidence-contract.md |
|
| task | FIN-WP-0004-T08 | done | — | workplans/FIN-WP-0004-resource-cost-evidence-contract.md |
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Fin-hub ↔ resource-control authority contract v0.1
|
# Fin-hub ↔ resource-control authority contract v0.1
|
||||||
|
|
||||||
Status: authority boundary reviewed; v0.1 implementation acceptance pending
|
Status: v0.1 ratified; operational booked-cost round trip pending
|
||||||
|
|
||||||
Owners: `fin-hub` / `resource-control`
|
Owners: `fin-hub` / `resource-control`
|
||||||
|
|
||||||
|
|
@ -176,8 +176,9 @@ reference the current record of the same type. Fin-hub stores these in
|
||||||
|
|
||||||
## Acceptance status
|
## Acceptance status
|
||||||
|
|
||||||
Resource-control accepted the authority boundary in principle on 2026-08-11.
|
Resource-control ratified the v0.1 authority and schema contract on 2026-08-11
|
||||||
The review is preserved at
|
after cross-repository preflight matched and idempotently replayed all 12
|
||||||
|
current backup forecast records. The review is preserved at
|
||||||
`history/2026-08-11-resource-control-review-fin-wp-0004.md`. Contract v0.1 is
|
`history/2026-08-11-resource-control-review-fin-wp-0004.md`. Contract v0.1 is
|
||||||
not accepted until the implementation-integrity tasks and reconciled backup
|
implemented; operational acceptance remains gated by the first real provider
|
||||||
round trip pass.
|
fact and reconciled backup round trip in `FIN-WP-0004-T05`.
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ from fin_hub.ingest.anthropic import parse_anthropic_billing_csv
|
||||||
from fin_hub.ingest.cloud import parse_cloud_cost_csv
|
from fin_hub.ingest.cloud import parse_cloud_cost_csv
|
||||||
from fin_hub.ingest.hosteurope import parse_hosteurope_csv
|
from fin_hub.ingest.hosteurope import parse_hosteurope_csv
|
||||||
from fin_hub.services.alerts import evaluate_budget_alerts
|
from fin_hub.services.alerts import evaluate_budget_alerts
|
||||||
|
from fin_hub.services.allocation import shared_cost_allocations
|
||||||
from fin_hub.services.evaluate import evaluate_runway
|
from fin_hub.services.evaluate import evaluate_runway
|
||||||
from fin_hub.services.evidence import write_runway_evidence
|
from fin_hub.services.evidence import write_runway_evidence
|
||||||
from fin_hub.services.ledger import (
|
from fin_hub.services.ledger import (
|
||||||
|
|
@ -149,6 +150,12 @@ def _cmd_ledger_margins(args: argparse.Namespace) -> int:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def _cmd_ledger_allocations(args: argparse.Namespace) -> int:
|
||||||
|
reports = shared_cost_allocations(ledger_path=_ledger_path(args))
|
||||||
|
print(json.dumps([report.as_dict() for report in reports], indent=2, default=str))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def _cmd_evaluate(args: argparse.Namespace) -> int:
|
def _cmd_evaluate(args: argparse.Namespace) -> int:
|
||||||
report = evaluate_runway(
|
report = evaluate_runway(
|
||||||
ledger_path=_ledger_path(args),
|
ledger_path=_ledger_path(args),
|
||||||
|
|
@ -261,6 +268,13 @@ def build_parser() -> argparse.ArgumentParser:
|
||||||
ledger_margins.add_argument("--ledger", help="Ledger database path")
|
ledger_margins.add_argument("--ledger", help="Ledger database path")
|
||||||
ledger_margins.set_defaults(func=_cmd_ledger_margins)
|
ledger_margins.set_defaults(func=_cmd_ledger_margins)
|
||||||
|
|
||||||
|
ledger_allocations = ledger_sub.add_parser(
|
||||||
|
"allocations",
|
||||||
|
help="Reconcile resource-control allocation evidence to booked facts",
|
||||||
|
)
|
||||||
|
ledger_allocations.add_argument("--ledger", help="Ledger database path")
|
||||||
|
ledger_allocations.set_defaults(func=_cmd_ledger_allocations)
|
||||||
|
|
||||||
evaluate = sub.add_parser(
|
evaluate = sub.add_parser(
|
||||||
"evaluate",
|
"evaluate",
|
||||||
help="Evaluate runway from ledger burns (cron/systemd friendly)",
|
help="Evaluate runway from ledger burns (cron/systemd friendly)",
|
||||||
|
|
|
||||||
162
src/fin_hub/services/allocation.py
Normal file
162
src/fin_hub/services/allocation.py
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
"""Consume authoritative technical allocation evidence for financial reporting."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from dataclasses import asdict, dataclass
|
||||||
|
from datetime import date
|
||||||
|
from decimal import Decimal, ROUND_FLOOR
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from fin_hub.money import MONEY_QUANTUM, minor_money, money_minor
|
||||||
|
from fin_hub.schemas.exchange import AllocationEvidence
|
||||||
|
from fin_hub.services.exchange import _ensure_planning_schema
|
||||||
|
from fin_hub.services.ledger import _connect, default_ledger_path
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class AllocatedTarget:
|
||||||
|
target_key: str
|
||||||
|
share: Decimal
|
||||||
|
amount: Decimal
|
||||||
|
rounding_adjustment: Decimal
|
||||||
|
|
||||||
|
def as_dict(self) -> dict:
|
||||||
|
return asdict(self)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class AllocationReport:
|
||||||
|
allocation_id: str
|
||||||
|
method: str
|
||||||
|
financial_fact_ids: tuple[str, ...]
|
||||||
|
period_start: date
|
||||||
|
period_end: date
|
||||||
|
environment: str | None
|
||||||
|
currency: str
|
||||||
|
booked_amount: Decimal
|
||||||
|
targets: tuple[AllocatedTarget, ...]
|
||||||
|
residual_share: Decimal
|
||||||
|
residual_amount: Decimal
|
||||||
|
residual_rounding_adjustment: Decimal
|
||||||
|
source_evidence: tuple[str, ...]
|
||||||
|
|
||||||
|
def as_dict(self) -> dict:
|
||||||
|
return asdict(self)
|
||||||
|
|
||||||
|
|
||||||
|
def _split_minor_units(
|
||||||
|
total_minor: int,
|
||||||
|
shares: list[tuple[str, Decimal]],
|
||||||
|
) -> dict[str, tuple[int, Decimal]]:
|
||||||
|
"""Allocate exact minor units with deterministic largest-remainder rounding."""
|
||||||
|
|
||||||
|
floors: dict[str, int] = {}
|
||||||
|
fractions: list[tuple[Decimal, str]] = []
|
||||||
|
for key, share in shares:
|
||||||
|
exact = Decimal(total_minor) * share
|
||||||
|
floor = int(exact.to_integral_value(rounding=ROUND_FLOOR))
|
||||||
|
floors[key] = floor
|
||||||
|
fractions.append((exact - Decimal(floor), key))
|
||||||
|
remainder = total_minor - sum(floors.values())
|
||||||
|
for _fraction, key in sorted(fractions, key=lambda item: (-item[0], item[1]))[:remainder]:
|
||||||
|
floors[key] += 1
|
||||||
|
return {
|
||||||
|
key: (
|
||||||
|
floors[key],
|
||||||
|
Decimal(floors[key]) - (Decimal(total_minor) * share),
|
||||||
|
)
|
||||||
|
for key, share in shares
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def shared_cost_allocations(*, ledger_path: Path | None = None) -> list[AllocationReport]:
|
||||||
|
"""Reconcile current allocation evidence to current booked facts."""
|
||||||
|
|
||||||
|
ledger = ledger_path or default_ledger_path()
|
||||||
|
with _connect(ledger) as conn:
|
||||||
|
_ensure_planning_schema(conn)
|
||||||
|
allocation_rows = conn.execute(
|
||||||
|
"SELECT payload_json FROM planning_evidence "
|
||||||
|
"WHERE record_type = 'allocation' AND is_current = 1 ORDER BY record_id"
|
||||||
|
).fetchall()
|
||||||
|
claimed_facts: dict[str, str] = {}
|
||||||
|
reports: list[AllocationReport] = []
|
||||||
|
for allocation_row in allocation_rows:
|
||||||
|
allocation = AllocationEvidence.model_validate_json(
|
||||||
|
allocation_row["payload_json"]
|
||||||
|
)
|
||||||
|
duplicate_ids = {
|
||||||
|
fact_id
|
||||||
|
for fact_id in allocation.financial_fact_ids
|
||||||
|
if allocation.financial_fact_ids.count(fact_id) > 1
|
||||||
|
}
|
||||||
|
if duplicate_ids:
|
||||||
|
raise ValueError(f"allocation repeats financial facts: {sorted(duplicate_ids)}")
|
||||||
|
fact_rows = []
|
||||||
|
for fact_id in allocation.financial_fact_ids:
|
||||||
|
owner = claimed_facts.get(fact_id)
|
||||||
|
if owner is not None:
|
||||||
|
raise ValueError(
|
||||||
|
f"financial fact {fact_id} is allocated by both {owner} "
|
||||||
|
f"and {allocation.record_id}"
|
||||||
|
)
|
||||||
|
fact = conn.execute(
|
||||||
|
"SELECT * FROM ledger_entries "
|
||||||
|
"WHERE financial_fact_id = ? AND is_current = 1",
|
||||||
|
(fact_id,),
|
||||||
|
).fetchone()
|
||||||
|
if fact is None:
|
||||||
|
raise ValueError(f"allocation references missing/current fact {fact_id}")
|
||||||
|
if fact["currency"] != allocation.currency:
|
||||||
|
raise ValueError(f"allocation currency does not match fact {fact_id}")
|
||||||
|
if allocation.environment and fact["environment"] != allocation.environment:
|
||||||
|
raise ValueError(f"allocation environment does not match fact {fact_id}")
|
||||||
|
fact_period = date.fromisoformat(f"{fact['period_month']}-01")
|
||||||
|
if not (allocation.period_start <= fact_period <= allocation.period_end):
|
||||||
|
raise ValueError(f"allocation period does not include fact {fact_id}")
|
||||||
|
claimed_facts[fact_id] = allocation.record_id
|
||||||
|
fact_rows.append(fact)
|
||||||
|
|
||||||
|
total_minor = sum(int(row["amount_minor"]) for row in fact_rows)
|
||||||
|
if total_minor != money_minor(allocation.allocated_amount):
|
||||||
|
raise ValueError(
|
||||||
|
f"allocation {allocation.record_id} amount does not reconcile "
|
||||||
|
"to its current booked facts"
|
||||||
|
)
|
||||||
|
shares = [(share.target_key, share.share) for share in allocation.shares]
|
||||||
|
if len({key for key, _share in shares}) != len(shares):
|
||||||
|
raise ValueError("allocation target keys must be unique")
|
||||||
|
residual_key = "__residual__"
|
||||||
|
split = _split_minor_units(
|
||||||
|
total_minor,
|
||||||
|
[*shares, (residual_key, allocation.residual_share)],
|
||||||
|
)
|
||||||
|
targets = tuple(
|
||||||
|
AllocatedTarget(
|
||||||
|
target_key=key,
|
||||||
|
share=share,
|
||||||
|
amount=minor_money(split[key][0]),
|
||||||
|
rounding_adjustment=(split[key][1] * MONEY_QUANTUM),
|
||||||
|
)
|
||||||
|
for key, share in shares
|
||||||
|
)
|
||||||
|
residual_minor, residual_adjustment = split[residual_key]
|
||||||
|
reports.append(
|
||||||
|
AllocationReport(
|
||||||
|
allocation_id=allocation.record_id,
|
||||||
|
method=allocation.method,
|
||||||
|
financial_fact_ids=tuple(allocation.financial_fact_ids),
|
||||||
|
period_start=allocation.period_start,
|
||||||
|
period_end=allocation.period_end,
|
||||||
|
environment=allocation.environment,
|
||||||
|
currency=allocation.currency,
|
||||||
|
booked_amount=minor_money(total_minor),
|
||||||
|
targets=targets,
|
||||||
|
residual_share=allocation.residual_share,
|
||||||
|
residual_amount=minor_money(residual_minor),
|
||||||
|
residual_rounding_adjustment=residual_adjustment * MONEY_QUANTUM,
|
||||||
|
source_evidence=tuple(allocation.source_evidence),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return reports
|
||||||
129
tests/test_allocation.py
Normal file
129
tests/test_allocation.py
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
import sqlite3
|
||||||
|
from decimal import Decimal
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from fin_hub.services.allocation import shared_cost_allocations
|
||||||
|
from fin_hub.services.exchange import ingest_planning_evidence
|
||||||
|
from fin_hub.services.ledger import import_csv
|
||||||
|
|
||||||
|
|
||||||
|
def _booked_fact(tmp_path: Path, *, amount: str = "100.01", currency: str = "EUR"):
|
||||||
|
ledger = tmp_path / "ledger.db"
|
||||||
|
source = tmp_path / "shared.csv"
|
||||||
|
source.write_text(
|
||||||
|
"product,amount,currency,invoice_date,environment\n"
|
||||||
|
f"Shared cluster,{amount},{currency},2026-07-01,production\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
import_csv(source, "hosteurope", ledger_path=ledger)
|
||||||
|
with sqlite3.connect(ledger) as conn:
|
||||||
|
fact_id = conn.execute(
|
||||||
|
"SELECT financial_fact_id FROM ledger_entries WHERE is_current = 1"
|
||||||
|
).fetchone()[0]
|
||||||
|
return ledger, fact_id
|
||||||
|
|
||||||
|
|
||||||
|
def _allocation(fact_id: str, **overrides) -> dict:
|
||||||
|
payload = {
|
||||||
|
"schema_version": "0.1",
|
||||||
|
"record_type": "allocation",
|
||||||
|
"record_id": "allocation:shared-cluster: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:allocation:namespace-cpu-v1"],
|
||||||
|
"created_at": "2026-08-11T09:00:00Z",
|
||||||
|
"financial_fact_ids": [fact_id],
|
||||||
|
"method": "namespace-cpu-v1",
|
||||||
|
"allocated_amount": "100.01",
|
||||||
|
"shares": [
|
||||||
|
{"target_key": "client:acme|app:portal|instance:prod-01", "share": "0.3333"},
|
||||||
|
{"target_key": "client:beta|app:portal|instance:prod-01", "share": "0.3333"},
|
||||||
|
],
|
||||||
|
"residual_share": "0.3334",
|
||||||
|
}
|
||||||
|
payload.update(overrides)
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
def test_shared_allocation_reconciles_targets_rounding_and_residual(tmp_path: Path):
|
||||||
|
ledger, fact_id = _booked_fact(tmp_path)
|
||||||
|
ingest_planning_evidence(_allocation(fact_id), ledger_path=ledger)
|
||||||
|
|
||||||
|
report = shared_cost_allocations(ledger_path=ledger)[0]
|
||||||
|
|
||||||
|
assert report.booked_amount == Decimal("100.01")
|
||||||
|
assert sum((target.amount for target in report.targets), report.residual_amount) == Decimal(
|
||||||
|
"100.01"
|
||||||
|
)
|
||||||
|
assert report.residual_amount > Decimal("0")
|
||||||
|
assert report.method == "namespace-cpu-v1"
|
||||||
|
assert report.source_evidence
|
||||||
|
|
||||||
|
|
||||||
|
def test_revised_allocation_supersedes_prior_shares(tmp_path: Path):
|
||||||
|
ledger, fact_id = _booked_fact(tmp_path)
|
||||||
|
original = _allocation(fact_id)
|
||||||
|
ingest_planning_evidence(original, ledger_path=ledger)
|
||||||
|
revised = _allocation(
|
||||||
|
fact_id,
|
||||||
|
record_id="allocation:shared-cluster:2026-07:v2",
|
||||||
|
revision_of=original["record_id"],
|
||||||
|
shares=[
|
||||||
|
{"target_key": "client:acme|app:portal|instance:prod-01", "share": "0.75"}
|
||||||
|
],
|
||||||
|
residual_share="0.25",
|
||||||
|
)
|
||||||
|
ingest_planning_evidence(revised, ledger_path=ledger)
|
||||||
|
|
||||||
|
reports = shared_cost_allocations(ledger_path=ledger)
|
||||||
|
|
||||||
|
assert len(reports) == 1
|
||||||
|
assert reports[0].allocation_id == revised["record_id"]
|
||||||
|
assert reports[0].targets[0].share == Decimal("0.75")
|
||||||
|
|
||||||
|
|
||||||
|
def test_allocation_rejects_missing_fact(tmp_path: Path):
|
||||||
|
ledger = tmp_path / "ledger.db"
|
||||||
|
ingest_planning_evidence(
|
||||||
|
_allocation("fact:missing"),
|
||||||
|
ledger_path=ledger,
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="missing/current fact"):
|
||||||
|
shared_cost_allocations(ledger_path=ledger)
|
||||||
|
|
||||||
|
|
||||||
|
def test_allocation_rejects_currency_or_amount_mismatch(tmp_path: Path):
|
||||||
|
ledger, fact_id = _booked_fact(tmp_path)
|
||||||
|
ingest_planning_evidence(
|
||||||
|
_allocation(fact_id, currency="USD"),
|
||||||
|
ledger_path=ledger,
|
||||||
|
)
|
||||||
|
with pytest.raises(ValueError, match="currency"):
|
||||||
|
shared_cost_allocations(ledger_path=ledger)
|
||||||
|
|
||||||
|
|
||||||
|
def test_fact_cannot_be_allocated_twice(tmp_path: Path):
|
||||||
|
ledger, fact_id = _booked_fact(tmp_path)
|
||||||
|
first = _allocation(fact_id)
|
||||||
|
second = _allocation(
|
||||||
|
fact_id,
|
||||||
|
record_id="allocation:duplicate",
|
||||||
|
shares=[{"target_key": "client:acme", "share": "1"}],
|
||||||
|
residual_share="0",
|
||||||
|
)
|
||||||
|
ingest_planning_evidence(first, ledger_path=ledger)
|
||||||
|
ingest_planning_evidence(second, ledger_path=ledger)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="allocated by both"):
|
||||||
|
shared_cost_allocations(ledger_path=ledger)
|
||||||
|
|
@ -116,7 +116,7 @@ prices remain auditable, and duplicate prices require an explicit revision.
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: FIN-WP-0002-T03
|
id: FIN-WP-0002-T03
|
||||||
status: todo
|
status: done
|
||||||
priority: medium
|
priority: medium
|
||||||
state_hub_task_id: "899a5c29-843c-481f-a63a-42d4d327af5d"
|
state_hub_task_id: "899a5c29-843c-481f-a63a-42d4d327af5d"
|
||||||
```
|
```
|
||||||
|
|
@ -130,11 +130,19 @@ where resource-control supplies it.
|
||||||
Test allocation totals, rounding, missing evidence, shared overhead, revised
|
Test allocation totals, rounding, missing evidence, shared overhead, revised
|
||||||
allocations, and prevention of duplicate booked cost.
|
allocations, and prevention of duplicate booked cost.
|
||||||
|
|
||||||
|
Completed 2026-08-11: added a reporting-only allocation consumer for current
|
||||||
|
resource-control `AllocationEvidence`. It requires uniquely claimed current
|
||||||
|
financial facts, validates period/environment/currency and producer totals,
|
||||||
|
retains the versioned method and provenance, and uses deterministic
|
||||||
|
largest-remainder minor-unit rounding. Per-instance targets plus the explicit
|
||||||
|
unattributed residual reconcile exactly to booked cost. Tests cover rounding,
|
||||||
|
missing facts, mismatches, evidence revisions, and duplicate allocation.
|
||||||
|
|
||||||
## Export a per-client billing basis
|
## Export a per-client billing basis
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: FIN-WP-0002-T04
|
id: FIN-WP-0002-T04
|
||||||
status: wait
|
status: todo
|
||||||
priority: medium
|
priority: medium
|
||||||
state_hub_task_id: "b5886131-82f6-49fb-a7e3-ba47d5b627a8"
|
state_hub_task_id: "b5886131-82f6-49fb-a7e3-ba47d5b627a8"
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -162,7 +162,7 @@ without reproducing fin-hub's allocator or runway logic.
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: FIN-WP-0004-T05
|
id: FIN-WP-0004-T05
|
||||||
status: todo
|
status: wait
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "9a411734-76f6-4c8d-9a48-b5a08cf8fd1f"
|
state_hub_task_id: "9a411734-76f6-4c8d-9a48-b5a08cf8fd1f"
|
||||||
```
|
```
|
||||||
|
|
@ -179,6 +179,16 @@ Done when the same cost is booked exactly once, projected to resource-control,
|
||||||
joined to technical evidence, and returned as planning or variance evidence
|
joined to technical evidence, and returned as planning or variance evidence
|
||||||
without becoming a second financial fact.
|
without becoming a second financial fact.
|
||||||
|
|
||||||
|
Preflight 2026-08-11: resource-control verified fin-hub commit `0034330`, and
|
||||||
|
both implementations produced identical canonical payloads for all 12 live
|
||||||
|
backup forecast rows. Replaying them retained exactly 12 planning-store rows.
|
||||||
|
Evidence is in resource-control at
|
||||||
|
`docs/evidence/RESOURCE-WP-0003-fin-hub-contract-preflight-2026-08-11.md`.
|
||||||
|
|
||||||
|
The task now waits on `RESOURCE-WP-0002` procurement and the first real
|
||||||
|
provider fact attributable to `platform:audit-storage`. Synthetic records test
|
||||||
|
the contract but do not satisfy the operational booked-cost round trip.
|
||||||
|
|
||||||
## T06 — Generalize and operate the contract
|
## T06 — Generalize and operate the contract
|
||||||
|
|
||||||
```task
|
```task
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue