fix: retain appointment cancellation reasons and query history

This commit is contained in:
2026-10-11 13:26:00 +08:00
parent d4b72594ab
commit 43494cbfa3
11 changed files with 255 additions and 14 deletions
@@ -105,6 +105,10 @@ class InboundPlanControllerTest {
assertThatThrownBy(() -> controller.fbaAppointmentCalendar(customer,
java.time.LocalDate.of(2026, 10, 1), java.time.LocalDate.of(2026, 11, 1)))
.isInstanceOf(ResponseStatusException.class);
assertThatThrownBy(() -> controller.fbaAppointmentCancellations(customer,"APT-1",1,20))
.isInstanceOf(ResponseStatusException.class);
assertThatThrownBy(() -> controller.fbaAppointmentCancellationRecords(customer,1,20))
.isInstanceOf(ResponseStatusException.class);
verify(appointmentService, never()).detail(org.mockito.ArgumentMatchers.any());
}
@@ -30,6 +30,12 @@ class InboundPlanPermissionContractTest {
}
assertEquals("hasAnyAuthority('" + LIST + "','platform:appointment:list')",
methods.get("fbaAppointmentDetail").getAnnotation(PreAuthorize.class).value());
assertEquals("hasAnyAuthority('" + LIST + "','platform:appointment:list')",
methods.get("fbaAppointmentCancellations").getAnnotation(PreAuthorize.class).value());
assertEquals("hasAnyAuthority('" + LIST + "','platform:appointment:list')",
methods.get("fbaAppointmentCancellationRecords").getAnnotation(PreAuthorize.class).value());
assertEquals("hasAuthority('platform:appointment:delete')",
methods.get("deleteFbaAppointment").getAnnotation(PreAuthorize.class).value());
assertEquals("hasAuthority('platform:appointment:list')",
methods.get("fbaAppointmentCalendar").getAnnotation(PreAuthorize.class).value());
}
@@ -1,5 +1,7 @@
package com.baiyue.lastleg.inboundplan.service;
import org.springframework.web.server.ResponseStatusException;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.MybatisSqlSessionFactoryBuilder;
import com.baiyue.lastleg.appointmentplanning.*;
@@ -102,7 +104,7 @@ class FbaAppointmentPersistenceTest {
@Test void cancellationReleasesGeneratedClaimsWhileReplayKeepsItsOriginalSnapshot() {
try(var context=new AnnotationConfigApplicationContext(Config.class)) {
var service=context.getBean(InboundFbaAppointmentService.class);var first=service.createFromSources("operator",request("first"));
service.delete("operator",first.appointmentNo(),0);
service.delete("operator",first.appointmentNo(),new CancelFbaAppointmentRequest(0,"收货方取消时段"));
var second=service.createFromSources("operator",request("second"));assertNotEquals(first.appointmentNo(),second.appointmentNo());
assertEquals("CANCELLED",service.detail(first.appointmentNo()).appointmentStatus());
assertEquals(first,service.createFromSources("operator",request("first")));
@@ -110,6 +112,35 @@ class FbaAppointmentPersistenceTest {
assertEquals(2,db.queryForObject("SELECT COUNT(*) FROM ll_inbound_fba_appointment_item WHERE active=1",Integer.class));
}
}
@Test void cancellationHistoryIsPersistedPagedScopedAndDoesNotExposeIdentities() throws Exception {
try(var context=new AnnotationConfigApplicationContext(Config.class)) {
var service=context.getBean(InboundFbaAppointmentService.class);
var original=service.createFromSources("internal-operator",request("history-first"));
var reason=new CancelFbaAppointmentRequest(0,"仓库临时关闭");
service.delete("internal-operator",original.appointmentNo(),reason);
service.delete("internal-operator",original.appointmentNo(),reason);
var page=service.cancellationHistory(original.appointmentNo(),1,1);
assertEquals(1,service.cancellations(1,1).total());
assertEquals(original.appointmentNo(),service.cancellations(1,1).records().get(0).appointmentNo());
assertTrue(service.cancellations(2,1).records().isEmpty());
assertEquals(1,page.total());assertEquals(1,page.records().size());
assertEquals("仓库临时关闭",page.records().get(0).cancelReason());
assertEquals(original.appointmentRemark(),page.records().get(0).snapshot().appointmentRemark());
assertEquals("CANCELLED",page.records().get(0).snapshot().appointmentStatus());
var json=new ObjectMapper().findAndRegisterModules().writeValueAsString(page);
assertFalse(json.contains("internal-operator"));assertFalse(json.contains("responseJson"));assertFalse(json.contains("sourceKey"));
assertTrue(service.cancellationHistory(original.appointmentNo(),2,1).records().isEmpty());
var second=service.createFromSources("internal-operator",request("history-second"));
assertEquals(0,service.cancellationHistory(second.appointmentNo(),1,20).total());
assertThrows(ResponseStatusException.class,()->service.cancellationHistory("AP-NOT-FOUND",1,20));
var db=new JdbcTemplate(context.getBean(DataSource.class));
var mapper=new ObjectMapper().findAndRegisterModules();
var value=(com.fasterxml.jackson.databind.node.ObjectNode)mapper.readTree(db.queryForObject("SELECT response_json FROM ll_fba_appointment_command WHERE operation=?",String.class,"CANCEL:"+original.appointmentNo()));
value.remove("cancelReason");
db.update("UPDATE ll_fba_appointment_command SET response_json=? WHERE operation=?",mapper.writeValueAsString(value),"CANCEL:"+original.appointmentNo());
assertNull(service.cancellationHistory(original.appointmentNo(),1,20).records().get(0).cancelReason());
}
}
@Test void unknownQuantityOnReviewDoesNotSilentlyKeepThePreviousKnownQuantity() {
try(var context=new AnnotationConfigApplicationContext(Config.class)) {
var service=context.getBean(InboundFbaAppointmentService.class);var saved=service.createFromSources("operator",request("create"));
@@ -146,17 +146,17 @@ class InboundFbaAppointmentServiceTest {
@Test void staleUpdateAndStaleCancellationDoNotWrite() {
var result=create("original");
assertThrows(ResponseStatusException.class,()->service.update("operator",result.appointmentNo(),new UpdateFbaAppointmentRequest(pending(null),2,"stale-update")));
assertThrows(ResponseStatusException.class,()->service.delete("operator",result.appointmentNo(),2));
assertThrows(ResponseStatusException.class,()->service.delete("operator",result.appointmentNo(),new CancelFbaAppointmentRequest(2,"收货方取消时段")));
verify(headers,never()).update(any(InboundFbaAppointment.class),any());verify(headers,never()).updateById(any(InboundFbaAppointment.class));
}
@Test void activeDispatchBlocksChangingOrCancellingItsAppointment() {
var result=create("original");when(query.countActiveDispatches(result.id())).thenReturn(1L);
assertThrows(ResponseStatusException.class,()->service.update("operator",result.appointmentNo(),new UpdateFbaAppointmentRequest(pending("change"),0,"blocked-update")));
assertThrows(ResponseStatusException.class,()->service.delete("operator",result.appointmentNo(),0));assertTrue(storedItems.stream().allMatch(InboundFbaAppointmentItem::getActive));
assertThrows(ResponseStatusException.class,()->service.delete("operator",result.appointmentNo(),new CancelFbaAppointmentRequest(0,"收货方取消时段")));assertTrue(storedItems.stream().allMatch(InboundFbaAppointmentItem::getActive));
}
@Test void cancellationPreservesConfirmedFactsAndHistoryButCannotRestoreOverAnotherActiveClaim() {
var result=service.createFromSources("operator",request("confirmed",confirmed(),"ORDER:1"));
service.delete("operator",result.appointmentNo(),0);
service.delete("operator",result.appointmentNo(),new CancelFbaAppointmentRequest(0,"收货方取消时段"));
assertEquals("CANCELLED",storedHeaders.get(0).getAppointmentStatus());assertEquals(at,storedHeaders.get(0).getAppointmentAt());assertFalse(storedItems.get(0).getActive());
assertEquals(result,service.createFromSources("operator",request("confirmed",confirmed(),"ORDER:1")));
when(query.countActiveSources(anyList(),anyList(),eq(result.id()))).thenReturn(1L);
@@ -172,4 +172,19 @@ class InboundFbaAppointmentServiceTest {
var one=create("first");when(headers.selectList(any())).thenReturn(List.copyOf(storedHeaders));clearInvocations(items);
var calendar=service.calendar(from,from.plusDays(30));assertEquals(one.items(),calendar.get(0).items());verify(items,times(1)).selectList(any());
}
@Test void cancellationRequiresReasonAndKeepsOriginalRemarkAndCommandIdentity() {
var original=service.createFromSources("operator",request("cancel-test",pending("保留业务备注"),"ORDER:1"));
for(String reason:Arrays.asList(null," ","x".repeat(1001))) {
assertThrows(ResponseStatusException.class,()->service.delete("operator",original.appointmentNo(),new CancelFbaAppointmentRequest(0,reason)));
}
var cancel=new CancelFbaAppointmentRequest(0,"收货方取消时段");
service.delete("operator",original.appointmentNo(),cancel);
service.delete("operator",original.appointmentNo(),cancel);
assertEquals("保留业务备注",service.detail(original.appointmentNo()).appointmentRemark());
assertEquals(1,service.detail(original.appointmentNo()).version());
assertThrows(ResponseStatusException.class,()->service.delete("operator",original.appointmentNo(),new CancelFbaAppointmentRequest(0,"不同原因")));
assertThrows(ResponseStatusException.class,()->service.delete("another-operator",original.appointmentNo(),cancel));
assertThrows(ResponseStatusException.class,()->service.cancellationHistory(original.appointmentNo(),0,20));
assertThrows(ResponseStatusException.class,()->service.cancellationHistory(original.appointmentNo(),1,101));
}
}