实现Rooming List确认自动置DEF

This commit is contained in:
andy
2026-07-21 09:16:05 +07:00
parent 8d181ee8ad
commit b44ab94113
5 changed files with 558 additions and 11 deletions

View File

@@ -194,6 +194,199 @@ class ReservationV4CommandControllerTest {
assertAuditCount("V4_CARD_CONFIRM", "v4-command-admin", seeded.orderTask().id().toString(), 2);
}
@Test
void shouldSetGroupBookingStatusDefWhenConfirmingGroupRoomingListFromTen() throws Exception {
Long orderId = 990000000000080001L;
SeededOrderTask roomInformation = seedOrderTask(
HOTEL_ID,
"mail-v4-command-rooming-def-ten-room-info-001",
Instant.parse("2026-07-19T01:14:00Z"),
orderId);
confirmBasicCard(roomInformation);
confirmRoomInformationBusinessCard(roomInformation, "TEN");
SeededOrderTask roomingList = seedRoomingListOrderTask(
HOTEL_ID,
"mail-v4-command-rooming-def-ten-list-001",
Instant.parse("2026-07-19T01:15:00Z"),
orderId,
"GROUP");
confirmBasicCard(roomingList);
performAuthorized(mockMvc, adminToken(), post("/api/reservation/order-tasks/{orderTaskId}/cards/{cardId}/confirm",
roomingList.orderTask().id(),
roomingList.businessCard().id())
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"version": 0
}
"""))
.andExpect(status().isOk())
.andExpect(jsonPath("$.business_cards[0].card_status").value("CONFIRMED"));
String confirmedPayload = confirmedPayloadJson(roomInformation.businessCard().id());
org.assertj.core.api.Assertions.assertThat(confirmedPayload)
.contains("\"group_booking_status\":\"DEF\"")
.contains("\"group_booking_status_label\":\"DEF-Definite\"");
assertAuditCount("V4_ROOMING_LIST_AUTO_DEF", "v4-command-admin",
roomInformation.businessCard().id().toString(), "\"update_status\":\"UPDATED\"", 1);
}
@Test
void shouldOverrideGroupBookingStatusInqToDefWhenConfirmingGroupRoomingList() throws Exception {
Long orderId = 990000000000080002L;
SeededOrderTask roomInformation = seedOrderTask(
HOTEL_ID,
"mail-v4-command-rooming-def-inq-room-info-001",
Instant.parse("2026-07-19T01:16:00Z"),
orderId);
confirmBasicCard(roomInformation);
confirmRoomInformationBusinessCard(roomInformation, "INQ");
SeededOrderTask roomingList = seedRoomingListOrderTask(
HOTEL_ID,
"mail-v4-command-rooming-def-inq-list-001",
Instant.parse("2026-07-19T01:17:00Z"),
orderId,
"GROUP");
confirmBasicCard(roomingList);
performAuthorized(mockMvc, adminToken(), post("/api/reservation/order-tasks/{orderTaskId}/cards/{cardId}/confirm",
roomingList.orderTask().id(),
roomingList.businessCard().id())
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"version": 0
}
"""))
.andExpect(status().isOk());
org.assertj.core.api.Assertions.assertThat(confirmedPayloadJson(roomInformation.businessCard().id()))
.contains("\"group_booking_status\":\"DEF\"")
.contains("\"group_booking_status_label\":\"DEF-Definite\"");
}
@Test
void shouldSetGroupBookingStatusDefWhenResolvingGroupRoomingListReview() throws Exception {
Long orderId = 990000000000080005L;
SeededOrderTask roomInformation = seedOrderTask(
HOTEL_ID,
"mail-v4-command-rooming-def-review-room-info-001",
Instant.parse("2026-07-19T01:20:10Z"),
orderId);
confirmBasicCard(roomInformation);
confirmRoomInformationBusinessCard(roomInformation, "TEN");
SeededOrderTask roomingList = seedReviewRoomingListOrderTask(
HOTEL_ID,
"mail-v4-command-rooming-def-review-list-001",
Instant.parse("2026-07-19T01:20:20Z"),
orderId);
confirmBasicCard(roomingList);
performAuthorized(mockMvc, adminToken(), post(
"/api/reservation/order-tasks/{orderTaskId}/cards/{cardId}/review-resolution",
roomingList.orderTask().id(),
roomingList.businessCard().id())
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"version": 0,
"reason": "确认 Rooming List 事项"
}
"""))
.andExpect(status().isOk())
.andExpect(jsonPath("$.business_cards[0].card_status").value("CONFIRMED"));
org.assertj.core.api.Assertions.assertThat(confirmedPayloadJson(roomInformation.businessCard().id()))
.contains("\"group_booking_status\":\"DEF\"")
.contains("\"group_booking_status_label\":\"DEF-Definite\"");
assertAuditCount("V4_ROOMING_LIST_AUTO_DEF", "v4-command-admin",
roomInformation.businessCard().id().toString(), "\"update_status\":\"UPDATED\"", 1);
}
@Test
void shouldNotChangeGroupBookingStatusWhenConfirmingFitRoomingList() throws Exception {
Long orderId = 990000000000080003L;
SeededOrderTask roomInformation = seedOrderTaskWithBusinessCard(
HOTEL_ID,
"mail-v4-command-rooming-def-fit-room-info-001",
Instant.parse("2026-07-19T01:18:00Z"),
orderId,
"FIT",
"CONFIRMATION_NUMBER",
"FIT-V4-COMMAND-001",
ReservationV4CardType.ROOM_INFORMATION.name(),
"NEW_BOOKING",
"""
{"card_type":"ROOM_INFORMATION","event_type":"NEW_BOOKING","guest_name":"FIT Guest","room_items":[{"room_type_code":"KING","room_count":1}]}
""");
confirmBasicCard(roomInformation);
confirmRoomInformationBusinessCard(roomInformation, null);
String beforePayload = confirmedPayloadJson(roomInformation.businessCard().id());
SeededOrderTask roomingList = seedRoomingListOrderTask(
HOTEL_ID,
"mail-v4-command-rooming-def-fit-list-001",
Instant.parse("2026-07-19T01:19:00Z"),
orderId,
"FIT");
confirmBasicCard(roomingList);
performAuthorized(mockMvc, adminToken(), post("/api/reservation/order-tasks/{orderTaskId}/cards/{cardId}/confirm",
roomingList.orderTask().id(),
roomingList.businessCard().id())
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"version": 0
}
"""))
.andExpect(status().isOk());
org.assertj.core.api.Assertions.assertThat(confirmedPayloadJson(roomInformation.businessCard().id()))
.isEqualTo(beforePayload)
.doesNotContain("\"group_booking_status\":\"DEF\"");
}
@Test
void shouldConfirmGroupRoomingListWithoutRoomInformationProjection() throws Exception {
Long orderId = 990000000000080004L;
SeededOrderTask roomingList = seedRoomingListOrderTask(
HOTEL_ID,
"mail-v4-command-rooming-def-no-projection-001",
Instant.parse("2026-07-19T01:20:00Z"),
orderId,
"GROUP");
confirmBasicCard(roomingList);
performAuthorized(mockMvc, adminToken(), post("/api/reservation/order-tasks/{orderTaskId}/cards/{cardId}/confirm",
roomingList.orderTask().id(),
roomingList.businessCard().id())
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"version": 0
}
"""))
.andExpect(status().isOk())
.andExpect(jsonPath("$.order_task.order_task_status").value("COMPLETED"))
.andExpect(jsonPath("$.business_cards[0].card_status").value("CONFIRMED"));
Integer roomInformationCount = jdbcTemplate.queryForObject(
"""
SELECT COUNT(*)
FROM workflow_reservation_v4_task_card
WHERE hotel_id = ?
AND card_type = 'ROOM_INFORMATION'
AND source_message_id = ?
AND logic_deleted_at IS NULL
""",
Integer.class,
HOTEL_ID,
roomingList.orderTask().sourceMessageId());
org.assertj.core.api.Assertions.assertThat(roomInformationCount).isZero();
assertAuditCount("V4_ROOMING_LIST_AUTO_DEF", "v4-command-admin",
roomingList.businessCard().id().toString(), "\"update_status\":\"SKIPPED_NO_ROOM_INFORMATION_PROJECTION\"", 1);
}
@Test
void shouldRejectBasicInformationConfirmWhenAccountCodeUnknown() throws Exception {
SeededOrderTask seeded = seedOrderTask(
@@ -1354,6 +1547,36 @@ class ReservationV4CommandControllerTest {
.andExpect(status().isOk());
}
private void confirmRoomInformationBusinessCard(SeededOrderTask seeded, String groupBookingStatus) throws Exception {
String groupBookingStatusFragment = groupBookingStatus == null
? ""
: """
,"group_booking_status": "%s"
""".formatted(groupBookingStatus);
performAuthorized(mockMvc, adminToken(), post("/api/reservation/order-tasks/{orderTaskId}/cards/{cardId}/confirm",
seeded.orderTask().id(),
seeded.businessCard().id())
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"version": 0,
"confirmed_payload": {
"room_information": {
"final_values": {
"arrival_date": "2026-07-26",
"departure_date": "2026-07-29",
"rate_code": "BAR",
"fit_name": "FIT Guest",
"breakfast_included": true,
"room_items": [{"room_type_code": "TWN", "room_count": 2}]%s
}
}
}
}
""".formatted(groupBookingStatusFragment)))
.andExpect(status().isOk());
}
private void resolveBusinessReviewCard(SeededOrderTask seeded) throws Exception {
performAuthorized(mockMvc, adminToken(), post(
"/api/reservation/order-tasks/{orderTaskId}/cards/{cardId}/review-resolution",
@@ -1371,6 +1594,17 @@ class ReservationV4CommandControllerTest {
.andExpect(status().isOk());
}
private String confirmedPayloadJson(Long cardId) {
return jdbcTemplate.queryForObject(
"""
SELECT confirmed_payload_json
FROM workflow_reservation_v4_task_card
WHERE id = ?
""",
String.class,
cardId);
}
private SeededOrderTask seedOrderTask(String hotelId, String externalMessageId, Instant receivedAt) {
return seedOrderTask(hotelId, externalMessageId, receivedAt, null);
}
@@ -1391,6 +1625,30 @@ class ReservationV4CommandControllerTest {
Instant receivedAt,
Long orderId,
String businessDisplayPayloadJson) {
return seedOrderTaskWithBusinessCard(
hotelId,
externalMessageId,
receivedAt,
orderId,
"GROUP",
"GROUP_CODE",
"GRP-V4-COMMAND-001",
ReservationV4CardType.ROOM_INFORMATION.name(),
"NEW_BOOKING",
businessDisplayPayloadJson);
}
private SeededOrderTask seedOrderTaskWithBusinessCard(
String hotelId,
String externalMessageId,
Instant receivedAt,
Long orderId,
String targetBookingType,
String targetLocatorType,
String targetLocatorValue,
String businessCardType,
String businessEventType,
String businessDisplayPayloadJson) {
SourceMessageCaptureResult source = captureSourceMessage(
hotelId,
externalMessageId,
@@ -1404,9 +1662,9 @@ class ReservationV4CommandControllerTest {
"order-command-" + externalMessageId,
1,
orderId,
"GROUP",
"GROUP_CODE",
"GRP-V4-COMMAND-001",
targetBookingType,
targetLocatorType,
targetLocatorValue,
ReservationV4TargetResolutionStatus.RESOLVED.name(),
ReservationV4OrderTaskStatus.OPEN.name(),
LocalDateTime.ofInstant(receivedAt, ZoneOffset.UTC),
@@ -1422,11 +1680,34 @@ class ReservationV4CommandControllerTest {
{"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,
businessCardType, businessEventType, 1, 30,
ReservationV4CardStatus.PENDING_CONFIRM.name(), null, businessDisplayPayloadJson);
return new SeededOrderTask(orderTask, sourceCard, basicCard, businessCard);
}
private SeededOrderTask seedRoomingListOrderTask(
String hotelId,
String externalMessageId,
Instant receivedAt,
Long orderId,
String targetBookingType) {
String locatorType = "GROUP".equals(targetBookingType) ? "GROUP_CODE" : "CONFIRMATION_NUMBER";
String locatorValue = "GROUP".equals(targetBookingType) ? "GRP-V4-ROOMING-001" : "FIT-V4-ROOMING-001";
return seedOrderTaskWithBusinessCard(
hotelId,
externalMessageId,
receivedAt,
orderId,
targetBookingType,
locatorType,
locatorValue,
ReservationV4CardType.ROOMING_LIST.name(),
"ROOMING_LIST",
"""
{"card_type":"ROOMING_LIST","event_type":"ROOMING_LIST","business_fields":{"attachment_ids":["att-rooming-list-001"],"rooming_list_action":"CONFIRM_RECEIVED"}}
""");
}
private SeededOrderTask seedReviewOrderTask(
String hotelId,
String externalMessageId,
@@ -1501,6 +1782,32 @@ class ReservationV4CommandControllerTest {
String businessStatus,
String businessDisplayPayloadJson,
String businessValidationErrorsJson) {
return seedReviewOrderTaskWithBusinessCard(
hotelId,
externalMessageId,
receivedAt,
orderId,
targetResolutionStatus,
basicStatus,
businessStatus,
ReservationV4CardType.ROOM_INFORMATION.name(),
"NEW_BOOKING",
businessDisplayPayloadJson,
businessValidationErrorsJson);
}
private SeededOrderTask seedReviewOrderTaskWithBusinessCard(
String hotelId,
String externalMessageId,
Instant receivedAt,
Long orderId,
String targetResolutionStatus,
String basicStatus,
String businessStatus,
String businessCardType,
String businessEventType,
String businessDisplayPayloadJson,
String businessValidationErrorsJson) {
SourceMessageCaptureResult source = captureSourceMessage(
hotelId,
externalMessageId,
@@ -1532,11 +1839,34 @@ class ReservationV4CommandControllerTest {
{"card_type":"BASIC_INFORMATION","order_ref":"ORDER-REVIEW","basic_information":{"account_code":null,"manual_review":true,"missing_fields":["/basic_information/account_code"]}}
""");
ReservationV4TaskCardSnapshot businessCard = insertCard(orderTask, hotelId,
ReservationV4CardType.ROOM_INFORMATION.name(), "NEW_BOOKING", 1, 30,
businessCardType, businessEventType, 1, 30,
businessStatus, reviewStatusFor(businessStatus), businessDisplayPayloadJson, businessValidationErrorsJson);
return new SeededOrderTask(orderTask, sourceCard, basicCard, businessCard);
}
private SeededOrderTask seedReviewRoomingListOrderTask(
String hotelId,
String externalMessageId,
Instant receivedAt,
Long orderId) {
return seedReviewOrderTaskWithBusinessCard(
hotelId,
externalMessageId,
receivedAt,
orderId,
orderId == null
? ReservationV4TargetResolutionStatus.UNRESOLVED.name()
: ReservationV4TargetResolutionStatus.RESOLVED.name(),
ReservationV4CardStatus.PENDING_CONFIRM.name(),
ReservationV4CardStatus.REVIEW_REQUIRED.name(),
ReservationV4CardType.ROOMING_LIST.name(),
"ROOMING_LIST",
"""
{"card_type":"ROOMING_LIST","event_type":"ROOMING_LIST","business_fields":{"attachment_ids":["att-rooming-list-review-001"],"rooming_list_action":"CONFIRM_RECEIVED"}}
""",
null);
}
private String reviewStatusFor(String cardStatus) {
return ReservationV4CardStatus.REVIEW_REQUIRED.name().equals(cardStatus)
? ReservationReviewStatus.PENDING.name()
@@ -1637,6 +1967,15 @@ class ReservationV4CommandControllerTest {
}
private void assertAuditCount(String action, String actorId, String snapshotFragment, int expectedCount) {
assertAuditCount(action, actorId, snapshotFragment, null, expectedCount);
}
private void assertAuditCount(
String action,
String actorId,
String snapshotFragment,
String secondSnapshotFragment,
int expectedCount) {
Integer count = jdbcTemplate.queryForObject(
"""
SELECT COUNT(*)
@@ -1645,12 +1984,15 @@ class ReservationV4CommandControllerTest {
AND action = ?
AND actor_id = ?
AND after_snapshot_json LIKE ?
AND (? IS NULL OR after_snapshot_json LIKE ?)
""",
Integer.class,
HOTEL_ID,
action,
actorId,
"%" + snapshotFragment + "%");
"%" + snapshotFragment + "%",
secondSnapshotFragment,
secondSnapshotFragment == null ? null : "%" + secondSnapshotFragment + "%");
org.assertj.core.api.Assertions.assertThat(count).isEqualTo(expectedCount);
}