"""Serialize explicit ordered rows to the existing ARR processor XML format. No API selection, report semantics, filtering, deduplication or pricing occurs here. Output is a processing projection, not a native Opera report replica. """ from __future__ import annotations import xml.etree.ElementTree as ET from .arr_xml_contract import MAX_BYTES, SCALARS, SourceDocument, validate_document from .collect_arr_source import require def serialize(document: SourceDocument) -> bytes: validate_document(document) root = ET.Element("RES_DETAIL") groups = ET.SubElement(root, "LIST_G_GROUP_BY1") group = ET.SubElement(groups, "G_GROUP_BY1") day = document.report_date ET.SubElement(group, "GROUPBY1_SORT_COL").text = f"{day.year:04d}{day.month:02d}{day.day:02d}" # Use the full-year ISO representation accepted by the existing processor. # A two-digit display year can silently map historical dates to another century. ET.SubElement(group, "GROUPBY1_COL").text = document.report_date.isoformat() rows = ET.SubElement(group, "LIST_G_RESERVATION") for row in document.rows: node = ET.SubElement(rows, "G_RESERVATION") ET.SubElement(node, "RESORT").text = document.hotel_id ET.SubElement(node, "RESV_NAME_ID").text = row.reservation_id for attribute, tag in SCALARS: ET.SubElement(node, tag).text = getattr(row, attribute) comments = ET.SubElement(node, "LIST_G_COMMENT_RESV_NAME_ID") for text in row.notes: ET.SubElement(ET.SubElement(comments, "G_COMMENT_RESV_NAME_ID"), "RES_COMMENT").text = text traces = ET.SubElement(node, "LIST_G_DEPT_ID") for text in row.traces: ET.SubElement(ET.SubElement(traces, "G_DEPT_ID"), "TRACE_TEXT").text = text # XML parsers normalize literal CR; character references preserve it exactly. raw = ET.tostring(root, encoding="utf-8", xml_declaration=True).replace(b"\r", b" ") + b"\n" require(len(raw) <= MAX_BYTES, "arr_xml_output_budget_exceeded") return raw