完成M002 V3 CP4列表详情展示

This commit is contained in:
andy
2026-07-11 15:53:22 +08:00
parent 9bcbe8a3ac
commit 6416c1b78a
15 changed files with 647 additions and 30 deletions

View File

@@ -29,6 +29,10 @@ import java.time.LocalDateTime;
* @param transitionSourceEventIndex AI transition 来源事件序号
* @param catalogCode Skill 目录代码
* @param skillId Skill 标识
* @param routeCode M002 V3 路由码
* @param systemProcessCategory 系统处理分类
* @param adapterErrorCode Adapter 契约错误代码
* @param adapterErrorMessage Adapter 契约错误说明
*/
public record ReservationAiQueryTaskSnapshot(
Long id,
@@ -54,6 +58,10 @@ public record ReservationAiQueryTaskSnapshot(
LocalDateTime updatedAt,
Integer transitionSourceEventIndex,
String catalogCode,
String skillId
String skillId,
String routeCode,
String systemProcessCategory,
String adapterErrorCode,
String adapterErrorMessage
) {
}

View File

@@ -0,0 +1,55 @@
package cn.nianxx.thhotel.workflows.reservation.common.dto;
import java.time.LocalDateTime;
/**
* AI transition 展示快照。用于任务详情展示同一批次的 V3 路由、契约错误和未处理意图,不直接暴露 Entity。
*
* @param id AI transition 主键
* @param hotelId 酒店上下文 ID
* @param batchId AI 入站批次 ID
* @param sourceMessageId 来源 SourceMessage Inbox ID
* @param sourceEventIndex AI current 事件序号
* @param arrayIndex AI 返回数组顺序,从 1 开始
* @param executionOrder 初始执行顺序
* @param catalogCode Skill 目录代码
* @param skillId Skill 标识
* @param resultType AI 结果类型
* @param aiTaskType AI 原始任务类型
* @param routeCode M002 V3 路由码
* @param systemProcessCategory 系统处理分类
* @param systemTaskType 系统主任务类型
* @param taskCardType 任务卡类型
* @param taskSubtype 业务动作 subtype
* @param currentOrHistory 当前或历史标识
* @param adapterErrorCode Adapter 契约错误代码
* @param adapterErrorMessage Adapter 契约错误说明
* @param aiPayloadJson AI 原始 item JSON
* @param createdAt 创建 UTC 时间
* @param updatedAt 最近更新 UTC 时间
*/
public record ReservationAiTransitionSnapshot(
Long id,
String hotelId,
Long batchId,
Long sourceMessageId,
Integer sourceEventIndex,
Integer arrayIndex,
Integer executionOrder,
String catalogCode,
String skillId,
String resultType,
String aiTaskType,
String routeCode,
String systemProcessCategory,
String systemTaskType,
String taskCardType,
String taskSubtype,
String currentOrHistory,
String adapterErrorCode,
String adapterErrorMessage,
String aiPayloadJson,
LocalDateTime createdAt,
LocalDateTime updatedAt
) {
}

View File

@@ -0,0 +1,44 @@
package cn.nianxx.thhotel.workflows.reservation.common.result;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* 任务详情中的 AI transition 展示块。用于展示未建任务的关联 V3 诊断信息。
*
* @param transitionId AI transition ID按字符串返回避免前端长整型精度问题
* @param sourceEventIndex AI current 事件序号
* @param arrayIndex AI 返回数组顺序
* @param resultType AI 结果类型
* @param aiTaskType AI 原始任务类型
* @param taskSubtype 业务动作 subtype
* @param routeCode M002 V3 路由码
* @param systemProcessCategory 系统处理分类
* @param adapterErrorCode Adapter 契约错误代码
* @param adapterErrorMessage Adapter 契约错误说明
* @param payloadFragment 对前端展示安全的 AI item 片段,当前不包含邮件原文
*/
public record ReservationAiTransitionDisplayResult(
@JsonProperty("transition_id")
String transitionId,
@JsonProperty("source_event_index")
Integer sourceEventIndex,
@JsonProperty("array_index")
Integer arrayIndex,
@JsonProperty("result_type")
String resultType,
@JsonProperty("ai_task_type")
String aiTaskType,
@JsonProperty("task_subtype")
String taskSubtype,
@JsonProperty("route_code")
String routeCode,
@JsonProperty("system_process_category")
String systemProcessCategory,
@JsonProperty("adapter_error_code")
String adapterErrorCode,
@JsonProperty("adapter_error_message")
String adapterErrorMessage,
@JsonProperty("payload_fragment")
Object payloadFragment
) {
}

View File

@@ -8,7 +8,11 @@ import java.time.OffsetDateTime;
*
* @param taskId 任务 ID
* @param taskType 系统主任务类型
* @param resultType AI 结果类型
* @param aiTaskType AI 原始任务类型
* @param taskSubtype 任务 subtype
* @param routeCode M002 V3 路由码
* @param systemProcessCategory 系统处理分类
* @param taskStatus 任务状态
* @param cardName 任务卡展示名称
* @param queueSequence 同订单队列顺序
@@ -28,8 +32,16 @@ public record ReservationOrderTaskTimelineItemResult(
String taskId,
@JsonProperty("task_type")
String taskType,
@JsonProperty("result_type")
String resultType,
@JsonProperty("ai_task_type")
String aiTaskType,
@JsonProperty("task_subtype")
String taskSubtype,
@JsonProperty("route_code")
String routeCode,
@JsonProperty("system_process_category")
String systemProcessCategory,
@JsonProperty("task_status")
String taskStatus,
@JsonProperty("card_name")

View File

@@ -9,6 +9,11 @@ import com.fasterxml.jackson.annotation.JsonProperty;
* @param entryResultMeaning 入口结果语义
* @param entryResultDescription 入口结果中文说明
* @param entryResultSourceMessageId SuperAgent 回传的外部来源消息 ID
* @param resultType AI 结果类型
* @param routeCode M002 V3 路由码
* @param agentAssessment S10/S99 入口评估对象
* @param notification S10/S99 入口通知对象
* @param manualReview S99 人工复核对象S10 通常为空
* @param rawAnswer SuperAgent 原始文本返回
*/
public record ReservationSourceMessageOnlyResult(
@@ -20,6 +25,16 @@ public record ReservationSourceMessageOnlyResult(
String entryResultDescription,
@JsonProperty("entry_result_source_message_id")
String entryResultSourceMessageId,
@JsonProperty("result_type")
String resultType,
@JsonProperty("route_code")
String routeCode,
@JsonProperty("agent_assessment")
Object agentAssessment,
@JsonProperty("notification")
Object notification,
@JsonProperty("manual_review")
Object manualReview,
@JsonProperty("raw_answer")
String rawAnswer
) {

View File

@@ -17,6 +17,11 @@ import java.util.List;
* @param conversationMessageCount 同一外部邮件会话下的消息数量
* @param systemTaskType 系统主任务类型
* @param taskCardType 任务卡类型
* @param resultType AI 结果类型
* @param aiTaskType AI 原始任务类型
* @param taskSubtype 业务动作 subtype
* @param routeCode M002 V3 路由码
* @param systemProcessCategory 系统处理分类
* @param taskStatus 任务状态
* @param fieldContractVersion 字段矩阵契约版本
* @param draftPayload 草稿 payload
@@ -25,6 +30,8 @@ import java.util.List;
* @param availability 当前可处理状态
* @param fields 按字段矩阵生成的字段列表
* @param operaOperations OPERA 模拟操作列表
* @param adapterContractErrors 同批次未建任务的 Adapter 契约错误展示块
* @param unhandledIntents 同批次未建任务的未处理意图展示块
*/
public record ReservationTaskDetailResult(
@JsonProperty("task_id")
@@ -47,6 +54,16 @@ public record ReservationTaskDetailResult(
String systemTaskType,
@JsonProperty("task_card_type")
String taskCardType,
@JsonProperty("result_type")
String resultType,
@JsonProperty("ai_task_type")
String aiTaskType,
@JsonProperty("task_subtype")
String taskSubtype,
@JsonProperty("route_code")
String routeCode,
@JsonProperty("system_process_category")
String systemProcessCategory,
@JsonProperty("task_status")
String taskStatus,
@JsonProperty("field_contract_version")
@@ -60,6 +77,10 @@ public record ReservationTaskDetailResult(
ReservationTaskAvailabilityResult availability,
List<ReservationTaskFieldResult> fields,
@JsonProperty("opera_operations")
List<ReservationOperaOperationResult> operaOperations
List<ReservationOperaOperationResult> operaOperations,
@JsonProperty("adapter_contract_errors")
List<ReservationAiTransitionDisplayResult> adapterContractErrors,
@JsonProperty("unhandled_intents")
List<ReservationAiTransitionDisplayResult> unhandledIntents
) {
}

View File

@@ -12,7 +12,11 @@ import java.time.OffsetDateTime;
* @param displayOrderKey 前端优先展示的订单业务号或临时订单号
* @param temporaryOrderNo 临时订单号
* @param taskType 系统主任务类型
* @param resultType AI 结果类型
* @param aiTaskType AI 原始任务类型
* @param taskSubtype 任务 subtype
* @param routeCode M002 V3 路由码
* @param systemProcessCategory 系统处理分类
* @param taskStatus 任务状态
* @param cardName 任务卡展示名称,第一版使用任务卡类型
* @param queueSequence 同订单队列顺序
@@ -41,8 +45,16 @@ public record ReservationTaskWorkbenchItemResult(
String temporaryOrderNo,
@JsonProperty("task_type")
String taskType,
@JsonProperty("result_type")
String resultType,
@JsonProperty("ai_task_type")
String aiTaskType,
@JsonProperty("task_subtype")
String taskSubtype,
@JsonProperty("route_code")
String routeCode,
@JsonProperty("system_process_category")
String systemProcessCategory,
@JsonProperty("task_status")
String taskStatus,
@JsonProperty("card_name")

View File

@@ -5,6 +5,7 @@ import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAiBatchSnap
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAiQueryOrderSnapshot;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAiQueryTaskSnapshot;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAiTransitionDraft;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAiTransitionSnapshot;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAuditLogDraft;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAuditLogSnapshot;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationOperaOperationAttemptDraft;
@@ -288,6 +289,40 @@ public class MybatisReservationAiWorkflowRepository implements ReservationAiWork
return Optional.ofNullable(entity).map(this::toTaskCardSnapshot);
}
/**
* 按 AI transition ID 查询展示快照,用于任务详情补充 V3 入站路由上下文。
*/
@Override
public Optional<ReservationAiTransitionSnapshot> findTransitionById(String hotelId, Long transitionId) {
if (transitionId == null) {
return Optional.empty();
}
ReservationAiTransitionEntity entity = transitionMapper.selectOne(
Wrappers.<ReservationAiTransitionEntity>lambdaQuery()
.eq(ReservationAiTransitionEntity::getHotelId, hotelId)
.eq(ReservationAiTransitionEntity::getId, transitionId)
.last("LIMIT 1"));
return Optional.ofNullable(entity).map(this::toAiTransitionSnapshot);
}
/**
* 按 AI 批次查询全部 transition 展示快照,按 AI 返回顺序稳定排序。
*/
@Override
public List<ReservationAiTransitionSnapshot> findTransitionsByBatchId(String hotelId, Long batchId) {
if (batchId == null) {
return List.of();
}
return transitionMapper.selectList(Wrappers.<ReservationAiTransitionEntity>lambdaQuery()
.eq(ReservationAiTransitionEntity::getHotelId, hotelId)
.eq(ReservationAiTransitionEntity::getBatchId, batchId)
.orderByAsc(ReservationAiTransitionEntity::getArrayIndex)
.orderByAsc(ReservationAiTransitionEntity::getId))
.stream()
.map(this::toAiTransitionSnapshot)
.toList();
}
/**
* 查询同订单前置队列任务Message Notification 等非队列任务不会进入结果。
*/
@@ -1021,7 +1056,40 @@ public class MybatisReservationAiWorkflowRepository implements ReservationAiWork
entity.getUpdatedAt(),
transition == null ? null : transition.getSourceEventIndex(),
transition == null ? null : transition.getCatalogCode(),
transition == null ? null : transition.getSkillId());
transition == null ? null : transition.getSkillId(),
transition == null ? null : transition.getRouteCode(),
transition == null ? null : transition.getSystemProcessCategory(),
transition == null ? null : transition.getAdapterErrorCode(),
transition == null ? null : transition.getAdapterErrorMessage());
}
/**
* 转换 AI transition 实体为展示快照。
*/
private ReservationAiTransitionSnapshot toAiTransitionSnapshot(ReservationAiTransitionEntity entity) {
return new ReservationAiTransitionSnapshot(
entity.getId(),
entity.getHotelId(),
entity.getBatchId(),
entity.getSourceMessageId(),
entity.getSourceEventIndex(),
entity.getArrayIndex(),
entity.getExecutionOrder(),
entity.getCatalogCode(),
entity.getSkillId(),
entity.getResultType(),
entity.getAiTaskType(),
entity.getRouteCode(),
entity.getSystemProcessCategory(),
entity.getSystemTaskType(),
entity.getTaskCardType(),
entity.getTaskSubtype(),
entity.getCurrentOrHistory(),
entity.getAdapterErrorCode(),
entity.getAdapterErrorMessage(),
entity.getAiPayloadJson(),
entity.getCreatedAt(),
entity.getUpdatedAt());
}
/**

View File

@@ -4,6 +4,7 @@ import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAiBatchDraf
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAiBatchSnapshot;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAiQueryOrderSnapshot;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAiQueryTaskSnapshot;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAiTransitionSnapshot;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAiTransitionDraft;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAuditLogDraft;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAuditLogSnapshot;
@@ -82,6 +83,16 @@ public interface ReservationAiWorkflowRepository {
*/
Optional<ReservationTaskCardSnapshot> findTaskCardByTaskId(String hotelId, Long taskId);
/**
* 按 AI transition ID 查询展示快照,用于任务详情补充 V3 路由和批次上下文。
*/
Optional<ReservationAiTransitionSnapshot> findTransitionById(String hotelId, Long transitionId);
/**
* 查询同一 AI 批次下的 transition 展示快照,用于任务详情展示未建任务的契约错误和未处理意图。
*/
List<ReservationAiTransitionSnapshot> findTransitionsByBatchId(String hotelId, Long batchId);
/**
* 查询同订单中排在当前任务之前且参与队列的任务。
*/

View File

@@ -329,7 +329,11 @@ public class ReservationFrontendQueryServiceImpl implements ReservationFrontendQ
displayOrderKey(order),
order == null || isHiddenSystemOrder(order) ? null : order.temporaryOrderCode(),
task.systemTaskType(),
task.resultType(),
task.aiTaskType(),
task.taskSubtype(),
task.routeCode(),
task.systemProcessCategory(),
task.taskStatus(),
task.taskCardType(),
task.executionOrder(),
@@ -356,7 +360,11 @@ public class ReservationFrontendQueryServiceImpl implements ReservationFrontendQ
return new ReservationOrderTaskTimelineItemResult(
task.id().toString(),
task.systemTaskType(),
task.resultType(),
task.aiTaskType(),
task.taskSubtype(),
task.routeCode(),
task.systemProcessCategory(),
task.taskStatus(),
task.taskCardType(),
task.executionOrder(),

View File

@@ -4,6 +4,7 @@ import cn.nianxx.thhotel.platform.message.common.dto.SourceMessageSummaryRespons
import cn.nianxx.thhotel.platform.message.service.SourceMessageQueryService;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAuditLogDraft;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAuditLogSnapshot;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAiTransitionSnapshot;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationOperaOperationAttemptDraft;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationOperaOperationAttemptSnapshot;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationOperaOperationDraft;
@@ -12,6 +13,7 @@ import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationOrderSnapsh
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationTaskCardFieldDefinition;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationTaskCardSnapshot;
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationTaskSnapshot;
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationAiSystemProcessCategory;
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationBusinessKeySource;
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationOperaAttemptStatus;
import cn.nianxx.thhotel.workflows.reservation.common.enums.ReservationOperaOperationStatus;
@@ -24,6 +26,7 @@ import cn.nianxx.thhotel.workflows.reservation.common.request.ManualReviewConver
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationOperaSimulationRequest;
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationTaskPayloadMutationRequest;
import cn.nianxx.thhotel.workflows.reservation.common.result.ManualReviewConversionResult;
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationAiTransitionDisplayResult;
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationOperaOperationAttemptResult;
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationOperaOperationResult;
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationSourceMessageOnlyResult;
@@ -112,6 +115,8 @@ public class ReservationTaskWorkflowServiceImpl implements ReservationTaskWorkfl
List<ReservationOperaOperationResult> operaOperations = findOperaOperationResults(task);
ReservationSourceMessageOnlyResult sourceMessageOnlyResult = buildSourceMessageOnlyResult(task, taskCard);
SourceMessageDetailContext sourceContext = findSourceMessageDetailContext(task);
ReservationAiTransitionSnapshot transition = findTaskTransition(task);
List<ReservationAiTransitionSnapshot> relatedTransitions = findRelatedTransitions(task, transition);
return new ReservationTaskDetailResult(
task.id().toString(),
task.orderId().toString(),
@@ -123,6 +128,11 @@ public class ReservationTaskWorkflowServiceImpl implements ReservationTaskWorkfl
sourceContext.conversationMessageCount(),
task.systemTaskType(),
task.taskCardType(),
task.resultType(),
task.aiTaskType(),
task.taskSubtype(),
transition == null ? null : transition.routeCode(),
transition == null ? null : transition.systemProcessCategory(),
task.taskStatus(),
taskCard.fieldContractVersion(),
jsonPayloadToObject(taskCard.draftPayloadJson()),
@@ -130,7 +140,13 @@ public class ReservationTaskWorkflowServiceImpl implements ReservationTaskWorkfl
sourceMessageOnlyResult,
availability,
fields,
operaOperations);
operaOperations,
buildTransitionDisplayResults(
relatedTransitions,
ReservationAiSystemProcessCategory.ADAPTER_CONTRACT_ERROR),
buildTransitionDisplayResults(
relatedTransitions,
ReservationAiSystemProcessCategory.UNHANDLED_CURRENT_INTENT));
}
/**
@@ -854,9 +870,86 @@ public class ReservationTaskWorkflowServiceImpl implements ReservationTaskWorkfl
textValue(payload, "entry_result_meaning"),
textValue(payload, "entry_result_description"),
textValue(payload, "source_message_id"),
textValue(payload, "result_type"),
textValue(payload, "route_code"),
rawAnswerField(payload, "agent_assessment"),
rawAnswerField(payload, "notification"),
rawAnswerField(payload, "manual_review"),
textValue(payload, "raw_answer"));
}
/**
* 查询当前任务对应的 AI transition用于详情展示 V3 route 和系统处理分类。
*/
private ReservationAiTransitionSnapshot findTaskTransition(ReservationTaskSnapshot task) {
return workflowRepository.findTransitionById(task.hotelId(), task.aiTransitionId()).orElse(null);
}
/**
* 查询同一 AI 批次下的关联 transition用于展示没有生成任务的诊断类结果。
*/
private List<ReservationAiTransitionSnapshot> findRelatedTransitions(
ReservationTaskSnapshot task,
ReservationAiTransitionSnapshot transition) {
if (transition == null) {
return List.of();
}
return workflowRepository.findTransitionsByBatchId(task.hotelId(), transition.batchId());
}
/**
* 按系统处理分类构造详情页展示块。
*/
private List<ReservationAiTransitionDisplayResult> buildTransitionDisplayResults(
List<ReservationAiTransitionSnapshot> transitions,
ReservationAiSystemProcessCategory category) {
if (transitions == null || transitions.isEmpty() || category == null) {
return List.of();
}
return transitions.stream()
.filter(transition -> category.name().equals(transition.systemProcessCategory()))
.map(this::toTransitionDisplayResult)
.toList();
}
/**
* 转换 AI transition 快照为前端详情展示块。
*/
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(),
jsonPayloadToObject(transition.aiPayloadJson()));
}
/**
* 从 S10/S99 原始回答中提取指定对象字段;旧 S000/S999 文本无法解析时返回 null。
*/
private Object rawAnswerField(JsonNode payload, String fieldName) {
String rawAnswer = textValue(payload, "raw_answer");
if (trimToNull(rawAnswer) == null || trimToNull(fieldName) == null) {
return null;
}
try {
JsonNode root = objectMapper.readTree(rawAnswer);
JsonNode fieldNode = root.path(fieldName);
if (fieldNode.isMissingNode() || fieldNode.isNull()) {
return null;
}
return objectMapper.convertValue(fieldNode, Object.class);
} catch (Exception exception) {
return null;
}
}
/**
* 写入草稿保存或最终确认审计。审计只记录 payload 摘要,不保存完整客户字段。
*/

