实现M002 V3 P0.1 Parent Group路由修订
This commit is contained in:
@@ -106,22 +106,6 @@ public enum ReservationAiRouteDefinition {
|
||||
ReservationAiSystemProcessCategory.BUSINESS_TASK,
|
||||
ReservationSystemTaskType.CANCEL_BOOKING,
|
||||
ReservationTaskCardType.CANCEL_BOOKING),
|
||||
LINKED_PARENT_RELEASE_AFTER_CHILD_SPLIT_NORMAL(
|
||||
"R07_LINKED_PARENT_RELEASE_AFTER_CHILD_SPLIT_NORMAL",
|
||||
"normal_task",
|
||||
"Cancel Booking",
|
||||
"linked_parent_release_after_child_split",
|
||||
ReservationAiSystemProcessCategory.BUSINESS_TASK,
|
||||
ReservationSystemTaskType.CANCEL_BOOKING,
|
||||
ReservationTaskCardType.CANCEL_BOOKING),
|
||||
LINKED_PARENT_RELEASE_AFTER_CHILD_SPLIT_REVIEW(
|
||||
"R07_LINKED_PARENT_RELEASE_AFTER_CHILD_SPLIT_REVIEW",
|
||||
"manual_review",
|
||||
"Cancel Booking",
|
||||
"linked_parent_release_after_child_split",
|
||||
ReservationAiSystemProcessCategory.BUSINESS_TASK,
|
||||
ReservationSystemTaskType.CANCEL_BOOKING,
|
||||
ReservationTaskCardType.CANCEL_BOOKING),
|
||||
CANCEL_ALLOTMENT_CONTROL_BLOCK_NORMAL(
|
||||
"R08_CANCEL_ALLOTMENT_CONTROL_BLOCK_NORMAL",
|
||||
"normal_task",
|
||||
|
||||
@@ -213,6 +213,10 @@ public class ReservationAiTaskIntakeServiceImpl implements ReservationAiTaskInta
|
||||
if (!taskResults.isArray() || taskResults.isEmpty()) {
|
||||
throw error(HttpStatus.BAD_REQUEST, "TASK_RESULTS_EMPTY", "ai_task_results 不能为空。");
|
||||
}
|
||||
V3EventContractIssue legacyIssue = inspectV2TaskResultsContractIssue(taskResults);
|
||||
if (legacyIssue != null) {
|
||||
throw error(HttpStatus.BAD_REQUEST, "ADAPTER_CONTRACT_ERROR", legacyIssue.message());
|
||||
}
|
||||
|
||||
String hotelId = sourceMessage.hotelId();
|
||||
String requestPayloadSha256 = sha256(rawBody == null ? "" : rawBody);
|
||||
@@ -364,10 +368,14 @@ public class ReservationAiTaskIntakeServiceImpl implements ReservationAiTaskInta
|
||||
}
|
||||
|
||||
List<SuperAgentTaskResultItemResponse> responseItems = new ArrayList<>(itemCount);
|
||||
Set<String> acceptedParentSplitCodes = new LinkedHashSet<>();
|
||||
for (int index = 0; index < messageEvents.size(); index++) {
|
||||
JsonNode event = messageEvents.get(index);
|
||||
int arrayIndex = index + 1;
|
||||
V3EventContractIssue contractIssue = inspectV3EventContractIssue(event, messageEvents);
|
||||
if (contractIssue == null) {
|
||||
contractIssue = inspectV3ParentSplitDuplicateIssue(event, acceptedParentSplitCodes);
|
||||
}
|
||||
if (contractIssue != null) {
|
||||
responseItems.add(createAdapterContractErrorTransition(
|
||||
hotelId,
|
||||
@@ -426,6 +434,32 @@ public class ReservationAiTaskIntakeServiceImpl implements ReservationAiTaskInta
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 V2 兼容 ai_task_results[] 中不再允许当前 producer 继续提交的旧 Parent split 三元组。
|
||||
*/
|
||||
private V3EventContractIssue inspectV2TaskResultsContractIssue(JsonNode taskResults) {
|
||||
for (JsonNode item : taskResults) {
|
||||
if (isLegacyParentSplitTriplet(
|
||||
textAt(item, "result_type"),
|
||||
textAt(item, "task_type"),
|
||||
textAt(item, "task_subtype"))) {
|
||||
return new V3EventContractIssue(
|
||||
"LINKED_PARENT_RELEASE_LEGACY_CANCEL_BOOKING_UNSUPPORTED",
|
||||
"当前 producer 不再支持 Cancel Booking + linked_parent_release_after_child_split,Parent split 父事件必须使用 Cancel Allotment。");
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 识别 P0.1 已删除的旧 Parent split 三元组。
|
||||
*/
|
||||
private boolean isLegacyParentSplitTriplet(String resultType, String taskType, String taskSubtype) {
|
||||
return ("normal_task".equals(resultType) || "manual_review".equals(resultType))
|
||||
&& "Cancel Booking".equals(taskType)
|
||||
&& "linked_parent_release_after_child_split".equals(taskSubtype);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 V3 业务根的批次级 P0 结构;单个事件内容错误由 event 级 transition 承接。
|
||||
*/
|
||||
@@ -705,13 +739,9 @@ public class ReservationAiTaskIntakeServiceImpl implements ReservationAiTaskInta
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel Booking 需要先识别 parent split 关系,再识别普通取消对象。
|
||||
* Cancel Booking 只识别普通取消对象;Parent split 在 P0.1 后统一走 Cancel Allotment。
|
||||
*/
|
||||
private String cancelBookingSubtype(JsonNode event, JsonNode extractedFields) {
|
||||
String relationshipType = firstText(extractedFields, event, "relationship_type");
|
||||
if ("linked_parent_release_after_child_split".equals(relationshipType)) {
|
||||
return "linked_parent_release_after_child_split";
|
||||
}
|
||||
return switch (nullToEmpty(firstText(extractedFields, event, "cancel_object_type"))) {
|
||||
case "fit_reservation" -> "cancel_fit_reservation";
|
||||
case "group_block" -> "cancel_group_block";
|
||||
@@ -740,6 +770,11 @@ public class ReservationAiTaskIntakeServiceImpl implements ReservationAiTaskInta
|
||||
return manualReviewIssue;
|
||||
}
|
||||
}
|
||||
if (isLegacyLinkedParentCancelBooking(event)) {
|
||||
return new V3EventContractIssue(
|
||||
"LINKED_PARENT_RELEASE_LEGACY_CANCEL_BOOKING_UNSUPPORTED",
|
||||
"当前 producer 不再支持 Cancel Booking + linked_parent_release_after_child_split,Parent split 父事件必须使用 Cancel Allotment。");
|
||||
}
|
||||
if (isLinkedParentReleaseCandidate(event) && !validLinkedParentReleaseCandidate(event, messageEvents)) {
|
||||
return new V3EventContractIssue(
|
||||
"LINKED_PARENT_RELEASE_CONTRACT_INCOMPLETE",
|
||||
@@ -770,14 +805,54 @@ public class ReservationAiTaskIntakeServiceImpl implements ReservationAiTaskInta
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 parent split 候选的最小可追溯关系字段,避免只凭 relationship_type 建业务任务。
|
||||
* 判断当前 producer 是否仍在输出旧 Parent Cancel Booking 组合。
|
||||
*/
|
||||
private boolean isLegacyLinkedParentCancelBooking(JsonNode event) {
|
||||
JsonNode extractedFields = event == null ? null : event.path("extracted_fields");
|
||||
return isLegacyParentSplitTriplet(
|
||||
event == null || isNullOrMissing(event.get("manual_review"))
|
||||
? AiResultType.NORMAL_TASK.code()
|
||||
: AiResultType.MANUAL_REVIEW.code(),
|
||||
trimToNull(textAt(event, "event_type")),
|
||||
firstText(extractedFields, event, "relationship_type"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验同一 V3 业务根中同一个 Parent split cluster 只能有一个 Parent 候选。
|
||||
*/
|
||||
private V3EventContractIssue inspectV3ParentSplitDuplicateIssue(JsonNode event, Set<String> acceptedParentSplitCodes) {
|
||||
if (!isLinkedParentReleaseCandidate(event)) {
|
||||
return null;
|
||||
}
|
||||
String parentGroupCode = trimToNull(textAt(event.path("case_keys"), "group_code"));
|
||||
if (parentGroupCode != null && !acceptedParentSplitCodes.add(parentGroupCode)) {
|
||||
return new V3EventContractIssue(
|
||||
"LINKED_PARENT_RELEASE_DUPLICATE_PARENT",
|
||||
"同一个 Parent split cluster 只能有一个 Parent Cancel Allotment 候选。");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 P0.1 parent split 候选的最小可追溯关系字段,避免只凭 relationship_type 建业务任务。
|
||||
*/
|
||||
private boolean validLinkedParentReleaseCandidate(JsonNode event, JsonNode messageEvents) {
|
||||
JsonNode extractedFields = event == null ? null : event.path("extracted_fields");
|
||||
JsonNode caseKeys = event == null ? null : event.path("case_keys");
|
||||
if (!("Cancel Booking".equals(trimToNull(textAt(event, "event_type")))
|
||||
&& "group_block".equals(firstText(extractedFields, event, "cancel_object_type"))
|
||||
&& trimToNull(textAt(caseKeys, "group_code")) != null
|
||||
String parentGroupCode = trimToNull(textAt(caseKeys, "group_code"));
|
||||
String parentBlockCode = trimToNull(textAt(caseKeys, "block_code"));
|
||||
if (!("Cancel Allotment".equals(trimToNull(textAt(event, "event_type")))
|
||||
&& "entire_allotment_control_block".equals(firstText(extractedFields, event, "cancel_scope"))
|
||||
&& isBooleanTrue(extractedFields.path("parent_release_or_cancel_candidate"))
|
||||
&& "parent_to_child_allocation_split".equals(firstText(extractedFields, event, "release_reason"))
|
||||
&& parentGroupCode != null
|
||||
&& parentGroupCode.equals(firstText(extractedFields, event, "parent_group_code"))
|
||||
&& isBooleanTrue(extractedFields.path("allocation_split_from_parent"))
|
||||
&& "New Booking".equals(trimToNull(textAt(event, "related_event_type")))
|
||||
&& event != null
|
||||
&& isBooleanTrue(event.path("requires_downstream_hard_validation"))
|
||||
&& parentGroupCode != null
|
||||
&& parentGroupCode.equals(parentBlockCode)
|
||||
&& nonEmptyArray(extractedFields, "child_group_codes")
|
||||
&& nonEmptyArray(event, "related_source_event_indices"))) {
|
||||
return false;
|
||||
@@ -789,25 +864,28 @@ public class ReservationAiTaskIntakeServiceImpl implements ReservationAiTaskInta
|
||||
|| childGroupCodes.size() != relatedSourceEventIndices.size()) {
|
||||
return false;
|
||||
}
|
||||
Set<String> expectedChildGroups = new LinkedHashSet<>(childGroupCodes);
|
||||
Set<String> childGroupCodeSet = new LinkedHashSet<>(childGroupCodes);
|
||||
Set<String> relatedEventIds = new LinkedHashSet<>(relatedSourceEventIndices);
|
||||
if (expectedChildGroups.size() != childGroupCodes.size()
|
||||
if (childGroupCodeSet.size() != childGroupCodes.size()
|
||||
|| relatedEventIds.size() != relatedSourceEventIndices.size()) {
|
||||
return false;
|
||||
}
|
||||
Set<String> actualChildGroups = new LinkedHashSet<>();
|
||||
for (String sourceEventIndex : relatedSourceEventIndices) {
|
||||
for (int index = 0; index < relatedSourceEventIndices.size(); index++) {
|
||||
String sourceEventIndex = relatedSourceEventIndices.get(index);
|
||||
JsonNode childEvent = findV3MessageEventBySourceEventIndex(messageEvents, sourceEventIndex);
|
||||
if (childEvent == null || !"New Booking".equals(trimToNull(textAt(childEvent, "event_type")))) {
|
||||
return false;
|
||||
}
|
||||
String childGroupCode = trimToNull(textAt(childEvent.path("case_keys"), "group_code"));
|
||||
if (childGroupCode == null || !expectedChildGroups.contains(childGroupCode)) {
|
||||
JsonNode childExtractedFields = childEvent.path("extracted_fields");
|
||||
if (!"Group Block".equals(firstText(childExtractedFields, childEvent, "booking_object_type"))) {
|
||||
return false;
|
||||
}
|
||||
String childGroupCode = trimToNull(textAt(childEvent.path("case_keys"), "group_code"));
|
||||
if (!childGroupCodes.get(index).equals(childGroupCode)) {
|
||||
return false;
|
||||
}
|
||||
actualChildGroups.add(childGroupCode);
|
||||
}
|
||||
return actualChildGroups.equals(expectedChildGroups);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user