Harden resource cost evidence contract

This commit is contained in:
tegwick 2026-08-11 10:25:03 +02:00
parent 080f756fff
commit 00343307fd
22 changed files with 1623 additions and 130 deletions

View file

@ -5,29 +5,77 @@ from __future__ import annotations
import uuid
from datetime import date
from sqlalchemy import Date, Float, ForeignKey, String
from decimal import Decimal
from sqlalchemy import Boolean, CheckConstraint, Date, ForeignKey, Index, Numeric, String, event, text
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column
from hub_core.models.base import Base, TimestampMixin
from fin_hub.money import EngagementPriceTerms
class EngagementPrice(Base, TimestampMixin):
"""A reporting entitlement price, not an invoice or payment record."""
__tablename__ = "fin_engagement_prices"
__table_args__ = (
CheckConstraint("amount >= 0", name="ck_fin_engagement_prices_nonnegative"),
CheckConstraint("length(currency) = 3", name="ck_fin_engagement_prices_currency"),
CheckConstraint(
"effective_to IS NULL OR effective_to >= effective_from",
name="ck_fin_engagement_prices_effective_dates",
),
Index(
"uq_fin_engagement_prices_current_basis",
"cost_attribution_key",
"period_month",
"currency",
unique=True,
postgresql_where=text("is_current"),
sqlite_where=text("is_current = 1"),
),
)
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
client_id: Mapped[str] = mapped_column(String(128), nullable=False, index=True)
application_id: Mapped[str] = mapped_column(String(128), nullable=False, index=True)
app_instance_id: Mapped[str] = mapped_column(String(128), nullable=False, index=True)
cost_attribution_key: Mapped[str] = mapped_column(String(423), nullable=False, index=True)
period_month: Mapped[str] = mapped_column(String(7), nullable=False, index=True)
effective_from: Mapped[date] = mapped_column(Date, nullable=False, index=True)
effective_to: Mapped[date | None] = mapped_column(Date, nullable=True)
amount: Mapped[float] = mapped_column(Float, nullable=False)
amount: Mapped[Decimal] = mapped_column(Numeric(18, 2), nullable=False)
currency: Mapped[str] = mapped_column(String(3), nullable=False, default="EUR")
source: Mapped[str] = mapped_column(String(128), nullable=False)
revision_of: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("fin_engagement_prices.id"), nullable=True
)
is_current: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
notes: Mapped[str | None] = mapped_column(String(512), nullable=True)
@event.listens_for(EngagementPrice, "before_insert")
@event.listens_for(EngagementPrice, "before_update")
def _validate_engagement_price(_mapper, _connection, target: EngagementPrice) -> None:
if target.effective_to is not None and target.effective_to < target.effective_from:
raise ValueError("effective_to cannot precede effective_from")
terms = EngagementPriceTerms.validate(
client_id=target.client_id,
application_id=target.application_id,
app_instance_id=target.app_instance_id,
period_month=target.effective_from.strftime("%Y-%m"),
amount=target.amount,
currency=target.currency,
source=target.source,
)
target.client_id = terms.attribution.client_id
target.application_id = terms.attribution.application_id
target.app_instance_id = terms.attribution.app_instance_id
if target.cost_attribution_key not in (None, terms.attribution.key):
raise ValueError("cost_attribution_key does not match its attribution dimensions")
target.cost_attribution_key = terms.attribution.key
target.period_month = terms.period_month
target.amount = terms.amount
target.currency = terms.currency
target.source = terms.source

View file

@ -4,13 +4,15 @@ from __future__ import annotations
import uuid
from datetime import date
from decimal import Decimal
from sqlalchemy import CheckConstraint, Date, Float, String, event
from sqlalchemy import CheckConstraint, Date, Numeric, String, event
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column
from hub_core.models.base import Base, TimestampMixin
from fin_hub.attribution import optional_attribution
from fin_hub.money import currency_code, money, reporting_month
class ServiceCost(Base, TimestampMixin):
@ -29,7 +31,7 @@ class ServiceCost(Base, TimestampMixin):
service_id: Mapped[str] = mapped_column(String(128), nullable=False, index=True)
environment: Mapped[str] = mapped_column(String(64), nullable=False, default="production")
period_month: Mapped[str] = mapped_column(String(7), nullable=False, index=True)
amount: Mapped[float] = mapped_column(Float, nullable=False, default=0.0)
amount: Mapped[Decimal] = mapped_column(Numeric(18, 2), nullable=False, default=Decimal("0"))
currency: Mapped[str] = mapped_column(String(3), nullable=False, default="EUR")
source: Mapped[str] = mapped_column(String(32), nullable=False)
incurred_on: Mapped[date | None] = mapped_column(Date, nullable=True)
@ -50,3 +52,9 @@ def _normalize_service_cost_attribution(_mapper, _connection, target: ServiceCos
if target.cost_attribution_key not in (None, expected_key):
raise ValueError("cost_attribution_key does not match its attribution dimensions")
target.cost_attribution_key = expected_key
target.period_month = reporting_month(target.period_month)
target.amount = money(target.amount)
target.currency = currency_code(target.currency)
target.source = target.source.strip()
if not target.source:
raise ValueError("source is required")