补齐订单详情V4任务时间线
This commit is contained in:
@@ -1,17 +1,21 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.common.result;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 前端订单详情响应,包含订单摘要、任务时间线和非阻塞警告。
|
||||
* 前端订单详情响应,包含订单摘要、旧任务时间线、V4 订单任务时间线和非阻塞警告。
|
||||
*
|
||||
* @param order 订单摘要
|
||||
* @param tasks 同订单任务时间线
|
||||
* @param tasks 同订单旧任务时间线
|
||||
* @param v4OrderTasks 同订单 V4 订单任务时间线
|
||||
* @param warnings 当前无法提供的扩展信息或非阻塞提醒
|
||||
*/
|
||||
public record ReservationOrderDetailResult(
|
||||
ReservationOrderSummaryResult order,
|
||||
List<ReservationOrderTaskTimelineItemResult> tasks,
|
||||
@JsonProperty("v4_order_tasks")
|
||||
List<ReservationOrderV4TaskTimelineItemResult> v4OrderTasks,
|
||||
List<String> warnings
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.common.result;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
/**
|
||||
* 订单详情页 V4 订单任务时间线单项。用于旧订单详情页补充展示 V4 多卡任务摘要。
|
||||
*
|
||||
* @param orderTaskId V4 订单任务 ID
|
||||
* @param orderRef V4 回调包内订单引用
|
||||
* @param orderTaskStatus V4 订单任务状态
|
||||
* @param cardCounts V4 卡片数量摘要
|
||||
* @param sourceMessageSummary 来源邮件安全摘要
|
||||
* @param sourceReceivedAt 来源邮件接收 UTC 时间
|
||||
* @param createdAt V4 订单任务创建 UTC 时间
|
||||
* @param updatedAt V4 订单任务更新 UTC 时间
|
||||
* @param latestActivityAt V4 订单任务及其卡片的最新活动 UTC 时间
|
||||
*/
|
||||
public record ReservationOrderV4TaskTimelineItemResult(
|
||||
@JsonProperty("order_task_id")
|
||||
String orderTaskId,
|
||||
@JsonProperty("order_ref")
|
||||
String orderRef,
|
||||
@JsonProperty("order_task_status")
|
||||
String orderTaskStatus,
|
||||
@JsonProperty("card_counts")
|
||||
ReservationV4CardCountsResult cardCounts,
|
||||
@JsonProperty("source_message_summary")
|
||||
ReservationV4SourceMessageSummaryResult sourceMessageSummary,
|
||||
@JsonProperty("source_received_at")
|
||||
OffsetDateTime sourceReceivedAt,
|
||||
@JsonProperty("created_at")
|
||||
OffsetDateTime createdAt,
|
||||
@JsonProperty("updated_at")
|
||||
OffsetDateTime updatedAt,
|
||||
@JsonProperty("latest_activity_at")
|
||||
OffsetDateTime latestActivityAt
|
||||
) {
|
||||
}
|
||||
@@ -9,9 +9,12 @@ import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAiQueryOrde
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAiQueryTaskSnapshot;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationPageSnapshot;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationTaskSnapshot;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4OrderTaskSnapshot;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationV4TaskCardSnapshot;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationOrderKeyType;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationOrderVisibility;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationTaskStatus;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationV4CardStatus;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationOrderListQueryRequest;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationTaskWorkbenchQueryRequest;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationOrderDetailResult;
|
||||
@@ -19,12 +22,17 @@ import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationOrderLis
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationOrderListResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationOrderSummaryResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationOrderTaskTimelineItemResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationOrderV4TaskTimelineItemResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationPaginationResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationTaskAvailabilityResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationTaskWorkbenchItemResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationTaskWorkbenchListResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationV4CardCountsResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationV4SourceMessageSummaryResult;
|
||||
import cn.nianxx.thhotel.workflows.reservation.repository.ReservationAiWorkflowRepository;
|
||||
import cn.nianxx.thhotel.workflows.reservation.repository.ReservationV4WorkflowRepository;
|
||||
import cn.nianxx.thhotel.workflows.reservation.service.ReservationFrontendQueryService;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
@@ -49,6 +57,7 @@ public class ReservationFrontendQueryServiceImpl implements ReservationFrontendQ
|
||||
private static final String REASON_READ_ONLY = "READ_ONLY";
|
||||
|
||||
private final ReservationAiWorkflowRepository workflowRepository;
|
||||
private final ReservationV4WorkflowRepository v4WorkflowRepository;
|
||||
private final SourceMessageQueryService sourceMessageQueryService;
|
||||
private final ReservationTaskAvailabilityResolver availabilityResolver;
|
||||
private final HotelContextService hotelContextService;
|
||||
@@ -58,10 +67,12 @@ public class ReservationFrontendQueryServiceImpl implements ReservationFrontendQ
|
||||
*/
|
||||
public ReservationFrontendQueryServiceImpl(
|
||||
ReservationAiWorkflowRepository workflowRepository,
|
||||
ReservationV4WorkflowRepository v4WorkflowRepository,
|
||||
SourceMessageQueryService sourceMessageQueryService,
|
||||
ReservationTaskAvailabilityResolver availabilityResolver,
|
||||
HotelContextService hotelContextService) {
|
||||
this.workflowRepository = workflowRepository;
|
||||
this.v4WorkflowRepository = v4WorkflowRepository;
|
||||
this.sourceMessageQueryService = sourceMessageQueryService;
|
||||
this.availabilityResolver = availabilityResolver;
|
||||
this.hotelContextService = hotelContextService;
|
||||
@@ -172,7 +183,10 @@ public class ReservationFrontendQueryServiceImpl implements ReservationFrontendQ
|
||||
availabilityOrReadOnly(task, availabilityByTaskId),
|
||||
sourceContextsById.get(task.sourceMessageId())))
|
||||
.toList();
|
||||
return new ReservationOrderDetailResult(toOrderSummary(order), tasks, List.of());
|
||||
List<ReservationOrderV4TaskTimelineItemResult> v4OrderTasks = Boolean.FALSE.equals(includeTasks)
|
||||
? List.of()
|
||||
: findV4OrderTaskTimeline(orderHotelId, order.id());
|
||||
return new ReservationOrderDetailResult(toOrderSummary(order), tasks, v4OrderTasks, List.of());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -304,6 +318,104 @@ public class ReservationFrontendQueryServiceImpl implements ReservationFrontendQ
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询订单详情页 V4 订单任务时间线,复用当前订单酒店作为对象级隔离边界。
|
||||
*/
|
||||
private List<ReservationOrderV4TaskTimelineItemResult> findV4OrderTaskTimeline(String hotelId, Long orderId) {
|
||||
List<ReservationV4OrderTaskSnapshot> orderTasks = v4WorkflowRepository.findOrderTasksByOrderIds(
|
||||
hotelId,
|
||||
List.of(orderId));
|
||||
if (orderTasks.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
Map<Long, List<ReservationV4TaskCardSnapshot>> cardsByOrderTaskId = findV4TaskCardsByOrderTaskId(
|
||||
hotelId,
|
||||
orderTasks);
|
||||
Map<Long, ReservationV4SourceMessageSummaryResult> sourceSummariesById = findV4SourceSummariesById(
|
||||
hotelId,
|
||||
orderTasks.stream().map(ReservationV4OrderTaskSnapshot::sourceMessageId).toList());
|
||||
return orderTasks.stream()
|
||||
.map(orderTask -> toV4TimelineItem(
|
||||
orderTask,
|
||||
cardsByOrderTaskId.getOrDefault(orderTask.id(), List.of()),
|
||||
sourceSummariesById.get(orderTask.sourceMessageId())))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量读取 V4 订单任务卡片并按订单任务 ID 分组,用于订单详情页统计卡片状态。
|
||||
*/
|
||||
private Map<Long, List<ReservationV4TaskCardSnapshot>> findV4TaskCardsByOrderTaskId(
|
||||
String hotelId,
|
||||
List<ReservationV4OrderTaskSnapshot> orderTasks) {
|
||||
List<Long> orderTaskIds = orderTasks.stream()
|
||||
.map(ReservationV4OrderTaskSnapshot::id)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.toList();
|
||||
List<ReservationV4TaskCardSnapshot> cards = v4WorkflowRepository.findTaskCardsByOrderTaskIds(
|
||||
hotelId,
|
||||
orderTaskIds);
|
||||
Map<Long, List<ReservationV4TaskCardSnapshot>> result = new LinkedHashMap<>();
|
||||
for (ReservationV4TaskCardSnapshot card : cards) {
|
||||
result.computeIfAbsent(card.v4OrderTaskId(), ignored -> new java.util.ArrayList<>()).add(card);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量读取 V4 来源邮件安全摘要;若数据缺失或酒店不匹配,只返回带 ID 的空摘要避免泄露。
|
||||
*/
|
||||
private Map<Long, ReservationV4SourceMessageSummaryResult> findV4SourceSummariesById(
|
||||
String hotelId,
|
||||
List<Long> sourceMessageIds) {
|
||||
if (sourceMessageIds == null || sourceMessageIds.isEmpty()) {
|
||||
return Map.of();
|
||||
}
|
||||
List<Long> uniqueIds = sourceMessageIds.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.toList();
|
||||
List<SourceMessageSummaryResponse> summaries = sourceMessageQueryService.getSummariesByIds(uniqueIds);
|
||||
List<SourceMessageSummaryResponse> safeSummaries = summaries.stream()
|
||||
.filter(summary -> Objects.equals(hotelId, summary.hotelId()))
|
||||
.toList();
|
||||
Map<String, Long> conversationCounts = sourceMessageQueryService.countByExternalConversationIds(
|
||||
hotelId,
|
||||
safeSummaries.stream()
|
||||
.map(SourceMessageSummaryResponse::externalConversationId)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.toList());
|
||||
Map<Long, ReservationV4SourceMessageSummaryResult> result = new LinkedHashMap<>();
|
||||
for (SourceMessageSummaryResponse summary : safeSummaries) {
|
||||
Long sourceMessageId = Long.valueOf(summary.id());
|
||||
result.put(sourceMessageId, new ReservationV4SourceMessageSummaryResult(
|
||||
summary.id(),
|
||||
summary.hotelId(),
|
||||
summary.externalMessageId(),
|
||||
summary.externalConversationId(),
|
||||
summary.subject(),
|
||||
summary.senderSummary(),
|
||||
summary.receivedAt(),
|
||||
summary.sourceSentAt(),
|
||||
conversationCounts.getOrDefault(summary.externalConversationId(), 1L)));
|
||||
}
|
||||
for (Long sourceMessageId : uniqueIds) {
|
||||
result.putIfAbsent(sourceMessageId, new ReservationV4SourceMessageSummaryResult(
|
||||
sourceMessageId.toString(),
|
||||
hotelId,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按来源消息安全摘要关键词查找 SourceMessage ID,避免任务列表直接读取来源消息正文。
|
||||
*/
|
||||
@@ -380,6 +492,63 @@ public class ReservationFrontendQueryServiceImpl implements ReservationFrontendQ
|
||||
UtcTimeFormatter.toUtcOffsetDateTime(task.createdAt()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换 V4 订单任务快照为订单详情页时间线安全摘要。
|
||||
*/
|
||||
private ReservationOrderV4TaskTimelineItemResult toV4TimelineItem(
|
||||
ReservationV4OrderTaskSnapshot orderTask,
|
||||
List<ReservationV4TaskCardSnapshot> cards,
|
||||
ReservationV4SourceMessageSummaryResult sourceSummary) {
|
||||
return new ReservationOrderV4TaskTimelineItemResult(
|
||||
orderTask.id().toString(),
|
||||
orderTask.orderRef(),
|
||||
orderTask.orderTaskStatus(),
|
||||
v4CardCounts(cards),
|
||||
sourceSummary,
|
||||
UtcTimeFormatter.toUtcOffsetDateTime(orderTask.sourceReceivedAt()),
|
||||
UtcTimeFormatter.toUtcOffsetDateTime(orderTask.createdAt()),
|
||||
UtcTimeFormatter.toUtcOffsetDateTime(orderTask.updatedAt()),
|
||||
UtcTimeFormatter.toUtcOffsetDateTime(latestV4ActivityAt(orderTask, cards)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计 V4 订单任务下各状态卡片数量,供订单详情时间线轻量展示。
|
||||
*/
|
||||
private ReservationV4CardCountsResult v4CardCounts(List<ReservationV4TaskCardSnapshot> cards) {
|
||||
List<ReservationV4TaskCardSnapshot> safeCards = cards == null ? List.of() : cards;
|
||||
return new ReservationV4CardCountsResult(
|
||||
safeCards.size(),
|
||||
countV4Cards(safeCards, ReservationV4CardStatus.READONLY.name()),
|
||||
countV4Cards(safeCards, ReservationV4CardStatus.PENDING_CONFIRM.name()),
|
||||
countV4Cards(safeCards, ReservationV4CardStatus.REVIEW_REQUIRED.name()),
|
||||
countV4Cards(safeCards, ReservationV4CardStatus.CONFIRMED.name()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计指定 V4 卡片状态数量。
|
||||
*/
|
||||
private int countV4Cards(List<ReservationV4TaskCardSnapshot> cards, String cardStatus) {
|
||||
return Math.toIntExact(cards.stream()
|
||||
.filter(card -> cardStatus.equals(card.cardStatus()))
|
||||
.count());
|
||||
}
|
||||
|
||||
/**
|
||||
* 派生 V4 订单任务最新活动时间,卡片确认或复核后可驱动订单详情时间线更新。
|
||||
*/
|
||||
private LocalDateTime latestV4ActivityAt(
|
||||
ReservationV4OrderTaskSnapshot orderTask,
|
||||
List<ReservationV4TaskCardSnapshot> cards) {
|
||||
LocalDateTime latest = orderTask.updatedAt() == null ? orderTask.createdAt() : orderTask.updatedAt();
|
||||
for (ReservationV4TaskCardSnapshot card : cards == null ? List.<ReservationV4TaskCardSnapshot>of() : cards) {
|
||||
LocalDateTime cardActivityAt = card.updatedAt() == null ? card.createdAt() : card.updatedAt();
|
||||
if (cardActivityAt != null && (latest == null || cardActivityAt.isAfter(latest))) {
|
||||
latest = cardActivityAt;
|
||||
}
|
||||
}
|
||||
return latest;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换订单快照为前端订单列表单行结果。
|
||||
*/
|
||||
|
||||
@@ -24,14 +24,23 @@ import cn.nianxx.thhotel.platform.message.service.SourceMessageQueryService;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationOrderDraft;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationOrderSnapshot;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationTaskDraft;
|
||||
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.ReservationV4TaskCardDraft;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationOrderKeyType;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationOrderStatus;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationOrderVisibility;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationTaskStatus;
|
||||
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.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.ReservationV4WorkflowRepository;
|
||||
import java.sql.Timestamp;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -73,6 +82,9 @@ class ReservationFrontendQueryControllerTest {
|
||||
@MockitoSpyBean
|
||||
private ReservationAiWorkflowRepository workflowRepository;
|
||||
|
||||
@Autowired
|
||||
private ReservationV4WorkflowRepository v4WorkflowRepository;
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
@@ -409,6 +421,7 @@ class ReservationFrontendQueryControllerTest {
|
||||
.andExpect(jsonPath("$.tasks[1].task_status").value("PENDING_CONFIRM"))
|
||||
.andExpect(jsonPath("$.tasks[1].can_process").value(true))
|
||||
.andExpect(jsonPath("$.tasks[1].readonly_reason_code").value("PROCESSABLE"))
|
||||
.andExpect(jsonPath("$.v4_order_tasks.length()").value(0))
|
||||
.andExpect(jsonPath("$.warnings.length()").value(0));
|
||||
verify(workflowRepository, never()).findQueueTasksBefore(anyString(), anyLong(), any());
|
||||
|
||||
@@ -417,7 +430,119 @@ class ReservationFrontendQueryControllerTest {
|
||||
.param("include_tasks", "false"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.order.order_id").value(orderId.toString()))
|
||||
.andExpect(jsonPath("$.tasks.length()").value(0));
|
||||
.andExpect(jsonPath("$.tasks.length()").value(0))
|
||||
.andExpect(jsonPath("$.v4_order_tasks.length()").value(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnOrderDetailWithV4OrderTaskTimeline() throws Exception {
|
||||
SourceMessageCaptureResult laterSource = captureSourceMessage(
|
||||
"mail-frontend-order-detail-v4-later-001",
|
||||
"Frontend V4 Later",
|
||||
Instant.parse("2026-07-08T09:00:00Z"));
|
||||
SourceMessageCaptureResult earlierSource = captureSourceMessage(
|
||||
"mail-frontend-order-detail-v4-earlier-001",
|
||||
"Frontend V4 Earlier",
|
||||
Instant.parse("2026-07-08T07:00:00Z"));
|
||||
Long orderId = 930000000000003001L;
|
||||
insertActiveGroupOrder(orderId, earlierSource.inboxId(), "GRP-FRONTEND-V4-DETAIL-001");
|
||||
ReservationV4OrderTaskSnapshot laterOrderTask = insertV4OrderTask(
|
||||
930000000000003101L,
|
||||
laterSource,
|
||||
orderId,
|
||||
"order-later",
|
||||
"GRP-FRONTEND-V4-LATER-001",
|
||||
Instant.parse("2026-07-08T09:00:00Z"),
|
||||
HOTEL_ID);
|
||||
ReservationV4OrderTaskSnapshot earlierOrderTask = insertV4OrderTask(
|
||||
930000000000003102L,
|
||||
earlierSource,
|
||||
orderId,
|
||||
"order-earlier",
|
||||
"GRP-FRONTEND-V4-EARLIER-001",
|
||||
Instant.parse("2026-07-08T07:00:00Z"),
|
||||
HOTEL_ID);
|
||||
insertV4TaskCard(earlierOrderTask, ReservationV4CardType.SOURCE_MESSAGE_DISPLAY.name(), null, 0, 10,
|
||||
ReservationV4CardStatus.READONLY.name(), null, """
|
||||
{"card_type":"SOURCE_MESSAGE_DISPLAY","source_message":{"subject":"Frontend V4 Earlier"}}
|
||||
""");
|
||||
insertV4TaskCard(earlierOrderTask, ReservationV4CardType.BASIC_INFORMATION.name(), null, 0, 20,
|
||||
ReservationV4CardStatus.CONFIRMED.name(), null, """
|
||||
{"card_type":"BASIC_INFORMATION","order_ref":"order-earlier"}
|
||||
""");
|
||||
insertV4TaskCard(earlierOrderTask, ReservationV4CardType.ROOM_INFORMATION.name(), "NEW_BOOKING", 1, 30,
|
||||
ReservationV4CardStatus.REVIEW_REQUIRED.name(), "PENDING", """
|
||||
{"card_type":"ROOM_INFORMATION","event_type":"NEW_BOOKING","missing_fields":["room_items"]}
|
||||
""");
|
||||
insertV4TaskCard(laterOrderTask, ReservationV4CardType.SOURCE_MESSAGE_DISPLAY.name(), null, 0, 10,
|
||||
ReservationV4CardStatus.READONLY.name(), null, "{}");
|
||||
insertV4TaskCard(laterOrderTask, ReservationV4CardType.BASIC_INFORMATION.name(), null, 0, 20,
|
||||
ReservationV4CardStatus.PENDING_CONFIRM.name(), null, "{}");
|
||||
updateV4OrderTaskUpdatedAt(earlierOrderTask.id(), Instant.parse("2026-07-08T07:10:00Z"));
|
||||
updateV4TaskCardUpdatedAt(earlierOrderTask.id(), 30, Instant.parse("2026-07-08T07:45:00Z"));
|
||||
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/orders/{orderId}", orderId)
|
||||
.param("hotel_id", HOTEL_ID)
|
||||
.param("include_tasks", "true"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.tasks.length()").value(0))
|
||||
.andExpect(jsonPath("$.v4_order_tasks.length()").value(2))
|
||||
.andExpect(jsonPath("$.v4_order_tasks[0].order_task_id").value(earlierOrderTask.id().toString()))
|
||||
.andExpect(jsonPath("$.v4_order_tasks[0].order_ref").value("order-earlier"))
|
||||
.andExpect(jsonPath("$.v4_order_tasks[0].order_task_status").value("OPEN"))
|
||||
.andExpect(jsonPath("$.v4_order_tasks[0].card_counts.total_count").value(3))
|
||||
.andExpect(jsonPath("$.v4_order_tasks[0].card_counts.readonly_count").value(1))
|
||||
.andExpect(jsonPath("$.v4_order_tasks[0].card_counts.review_required_count").value(1))
|
||||
.andExpect(jsonPath("$.v4_order_tasks[0].card_counts.confirmed_count").value(1))
|
||||
.andExpect(jsonPath("$.v4_order_tasks[0].source_message_summary.source_message_id")
|
||||
.value(earlierSource.inboxId().toString()))
|
||||
.andExpect(jsonPath("$.v4_order_tasks[0].source_message_summary.subject").value("Frontend V4 Earlier"))
|
||||
.andExpect(jsonPath("$.v4_order_tasks[0].source_received_at").value("2026-07-08T07:00:00Z"))
|
||||
.andExpect(jsonPath("$.v4_order_tasks[0].created_at").value(matchesPattern(UTC_INSTANT_PATTERN)))
|
||||
.andExpect(jsonPath("$.v4_order_tasks[0].updated_at").value(matchesPattern(UTC_INSTANT_PATTERN)))
|
||||
.andExpect(jsonPath("$.v4_order_tasks[0].latest_activity_at").value("2026-07-08T07:45:00Z"))
|
||||
.andExpect(jsonPath("$.v4_order_tasks[1].order_task_id").value(laterOrderTask.id().toString()))
|
||||
.andExpect(jsonPath("$.v4_order_tasks[1].source_received_at").value("2026-07-08T09:00:00Z"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHideV4OrderTaskTimelineWhenIncludeTasksFalseAndIgnoreCrossHotelRows() throws Exception {
|
||||
SourceMessageCaptureResult source = captureSourceMessage(
|
||||
"mail-frontend-order-detail-v4-compatible-001",
|
||||
"Frontend V4 Compatible",
|
||||
Instant.parse("2026-07-08T10:00:00Z"));
|
||||
Long orderId = 930000000000003201L;
|
||||
insertActiveGroupOrder(orderId, source.inboxId(), "GRP-FRONTEND-V4-COMPATIBLE-001");
|
||||
insertV4OrderTask(
|
||||
930000000000003202L,
|
||||
source,
|
||||
orderId,
|
||||
"order-visible",
|
||||
"GRP-FRONTEND-V4-COMPATIBLE-001",
|
||||
Instant.parse("2026-07-08T10:00:00Z"),
|
||||
HOTEL_ID);
|
||||
insertV4OrderTask(
|
||||
930000000000003203L,
|
||||
source,
|
||||
orderId,
|
||||
"order-cross-hotel",
|
||||
"GRP-FRONTEND-V4-CROSS-001",
|
||||
Instant.parse("2026-07-08T10:05:00Z"),
|
||||
"HOTEL-OTHER");
|
||||
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/orders/{orderId}", orderId)
|
||||
.param("hotel_id", HOTEL_ID)
|
||||
.param("include_tasks", "false"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.tasks.length()").value(0))
|
||||
.andExpect(jsonPath("$.v4_order_tasks.length()").value(0));
|
||||
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/orders/{orderId}", orderId)
|
||||
.param("hotel_id", HOTEL_ID)
|
||||
.param("include_tasks", "true"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.v4_order_tasks.length()").value(1))
|
||||
.andExpect(jsonPath("$.v4_order_tasks[0].order_ref").value("order-visible"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -528,6 +653,74 @@ class ReservationFrontendQueryControllerTest {
|
||||
""", orderVisibility, orderId);
|
||||
}
|
||||
|
||||
private void updateV4OrderTaskUpdatedAt(Long orderTaskId, Instant updatedAt) {
|
||||
jdbcTemplate.update("""
|
||||
UPDATE workflow_reservation_v4_order_task
|
||||
SET updated_at = ?
|
||||
WHERE id = ?
|
||||
""", Timestamp.valueOf(LocalDateTime.ofInstant(updatedAt, ZoneOffset.UTC)), orderTaskId);
|
||||
}
|
||||
|
||||
private void updateV4TaskCardUpdatedAt(Long orderTaskId, Integer cardSortOrder, Instant updatedAt) {
|
||||
jdbcTemplate.update("""
|
||||
UPDATE workflow_reservation_v4_task_card
|
||||
SET updated_at = ?
|
||||
WHERE v4_order_task_id = ?
|
||||
AND card_sort_order = ?
|
||||
""", Timestamp.valueOf(LocalDateTime.ofInstant(updatedAt, ZoneOffset.UTC)), orderTaskId, cardSortOrder);
|
||||
}
|
||||
|
||||
private ReservationV4OrderTaskSnapshot insertV4OrderTask(
|
||||
Long aiBatchId,
|
||||
SourceMessageCaptureResult source,
|
||||
Long orderId,
|
||||
String orderRef,
|
||||
String targetLocatorValue,
|
||||
Instant sourceReceivedAt,
|
||||
String hotelId) {
|
||||
LocalDateTime now = LocalDateTime.ofInstant(sourceReceivedAt.plusSeconds(10), ZoneOffset.UTC);
|
||||
return v4WorkflowRepository.findOrCreateOrderTask(new ReservationV4OrderTaskDraft(
|
||||
hotelId,
|
||||
source.inboxId(),
|
||||
aiBatchId,
|
||||
orderRef,
|
||||
1,
|
||||
orderId,
|
||||
"GROUP",
|
||||
"GROUP_CODE",
|
||||
targetLocatorValue,
|
||||
ReservationV4TargetResolutionStatus.RESOLVED.name(),
|
||||
ReservationV4OrderTaskStatus.OPEN.name(),
|
||||
LocalDateTime.ofInstant(sourceReceivedAt, ZoneOffset.UTC),
|
||||
now));
|
||||
}
|
||||
|
||||
private void insertV4TaskCard(
|
||||
ReservationV4OrderTaskSnapshot orderTask,
|
||||
String cardType,
|
||||
String eventType,
|
||||
Integer sourceEventIndex,
|
||||
Integer cardSortOrder,
|
||||
String cardStatus,
|
||||
String reviewStatus,
|
||||
String displayPayloadJson) {
|
||||
v4WorkflowRepository.insertTaskCard(new ReservationV4TaskCardDraft(
|
||||
orderTask.hotelId(),
|
||||
orderTask.id(),
|
||||
orderTask.sourceMessageId(),
|
||||
eventType == null ? null : orderTask.aiBatchId() + cardSortOrder,
|
||||
cardType,
|
||||
eventType,
|
||||
sourceEventIndex,
|
||||
cardSortOrder,
|
||||
cardStatus,
|
||||
reviewStatus,
|
||||
"{}",
|
||||
displayPayloadJson,
|
||||
null,
|
||||
orderTask.createdAt()));
|
||||
}
|
||||
|
||||
private void insertActiveGroupOrder(Long orderId, Long sourceMessageId, String groupCode) {
|
||||
insertGroupOrder(orderId, sourceMessageId, groupCode, "ACTIVE");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user