Harden resource cost evidence contract
This commit is contained in:
parent
080f756fff
commit
00343307fd
22 changed files with 1623 additions and 130 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue