"""Pydantic request models for the book-close calendar + period lifecycle (PR1).""" from __future__ import annotations import re from datetime import date from pydantic import BaseModel, Field, field_validator from app.domains.finance.close_models import RUN_STATUSES, WORK_WEEKEND_DAYS _PERIOD_RE = re.compile(r"^\d{4}-(0[1-9]|1[0-2])$") def _check_period(v: str) -> str: if not _PERIOD_RE.match((v or "").strip()): raise ValueError("period must be 'YYYY-MM'") return v.strip() class AnchorsBody(BaseModel): anchor_minus_1: date | None = Field(None, description="Day-1 anchor (last day of closing month).") anchor_plus_1: date | None = Field(None, description="Day+1 anchor (first day of following month).") class OverrideBody(BaseModel): override_close_date: date | None = Field( None, description="Manual close-date override; null clears the override." ) reason: str | None = Field(None, max_length=2000, description="Audit note (required when setting).") class StatusBody(BaseModel): status: str = Field(..., description="Target lifecycle status.") @field_validator("status") @classmethod def _v(cls, v: str) -> str: v = (v or "").strip() if v not in RUN_STATUSES: raise ValueError(f"status must be one of {RUN_STATUSES}") return v class ConfigBody(BaseModel): target_workday: int | None = Field(None, ge=1, le=20) work_weekend_day: str | None = Field(None) @field_validator("work_weekend_day") @classmethod def _v(cls, v: str | None) -> str | None: if v is None: return v v = v.strip().lower() if v not in WORK_WEEKEND_DAYS: raise ValueError("work_weekend_day must be 'saturday' or 'sunday'") return v class HolidayBody(BaseModel): day: date country: str = Field("CN", max_length=8) name: str | None = Field(None, max_length=255) @field_validator("country") @classmethod def _vc(cls, v: str) -> str: return (v or "CN").strip().upper() class MakeupBody(BaseModel): day: date note: str | None = Field(None, max_length=255)