Implement client cost attribution

This commit is contained in:
tegwick 2026-08-10 20:32:09 +02:00
parent d2b9bc4b32
commit 33883e0977
15 changed files with 362 additions and 21 deletions

View file

@ -5,15 +5,25 @@ from __future__ import annotations
import uuid
from datetime import date
from sqlalchemy import Date, Float, String
from sqlalchemy import CheckConstraint, Date, Float, 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
class ServiceCost(Base, TimestampMixin):
__tablename__ = "fin_service_costs"
__table_args__ = (
CheckConstraint(
"(client_id IS NULL AND application_id IS NULL AND app_instance_id IS NULL "
"AND cost_attribution_key IS NULL) OR "
"(client_id IS NOT NULL AND application_id IS NOT NULL "
"AND app_instance_id IS NOT NULL AND cost_attribution_key IS NOT NULL)",
name="ck_fin_service_costs_complete_attribution",
),
)
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
service_id: Mapped[str] = mapped_column(String(128), nullable=False, index=True)
@ -23,4 +33,20 @@ class ServiceCost(Base, TimestampMixin):
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)
notes: Mapped[str | None] = mapped_column(String(512), nullable=True)
notes: Mapped[str | None] = mapped_column(String(512), nullable=True)
client_id: Mapped[str | None] = mapped_column(String(128), nullable=True, index=True)
application_id: Mapped[str | None] = mapped_column(String(128), nullable=True, index=True)
app_instance_id: Mapped[str | None] = mapped_column(String(128), nullable=True, index=True)
cost_attribution_key: Mapped[str | None] = mapped_column(String(423), nullable=True, index=True)
@event.listens_for(ServiceCost, "before_insert")
@event.listens_for(ServiceCost, "before_update")
def _normalize_service_cost_attribution(_mapper, _connection, target: ServiceCost) -> None:
attribution = optional_attribution(
target.client_id, target.application_id, target.app_instance_id
)
expected_key = attribution.key if attribution else None
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