实现V4 Payment附件安全摘要
This commit is contained in:
@@ -208,6 +208,89 @@ class ReservationV4CommandControllerTest {
|
||||
assertAuditCount("V4_CARD_CONFIRM", "v4-command-admin", seeded.orderTask().id().toString(), 2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldConfirmPaymentCardWithVersionOnlyAndKeepAttachmentIdsReadonly() throws Exception {
|
||||
SeededOrderTask seeded = seedOrderTaskWithBusinessCard(
|
||||
HOTEL_ID,
|
||||
"mail-v4-command-payment-confirm-001",
|
||||
Instant.parse("2026-07-19T01:12:00Z"),
|
||||
null,
|
||||
"GROUP",
|
||||
"GROUP_CODE",
|
||||
"GRP-V4-PAYMENT-COMMAND-001",
|
||||
ReservationV4CardType.PAYMENT.name(),
|
||||
"PAYMENT",
|
||||
"""
|
||||
{
|
||||
"card_type":"PAYMENT",
|
||||
"event_type":"PAYMENT",
|
||||
"business_fields":{"attachment_ids":["att-pay-original-001"]}
|
||||
}
|
||||
""");
|
||||
confirmBasicCard(seeded);
|
||||
|
||||
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": {
|
||||
"business_fields": {
|
||||
"attachment_ids": ["att-pay-replaced-001"]
|
||||
}
|
||||
}
|
||||
}
|
||||
"""))
|
||||
.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.business_fields.attachment_ids[0]")
|
||||
.value("att-pay-original-001"))
|
||||
.andExpect(content().string(not(containsString("att-pay-replaced-001"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectPaymentReviewResolutionWhenTryingToModifyAttachmentIds() throws Exception {
|
||||
SeededOrderTask seeded = seedReviewOrderTaskWithBusinessCard(
|
||||
HOTEL_ID,
|
||||
"mail-v4-command-payment-review-readonly-001",
|
||||
Instant.parse("2026-07-19T01:12:30Z"),
|
||||
990000000000070120L,
|
||||
ReservationV4TargetResolutionStatus.RESOLVED.name(),
|
||||
ReservationV4CardStatus.CONFIRMED.name(),
|
||||
ReservationV4CardStatus.REVIEW_REQUIRED.name(),
|
||||
ReservationV4CardType.PAYMENT.name(),
|
||||
"PAYMENT",
|
||||
"""
|
||||
{
|
||||
"card_type":"PAYMENT",
|
||||
"event_type":"PAYMENT",
|
||||
"business_fields":{"attachment_ids":["att-pay-review-original-001"]}
|
||||
}
|
||||
""",
|
||||
"""
|
||||
[{"field":"business_fields.attachment_ids.0","message":"付款凭证附件集合只读"}]
|
||||
""");
|
||||
|
||||
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,
|
||||
"field_overrides": [
|
||||
{"field_pointer": "/business_fields/attachment_ids/0", "value": "att-pay-review-replaced-001"}
|
||||
]
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.error_code").value("V4_REVIEW_POINTER_READONLY"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSetGroupBookingStatusDefWhenConfirmingGroupRoomingListFromTen() throws Exception {
|
||||
Long orderId = 990000000000080001L;
|
||||
|
||||
@@ -17,9 +17,12 @@ 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.dto.SourceMessageMediaSummary;
|
||||
import cn.nianxx.thhotel.platform.message.common.request.CaptureSourceMessageCommand;
|
||||
import cn.nianxx.thhotel.platform.message.common.request.CaptureSourceMessageMedia;
|
||||
import cn.nianxx.thhotel.platform.message.common.result.SourceMessageCaptureResult;
|
||||
import cn.nianxx.thhotel.platform.message.service.SourceMessageCaptureService;
|
||||
import cn.nianxx.thhotel.platform.message.service.SourceMessageQueryService;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAiBatchDraft;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAiTransitionDraft;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAuditLogDraft;
|
||||
@@ -78,6 +81,8 @@ class ReservationV4QueryControllerTest {
|
||||
|
||||
@Autowired
|
||||
private SourceMessageCaptureService captureService;
|
||||
@Autowired
|
||||
private SourceMessageQueryService sourceMessageQueryService;
|
||||
|
||||
@Autowired
|
||||
private ReservationV4WorkflowRepository workflowRepository;
|
||||
@@ -593,6 +598,197 @@ class ReservationV4QueryControllerTest {
|
||||
.andExpect(content().string(not(containsString("oss.example.test"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnPaymentAttachmentSafeSummariesWithoutUrlsOrEditableAttachmentIds() throws Exception {
|
||||
SourceMessageCaptureResult source = captureSourceMessage(
|
||||
"mail-v4-query-payment-attachments-001",
|
||||
"V4 Query Payment Attachments",
|
||||
Instant.parse("2026-07-18T03:02:47Z"),
|
||||
HOTEL_ID,
|
||||
List.of(
|
||||
new CaptureSourceMessageMedia(
|
||||
"ATTACHMENT",
|
||||
"payment-slip.png",
|
||||
"image/png",
|
||||
12345L,
|
||||
"https://oss.example.test/payment-slip.png?signature=secret",
|
||||
"att-pay-image-001"),
|
||||
new CaptureSourceMessageMedia(
|
||||
"ATTACHMENT",
|
||||
"payment-voucher.pdf",
|
||||
"application/pdf",
|
||||
67890L,
|
||||
"oss://private/payment-voucher.pdf",
|
||||
"att-pay-pdf-001")));
|
||||
ReservationV4OrderTaskSnapshot orderTask = seedOrderTaskWithBusinessCardType(
|
||||
source,
|
||||
990000000000003701L,
|
||||
Instant.parse("2026-07-18T03:02:47Z"),
|
||||
"GROUP",
|
||||
"GROUP_CODE",
|
||||
"GRP-V4-PAYMENT-SAFE-001",
|
||||
ReservationV4CardType.PAYMENT.name(),
|
||||
"PAYMENT",
|
||||
ReservationV4CardStatus.CONFIRMED.name(),
|
||||
ReservationV4CardStatus.PENDING_CONFIRM.name(),
|
||||
"""
|
||||
{
|
||||
"card_type":"PAYMENT",
|
||||
"event_type":"PAYMENT",
|
||||
"business_fields":{
|
||||
"attachment_ids":["att-pay-image-001","att-pay-pdf-001"]
|
||||
}
|
||||
}
|
||||
""",
|
||||
null);
|
||||
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/order-tasks/{orderTaskId}", orderTask.id())
|
||||
.param("hotel_id", HOTEL_ID))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.business_cards[0].card_type").value("PAYMENT"))
|
||||
.andExpect(jsonPath("$.business_cards[0].display_payload.payment_attachments.length()").value(2))
|
||||
.andExpect(jsonPath("$.business_cards[0].display_payload.payment_attachments[0].attachment_id")
|
||||
.value("att-pay-image-001"))
|
||||
.andExpect(jsonPath("$.business_cards[0].display_payload.payment_attachments[0].file_name")
|
||||
.value("payment-slip.png"))
|
||||
.andExpect(jsonPath("$.business_cards[0].display_payload.payment_attachments[0].content_type")
|
||||
.value("image/png"))
|
||||
.andExpect(jsonPath("$.business_cards[0].display_payload.payment_attachments[0].size_bytes")
|
||||
.value(12345))
|
||||
.andExpect(jsonPath("$.business_cards[0].display_payload.payment_attachments[0].is_image")
|
||||
.value(true))
|
||||
.andExpect(jsonPath("$.business_cards[0].display_payload.payment_attachments[0].preview_available")
|
||||
.value(true))
|
||||
.andExpect(jsonPath("$.business_cards[0].display_payload.payment_attachments[0].download_available")
|
||||
.value(true))
|
||||
.andExpect(jsonPath("$.business_cards[0].display_payload.payment_attachments[0].external_media_id")
|
||||
.value("att-pay-image-001"))
|
||||
.andExpect(jsonPath("$.business_cards[0].display_payload.payment_attachments[1].attachment_id")
|
||||
.value("att-pay-pdf-001"))
|
||||
.andExpect(jsonPath("$.business_cards[0].display_payload.payment_attachments[1].is_image")
|
||||
.value(false))
|
||||
.andExpect(jsonPath("$.business_cards[0].display_payload.payment_attachments[1].preview_available")
|
||||
.value(false))
|
||||
.andExpect(jsonPath("$.business_cards[0].display_payload.payment_attachments[1].download_available")
|
||||
.value(true))
|
||||
.andExpect(jsonPath("$.business_cards[0].fields[?(@.field_pointer=='/business_fields/attachment_ids/0')]")
|
||||
.doesNotExist())
|
||||
.andExpect(jsonPath("$.business_cards[0].fields[?(@.field_pointer=='/business_fields/attachment_ids/1')]")
|
||||
.doesNotExist())
|
||||
.andExpect(content().string(not(containsString("externalUrl"))))
|
||||
.andExpect(content().string(not(containsString("download_url"))))
|
||||
.andExpect(content().string(not(containsString("signedUrl"))))
|
||||
.andExpect(content().string(not(containsString("https://oss.example.test"))))
|
||||
.andExpect(content().string(not(containsString("oss://private"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotExposeCrossHotelPaymentAttachmentSummaryWhenV4ReferenceIsPolluted() throws Exception {
|
||||
SourceMessageCaptureResult otherSource = captureSourceMessage(
|
||||
"mail-v4-query-payment-cross-hotel-001",
|
||||
"V4 Query Payment Cross Hotel Attachments",
|
||||
Instant.parse("2026-07-18T03:02:48Z"),
|
||||
OTHER_HOTEL_ID,
|
||||
List.of(new CaptureSourceMessageMedia(
|
||||
"ATTACHMENT",
|
||||
"other-hotel-payment-secret.pdf",
|
||||
"application/pdf",
|
||||
55555L,
|
||||
"https://oss.example.test/other-hotel-payment-secret.pdf",
|
||||
"att-other-hotel-secret-001")));
|
||||
ReservationV4OrderTaskSnapshot orderTask = seedOrderTaskWithBusinessCardType(
|
||||
otherSource,
|
||||
990000000000003702L,
|
||||
Instant.parse("2026-07-18T03:02:48Z"),
|
||||
"GROUP",
|
||||
"GROUP_CODE",
|
||||
"GRP-V4-PAYMENT-CROSS-HOTEL-001",
|
||||
ReservationV4CardType.PAYMENT.name(),
|
||||
"PAYMENT",
|
||||
ReservationV4CardStatus.CONFIRMED.name(),
|
||||
ReservationV4CardStatus.PENDING_CONFIRM.name(),
|
||||
"""
|
||||
{
|
||||
"card_type":"PAYMENT",
|
||||
"event_type":"PAYMENT",
|
||||
"business_fields":{
|
||||
"attachment_ids":["att-other-hotel-secret-001"]
|
||||
}
|
||||
}
|
||||
""",
|
||||
null);
|
||||
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/order-tasks/{orderTaskId}", orderTask.id())
|
||||
.param("hotel_id", HOTEL_ID))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.business_cards[0].display_payload.payment_attachments[0].attachment_id")
|
||||
.value("att-other-hotel-secret-001"))
|
||||
.andExpect(jsonPath("$.business_cards[0].display_payload.payment_attachments[0].file_name")
|
||||
.doesNotExist())
|
||||
.andExpect(jsonPath("$.business_cards[0].display_payload.payment_attachments[0].content_type")
|
||||
.doesNotExist())
|
||||
.andExpect(jsonPath("$.business_cards[0].display_payload.payment_attachments[0].download_available")
|
||||
.value(false))
|
||||
.andExpect(jsonPath("$.business_cards[0].display_payload.payment_attachments[0].unavailable_reason_code")
|
||||
.value("ATTACHMENT_NOT_FOUND"))
|
||||
.andExpect(content().string(not(containsString("other-hotel-payment-secret.pdf"))))
|
||||
.andExpect(content().string(not(containsString("https://oss.example.test/other-hotel-payment-secret.pdf"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotMatchPaymentAttachmentByInternalSourceMessageMediaId() throws Exception {
|
||||
SourceMessageCaptureResult source = captureSourceMessage(
|
||||
"mail-v4-query-payment-internal-media-id-001",
|
||||
"V4 Query Payment Internal Media Id",
|
||||
Instant.parse("2026-07-18T03:02:49Z"),
|
||||
HOTEL_ID,
|
||||
List.of(new CaptureSourceMessageMedia(
|
||||
"ATTACHMENT",
|
||||
"payment-internal-id-should-not-match.png",
|
||||
"image/png",
|
||||
34567L,
|
||||
"https://oss.example.test/payment-internal-id-should-not-match.png",
|
||||
"att-payment-real-external-001")));
|
||||
SourceMessageMediaSummary mediaSummary = sourceMessageQueryService
|
||||
.getMediaSummaries(HOTEL_ID, source.inboxId())
|
||||
.get(0);
|
||||
ReservationV4OrderTaskSnapshot orderTask = seedOrderTaskWithBusinessCardType(
|
||||
source,
|
||||
990000000000003703L,
|
||||
Instant.parse("2026-07-18T03:02:49Z"),
|
||||
"GROUP",
|
||||
"GROUP_CODE",
|
||||
"GRP-V4-PAYMENT-INTERNAL-MEDIA-ID-001",
|
||||
ReservationV4CardType.PAYMENT.name(),
|
||||
"PAYMENT",
|
||||
ReservationV4CardStatus.CONFIRMED.name(),
|
||||
ReservationV4CardStatus.PENDING_CONFIRM.name(),
|
||||
"""
|
||||
{
|
||||
"card_type":"PAYMENT",
|
||||
"event_type":"PAYMENT",
|
||||
"business_fields":{
|
||||
"attachment_ids":["%s"]
|
||||
}
|
||||
}
|
||||
""".formatted(mediaSummary.mediaId()),
|
||||
null);
|
||||
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/order-tasks/{orderTaskId}", orderTask.id())
|
||||
.param("hotel_id", HOTEL_ID))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.business_cards[0].display_payload.payment_attachments[0].attachment_id")
|
||||
.value(mediaSummary.mediaId()))
|
||||
.andExpect(jsonPath("$.business_cards[0].display_payload.payment_attachments[0].file_name")
|
||||
.doesNotExist())
|
||||
.andExpect(jsonPath("$.business_cards[0].display_payload.payment_attachments[0].download_available")
|
||||
.value(false))
|
||||
.andExpect(jsonPath("$.business_cards[0].display_payload.payment_attachments[0].unavailable_reason_code")
|
||||
.value("ATTACHMENT_NOT_FOUND"))
|
||||
.andExpect(content().string(not(containsString("payment-internal-id-should-not-match.png"))))
|
||||
.andExpect(content().string(not(containsString("att-payment-real-external-001"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnReadonlyRoomInformationDisplayModelForCancelBooking() throws Exception {
|
||||
Long orderId = 990000000000777002L;
|
||||
@@ -1235,6 +1431,67 @@ class ReservationV4QueryControllerTest {
|
||||
return orderTask;
|
||||
}
|
||||
|
||||
private ReservationV4OrderTaskSnapshot seedOrderTaskWithBusinessCardType(
|
||||
SourceMessageCaptureResult source,
|
||||
Long aiBatchId,
|
||||
Instant receivedAt,
|
||||
String targetBookingType,
|
||||
String targetLocatorType,
|
||||
String targetLocatorValue,
|
||||
String businessCardType,
|
||||
String businessEventType,
|
||||
String basicCardStatus,
|
||||
String businessCardStatus,
|
||||
String businessDisplayPayloadJson,
|
||||
String confirmedPayloadJson) {
|
||||
LocalDateTime now = LocalDateTime.ofInstant(receivedAt.plusSeconds(10), ZoneOffset.UTC);
|
||||
ReservationV4OrderTaskSnapshot orderTask = workflowRepository.findOrCreateOrderTask(new ReservationV4OrderTaskDraft(
|
||||
HOTEL_ID,
|
||||
source.inboxId(),
|
||||
aiBatchId,
|
||||
"order-generic-card-" + source.inboxId(),
|
||||
1,
|
||||
null,
|
||||
targetBookingType,
|
||||
targetLocatorType,
|
||||
targetLocatorValue,
|
||||
ReservationV4TargetResolutionStatus.RESOLVED.name(),
|
||||
ReservationV4OrderTaskStatus.OPEN.name(),
|
||||
LocalDateTime.ofInstant(receivedAt, ZoneOffset.UTC),
|
||||
now));
|
||||
insertCard(orderTask, ReservationV4CardType.SOURCE_MESSAGE_DISPLAY.name(), null, 0, 10,
|
||||
ReservationV4CardStatus.READONLY.name(), null, """
|
||||
{"card_type":"SOURCE_MESSAGE_DISPLAY","source_message":{"subject":"V4 Query Business"}}
|
||||
""");
|
||||
insertCard(orderTask, ReservationV4CardType.BASIC_INFORMATION.name(), null, 0, 20,
|
||||
basicCardStatus, reviewStatusFor(basicCardStatus), """
|
||||
{
|
||||
"card_type":"BASIC_INFORMATION",
|
||||
"order_ref":"order-1",
|
||||
"basic_information":{"account_code":"QBD_TRAVEL","market_code":"LEISURE","source_code":"TRAVEL_AGENT"}
|
||||
}
|
||||
""");
|
||||
ReservationV4TaskCardSnapshot businessCard = insertCard(
|
||||
orderTask,
|
||||
businessCardType,
|
||||
businessEventType,
|
||||
1,
|
||||
30,
|
||||
businessCardStatus,
|
||||
reviewStatusFor(businessCardStatus),
|
||||
businessDisplayPayloadJson);
|
||||
if (confirmedPayloadJson != null) {
|
||||
workflowRepository.confirmTaskCardWithVersion(
|
||||
HOTEL_ID,
|
||||
businessCard.id(),
|
||||
businessCard.version(),
|
||||
confirmedPayloadJson,
|
||||
"v4-query-admin",
|
||||
now);
|
||||
}
|
||||
return orderTask;
|
||||
}
|
||||
|
||||
private void seedConfirmedRoomInformationProjection(
|
||||
Long orderId,
|
||||
String externalMessageId,
|
||||
@@ -1370,6 +1627,15 @@ class ReservationV4QueryControllerTest {
|
||||
String subject,
|
||||
Instant receivedAt,
|
||||
String hotelId) {
|
||||
return captureSourceMessage(externalMessageId, subject, receivedAt, hotelId, List.of());
|
||||
}
|
||||
|
||||
private SourceMessageCaptureResult captureSourceMessage(
|
||||
String externalMessageId,
|
||||
String subject,
|
||||
Instant receivedAt,
|
||||
String hotelId,
|
||||
List<CaptureSourceMessageMedia> mediaItems) {
|
||||
return captureService.capture(new CaptureSourceMessageCommand(
|
||||
hotelId,
|
||||
"AGENTBUS",
|
||||
@@ -1386,7 +1652,7 @@ class ReservationV4QueryControllerTest {
|
||||
"<html><body>Please handle V4 query message.</body></html>",
|
||||
"{\"source\":{\"external_message_id\":\"" + externalMessageId + "\"}}",
|
||||
"agentbus-outlook-v1",
|
||||
List.of()
|
||||
mediaItems
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -1091,6 +1091,29 @@ class SuperAgentTaskResultControllerTest {
|
||||
.contains("att-pay-1")
|
||||
.contains("payment-slip.jpg")
|
||||
.doesNotContain("https://oss.example.test");
|
||||
Long orderTaskId = jdbcTemplate.queryForObject("""
|
||||
SELECT id
|
||||
FROM workflow_reservation_v4_order_task
|
||||
WHERE source_message_id = ?
|
||||
AND order_ref = 'order-1'
|
||||
LIMIT 1
|
||||
""", Long.class, source.inboxId());
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/order-tasks/{orderTaskId}", orderTaskId)
|
||||
.param("hotel_id", "HOTEL-TEST"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.business_cards[?(@.card_type=='PAYMENT')].display_payload.payment_attachments[0].attachment_id")
|
||||
.value(contains("att-pay-1")))
|
||||
.andExpect(jsonPath("$.business_cards[?(@.card_type=='PAYMENT')].display_payload.payment_attachments[0].file_name")
|
||||
.value(contains("payment-slip.jpg")))
|
||||
.andExpect(jsonPath("$.business_cards[?(@.card_type=='PAYMENT')].display_payload.payment_attachments[0].content_type")
|
||||
.value(contains("image/jpeg")))
|
||||
.andExpect(jsonPath("$.business_cards[?(@.card_type=='PAYMENT')].display_payload.payment_attachments[0].is_image")
|
||||
.value(contains(true)))
|
||||
.andExpect(jsonPath("$.business_cards[?(@.card_type=='PAYMENT')].display_payload.payment_attachments[0].download_available")
|
||||
.value(contains(true)))
|
||||
.andExpect(content().string(not(containsString("\"field_pointer\":\"/business_fields/attachment_ids"))))
|
||||
.andExpect(content().string(not(containsString("https://oss.example.test"))))
|
||||
.andExpect(content().string(not(containsString("raw-event-attachment.jpg"))));
|
||||
List<String> displayPayloads = jdbcTemplate.queryForList("""
|
||||
SELECT display_payload_json
|
||||
FROM workflow_reservation_v4_task_card
|
||||
|
||||
Reference in New Issue
Block a user