修复V4房型信息复核与自动DEF展示

This commit is contained in:
andy
2026-07-21 10:40:44 +07:00
parent c28130b3da
commit de6a52d708
10 changed files with 270 additions and 50 deletions

View File

@@ -2,8 +2,10 @@ package cn.nianxx.thhotel.workflows.reservation.control;
import static cn.nianxx.thhotel.support.MockMvcAuthTestSupport.loginToken;
import static cn.nianxx.thhotel.support.MockMvcAuthTestSupport.performAuthorized;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.not;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
@@ -228,6 +230,18 @@ class ReservationV4CommandControllerTest {
org.assertj.core.api.Assertions.assertThat(confirmedPayload)
.contains("\"group_booking_status\":\"DEF\"")
.contains("\"group_booking_status_label\":\"DEF-Definite\"");
performAuthorized(mockMvc, adminToken(), get("/api/reservation/order-tasks/{orderTaskId}",
roomInformation.orderTask().id()).param("hotel_id", HOTEL_ID))
.andExpect(status().isOk())
.andExpect(jsonPath("$.business_cards[0].display_payload.room_information.final_values.group_booking_status")
.value("DEF"))
.andExpect(jsonPath("$.business_cards[0].confirmed_payload.room_information.final_values.group_booking_status")
.value("DEF"));
performAuthorized(mockMvc, adminToken(), get("/api/reservation/order-tasks/{orderTaskId}/audits",
roomingList.orderTask().id()).param("hotel_id", HOTEL_ID))
.andExpect(status().isOk())
.andExpect(jsonPath("$.items[?(@.action=='V4_ROOMING_LIST_AUTO_DEF')].after_snapshot.update_status")
.value(contains("UPDATED")));
assertAuditCount("V4_ROOMING_LIST_AUTO_DEF", "v4-command-admin",
roomInformation.businessCard().id().toString(), "\"update_status\":\"UPDATED\"", 1);
}
@@ -980,6 +994,10 @@ class ReservationV4CommandControllerTest {
"version": 0,
"reason": "按稳定 Room Information 模型修正房型",
"field_overrides": [
{
"field_pointer": "/room_information/final_values/arrival_date",
"value": "2026-08-02"
},
{
"field_pointer": "/room_information/final_values/room_items/0/room_type_code",
"value": "RM2"
@@ -990,10 +1008,14 @@ class ReservationV4CommandControllerTest {
.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.arrival_date")
.value("2026-08-02"))
.andExpect(jsonPath("$.business_cards[0].confirmed_payload.room_information.final_values.nights")
.value(2))
.value(1))
.andExpect(jsonPath("$.business_cards[0].confirmed_payload.business_fields").doesNotExist())
.andExpect(jsonPath("$.business_cards[0].review_resolution.field_overrides[0].field_pointer")
.value("/room_information/final_values/arrival_date"))
.andExpect(jsonPath("$.business_cards[0].review_resolution.field_overrides[1].field_pointer")
.value("/room_information/final_values/room_items/0/room_type_code"));
}
@@ -1049,6 +1071,67 @@ class ReservationV4CommandControllerTest {
.andExpect(jsonPath("$.error_code").value("V4_REVIEW_POINTER_READONLY"));
}
@Test
void shouldRejectRoomInformationReviewWhenFitBreakfastIsDerivedFromRateCode() throws Exception {
seedRateCode(HOTEL_ID, 990000000000070199L, "FIT-RB");
SeededOrderTask seeded = seedReviewOrderTaskWithBusinessCard(
HOTEL_ID,
"mail-v4-command-review-room-info-derived-breakfast-001",
Instant.parse("2026-07-19T01:22:08Z"),
990000000000070106L,
ReservationV4CardStatus.PENDING_CONFIRM.name(),
ReservationV4CardStatus.REVIEW_REQUIRED.name(),
"""
{
"card_type":"ROOM_INFORMATION",
"event_type":"NEW_BOOKING",
"target_order":{"booking_type":"FIT","locator_type":"BOOKING_CODE","locator_value":"FIT-V4-REVIEW-RI-001"},
"business_fields":{
"event_type":"NEW_BOOKING",
"guest_name":"FIT REVIEW",
"arrival_date":"2026-08-01",
"departure_date":"2026-08-03",
"rate_code":"FIT-RB",
"room_items":[{"room_type_code":"TWN","room_count":1}]
}
}
""",
"""
[
{
"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()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.business_cards[0].display_payload.room_information.final_values.breakfast_included")
.value(true))
.andExpect(jsonPath("$.business_cards[0].fields[?(@.field_pointer=='/room_information/final_values/breakfast_included')]")
.doesNotExist());
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,
"field_overrides": [
{"field_pointer": "/room_information/final_values/breakfast_included", "value": false}
]
}
"""))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.error_code").value("V4_REVIEW_POINTER_NOT_ALLOWED"));
}
@Test
void shouldRejectReviewResolutionWhenResolvedOrderTaskRebindsToDifferentOrder() throws Exception {
Long currentOrderId = 990000000000070111L;
@@ -1577,6 +1660,20 @@ class ReservationV4CommandControllerTest {
.andExpect(status().isOk());
}
private void seedRateCode(String hotelId, Long id, String code) {
jdbcTemplate.update(
"""
INSERT INTO workflow_reservation_catalog_code (
id, hotel_id, catalog_type, code, display_name, status, source_system, external_id,
sort_order, catalog_version, last_synced_at, metadata_json, version,
created_at, updated_at, logic_deleted_at, logic_deleted_reason
) VALUES (?, ?, 'RATE_CODE', ?, ?, 'ACTIVE', 'SYSTEM_MANAGED', NULL,
990, 'test-review-derived-breakfast-v1', NULL, '{"pricing_available":false}', 0,
'2026-07-19 00:00:00.000000', '2026-07-19 00:00:00.000000', NULL, NULL)
""",
id, hotelId, code, code);
}
private void resolveBusinessReviewCard(SeededOrderTask seeded) throws Exception {
performAuthorized(mockMvc, adminToken(), post(
"/api/reservation/order-tasks/{orderTaskId}/cards/{cardId}/review-resolution",

View File

@@ -429,7 +429,7 @@ class ReservationV4QueryControllerTest {
}
@Test
void shouldOnlyMarkRoomInformationReviewProblemFieldsEditable() throws Exception {
void shouldMarkRoomInformationReviewBusinessWhitelistFieldsEditable() throws Exception {
ReservationV4OrderTaskSnapshot orderTask = seedOrderTaskWithBusinessPayload(
"mail-v4-room-info-review-fields-001",
Instant.parse("2026-07-18T03:02:30Z"),
@@ -470,11 +470,60 @@ class ReservationV4QueryControllerTest {
.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/arrival_date')].editable")
.value(contains(false)))
.value(contains(true)))
.andExpect(jsonPath("$.business_cards[0].fields[?(@.field_pointer=='/room_information/final_values/departure_date')].editable")
.value(contains(false)))
.value(contains(true)))
.andExpect(jsonPath("$.business_cards[0].fields[?(@.field_pointer=='/room_information/final_values/rate_code')].editable")
.value(contains(false)));
.value(contains(true)))
.andExpect(jsonPath("$.business_cards[0].fields[?(@.field_pointer=='/room_information/final_values/rate_code')].write_target")
.value(contains("review_resolution.field_overrides")))
.andExpect(jsonPath("$.business_cards[0].fields[?(@.field_pointer=='/room_information/final_values/nights')]")
.doesNotExist());
}
@Test
void shouldNotExposeBasicInformationTargetOrderInDisplayOrConfirmedPayload() throws Exception {
ReservationV4OrderTaskSnapshot orderTask = seedOrderTaskWithBusinessPayload(
"mail-v4-query-basic-target-order-safe-001",
Instant.parse("2026-07-18T03:02:40Z"),
"GROUP",
"GROUP_CODE",
"GRP-V4-BASIC-SAFE-001",
"NEW_BOOKING",
ReservationV4CardStatus.PENDING_CONFIRM.name(),
ReservationV4CardStatus.PENDING_CONFIRM.name(),
"""
{"card_type":"ROOM_INFORMATION","event_type":"NEW_BOOKING","room_items":[{"room_type_code":"TWN","room_count":2}]}
""",
null);
ReservationV4TaskCardSnapshot basicCard = workflowRepository.findTaskCardsByOrderTaskId(HOTEL_ID, orderTask.id())
.stream()
.filter(card -> ReservationV4CardType.BASIC_INFORMATION.name().equals(card.cardType()))
.findFirst()
.orElseThrow();
workflowRepository.confirmTaskCardWithVersion(
HOTEL_ID,
basicCard.id(),
basicCard.version(),
"""
{
"basic_information":{"account_code":"QBD_TRAVEL","market_code":"LEISURE","source_code":"TRAVEL_AGENT"},
"target_order":{"locator_value":"SHOULD-NOT-LEAK-CONFIRMED"}
}
""",
"v4-query-admin",
LocalDateTime.ofInstant(Instant.parse("2026-07-18T03:02:50Z"), ZoneOffset.UTC));
performAuthorized(mockMvc, adminToken(), get("/api/reservation/order-tasks/{orderTaskId}", orderTask.id())
.param("hotel_id", HOTEL_ID))
.andExpect(status().isOk())
.andExpect(jsonPath("$.basic_information_card.display_payload.basic_information.account_code")
.value("QBD_TRAVEL"))
.andExpect(jsonPath("$.basic_information_card.confirmed_payload.basic_information.account_code")
.value("QBD_TRAVEL"))
.andExpect(jsonPath("$.basic_information_card.display_payload.target_order").doesNotExist())
.andExpect(jsonPath("$.basic_information_card.confirmed_payload.target_order").doesNotExist())
.andExpect(content().string(not(containsString("SHOULD-NOT-LEAK"))));
}
@Test
@@ -1024,7 +1073,12 @@ class ReservationV4QueryControllerTest {
""");
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"}}
{
"card_type":"BASIC_INFORMATION",
"order_ref":"order-1",
"target_order":{"locator_value":"SHOULD-NOT-LEAK-DISPLAY"},
"basic_information":{"account_code":"QBD_TRAVEL","market_code":"LEISURE","source_code":"TRAVEL_AGENT"}
}
""");
ReservationV4TaskCardSnapshot businessCard = insertCard(
orderTask,