View File

@@ -745,6 +745,78 @@ class SuperAgentTaskResultControllerTest {
assertThat(taskCount).isEqualTo(1L);
}
@Test
void shouldExposeStructuredS99NotificationInTaskListAndDetail() throws Exception {
captureSourceMessage("mail-v3-s99-frontend-visible-001");
String body = """
{
"source_message": {
"source_message_id": "mail-v3-s99-frontend-visible-001",
"subject": null,
"from": null,
"cc": [],
"received_at": null,
"source_channel": "Email"
},
"route_code": "S99",
"handler_type": "main_agent_outcome",
"result_type": "source_message_review_notification",
"current_or_history": "current",
"agent_assessment": {
"status": "insufficient_business_material",
"reason_code": "cannot_form_business_material_package",
"automation_action": "none"
},
"notification": {
"required": true,
"notification_type": "source_message_review",
"show_source_message": true,
"requires_user_decision": true
},
"manual_review": {
"reason_code": "cannot_form_business_material_package",
"review_notes": "需要人工查看原邮件。"
}
}
""";
MvcResult result = mockMvc.perform(signedPost(body, "nonce-v3-s99-frontend-visible-001"))
.andExpect(status().isCreated())
.andReturn();
String taskId = com.jayway.jsonpath.JsonPath.read(
result.getResponse().getContentAsString(),
"$.items[0].task_id"
);
mockMvc.perform(get("/api/reservation/tasks")
.param("hotel_id", "HOTEL-TEST")
.param("task_type", "SOURCE_MESSAGE_ONLY")
.param("keyword", "mail-v3-s99-frontend-visible-001"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.items[0].task_id").value(taskId))
.andExpect(jsonPath("$.items[0].result_type").value("source_message_review_notification"))
.andExpect(jsonPath("$.items[0].ai_task_type").value("S99"))
.andExpect(jsonPath("$.items[0].route_code").value("S99"))
.andExpect(jsonPath("$.items[0].system_process_category").value("SOURCE_MESSAGE_NOTIFICATION"));
mockMvc.perform(get("/api/reservation/tasks/{taskId}", taskId))
.andExpect(status().isOk())
.andExpect(jsonPath("$.result_type").value("source_message_review_notification"))
.andExpect(jsonPath("$.ai_task_type").value("S99"))
.andExpect(jsonPath("$.task_subtype").value("S99"))
.andExpect(jsonPath("$.route_code").value("S99"))
.andExpect(jsonPath("$.system_process_category").value("SOURCE_MESSAGE_NOTIFICATION"))
.andExpect(jsonPath("$.source_message_only_result.result_type")
.value("source_message_review_notification"))
.andExpect(jsonPath("$.source_message_only_result.route_code").value("S99"))
.andExpect(jsonPath("$.source_message_only_result.agent_assessment.status")
.value("insufficient_business_material"))
.andExpect(jsonPath("$.source_message_only_result.notification.notification_type")
.value("source_message_review"))
.andExpect(jsonPath("$.source_message_only_result.manual_review.reason_code")
.value("cannot_form_business_material_package"));
}
@Test
void shouldCreateBusinessTaskFromV3BusinessRootMessageEvent() throws Exception {
SourceMessageCaptureResult source = captureSourceMessage("mail-v3-business-root-001");
@@ -968,6 +1040,116 @@ class SuperAgentTaskResultControllerTest {
assertThat(taskCount).isEqualTo(1L);
}
@Test
void shouldExposeV3RouteAndRelatedTransitionBlocksInFrontendQueries() throws Exception {
captureSourceMessage("mail-v3-frontend-route-blocks-001");
String body = """
{
"source_message": {
"source_message_id": "mail-v3-frontend-route-blocks-001",
"subject": "Mixed events",
"from": null,
"cc": [],
"received_at": null,
"source_channel": "Email"
},
"message_events": [
{
"event_type": "Trace",
"source_event_index": "E1",
"current_or_history": "current",
"case_keys": {
"group_code": "GRP-V3-FRONTEND-BLOCK-001",
"confirmation_number": null
},
"extracted_fields": {},
"contract_errors": [
{
"path": "/extracted_fields",
"code": "unsupported_contract"
}
],
"manual_review": null
},
{
"event_type": "New Booking",
"source_event_index": "E2",
"current_or_history": "current",
"case_keys": {
"group_code": null,
"confirmation_number": "CNF-V3-FRONTEND-BLOCK-001"
},
"extracted_fields": {
"booking_object_type": "FIT Reservation"
},
"manual_review": null
}
],
"case_candidates": [],
"extraction_warnings": [],
"unhandled_current_intents": [
{
"intent_type": "unsupported_active_request",
"reason_code": "no_task_card_available"
}
]
}
""";
MvcResult result = mockMvc.perform(signedPost(body, "nonce-v3-frontend-route-blocks-001"))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.accepted_count").value(3))
.andReturn();
String orderId = com.jayway.jsonpath.JsonPath.read(
result.getResponse().getContentAsString(),
"$.items[1].order_id"
);
String taskId = com.jayway.jsonpath.JsonPath.read(
result.getResponse().getContentAsString(),
"$.items[1].task_id"
);
mockMvc.perform(get("/api/reservation/tasks")
.param("hotel_id", "HOTEL-TEST")
.param("keyword", "mail-v3-frontend-route-blocks-001"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.items[0].task_id").value(taskId))
.andExpect(jsonPath("$.items[0].result_type").value("normal_task"))
.andExpect(jsonPath("$.items[0].ai_task_type").value("New Booking"))
.andExpect(jsonPath("$.items[0].route_code").value("R01_NEW_FIT_RESERVATION_NORMAL"))
.andExpect(jsonPath("$.items[0].system_process_category").value("BUSINESS_TASK"));
mockMvc.perform(get("/api/reservation/orders/{orderId}", orderId)
.param("hotel_id", "HOTEL-TEST")
.param("include_tasks", "true"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.tasks[0].task_id").value(taskId))
.andExpect(jsonPath("$.tasks[0].result_type").value("normal_task"))
.andExpect(jsonPath("$.tasks[0].ai_task_type").value("New Booking"))
.andExpect(jsonPath("$.tasks[0].route_code").value("R01_NEW_FIT_RESERVATION_NORMAL"))
.andExpect(jsonPath("$.tasks[0].system_process_category").value("BUSINESS_TASK"));
mockMvc.perform(get("/api/reservation/tasks/{taskId}", taskId))
.andExpect(status().isOk())
.andExpect(jsonPath("$.result_type").value("normal_task"))
.andExpect(jsonPath("$.ai_task_type").value("New Booking"))
.andExpect(jsonPath("$.task_subtype").value("new_fit_reservation"))
.andExpect(jsonPath("$.route_code").value("R01_NEW_FIT_RESERVATION_NORMAL"))
.andExpect(jsonPath("$.system_process_category").value("BUSINESS_TASK"))
.andExpect(jsonPath("$.adapter_contract_errors.length()").value(1))
.andExpect(jsonPath("$.adapter_contract_errors[0].adapter_error_code")
.value("EVENT_CONTRACT_ERROR"))
.andExpect(jsonPath("$.adapter_contract_errors[0].payload_fragment.contract_errors[0].code")
.value("unsupported_contract"))
.andExpect(jsonPath("$.unhandled_intents.length()").value(1))
.andExpect(jsonPath("$.unhandled_intents[0].route_code")
.value("R42_UNHANDLED_CURRENT_INTENT"))
.andExpect(jsonPath("$.unhandled_intents[0].system_process_category")
.value("UNHANDLED_CURRENT_INTENT"))
.andExpect(jsonPath("$.unhandled_intents[0].payload_fragment.intent_type")
.value("unsupported_active_request"));
}
@Test
void shouldPersistAdapterContractErrorForIncompleteV3ManualReview() throws Exception {
SourceMessageCaptureResult source = captureSourceMessage("mail-v3-manual-review-incomplete-001");