完成 V4 目录校验和字段白名单
This commit is contained in:
@@ -149,7 +149,10 @@ class ReservationV4CommandControllerTest {
|
||||
{
|
||||
"version": 0,
|
||||
"confirmed_payload": {
|
||||
"company": "Q.B.D. TRAVEL GROUP CO., LTD",
|
||||
"card_type": "BASIC_INFORMATION",
|
||||
"basic_information": {
|
||||
"account_code": "QBD_TRAVEL"
|
||||
},
|
||||
"group_code": "GRP-V4-COMMAND-001"
|
||||
}
|
||||
}
|
||||
@@ -159,6 +162,10 @@ class ReservationV4CommandControllerTest {
|
||||
.andExpect(jsonPath("$.basic_information_card.confirmed_by").value("v4-command-admin"))
|
||||
.andExpect(jsonPath("$.basic_information_card.confirmed_payload.group_code")
|
||||
.value("GRP-V4-COMMAND-001"))
|
||||
.andExpect(jsonPath("$.basic_information_card.confirmed_payload.basic_information.market_code")
|
||||
.value("LEISURE"))
|
||||
.andExpect(jsonPath("$.basic_information_card.confirmed_payload.basic_information.source_code")
|
||||
.value("TRAVEL_AGENT"))
|
||||
.andExpect(jsonPath("$.business_cards[0].availability.confirmable").value(true))
|
||||
.andExpect(jsonPath("$.business_cards[0].availability.read_only").value(false))
|
||||
.andExpect(content().string(not(org.hamcrest.Matchers.containsString("private.example.test"))));
|
||||
@@ -185,6 +192,33 @@ class ReservationV4CommandControllerTest {
|
||||
assertAuditCount("V4_CARD_CONFIRM", "v4-command-admin", seeded.orderTask().id().toString(), 2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectBasicInformationConfirmWhenAccountCodeUnknown() throws Exception {
|
||||
SeededOrderTask seeded = seedOrderTask(
|
||||
HOTEL_ID,
|
||||
"mail-v4-command-confirm-unknown-account-001",
|
||||
Instant.parse("2026-07-19T01:12:00Z"));
|
||||
|
||||
performAuthorized(mockMvc, adminToken(), post("/api/reservation/order-tasks/{orderTaskId}/cards/{cardId}/confirm",
|
||||
seeded.orderTask().id(),
|
||||
seeded.basicCard().id())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"version": 0,
|
||||
"confirmed_payload": {
|
||||
"card_type": "BASIC_INFORMATION",
|
||||
"basic_information": {
|
||||
"account_code": "UNKNOWN_ACCOUNT"
|
||||
}
|
||||
}
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.error_code").value("V4_FIELD_VALIDATION_FAILED"))
|
||||
.andExpect(jsonPath("$.details[0]").value("basic_information.account_code: Account Code 不在信息系统目录中。"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectRepeatedCardConfirm() throws Exception {
|
||||
SeededOrderTask seeded = seedOrderTask(
|
||||
@@ -244,12 +278,44 @@ class ReservationV4CommandControllerTest {
|
||||
.andExpect(jsonPath("$.basic_information_card.review_status").value("RESOLVED"))
|
||||
.andExpect(jsonPath("$.basic_information_card.confirmed_by").value("v4-command-admin"))
|
||||
.andExpect(jsonPath("$.basic_information_card.confirmed_payload.basic_information.account_code").value("QBD_TRAVEL"))
|
||||
.andExpect(jsonPath("$.basic_information_card.confirmed_payload.basic_information.market_code").value("LEISURE"))
|
||||
.andExpect(jsonPath("$.basic_information_card.confirmed_payload.basic_information.source_code").value("TRAVEL_AGENT"))
|
||||
.andExpect(jsonPath("$.basic_information_card.review_resolution.field_overrides[0].field_pointer")
|
||||
.value("/basic_information/account_code"))
|
||||
.andExpect(jsonPath("$.business_cards[0].availability.confirmable").value(true));
|
||||
assertAuditCount("V4_CARD_REVIEW_RESOLVE", "v4-command-admin", seeded.orderTask().id().toString(), 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectBasicInformationReviewWhenAccountCodeUnknown() throws Exception {
|
||||
Long confirmedOrderId = 990000000000070012L;
|
||||
seedReservationOrder(HOTEL_ID, confirmedOrderId);
|
||||
SeededOrderTask seeded = seedReviewOrderTask(
|
||||
HOTEL_ID,
|
||||
"mail-v4-command-review-unknown-account-001",
|
||||
Instant.parse("2026-07-19T01:21:10Z"),
|
||||
null,
|
||||
ReservationV4CardStatus.REVIEW_REQUIRED.name(),
|
||||
ReservationV4CardStatus.PENDING_CONFIRM.name());
|
||||
|
||||
performAuthorized(mockMvc, adminToken(), post(
|
||||
"/api/reservation/order-tasks/{orderTaskId}/cards/{cardId}/review-resolution",
|
||||
seeded.orderTask().id(),
|
||||
seeded.basicCard().id())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"version": 0,
|
||||
"confirmed_order_id": "990000000000070012",
|
||||
"field_overrides": [
|
||||
{"field_pointer": "/basic_information/account_code", "value": "UNKNOWN_ACCOUNT"}
|
||||
]
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.error_code").value("V4_FIELD_VALIDATION_FAILED"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolveOrderOwnershipStatusWhenConfirmedOrderIdAlreadyBound() throws Exception {
|
||||
Long confirmedOrderId = 990000000000070009L;
|
||||
@@ -319,6 +385,54 @@ class ReservationV4CommandControllerTest {
|
||||
.andExpect(jsonPath("$.business_cards[0].review_resolution.reason").value("确认房型映射"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolveBusinessCardDirectoryValidationError() throws Exception {
|
||||
SeededOrderTask seeded = seedReviewOrderTaskWithBusinessCard(
|
||||
HOTEL_ID,
|
||||
"mail-v4-command-review-directory-001",
|
||||
Instant.parse("2026-07-19T01:22:05Z"),
|
||||
990000000000070004L,
|
||||
ReservationV4CardStatus.PENDING_CONFIRM.name(),
|
||||
ReservationV4CardStatus.REVIEW_REQUIRED.name(),
|
||||
"""
|
||||
{"event_type":"NEW_BOOKING","route_code":"S01","business_fields":{"order_ref":"ORDER-REVIEW","event_type":"NEW_BOOKING","manual_review":true,"room_items":[{"room_type_code":"UNKNOWN_TYPE","room_count":2}]}}
|
||||
""",
|
||||
"""
|
||||
[
|
||||
{
|
||||
"field_path": "business_fields.room_items.0.room_type_code",
|
||||
"field_pointer": "/business_fields/room_items/0/room_type_code",
|
||||
"message": "房型代码不在第一版目录中。",
|
||||
"detail": "business_fields.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": "/business_fields/room_items/0/room_type_code",
|
||||
"value": "TWN"
|
||||
}
|
||||
]
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.business_cards[0].card_status").value("CONFIRMED"))
|
||||
.andExpect(jsonPath("$.business_cards[0].confirmed_payload.business_fields.room_items[0].room_type_code")
|
||||
.value("TWN"))
|
||||
.andExpect(jsonPath("$.business_cards[0].review_resolution.reason").value("修正房型目录"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectReviewResolutionWhenResolvedOrderTaskRebindsToDifferentOrder() throws Exception {
|
||||
Long currentOrderId = 990000000000070111L;
|
||||
@@ -805,7 +919,13 @@ class ReservationV4CommandControllerTest {
|
||||
.content("""
|
||||
{
|
||||
"version": 0,
|
||||
"confirmed_payload": {"group_code": "GRP-V4-COMMAND-001"}
|
||||
"confirmed_payload": {
|
||||
"card_type": "BASIC_INFORMATION",
|
||||
"basic_information": {
|
||||
"account_code": "QBD_TRAVEL"
|
||||
},
|
||||
"group_code": "GRP-V4-COMMAND-001"
|
||||
}
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isOk());
|
||||
@@ -865,7 +985,7 @@ class ReservationV4CommandControllerTest {
|
||||
ReservationV4TaskCardSnapshot basicCard = insertCard(orderTask, hotelId,
|
||||
ReservationV4CardType.BASIC_INFORMATION.name(), null, 0, 20,
|
||||
ReservationV4CardStatus.PENDING_CONFIRM.name(), null, """
|
||||
{"card_type":"BASIC_INFORMATION","company":"Q.B.D. TRAVEL GROUP CO., LTD","group_code":"GRP-V4-COMMAND-001"}
|
||||
{"card_type":"BASIC_INFORMATION","order_ref":"ORDER-COMMAND","basic_information":{"account_code":"QBD_TRAVEL","manual_review":null}}
|
||||
""");
|
||||
ReservationV4TaskCardSnapshot businessCard = insertCard(orderTask, hotelId,
|
||||
ReservationV4CardType.ROOM_INFORMATION.name(), "NEW_BOOKING", 1, 30,
|
||||
@@ -902,6 +1022,53 @@ class ReservationV4CommandControllerTest {
|
||||
String targetResolutionStatus,
|
||||
String basicStatus,
|
||||
String businessStatus) {
|
||||
return seedReviewOrderTaskWithBusinessCard(
|
||||
hotelId,
|
||||
externalMessageId,
|
||||
receivedAt,
|
||||
orderId,
|
||||
targetResolutionStatus,
|
||||
basicStatus,
|
||||
businessStatus,
|
||||
"""
|
||||
{"event_type":"NEW_BOOKING","route_code":"S01","business_fields":{"order_ref":"ORDER-REVIEW","event_type":"NEW_BOOKING","manual_review":true,"room_items":[{"room_type_code":"TWN","room_count":2,"pms_room_type_code":null}]}}
|
||||
""",
|
||||
null);
|
||||
}
|
||||
|
||||
private SeededOrderTask seedReviewOrderTaskWithBusinessCard(
|
||||
String hotelId,
|
||||
String externalMessageId,
|
||||
Instant receivedAt,
|
||||
Long orderId,
|
||||
String basicStatus,
|
||||
String businessStatus,
|
||||
String businessDisplayPayloadJson,
|
||||
String businessValidationErrorsJson) {
|
||||
return seedReviewOrderTaskWithBusinessCard(
|
||||
hotelId,
|
||||
externalMessageId,
|
||||
receivedAt,
|
||||
orderId,
|
||||
orderId == null
|
||||
? ReservationV4TargetResolutionStatus.UNRESOLVED.name()
|
||||
: ReservationV4TargetResolutionStatus.RESOLVED.name(),
|
||||
basicStatus,
|
||||
businessStatus,
|
||||
businessDisplayPayloadJson,
|
||||
businessValidationErrorsJson);
|
||||
}
|
||||
|
||||
private SeededOrderTask seedReviewOrderTaskWithBusinessCard(
|
||||
String hotelId,
|
||||
String externalMessageId,
|
||||
Instant receivedAt,
|
||||
Long orderId,
|
||||
String targetResolutionStatus,
|
||||
String basicStatus,
|
||||
String businessStatus,
|
||||
String businessDisplayPayloadJson,
|
||||
String businessValidationErrorsJson) {
|
||||
SourceMessageCaptureResult source = captureSourceMessage(
|
||||
hotelId,
|
||||
externalMessageId,
|
||||
@@ -934,9 +1101,7 @@ class ReservationV4CommandControllerTest {
|
||||
""");
|
||||
ReservationV4TaskCardSnapshot businessCard = insertCard(orderTask, hotelId,
|
||||
ReservationV4CardType.ROOM_INFORMATION.name(), "NEW_BOOKING", 1, 30,
|
||||
businessStatus, reviewStatusFor(businessStatus), """
|
||||
{"event_type":"NEW_BOOKING","route_code":"S01","business_fields":{"order_ref":"ORDER-REVIEW","event_type":"NEW_BOOKING","manual_review":true,"room_items":[{"room_type_code":"TWN","room_count":2,"pms_room_type_code":null}]}}
|
||||
""");
|
||||
businessStatus, reviewStatusFor(businessStatus), businessDisplayPayloadJson, businessValidationErrorsJson);
|
||||
return new SeededOrderTask(orderTask, sourceCard, basicCard, businessCard);
|
||||
}
|
||||
|
||||
@@ -980,6 +1145,21 @@ class ReservationV4CommandControllerTest {
|
||||
String cardStatus,
|
||||
String reviewStatus,
|
||||
String displayPayloadJson) {
|
||||
return insertCard(orderTask, hotelId, cardType, eventType, sourceEventIndex, sortOrder, cardStatus, reviewStatus,
|
||||
displayPayloadJson, null);
|
||||
}
|
||||
|
||||
private ReservationV4TaskCardSnapshot insertCard(
|
||||
ReservationV4OrderTaskSnapshot orderTask,
|
||||
String hotelId,
|
||||
String cardType,
|
||||
String eventType,
|
||||
Integer sourceEventIndex,
|
||||
Integer sortOrder,
|
||||
String cardStatus,
|
||||
String reviewStatus,
|
||||
String displayPayloadJson,
|
||||
String validationErrorsJson) {
|
||||
return workflowRepository.insertTaskCard(new ReservationV4TaskCardDraft(
|
||||
hotelId,
|
||||
orderTask.id(),
|
||||
@@ -995,7 +1175,7 @@ class ReservationV4CommandControllerTest {
|
||||
{"private_url":"https://private.example.test/raw","raw":"must stay internal"}
|
||||
""",
|
||||
displayPayloadJson,
|
||||
null,
|
||||
validationErrorsJson,
|
||||
orderTask.createdAt()));
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ 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.matchesPattern;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
@@ -240,8 +241,20 @@ class ReservationV4QueryControllerTest {
|
||||
.andExpect(jsonPath("$.order_task.target_locator_value").value("GRP-V4-QUERY-001"))
|
||||
.andExpect(jsonPath("$.source_message_card.card_type").value("SOURCE_MESSAGE_DISPLAY"))
|
||||
.andExpect(jsonPath("$.basic_information_card.card_type").value("BASIC_INFORMATION"))
|
||||
.andExpect(jsonPath("$.basic_information_card.fields[?(@.field_pointer=='/basic_information/account_code')].control_type")
|
||||
.value(contains("select")))
|
||||
.andExpect(jsonPath("$.basic_information_card.fields[?(@.field_pointer=='/basic_information/account_code')].options_source")
|
||||
.value(contains("reservation_v4_account_catalog")))
|
||||
.andExpect(jsonPath("$.basic_information_card.fields[?(@.field_pointer=='/basic_information/market_code')].raw_readonly")
|
||||
.value(contains(true)))
|
||||
.andExpect(jsonPath("$.basic_information_card.fields[?(@.field_pointer=='/basic_information/source_code')].raw_readonly")
|
||||
.value(contains(true)))
|
||||
.andExpect(jsonPath("$.business_cards[0].card_type").value("ROOM_INFORMATION"))
|
||||
.andExpect(jsonPath("$.business_cards[0].display_payload.event_type").value("NEW_BOOKING"))
|
||||
.andExpect(jsonPath("$.business_cards[0].fields[?(@.field_pointer=='/room_items/0/room_type_code')].control_type")
|
||||
.value(contains("select")))
|
||||
.andExpect(jsonPath("$.business_cards[0].fields[?(@.field_pointer=='/room_items/0/room_count')].control_type")
|
||||
.value(contains("number")))
|
||||
.andExpect(jsonPath("$.business_cards[0].ai_payload_json").doesNotExist())
|
||||
.andExpect(jsonPath("$.availability.read_only").value(false))
|
||||
.andExpect(jsonPath("$.availability.confirmable").value(true))
|
||||
@@ -488,7 +501,7 @@ class ReservationV4QueryControllerTest {
|
||||
""");
|
||||
insertCard(orderTask, ReservationV4CardType.BASIC_INFORMATION.name(), null, 0, 20,
|
||||
ReservationV4CardStatus.PENDING_CONFIRM.name(), null, """
|
||||
{"card_type":"BASIC_INFORMATION","account_code":"QBD_TRAVEL"}
|
||||
{"card_type":"BASIC_INFORMATION","order_ref":"order-1","basic_information":{"account_code":"QBD_TRAVEL","market_code":"LEISURE","source_code":"TRAVEL_AGENT"}}
|
||||
""");
|
||||
insertCard(orderTask, ReservationV4CardType.ROOM_INFORMATION.name(), "NEW_BOOKING", 1, 30,
|
||||
ReservationV4CardStatus.PENDING_CONFIRM.name(), null, """
|
||||
|
||||
@@ -1206,6 +1206,56 @@ class SuperAgentTaskResultControllerTest {
|
||||
assertThat(basicReviewCount).isEqualTo(1L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMarkV4BasicInformationReviewRequiredWhenAccountCodeUnknown() throws Exception {
|
||||
SourceMessageCaptureResult source = captureSourceMessage("mail-v4-unknown-account-001");
|
||||
String body = v4BusinessRootBody("mail-v4-unknown-account-001")
|
||||
.replace("\"account_code\": \"QBD_TRAVEL\"", "\"account_code\": \"UNKNOWN_ACCOUNT\"")
|
||||
.replace("GRP-V4-001", "GRP-V4-UNKNOWN-ACCOUNT-001");
|
||||
|
||||
mockMvc.perform(signedPost(body, "nonce-v4-unknown-account-001"))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.accepted_count").value(2));
|
||||
|
||||
String basicValidationErrors = jdbcTemplate.queryForObject("""
|
||||
SELECT validation_errors_json
|
||||
FROM workflow_reservation_v4_task_card
|
||||
WHERE source_message_id = ?
|
||||
AND card_type = 'BASIC_INFORMATION'
|
||||
AND card_status = 'REVIEW_REQUIRED'
|
||||
AND review_status = 'PENDING'
|
||||
LIMIT 1
|
||||
""", String.class, source.inboxId());
|
||||
assertThat(basicValidationErrors)
|
||||
.contains("basic_information.account_code")
|
||||
.contains("Account Code 不在信息系统目录中");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMarkV4BusinessCardReviewRequiredWhenRoomTypeCodeUnknown() throws Exception {
|
||||
SourceMessageCaptureResult source = captureSourceMessage("mail-v4-unknown-room-type-001");
|
||||
String body = v4BusinessRootBody("mail-v4-unknown-room-type-001")
|
||||
.replace("\"room_type_code\": \"TWN\"", "\"room_type_code\": \"UNKNOWN_ROOM\"")
|
||||
.replace("GRP-V4-001", "GRP-V4-UNKNOWN-ROOM-001");
|
||||
|
||||
mockMvc.perform(signedPost(body, "nonce-v4-unknown-room-type-001"))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.accepted_count").value(2));
|
||||
|
||||
String roomValidationErrors = jdbcTemplate.queryForObject("""
|
||||
SELECT validation_errors_json
|
||||
FROM workflow_reservation_v4_task_card
|
||||
WHERE source_message_id = ?
|
||||
AND card_type = 'ROOM_INFORMATION'
|
||||
AND card_status = 'REVIEW_REQUIRED'
|
||||
AND review_status = 'PENDING'
|
||||
LIMIT 1
|
||||
""", String.class, source.inboxId());
|
||||
assertThat(roomValidationErrors)
|
||||
.contains("business_fields.room_items.0.room_type_code")
|
||||
.contains("房型代码不在第一版目录中");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateV4CancelTraceAndRoomingListTasksInEventOrder() throws Exception {
|
||||
SourceMessageCaptureResult source = captureSourceMessage("mail-v4-cancel-trace-rooming-001");
|
||||
|
||||
Reference in New Issue
Block a user