Files
ARR-2.0-0918/integrations/ohip/source_facts_contract.py
T

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}