fix: retain appointment cancellation reasons and query history
This commit is contained in:
+25
-2
@@ -7,6 +7,9 @@ import com.baiyue.lastleg.inboundplan.dto.ConvertContainerInboundPlanRequest;
|
||||
import com.baiyue.lastleg.inboundplan.dto.ContainerInboundPlanPreviewResponse;
|
||||
import com.baiyue.lastleg.inboundplan.dto.ManualInboundPlanRequest;
|
||||
import com.baiyue.lastleg.inboundplan.dto.BatchFbaAppointmentRequest;
|
||||
import com.baiyue.lastleg.inboundplan.dto.CancelFbaAppointmentRequest;
|
||||
import com.baiyue.lastleg.inboundplan.dto.FbaAppointmentCancellationHistory;
|
||||
import com.baiyue.lastleg.inboundplan.dto.FbaAppointmentCancellationSummary;
|
||||
import com.baiyue.lastleg.inboundplan.dto.FbaAppointmentResponse;
|
||||
import com.baiyue.lastleg.inboundplan.dto.InboundPlanResponse;
|
||||
import com.baiyue.lastleg.inboundplan.dto.InboundPlanListResponse;
|
||||
@@ -167,12 +170,32 @@ public class InboundPlanController {
|
||||
@PreAuthorize("hasAuthority('platform:appointment:delete')")
|
||||
@Operation(summary = "删除未关联排车的预约", description = "Required authority: platform:appointment:delete")
|
||||
public ApiResponse<Void> deleteFbaAppointment(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String appointmentNo, @RequestParam int version) {
|
||||
@PathVariable String appointmentNo, @Valid @RequestBody CancelFbaAppointmentRequest request) {
|
||||
requireAppointmentPlatform(user);
|
||||
appointmentService.delete(String.valueOf(user.userId()), appointmentNo, version);
|
||||
appointmentService.delete(String.valueOf(user.userId()), appointmentNo, request);
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
@GetMapping("/fba-appointments/{appointmentNo}/cancellations")
|
||||
@PreAuthorize("hasAnyAuthority('" + OverseasPermissions.INBOUND_ORDER_LIST + "','" + OverseasPermissions.APPOINTMENT_LIST + "')")
|
||||
@Operation(summary = "分页读取预约取消历史", description = "平台专用;沿用预约详情只读权限,不返回内部身份或原始命令JSON")
|
||||
public ApiResponse<PageResult<FbaAppointmentCancellationHistory>> fbaAppointmentCancellations(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,@PathVariable String appointmentNo,
|
||||
@RequestParam(defaultValue="1") int page,@RequestParam(defaultValue="20") int size) {
|
||||
requireAppointmentPlatform(user);
|
||||
return ApiResponse.success(appointmentService.cancellationHistory(appointmentNo,page,size));
|
||||
}
|
||||
|
||||
@GetMapping("/fba-appointments/cancellations")
|
||||
@PreAuthorize("hasAnyAuthority('" + OverseasPermissions.INBOUND_ORDER_LIST + "','" + OverseasPermissions.APPOINTMENT_LIST + "')")
|
||||
@Operation(summary = "分页读取预约取消记录摘要", description = "平台专用;沿用预约详情只读权限,仅取消命令摘要,不逐行查询")
|
||||
public ApiResponse<PageResult<FbaAppointmentCancellationSummary>> fbaAppointmentCancellationRecords(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,@RequestParam(defaultValue="1") int page,
|
||||
@RequestParam(defaultValue="20") int size) {
|
||||
requireAppointmentPlatform(user);
|
||||
return ApiResponse.success(appointmentService.cancellations(page,size));
|
||||
}
|
||||
|
||||
private void requireAppointmentPlatform(AuthenticatedUser user) {
|
||||
if (user == null || user.userId() == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
||||
if (!"PLATFORM".equals(user.appCode()) || user.tenantId() != 0L) {
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package com.baiyue.lastleg.inboundplan.dto;
|
||||
|
||||
import jakarta.validation.constraints.*;
|
||||
|
||||
public record CancelFbaAppointmentRequest(@NotNull @Min(0) Integer version,
|
||||
@NotBlank @Size(max=1000) String cancelReason) { }
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package com.baiyue.lastleg.inboundplan.dto;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/** Business-only cancellation history; never exposes command JSON or internal identities. */
|
||||
public record FbaAppointmentCancellationHistory(String appointmentNo, Integer version, String cancelReason,
|
||||
LocalDateTime cancelledAt, Snapshot snapshot) {
|
||||
public record Snapshot(String fbaWarehouseCode, String appointmentStatus, String appointmentRemark,
|
||||
LocalDate plannedDate, LocalDateTime plannedAt, LocalDateTime plannedEndAt,
|
||||
String confirmationNo, LocalDateTime appointmentAt, LocalDateTime appointmentEndAt,
|
||||
String timezone, int selectedCount) { }
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package com.baiyue.lastleg.inboundplan.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public record FbaAppointmentCancellationSummary(String appointmentNo,Integer version,String cancelReason,
|
||||
LocalDateTime cancelledAt,String fbaWarehouseCode,int selectedCount) { }
|
||||
+5
@@ -11,4 +11,9 @@ public interface FbaAppointmentCommandMapper {
|
||||
int begin(@Param("key") String key,@Param("hash") String hash,@Param("operation") String operation,@Param("operator") String operator,@Param("createdAt") java.time.LocalDateTime createdAt);
|
||||
@Update("UPDATE ll_fba_appointment_command SET appointment_no=#{appointmentNo},response_json=#{response} WHERE command_key=#{key} AND response_json IS NULL")
|
||||
int finish(@Param("key") String key,@Param("appointmentNo") String appointmentNo,@Param("response") String response);
|
||||
record Cancellation(String responseJson,java.time.LocalDateTime createdAt) { }
|
||||
@Select("SELECT COUNT(*) FROM ll_fba_appointment_command WHERE operation=CONCAT('CANCEL:',appointment_no) AND response_json IS NOT NULL AND (#{number} IS NULL OR appointment_no=#{number})")
|
||||
long cancellationCount(@Param("number") String number);
|
||||
@Select("SELECT response_json AS responseJson,created_at AS createdAt FROM ll_fba_appointment_command WHERE operation=CONCAT('CANCEL:',appointment_no) AND response_json IS NOT NULL AND (#{number} IS NULL OR appointment_no=#{number}) ORDER BY created_at DESC,command_key DESC LIMIT #{size} OFFSET #{offset}")
|
||||
java.util.List<Cancellation> cancellations(@Param("number") String number,@Param("size") int size,@Param("offset") long offset);
|
||||
}
|
||||
|
||||
+42
-5
@@ -120,12 +120,15 @@ public class InboundFbaAppointmentService {
|
||||
});
|
||||
}
|
||||
@Transactional(isolation=org.springframework.transaction.annotation.Isolation.READ_COMMITTED)
|
||||
public void delete(String operatorId,String appointmentNo,int version) {
|
||||
command("CANCEL:"+appointmentNo,operatorId,"CANCEL:"+appointmentNo+":"+version,version,()->{
|
||||
public void delete(String operatorId,String appointmentNo,CancelFbaAppointmentRequest request) {
|
||||
if(request==null || request.version()==null || request.version()<0 || clean(request.cancelReason())==null || request.cancelReason().length()>1000) throw bad("请填写取消原因和有效版本");
|
||||
int version=request.version();
|
||||
var cancellation=new CancelFbaAppointmentRequest(version,request.cancelReason().trim());
|
||||
command("CANCEL:"+appointmentNo,operatorId,"CANCEL:"+appointmentNo+":"+version,cancellation,()->{
|
||||
var appointment=find(appointmentNo,true);if(appointment==null) throw missing();
|
||||
if(!Objects.equals(appointment.getVersion(),version)) throw conflict("预约已更新,请重新读取后操作");
|
||||
if(queryMapper.countActiveDispatches(appointment.getId())>0) throw conflict("预约货物已关联排车,不允许取消,请先处理关联排车");
|
||||
appointment.setAppointmentStatus("CANCELLED");appointment.setAppointmentRemark("操作员取消预约安排");
|
||||
appointment.setAppointmentStatus("CANCELLED");
|
||||
appointment.setVersion(version+1);appointment.setUpdatedAt(now());appointmentMapper.updateById(appointment);
|
||||
var rows=items(appointment.getId());rows.forEach(item->{item.setActive(false);itemMapper.updateById(item);});
|
||||
return response(appointment,rows);
|
||||
@@ -196,13 +199,47 @@ public class InboundFbaAppointmentService {
|
||||
try {commands.begin(key,hash,operation,operator,now());}
|
||||
catch(DuplicateKeyException duplicate){return replay(commands.find(key),hash);}
|
||||
var response=action.get();
|
||||
try {if(commands.finish(key,response.appointmentNo(),json.writeValueAsString(response))!=1)throw conflict("预约命令结果未能保存");}
|
||||
try {if(commands.finish(key,response.appointmentNo(),commandSnapshot(response,request))!=1)throw conflict("预约命令结果未能保存");}
|
||||
catch(com.fasterxml.jackson.core.JsonProcessingException error){throw new IllegalStateException("无法保存预约命令结果",error);}
|
||||
return response;
|
||||
}
|
||||
private String commandSnapshot(FbaAppointmentResponse response,Object request) throws com.fasterxml.jackson.core.JsonProcessingException {
|
||||
var node=json.valueToTree(response);
|
||||
if(request instanceof CancelFbaAppointmentRequest cancellation) ((com.fasterxml.jackson.databind.node.ObjectNode)node).put("cancelReason",cancellation.cancelReason());
|
||||
return json.writeValueAsString(node);
|
||||
}
|
||||
private FbaAppointmentResponse readSnapshot(String value) throws com.fasterxml.jackson.core.JsonProcessingException {
|
||||
var node=(com.fasterxml.jackson.databind.node.ObjectNode)json.readTree(value);
|
||||
node.remove("cancelReason");
|
||||
return json.treeToValue(node,FbaAppointmentResponse.class);
|
||||
}
|
||||
@Transactional(readOnly=true)
|
||||
public PageResult<FbaAppointmentCancellationHistory> cancellationHistory(String appointmentNo,int page,int size) {
|
||||
if(page<1 || size<1 || size>100) throw bad("分页参数无效");
|
||||
var appointment=find(appointmentNo,false);if(appointment==null) throw missing();
|
||||
return readCancellationHistory(appointment.getAppointmentNo(),page,size);
|
||||
}
|
||||
@Transactional(readOnly=true)
|
||||
public PageResult<FbaAppointmentCancellationSummary> cancellations(int page,int size) {
|
||||
if(page<1 || size<1 || size>100) throw bad("分页参数无效");
|
||||
var history=readCancellationHistory(null,page,size);
|
||||
return new PageResult<>(history.records().stream().map(row->new FbaAppointmentCancellationSummary(row.appointmentNo(),row.version(),row.cancelReason(),row.cancelledAt(),row.snapshot().fbaWarehouseCode(),row.snapshot().selectedCount())).toList(),page,size,history.total(),history.pages());
|
||||
}
|
||||
private PageResult<FbaAppointmentCancellationHistory> readCancellationHistory(String number,int page,int size) {
|
||||
long total=commands.cancellationCount(number);
|
||||
var records=commands.cancellations(number,size,(long)(page-1)*size).stream().map(row->{
|
||||
try {
|
||||
var snapshot=readSnapshot(row.responseJson());
|
||||
var reason=json.readTree(row.responseJson()).path("cancelReason");
|
||||
return new FbaAppointmentCancellationHistory(snapshot.appointmentNo(),snapshot.version(),reason.isTextual()?clean(reason.asText()):null,row.createdAt(),
|
||||
new FbaAppointmentCancellationHistory.Snapshot(snapshot.fbaWarehouseCode(),snapshot.appointmentStatus(),snapshot.appointmentRemark(),snapshot.plannedDate(),snapshot.plannedAt(),snapshot.plannedEndAt(),snapshot.confirmationNo(),snapshot.appointmentAt(),snapshot.appointmentEndAt(),snapshot.timezone(),snapshot.selectedCount()));
|
||||
} catch(com.fasterxml.jackson.core.JsonProcessingException error) {throw new IllegalStateException("取消历史不可读",error);}
|
||||
}).toList();
|
||||
return new PageResult<>(records,page,size,total,(total+size-1)/size);
|
||||
}
|
||||
private FbaAppointmentResponse replay(FbaAppointmentCommandMapper.Command value,String hash) {
|
||||
if(value==null || !hash.equals(value.requestHash()) || value.responseJson()==null)throw conflict("重复请求内容不一致或结果尚未就绪,请重新核对");
|
||||
try {return json.readValue(value.responseJson(),FbaAppointmentResponse.class);}
|
||||
try {return readSnapshot(value.responseJson());}
|
||||
catch(com.fasterxml.jackson.core.JsonProcessingException error){throw new IllegalStateException("预约命令记录不可读",error);}
|
||||
}
|
||||
private InboundFbaAppointment find(String number,boolean lock) {
|
||||
|
||||
@@ -34,9 +34,17 @@ paths:
|
||||
schema: {type: string}
|
||||
delete:
|
||||
summary: 取消未关联有效排车的预约并释放占用
|
||||
description: '平台专用,权限platform:appointment:delete。校验版本后保留主单、确认事实及明细历史,设置CANCELLED并释放货源占用;不删除客户货物或回退库存。已有关联有效排车时拒绝。取消命令按系统单号、版本和操作者幂等。'
|
||||
parameters:
|
||||
- {name: version, in: query, required: true, schema: {type: integer, minimum: 0}}
|
||||
description: '平台专用,权限platform:appointment:delete。校验版本后保留主单、确认事实及明细历史,设置CANCELLED并释放货源占用;不删除客户货物或回退库存。已有关联有效排车时拒绝。取消原因必填,独立保存于取消命令历史,不覆盖原业务备注;按系统单号、版本、操作者及原因校验幂等,异内容拒绝。'
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [version, cancelReason]
|
||||
properties:
|
||||
version: {type: integer, minimum: 0}
|
||||
cancelReason: {type: string, minLength: 1, maxLength: 1000, description: 实际取消原因,不能仅含空白}
|
||||
responses:
|
||||
'200': {description: 业务响应,成功code为字符串200;版本冲突或已关联排车返回业务错误}
|
||||
'403': {description: 无删除权限}
|
||||
@@ -63,6 +71,93 @@ paths:
|
||||
responses:
|
||||
'200': {$ref: '#/components/responses/AppointmentResult'}
|
||||
'403': {description: 无写权限}
|
||||
/api/platform/v1/last-leg/inbound-plans/fba-appointments/cancellations:
|
||||
get:
|
||||
summary: 分页发现预约快捷取消记录
|
||||
description: '仅平台;权限同详情:platform:appointment:list或platform:overseas-warehouse:inbound-order:list。仅取消命令摘要,不扩展普通编辑历史。服务端分页,不逐行读取详情;按取消时间倒序。'
|
||||
parameters:
|
||||
- {name: page, in: query, schema: {type: integer, minimum: 1, default: 1}}
|
||||
- {name: size, in: query, schema: {type: integer, minimum: 1, maximum: 100, default: 20}}
|
||||
responses:
|
||||
'200':
|
||||
description: code为字符串200;data含records、page、size、total、pages
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
code: {type: string}
|
||||
data:
|
||||
type: object
|
||||
properties:
|
||||
page: {type: integer}
|
||||
size: {type: integer}
|
||||
total: {type: integer, format: int64}
|
||||
pages: {type: integer, format: int64}
|
||||
records:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
appointmentNo: {type: string}
|
||||
version: {type: integer}
|
||||
cancelReason: {type: string, nullable: true}
|
||||
cancelledAt: {type: string, description: UTC取消时间}
|
||||
fbaWarehouseCode: {type: string}
|
||||
selectedCount: {type: integer}
|
||||
'400': {description: 分页参数无效}
|
||||
'403': {description: 非平台身份或无读取权限}
|
||||
/api/platform/v1/last-leg/inbound-plans/fba-appointments/{appointmentNo}/cancellations:
|
||||
get:
|
||||
summary: 分页读取此预约的快捷取消命令历史
|
||||
description: '仅平台;权限同预约详情:platform:appointment:list或platform:overseas-warehouse:inbound-order:list。按预约业务号限定,最新取消在前。不包含普通编辑记录,不返回原始JSON、命令键或内部账号ID;历史缺原因返回null,不回填。'
|
||||
parameters:
|
||||
- {name: appointmentNo, in: path, required: true, schema: {type: string}}
|
||||
- {name: page, in: query, schema: {type: integer, minimum: 1, default: 1}}
|
||||
- {name: size, in: query, schema: {type: integer, minimum: 1, maximum: 100, default: 20}}
|
||||
responses:
|
||||
'200':
|
||||
description: 业务响应,code为字符串200
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
code: {type: string}
|
||||
message: {type: string}
|
||||
data:
|
||||
type: object
|
||||
properties:
|
||||
page: {type: integer}
|
||||
size: {type: integer}
|
||||
total: {type: integer, format: int64}
|
||||
pages: {type: integer, format: int64}
|
||||
records:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
appointmentNo: {type: string}
|
||||
version: {type: integer, description: 取消后版本}
|
||||
cancelReason: {type: string, nullable: true}
|
||||
cancelledAt: {type: string, description: UTC取消命令时间,yyyy-MM-ddTHH:mm:ss}
|
||||
snapshot:
|
||||
type: object
|
||||
properties:
|
||||
fbaWarehouseCode: {type: string}
|
||||
appointmentStatus: {type: string}
|
||||
appointmentRemark: {type: string, nullable: true, description: 原业务备注}
|
||||
plannedDate: {type: string, format: date, nullable: true}
|
||||
plannedAt: {type: string, nullable: true}
|
||||
plannedEndAt: {type: string, nullable: true}
|
||||
confirmationNo: {type: string, nullable: true}
|
||||
appointmentAt: {type: string, nullable: true}
|
||||
appointmentEndAt: {type: string, nullable: true}
|
||||
timezone: {type: string, nullable: true}
|
||||
selectedCount: {type: integer}
|
||||
'400': {description: 分页参数无效}
|
||||
'403': {description: 非平台身份或无详情读取权限}
|
||||
'404': {description: 预约不存在}
|
||||
/api/platform/v1/last-leg/inbound-plans/fba-appointments/calendar:
|
||||
get:
|
||||
operationId: fbaAppointmentCalendar
|
||||
|
||||
+4
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
+6
@@ -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());
|
||||
}
|
||||
|
||||
+32
-1
@@ -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"));
|
||||
|
||||
+18
-3
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user