"""History persistence for the 年假计算器. Each invocation of the calculator writes one row capturing the full computation graph: inputs, per-segment breakdown (formula + raw + rounded), pre-cap totals, post-cap final values, used / carryover balances, and the resulting unused balances. Used both for audit defensibility (HR responds to employee questions with the exact numbers they computed) and for spot-checking rule-engine regressions over time. """ from __future__ import annotations import uuid from datetime import date as date_type from datetime import datetime from decimal import Decimal from sqlalchemy import CHAR, JSON, Boolean, Date, DateTime, Numeric, String, Text from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.sql import func from app.core.database import Base class HrToolsAnnualLeaveCalculation(Base): __tablename__ = "hr_tools_annual_leave_calculations" id: Mapped[str] = mapped_column( CHAR(36), primary_key=True, default=lambda: str(uuid.uuid4()), ) # Subject of calculation person_id: Mapped[str | None] = mapped_column(CHAR(36), index=True) employee_no: Mapped[str | None] = mapped_column(String(64)) employee_name: Mapped[str | None] = mapped_column(String(128)) # Inputs work_start_date: Mapped[date_type] = mapped_column(Date, nullable=False) hire_date: Mapped[date_type] = mapped_column(Date, nullable=False) termination_date: Mapped[date_type] = mapped_column(Date, nullable=False) fiscal_year_start: Mapped[date_type] = mapped_column(Date, nullable=False) fiscal_year_end: Mapped[date_type] = mapped_column(Date, nullable=False) carry_legal_days: Mapped[Decimal] = mapped_column(Numeric(4, 1), nullable=False) carry_welfare_days: Mapped[Decimal] = mapped_column(Numeric(4, 1), nullable=False) used_legal_days: Mapped[Decimal] = mapped_column(Numeric(4, 1), nullable=False) used_welfare_days: Mapped[Decimal] = mapped_column(Numeric(4, 1), nullable=False) # Full-year entitlement (assumes employed all 365 days). One JSON # column per block — each segment carries both legal_basis and # welfare_basis, so a separate legal/welfare segment list would be # the same data twice. raw uses Numeric(8,4) so a future rule that # raises max combined entitlement past 99.99 days doesn't silently # overflow. full_year_segments: Mapped[list] = mapped_column(JSON, nullable=False) full_year_legal_raw: Mapped[Decimal] = mapped_column(Numeric(8, 4), nullable=False) full_year_welfare_raw: Mapped[Decimal] = mapped_column(Numeric(8, 4), nullable=False) full_year_legal_rounded: Mapped[Decimal] = mapped_column(Numeric(4, 1), nullable=False) full_year_welfare_rounded: Mapped[Decimal] = mapped_column(Numeric(4, 1), nullable=False) full_year_legal_capped: Mapped[Decimal] = mapped_column(Numeric(4, 1), nullable=False) full_year_welfare_capped: Mapped[Decimal] = mapped_column(Numeric(4, 1), nullable=False) # Prorated by actual employed days (settlement) settlement_segments: Mapped[list] = mapped_column(JSON, nullable=False) settlement_legal_raw: Mapped[Decimal] = mapped_column(Numeric(8, 4), nullable=False) settlement_welfare_raw: Mapped[Decimal] = mapped_column(Numeric(8, 4), nullable=False) settlement_legal_rounded: Mapped[Decimal] = mapped_column(Numeric(4, 1), nullable=False) settlement_welfare_rounded: Mapped[Decimal] = mapped_column(Numeric(4, 1), nullable=False) settlement_legal_capped: Mapped[Decimal] = mapped_column(Numeric(4, 1), nullable=False) settlement_welfare_capped: Mapped[Decimal] = mapped_column(Numeric(4, 1), nullable=False) # Final balance (capped settlement + carry - used) balance_legal_days: Mapped[Decimal] = mapped_column(Numeric(4, 1), nullable=False) balance_welfare_days: Mapped[Decimal] = mapped_column(Numeric(4, 1), nullable=False) cap_applied_full_year: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) cap_applied_settlement: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) # 基数 cap (2026-08-26) — the prorated result was pulled back to the # employee's own annual-leave basis. Kept SEPARATE from the 15-day # combined cap above because the two produce different explanations # to an employee disputing a settlement. Backfilled False on # existing rows, which is the truth: no row written before this # rule shipped ever had a 基数 cap applied to it. basis_cap_applied_full_year: Mapped[bool] = mapped_column( Boolean, nullable=False, default=False, ) basis_cap_applied_settlement: Mapped[bool] = mapped_column( Boolean, nullable=False, default=False, ) # Audit calculated_by_user_id: Mapped[str] = mapped_column(CHAR(36), nullable=False, index=True) calculated_by_name: Mapped[str] = mapped_column(String(128), nullable=False) notes: Mapped[str | None] = mapped_column(Text) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now(), )