实现 V4 卡片确认和来源通知确认
This commit is contained in:
@@ -0,0 +1,508 @@
|
||||
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.not;
|
||||
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;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
import cn.nianxx.thhotel.ThHotelApplication;
|
||||
import cn.nianxx.thhotel.platform.hotel.repository.PlatformHotelRepository;
|
||||
import cn.nianxx.thhotel.platform.identity.common.enums.PlatformUserStatus;
|
||||
import cn.nianxx.thhotel.platform.identity.domain.PlatformUserEntity;
|
||||
import cn.nianxx.thhotel.platform.identity.repository.PlatformIdentityRepository;
|
||||
import cn.nianxx.thhotel.platform.identity.service.impl.AuthPasswordService;
|
||||
import cn.nianxx.thhotel.platform.message.common.request.CaptureSourceMessageCommand;
|
||||
import cn.nianxx.thhotel.platform.message.common.result.SourceMessageCaptureResult;
|
||||
import cn.nianxx.thhotel.platform.message.service.SourceMessageCaptureService;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4OrderTaskDraft;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4OrderTaskSnapshot;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4SourceNotificationDraft;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4SourceNotificationSnapshot;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4TaskCardDraft;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4TaskCardSnapshot;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationV4CardStatus;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationV4CardType;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationV4NotificationStatus;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationV4OrderTaskStatus;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationV4TargetResolutionStatus;
|
||||
import cn.nianxx.thhotel.workflows.reservation.repository.ReservationV4SourceNotificationRepository;
|
||||
import cn.nianxx.thhotel.workflows.reservation.repository.ReservationV4WorkflowRepository;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
@SpringBootTest(
|
||||
classes = ThHotelApplication.class,
|
||||
properties = {
|
||||
"spring.datasource.url=jdbc:h2:mem:reservation_v4_command_controller_test;MODE=MySQL;DATABASE_TO_LOWER=TRUE;CASE_INSENSITIVE_IDENTIFIERS=TRUE",
|
||||
"auth.bootstrap.admin.username=v4-command-admin",
|
||||
"auth.bootstrap.admin.password=Admin@123456",
|
||||
"auth.bootstrap.admin.display-name=V4命令管理员",
|
||||
"auth.bootstrap.default-hotel-id=HOTEL-TEST",
|
||||
"auth.bootstrap.default-hotel-name=测试酒店",
|
||||
"auth.bootstrap.default-hotel-time-zone=Asia/Bangkok",
|
||||
"auth.session.ttl-minutes=720"
|
||||
})
|
||||
@AutoConfigureMockMvc
|
||||
@ActiveProfiles("test")
|
||||
class ReservationV4CommandControllerTest {
|
||||
|
||||
private static final String HOTEL_ID = "HOTEL-TEST";
|
||||
private static final String OTHER_HOTEL_ID = "HOTEL-OTHER";
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
private SourceMessageCaptureService captureService;
|
||||
|
||||
@Autowired
|
||||
private ReservationV4WorkflowRepository workflowRepository;
|
||||
|
||||
@Autowired
|
||||
private ReservationV4SourceNotificationRepository sourceNotificationRepository;
|
||||
|
||||
@Autowired
|
||||
private PlatformIdentityRepository identityRepository;
|
||||
|
||||
@Autowired
|
||||
private PlatformHotelRepository hotelRepository;
|
||||
|
||||
@Autowired
|
||||
private AuthPasswordService passwordService;
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
private String adminToken;
|
||||
private String noPermissionToken;
|
||||
|
||||
@BeforeEach
|
||||
void ensureNoPermissionUser() {
|
||||
PlatformUserEntity user = identityRepository.findUserByUsername("v4-command-no-permission")
|
||||
.orElseGet(() -> {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
PlatformUserEntity created = new PlatformUserEntity();
|
||||
created.setUsername("v4-command-no-permission");
|
||||
created.setPasswordHash(passwordService.hash("NoPerm@123456"));
|
||||
created.setDisplayName("V4 命令无权限用户");
|
||||
created.setUserStatus(PlatformUserStatus.ACTIVE.name());
|
||||
created.setSuperAdmin(false);
|
||||
created.setPasswordChangedAt(now);
|
||||
created.setCreatedAt(now);
|
||||
created.setUpdatedAt(now);
|
||||
identityRepository.insertUser(created);
|
||||
return created;
|
||||
});
|
||||
hotelRepository.ensureUserHotel(user.getId(), HOTEL_ID, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectBusinessCardConfirmBeforeBasicInformation() throws Exception {
|
||||
SeededOrderTask seeded = seedOrderTask(
|
||||
HOTEL_ID,
|
||||
"mail-v4-command-basic-block-001",
|
||||
Instant.parse("2026-07-19T01:00:00Z"));
|
||||
|
||||
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_items": [{"room_type_code": "TWN", "room_count": 2}]
|
||||
}
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isConflict())
|
||||
.andExpect(jsonPath("$.error_code").value("V4_BASIC_INFORMATION_NOT_CONFIRMED"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldConfirmBasicInformationAndThenBusinessCard() throws Exception {
|
||||
SeededOrderTask seeded = seedOrderTask(
|
||||
HOTEL_ID,
|
||||
"mail-v4-command-confirm-001",
|
||||
Instant.parse("2026-07-19T01:10: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": {
|
||||
"company": "Q.B.D. TRAVEL GROUP CO., LTD",
|
||||
"group_code": "GRP-V4-COMMAND-001"
|
||||
}
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.basic_information_card.card_status").value("CONFIRMED"))
|
||||
.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("$.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"))));
|
||||
|
||||
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_items": [{"room_type_code": "TWN", "room_count": 2}]
|
||||
}
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.order_task.order_task_status").value("COMPLETED"))
|
||||
.andExpect(jsonPath("$.business_cards[0].card_status").value("CONFIRMED"))
|
||||
.andExpect(jsonPath("$.business_cards[0].confirmed_payload.room_items[0].room_type_code")
|
||||
.value("TWN"))
|
||||
.andExpect(jsonPath("$.business_cards[0].availability.confirmable").value(false))
|
||||
.andExpect(jsonPath("$.business_cards[0].availability.readonly_reason_code").value("CARD_LOCKED"));
|
||||
assertAuditCount("V4_CARD_CONFIRM", "v4-command-admin", seeded.orderTask().id().toString(), 2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectRepeatedCardConfirm() throws Exception {
|
||||
SeededOrderTask seeded = seedOrderTask(
|
||||
HOTEL_ID,
|
||||
"mail-v4-command-repeat-001",
|
||||
Instant.parse("2026-07-19T01:20:00Z"));
|
||||
confirmBasicCard(seeded);
|
||||
|
||||
performAuthorized(mockMvc, adminToken(), post("/api/reservation/order-tasks/{orderTaskId}/cards/{cardId}/confirm",
|
||||
seeded.orderTask().id(),
|
||||
seeded.basicCard().id())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"version": 1,
|
||||
"confirmed_payload": {"group_code": "SHOULD-NOT-REWRITE"}
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isConflict())
|
||||
.andExpect(jsonPath("$.error_code").value("V4_CARD_ALREADY_CONFIRMED"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectCardConfirmWhenPriorOrderTaskIsOpen() throws Exception {
|
||||
Long sharedOrderId = 990000000000040001L;
|
||||
seedOrderTask(
|
||||
HOTEL_ID,
|
||||
"mail-v4-command-prior-open-001",
|
||||
Instant.parse("2026-07-19T01:25:00Z"),
|
||||
sharedOrderId);
|
||||
SeededOrderTask later = seedOrderTask(
|
||||
HOTEL_ID,
|
||||
"mail-v4-command-prior-open-002",
|
||||
Instant.parse("2026-07-19T01:26:00Z"),
|
||||
sharedOrderId);
|
||||
|
||||
performAuthorized(mockMvc, adminToken(), post("/api/reservation/order-tasks/{orderTaskId}/cards/{cardId}/confirm",
|
||||
later.orderTask().id(),
|
||||
later.basicCard().id())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"version": 0,
|
||||
"confirmed_payload": {"group_code": "GRP-V4-COMMAND-001"}
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isConflict())
|
||||
.andExpect(jsonPath("$.error_code").value("V4_PRIOR_ORDER_TASK_NOT_COMPLETED"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAckS10SourceNotification() throws Exception {
|
||||
ReservationV4SourceNotificationSnapshot notification = seedSourceNotification(
|
||||
HOTEL_ID,
|
||||
"mail-v4-command-s10-ack-001",
|
||||
"S10",
|
||||
Instant.parse("2026-07-19T01:30:00Z"));
|
||||
|
||||
performAuthorized(mockMvc, adminToken(), post("/api/reservation/source-notifications/{notificationId}/ack",
|
||||
notification.id())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"version": 0
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.notification.notification_status").value("ACKED"))
|
||||
.andExpect(jsonPath("$.notification.ack_by").value("v4-command-admin"))
|
||||
.andExpect(jsonPath("$.source_message_card.card_status").value("ACKED"))
|
||||
.andExpect(jsonPath("$.availability.ackable").value(false))
|
||||
.andExpect(jsonPath("$.availability.readonly_reason_code").value("CARD_LOCKED"));
|
||||
assertAuditCount("V4_SOURCE_NOTIFICATION_ACK", "v4-command-admin", notification.id().toString(), 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectV4CommandsWhenPermissionMissing() throws Exception {
|
||||
SeededOrderTask seeded = seedOrderTask(
|
||||
HOTEL_ID,
|
||||
"mail-v4-command-no-permission-001",
|
||||
Instant.parse("2026-07-19T01:40:00Z"));
|
||||
ReservationV4SourceNotificationSnapshot notification = seedSourceNotification(
|
||||
HOTEL_ID,
|
||||
"mail-v4-command-no-permission-s10-001",
|
||||
"S10",
|
||||
Instant.parse("2026-07-19T01:41:00Z"));
|
||||
|
||||
performAuthorized(mockMvc, noPermissionToken(), post("/api/reservation/order-tasks/{orderTaskId}/cards/{cardId}/confirm",
|
||||
seeded.orderTask().id(),
|
||||
seeded.basicCard().id())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{"version": 0}
|
||||
"""))
|
||||
.andExpect(status().isForbidden())
|
||||
.andExpect(jsonPath("$.error_code").value("FRONTEND_PERMISSION_DENIED"));
|
||||
performAuthorized(mockMvc, noPermissionToken(), post("/api/reservation/source-notifications/{notificationId}/ack",
|
||||
notification.id())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{"version": 0}
|
||||
"""))
|
||||
.andExpect(status().isForbidden())
|
||||
.andExpect(jsonPath("$.error_code").value("FRONTEND_PERMISSION_DENIED"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectV4CommandsWhenHotelAccessDenied() throws Exception {
|
||||
SeededOrderTask seeded = seedOrderTask(
|
||||
OTHER_HOTEL_ID,
|
||||
"mail-v4-command-cross-hotel-001",
|
||||
Instant.parse("2026-07-19T01:50:00Z"));
|
||||
ReservationV4SourceNotificationSnapshot notification = seedSourceNotification(
|
||||
OTHER_HOTEL_ID,
|
||||
"mail-v4-command-cross-hotel-s10-001",
|
||||
"S99",
|
||||
Instant.parse("2026-07-19T01:51: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}
|
||||
"""))
|
||||
.andExpect(status().isForbidden())
|
||||
.andExpect(jsonPath("$.error_code").value("HOTEL_ACCESS_DENIED"));
|
||||
performAuthorized(mockMvc, adminToken(), post("/api/reservation/source-notifications/{notificationId}/ack",
|
||||
notification.id())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{"version": 0}
|
||||
"""))
|
||||
.andExpect(status().isForbidden())
|
||||
.andExpect(jsonPath("$.error_code").value("HOTEL_ACCESS_DENIED"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 V4 命令管理员 token,测试通过真实登录链路覆盖前端鉴权。
|
||||
*/
|
||||
private String adminToken() throws Exception {
|
||||
if (adminToken == null) {
|
||||
adminToken = loginToken(mockMvc, "v4-command-admin", "Admin@123456");
|
||||
}
|
||||
return adminToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取没有 RESERVATION_TASK_CONFIRM 权限的普通用户 token。
|
||||
*/
|
||||
private String noPermissionToken() throws Exception {
|
||||
if (noPermissionToken == null) {
|
||||
noPermissionToken = loginToken(mockMvc, "v4-command-no-permission", "NoPerm@123456");
|
||||
}
|
||||
return noPermissionToken;
|
||||
}
|
||||
|
||||
private void confirmBasicCard(SeededOrderTask seeded) throws Exception {
|
||||
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": {"group_code": "GRP-V4-COMMAND-001"}
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isOk());
|
||||
}
|
||||
|
||||
private SeededOrderTask seedOrderTask(String hotelId, String externalMessageId, Instant receivedAt) {
|
||||
return seedOrderTask(hotelId, externalMessageId, receivedAt, null);
|
||||
}
|
||||
|
||||
private SeededOrderTask seedOrderTask(
|
||||
String hotelId,
|
||||
String externalMessageId,
|
||||
Instant receivedAt,
|
||||
Long orderId) {
|
||||
SourceMessageCaptureResult source = captureSourceMessage(
|
||||
hotelId,
|
||||
externalMessageId,
|
||||
"V4 Command Business",
|
||||
receivedAt);
|
||||
LocalDateTime now = LocalDateTime.ofInstant(receivedAt.plusSeconds(10), ZoneOffset.UTC);
|
||||
ReservationV4OrderTaskSnapshot orderTask = workflowRepository.findOrCreateOrderTask(new ReservationV4OrderTaskDraft(
|
||||
hotelId,
|
||||
source.inboxId(),
|
||||
990000000000010001L + Math.abs(externalMessageId.hashCode()),
|
||||
"order-command-" + externalMessageId,
|
||||
1,
|
||||
orderId,
|
||||
"GROUP",
|
||||
"GROUP_CODE",
|
||||
"GRP-V4-COMMAND-001",
|
||||
ReservationV4TargetResolutionStatus.RESOLVED.name(),
|
||||
ReservationV4OrderTaskStatus.OPEN.name(),
|
||||
LocalDateTime.ofInstant(receivedAt, ZoneOffset.UTC),
|
||||
now));
|
||||
ReservationV4TaskCardSnapshot sourceCard = insertCard(orderTask, hotelId,
|
||||
ReservationV4CardType.SOURCE_MESSAGE_DISPLAY.name(), null, 0, 10,
|
||||
ReservationV4CardStatus.READONLY.name(), null, """
|
||||
{"card_type":"SOURCE_MESSAGE_DISPLAY","source_message":{"subject":"V4 Command Business"}}
|
||||
""");
|
||||
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"}
|
||||
""");
|
||||
ReservationV4TaskCardSnapshot businessCard = insertCard(orderTask, hotelId,
|
||||
ReservationV4CardType.ROOM_INFORMATION.name(), "NEW_BOOKING", 1, 30,
|
||||
ReservationV4CardStatus.PENDING_CONFIRM.name(), null, """
|
||||
{"card_type":"ROOM_INFORMATION","event_type":"NEW_BOOKING","room_items":[{"room_type_code":"TWN","room_count":2}]}
|
||||
""");
|
||||
return new SeededOrderTask(orderTask, sourceCard, basicCard, businessCard);
|
||||
}
|
||||
|
||||
private ReservationV4SourceNotificationSnapshot seedSourceNotification(
|
||||
String hotelId,
|
||||
String externalMessageId,
|
||||
String routeCode,
|
||||
Instant receivedAt) {
|
||||
SourceMessageCaptureResult source = captureSourceMessage(
|
||||
hotelId,
|
||||
externalMessageId,
|
||||
"V4 Command Notification",
|
||||
receivedAt);
|
||||
return sourceNotificationRepository.findOrCreateSourceNotification(new ReservationV4SourceNotificationDraft(
|
||||
hotelId,
|
||||
source.inboxId(),
|
||||
990000000000020001L + Math.abs(externalMessageId.hashCode()),
|
||||
null,
|
||||
routeCode,
|
||||
ReservationV4NotificationStatus.ACK_REQUIRED.name(),
|
||||
"""
|
||||
{"route_code":"%s","source_message":{"body":"Sensitive raw notification body"}}
|
||||
""".formatted(routeCode),
|
||||
LocalDateTime.ofInstant(receivedAt, ZoneOffset.UTC),
|
||||
LocalDateTime.ofInstant(receivedAt.plusSeconds(20), ZoneOffset.UTC)));
|
||||
}
|
||||
|
||||
private ReservationV4TaskCardSnapshot insertCard(
|
||||
ReservationV4OrderTaskSnapshot orderTask,
|
||||
String hotelId,
|
||||
String cardType,
|
||||
String eventType,
|
||||
Integer sourceEventIndex,
|
||||
Integer sortOrder,
|
||||
String cardStatus,
|
||||
String reviewStatus,
|
||||
String displayPayloadJson) {
|
||||
return workflowRepository.insertTaskCard(new ReservationV4TaskCardDraft(
|
||||
hotelId,
|
||||
orderTask.id(),
|
||||
orderTask.sourceMessageId(),
|
||||
eventType == null ? null : 990000000000030001L + sourceEventIndex,
|
||||
cardType,
|
||||
eventType,
|
||||
sourceEventIndex,
|
||||
sortOrder,
|
||||
cardStatus,
|
||||
reviewStatus,
|
||||
"""
|
||||
{"private_url":"https://private.example.test/raw","raw":"must stay internal"}
|
||||
""",
|
||||
displayPayloadJson,
|
||||
null,
|
||||
orderTask.createdAt()));
|
||||
}
|
||||
|
||||
private SourceMessageCaptureResult captureSourceMessage(
|
||||
String hotelId,
|
||||
String externalMessageId,
|
||||
String subject,
|
||||
Instant receivedAt) {
|
||||
return captureService.capture(new CaptureSourceMessageCommand(
|
||||
hotelId,
|
||||
"AGENTBUS",
|
||||
"EMAIL",
|
||||
externalMessageId,
|
||||
"thread-" + externalMessageId,
|
||||
"frame-" + externalMessageId,
|
||||
"session-v4-command",
|
||||
receivedAt,
|
||||
null,
|
||||
"guest@example.test",
|
||||
subject,
|
||||
"Please handle V4 command message.",
|
||||
"<html><body>Please handle V4 command message.</body></html>",
|
||||
"{\"source\":{\"external_message_id\":\"" + externalMessageId + "\"}}",
|
||||
"agentbus-outlook-v1",
|
||||
List.of()
|
||||
));
|
||||
}
|
||||
|
||||
private void assertAuditCount(String action, String actorId, String snapshotFragment, int expectedCount) {
|
||||
Integer count = jdbcTemplate.queryForObject(
|
||||
"""
|
||||
SELECT COUNT(*)
|
||||
FROM workflow_reservation_audit_log
|
||||
WHERE hotel_id = ?
|
||||
AND action = ?
|
||||
AND actor_id = ?
|
||||
AND after_snapshot_json LIKE ?
|
||||
""",
|
||||
Integer.class,
|
||||
HOTEL_ID,
|
||||
action,
|
||||
actorId,
|
||||
"%" + snapshotFragment + "%");
|
||||
org.assertj.core.api.Assertions.assertThat(count).isEqualTo(expectedCount);
|
||||
}
|
||||
|
||||
private record SeededOrderTask(
|
||||
ReservationV4OrderTaskSnapshot orderTask,
|
||||
ReservationV4TaskCardSnapshot sourceCard,
|
||||
ReservationV4TaskCardSnapshot basicCard,
|
||||
ReservationV4TaskCardSnapshot businessCard
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -243,8 +243,9 @@ class ReservationV4QueryControllerTest {
|
||||
.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].ai_payload_json").doesNotExist())
|
||||
.andExpect(jsonPath("$.availability.read_only").value(true))
|
||||
.andExpect(jsonPath("$.availability.readonly_reason_code").value("COMMAND_API_PENDING"))
|
||||
.andExpect(jsonPath("$.availability.read_only").value(false))
|
||||
.andExpect(jsonPath("$.availability.confirmable").value(true))
|
||||
.andExpect(jsonPath("$.availability.readonly_reason_code").value("PROCESSABLE"))
|
||||
.andExpect(jsonPath("$.source_message_summary.subject").value("V4 Query Business"))
|
||||
.andExpect(content().string(not(containsString("https://private.example.test"))));
|
||||
}
|
||||
@@ -266,8 +267,9 @@ class ReservationV4QueryControllerTest {
|
||||
.andExpect(jsonPath("$.items[0].order_task.display_order_key").value("GRP-V4-QUERY-001"))
|
||||
.andExpect(jsonPath("$.items[0].card_counts.pending_confirm_count").value(2))
|
||||
.andExpect(jsonPath("$.items[0].display_status").value("OPEN"))
|
||||
.andExpect(jsonPath("$.items[0].availability.read_only").value(true))
|
||||
.andExpect(jsonPath("$.items[0].availability.readonly_reason_code").value("COMMAND_API_PENDING"))
|
||||
.andExpect(jsonPath("$.items[0].availability.read_only").value(false))
|
||||
.andExpect(jsonPath("$.items[0].availability.confirmable").value(true))
|
||||
.andExpect(jsonPath("$.items[0].availability.readonly_reason_code").value("PROCESSABLE"))
|
||||
.andExpect(jsonPath("$.page.total").value(1));
|
||||
}
|
||||
|
||||
@@ -326,8 +328,9 @@ class ReservationV4QueryControllerTest {
|
||||
.andExpect(jsonPath("$.source_message_card.card_type").value("SOURCE_MESSAGE_NOTIFICATION"))
|
||||
.andExpect(jsonPath("$.conversation_summary.source_message_id")
|
||||
.value(notification.sourceMessageId().toString()))
|
||||
.andExpect(jsonPath("$.availability.ackable").value(false))
|
||||
.andExpect(jsonPath("$.availability.read_only").value(true))
|
||||
.andExpect(jsonPath("$.availability.ackable").value(true))
|
||||
.andExpect(jsonPath("$.availability.read_only").value(false))
|
||||
.andExpect(jsonPath("$.availability.readonly_reason_code").value("PROCESSABLE"))
|
||||
.andExpect(jsonPath("$.raw_payload_json").doesNotExist())
|
||||
.andExpect(content().string(not(containsString("Sensitive raw notification body"))));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user