实现M002 V3 P0.1 Parent Group路由修订

This commit is contained in:
andy
2026-07-12 11:42:01 +08:00
parent 0358b34159
commit 92489af18e
42 changed files with 3971 additions and 79 deletions

View File

@@ -7,8 +7,8 @@ import org.junit.jupiter.api.Test;
class ReservationAiRouteDefinitionTest {
@Test
void shouldKeepFortyTwoM002V3P0RouteDefinitions() {
assertThat(ReservationAiRouteDefinition.values()).hasSize(42);
void shouldKeepFortyM002V3P01RouteDefinitions() {
assertThat(ReservationAiRouteDefinition.values()).hasSize(40);
}
@Test
@@ -25,5 +25,15 @@ class ReservationAiRouteDefinitionTest {
"Payment Evidence",
"payment_evidence"))
.contains(ReservationAiRouteDefinition.PAYMENT_EVIDENCE_REVIEW);
assertThat(ReservationAiRouteDefinition.findByTriplet(
"normal_task",
"Cancel Allotment",
"cancel_allotment_control_block"))
.contains(ReservationAiRouteDefinition.CANCEL_ALLOTMENT_CONTROL_BLOCK_NORMAL);
assertThat(ReservationAiRouteDefinition.findByTriplet(
"normal_task",
"Cancel Booking",
"linked_parent_release_after_child_split"))
.isEmpty();
}
}

View File

