Add temporal validity and lifecycle tracking to categories

This commit is contained in:
2026-10-27 18:39:53 +01:00
parent 4efef98c9c
commit 048a6c6f38
4 changed files with 373 additions and 9 deletions
+73 -3
View File
@@ -1,8 +1,17 @@
from datetime import date
from datetime import UTC, date, datetime
from decimal import Decimal
from pydantic import Field, computed_field, field_validator
from sqlalchemy import Date, ForeignKey, Integer, String, UniqueConstraint
from sqlalchemy import (
Date,
DateTime,
ForeignKey,
Index,
Integer,
String,
TypeDecorator,
text,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
from src.constants import IBAN_PATTERN, Currency
@@ -118,10 +127,48 @@ class Category(ExportBaseModel):
return f'Category.from_str("{self.__str__()}")'
class UTCDateTime(TypeDecorator[datetime]):
"""Store UTC timestamps and return timezone-aware UTC values on every dialect."""
impl = DateTime
cache_ok = True
def load_dialect_impl(self, dialect):
return dialect.type_descriptor(DateTime(timezone=True))
def process_bind_param(self, value: datetime | None, dialect):
if value is None:
return None
if value.tzinfo is None or value.utcoffset() is None:
return value.replace(tzinfo=UTC)
return value.astimezone(UTC)
def process_result_value(self, value: datetime | None, dialect):
if value is None:
return None
if value.tzinfo is None or value.utcoffset() is None:
return value.replace(tzinfo=UTC)
return value.astimezone(UTC)
class CategoryDb(BaseModelDb):
__tablename__ = "category"
__table_args__ = (
UniqueConstraint("name", "parent_id", name="uq_category_name_parent"),
Index(
"uq_category_active_name_parent",
"name",
"parent_id",
unique=True,
sqlite_where=text("valid_to IS NULL AND parent_id IS NOT NULL"),
postgresql_where=text("valid_to IS NULL AND parent_id IS NOT NULL"),
),
Index(
"uq_category_active_root_name",
"name",
unique=True,
sqlite_where=text("valid_to IS NULL AND parent_id IS NULL"),
postgresql_where=text("valid_to IS NULL AND parent_id IS NULL"),
),
)
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
@@ -129,6 +176,29 @@ class CategoryDb(BaseModelDb):
parent_id: Mapped[int | None] = mapped_column(
ForeignKey("category.id"), nullable=True
)
valid_from: Mapped[datetime | None] = mapped_column(
UTCDateTime(),
nullable=True,
info={
"description": "UTC start of validity; NULL means valid since the indefinite past."
},
)
valid_to: Mapped[datetime | None] = mapped_column(
UTCDateTime(),
nullable=True,
info={
"description": "UTC end of validity; NULL means the version is currently valid."
},
)
last_action: Mapped[str] = mapped_column(
String,
nullable=False,
default="created",
server_default="created",
info={
"description": "Most recent lifecycle action recorded for this category version."
},
)
# Relationships
parent = relationship(