实现 M002 V4 入站解析基线

This commit is contained in:
andy
2026-07-18 17:32:07 +07:00
parent b949177feb
commit 6be14f2b40
9 changed files with 1445 additions and 31 deletions

View File

@@ -989,6 +989,138 @@ class SuperAgentTaskResultControllerTest {
assertThat(transitionCount).isEqualTo(1L);
}
@Test
void shouldCreateBusinessTasksFromV4OrderContextsAndMessageEventsInOrder() throws Exception {
SourceMessageCaptureResult source = captureSourceMessage("mail-v4-business-root-001");
String body = v4BusinessRootBody("mail-v4-business-root-001");
MvcResult result = mockMvc.perform(signedPost(body, "nonce-v4-business-root-001"))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.source_message_id").value("mail-v4-business-root-001"))
.andExpect(jsonPath("$.accepted_count").value(2))
.andExpect(jsonPath("$.items[0].source_event_index").value(1))
.andExpect(jsonPath("$.items[0].array_index").value(1))
.andExpect(jsonPath("$.items[0].route_code").value("R02_NEW_GROUP_BLOCK_NORMAL"))
.andExpect(jsonPath("$.items[0].system_task_type").value("NEW_BOOKING"))
.andExpect(jsonPath("$.items[0].task_card_type").value("NEW_BOOKING"))
.andExpect(jsonPath("$.items[0].execution_order").value(1))
.andExpect(jsonPath("$.items[1].source_event_index").value(2))
.andExpect(jsonPath("$.items[1].array_index").value(2))
.andExpect(jsonPath("$.items[1].route_code").value("R10_PAYMENT_EVIDENCE_NORMAL"))
.andExpect(jsonPath("$.items[1].system_task_type").value("UPDATE_BOOKING"))
.andExpect(jsonPath("$.items[1].task_card_type").value("PAYMENT_EVIDENCE"))
.andExpect(jsonPath("$.items[1].execution_order").value(2))
.andReturn();
String firstOrderId = com.jayway.jsonpath.JsonPath.read(
result.getResponse().getContentAsString(),
"$.items[0].order_id");
String secondOrderId = com.jayway.jsonpath.JsonPath.read(
result.getResponse().getContentAsString(),
"$.items[1].order_id");
assertThat(secondOrderId).isEqualTo(firstOrderId);
Long transitionCount = jdbcTemplate.queryForObject("""
SELECT COUNT(*)
FROM workflow_reservation_ai_transition
WHERE source_message_id = ?
AND catalog_code = 'M002V4'
AND skill_id = 'booking-desk-event-v4'
AND route_code IN ('R02_NEW_GROUP_BLOCK_NORMAL', 'R10_PAYMENT_EVIDENCE_NORMAL')
AND ai_payload_json LIKE '%"v4_message_event"%'
""", Long.class, source.inboxId());
assertThat(transitionCount).isEqualTo(2L);
}
@Test
void shouldCreateReadOnlyTaskForV4S10WithoutLegacyResultType() throws Exception {
SourceMessageCaptureResult source = captureSourceMessage("mail-v4-s10-entry-result-001");
String body = v4S10Body("mail-v4-s10-entry-result-001");
mockMvc.perform(signedPost(body, "nonce-v4-s10-entry-result-001"))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.source_message_id").value("mail-v4-s10-entry-result-001"))
.andExpect(jsonPath("$.accepted_count").value(1))
.andExpect(jsonPath("$.items[0].route_code").value("S10"))
.andExpect(jsonPath("$.items[0].system_process_category").value("SOURCE_MESSAGE_NOTIFICATION"))
.andExpect(jsonPath("$.items[0].system_task_type").value("SOURCE_MESSAGE_ONLY"))
.andExpect(jsonPath("$.items[0].task_status").value("COMPLETED"));
Long transitionCount = jdbcTemplate.queryForObject("""
SELECT COUNT(*)
FROM workflow_reservation_ai_transition
WHERE source_message_id = ?
AND result_type = 'source_message_review_notification'
AND ai_task_type = 'S10'
AND route_code = 'S10'
""", Long.class, source.inboxId());
assertThat(transitionCount).isEqualTo(1L);
}
@Test
void shouldRejectMalformedV4PackageWithClearAdapterContractError() throws Exception {
String body = v4MalformedMissingOrderContextsBody("mail-v4-malformed-missing-order-contexts-001");
mockMvc.perform(signedPost(body, "nonce-v4-malformed-missing-order-contexts-001"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.error_code").value("ADAPTER_CONTRACT_ERROR"))
.andExpect(jsonPath("$.message").value("V4 根结构字段不完整。"));
}
@Test
void shouldPersistAdapterContractErrorWhenV4PaymentReferencesUnknownAttachment() throws Exception {
SourceMessageCaptureResult source = captureSourceMessage("mail-v4-payment-unknown-attachment-001");
String body = v4PaymentBody("mail-v4-payment-unknown-attachment-001", "[\"att-missing\"]");
mockMvc.perform(signedPost(body, "nonce-v4-payment-unknown-attachment-001"))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.accepted_count").value(1))
.andExpect(jsonPath("$.items[0].adapter_error_code").value("PAYMENT_ATTACHMENT_ID_NOT_FOUND"))
.andExpect(jsonPath("$.items[0].task_id").doesNotExist());
Long taskCount = jdbcTemplate.queryForObject("""
SELECT COUNT(*)
FROM workflow_reservation_task
WHERE source_message_id = ?
""", Long.class, source.inboxId());
Long adapterErrorCount = jdbcTemplate.queryForObject("""
SELECT COUNT(*)
FROM workflow_reservation_ai_transition
WHERE source_message_id = ?
AND system_process_category = 'ADAPTER_CONTRACT_ERROR'
AND adapter_error_code = 'PAYMENT_ATTACHMENT_ID_NOT_FOUND'
""", Long.class, source.inboxId());
assertThat(taskCount).isZero();
assertThat(adapterErrorCount).isEqualTo(1L);
}
@Test
void shouldPersistAdapterContractErrorWhenV4UpdateContainsRateCode() throws Exception {
SourceMessageCaptureResult source = captureSourceMessage("mail-v4-update-rate-code-001");
String body = v4UpdateWithRateCodeBody("mail-v4-update-rate-code-001");
mockMvc.perform(signedPost(body, "nonce-v4-update-rate-code-001"))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.accepted_count").value(1))
.andExpect(jsonPath("$.items[0].adapter_error_code").value("UPDATE_RATE_CODE_NOT_ALLOWED"))
.andExpect(jsonPath("$.items[0].task_id").doesNotExist());
Long taskCount = jdbcTemplate.queryForObject("""
SELECT COUNT(*)
FROM workflow_reservation_task
WHERE source_message_id = ?
""", Long.class, source.inboxId());
Long adapterErrorCount = jdbcTemplate.queryForObject("""
SELECT COUNT(*)
FROM workflow_reservation_ai_transition
WHERE source_message_id = ?
AND system_process_category = 'ADAPTER_CONTRACT_ERROR'
AND adapter_error_code = 'UPDATE_RATE_CODE_NOT_ALLOWED'
""", Long.class, source.inboxId());
assertThat(taskCount).isZero();
assertThat(adapterErrorCount).isEqualTo(1L);
}
@Test
void shouldAcceptV2TaskResultWhenSourceMessageStoredAsOutlookChannelWithoutSourceChannel() throws Exception {
String externalMessageId = "mail-v2-task-result-outlook-channel-001";
@@ -3183,6 +3315,204 @@ class SuperAgentTaskResultControllerTest {
""".formatted(externalSourceMessageId);
}
private String v4BusinessRootBody(String externalSourceMessageId) {
return """
{
"route_code": null,
"source_message": {
"source_message_id": "%s",
"conversation_id": "thread-v4-001",
"subject": "Group booking and payment",
"sender": "agent@example.test",
"sent_at": "2026-07-18T02:10:00Z",
"body": "Please create group GRP-V4-001 and note payment attached.",
"body_content_type": "text/plain",
"attachments": [
{
"id": "att-pay-1",
"name": "payment-slip.jpg",
"content_type": "image/jpeg",
"url": "https://oss.example.test/payment-slip.jpg",
"size": 251524
}
]
},
"order_contexts": [
{
"order_ref": "order-1",
"basic_information": {
"account_code": "QBD_TRAVEL",
"manual_review": null
}
}
],
"message_events": [
{
"order_ref": "order-1",
"event_type": "NEW_BOOKING",
"target_order": {
"booking_type": "GROUP",
"locator_type": "GROUP_CODE",
"locator_value": "GRP-V4-001"
},
"arrival_date": "2026-07-26",
"departure_date": "2026-07-29",
"rate_code": "BAR",
"booking_scenario": "STANDARD",
"room_items": [
{
"room_type_code": "TWN",
"room_count": 2
}
],
"manual_review": null
},
{
"order_ref": "order-1",
"event_type": "PAYMENT",
"target_order": {
"booking_type": "GROUP",
"locator_type": "GROUP_CODE",
"locator_value": "GRP-V4-001"
},
"attachment_ids": ["att-pay-1"],
"manual_review": null
}
]
}
""".formatted(externalSourceMessageId);
}
private String v4S10Body(String externalSourceMessageId) {
return """
{
"route_code": "S10",
"source_message": {
"source_message_id": "%s",
"conversation_id": "thread-v4-s10-001",
"subject": "Thanks",
"sender": "guest@example.test",
"sent_at": "2026-07-18T02:10:00Z",
"body": "Noted with thanks.",
"body_content_type": "text/plain",
"attachments": []
},
"order_contexts": [],
"message_events": []
}
""".formatted(externalSourceMessageId);
}
private String v4MalformedMissingOrderContextsBody(String externalSourceMessageId) {
return """
{
"route_code": null,
"source_message": {
"source_message_id": "%s",
"conversation_id": "thread-v4-malformed-001",
"subject": "Malformed V4 package",
"sender": "agent@example.test",
"sent_at": "2026-07-18T02:10:00Z",
"body": "Missing order contexts.",
"body_content_type": "text/plain",
"attachments": []
},
"message_events": []
}
""".formatted(externalSourceMessageId);
}
private String v4PaymentBody(String externalSourceMessageId, String attachmentIdsJson) {
return """
{
"route_code": null,
"source_message": {
"source_message_id": "%s",
"conversation_id": "thread-v4-payment-001",
"subject": "Payment evidence",
"sender": "agent@example.test",
"sent_at": "2026-07-18T02:10:00Z",
"body": "Payment attached.",
"body_content_type": "text/plain",
"attachments": [
{
"id": "att-pay-1",
"name": "payment-slip.jpg",
"content_type": "image/jpeg",
"url": "https://oss.example.test/payment-slip.jpg",
"size": 251524
}
]
},
"order_contexts": [
{
"order_ref": "order-1",
"basic_information": {
"account_code": "QBD_TRAVEL",
"manual_review": null
}
}
],
"message_events": [
{
"order_ref": "order-1",
"event_type": "PAYMENT",
"target_order": {
"booking_type": "GROUP",
"locator_type": "GROUP_CODE",
"locator_value": "GRP-V4-PAY-001"
},
"attachment_ids": %s,
"manual_review": null
}
]
}
""".formatted(externalSourceMessageId, attachmentIdsJson);
}
private String v4UpdateWithRateCodeBody(String externalSourceMessageId) {
return """
{
"route_code": null,
"source_message": {
"source_message_id": "%s",
"conversation_id": "thread-v4-update-rate-001",
"subject": "Update booking",
"sender": "agent@example.test",
"sent_at": "2026-07-18T02:10:00Z",
"body": "Please update stay and rate code.",
"body_content_type": "text/plain",
"attachments": []
},
"order_contexts": [
{
"order_ref": "order-1",
"basic_information": {
"account_code": "QBD_TRAVEL",
"manual_review": null
}
}
],
"message_events": [
{
"order_ref": "order-1",
"event_type": "UPDATE_BOOKING",
"target_order": {
"booking_type": "GROUP",
"locator_type": "GROUP_CODE",
"locator_value": "GRP-V4-UPD-001"
},
"after": {
"arrival_date": "2026-08-01",
"rate_code": "BAR"
},
"manual_review": null
}
]
}
""".formatted(externalSourceMessageId);
}
private String[] createReadyTaskWithTwoOperaOperations(
String externalMessageId,
String nonce,