Add temporal validity and lifecycle tracking to categories
This commit is contained in:
+73
-3
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user