from __future__ import annotations import calendar from datetime import datetime, time, timedelta from zoneinfo import ZoneInfo from app.services.credit.utils import ensure_aware BUSINESS_TZ = ZoneInfo("Asia/Shanghai") def to_business_time(value: datetime) -> datetime: return ensure_aware(value).astimezone(BUSINESS_TZ) def add_natural_months(anchor_at: datetime, months: int) -> datetime: """始终基于传入锚点计算自然月,月底压缩后下月恢复原锚点日。""" anchor = to_business_time(anchor_at) month_index = anchor.year * 12 + anchor.month - 1 + int(months) year, month_zero = divmod(month_index, 12) month = month_zero + 1 target_day = min(anchor.day, calendar.monthrange(year, month)[1]) return anchor.replace(year=year, month=month, day=target_day) def natural_month_period(anchor_at: datetime, sequence: int) -> tuple[datetime, datetime]: start = add_natural_months(anchor_at, sequence) end = add_natural_months(anchor_at, sequence + 1) return start, end def next_local_midnight(value: datetime) -> datetime: local = to_business_time(value) next_day = local.date() + timedelta(days=1) return datetime.combine(next_day, time.min, tzinfo=BUSINESS_TZ) def last_usable_at(expires_at: datetime) -> datetime: return ensure_aware(expires_at) - timedelta(microseconds=1)