修复M002 V4入站解析边界

This commit is contained in:
andy
2026-07-18 23:05:15 +07:00
parent 6be14f2b40
commit 3ecbf7eea8
11 changed files with 1326 additions and 706 deletions

View File

@@ -1032,6 +1032,56 @@ class SuperAgentTaskResultControllerTest {
assertThat(transitionCount).isEqualTo(2L);
}
@Test
void shouldCreateV4CancelTraceAndRoomingListTasksInEventOrder() throws Exception {
SourceMessageCaptureResult source = captureSourceMessage("mail-v4-cancel-trace-rooming-001");
String body = v4CancelTraceRoomingListBody("mail-v4-cancel-trace-rooming-001");
MvcResult result = mockMvc.perform(signedPost(body, "nonce-v4-cancel-trace-rooming-001"))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.source_message_id").value("mail-v4-cancel-trace-rooming-001"))
.andExpect(jsonPath("$.accepted_count").value(3))
.andExpect(jsonPath("$.items[0].route_code").value("R06_CANCEL_GROUP_BLOCK_NORMAL"))
.andExpect(jsonPath("$.items[0].system_task_type").value("CANCEL_BOOKING"))
.andExpect(jsonPath("$.items[0].task_card_type").value("CANCEL_BOOKING"))
.andExpect(jsonPath("$.items[0].execution_order").value(1))
.andExpect(jsonPath("$.items[1].route_code").value("R14_GENERAL_REQUEST_NORMAL"))
.andExpect(jsonPath("$.items[1].system_task_type").value("UPDATE_BOOKING"))
.andExpect(jsonPath("$.items[1].task_card_type").value("TRACE_RESERVATION_NOTES"))
.andExpect(jsonPath("$.items[1].execution_order").value(2))
.andExpect(jsonPath("$.items[2].route_code").value("R11_ROOMING_LIST_NORMAL"))
.andExpect(jsonPath("$.items[2].system_task_type").value("UPDATE_BOOKING"))
.andExpect(jsonPath("$.items[2].task_card_type").value("ROOMING_LIST"))
.andExpect(jsonPath("$.items[2].execution_order").value(3))
.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");
String thirdOrderId = com.jayway.jsonpath.JsonPath.read(
result.getResponse().getContentAsString(),
"$.items[2].order_id");
assertThat(secondOrderId).isEqualTo(firstOrderId);
assertThat(thirdOrderId).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 (
'R06_CANCEL_GROUP_BLOCK_NORMAL',
'R14_GENERAL_REQUEST_NORMAL',
'R11_ROOMING_LIST_NORMAL'
)
""", Long.class, source.inboxId());
assertThat(transitionCount).isEqualTo(3L);
}
@Test
void shouldCreateReadOnlyTaskForV4S10WithoutLegacyResultType() throws Exception {
SourceMessageCaptureResult source = captureSourceMessage("mail-v4-s10-entry-result-001");
@@ -1058,13 +1108,59 @@ class SuperAgentTaskResultControllerTest {
}
@Test
void shouldRejectMalformedV4PackageWithClearAdapterContractError() throws Exception {
void shouldPersistPackageAdapterContractErrorWhenV4RootIsMalformed() throws Exception {
SourceMessageCaptureResult source = captureSourceMessage("mail-v4-malformed-missing-order-contexts-001");
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 根结构字段不完整。"));
.andExpect(status().isCreated())
.andExpect(jsonPath("$.accepted_count").value(1))
.andExpect(jsonPath("$.items[0].adapter_error_code").value("V4_ROOT_CONTRACT_INVALID"))
.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 catalog_code = 'M002V4'
AND system_process_category = 'ADAPTER_CONTRACT_ERROR'
AND adapter_error_code = 'V4_ROOT_CONTRACT_INVALID'
""", Long.class, source.inboxId());
assertThat(taskCount).isZero();
assertThat(adapterErrorCount).isEqualTo(1L);
}
@Test
void shouldPersistPackageAdapterContractErrorWhenV4S10OmitsEmptyArrays() throws Exception {
SourceMessageCaptureResult source = captureSourceMessage("mail-v4-s10-missing-empty-arrays-001");
String body = v4S10MissingArraysBody("mail-v4-s10-missing-empty-arrays-001");
mockMvc.perform(signedPost(body, "nonce-v4-s10-missing-empty-arrays-001"))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.accepted_count").value(1))
.andExpect(jsonPath("$.items[0].adapter_error_code").value("V4_ROOT_CONTRACT_INVALID"))
.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 catalog_code = 'M002V4'
AND system_process_category = 'ADAPTER_CONTRACT_ERROR'
AND adapter_error_code = 'V4_ROOT_CONTRACT_INVALID'
""", Long.class, source.inboxId());
assertThat(taskCount).isZero();
assertThat(adapterErrorCount).isEqualTo(1L);
}
@Test
@@ -1094,6 +1190,36 @@ class SuperAgentTaskResultControllerTest {
assertThat(adapterErrorCount).isEqualTo(1L);
}
@Test
void shouldNotLetInvalidV4SiblingEventBlockLaterValidEvent() throws Exception {
SourceMessageCaptureResult source = captureSourceMessage("mail-v4-invalid-payment-then-cancel-001");
String body = v4InvalidPaymentThenCancelBody("mail-v4-invalid-payment-then-cancel-001");
mockMvc.perform(signedPost(body, "nonce-v4-invalid-payment-then-cancel-001"))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.accepted_count").value(2))
.andExpect(jsonPath("$.items[0].adapter_error_code").value("PAYMENT_ATTACHMENT_ID_NOT_FOUND"))
.andExpect(jsonPath("$.items[0].task_id").doesNotExist())
.andExpect(jsonPath("$.items[1].route_code").value("R06_CANCEL_GROUP_BLOCK_NORMAL"))
.andExpect(jsonPath("$.items[1].task_id").exists())
.andExpect(jsonPath("$.items[1].execution_order").value(1));
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).isEqualTo(1L);
assertThat(adapterErrorCount).isEqualTo(1L);
}
@Test
void shouldPersistAdapterContractErrorWhenV4UpdateContainsRateCode() throws Exception {
SourceMessageCaptureResult source = captureSourceMessage("mail-v4-update-rate-code-001");
@@ -3383,6 +3509,72 @@ class SuperAgentTaskResultControllerTest {
""".formatted(externalSourceMessageId);
}
private String v4CancelTraceRoomingListBody(String externalSourceMessageId) {
return """
{
"route_code": null,
"source_message": {
"source_message_id": "%s",
"conversation_id": "thread-v4-cancel-trace-rooming-001",
"subject": "Cancel, trace and rooming list",
"sender": "agent@example.test",
"sent_at": "2026-07-18T02:20:00Z",
"body": "Please cancel group, add trace and note rooming list.",
"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": "CANCEL_BOOKING",
"target_order": {
"booking_type": "GROUP",
"locator_type": "GROUP_CODE",
"locator_value": "GRP-V4-CTR-001"
},
"manual_review": null
},
{
"order_ref": "order-1",
"event_type": "TRACE_RESERVATION_NOTES",
"target_order": {
"booking_type": "GROUP",
"locator_type": "GROUP_CODE",
"locator_value": "GRP-V4-CTR-001"
},
"trace_items": [
{
"item_type": "GENERAL",
"text": "Guest requests quiet rooms.",
"department_code": "FO"
}
],
"manual_review": null
},
{
"order_ref": "order-1",
"event_type": "ROOMING_LIST",
"target_order": {
"booking_type": "GROUP",
"locator_type": "GROUP_CODE",
"locator_value": "GRP-V4-CTR-001"
},
"manual_review": null
}
]
}
""".formatted(externalSourceMessageId);
}
private String v4S10Body(String externalSourceMessageId) {
return """
{
@@ -3422,6 +3614,24 @@ class SuperAgentTaskResultControllerTest {
""".formatted(externalSourceMessageId);
}
private String v4S10MissingArraysBody(String externalSourceMessageId) {
return """
{
"route_code": "S10",
"source_message": {
"source_message_id": "%s",
"conversation_id": "thread-v4-s10-missing-empty-arrays-001",
"subject": "V4 S10 without empty arrays",
"sender": "agent@example.test",
"sent_at": "2026-07-18T02:15:00Z",
"body": "S10 package omitted empty arrays.",
"body_content_type": "text/plain",
"attachments": []
}
}
""".formatted(externalSourceMessageId);
}
private String v4PaymentBody(String externalSourceMessageId, String attachmentIdsJson) {
return """
{
@@ -3470,6 +3680,56 @@ class SuperAgentTaskResultControllerTest {
""".formatted(externalSourceMessageId, attachmentIdsJson);
}
private String v4InvalidPaymentThenCancelBody(String externalSourceMessageId) {
return """
{
"route_code": null,
"source_message": {
"source_message_id": "%s",
"conversation_id": "thread-v4-invalid-payment-then-cancel-001",
"subject": "Bad payment followed by cancel",
"sender": "agent@example.test",
"sent_at": "2026-07-18T02:25:00Z",
"body": "Payment reference failed but cancel is valid.",
"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": "PAYMENT",
"target_order": {
"booking_type": "GROUP",
"locator_type": "GROUP_CODE",
"locator_value": "GRP-V4-BAD-PAY-001"
},
"attachment_ids": ["att-missing"],
"manual_review": null
},
{
"order_ref": "order-1",
"event_type": "CANCEL_BOOKING",
"target_order": {
"booking_type": "GROUP",
"locator_type": "GROUP_CODE",
"locator_value": "GRP-V4-CANCEL-AFTER-BAD-001"
},
"manual_review": null
}
]
}
""".formatted(externalSourceMessageId);
}
private String v4UpdateWithRateCodeBody(String externalSourceMessageId) {
return """
{