修复V4 Trace确认态字段刷新
This commit is contained in:
@@ -20,6 +20,7 @@ import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationV4Notific
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationV4OrderTaskStatus;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationV4ReadonlyReasonCode;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationV4WorkbenchItemType;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationReviewStatus;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationV4OrderTaskQueryRequest;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationV4SourceNotificationQueryRequest;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationV4WorkbenchQueryRequest;
|
||||
@@ -426,6 +427,8 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
|
||||
JsonNode reviewResolution = parseJson(card.reviewResolutionJson());
|
||||
JsonNode validationErrors = parseJson(card.validationErrorsJson());
|
||||
ReservationV4ActionAvailabilityResult availability = cardAvailability(card, orderAvailability, basicCard);
|
||||
JsonNode responseValidationErrors = validationErrorsForCardResponse(card, confirmedPayload, validationErrors);
|
||||
JsonNode fieldsPayload = fieldsPayloadForCard(card, displayPayload, confirmedPayload);
|
||||
return new ReservationV4TaskCardResult(
|
||||
card.id().toString(),
|
||||
card.cardType(),
|
||||
@@ -437,8 +440,8 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
|
||||
displayPayload,
|
||||
confirmedPayload,
|
||||
reviewResolution,
|
||||
validationErrors,
|
||||
cardFields(card, displayPayload, validationErrors, availability),
|
||||
responseValidationErrors,
|
||||
cardFields(card, fieldsPayload, responseValidationErrors, availability),
|
||||
card.confirmedBy(),
|
||||
UtcTimeFormatter.toUtcOffsetDateTime(card.confirmedAt()),
|
||||
card.version(),
|
||||
@@ -447,6 +450,49 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
|
||||
availability);
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择字段展示的数据源;Trace 确认后以用户确认快照为准,避免刷新时回到 AI 原始建议。
|
||||
*/
|
||||
private JsonNode fieldsPayloadForCard(
|
||||
ReservationV4TaskCardSnapshot card,
|
||||
JsonNode displayPayload,
|
||||
JsonNode confirmedPayload) {
|
||||
if (shouldUseTraceConfirmedPayload(card, confirmedPayload)) {
|
||||
return confirmedPayload;
|
||||
}
|
||||
return displayPayload;
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择响应校验错误;Trace 确认或复核成功后旧入站错误已经被解消,前端不再展示。
|
||||
*/
|
||||
private JsonNode validationErrorsForCardResponse(
|
||||
ReservationV4TaskCardSnapshot card,
|
||||
JsonNode confirmedPayload,
|
||||
JsonNode validationErrors) {
|
||||
if (shouldUseTraceConfirmedPayload(card, confirmedPayload)) {
|
||||
return objectMapper.createArrayNode();
|
||||
}
|
||||
return validationErrors;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断 Trace 卡是否已经有可用于刷新字段展示的安全确认快照。
|
||||
*/
|
||||
private boolean shouldUseTraceConfirmedPayload(ReservationV4TaskCardSnapshot card, JsonNode confirmedPayload) {
|
||||
if (!ReservationV4CardType.TRACE_RESERVATION_NOTES.name().equals(card.cardType())) {
|
||||
return false;
|
||||
}
|
||||
if (confirmedPayload == null || confirmedPayload.isNull() || confirmedPayload.isMissingNode()) {
|
||||
return false;
|
||||
}
|
||||
if (!confirmedPayload.path("trace_items").isArray()) {
|
||||
return false;
|
||||
}
|
||||
return ReservationV4CardStatus.CONFIRMED.name().equals(card.cardStatus())
|
||||
|| ReservationReviewStatus.RESOLVED.name().equals(card.reviewStatus());
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成卡片安全展示 payload;Room Information 卡在这里补稳定业务展示模型。
|
||||
*/
|
||||
|
||||
@@ -788,6 +788,24 @@ class ReservationV4CommandControllerTest {
|
||||
.andExpect(jsonPath("$.business_cards[0].confirmed_payload.trace_items[1].attachment_url").doesNotExist())
|
||||
.andExpect(content().string(not(containsString("SHOULD-NOT-PERSIST"))))
|
||||
.andExpect(content().string(not(containsString("https://oss.example.test"))));
|
||||
|
||||
MvcResult refreshedResult = performAuthorized(mockMvc, adminToken(),
|
||||
get("/api/reservation/order-tasks/{orderTaskId}", seeded.orderTask().id())
|
||||
.param("hotel_id", HOTEL_ID))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn();
|
||||
JsonNode refreshedDetail = objectMapper.readTree(refreshedResult.getResponse().getContentAsString());
|
||||
assertTraceFieldValue(refreshedDetail, "/trace_items/0/text", "Updated trace");
|
||||
assertTraceFieldValue(refreshedDetail, "/trace_items/0/department_code", "HSK");
|
||||
assertTraceFieldValue(refreshedDetail, "/trace_items/1/target_room_type_code", "RM3");
|
||||
assertTraceFieldValue(refreshedDetail, "/trace_items/1/extra_bed_room_count", 2);
|
||||
assertTraceFieldValidationErrorsEmpty(refreshedDetail, "/trace_items/0/text");
|
||||
org.assertj.core.api.Assertions.assertThat(refreshedDetail.path("business_cards").get(0)
|
||||
.path("validation_errors").isArray())
|
||||
.isTrue();
|
||||
org.assertj.core.api.Assertions.assertThat(refreshedDetail.path("business_cards").get(0)
|
||||
.path("validation_errors").size())
|
||||
.isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -1570,6 +1588,107 @@ class ReservationV4CommandControllerTest {
|
||||
.andExpect(jsonPath("$.business_cards[0].confirmed_payload.target_order").doesNotExist());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRefreshExtraBedTraceFieldsFromConfirmedPayloadAfterReviewResolution() throws Exception {
|
||||
SeededOrderTask seeded = seedReviewOrderTaskWithBusinessCard(
|
||||
HOTEL_ID,
|
||||
"mail-v4-command-review-trace-extra-bed-refresh-001",
|
||||
Instant.parse("2026-07-19T01:22:29Z"),
|
||||
990000000000070127L,
|
||||
ReservationV4TargetResolutionStatus.RESOLVED.name(),
|
||||
ReservationV4CardStatus.PENDING_CONFIRM.name(),
|
||||
ReservationV4CardStatus.REVIEW_REQUIRED.name(),
|
||||
ReservationV4CardType.TRACE_RESERVATION_NOTES.name(),
|
||||
"TRACE_RESERVATION_NOTES",
|
||||
"""
|
||||
{
|
||||
"card_type":"TRACE_RESERVATION_NOTES",
|
||||
"event_type":"TRACE_RESERVATION_NOTES",
|
||||
"target_order":{"locator_value":"SHOULD-NOT-PERSIST-TRACE"},
|
||||
"business_fields":{
|
||||
"event_type":"TRACE_RESERVATION_NOTES",
|
||||
"trace_items":[
|
||||
{
|
||||
"item_type":"EXTRA_BED",
|
||||
"target_room_type_code":"UNKNOWN_ROOM",
|
||||
"extra_bed_room_count":0,
|
||||
"department_code":"SALES",
|
||||
"content":"SHOULD-NOT-PERSIST-CONTENT",
|
||||
"attachment_url":"https://oss.example.test/trace-review-secret.png"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
""",
|
||||
"""
|
||||
[
|
||||
{
|
||||
"field_path": "trace_items.0.target_room_type_code",
|
||||
"field_pointer": "/trace_items/0/target_room_type_code",
|
||||
"message": "房型代码不在第一版目录中。",
|
||||
"detail": "trace_items.0.target_room_type_code: 房型代码不在第一版目录中。"
|
||||
},
|
||||
{
|
||||
"field_path": "trace_items.0.extra_bed_room_count",
|
||||
"field_pointer": "/trace_items/0/extra_bed_room_count",
|
||||
"message": "加床房间数必须为正整数。",
|
||||
"detail": "trace_items.0.extra_bed_room_count: 加床房间数必须为正整数。"
|
||||
},
|
||||
{
|
||||
"field_path": "trace_items.0.department_code",
|
||||
"field_pointer": "/trace_items/0/department_code",
|
||||
"message": "Department Code 只允许 FO、HSK、FO+HSK。",
|
||||
"detail": "trace_items.0.department_code: Department Code 只允许 FO、HSK、FO+HSK。"
|
||||
}
|
||||
]
|
||||
""");
|
||||
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": "修正 Trace Extra Bed 字段",
|
||||
"field_overrides": [
|
||||
{"field_pointer": "/trace_items/0/target_room_type_code", "value": "RM2"},
|
||||
{"field_pointer": "/trace_items/0/extra_bed_room_count", "value": 2},
|
||||
{"field_pointer": "/trace_items/0/department_code", "value": "FO+HSK"}
|
||||
]
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.business_cards[0].card_status").value("CONFIRMED"))
|
||||
.andExpect(jsonPath("$.business_cards[0].confirmed_payload.trace_items[0].target_room_type_code")
|
||||
.value("RM2"))
|
||||
.andExpect(jsonPath("$.business_cards[0].confirmed_payload.trace_items[0].extra_bed_room_count")
|
||||
.value(2))
|
||||
.andExpect(jsonPath("$.business_cards[0].confirmed_payload.trace_items[0].department_code")
|
||||
.value("FO+HSK"));
|
||||
|
||||
MvcResult refreshedResult = performAuthorized(mockMvc, adminToken(),
|
||||
get("/api/reservation/order-tasks/{orderTaskId}", seeded.orderTask().id())
|
||||
.param("hotel_id", HOTEL_ID))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn();
|
||||
JsonNode refreshedDetail = objectMapper.readTree(refreshedResult.getResponse().getContentAsString());
|
||||
assertTraceFieldValue(refreshedDetail, "/trace_items/0/target_room_type_code", "RM2");
|
||||
assertTraceFieldValue(refreshedDetail, "/trace_items/0/extra_bed_room_count", 2);
|
||||
assertTraceFieldValue(refreshedDetail, "/trace_items/0/department_code", "FO+HSK");
|
||||
assertTraceFieldValidationErrorsEmpty(refreshedDetail, "/trace_items/0/target_room_type_code");
|
||||
assertTraceFieldValidationErrorsEmpty(refreshedDetail, "/trace_items/0/extra_bed_room_count");
|
||||
assertTraceFieldValidationErrorsEmpty(refreshedDetail, "/trace_items/0/department_code");
|
||||
org.assertj.core.api.Assertions.assertThat(refreshedDetail.path("business_cards").get(0)
|
||||
.path("validation_errors").isArray())
|
||||
.isTrue();
|
||||
org.assertj.core.api.Assertions.assertThat(refreshedDetail.path("business_cards").get(0)
|
||||
.path("validation_errors").size())
|
||||
.isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolveEditableRoomInformationPointerWhenLeafMissingInPayload() throws Exception {
|
||||
SeededOrderTask seeded = seedReviewOrderTaskWithBusinessCard(
|
||||
@@ -2430,6 +2549,44 @@ class ReservationV4CommandControllerTest {
|
||||
return objectMapper.getNodeFactory().textNode("UPDATED");
|
||||
}
|
||||
|
||||
private void assertTraceFieldValue(JsonNode detail, String pointer, String expectedValue) {
|
||||
JsonNode field = traceField(detail, pointer);
|
||||
org.assertj.core.api.Assertions.assertThat(field.path("value").asText())
|
||||
.as(pointer + " 刷新后的字段值")
|
||||
.isEqualTo(expectedValue);
|
||||
}
|
||||
|
||||
private void assertTraceFieldValue(JsonNode detail, String pointer, int expectedValue) {
|
||||
JsonNode field = traceField(detail, pointer);
|
||||
org.assertj.core.api.Assertions.assertThat(field.path("value").asInt())
|
||||
.as(pointer + " 刷新后的字段值")
|
||||
.isEqualTo(expectedValue);
|
||||
}
|
||||
|
||||
private void assertTraceFieldValidationErrorsEmpty(JsonNode detail, String pointer) {
|
||||
JsonNode field = traceField(detail, pointer);
|
||||
org.assertj.core.api.Assertions.assertThat(field.path("validation_errors").isArray())
|
||||
.as(pointer + " 字段错误集合")
|
||||
.isTrue();
|
||||
org.assertj.core.api.Assertions.assertThat(field.path("validation_errors").size())
|
||||
.as(pointer + " 字段错误数量")
|
||||
.isZero();
|
||||
}
|
||||
|
||||
private JsonNode traceField(JsonNode detail, String pointer) {
|
||||
for (JsonNode card : detail.path("business_cards")) {
|
||||
if (!ReservationV4CardType.TRACE_RESERVATION_NOTES.name().equals(card.path("card_type").asText())) {
|
||||
continue;
|
||||
}
|
||||
for (JsonNode field : card.path("fields")) {
|
||||
if (pointer.equals(field.path("field_pointer").asText())) {
|
||||
return field;
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new AssertionError("未找到 Trace 字段:" + pointer);
|
||||
}
|
||||
|
||||
private void confirmRoomInformationBusinessCard(SeededOrderTask seeded, String groupBookingStatus) throws Exception {
|
||||
String groupBookingStatusFragment = groupBookingStatus == null
|
||||
? ""
|
||||
|
||||
Reference in New Issue
Block a user