133 lines
8.4 KiB
Python
133 lines
8.4 KiB
Python
"""Unaccepted field-evidence format; no report projection or business policy.
|
|
|
|
Paths identify observations, not interchangeable values or fallback priorities.
|
|
The independent extractor and verifier share only this declarative contract and
|
|
the exact scalar encoding. All arrays retain their original order and indexes.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from decimal import Decimal
|
|
import hashlib
|
|
import json
|
|
|
|
|
|
VERSION = "arr-source-field-evidence/v1"
|
|
MAX_FACTS_BYTES = 128 * 1024 * 1024
|
|
FLAGS = {"source_mapping_verified": False, "report_equivalence_verified": False, "finance_ready": False}
|
|
|
|
# Each tuple is (source, variant, relative path). No predicate, joining,
|
|
# formatting, defaulting or preferred alternative is implied by this list.
|
|
FIELDS = {
|
|
# Preserve the original evidence contract. Typed BlockCode values observed
|
|
# later are already retained under BLOCK_ID + BLOCK_ID_TYPE context below;
|
|
# preparation/v3 selects them from the verified raw search/detail documents.
|
|
"BLOCK_CODE": (),
|
|
"ADULTS": (("detail", "stay", "roomStay/guestCounts/adults"),
|
|
("detail", "rate_segments", "roomStay/roomRates/*/guestCounts/adults")),
|
|
"CHILDREN": (("detail", "stay", "roomStay/guestCounts/children"),
|
|
("detail", "rate_segments", "roomStay/roomRates/*/guestCounts/children")),
|
|
"COMPANY_NAME": (("detail", "reservation_profiles", "reservationProfiles/reservationProfile/*/profile/company/companyName"),
|
|
("detail", "rate_profiles", "roomStay/roomRates/*/stayProfiles/*/profile/company/companyName")),
|
|
# These are typed ID candidates; ID_TYPE context is mandatory. This stage
|
|
# deliberately does not pick the first ID or assume each ID is Confirmation.
|
|
"CONFIRMATION_NO": (("detail", "typed_id_candidates", "reservationIdList/*/id"),),
|
|
"DISP_ROOM_NO": (("detail", "current_room", "roomStay/currentRoomInfo/roomId"),
|
|
("detail", "rate_segments", "roomStay/roomRates/*/roomId")),
|
|
"EFFECTIVE_RATE_AMOUNT": (("rate", "raw_day_rate", "detail/totalRateAmount"),
|
|
("assessment", "validated_day_rate", "effective_rate")),
|
|
"FULL_NAME": (("search", "display_name", "reservationGuest/fullName"),),
|
|
"RES_COMMENT": (("detail", "all_returned_comments", "comments/*/comment/text/value"),),
|
|
"TRACE_TEXT": (("detail", "all_returned_traces", "traces/*/traceText"),),
|
|
"NO_OF_ROOMS": (("detail", "rate_segments", "roomStay/roomRates/*/numberOfUnits"),),
|
|
"PRODUCTS": (("detail", "package_codes_only", "reservationPackages/*/packageCode"),),
|
|
"RATE_CODE": (("detail", "rate_segments", "roomStay/roomRates/*/ratePlanCode"),),
|
|
"ROOM_CATEGORY_LABEL": (("detail", "room_type_codes_only", "roomStay/roomRates/*/roomType"),),
|
|
"ARRIVAL": (("detail", "stay", "roomStay/arrivalDate"),),
|
|
"DEPARTURE": (("detail", "stay", "roomStay/departureDate"),),
|
|
}
|
|
|
|
CONTEXT_PATHS = {
|
|
"RESERVATION_STATUS": (("detail", "status", "reservationStatus"),),
|
|
"ID_TYPE": (("detail", "typed_ids", "reservationIdList/*/type"),),
|
|
"RATE_START": (("detail", "segments", "roomStay/roomRates/*/start"),),
|
|
"RATE_END": (("detail", "segments", "roomStay/roomRates/*/end"),),
|
|
"PROFILE_ROLE": (("detail", "reservation", "reservationProfiles/reservationProfile/*/reservationProfileType"),
|
|
("detail", "rate_segments", "roomStay/roomRates/*/stayProfiles/*/reservationProfileType")),
|
|
"GUEST_PRIMARY": (("detail", "guests", "reservationGuests/*/primary"),),
|
|
"NAME_TYPE": (("detail", "names", "reservationGuests/*/profileInfo/profile/customer/personName/*/nameType"),),
|
|
"SURNAME": (("detail", "names", "reservationGuests/*/profileInfo/profile/customer/personName/*/surname"),),
|
|
"GIVEN_NAME": (("detail", "names", "reservationGuests/*/profileInfo/profile/customer/personName/*/givenName"),),
|
|
"MIDDLE_NAME": (("detail", "names", "reservationGuests/*/profileInfo/profile/customer/personName/*/middleName"),),
|
|
"NAME_TITLE": (("detail", "names", "reservationGuests/*/profileInfo/profile/customer/personName/*/nameTitle"),),
|
|
"COMMENT_TYPE": (("detail", "comments", "comments/*/comment/type"),),
|
|
"COMMENT_LOCATION": (("detail", "comments", "comments/*/comment/notificationLocation"),),
|
|
"COMMENT_INTERNAL": (("detail", "comments", "comments/*/comment/internal"),),
|
|
"COMMENT_CONFIDENTIAL": (("detail", "comments", "comments/*/comment/confidential"),),
|
|
"TRACE_RESOLVED_ON": (("detail", "traces", "traces/*/resolveInfo/resolvedOn"),),
|
|
"TRACE_RESOLVED_BY": (("detail", "traces", "traces/*/resolveInfo/resolvedBy"),),
|
|
"BLOCK_ID": (("detail", "segments", "roomStay/roomRates/*/reservationBlock/blockIdList/*/id"),),
|
|
"BLOCK_ID_TYPE": (("detail", "segments", "roomStay/roomRates/*/reservationBlock/blockIdList/*/type"),),
|
|
"DAY_RATE_CURRENCY": (("rate", "raw", "detail/revenue/currencyCode"),
|
|
("assessment", "validated", "currency")),
|
|
"DETAIL_CURRENCY": (("detail", "rates", "roomStay/roomRates/*/rates/rate/*/base/currencyCode"),),
|
|
"RATE_SUPPRESSED": (("rate", "raw", "detail/rateSuppressed"),),
|
|
"RATE_ISSUE": (("assessment", "validation", "error"),),
|
|
}
|
|
|
|
BLOCKERS = ("report_inclusion", "report_order_and_row_granularity", "company_role_and_display",
|
|
"name_display", "historical_room", "note_and_trace_scope_order", "rate_interval_semantics",
|
|
"optional_report_displays", "same_source_acceptance")
|
|
|
|
|
|
def canonical(value) -> bytes:
|
|
return (json.dumps(value, ensure_ascii=True, sort_keys=True, separators=(",", ":"), allow_nan=False) + "\n").encode()
|
|
|
|
|
|
CONTRACT_SHA256 = hashlib.sha256(canonical({"version": VERSION, "fields": FIELDS,
|
|
"context": CONTEXT_PATHS, "flags": FLAGS, "blockers": BLOCKERS})).hexdigest()
|
|
|
|
# v1 stays byte-for-byte compatible with evidence from three-operation captures.
|
|
# v2 adds observations from the fourth operation; no report mapping is selected.
|
|
NAMED_VERSION = "arr-source-field-evidence/v2"
|
|
NAMED_FIELDS = {**FIELDS, "FULL_NAME": (*FIELDS["FULL_NAME"],
|
|
("profile", "raw_summary_name", "profile/formerName/fullName"),
|
|
("assessment", "validated_primary_name", "profile/full_name"))}
|
|
NAMED_CONTEXT_PATHS = {**CONTEXT_PATHS,
|
|
"GUEST_PROFILE_ID": (("detail", "guests", "reservationGuests/*/profileInfo/profileIdList/*/id"),),
|
|
"GUEST_PROFILE_ID_TYPE": (("detail", "guests", "reservationGuests/*/profileInfo/profileIdList/*/type"),),
|
|
"NAME_PREFIX": (("detail", "names", "reservationGuests/*/profileInfo/profile/customer/personName/*/namePrefix"),),
|
|
"SUMMARY_PROFILE_ID": (("profile", "identity", "profileIdList/*/id"),),
|
|
"SUMMARY_PROFILE_ID_TYPE": (("profile", "identity", "profileIdList/*/type"),),
|
|
"SUMMARY_NAME_TYPE": (("profile", "name", "profile/formerName/nameType"),),
|
|
"SUMMARY_SURNAME": (("profile", "name", "profile/formerName/name"),),
|
|
"SUMMARY_GIVEN_NAME": (("profile", "name", "profile/formerName/givenName"),),
|
|
"SUMMARY_MIDDLE_NAME": (("profile", "name", "profile/formerName/middleName"),),
|
|
"SUMMARY_NAME_TITLE": (("profile", "name", "profile/formerName/nameTitle"),),
|
|
"SUMMARY_NAME_PREFIX": (("profile", "name", "profile/formerName/namePrefix"),),
|
|
"NAME_ISSUE": (("assessment", "validation", "profile/error"),)}
|
|
NAMED_BLOCKERS = tuple("note_scope_order" if item == "note_and_trace_scope_order" else item for item in BLOCKERS)
|
|
NAMED_CONTRACT_SHA256 = hashlib.sha256(canonical({"version": NAMED_VERSION, "fields": NAMED_FIELDS,
|
|
"context": NAMED_CONTEXT_PATHS, "flags": FLAGS, "blockers": NAMED_BLOCKERS})).hexdigest()
|
|
|
|
|
|
def definitions(named=False):
|
|
"""Select declarations only; extraction and joining remain independent."""
|
|
return ((NAMED_VERSION, NAMED_CONTRACT_SHA256, NAMED_FIELDS, NAMED_CONTEXT_PATHS, NAMED_BLOCKERS)
|
|
if named else (VERSION, CONTRACT_SHA256, FIELDS, CONTEXT_PATHS, BLOCKERS))
|
|
|
|
|
|
def kind(value) -> str:
|
|
return {str: "string", int: "integer", Decimal: "decimal", bool: "boolean",
|
|
dict: "object", list: "array", type(None): "null"}.get(type(value), "unsupported")
|
|
|
|
|
|
def atom(value) -> dict:
|
|
"""Lossless scalar encoding, not business validation; booleans stay booleans."""
|
|
tag = kind(value)
|
|
if tag == "null":
|
|
return {"state": "null", "kind": tag}
|
|
if tag in {"object", "array", "unsupported"}:
|
|
return {"state": "non_scalar", "kind": tag}
|
|
return {"state": "explicit_blank" if type(value) is str and not value.strip() else "present",
|
|
"kind": tag, "value": str(value) if type(value) in {int, Decimal} else value}
|