修复M002 V4入站解析边界
This commit is contained in:
@@ -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 """
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user