fix: validate transfer rejection and align dispatch HTTP contracts

This commit is contained in:
2026-09-14 13:46:32 +08:00
parent a2ea0ba9e6
commit d8e2dc59ae
9 changed files with 120 additions and 13 deletions
@@ -52,6 +52,7 @@ public final class WarehouseTransferRequests {
}
}
/** 审核通过备注选填;驳回必须提供非空原因,由服务在任何状态写入前校验。 */
public record Review(@NotBlank @Size(max = 32) String decision,
@Size(max = 1000) String remark) {}
@@ -358,6 +358,9 @@ public class WarehouseTransferService {
case "REJECT", REJECTED -> REJECTED;
default -> throw badRequest("Review decision must be APPROVE or REJECT");
};
if (REJECTED.equals(next) && !StringUtils.hasText(request.remark())) {
throw badRequest("驳回原因不能为空");
}
WarehouseTransferOrder transfer = transfers.lockNo(customerId, transferNo);
if (transfer == null) throw notFound("Warehouse transfer order not found");
if (!PENDING_REVIEW.equals(transfer.getStatus())) {
@@ -384,7 +387,8 @@ public class WarehouseTransferService {
order.setUpdatedAt(now); orders.updateById(order);
if (!java.util.Objects.equals(before, orderStatus)) {
insertOrderHistory(customerId, order.getId(), before, orderStatus, transferNo,
"Warehouse transfer reviewed: " + next, now);
"Warehouse transfer reviewed: " + next
+ (StringUtils.hasText(request.remark()) ? "; " + request.remark().trim() : ""), now);
}
return detail(customerId, transferNo);
}
@@ -222,7 +222,7 @@ class WarehouseTransferServiceTest {
when(sourceOrders.selectList(any())).thenReturn(List.of());
var result = service.review("TENANT-A", "WTR-1", "USER-1",
new WarehouseTransferRequests.Review("APPROVE", "approved"));
new WarehouseTransferRequests.Review("APPROVE", null));
assertEquals(WarehouseTransferService.APPROVED, transfer.getStatus());
assertEquals(WarehouseTransferService.PENDING_ASSIGNMENT, tailOrder.getCurrentStatus());
@@ -233,6 +233,41 @@ class WarehouseTransferServiceTest {
verifyNoInteractions(inventory);
}
@Test
void rejectionRequiresNonBlankReasonBeforeAnyStateOrInventoryChange() {
for (String decision : List.of("REJECT", "REJECTED")) {
for (String reason : new String[] { null, "", " \t\n" }) {
var error = assertThrows(ResponseStatusException.class, () -> service.review(
"TENANT-A", "WTR-1", "USER-1", new WarehouseTransferRequests.Review(decision, reason)));
assertEquals(400, error.getStatusCode().value());
assertEquals("驳回原因不能为空", error.getReason());
}
}
verifyNoInteractions(transfers, orders, orderHistories, inventory);
}
@Test
void rejectionWithReasonCancelsLinkedOrderAndPreservesReasonInHistory() {
WarehouseTransferOrder transfer = persistedTransfer();
transfer.setStatus(WarehouseTransferService.PENDING_REVIEW);
LastlegOrder tailOrder = historicalOrder();
tailOrder.setId(31L); tailOrder.setCurrentStatus(WarehouseTransferService.PENDING_REVIEW);
when(transfers.lockNo("TENANT-A", "WTR-1")).thenReturn(transfer);
when(transfers.findNo("TENANT-A", "WTR-1")).thenReturn(transfer);
when(orders.selectById(31L)).thenReturn(tailOrder);
when(transferItems.selectList(any())).thenReturn(List.of());
when(sourceOrders.selectList(any())).thenReturn(List.of());
var result = service.review("TENANT-A", "WTR-1", "USER-1",
new WarehouseTransferRequests.Review("REJECT", " 地址不完整 "));
assertEquals("REJECTED", result.status());
assertEquals("CANCELLED", tailOrder.getCurrentStatus());
var history = ArgumentCaptor.forClass(com.baiyue.lastleg.entity.LastlegOrderStatusHistory.class);
verify(orderHistories).insert(history.capture());
assertTrue(history.getValue().getRemark().endsWith("; 地址不完整"));
verifyNoInteractions(inventory);
}
@Test
void approvedTransferAllocatesExactSourceInboundPalletWithoutMovingInventory() {
WarehouseTransferOrder transfer = transfer("APPROVED");