Files
X-Financial/server/src/app/models/expense_case.py

120 lines
5.3 KiB
Python
Raw Normal View History

from __future__ import annotations
import uuid
from datetime import datetime
from typing import Any
from sqlalchemy import DateTime, ForeignKey, Index, Integer, String, Text, UniqueConstraint, func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.types import JSON
from app.db.base_class import Base
def _new_id() -> str:
return str(uuid.uuid4())
class ExpenseCase(Base):
__tablename__ = "expense_cases"
__table_args__ = (
UniqueConstraint("tenant_id", "id", name="uq_expense_cases_tenant_id"),
UniqueConstraint("tenant_id", "case_no", name="uq_expense_cases_tenant_case_no"),
Index("ix_expense_cases_tenant_stage", "tenant_id", "current_stage"),
Index("ix_expense_cases_tenant_status", "tenant_id", "status"),
)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=_new_id)
tenant_id: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
case_no: Mapped[str] = mapped_column(String(80), nullable=False)
scene_code: Mapped[str] = mapped_column(String(50), nullable=False, default="other")
title: Mapped[str] = mapped_column(String(200), nullable=False)
owner_employee_id: Mapped[str | None] = mapped_column(String(36), nullable=True, index=True)
current_stage: Mapped[str] = mapped_column(String(40), nullable=False, default="claiming")
status: Mapped[str] = mapped_column(String(30), nullable=False, default="active")
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
links = relationship(
"ExpenseCaseLink",
back_populates="expense_case",
cascade="all, delete-orphan",
order_by="asc(ExpenseCaseLink.created_at)",
)
events = relationship(
"BusinessEvent",
back_populates="expense_case",
cascade="all, delete-orphan",
order_by="asc(BusinessEvent.occurred_at)",
)
class ExpenseCaseLink(Base):
__tablename__ = "expense_case_links"
__table_args__ = (
UniqueConstraint("resource_type", "resource_id", name="uq_expense_case_links_resource"),
Index("ix_expense_case_links_tenant_case", "tenant_id", "expense_case_id"),
)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=_new_id)
tenant_id: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
expense_case_id: Mapped[str] = mapped_column(
ForeignKey("expense_cases.id", ondelete="CASCADE"), nullable=False
)
resource_type: Mapped[str] = mapped_column(String(50), nullable=False)
resource_id: Mapped[str] = mapped_column(String(100), nullable=False)
relation_type: Mapped[str] = mapped_column(String(50), nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
expense_case = relationship("ExpenseCase", back_populates="links")
class BusinessEvent(Base):
__tablename__ = "business_events"
__table_args__ = (
UniqueConstraint(
"tenant_id",
"expense_case_id",
"id",
name="uq_business_events_tenant_case_id",
),
UniqueConstraint(
"tenant_id",
"aggregate_type",
"aggregate_id",
"event_type",
"idempotency_key",
name="uq_business_event_idempotency",
),
Index("ix_business_events_tenant_case_time", "tenant_id", "expense_case_id", "occurred_at"),
Index("ix_business_events_outbox", "delivery_status", "occurred_at"),
Index("ix_business_events_aggregate", "aggregate_type", "aggregate_id"),
)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=_new_id)
tenant_id: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
expense_case_id: Mapped[str] = mapped_column(
ForeignKey("expense_cases.id", ondelete="CASCADE"), nullable=False
)
aggregate_type: Mapped[str] = mapped_column(String(50), nullable=False)
aggregate_id: Mapped[str] = mapped_column(String(100), nullable=False)
event_type: Mapped[str] = mapped_column(String(80), nullable=False, index=True)
event_version: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
idempotency_key: Mapped[str] = mapped_column(String(120), nullable=False)
correlation_id: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
causation_id: Mapped[str | None] = mapped_column(String(64), nullable=True)
actor_id: Mapped[str] = mapped_column(String(120), nullable=False)
actor_type: Mapped[str] = mapped_column(String(30), nullable=False, default="user")
payload_json: Mapped[dict[str, Any]] = mapped_column(JSON, nullable=False, default=dict)
delivery_status: Mapped[str] = mapped_column(String(20), nullable=False, default="pending")
delivery_attempts: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
last_delivery_error: Mapped[str | None] = mapped_column(Text(), nullable=True)
occurred_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now()
)
published_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
expense_case = relationship("ExpenseCase", back_populates="events")