实现 M002 CP5 任务确认与 OPERA 模拟骨架
This commit is contained in:
@@ -6,6 +6,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
@@ -494,6 +495,368 @@ class SuperAgentTaskResultControllerTest {
|
||||
.value(org.hamcrest.Matchers.contains("CNF-MATRIX-001")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSaveDraftPayloadForEditableTaskWithoutChangingAiSnapshot() throws Exception {
|
||||
SourceMessageCaptureResult source = captureSourceMessage("mail-cp5-draft-save-001");
|
||||
String body = completeNewBookingBody(source.inboxId().toString(), "CNF-CP5-DRAFT-001");
|
||||
MvcResult createResult = mockMvc.perform(signedPost(body, "nonce-cp5-draft-save-001"))
|
||||
.andExpect(status().isCreated())
|
||||
.andReturn();
|
||||
String taskId = com.jayway.jsonpath.JsonPath.read(
|
||||
createResult.getResponse().getContentAsString(),
|
||||
"$.items[0].task_id"
|
||||
);
|
||||
|
||||
mockMvc.perform(put("/api/reservation/tasks/{taskId}/draft", taskId)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"field_values": {
|
||||
"extracted_fields.room_quantity": 3,
|
||||
"extracted_fields.pms_room_type_code": "RM3"
|
||||
}
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.task_id").value(taskId))
|
||||
.andExpect(jsonPath("$.task_status").value("PENDING_CONFIRM"))
|
||||
.andExpect(jsonPath("$.draft_payload.field_values['extracted_fields.room_quantity']").value(3))
|
||||
.andExpect(jsonPath("$.draft_payload.field_values['extracted_fields.pms_room_type_code']").value("RM3"));
|
||||
|
||||
String draftPayloadJson = jdbcTemplate.queryForObject("""
|
||||
SELECT draft_payload_json
|
||||
FROM workflow_reservation_task_card
|
||||
WHERE task_id = ?
|
||||
""", String.class, Long.valueOf(taskId));
|
||||
String aiPayloadJson = jdbcTemplate.queryForObject("""
|
||||
SELECT ai_payload_json
|
||||
FROM workflow_reservation_task_card
|
||||
WHERE task_id = ?
|
||||
""", String.class, Long.valueOf(taskId));
|
||||
assertThat(draftPayloadJson).contains("\"extracted_fields.room_quantity\":3");
|
||||
assertThat(aiPayloadJson).contains("\"room_quantity\":2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldConfirmTaskWithMergedPayloadAndMoveToReady() throws Exception {
|
||||
SourceMessageCaptureResult source = captureSourceMessage("mail-cp5-confirm-ready-001");
|
||||
String body = completeNewBookingBody(source.inboxId().toString(), null);
|
||||
MvcResult createResult = mockMvc.perform(signedPost(body, "nonce-cp5-confirm-ready-001"))
|
||||
.andExpect(status().isCreated())
|
||||
.andReturn();
|
||||
String taskId = com.jayway.jsonpath.JsonPath.read(
|
||||
createResult.getResponse().getContentAsString(),
|
||||
"$.items[0].task_id"
|
||||
);
|
||||
|
||||
mockMvc.perform(post("/api/reservation/tasks/{taskId}/confirm", taskId)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"field_values": {
|
||||
"extracted_fields.room_quantity": 4,
|
||||
"extracted_fields.pms_room_type_code": "RM4"
|
||||
}
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.task_id").value(taskId))
|
||||
.andExpect(jsonPath("$.task_status").value("READY"))
|
||||
.andExpect(jsonPath("$.confirmed_payload.field_values['extracted_fields.room_quantity']").value(4))
|
||||
.andExpect(jsonPath("$.confirmed_payload.field_values['extracted_fields.pms_room_type_code']").value("RM4"))
|
||||
.andExpect(jsonPath("$.confirmed_payload.field_values['extracted_fields.arrival_date']").value("2026-08-01"))
|
||||
.andExpect(jsonPath("$.opera_operations.length()").value(2))
|
||||
.andExpect(jsonPath("$.opera_operations[0].operation_sequence").value(1))
|
||||
.andExpect(jsonPath("$.opera_operations[0].operation_status").value("PENDING"))
|
||||
.andExpect(jsonPath("$.opera_operations[1].operation_sequence").value(2))
|
||||
.andExpect(jsonPath("$.opera_operations[1].operation_status").value("PENDING"));
|
||||
|
||||
mockMvc.perform(get("/api/reservation/tasks/{taskId}", taskId))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.opera_operations.length()").value(2))
|
||||
.andExpect(jsonPath("$.opera_operations[0].operation_sequence").value(1))
|
||||
.andExpect(jsonPath("$.opera_operations[0].operation_status").value("PENDING"))
|
||||
.andExpect(jsonPath("$.opera_operations[1].operation_sequence").value(2))
|
||||
.andExpect(jsonPath("$.opera_operations[1].operation_status").value("PENDING"));
|
||||
|
||||
String taskStatus = jdbcTemplate.queryForObject("""
|
||||
SELECT task_status
|
||||
FROM workflow_reservation_task
|
||||
WHERE id = ?
|
||||
""", String.class, Long.valueOf(taskId));
|
||||
Long confirmedAtCount = jdbcTemplate.queryForObject("""
|
||||
SELECT COUNT(*)
|
||||
FROM workflow_reservation_task
|
||||
WHERE id = ?
|
||||
AND confirmed_at IS NOT NULL
|
||||
""", Long.class, Long.valueOf(taskId));
|
||||
String confirmedPayloadJson = jdbcTemplate.queryForObject("""
|
||||
SELECT confirmed_payload_json
|
||||
FROM workflow_reservation_task_card
|
||||
WHERE task_id = ?
|
||||
""", String.class, Long.valueOf(taskId));
|
||||
Long auditCount = jdbcTemplate.queryForObject("""
|
||||
SELECT COUNT(*)
|
||||
FROM workflow_reservation_audit_log
|
||||
WHERE task_id = ?
|
||||
AND action = 'TASK_CONFIRM'
|
||||
""", Long.class, Long.valueOf(taskId));
|
||||
assertThat(taskStatus).isEqualTo("READY");
|
||||
assertThat(confirmedAtCount).isEqualTo(1L);
|
||||
assertThat(confirmedPayloadJson).contains("\"extracted_fields.room_quantity\":4");
|
||||
assertThat(auditCount).isEqualTo(1L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldExecuteFirstOperaSimulationOperationAndRecordAttempt() throws Exception {
|
||||
String[] taskAndOperationIds = createReadyTaskWithTwoOperaOperations(
|
||||
"mail-cp5-opera-execute-001",
|
||||
"nonce-cp5-opera-execute-001",
|
||||
"CNF-CP5-OPERA-EXEC-001");
|
||||
String taskId = taskAndOperationIds[0];
|
||||
String firstOperationId = taskAndOperationIds[1];
|
||||
|
||||
mockMvc.perform(post(
|
||||
"/api/reservation/tasks/{taskId}/opera-operations/{operationId}/execute",
|
||||
taskId,
|
||||
firstOperationId)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"simulate_success": true
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.operation_id").value(firstOperationId))
|
||||
.andExpect(jsonPath("$.operation_status").value("SUCCEEDED"))
|
||||
.andExpect(jsonPath("$.attempt_count").value(1))
|
||||
.andExpect(jsonPath("$.attempts[0].attempt_number").value(1))
|
||||
.andExpect(jsonPath("$.attempts[0].attempt_status").value("SUCCEEDED"));
|
||||
|
||||
mockMvc.perform(get("/api/reservation/tasks/{taskId}", taskId))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.opera_operations[0].operation_id").value(firstOperationId))
|
||||
.andExpect(jsonPath("$.opera_operations[0].operation_status").value("SUCCEEDED"))
|
||||
.andExpect(jsonPath("$.opera_operations[0].attempt_count").value(1))
|
||||
.andExpect(jsonPath("$.opera_operations[0].attempts[0].attempt_status").value("SUCCEEDED"));
|
||||
|
||||
Long attemptCount = jdbcTemplate.queryForObject("""
|
||||
SELECT COUNT(*)
|
||||
FROM workflow_reservation_opera_operation_attempt
|
||||
WHERE operation_id = ?
|
||||
AND attempt_status = 'SUCCEEDED'
|
||||
""", Long.class, Long.valueOf(firstOperationId));
|
||||
Long auditCount = jdbcTemplate.queryForObject("""
|
||||
SELECT COUNT(*)
|
||||
FROM workflow_reservation_audit_log
|
||||
WHERE task_id = ?
|
||||
AND action = 'OPERA_OPERATION_EXECUTE'
|
||||
""", Long.class, Long.valueOf(taskId));
|
||||
assertThat(attemptCount).isEqualTo(1L);
|
||||
assertThat(auditCount).isEqualTo(1L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRetryFailedOperaSimulationOperationAndRecordSecondAttempt() throws Exception {
|
||||
String[] taskAndOperationIds = createReadyTaskWithTwoOperaOperations(
|
||||
"mail-cp5-opera-retry-001",
|
||||
"nonce-cp5-opera-retry-001",
|
||||
"CNF-CP5-OPERA-RETRY-001");
|
||||
String taskId = taskAndOperationIds[0];
|
||||
String firstOperationId = taskAndOperationIds[1];
|
||||
|
||||
mockMvc.perform(post(
|
||||
"/api/reservation/tasks/{taskId}/opera-operations/{operationId}/execute",
|
||||
taskId,
|
||||
firstOperationId)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"simulate_success": false,
|
||||
"failure_message": "模拟失败"
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.operation_status").value("FAILED"))
|
||||
.andExpect(jsonPath("$.attempt_count").value(1))
|
||||
.andExpect(jsonPath("$.attempts[0].attempt_status").value("FAILED"));
|
||||
|
||||
mockMvc.perform(post(
|
||||
"/api/reservation/tasks/{taskId}/opera-operations/{operationId}/retry",
|
||||
taskId,
|
||||
firstOperationId)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"simulate_success": true
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.operation_status").value("SUCCEEDED"))
|
||||
.andExpect(jsonPath("$.attempt_count").value(2))
|
||||
.andExpect(jsonPath("$.attempts[1].attempt_number").value(2))
|
||||
.andExpect(jsonPath("$.attempts[1].attempt_status").value("SUCCEEDED"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectExecutingSecondOperaOperationBeforePreviousSucceeded() throws Exception {
|
||||
String[] taskAndOperationIds = createReadyTaskWithTwoOperaOperations(
|
||||
"mail-cp5-opera-order-001",
|
||||
"nonce-cp5-opera-order-001",
|
||||
"CNF-CP5-OPERA-ORDER-001");
|
||||
String taskId = taskAndOperationIds[0];
|
||||
String secondOperationId = taskAndOperationIds[2];
|
||||
|
||||
mockMvc.perform(post(
|
||||
"/api/reservation/tasks/{taskId}/opera-operations/{operationId}/execute",
|
||||
taskId,
|
||||
secondOperationId)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"simulate_success": true
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isConflict())
|
||||
.andExpect(jsonPath("$.error_code").value("OPERA_PREVIOUS_OPERATION_NOT_SUCCEEDED"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldListTaskAuditLogsForConfirmationAndOperaExecution() throws Exception {
|
||||
String[] taskAndOperationIds = createReadyTaskWithTwoOperaOperations(
|
||||
"mail-cp5-audit-list-001",
|
||||
"nonce-cp5-audit-list-001",
|
||||
"CNF-CP5-AUDIT-001");
|
||||
String taskId = taskAndOperationIds[0];
|
||||
String firstOperationId = taskAndOperationIds[1];
|
||||
|
||||
mockMvc.perform(post(
|
||||
"/api/reservation/tasks/{taskId}/opera-operations/{operationId}/execute",
|
||||
taskId,
|
||||
firstOperationId)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"simulate_success": true
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
mockMvc.perform(get("/api/reservation/tasks/{taskId}/audits", taskId))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.task_id").value(taskId))
|
||||
.andExpect(jsonPath("$.items[?(@.action=='TASK_CONFIRM')].action")
|
||||
.value(org.hamcrest.Matchers.contains("TASK_CONFIRM")))
|
||||
.andExpect(jsonPath("$.items[?(@.action=='OPERA_OPERATION_EXECUTE')].action")
|
||||
.value(org.hamcrest.Matchers.contains("OPERA_OPERATION_EXECUTE")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectConfirmWhenRequiredMatrixFieldMissing() throws Exception {
|
||||
SourceMessageCaptureResult source = captureSourceMessage("mail-cp5-required-missing-001");
|
||||
String body = minimalBody(source.inboxId().toString(), "New Booking", "normal_task", "new_fit_reservation", """
|
||||
"case_keys": {},
|
||||
"extracted_fields": {
|
||||
"booking_object_type": "FIT Reservation"
|
||||
}
|
||||
""");
|
||||
MvcResult createResult = mockMvc.perform(signedPost(body, "nonce-cp5-required-missing-001"))
|
||||
.andExpect(status().isCreated())
|
||||
.andReturn();
|
||||
String taskId = com.jayway.jsonpath.JsonPath.read(
|
||||
createResult.getResponse().getContentAsString(),
|
||||
"$.items[0].task_id"
|
||||
);
|
||||
|
||||
mockMvc.perform(post("/api/reservation/tasks/{taskId}/confirm", taskId)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"field_values": {}
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.error_code").value("TASK_FIELD_VALIDATION_FAILED"))
|
||||
.andExpect(jsonPath("$.details[0]").value(containsString("extracted_fields.arrival_date")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectConfirmWhenEnumValueIsOutsideMatrixOptions() throws Exception {
|
||||
SourceMessageCaptureResult source = captureSourceMessage("mail-cp5-enum-invalid-001");
|
||||
String body = completeNewBookingBody(source.inboxId().toString(), "CNF-CP5-ENUM-001");
|
||||
MvcResult createResult = mockMvc.perform(signedPost(body, "nonce-cp5-enum-invalid-001"))
|
||||
.andExpect(status().isCreated())
|
||||
.andReturn();
|
||||
String taskId = com.jayway.jsonpath.JsonPath.read(
|
||||
createResult.getResponse().getContentAsString(),
|
||||
"$.items[0].task_id"
|
||||
);
|
||||
|
||||
mockMvc.perform(post("/api/reservation/tasks/{taskId}/confirm", taskId)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"field_values": {
|
||||
"extracted_fields.pms_room_type_code": "BAD_ROOM"
|
||||
}
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.error_code").value("TASK_FIELD_VALIDATION_FAILED"))
|
||||
.andExpect(jsonPath("$.details[0]").value(containsString("extracted_fields.pms_room_type_code")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectConfirmWhenDateRuleIsInvalid() throws Exception {
|
||||
SourceMessageCaptureResult source = captureSourceMessage("mail-cp5-date-invalid-001");
|
||||
String body = completeNewBookingBody(source.inboxId().toString(), "CNF-CP5-DATE-001");
|
||||
MvcResult createResult = mockMvc.perform(signedPost(body, "nonce-cp5-date-invalid-001"))
|
||||
.andExpect(status().isCreated())
|
||||
.andReturn();
|
||||
String taskId = com.jayway.jsonpath.JsonPath.read(
|
||||
createResult.getResponse().getContentAsString(),
|
||||
"$.items[0].task_id"
|
||||
);
|
||||
|
||||
mockMvc.perform(post("/api/reservation/tasks/{taskId}/confirm", taskId)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"field_values": {
|
||||
"extracted_fields.arrival_date": "not-a-date"
|
||||
}
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.error_code").value("TASK_FIELD_VALIDATION_FAILED"))
|
||||
.andExpect(jsonPath("$.details[0]").value(containsString("extracted_fields.arrival_date")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectDraftSaveWhenPreviousQueueTaskBlocksCurrentTask() throws Exception {
|
||||
SourceMessageCaptureResult source = captureSourceMessage("mail-cp5-draft-blocked-001");
|
||||
String body = twoTaskBody(source.inboxId().toString(), "GRP-CP5-BLOCKED-001");
|
||||
MvcResult createResult = mockMvc.perform(signedPost(body, "nonce-cp5-draft-blocked-001"))
|
||||
.andExpect(status().isCreated())
|
||||
.andReturn();
|
||||
String secondTaskId = com.jayway.jsonpath.JsonPath.read(
|
||||
createResult.getResponse().getContentAsString(),
|
||||
"$.items[1].task_id"
|
||||
);
|
||||
|
||||
mockMvc.perform(put("/api/reservation/tasks/{taskId}/draft", secondTaskId)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"field_values": {
|
||||
"extracted_fields.update_subtypes[]": ["update_stay_dates"]
|
||||
}
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isConflict())
|
||||
.andExpect(jsonPath("$.error_code").value("TASK_READ_ONLY"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldConvertFallbackToUpdateBookingAndLogicDeleteEmptyTemporaryOrder() throws Exception {
|
||||
SourceMessageCaptureResult targetSource = captureSourceMessage("mail-fallback-target-order-001");
|
||||
@@ -809,6 +1172,58 @@ class SuperAgentTaskResultControllerTest {
|
||||
""".formatted(sourceMessageId, groupCode, groupCode);
|
||||
}
|
||||
|
||||
private String completeNewBookingBody(String sourceMessageId, String confirmationNumber) {
|
||||
String caseKeys = confirmationNumber == null || confirmationNumber.isBlank()
|
||||
? "{}"
|
||||
: "{\"confirmation_number\": \"" + confirmationNumber + "\"}";
|
||||
return minimalBody(sourceMessageId, "New Booking", "normal_task", "new_fit_reservation", """
|
||||
"case_keys": %s,
|
||||
"extracted_fields": {
|
||||
"booking_object_type": "FIT Reservation",
|
||||
"arrival_date": "2026-08-01",
|
||||
"departure_date": "2026-08-02",
|
||||
"room_quantity": 2,
|
||||
"room_type": "Deluxe King",
|
||||
"pms_room_type_code": "RM2"
|
||||
}
|
||||
""".formatted(caseKeys));
|
||||
}
|
||||
|
||||
private String[] createReadyTaskWithTwoOperaOperations(
|
||||
String externalMessageId,
|
||||
String nonce,
|
||||
String confirmationNumber) throws Exception {
|
||||
SourceMessageCaptureResult source = captureSourceMessage(externalMessageId);
|
||||
String body = completeNewBookingBody(source.inboxId().toString(), confirmationNumber);
|
||||
MvcResult createResult = mockMvc.perform(signedPost(body, nonce))
|
||||
.andExpect(status().isCreated())
|
||||
.andReturn();
|
||||
String taskId = com.jayway.jsonpath.JsonPath.read(
|
||||
createResult.getResponse().getContentAsString(),
|
||||
"$.items[0].task_id"
|
||||
);
|
||||
|
||||
MvcResult confirmResult = mockMvc.perform(post("/api/reservation/tasks/{taskId}/confirm", taskId)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"field_values": {}
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.opera_operations.length()").value(2))
|
||||
.andReturn();
|
||||
String firstOperationId = com.jayway.jsonpath.JsonPath.read(
|
||||
confirmResult.getResponse().getContentAsString(),
|
||||
"$.opera_operations[0].operation_id"
|
||||
);
|
||||
String secondOperationId = com.jayway.jsonpath.JsonPath.read(
|
||||
confirmResult.getResponse().getContentAsString(),
|
||||
"$.opera_operations[1].operation_id"
|
||||
);
|
||||
return new String[] {taskId, firstOperationId, secondOperationId};
|
||||
}
|
||||
|
||||
private String signature(String body, String nonce, String timestamp) throws Exception {
|
||||
return signature(body, nonce, timestamp, CLIENT_ID);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user