修复V4复核指针与任务详情脱敏

This commit is contained in:
andy
2026-07-21 11:35:17 +07:00
parent de6a52d708
commit f228c2b704
9 changed files with 452 additions and 20 deletions

View File

@@ -1019,6 +1019,146 @@ class ReservationV4CommandControllerTest {
.value("/room_information/final_values/room_items/0/room_type_code"));
}
@Test
void shouldResolveRoomInformationReviewWithEditableFieldPointerFromTaskDetail() throws Exception {
SeededOrderTask seeded = seedReviewOrderTaskWithBusinessCard(
HOTEL_ID,
"mail-v4-command-review-room-info-field-contract-001",
Instant.parse("2026-07-19T01:22:16Z"),
990000000000070114L,
ReservationV4TargetResolutionStatus.RESOLVED.name(),
ReservationV4CardStatus.PENDING_CONFIRM.name(),
ReservationV4CardStatus.REVIEW_REQUIRED.name(),
ReservationV4CardType.ROOM_INFORMATION.name(),
"UPDATE_BOOKING",
"""
{
"card_type":"ROOM_INFORMATION",
"event_type":"UPDATE_BOOKING",
"target_order":{"booking_type":"GROUP","locator_type":"GROUP_CODE","locator_value":"GRP-V4-REVIEW-RI-FIELD-001"},
"business_fields":{
"event_type":"UPDATE_BOOKING",
"after":{
"arrival_date":"2026-08-01",
"departure_date":"2026-08-03",
"room_items":[{"room_type_code":"UNKNOWN_TYPE","room_count":2}]
}
}
}
""",
"""
[
{
"field_path": "room_information.final_values.room_items.0.room_type_code",
"field_pointer": "/room_information/final_values/room_items/0/room_type_code",
"message": "房型代码不在第一版目录中。",
"detail": "room_information.final_values.room_items.0.room_type_code: 房型代码不在第一版目录中。"
}
]
""");
confirmBasicCard(seeded);
performAuthorized(mockMvc, adminToken(), get("/api/reservation/order-tasks/{orderTaskId}", seeded.orderTask().id())
.param("hotel_id", HOTEL_ID))
.andExpect(status().isOk())
.andExpect(jsonPath("$.business_cards[0].fields[?(@.field_pointer=='/room_information/final_values/room_items/0/room_type_code')].editable")
.value(contains(true)))
.andExpect(jsonPath("$.business_cards[0].fields[?(@.field_pointer=='/room_information/final_values/room_items/0/room_type_code')].write_target")
.value(contains("review_resolution.field_overrides")));
performAuthorized(mockMvc, adminToken(), post(
"/api/reservation/order-tasks/{orderTaskId}/cards/{cardId}/review-resolution",
seeded.orderTask().id(),
seeded.businessCard().id())
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"version": 0,
"reason": "按详情页白名单修正房型",
"field_overrides": [
{
"field_pointer": "/room_information/final_values/room_items/0/room_type_code",
"value": "RM2"
}
]
}
"""))
.andExpect(status().isOk())
.andExpect(jsonPath("$.business_cards[0].card_status").value("CONFIRMED"))
.andExpect(jsonPath("$.business_cards[0].confirmed_payload.room_information.final_values.room_items[0].room_type_code")
.value("RM2"))
.andExpect(jsonPath("$.business_cards[0].review_resolution.field_overrides[0].field_pointer")
.value("/room_information/final_values/room_items/0/room_type_code"));
}
@Test
void shouldResolveRoomInformationReviewWhenDisplayPayloadAlreadyUsesStableModel() throws Exception {
SeededOrderTask seeded = seedReviewOrderTaskWithBusinessCard(
HOTEL_ID,
"mail-v4-command-review-room-info-stable-display-001",
Instant.parse("2026-07-19T01:22:26Z"),
990000000000070124L,
ReservationV4TargetResolutionStatus.RESOLVED.name(),
ReservationV4CardStatus.PENDING_CONFIRM.name(),
ReservationV4CardStatus.REVIEW_REQUIRED.name(),
ReservationV4CardType.ROOM_INFORMATION.name(),
"UPDATE_BOOKING",
"""
{
"card_type":"ROOM_INFORMATION",
"event_type":"UPDATE_BOOKING",
"room_information":{
"event_type":"UPDATE_BOOKING",
"booking_type":"GROUP",
"current_values":{},
"proposed_values":{"room_items":[{"room_type_code":"UNKNOWN_TYPE","room_count":2}]},
"final_values":{
"arrival_date":"2026-08-01",
"departure_date":"2026-08-03",
"room_items":[{"room_type_code":"UNKNOWN_TYPE","room_count":2}],
"breakfast_included":true,
"group_booking_status":"TEN",
"group_booking_status_label":"TEN-Tentative"
}
}
}
""",
"""
[
{
"field_path": "room_information.final_values.room_items.0.room_type_code",
"field_pointer": "/room_information/final_values/room_items/0/room_type_code",
"message": "房型代码不在第一版目录中。",
"detail": "room_information.final_values.room_items.0.room_type_code: 房型代码不在第一版目录中。"
}
]
""");
confirmBasicCard(seeded);
performAuthorized(mockMvc, adminToken(), post(
"/api/reservation/order-tasks/{orderTaskId}/cards/{cardId}/review-resolution",
seeded.orderTask().id(),
seeded.businessCard().id())
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"version": 0,
"reason": "修正已稳定展示模型中的房型",
"field_overrides": [
{
"field_pointer": "/room_information/final_values/room_items/0/room_type_code",
"value": "RM2"
}
]
}
"""))
.andExpect(status().isOk())
.andExpect(jsonPath("$.business_cards[0].confirmed_payload.room_information.final_values.room_items[0].room_type_code")
.value("RM2"))
.andExpect(jsonPath("$.business_cards[0].confirmed_payload.room_information.final_values.nights")
.value(2));
}
@Test
void shouldRejectRoomInformationReviewWhenPointerTargetsDerivedReadonlyField() throws Exception {
SeededOrderTask seeded = seedReviewOrderTaskWithBusinessCard(

View File

@@ -526,6 +526,73 @@ class ReservationV4QueryControllerTest {
.andExpect(content().string(not(containsString("SHOULD-NOT-LEAK"))));
}
@Test
void shouldNotExposeTargetOrderInRoomingListDisplayOrConfirmedPayload() throws Exception {
ReservationV4OrderTaskSnapshot orderTask = seedOrderTaskWithBusinessCardType(
"mail-v4-query-rooming-target-order-safe-001",
Instant.parse("2026-07-18T03:02:45Z"),
"GROUP",
"GROUP_CODE",
"GRP-V4-ROOMING-SAFE-001",
ReservationV4CardType.ROOMING_LIST.name(),
"ROOMING_LIST",
ReservationV4CardStatus.CONFIRMED.name(),
ReservationV4CardStatus.PENDING_CONFIRM.name(),
"""
{
"card_type":"ROOMING_LIST",
"event_type":"ROOMING_LIST",
"target_order":{"locator_value":"SHOULD-NOT-LEAK-ROOMING-DISPLAY"},
"attachments":[
{
"name":"rooming-list.xlsx",
"url":"https://oss.example.test/rooming/display.xlsx",
"externalUrl":"https://oss.example.test/rooming/display-external.xlsx"
}
],
"business_fields":{
"attachment_ids":["att-rooming-safe-001"],
"rooming_list_action":"CONFIRM_RECEIVED",
"evidence_url":"https://oss.example.test/rooming/evidence.pdf",
"file_references":["https://oss.example.test/rooming/reference.pdf"]
}
}
""",
"""
{
"card_type":"ROOMING_LIST",
"event_type":"ROOMING_LIST",
"target_order":{"locator_value":"SHOULD-NOT-LEAK-ROOMING-CONFIRMED"},
"attachments":[
{
"name":"rooming-list-confirmed.xlsx",
"download_url":"https://oss.example.test/rooming/confirmed.xlsx"
}
],
"business_fields":{
"attachment_ids":["att-rooming-safe-001"],
"rooming_list_action":"CONFIRM_RECEIVED",
"signedUrl":"https://oss.example.test/rooming/signed.pdf"
}
}
""");
performAuthorized(mockMvc, adminToken(), get("/api/reservation/order-tasks/{orderTaskId}", orderTask.id())
.param("hotel_id", HOTEL_ID))
.andExpect(status().isOk())
.andExpect(jsonPath("$.business_cards[0].card_type").value("ROOMING_LIST"))
.andExpect(jsonPath("$.business_cards[0].display_payload.target_order").doesNotExist())
.andExpect(jsonPath("$.business_cards[0].confirmed_payload.target_order").doesNotExist())
.andExpect(jsonPath("$.business_cards[0].display_payload.attachments[0].name").value("rooming-list.xlsx"))
.andExpect(jsonPath("$.business_cards[0].display_payload.attachments[0].url").doesNotExist())
.andExpect(jsonPath("$.business_cards[0].display_payload.attachments[0].externalUrl").doesNotExist())
.andExpect(jsonPath("$.business_cards[0].display_payload.business_fields.evidence_url").doesNotExist())
.andExpect(jsonPath("$.business_cards[0].confirmed_payload.attachments[0].download_url").doesNotExist())
.andExpect(jsonPath("$.business_cards[0].confirmed_payload.business_fields.signedUrl").doesNotExist())
.andExpect(content().string(not(containsString("SHOULD-NOT-LEAK-ROOMING"))))
.andExpect(content().string(not(containsString("oss.example.test"))));
}
@Test
void shouldReturnReadonlyRoomInformationDisplayModelForCancelBooking() throws Exception {
Long orderId = 990000000000777002L;
@@ -1107,6 +1174,67 @@ class ReservationV4QueryControllerTest {
return orderTask;
}
private ReservationV4OrderTaskSnapshot seedOrderTaskWithBusinessCardType(
String externalMessageId,
Instant receivedAt,
String targetBookingType,
String targetLocatorType,
String targetLocatorValue,
String businessCardType,
String businessEventType,
String basicCardStatus,
String businessCardStatus,
String businessDisplayPayloadJson,
String confirmedPayloadJson) {
SourceMessageCaptureResult source = captureSourceMessage(externalMessageId, "V4 Query Business", receivedAt, HOTEL_ID);
LocalDateTime now = LocalDateTime.ofInstant(receivedAt.plusSeconds(10), ZoneOffset.UTC);
ReservationV4OrderTaskSnapshot orderTask = workflowRepository.findOrCreateOrderTask(new ReservationV4OrderTaskDraft(
HOTEL_ID,
source.inboxId(),
990000000000003001L + Math.abs(externalMessageId.hashCode()),
"order-generic-card-" + externalMessageId,
1,
null,
targetBookingType,
targetLocatorType,
targetLocatorValue,
ReservationV4TargetResolutionStatus.RESOLVED.name(),
ReservationV4OrderTaskStatus.OPEN.name(),
LocalDateTime.ofInstant(receivedAt, ZoneOffset.UTC),
now));
insertCard(orderTask, ReservationV4CardType.SOURCE_MESSAGE_DISPLAY.name(), null, 0, 10,
ReservationV4CardStatus.READONLY.name(), null, """
{"card_type":"SOURCE_MESSAGE_DISPLAY","source_message":{"subject":"V4 Query Business"}}
""");
insertCard(orderTask, ReservationV4CardType.BASIC_INFORMATION.name(), null, 0, 20,
basicCardStatus, reviewStatusFor(basicCardStatus), """
{
"card_type":"BASIC_INFORMATION",
"order_ref":"order-1",
"basic_information":{"account_code":"QBD_TRAVEL","market_code":"LEISURE","source_code":"TRAVEL_AGENT"}
}
""");
ReservationV4TaskCardSnapshot businessCard = insertCard(
orderTask,
businessCardType,
businessEventType,
1,
30,
businessCardStatus,
reviewStatusFor(businessCardStatus),
businessDisplayPayloadJson);
if (confirmedPayloadJson != null) {
workflowRepository.confirmTaskCardWithVersion(
HOTEL_ID,
businessCard.id(),
businessCard.version(),
confirmedPayloadJson,
"v4-query-admin",
now);
}
return orderTask;
}
private void seedConfirmedRoomInformationProjection(
Long orderId,
String externalMessageId,