Fix production ARR reads and add audited source field review

This commit is contained in:
Wyndham ARR committed 2026-10-08 17:01:54 +08:00
1 parent 2417b1a41c
commit fe8735aac2
26 files changed
+2417 -157

No files matched your search

+121 -10
View File
@@ -93,12 +93,13 @@ class SimulatedOHIP:
i = int(request["id"][3:])
amount = float(self.cases[i]["record"]["EFFECTIVE_RATE_AMOUNT"])
result = {"detail": {"totalRateAmount": amount, "rateSuppressed": False, "revenue": {"currencyCode": "THB"}}}
elif url.path == "/api/v1/profiles/searches":
operation = "searchProfiles"
identity = request["profileIds"][0]
elif url.path == "/api/v1/profiles":
operation = "getProfiles"
identity = query["profileIds"][0]
i = int(identity[5:])
name = self.cases[i]["record"]["FULL_NAME"]
result = {"profileSummaries": {"hasMore": False, "profileInfo": [{"profileIdList": [{"type": "Profile", "id": identity}],
result = {"profileSummaries": {"hasMore": False, "limit": 1, "offset": 1, "totalResults": 1,
"totalPages": 1, "profileInfo": [{"profileIdList": [{"type": "Profile", "id": identity}],
"profile": {"formerName": {"nameType": "Primary", "name": name, "givenName": "", "fullName": name}}}]}}
elif url.path.startswith("/api/v1/profiles/"):
operation = "getProfile"
@@ -199,12 +200,19 @@ class ARRDataTests(unittest.TestCase):
self.assertFalse(list(self.root.rglob("*.xml")))
for call in self.service.calls:
if call["operation"] == base.SEARCH:
self.assertEqual(set(call["body"]), {"arrivalStartDate", "arrivalEndDate", "limit", "offset"})
self.assertEqual(call["body"]["arrivalStartDate"], DAY)
self.assertEqual(call["body"]["arrivalEndDate"], DAY)
if call["operation"] == base.DETAIL:
self.assertNotIn("Traces", call["query"]["fetchInstructions"])
if call["operation"] == "searchRateInfo":
self.assertEqual(call["body"], {"id": call["body"]["id"], "type": "Reservation", "detailDate": DAY, "summaryInfo": False})
if call["operation"] == "getProfiles":
self.assertEqual(call["method"], "GET")
self.assertIsNone(call["body"])
self.assertEqual(call["query"], {"profileIds": call["query"]["profileIds"], "summaryInfo": ["true"],
"limit": ["1"], "offset": ["0"]})
self.assertEqual(len(call["query"]["profileIds"]), 1)
self.assertEqual([c["body"]["offset"] for c in self.service.calls if c["operation"] == base.SEARCH], [0, 2, 4, 0, 2, 4])
def test_completed_replay_does_not_create_transport_or_requery(self):
@@ -248,11 +256,11 @@ class ARRDataTests(unittest.TestCase):
self.source().fetch(DAY, REQUEST)
def test_failed_related_read_retries_and_next_fetch_uses_new_attempt(self):
self.service.status = lambda op, _: 503 if op == "searchProfiles" else 200
self.service.status = lambda op, _: 503 if op == "getProfiles" else 200
first, payload = self.fetch()
self.assertEqual(first["status"], "failed")
self.assertEqual(payload["records"][0]["fields"]["FULL_NAME"]["state"], "failed")
self.assertEqual(sum(c["operation"] == "searchProfiles" for c in self.service.calls), 3)
self.assertEqual(sum(c["operation"] == "getProfiles" for c in self.service.calls), 3)
original = Path(first["data_path"]).read_bytes()
self.service.status = lambda *_: 200
second, _ = self.fetch()
@@ -261,10 +269,10 @@ class ARRDataTests(unittest.TestCase):
self.assertEqual(Path(first["data_path"]).read_bytes(), original)
def test_permission_denied_is_not_retried_or_made_blank(self):
self.service.status = lambda op, _: 403 if op == "searchProfiles" else 200
self.service.status = lambda op, _: 403 if op == "getProfiles" else 200
result, field = self.field("FULL_NAME")
self.assertEqual((result["status"], field["state"], field["reason"]), ("failed", "failed", "http_permission_denied"))
self.assertEqual(sum(c["operation"] == "searchProfiles" for c in self.service.calls), 1)
self.assertEqual(sum(c["operation"] == "getProfiles" for c in self.service.calls), 1)
def test_suppressed_price_is_not_zero_or_base_price(self):
def edit(op, doc, _):
@@ -338,7 +346,7 @@ class ARRDataTests(unittest.TestCase):
self.service.rows[1]["reservationGuests"] = copy.deepcopy(self.service.rows[0]["reservationGuests"])
summary, payload = self.fetch()
self.assertEqual(summary["status"], "collected")
self.assertEqual(sum(c["operation"] == "searchProfiles" for c in self.service.calls), 1)
self.assertEqual(sum(c["operation"] == "getProfiles" for c in self.service.calls), 1)
for row in payload["records"]:
self.assertTrue(row["sources"])
self.assertEqual(row["fields"]["FULL_NAME"]["value"], "模拟客人001")
@@ -348,7 +356,36 @@ class ARRDataTests(unittest.TestCase):
guests.append(copy.deepcopy(guests[0]))
_, field = self.field("FULL_NAME")
self.assertEqual(field["reason"], "ambiguous_primary_guest")
self.assertFalse(any(c["operation"] == "searchProfiles" for c in self.service.calls))
self.assertFalse(any(c["operation"] == "getProfiles" for c in self.service.calls))
def test_get_profile_summary_rejects_wrong_context_identity_and_coverage(self):
modes = {"operation": "operation_mismatch", "hotel": "hotel_mismatch",
"request_id": "missing_oracle_request_id", "identity": "profile_identity_mismatch",
"truncated": "profile_unexpected_more_results", "total": "profile_inconsistent_pagination",
"multiple": "profile_ambiguous_summary_count", "name": "profile_name_components_mismatch"}
for mode, reason in modes.items():
self.root = Path(self.temp.name) / ("summary-" + mode)
self.service = SimulatedOHIP()
def edit(op, doc, _, mode=mode):
if op != "getProfiles": return doc
summary = doc["data"]["profileSummaries"]
row = summary["profileInfo"][0]
if mode == "operation": doc["operation_id"] = "searchProfiles"
elif mode == "hotel": doc["hotel_id"] = "OTHER"
elif mode == "request_id": doc.pop("oracle_request_id")
elif mode == "identity": row["profileIdList"][0]["id"] = "other-guest"
elif mode == "truncated": summary["hasMore"] = True
elif mode == "total": summary["totalResults"] = 2
elif mode == "multiple": summary["profileInfo"].append(copy.deepcopy(row))
else: row["profile"]["formerName"]["name"] = "OTHER"
return doc
self.service.edit = edit
with self.subTest(mode=mode):
result, field = self.field("FULL_NAME")
self.assertFalse(result["input_complete"])
self.assertIsNone(field["value"])
self.assertEqual(field["reason"], reason)
self.assertEqual(sum(c["operation"] == "getProfiles" for c in self.service.calls), 1)
def test_missing_typed_block_code_fetches_bound_block(self):
block = {"blockIdList": [{"type": "Block", "id": "b1"}]}
@@ -365,6 +402,23 @@ class ARRDataTests(unittest.TestCase):
self.assertEqual(result["status"], "collected_with_gaps")
self.assertEqual(field["state"], "missing")
def test_native_optional_absence_requires_review_even_with_zero_package_price(self):
row = self.service.rows[0]
row.pop("reservationProfiles")
row.pop("reservationPackages")
row["roomStay"].pop("reservationBlock")
row["roomStay"]["roomRates"][0].pop("reservationBlock")
def edit(op, doc, _):
if op == "searchRateInfo": doc["data"]["detail"]["totalPackageAmount"] = 0
return doc
self.service.edit = edit
result, payload = self.fetch()
self.assertEqual(result["status"], "collected_with_gaps")
self.assertTrue(result["collection_complete"])
for name in ("BLOCK_CODE", "COMPANY_NAME", "PRODUCTS"):
self.assertEqual(payload["records"][0]["fields"][name]["state"], "missing")
self.assertIsNone(payload["records"][0]["fields"][name]["value"])
def test_block_supplement_cannot_overwrite_earlier_code_or_hotel(self):
for mismatch in ("code", "hotel"):
self.root = Path(self.temp.name) / ("block-" + mismatch)
@@ -456,6 +510,16 @@ class ARRDataTests(unittest.TestCase):
_, field = self.field("DISP_ROOM_NO")
self.assertEqual((field["state"], field["value"], field["reason"]), ("missing", None, "calendar_missing_rooms"))
def test_empty_native_calendar_is_an_unresolved_room_gap(self):
self.missing_room()
def edit(op, doc, _):
if op == "getRoomCalendar": doc["data"]["roomCalendar"] = {}
return doc
self.service.edit = edit
result, field = self.field("DISP_ROOM_NO")
self.assertEqual(result["status"], "collected_with_gaps")
self.assertEqual((field["state"], field["value"], field["reason"]), ("missing", None, "calendar_missing_rooms"))
def test_duplicate_search_ids_and_empty_day_fail_without_processing(self):
for duplicate in (True, False):
self.root = Path(self.temp.name) / str(duplicate)
@@ -478,6 +542,53 @@ class ARRDataTests(unittest.TestCase):
summary, _ = self.fetch()
self.assertEqual(summary["error"], "source_changed_during_collection")
def test_default_server_order_is_retained_and_order_change_fails_recheck(self):
self.service = SimulatedOHIP(2)
self.service.rows.reverse()
result, payload = self.fetch()
self.assertEqual(result["status"], "collected")
self.assertEqual([(r["source_sequence"], r["reservation_id"]) for r in payload["records"]],
[(1, "res1"), (2, "res0")])
self.root = Path(self.temp.name) / "order-change"
self.service = SimulatedOHIP(2)
count = 0
def edit(op, doc, _):
nonlocal count
if op == base.SEARCH:
count += 1
if count == 2: doc["data"]["reservations"]["reservationInfo"].reverse()
return doc
self.service.edit = edit
result, _ = self.fetch()
self.assertEqual((result["status"], result["error"]), ("failed", "source_changed_during_collection"))
def test_unsorted_paging_rejects_truncation_and_changing_totals(self):
for mode, expected in (("truncated", "incomplete_search_total"),
("totals", "pagination_totals_changed"), ("offset", "pagination_mismatch")):
self.root = Path(self.temp.name) / ("paging-" + mode)
self.service = SimulatedOHIP(2)
def edit(op, doc, request, mode=mode):
if op != base.SEARCH: return doc
page = doc["data"]["reservations"]
if mode == "truncated": page["hasMore"] = False
elif mode == "totals" and request["offset"] == 1: page["totalResults"] += 1
elif mode == "offset": page["offset"] = request["offset"]
return doc
self.service.edit = edit
with self.subTest(mode=mode):
result, _ = self.fetch(page_size=1)
self.assertEqual((result["status"], result["error"]), ("failed", expected))
def test_detail_must_match_searched_reservation_identity(self):
def edit(op, doc, _):
if op == base.DETAIL:
doc["data"]["reservations"]["reservation"][0]["reservationIdList"][0]["id"] = "other-reservation"
return doc
self.service.edit = edit
result, payload = self.fetch()
self.assertEqual((result["status"], result["error"]), ("failed", "detail_identity_mismatch"))
self.assertEqual(payload["records"], [])
def test_request_budget_is_enforced_before_network(self):
result, _ = self.fetch(max_requests=2)
self.assertEqual(result["status"], "failed")