93 lines
3.5 KiB
Python
93 lines
3.5 KiB
Python
"""Explicit row values for ARR processing XML, not an OHIP mapping policy.
|
|
|
|
Callers must already have resolved every value, note/trace selection and row
|
|
order from independently accepted source rules. No fields have defaults.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from datetime import date
|
|
import re
|
|
|
|
from .collect_arr_source import require
|
|
|
|
VERSION = "arr-processing-xml/v1"
|
|
# Match the existing frozen handoff/upload boundary, not the larger diagnostic
|
|
# native-XML comparator limit. A serialization result must fit that entrypoint.
|
|
MAX_BYTES = 25 * 1024 * 1024
|
|
MAX_ROWS = 10000
|
|
MAX_ELEMENTS = 500000
|
|
|
|
# Declarative correspondence only; shared by serializer and independent parser.
|
|
SCALARS = (
|
|
("block_code", "BLOCK_CODE"), ("adults", "ADULTS"), ("children", "CF_CHILDREN"),
|
|
("company_name", "COMPANY_NAME"), ("confirmation_no", "CONFIRMATION_NO"),
|
|
("room_no", "DISP_ROOM_NO"), ("effective_rate", "EFFECTIVE_RATE_AMOUNT"),
|
|
("full_name", "FULL_NAME"), ("no_of_rooms", "NO_OF_ROOMS"), ("products", "PRODUCTS"),
|
|
("rate_code", "RATE_CODE"), ("room_category_label", "ROOM_CATEGORY_LABEL"),
|
|
("arrival", "TRUNC_BEGIN"), ("departure", "TRUNC_END"),
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class SourceRow:
|
|
reservation_id: str
|
|
block_code: str
|
|
adults: str
|
|
children: str
|
|
company_name: str
|
|
confirmation_no: str
|
|
room_no: str
|
|
effective_rate: str
|
|
full_name: str
|
|
no_of_rooms: str
|
|
products: str
|
|
rate_code: str
|
|
room_category_label: str
|
|
arrival: str
|
|
departure: str
|
|
notes: tuple[str, ...]
|
|
traces: tuple[str, ...]
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class SourceDocument:
|
|
hotel_id: str
|
|
report_date: date
|
|
rows: tuple[SourceRow, ...]
|
|
|
|
|
|
def _text(value):
|
|
require(type(value) is str, "arr_xml_explicit_text_required")
|
|
require(all(char in "\t\n\r" or 0x20 <= ord(char) <= 0xD7FF
|
|
or 0xE000 <= ord(char) <= 0xFFFD or 0x10000 <= ord(char) <= 0x10FFFF
|
|
for char in value), "arr_xml_invalid_character")
|
|
return len(value.encode("utf-8"))
|
|
|
|
|
|
def validate_document(document: SourceDocument) -> None:
|
|
"""Validate transport shape only; existing processor owns business rules.
|
|
|
|
An explicit blank is retained even on a required whitelist field. A missing
|
|
value/None is rejected. This preserves invalid and non-whitelist source rows so
|
|
serialization cannot silently remove records to make processing succeed.
|
|
"""
|
|
require(type(document) is SourceDocument, "arr_xml_document_required")
|
|
require(type(document.hotel_id) is str and bool(re.fullmatch(r"[A-Za-z0-9_-]{1,64}", document.hotel_id)),
|
|
"arr_xml_hotel_required")
|
|
require(type(document.report_date) is date, "arr_xml_explicit_date_required")
|
|
require(type(document.rows) is tuple and 1 <= len(document.rows) <= MAX_ROWS, "arr_xml_record_count")
|
|
size, elements = 0, 6
|
|
for row in document.rows:
|
|
require(type(row) is SourceRow, "arr_xml_row_required")
|
|
size += _text(row.reservation_id)
|
|
require(bool(row.reservation_id.strip()), "arr_xml_reservation_identity_required")
|
|
for attribute, _ in SCALARS:
|
|
size += _text(getattr(row, attribute))
|
|
for collection in (row.notes, row.traces):
|
|
require(type(collection) is tuple, "arr_xml_explicit_collection_required")
|
|
for value in collection:
|
|
size += _text(value)
|
|
elements += 19 + 2 * (len(row.notes) + len(row.traces))
|
|
require(size <= MAX_BYTES and elements <= MAX_ELEMENTS, "arr_xml_input_budget_exceeded")
|