修复 V4 查询接口安全与筛选问题
This commit is contained in:
@@ -19,6 +19,8 @@ import java.time.OffsetDateTime;
|
||||
* @param orderTaskStatus 订单任务状态,仅 ORDER_TASK 返回
|
||||
* @param displayStatus 前端展示状态
|
||||
* @param readonlyReasonCode 只读原因稳定码
|
||||
* @param createdAt 条目创建 UTC 时间,用于前端调试和稳定排序兜底
|
||||
* @param updatedAt 条目最近更新 UTC 时间,用于同来源时间下的稳定排序
|
||||
*/
|
||||
public record ReservationV4WorkbenchItemResult(
|
||||
@JsonProperty("item_type")
|
||||
@@ -46,6 +48,10 @@ public record ReservationV4WorkbenchItemResult(
|
||||
@JsonProperty("display_status")
|
||||
String displayStatus,
|
||||
@JsonProperty("readonly_reason_code")
|
||||
String readonlyReasonCode
|
||||
String readonlyReasonCode,
|
||||
@JsonProperty("created_at")
|
||||
OffsetDateTime createdAt,
|
||||
@JsonProperty("updated_at")
|
||||
OffsetDateTime updatedAt
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ public interface ReservationV4OrderTaskMapper extends BaseMapper<ReservationV4Or
|
||||
WHERE c.hotel_id = ot.hotel_id
|
||||
AND c.v4_order_task_id = ot.id
|
||||
AND c.card_status = #{cardStatus}
|
||||
AND c.card_type != 'SOURCE_MESSAGE_DISPLAY'
|
||||
AND c.logic_deleted_at IS NULL
|
||||
)
|
||||
</if>
|
||||
|
||||
@@ -5,10 +5,12 @@ import cn.nianxx.thhotel.platform.hotel.service.HotelContextException;
|
||||
import cn.nianxx.thhotel.platform.hotel.service.HotelContextService;
|
||||
import cn.nianxx.thhotel.platform.message.common.dto.SourceMessageSummaryResponse;
|
||||
import cn.nianxx.thhotel.platform.message.service.SourceMessageQueryService;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAiTransitionSnapshot;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationPageSnapshot;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4OrderTaskSnapshot;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4SourceNotificationSnapshot;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4TaskCardSnapshot;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationAiSystemProcessCategory;
|
||||
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;
|
||||
@@ -18,6 +20,7 @@ import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationV4Workben
|
||||
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;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationAiTransitionDisplayResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationOrderSummaryResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationPaginationResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationV4ActionAvailabilityResult;
|
||||
@@ -32,6 +35,7 @@ import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationV4Source
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationV4TaskCardResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationV4WorkbenchItemResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationV4WorkbenchListResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.repository.ReservationAiWorkflowRepository;
|
||||
import cn.nianxx.thhotel.workflows.reservation.repository.ReservationV4SourceNotificationRepository;
|
||||
import cn.nianxx.thhotel.workflows.reservation.repository.ReservationV4WorkflowRepository;
|
||||
import cn.nianxx.thhotel.workflows.reservation.service.ReservationV4QueryService;
|
||||
@@ -66,6 +70,7 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
|
||||
private static final String DISPLAY_STATUS_OPEN = "OPEN";
|
||||
|
||||
private final ReservationV4WorkflowRepository workflowRepository;
|
||||
private final ReservationAiWorkflowRepository aiWorkflowRepository;
|
||||
private final ReservationV4SourceNotificationRepository sourceNotificationRepository;
|
||||
private final SourceMessageQueryService sourceMessageQueryService;
|
||||
private final HotelContextService hotelContextService;
|
||||
@@ -76,11 +81,13 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
|
||||
*/
|
||||
public ReservationV4QueryServiceImpl(
|
||||
ReservationV4WorkflowRepository workflowRepository,
|
||||
ReservationAiWorkflowRepository aiWorkflowRepository,
|
||||
ReservationV4SourceNotificationRepository sourceNotificationRepository,
|
||||
SourceMessageQueryService sourceMessageQueryService,
|
||||
HotelContextService hotelContextService,
|
||||
ObjectMapper objectMapper) {
|
||||
this.workflowRepository = workflowRepository;
|
||||
this.aiWorkflowRepository = aiWorkflowRepository;
|
||||
this.sourceNotificationRepository = sourceNotificationRepository;
|
||||
this.sourceMessageQueryService = sourceMessageQueryService;
|
||||
this.hotelContextService = hotelContextService;
|
||||
@@ -215,7 +222,7 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
|
||||
basicInformationCard,
|
||||
businessCards,
|
||||
cardCounts(cards),
|
||||
List.of(),
|
||||
adapterContractErrors(orderTask),
|
||||
orderAvailability);
|
||||
}
|
||||
|
||||
@@ -281,7 +288,9 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
|
||||
null,
|
||||
orderTask.orderTaskStatus(),
|
||||
item.displayStatus(),
|
||||
item.availability().readonlyReasonCode());
|
||||
item.availability().readonlyReasonCode(),
|
||||
orderTask.createdAt(),
|
||||
orderTask.updatedAt());
|
||||
}
|
||||
|
||||
private ReservationV4WorkbenchItemResult toWorkbenchSourceNotificationItem(
|
||||
@@ -301,7 +310,9 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
|
||||
notification.notificationStatus(),
|
||||
null,
|
||||
notification.notificationStatus(),
|
||||
availability.readonlyReasonCode());
|
||||
availability.readonlyReasonCode(),
|
||||
UtcTimeFormatter.toUtcOffsetDateTime(notification.createdAt()),
|
||||
UtcTimeFormatter.toUtcOffsetDateTime(notification.updatedAt()));
|
||||
}
|
||||
|
||||
private ReservationV4TaskCardResult toCardResult(
|
||||
@@ -573,7 +584,10 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
|
||||
}
|
||||
List<Long> uniqueIds = sourceMessageIds.stream().filter(Objects::nonNull).distinct().toList();
|
||||
List<SourceMessageSummaryResponse> summaries = sourceMessageQueryService.getSummariesByIds(uniqueIds);
|
||||
List<String> conversationIds = summaries.stream()
|
||||
List<SourceMessageSummaryResponse> safeSummaries = summaries.stream()
|
||||
.filter(summary -> Objects.equals(hotelId, summary.hotelId()))
|
||||
.toList();
|
||||
List<String> conversationIds = safeSummaries.stream()
|
||||
.map(SourceMessageSummaryResponse::externalConversationId)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
@@ -582,7 +596,7 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
|
||||
hotelId,
|
||||
conversationIds);
|
||||
Map<Long, ReservationV4SourceMessageSummaryResult> result = new LinkedHashMap<>();
|
||||
for (SourceMessageSummaryResponse summary : summaries) {
|
||||
for (SourceMessageSummaryResponse summary : safeSummaries) {
|
||||
Long sourceMessageId = Long.valueOf(summary.id());
|
||||
result.put(sourceMessageId, new ReservationV4SourceMessageSummaryResult(
|
||||
summary.id(),
|
||||
@@ -610,6 +624,59 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<ReservationAiTransitionDisplayResult> adapterContractErrors(ReservationV4OrderTaskSnapshot orderTask) {
|
||||
if (orderTask.aiBatchId() == null) {
|
||||
return List.of();
|
||||
}
|
||||
return aiWorkflowRepository.findTransitionsByBatchId(orderTask.hotelId(), orderTask.aiBatchId()).stream()
|
||||
.filter(transition -> ReservationAiSystemProcessCategory.ADAPTER_CONTRACT_ERROR.name()
|
||||
.equals(transition.systemProcessCategory()))
|
||||
.map(this::toTransitionDisplayResult)
|
||||
.toList();
|
||||
}
|
||||
|
||||
private ReservationAiTransitionDisplayResult toTransitionDisplayResult(ReservationAiTransitionSnapshot transition) {
|
||||
return new ReservationAiTransitionDisplayResult(
|
||||
transition.id().toString(),
|
||||
transition.sourceEventIndex(),
|
||||
transition.arrayIndex(),
|
||||
transition.resultType(),
|
||||
transition.aiTaskType(),
|
||||
transition.taskSubtype(),
|
||||
transition.routeCode(),
|
||||
transition.systemProcessCategory(),
|
||||
transition.adapterErrorCode(),
|
||||
transition.adapterErrorMessage(),
|
||||
diagnosticPayloadFragment(transition.aiPayloadJson()));
|
||||
}
|
||||
|
||||
private Object diagnosticPayloadFragment(String aiPayloadJson) {
|
||||
if (!hasText(aiPayloadJson)) {
|
||||
return null;
|
||||
}
|
||||
JsonNode payload = parseJson(aiPayloadJson);
|
||||
Map<String, Object> fragment = new LinkedHashMap<>();
|
||||
copyDiagnosticPayloadField(payload, fragment, "event_type");
|
||||
copyDiagnosticPayloadField(payload, fragment, "source_event_index");
|
||||
copyDiagnosticPayloadField(payload, fragment, "current_or_history");
|
||||
copyDiagnosticPayloadField(payload, fragment, "contract_errors");
|
||||
copyDiagnosticPayloadField(payload, fragment, "intent_type");
|
||||
copyDiagnosticPayloadField(payload, fragment, "reason_code");
|
||||
copyDiagnosticPayloadField(payload, fragment, "missing_fields");
|
||||
copyDiagnosticPayloadField(payload, fragment, "parent_source_event_index");
|
||||
copyDiagnosticPayloadField(payload, fragment, "linked_task_group_id");
|
||||
copyDiagnosticPayloadField(payload, fragment, "blocked_until_parent_completed");
|
||||
return fragment;
|
||||
}
|
||||
|
||||
private void copyDiagnosticPayloadField(JsonNode payload, Map<String, Object> fragment, String fieldName) {
|
||||
JsonNode value = payload == null ? null : payload.get(fieldName);
|
||||
if (value == null || value.isNull() || value.isMissingNode()) {
|
||||
return;
|
||||
}
|
||||
fragment.put(fieldName, objectMapper.convertValue(value, Object.class));
|
||||
}
|
||||
|
||||
private List<Long> findSourceMessageIdsByKeyword(String hotelId, String keyword) {
|
||||
if (!hasText(keyword)) {
|
||||
return List.of();
|
||||
@@ -725,7 +792,21 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
|
||||
private Comparator<ReservationV4WorkbenchItemResult> workbenchItemComparator() {
|
||||
return Comparator.comparing(ReservationV4WorkbenchItemResult::sourceReceivedAt,
|
||||
Comparator.nullsLast(Comparator.reverseOrder()))
|
||||
.thenComparing(ReservationV4WorkbenchItemResult::targetId, Comparator.reverseOrder());
|
||||
.thenComparing(ReservationV4WorkbenchItemResult::updatedAt,
|
||||
Comparator.nullsLast(Comparator.reverseOrder()))
|
||||
.thenComparing(ReservationV4WorkbenchItemResult::createdAt,
|
||||
Comparator.nullsLast(Comparator.reverseOrder()))
|
||||
.thenComparing(this::targetIdAsLong, Comparator.nullsLast(Comparator.reverseOrder()))
|
||||
.thenComparing(ReservationV4WorkbenchItemResult::targetId,
|
||||
Comparator.nullsLast(Comparator.reverseOrder()));
|
||||
}
|
||||
|
||||
private Long targetIdAsLong(ReservationV4WorkbenchItemResult item) {
|
||||
try {
|
||||
return item.targetId() == null ? null : Long.valueOf(item.targetId());
|
||||
} catch (NumberFormatException exception) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private ReservationV4WorkbenchQueryRequest normalizeWorkbenchRequest(ReservationV4WorkbenchQueryRequest request) {
|
||||
@@ -748,13 +829,45 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
|
||||
return new ReservationV4OrderTaskQueryRequest(
|
||||
normalizeHotelId(request.hotelId()),
|
||||
request.orderId(),
|
||||
trimToNull(request.orderTaskStatus()),
|
||||
trimToNull(request.cardStatus()),
|
||||
normalizeOrderTaskStatus(request.orderTaskStatus()),
|
||||
normalizeCardStatus(request.cardStatus()),
|
||||
trimToNull(request.keyword()),
|
||||
request.pageNum(),
|
||||
request.pageSize());
|
||||
}
|
||||
|
||||
private String normalizeOrderTaskStatus(String orderTaskStatus) {
|
||||
String normalized = trimToNull(orderTaskStatus);
|
||||
if (normalized == null) {
|
||||
return null;
|
||||
}
|
||||
for (ReservationV4OrderTaskStatus status : ReservationV4OrderTaskStatus.values()) {
|
||||
if (status.name().equals(normalized)) {
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
throw new ReservationTaskWorkflowException(
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"V4_ORDER_TASK_STATUS_INVALID",
|
||||
"V4 订单任务状态不支持。");
|
||||
}
|
||||
|
||||
private String normalizeCardStatus(String cardStatus) {
|
||||
String normalized = trimToNull(cardStatus);
|
||||
if (normalized == null) {
|
||||
return null;
|
||||
}
|
||||
for (ReservationV4CardStatus status : ReservationV4CardStatus.values()) {
|
||||
if (status.name().equals(normalized)) {
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
throw new ReservationTaskWorkflowException(
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"V4_CARD_STATUS_INVALID",
|
||||
"V4 任务卡状态不支持。");
|
||||
}
|
||||
|
||||
private String normalizeHotelId(String hotelId) {
|
||||
try {
|
||||
return hotelContextService.resolveCurrentHotelId(hotelId);
|
||||
|
||||
@@ -19,16 +19,22 @@ 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.ReservationAiBatchDraft;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAiTransitionDraft;
|
||||
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.enums.AiResultType;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationAiSystemProcessCategory;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationSystemTaskType;
|
||||
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.ReservationAiWorkflowRepository;
|
||||
import cn.nianxx.thhotel.workflows.reservation.repository.ReservationV4SourceNotificationRepository;
|
||||
import cn.nianxx.thhotel.workflows.reservation.repository.ReservationV4WorkflowRepository;
|
||||
import java.time.Instant;
|
||||
@@ -73,6 +79,9 @@ class ReservationV4QueryControllerTest {
|
||||
@Autowired
|
||||
private ReservationV4WorkflowRepository workflowRepository;
|
||||
|
||||
@Autowired
|
||||
private ReservationAiWorkflowRepository aiWorkflowRepository;
|
||||
|
||||
@Autowired
|
||||
private ReservationV4SourceNotificationRepository sourceNotificationRepository;
|
||||
@Autowired
|
||||
@@ -136,6 +145,7 @@ class ReservationV4QueryControllerTest {
|
||||
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/workbench-items")
|
||||
.param("hotel_id", HOTEL_ID)
|
||||
.param("keyword", "mail-v4-query-workbench")
|
||||
.param("page_num", "1")
|
||||
.param("page_size", "20"))
|
||||
.andExpect(status().isOk())
|
||||
@@ -159,6 +169,64 @@ class ReservationV4QueryControllerTest {
|
||||
.andExpect(content().string(not(containsString("Sensitive raw notification body"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnAdapterContractErrorsOnV4OrderTaskDetail() throws Exception {
|
||||
Instant receivedAt = Instant.parse("2026-07-18T03:10:00Z");
|
||||
SourceMessageCaptureResult source = captureSourceMessage(
|
||||
"mail-v4-query-adapter-error-001",
|
||||
"V4 Query Adapter Error",
|
||||
receivedAt,
|
||||
HOTEL_ID);
|
||||
Long batchId = insertAiBatch(source.inboxId(), LocalDateTime.ofInstant(receivedAt, ZoneOffset.UTC));
|
||||
insertAdapterContractErrorTransition(
|
||||
source.inboxId(),
|
||||
batchId,
|
||||
LocalDateTime.ofInstant(receivedAt.plusSeconds(1), ZoneOffset.UTC));
|
||||
ReservationV4OrderTaskSnapshot orderTask = seedOrderTask(
|
||||
source,
|
||||
batchId,
|
||||
receivedAt,
|
||||
"order-adapter-error",
|
||||
"GRP-V4-ADAPTER-001");
|
||||
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/order-tasks/{orderTaskId}", orderTask.id())
|
||||
.param("hotel_id", HOTEL_ID))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.adapter_contract_errors.length()").value(1))
|
||||
.andExpect(jsonPath("$.adapter_contract_errors[0].system_process_category")
|
||||
.value("ADAPTER_CONTRACT_ERROR"))
|
||||
.andExpect(jsonPath("$.adapter_contract_errors[0].adapter_error_code")
|
||||
.value("V4_PAYMENT_ATTACHMENT_NOT_FOUND"))
|
||||
.andExpect(jsonPath("$.adapter_contract_errors[0].payload_fragment.contract_errors[0]")
|
||||
.value("attachment_ids 未匹配来源附件"))
|
||||
.andExpect(content().string(not(containsString("raw_sensitive_payload"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotExposeCrossHotelSourceMessageSummaryWhenV4ReferenceIsPolluted() throws Exception {
|
||||
Instant receivedAt = Instant.parse("2026-07-18T03:20:00Z");
|
||||
SourceMessageCaptureResult otherSource = captureSourceMessage(
|
||||
"mail-v4-query-cross-source-001",
|
||||
"Other Hotel Secret Subject",
|
||||
receivedAt,
|
||||
OTHER_HOTEL_ID);
|
||||
ReservationV4OrderTaskSnapshot orderTask = seedOrderTask(
|
||||
otherSource,
|
||||
990000000000000901L,
|
||||
receivedAt,
|
||||
"order-cross-source",
|
||||
"GRP-V4-CROSS-SOURCE-001");
|
||||
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/order-tasks/{orderTaskId}", orderTask.id())
|
||||
.param("hotel_id", HOTEL_ID))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.source_message_summary.source_message_id")
|
||||
.value(otherSource.inboxId().toString()))
|
||||
.andExpect(jsonPath("$.source_message_summary.subject").doesNotExist())
|
||||
.andExpect(jsonPath("$.source_message_summary.external_message_id").doesNotExist())
|
||||
.andExpect(content().string(not(containsString("Other Hotel Secret Subject"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnV4OrderTaskDetailWithSafeCards() throws Exception {
|
||||
ReservationV4OrderTaskSnapshot orderTask = seedOrderTask("mail-v4-query-detail-001",
|
||||
@@ -203,6 +271,45 @@ class ReservationV4QueryControllerTest {
|
||||
.andExpect(jsonPath("$.page.total").value(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotMatchReadonlySourceMessageDisplayCardWhenFilteringBusinessCards() throws Exception {
|
||||
seedOrderTask("mail-v4-query-readonly-filter-001",
|
||||
Instant.parse("2026-07-18T03:40:00Z"));
|
||||
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/order-tasks")
|
||||
.param("hotel_id", HOTEL_ID)
|
||||
.param("card_status", "READONLY")
|
||||
.param("keyword", "mail-v4-query-readonly-filter-001")
|
||||
.param("page_num", "1")
|
||||
.param("page_size", "20"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.items.length()").value(0))
|
||||
.andExpect(jsonPath("$.page.total").value(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSortSameReceivedAtWorkbenchItemsByUpdatedAtAndId() throws Exception {
|
||||
Instant sameReceivedAt = Instant.parse("2026-07-18T03:50:00Z");
|
||||
ReservationV4SourceNotificationSnapshot notification = seedSourceNotification(
|
||||
"mail-v4-query-same-time-notification-001",
|
||||
"S10",
|
||||
sameReceivedAt);
|
||||
ReservationV4OrderTaskSnapshot orderTask = seedOrderTask(
|
||||
"mail-v4-query-same-time-order-001",
|
||||
sameReceivedAt);
|
||||
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/workbench-items")
|
||||
.param("hotel_id", HOTEL_ID)
|
||||
.param("keyword", "mail-v4-query-same-time")
|
||||
.param("page_num", "1")
|
||||
.param("page_size", "20"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.items[0].item_type").value("SOURCE_NOTIFICATION"))
|
||||
.andExpect(jsonPath("$.items[0].target_id").value(notification.id().toString()))
|
||||
.andExpect(jsonPath("$.items[1].item_type").value("ORDER_TASK"))
|
||||
.andExpect(jsonPath("$.items[1].target_id").value(orderTask.id().toString()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnV4SourceNotificationDetailWithoutRawPayload() throws Exception {
|
||||
ReservationV4SourceNotificationSnapshot notification = seedSourceNotification(
|
||||
@@ -296,6 +403,21 @@ class ReservationV4QueryControllerTest {
|
||||
.andExpect(jsonPath("$.error_code").value("V4_WORKBENCH_ITEM_TYPE_INVALID"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectUnsupportedOrderTaskStatusAndCardStatus() throws Exception {
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/order-tasks")
|
||||
.param("hotel_id", HOTEL_ID)
|
||||
.param("order_task_status", "UNKNOWN"))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.error_code").value("V4_ORDER_TASK_STATUS_INVALID"));
|
||||
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/order-tasks")
|
||||
.param("hotel_id", HOTEL_ID)
|
||||
.param("card_status", "UNKNOWN"))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.error_code").value("V4_CARD_STATUS_INVALID"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCapHugeWorkbenchPageSafely() throws Exception {
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/workbench-items")
|
||||
@@ -308,18 +430,32 @@ class ReservationV4QueryControllerTest {
|
||||
}
|
||||
|
||||
private ReservationV4OrderTaskSnapshot seedOrderTask(String externalMessageId, Instant receivedAt) {
|
||||
SourceMessageCaptureResult source = captureSourceMessage(externalMessageId, "V4 Query Business", receivedAt);
|
||||
SourceMessageCaptureResult source = captureSourceMessage(externalMessageId, "V4 Query Business", receivedAt, HOTEL_ID);
|
||||
return seedOrderTask(
|
||||
source,
|
||||
990000000000000001L,
|
||||
receivedAt,
|
||||
"order-1",
|
||||
"GRP-V4-QUERY-001");
|
||||
}
|
||||
|
||||
private ReservationV4OrderTaskSnapshot seedOrderTask(
|
||||
SourceMessageCaptureResult source,
|
||||
Long aiBatchId,
|
||||
Instant receivedAt,
|
||||
String orderRef,
|
||||
String targetLocatorValue) {
|
||||
LocalDateTime now = LocalDateTime.ofInstant(receivedAt.plusSeconds(10), ZoneOffset.UTC);
|
||||
ReservationV4OrderTaskSnapshot orderTask = workflowRepository.findOrCreateOrderTask(new ReservationV4OrderTaskDraft(
|
||||
HOTEL_ID,
|
||||
source.inboxId(),
|
||||
990000000000000001L,
|
||||
"order-1",
|
||||
aiBatchId,
|
||||
orderRef,
|
||||
1,
|
||||
null,
|
||||
"GROUP",
|
||||
"GROUP_CODE",
|
||||
"GRP-V4-QUERY-001",
|
||||
targetLocatorValue,
|
||||
ReservationV4TargetResolutionStatus.RESOLVED.name(),
|
||||
ReservationV4OrderTaskStatus.OPEN.name(),
|
||||
LocalDateTime.ofInstant(receivedAt, ZoneOffset.UTC),
|
||||
@@ -346,7 +482,8 @@ class ReservationV4QueryControllerTest {
|
||||
SourceMessageCaptureResult source = captureSourceMessage(
|
||||
externalMessageId,
|
||||
"V4 Query Notification",
|
||||
receivedAt);
|
||||
receivedAt,
|
||||
HOTEL_ID);
|
||||
return sourceNotificationRepository.findOrCreateSourceNotification(new ReservationV4SourceNotificationDraft(
|
||||
HOTEL_ID,
|
||||
source.inboxId(),
|
||||
@@ -392,9 +529,10 @@ class ReservationV4QueryControllerTest {
|
||||
private SourceMessageCaptureResult captureSourceMessage(
|
||||
String externalMessageId,
|
||||
String subject,
|
||||
Instant receivedAt) {
|
||||
Instant receivedAt,
|
||||
String hotelId) {
|
||||
return captureService.capture(new CaptureSourceMessageCommand(
|
||||
HOTEL_ID,
|
||||
hotelId,
|
||||
"AGENTBUS",
|
||||
"EMAIL",
|
||||
externalMessageId,
|
||||
@@ -412,4 +550,57 @@ class ReservationV4QueryControllerTest {
|
||||
List.of()
|
||||
));
|
||||
}
|
||||
|
||||
private Long insertAiBatch(Long sourceMessageId, LocalDateTime receivedAt) {
|
||||
return aiWorkflowRepository.insertBatch(new ReservationAiBatchDraft(
|
||||
HOTEL_ID,
|
||||
sourceMessageId,
|
||||
"sha256-v4-query-adapter-error",
|
||||
"batch-v4-query-adapter-error",
|
||||
"superagent-test",
|
||||
"request-v4-query-adapter-error",
|
||||
receivedAt,
|
||||
2,
|
||||
null));
|
||||
}
|
||||
|
||||
private void insertAdapterContractErrorTransition(Long sourceMessageId, Long batchId, LocalDateTime now) {
|
||||
aiWorkflowRepository.insertTransition(new ReservationAiTransitionDraft(
|
||||
HOTEL_ID,
|
||||
batchId,
|
||||
sourceMessageId,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
"P0",
|
||||
"reservation-v4",
|
||||
AiResultType.ADAPTER_CONTRACT_ERROR.code(),
|
||||
"adapter_contract_error",
|
||||
null,
|
||||
ReservationAiSystemProcessCategory.ADAPTER_CONTRACT_ERROR.name(),
|
||||
ReservationSystemTaskType.ADAPTER_CONTRACT_ERROR.name(),
|
||||
"ADAPTER_CONTRACT_ERROR",
|
||||
null,
|
||||
"current",
|
||||
null,
|
||||
null,
|
||||
"sha256-v4-query-adapter-error-item",
|
||||
"item-v4-query-adapter-error",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
"""
|
||||
{"event_type":"PAYMENT","source_event_index":2,"contract_errors":["attachment_ids 未匹配来源附件"],"raw_sensitive_payload":"must not leak"}
|
||||
""",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"V4_PAYMENT_ATTACHMENT_NOT_FOUND",
|
||||
"PAYMENT.attachment_ids 未匹配来源附件。",
|
||||
now));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user