fix: require dedicated appointment cancellation command
This commit is contained in:
+3
-2
@@ -95,14 +95,15 @@ public class InboundFbaAppointmentService {
|
||||
}
|
||||
@Transactional(isolation=org.springframework.transaction.annotation.Isolation.READ_COMMITTED)
|
||||
public FbaAppointmentResponse update(String operatorId,String appointmentNo,UpdateFbaAppointmentRequest request) {
|
||||
if ("CANCELLED".equals(status(request.details().appointmentStatus()))) throw bad("取消预约请使用取消操作并填写实际取消原因");
|
||||
return command("UPDATE:"+appointmentNo,operatorId,request.idempotencyKey(),request,()->{
|
||||
var appointment=find(appointmentNo,true);if(appointment==null) throw missing();
|
||||
if(!Objects.equals(appointment.getVersion(),request.version())) throw conflict("预约已更新,请重新读取后操作");
|
||||
if(queryMapper.countActiveDispatches(appointment.getId())>0) throw conflict("已关联有效排车,请先核对并处理运输安排后再改约");
|
||||
var current=items(appointment.getId());
|
||||
String status=status(request.details().appointmentStatus());
|
||||
if(Set.of("FAILED","CANCELLED").contains(status)) {
|
||||
if(clean(request.details().appointmentRemark())==null) throw bad("取消或未约成时请填写原因");
|
||||
if("FAILED".equals(status)) {
|
||||
if(clean(request.details().appointmentRemark())==null) throw bad("未约成时请填写原因");
|
||||
// Keep the last planned and confirmed times as historical facts.
|
||||
appointment.setAppointmentStatus(status);appointment.setAppointmentRemark(request.details().appointmentRemark().trim());
|
||||
current.forEach(item->{item.setActive(false);itemMapper.updateById(item);});
|
||||
|
||||
@@ -56,7 +56,7 @@ paths:
|
||||
'403': {description: 无读取权限}
|
||||
put:
|
||||
summary: 按版本更新预约
|
||||
description: '权限:platform:overseas-warehouse:inbound-order:fba-appointment。平台限定,版本及持久化幂等校验;已有有效排车时先处理运输安排。取消/失败必须写原因并保留原确认时间。恢复时重新核对来源、目的仓和占用;不能直接将SUCCESS退回PENDING。'
|
||||
description: '权限:platform:overseas-warehouse:inbound-order:fba-appointment。平台限定,版本及持久化幂等校验;已有有效排车时先处理运输安排。未约成必须写原因并保留原确认时间。禁止通过更新设置CANCELLED,取消必须调用DELETE并通过独立取消权限、实际原因和历史规则。恢复时重新核对来源、目的仓和占用;不能直接将SUCCESS退回PENDING。'
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
@@ -220,7 +220,7 @@ components:
|
||||
confirmationNo: {type: string, maxLength: 128, nullable: true, description: 仅SUCCESS登记收货方的外部约号;同一FBA仓不得重复}
|
||||
appointmentAt: {type: string, nullable: true, description: 对方确认开始当地时间;SUCCESS必填,PENDING不能当作计划填写}
|
||||
appointmentEndAt: {type: string, nullable: true, description: 明确结束时间时必须晚于开始时间;未知留空,不推算}
|
||||
appointmentStatus: {type: string, enum: [PENDING, SUCCESS, FAILED, CANCELLED]}
|
||||
appointmentStatus: {type: string, enum: [PENDING, SUCCESS, FAILED]}
|
||||
appointmentRemark: {type: string, maxLength: 1000, nullable: true}
|
||||
startOffset: {type: string, maxLength: 6, nullable: true, description: 当地重叠时刻的明确时差;必须符合目的仓IANA时区}
|
||||
endOffset: {type: string, maxLength: 6, nullable: true}
|
||||
|
||||
+14
@@ -112,6 +112,20 @@ class FbaAppointmentPersistenceTest {
|
||||
assertEquals(2,db.queryForObject("SELECT COUNT(*) FROM ll_inbound_fba_appointment_item WHERE active=1",Integer.class));
|
||||
}
|
||||
}
|
||||
@Test void restoringCancelledAppointmentRechecksClaimsAndPreservesCancellationHistory() {
|
||||
try(var context=new AnnotationConfigApplicationContext(Config.class)) {
|
||||
var service=context.getBean(InboundFbaAppointmentService.class);
|
||||
var original=service.createFromSources("operator",request("restore-original"));
|
||||
service.delete("operator",original.appointmentNo(),new CancelFbaAppointmentRequest(0,"仓库取消时段"));
|
||||
var history=service.cancellationHistory(original.appointmentNo(),1,20).records();
|
||||
var restored=service.update("operator",original.appointmentNo(),new UpdateFbaAppointmentRequest(request("unused").details(),1,"restore-command"));
|
||||
assertEquals("PENDING",restored.appointmentStatus());
|
||||
assertEquals(history,service.cancellationHistory(original.appointmentNo(),1,20).records());
|
||||
assertEquals("CANCELLED",history.get(0).snapshot().appointmentStatus());
|
||||
assertEquals("仓库取消时段",history.get(0).cancelReason());
|
||||
assertThrows(ResponseStatusException.class,()->service.createFromSources("operator",request("overlap-restored")));
|
||||
}
|
||||
}
|
||||
@Test void cancellationHistoryIsPersistedPagedScopedAndDoesNotExposeIdentities() throws Exception {
|
||||
try(var context=new AnnotationConfigApplicationContext(Config.class)) {
|
||||
var service=context.getBean(InboundFbaAppointmentService.class);
|
||||
|
||||
+13
@@ -154,6 +154,19 @@ class InboundFbaAppointmentServiceTest {
|
||||
assertThrows(ResponseStatusException.class,()->service.update("operator",result.appointmentNo(),new UpdateFbaAppointmentRequest(pending("change"),0,"blocked-update")));
|
||||
assertThrows(ResponseStatusException.class,()->service.delete("operator",result.appointmentNo(),new CancelFbaAppointmentRequest(0,"收货方取消时段")));assertTrue(storedItems.stream().allMatch(InboundFbaAppointmentItem::getActive));
|
||||
}
|
||||
@Test void ordinaryUpdateCannotCancelOrUseTheExistingRemarkAsCancellationReason() {
|
||||
var original=service.createFromSources("operator",request("cancel-bypass",pending("原业务备注"),"ORDER:1"));
|
||||
var details=new FbaAppointmentDetails(null,null,null,null,null,null,"CANCELLED","原业务备注",null,null);
|
||||
clearInvocations(commands,headers,items);
|
||||
var error=assertThrows(ResponseStatusException.class,()->service.update("operator",original.appointmentNo(),new UpdateFbaAppointmentRequest(details,0,"bypass")));
|
||||
assertEquals(400,error.getStatusCode().value());
|
||||
assertEquals("PENDING",storedHeaders.get(0).getAppointmentStatus());
|
||||
assertEquals("原业务备注",storedHeaders.get(0).getAppointmentRemark());
|
||||
assertEquals(0,storedHeaders.get(0).getVersion());
|
||||
assertTrue(storedItems.stream().allMatch(InboundFbaAppointmentItem::getActive));
|
||||
verify(commands,never()).begin(anyString(),anyString(),anyString(),anyString(),any());
|
||||
verify(headers,never()).update(any(InboundFbaAppointment.class),any());
|
||||
}
|
||||
@Test void cancellationPreservesConfirmedFactsAndHistoryButCannotRestoreOverAnotherActiveClaim() {
|
||||
var result=service.createFromSources("operator",request("confirmed",confirmed(),"ORDER:1"));
|
||||
service.delete("operator",result.appointmentNo(),new CancelFbaAppointmentRequest(0,"收货方取消时段"));
|
||||
|
||||
Reference in New Issue
Block a user