修复V4复核指针运行时白名单一致性

This commit is contained in:
andy
2026-07-21 12:16:19 +07:00
parent f228c2b704
commit 4bf53768ad
7 changed files with 366 additions and 76 deletions

View File

@@ -34,6 +34,10 @@ import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationV4TargetR
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationReviewStatus;
import cn.nianxx.thhotel.workflows.reservation.repository.ReservationV4SourceNotificationRepository;
import cn.nianxx.thhotel.workflows.reservation.repository.ReservationV4WorkflowRepository;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
@@ -47,6 +51,7 @@ import org.springframework.http.MediaType;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
@SpringBootTest(
classes = ThHotelApplication.class,
@@ -91,6 +96,9 @@ class ReservationV4CommandControllerTest {
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private ObjectMapper objectMapper;
private String adminToken;
private String noPermissionToken;
@@ -1091,6 +1099,86 @@ class ReservationV4CommandControllerTest {
.value("/room_information/final_values/room_items/0/room_type_code"));
}
@Test
void shouldAcceptEditableReviewFieldPointersReturnedByTaskDetail() throws Exception {
SeededOrderTask seeded = seedReviewOrderTaskWithBusinessCard(
HOTEL_ID,
"mail-v4-command-review-pointer-runtime-001",
Instant.parse("2026-07-19T01:22:18Z"),
990000000000070116L,
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-RUNTIME-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);
MvcResult detailResult = performAuthorized(mockMvc, adminToken(),
get("/api/reservation/order-tasks/{orderTaskId}", seeded.orderTask().id())
.param("hotel_id", HOTEL_ID))
.andExpect(status().isOk())
.andReturn();
JsonNode detail = objectMapper.readTree(detailResult.getResponse().getContentAsString());
ArrayNode overrides = objectMapper.createArrayNode();
for (JsonNode field : detail.path("business_cards").get(0).path("fields")) {
if (!field.path("editable").asBoolean(false)
|| !"review_resolution.field_overrides".equals(field.path("write_target").asText())) {
continue;
}
String pointer = field.path("field_pointer").asText();
ObjectNode override = objectMapper.createObjectNode();
override.put("field_pointer", pointer);
override.set("value", reviewOverrideValue(pointer));
overrides.add(override);
}
org.assertj.core.api.Assertions.assertThat(overrides)
.as("任务详情返回的可复核字段必须能直接提交给 review-resolution")
.isNotEmpty();
org.assertj.core.api.Assertions.assertThat(overrides.toString())
.contains("/room_information/final_values/room_items/0/room_type_code");
ObjectNode request = objectMapper.createObjectNode();
request.put("version", 0);
request.put("reason", "回归验证详情页可编辑 pointer 与命令侧白名单一致");
request.set("field_overrides", 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(objectMapper.writeValueAsString(request)))
.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"));
}
@Test
void shouldResolveRoomInformationReviewWhenDisplayPayloadAlreadyUsesStableModel() throws Exception {
SeededOrderTask seeded = seedReviewOrderTaskWithBusinessCard(
@@ -1770,6 +1858,34 @@ class ReservationV4CommandControllerTest {
.andExpect(status().isOk());
}
private JsonNode reviewOverrideValue(String pointer) {
if (pointer.endsWith("/group_block_name")) {
return objectMapper.getNodeFactory().textNode("GRP-V4-REVIEW-RUNTIME-001");
}
if (pointer.endsWith("/arrival_date")) {
return objectMapper.getNodeFactory().textNode("2026-08-01");
}
if (pointer.endsWith("/departure_date")) {
return objectMapper.getNodeFactory().textNode("2026-08-03");
}
if (pointer.endsWith("/rate_code")) {
return objectMapper.getNodeFactory().textNode("GROUP");
}
if (pointer.endsWith("/room_type_code")) {
return objectMapper.getNodeFactory().textNode("RM2");
}
if (pointer.endsWith("/room_count")) {
return objectMapper.getNodeFactory().numberNode(2);
}
if (pointer.endsWith("/group_booking_status")) {
return objectMapper.getNodeFactory().textNode("TEN");
}
if (pointer.endsWith("/breakfast_included")) {
return objectMapper.getNodeFactory().booleanNode(true);
}
return objectMapper.getNodeFactory().textNode("UPDATED");
}
private void confirmRoomInformationBusinessCard(SeededOrderTask seeded, String groupBookingStatus) throws Exception {
String groupBookingStatusFragment = groupBookingStatus == null
? ""