"""Pure deterministic rules for monthly channel report payloads.""" from __future__ import annotations import calendar import re from collections import defaultdict from datetime import date from decimal import Decimal from typing import Dict, Iterable, List, Sequence, Tuple from monthly_reports.contracts import ( KB_SHEET, STANDARD_SHEETS, ChannelObservation, ChannelSheet, DailyVersionPin, ErrorCode, MonthlyFact, MonthlyReport, MonthlyReportError, MonthlySnapshot, ) INVALID_SHEET_CHARS = re.compile(r"[:\\/?*\[\]]") def month_bounds(year: int, month: int) -> Tuple[date, date]: if year < 1900 or year > 9999 or month < 1 or month > 12: raise MonthlyReportError(ErrorCode.REQUEST_INVALID, "report month is invalid") return date(year, month, 1), date(year, month, calendar.monthrange(year, month)[1]) def validate_as_of(year: int, month: int, as_of_date: date) -> None: start, end = month_bounds(year, month) if as_of_date < start or as_of_date > end: raise MonthlyReportError( ErrorCode.REQUEST_INVALID, "as-of date must fall inside the report month", ) def _valid_sheet_name(value: str) -> bool: return ( bool(value) and value == value.strip() and len(value) <= 31 and INVALID_SHEET_CHARS.search(value) is None ) def _append_unique(target: List[str], seen: set[str], values: Iterable[str]) -> None: for value in values: if not _valid_sheet_name(value): raise MonthlyReportError( ErrorCode.SOURCE_INVALID, "source channel worksheet name is invalid", ) if value not in seen: target.append(value) seen.add(value) def channel_order(snapshot: MonthlySnapshot) -> Tuple[str, ...]: ordered: List[str] = [] seen: set[str] = set() _append_unique(ordered, seen, STANDARD_SHEETS) _append_unique( ordered, seen, (name for name in snapshot.preferred_channel_order if name not in STANDARD_SHEETS), ) observations: Sequence[ChannelObservation] = sorted( snapshot.channel_observations, key=lambda item: ( item.business_date, item.worksheet_order is None, item.worksheet_order if item.worksheet_order is not None else 2**31, item.worksheet, ), ) _append_unique(ordered, seen, (item.worksheet for item in observations)) _append_unique( ordered, seen, sorted({fact.channel_key for fact in snapshot.facts if fact.channel_key not in seen}), ) return tuple(ordered) def _require_nonnegative(value: int, label: str) -> None: if not isinstance(value, int) or isinstance(value, bool) or value < 0: raise MonthlyReportError(ErrorCode.SOURCE_INVALID, f"{label} is invalid") def _validate_fact(fact: MonthlyFact, start: date, as_of_date: date) -> None: if ( fact.daily_record_id <= 0 or fact.daily_version_id <= 0 or fact.business_date < start or fact.business_date > as_of_date or fact.arrival != fact.business_date or fact.departure < fact.arrival or fact.nights != (fact.departure - fact.arrival).days or not _valid_sheet_name(fact.channel_key) or fact.no_of_rooms <= 0 ): raise MonthlyReportError( ErrorCode.SOURCE_INVALID, "monthly source fact shape is invalid", ) _require_nonnegative(fact.adults, "adults") _require_nonnegative(fact.children, "children") for text in ( fact.company_name, fact.confirmation_no, fact.disp_room_no, fact.full_name, fact.rate_code, ): if not str(text).strip(): raise MonthlyReportError( ErrorCode.SOURCE_INVALID, "monthly source text is missing", ) for amount in ( fact.effective_rate_amount, fact.real_price, fact.total_price, ): if not isinstance(amount, Decimal) or not amount.is_finite() or amount < 0: raise MonthlyReportError( ErrorCode.SOURCE_INVALID, "monthly source amount is invalid", ) if fact.total_price != fact.real_price * fact.no_of_rooms * fact.nights: raise MonthlyReportError( ErrorCode.SOURCE_INVALID, "monthly source total does not match static fact inputs", ) if fact.channel_key == KB_SHEET: if fact.kb_amount != Decimal(fact.no_of_rooms * 100): raise MonthlyReportError( ErrorCode.SOURCE_INVALID, "monthly KB source amount is invalid", ) elif fact.kb_amount is not None: raise MonthlyReportError( ErrorCode.SOURCE_INVALID, "non-KB channel unexpectedly contains a KB amount", ) def _validate_pins( facts: Sequence[MonthlyFact], pins: Sequence[DailyVersionPin], start: date, as_of_date: date, ) -> Tuple[DailyVersionPin, ...]: by_date: Dict[date, int] = {} for pin in pins: if ( pin.business_date < start or pin.business_date > as_of_date or pin.daily_version_id <= 0 or ( pin.business_date in by_date and by_date[pin.business_date] != pin.daily_version_id ) ): raise MonthlyReportError( ErrorCode.SOURCE_INVALID, "monthly daily-version manifest is invalid", ) by_date[pin.business_date] = pin.daily_version_id for fact in facts: if by_date.get(fact.business_date) != fact.daily_version_id: raise MonthlyReportError( ErrorCode.SOURCE_INVALID, "monthly fact is not covered by the current daily manifest", ) return tuple( DailyVersionPin(business_date=current, daily_version_id=version_id) for current, version_id in sorted(by_date.items()) ) def _validate_observations( facts: Sequence[MonthlyFact], observations: Sequence[ChannelObservation], pins: Sequence[DailyVersionPin], ) -> None: pin_by_date = {pin.business_date: pin.daily_version_id for pin in pins} observed: Dict[Tuple[int, str], int] = {} orders: set[Tuple[int, int]] = set() for item in observations: if ( pin_by_date.get(item.business_date) != item.daily_version_id or not _valid_sheet_name(item.worksheet) or item.row_count < 0 or (item.worksheet_order is not None and item.worksheet_order <= 0) ): raise MonthlyReportError( ErrorCode.SOURCE_INVALID, "monthly channel observation is invalid", ) key = (item.daily_version_id, item.worksheet) if key in observed: raise MonthlyReportError( ErrorCode.SOURCE_INVALID, "monthly channel observation is duplicated", ) observed[key] = item.row_count if item.worksheet_order is not None: order_key = (item.daily_version_id, item.worksheet_order) if order_key in orders: raise MonthlyReportError( ErrorCode.SOURCE_INVALID, "monthly channel observation order is duplicated", ) orders.add(order_key) actual: Dict[Tuple[int, str], int] = defaultdict(int) for fact in facts: actual[(fact.daily_version_id, fact.channel_key)] += 1 for key, row_count in actual.items(): if observed.get(key) != row_count: raise MonthlyReportError( ErrorCode.SOURCE_INVALID, "monthly channel observation does not match retained facts", ) def build_monthly_report( year: int, month: int, as_of_date: date, snapshot: MonthlySnapshot, ) -> MonthlyReport: validate_as_of(year, month, as_of_date) start, _end = month_bounds(year, month) facts = tuple(sorted(snapshot.facts, key=lambda item: item.daily_record_id)) if len({fact.daily_record_id for fact in facts}) != len(facts): raise MonthlyReportError(ErrorCode.SOURCE_INVALID, "monthly source rows are duplicated") for fact in facts: _validate_fact(fact, start, as_of_date) if facts and max(fact.arrival for fact in facts) != as_of_date: raise MonthlyReportError( ErrorCode.SOURCE_INVALID, "monthly as-of date must equal the greatest included ARRIVAL", ) pins = _validate_pins(facts, snapshot.daily_versions, start, as_of_date) _validate_observations(facts, snapshot.channel_observations, pins) ordered_channels = channel_order(snapshot) grouped: Dict[str, List[MonthlyFact]] = defaultdict(list) for fact in facts: grouped[fact.channel_key].append(fact) channels = tuple( ChannelSheet( worksheet=name, worksheet_order=index, rows=tuple( sorted(grouped.get(name, []), key=lambda item: (item.arrival, item.daily_record_id)) ), ) for index, name in enumerate(ordered_channels, 1) ) if sum(len(channel.rows) for channel in channels) != len(facts): raise MonthlyReportError( ErrorCode.SOURCE_INVALID, "monthly source channel coverage is incomplete", ) filename = ( f"各渠道情况-{year:04d}年{month:02d}月-" f"更新至{as_of_date.month}.{as_of_date.day}.xlsx" ) return MonthlyReport( report_year=year, report_month=month, as_of_date=as_of_date, filename=filename, channels=channels, daily_versions=pins, )