@@ -149,6 +149,35 @@ class SuperAgentTaskResultControllerTest {
org.assertj.core.api.Assertions.assertThat(taskCardCount).isGreaterThanOrEqualTo(1L);
}
@Test
void shouldRejectLegacyAiTaskResultsParentSplitCancelBooking() throws Exception {
SourceMessageCaptureResult source = captureSourceMessage("mail-v2-parent-split-legacy-001");
String body = minimalBody(source.inboxId().toString(), "Cancel Booking", "normal_task",
"linked_parent_release_after_child_split", """
"case_keys": {"group_code": "PARENT-V2-001"},
"extracted_fields": {
"cancel_object_type": "group_block",
"parent_release_or_cancel_candidate": true,
"release_reason": "parent_to_child_allocation_split",
"parent_group_code": "PARENT-V2-001",
"child_group_codes": ["CHILD-V2-001"],
"allocation_split_from_parent": true
}
""");
mockMvc.perform(signedPost(body, "nonce-v2-parent-split-legacy-001"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.error_code").value("ADAPTER_CONTRACT_ERROR"))
.andExpect(content().string(containsString("Cancel Booking + linked_parent_release_after_child_split")));
Long taskCount = jdbcTemplate.queryForObject("""
SELECT COUNT(*)
FROM workflow_reservation_task
WHERE source_message_id = ?
""", Long.class, source.inboxId());
assertThat(taskCount).isZero();
}
@Test
void shouldResolveTaskResultSourceMessageByExternalMessageId() throws Exception {
String externalMessageId = "mail-external-source-id-001";

View File

@@ -269,6 +269,7 @@ class SuperAgentTaskResultP0FixtureRegressionTest {
void shouldCreateParentSplitTasksFromP0FixtureInEventOrder() throws Exception {
ObjectNode root = fixture("parent_split_two_children.json").deepCopy();
useParentSplitBusinessKeys(root, "PARENT-2608-OK", "CHILD-2608-OK-A", "CHILD-2608-OK-B");
convertParentSplitRootToP01(root);
String externalId = "p0-parent-split-001";
SourceMessageCaptureResult source = captureSourceMessage(externalId);
@@ -279,7 +280,7 @@ class SuperAgentTaskResultP0FixtureRegressionTest {
.andExpect(jsonPath("$.items[0].execution_order").value(1))
.andExpect(jsonPath("$.items[1].route_code").value("R02_NEW_GROUP_BLOCK_NORMAL"))
.andExpect(jsonPath("$.items[1].execution_order").value(1))
.andExpect(jsonPath("$.items[2].route_code").value("R07_LINKED_PARENT_RELEASE_AFTER_CHILD_SPLIT_NORMAL"))
.andExpect(jsonPath("$.items[2].route_code").value("R08_CANCEL_ALLOTMENT_CONTROL_BLOCK_NORMAL"))
.andReturn();
String parentOrderId = com.jayway.jsonpath.JsonPath.read(
@@ -297,16 +298,41 @@ class SuperAgentTaskResultP0FixtureRegressionTest {
WHERE source_message_id = ?
AND route_code IN (
'R02_NEW_GROUP_BLOCK_NORMAL',
'R07_LINKED_PARENT_RELEASE_AFTER_CHILD_SPLIT_NORMAL'
'R08_CANCEL_ALLOTMENT_CONTROL_BLOCK_NORMAL'
)
""", Long.class, source.inboxId());
assertThat(transitionCount).isEqualTo(3L);
}
@Test
void shouldFailClosedWhenCurrentProducerUsesLegacyParentCancelBooking() throws Exception {
ObjectNode root = fixture("parent_split_two_children.json").deepCopy();
useParentSplitBusinessKeys(root, "PARENT-2608-LEG", "CHILD-2608-LEG-A", "CHILD-2608-LEG-B");
String externalId = "p0-parent-split-legacy-001";
SourceMessageCaptureResult source = captureSourceMessage(externalId);
mockMvc.perform(signedPost(withSourceMessageId(root, externalId), "nonce-p0-parent-split-legacy-001"))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.accepted_count").value(3))
.andExpect(jsonPath("$.items[0].route_code").value("R02_NEW_GROUP_BLOCK_NORMAL"))
.andExpect(jsonPath("$.items[1].route_code").value("R02_NEW_GROUP_BLOCK_NORMAL"))
.andExpect(jsonPath("$.items[2].adapter_error_code")
.value("LINKED_PARENT_RELEASE_LEGACY_CANCEL_BOOKING_UNSUPPORTED"))
.andExpect(jsonPath("$.items[2].task_id").doesNotExist());
Long taskCount = jdbcTemplate.queryForObject("""
SELECT COUNT(*)
FROM workflow_reservation_task
WHERE source_message_id = ?
""", Long.class, source.inboxId());
assertThat(taskCount).isEqualTo(2L);
}
@Test
void shouldFailClosedWhenParentSplitReferencesDoNotMatchChildEvents() throws Exception {
ObjectNode root = fixture("parent_split_two_children.json").deepCopy();
useParentSplitBusinessKeys(root, "PARENT-2608-MIS", "CHILD-2608-MIS-A", "CHILD-2608-MIS-B");
convertParentSplitRootToP01(root);
ObjectNode parentEvent = (ObjectNode) root.path("message_events").get(2);
((ArrayNode) parentEvent.path("related_source_event_indices"))
.set(1, objectMapper.getNodeFactory().textNode("E_UNKNOWN_CHILD"));
@@ -330,9 +356,97 @@ class SuperAgentTaskResultP0FixtureRegressionTest {
assertThat(taskCount).isEqualTo(2L);
}
@Test
void shouldFailClosedWhenParentSplitReferenceOrderDoesNotMatchChildCodes() throws Exception {
ObjectNode root = fixture("parent_split_two_children.json").deepCopy();
useParentSplitBusinessKeys(root, "PARENT-2608-ORD", "CHILD-2608-ORD-A", "CHILD-2608-ORD-B");
convertParentSplitRootToP01(root);
ObjectNode parentEvent = (ObjectNode) root.path("message_events").get(2);
ArrayNode relatedIndices = (ArrayNode) parentEvent.path("related_source_event_indices");
relatedIndices.removeAll();
relatedIndices.add("E_CHILD_2");
relatedIndices.add("E_CHILD_1");
String externalId = "p0-parent-split-order-mismatch-001";
SourceMessageCaptureResult source = captureSourceMessage(externalId);
mockMvc.perform(signedPost(withSourceMessageId(root, externalId), "nonce-p0-parent-split-order-mismatch-001"))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.accepted_count").value(3))
.andExpect(jsonPath("$.items[0].route_code").value("R02_NEW_GROUP_BLOCK_NORMAL"))
.andExpect(jsonPath("$.items[1].route_code").value("R02_NEW_GROUP_BLOCK_NORMAL"))
.andExpect(jsonPath("$.items[2].adapter_error_code")
.value("LINKED_PARENT_RELEASE_CONTRACT_INCOMPLETE"))
.andExpect(jsonPath("$.items[2].task_id").doesNotExist());
Long taskCount = jdbcTemplate.queryForObject("""
SELECT COUNT(*)
FROM workflow_reservation_task
WHERE source_message_id = ?
""", Long.class, source.inboxId());
assertThat(taskCount).isEqualTo(2L);
}
@Test
void shouldFailClosedWhenParentSplitRequiredP01FieldsAreMissing() throws Exception {
ObjectNode root = fixture("parent_split_two_children.json").deepCopy();
useParentSplitBusinessKeys(root, "PARENT-2608-FIX", "CHILD-2608-FIX-A", "CHILD-2608-FIX-B");
convertParentSplitRootToP01(root);
ObjectNode parentExtractedFields = (ObjectNode) root.path("message_events").get(2).path("extracted_fields");
parentExtractedFields.remove("parent_release_or_cancel_candidate");
String externalId = "p0-parent-split-fixed-field-missing-001";
SourceMessageCaptureResult source = captureSourceMessage(externalId);
mockMvc.perform(signedPost(withSourceMessageId(root, externalId), "nonce-p0-parent-split-fixed-field-missing-001"))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.accepted_count").value(3))
.andExpect(jsonPath("$.items[0].route_code").value("R02_NEW_GROUP_BLOCK_NORMAL"))
.andExpect(jsonPath("$.items[1].route_code").value("R02_NEW_GROUP_BLOCK_NORMAL"))
.andExpect(jsonPath("$.items[2].adapter_error_code")
.value("LINKED_PARENT_RELEASE_CONTRACT_INCOMPLETE"))
.andExpect(jsonPath("$.items[2].task_id").doesNotExist());
Long taskCount = jdbcTemplate.queryForObject("""
SELECT COUNT(*)
FROM workflow_reservation_task
WHERE source_message_id = ?
""", Long.class, source.inboxId());
assertThat(taskCount).isEqualTo(2L);
}
@Test
void shouldFailClosedWhenParentSplitHasDuplicateParentCandidate() throws Exception {
ObjectNode root = fixture("parent_split_two_children.json").deepCopy();
useParentSplitBusinessKeys(root, "PARENT-2608-DUP", "CHILD-2608-DUP-A", "CHILD-2608-DUP-B");
convertParentSplitRootToP01(root);
ArrayNode events = (ArrayNode) root.path("message_events");
ObjectNode duplicateParent = events.get(2).deepCopy();
duplicateParent.put("source_event_index", "E_PARENT_CANCEL_DUP");
events.add(duplicateParent);
String externalId = "p0-parent-split-duplicate-parent-001";
SourceMessageCaptureResult source = captureSourceMessage(externalId);
mockMvc.perform(signedPost(withSourceMessageId(root, externalId), "nonce-p0-parent-split-duplicate-parent-001"))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.accepted_count").value(4))
.andExpect(jsonPath("$.items[0].route_code").value("R02_NEW_GROUP_BLOCK_NORMAL"))
.andExpect(jsonPath("$.items[1].route_code").value("R02_NEW_GROUP_BLOCK_NORMAL"))
.andExpect(jsonPath("$.items[2].route_code").value("R08_CANCEL_ALLOTMENT_CONTROL_BLOCK_NORMAL"))
.andExpect(jsonPath("$.items[3].adapter_error_code")
.value("LINKED_PARENT_RELEASE_DUPLICATE_PARENT"))
.andExpect(jsonPath("$.items[3].task_id").doesNotExist());
Long taskCount = jdbcTemplate.queryForObject("""
SELECT COUNT(*)
FROM workflow_reservation_task
WHERE source_message_id = ?
""", Long.class, source.inboxId());
assertThat(taskCount).isEqualTo(3L);
}
@Test
void shouldCreateRowDerivedMainParentAndTraceTasksFromP0Fixture() throws Exception {
ObjectNode root = fixture("row_multiple_derived.json").deepCopy();
convertParentSplitRootToP01(root);
String externalId = "p0-row-derived-001";
SourceMessageCaptureResult source = captureSourceMessage(externalId);
@@ -340,7 +454,7 @@ class SuperAgentTaskResultP0FixtureRegressionTest {
.andExpect(status().isCreated())
.andExpect(jsonPath("$.accepted_count").value(3))
.andExpect(jsonPath("$.items[0].route_code").value("R02_NEW_GROUP_BLOCK_NORMAL"))
.andExpect(jsonPath("$.items[1].route_code").value("R07_LINKED_PARENT_RELEASE_AFTER_CHILD_SPLIT_NORMAL"))
.andExpect(jsonPath("$.items[1].route_code").value("R08_CANCEL_ALLOTMENT_CONTROL_BLOCK_NORMAL"))
.andExpect(jsonPath("$.items[2].route_code").value("R13_EXTRA_BED_NORMAL"));
Long taskCount = jdbcTemplate.queryForObject("""
@@ -675,6 +789,27 @@ class SuperAgentTaskResultP0FixtureRegressionTest {
childGroupCodes.add(childGroupCodeTwo);
}
private void convertParentSplitRootToP01(ObjectNode root) {
ArrayNode events = (ArrayNode) root.path("message_events");
for (JsonNode item : events) {
ObjectNode event = (ObjectNode) item;
if (!"linked_parent_release_after_child_split".equals(event.path("relationship_type").asText(null))) {
continue;
}
ObjectNode caseKeys = (ObjectNode) event.path("case_keys");
String parentGroupCode = caseKeys.path("group_code").asText(null);
event.put("event_type", "Cancel Allotment");
caseKeys.put("block_code", parentGroupCode);
ObjectNode extractedFields = (ObjectNode) event.path("extracted_fields");
extractedFields.remove("cancel_object_type");
extractedFields.put("cancel_scope", "entire_allotment_control_block");
extractedFields.put("parent_release_or_cancel_candidate", true);
extractedFields.put("release_reason", "parent_to_child_allocation_split");
extractedFields.put("allocation_split_from_parent", true);
}
}
private void useManualReviewGroupCode(ObjectNode event, String groupCode) {
useEventGroupCode(event, groupCode);
((ObjectNode) event.path("manual_review").path("known_fields")).put("group_code", groupCode);