Test #1

Merged
admin merged 1726 commits from test into main 2026-09-17 11:52:45 +08:00
3 changed files with 51 additions and 33 deletions
Showing only changes of commit 5c331f173d - Show all commits
@@ -12,7 +12,7 @@ public final class PdaResponses {
List<Long> unloadPhotoFileIds, LocalDateTime unloadPhotoUploadedAt,
LocalDateTime estimatedEmptyReturnAt, int expectedPackageCount,
int pickedPackageCount, String orderType, boolean wholePalletInbound,
List<DropshipPallet> dropshipPallets) {
List<DropshipPallet> dropshipPallets, String inboundMode) {
public Task(long taskId, String taskNo, String planNo, String status,
LocalDateTime pushedAt, LocalDateTime startedAt, LocalDateTime completedAt,
List<Long> unloadPhotoFileIds, LocalDateTime unloadPhotoUploadedAt,
@@ -20,7 +20,7 @@ public final class PdaResponses {
int pickedPackageCount) {
this(taskId, taskNo, planNo, status, pushedAt, startedAt, completedAt,
unloadPhotoFileIds, unloadPhotoUploadedAt, estimatedEmptyReturnAt,
expectedPackageCount, pickedPackageCount, null, false, List.of());
expectedPackageCount, pickedPackageCount, null, false, List.of(), null);
}
public Task(long taskId, String taskNo, String planNo, String status,
@@ -146,7 +146,7 @@ public class InboundPdaWorkflowService {
public PdaResponses.Operation count(String customerId, String planNo, long allocationId,
PdaRequests.PackageOperation request) {
InboundPlan plan = requirePlan(customerId, planNo);
requireLegacyCounting(plan);
requireCountMode(plan);
InboundPdaOperation replay = replay(customerId, "COUNT", request.idempotencyKey(), request);
if (replay != null) return read(replay, PdaResponses.Operation.class);
InboundPdaTask task = requireActiveTask(customerId, plan);
@@ -174,7 +174,7 @@ public class InboundPdaWorkflowService {
public PdaResponses.Operation pick(String customerId, String planNo, long allocationId,
PdaRequests.PackageOperation request) {
InboundPlan plan = requirePlan(customerId, planNo);
requireLegacyCounting(plan);
requireCountMode(plan);
InboundPdaOperation replay = replay(customerId, "PICK", request.idempotencyKey(), request);
if (replay != null) return read(replay, PdaResponses.Operation.class);
InboundPdaTask task = requireActiveTask(customerId, plan);
@@ -250,6 +250,7 @@ public class InboundPdaWorkflowService {
@Transactional
public PdaResponses.Carton scanCarton(String customerId, String planNo, PdaRequests.ScanCarton request) {
InboundPlan plan = requirePlan(customerId, planNo);
requireCartonScanMode(plan);
InboundPdaOperation replay = replay(customerId, "SCAN_CARTON", request.idempotencyKey(), request);
if (replay != null) return read(replay, PdaResponses.Carton.class);
InboundPdaTask task = requireActiveTask(customerId, plan);
@@ -271,22 +272,12 @@ public class InboundPdaWorkflowService {
: requireLine(plan.getId(), carton.getLineId());
if (line == null) throw notFound("Carton mark does not belong to cargo in this inbound plan");
int requestedCount = request.packageCount() == null ? 1 : request.packageCount();
if (requestedCount < 1) throw conflict("Package count must be positive");
if (requestedCount != 1) throw conflict("Overseas warehouse carton scanning accepts exactly one carton per scan");
int alreadyScanned = (int) repository.scannedCartons(plan.getId(), line.getId());
if (alreadyScanned + requestedCount > value(line.getRequiredPackageCount())) {
throw conflict("This inbound plan line has reached its required carton count");
}
List<InboundPlanCarton> cartonsToScan = new ArrayList<>();
cartonsToScan.add(carton);
if (requestedCount > 1) {
repository.cartons(plan.getId()).stream()
.filter(candidate -> !Objects.equals(candidate.getId(), carton.getId()))
.filter(candidate -> Objects.equals(candidate.getLineId(), line.getId()))
.filter(candidate -> "UNASSIGNED".equals(candidate.getStatus()) || "EXPECTED".equals(candidate.getStatus()))
.filter(candidate -> sameWarehouse(candidate, pallet))
.limit(requestedCount - 1L).forEach(cartonsToScan::add);
if (cartonsToScan.size() != requestedCount) throw conflict("Not enough unscanned cartons for the requested quantity");
}
List<InboundPlanCarton> cartonsToScan = List.of(carton);
InboundPlanPalletAllocation allocation = repository.lockAllocation(plan.getId(), line.getId(), pallet.getId());
if (allocation == null) {
allocation = new InboundPlanPalletAllocation(); allocation.setId(IdWorker.getId()); allocation.setPlanId(plan.getId());
@@ -323,6 +314,7 @@ public class InboundPdaWorkflowService {
@Transactional
public PdaResponses.Carton removeCarton(String customerId, String planNo, PdaRequests.RemoveCarton request) {
InboundPlan plan = requirePlan(customerId, planNo);
requireCartonScanMode(plan);
InboundPdaOperation replay = replay(customerId, "REMOVE_CARTON", request.idempotencyKey(), request);
if (replay != null) return read(replay, PdaResponses.Carton.class);
InboundPdaTask task = requireActiveTask(customerId, plan);
@@ -460,7 +452,7 @@ public class InboundPdaWorkflowService {
}
}
List<InboundPlanCarton> cartons = repository.cartons(plan.getId());
if (!isDropship(plan) && !cartons.isEmpty()
if (mode(plan) == InboundMode.OWN_OVERSEAS && !cartons.isEmpty()
&& cartons.stream().anyMatch(carton -> !"ON_PALLET".equals(carton.getStatus()))) {
throw conflict("All expected carton marks must be scanned onto a pallet before stock-in");
}
@@ -676,6 +668,17 @@ public class InboundPdaWorkflowService {
repository.update(plan);
}
private void requireCountMode(InboundPlan plan) {
InboundMode mode = mode(plan);
if (mode == InboundMode.DROPSHIP) {
throw conflict("One-click dropship inbound must be picked by complete source pallet");
}
if (mode == InboundMode.OWN_OVERSEAS) {
throw conflict("Overseas warehouse inbound must be operated by one-carton scanning");
}
}
/** Backward-compatible guard for pallet merge, which remains a legacy quantity workflow. */
private void requireLegacyCounting(InboundPlan plan) {
if (isDropship(plan)) {
throw conflict("One-click dropship inbound must be picked by complete source pallet");
@@ -685,6 +688,14 @@ public class InboundPdaWorkflowService {
}
}
private void requireCartonScanMode(InboundPlan plan) {
if (mode(plan) != InboundMode.OWN_OVERSEAS) {
throw conflict(mode(plan) == InboundMode.FBA
? "FBA inbound must be operated by package quantity, not carton scanning"
: "One-click dropship inbound must be operated by complete source pallet");
}
}
private boolean isNotAssigned(InboundPlanPallet pallet) {
return pallet.getWarehousePalletId() == null || pallet.getWarehouseLocationId() == null
|| pallet.getWarehouseResourceLockId() == null
@@ -768,7 +779,7 @@ public class InboundPdaWorkflowService {
return new PdaResponses.Task(task.getId(), task.getTaskNo(), planNo, task.getStatus(),
task.getPushedAt(), task.getStartedAt(), task.getCompletedAt(), unloadPhotoFileIds(task),
task.getUnloadPhotoUploadedAt(), task.getEstimatedEmptyReturnAt(), expected, picked,
orderType, wholePalletInbound, dropshipPallets);
orderType, wholePalletInbound, dropshipPallets, mode(plan).name());
}
private PdaResponses.Task task(InboundPdaTask task, String planNo) {
@@ -801,6 +812,15 @@ public class InboundPdaWorkflowService {
return order != null && "ONE_CLICK_DROPSHIP".equals(order.getOrderType());
}
private InboundMode mode(InboundPlan plan) {
if (isDropship(plan)) return InboundMode.DROPSHIP;
List<InboundPlanLine> lines = repository.lines(plan.getId());
return lines.stream().anyMatch(line -> line.getFbaWarehouseCode() != null
&& !line.getFbaWarehouseCode().isBlank()) ? InboundMode.FBA : InboundMode.OWN_OVERSEAS;
}
private enum InboundMode { DROPSHIP, OWN_OVERSEAS, FBA }
private List<Long> unloadPhotoFileIds(InboundPdaTask task) {
if (task.getUnloadPhotoFileIds() == null || task.getUnloadPhotoFileIds().isBlank()) return List.of();
try {
@@ -167,27 +167,17 @@ class InboundPdaWorkflowServiceTest {
}
@Test
void cartonScanCanAutoSelectDestinationPalletAndBindAQuantity() {
void cartonScanRejectsBatchQuantityAndRequiresOneRealCartonPerScan() {
InboundPlanCarton first = carton("BOX-BATCH-1", "UNASSIGNED", targetLine.getId(), "WH-A");
InboundPlanCarton second = carton("BOX-BATCH-2", "UNASSIGNED", targetLine.getId(), "WH-A");
second.setId(42L);
activeTask();
when(repository.lockCarton("100", "BOX-BATCH-1")).thenReturn(first);
when(repository.pallets(plan.getId())).thenReturn(List.of(targetPallet));
when(repository.lockPallet(plan.getId(), targetPallet.getId())).thenReturn(targetPallet);
when(repository.line(plan.getId(), targetLine.getId())).thenReturn(targetLine);
when(repository.cartons(plan.getId())).thenReturn(List.of(first, second));
when(repository.lockAllocation(plan.getId(), targetLine.getId(), targetPallet.getId())).thenReturn(target);
var result = service.scanCarton("100", "LIP1",
new PdaRequests.ScanCarton("BOX-BATCH-1", null, 2, "scan-batch", "7", "worker"));
assertEquals(targetPallet.getId(), result.planPalletId());
assertEquals("PLT-21", result.palletNo());
assertEquals(2, result.scannedPackageCount());
assertEquals("ON_PALLET", first.getStatus());
assertEquals("ON_PALLET", second.getStatus());
assertEquals(2, target.getPickedPackageCount());
assertThrows(ResponseStatusException.class, () -> service.scanCarton("100", "LIP1",
new PdaRequests.ScanCarton("BOX-BATCH-1", null, 2, "scan-batch", "7", "worker")));
assertEquals("UNASSIGNED", first.getStatus());
assertEquals("UNASSIGNED", second.getStatus());
}
@Test
@@ -226,7 +216,9 @@ class InboundPdaWorkflowServiceTest {
@Test
void countMustPrecedePickAndPickedBoxesDriveActualVolume() {
targetLine.setFbaWarehouseCode("ONT8");
activeTask();
when(repository.lines(plan.getId())).thenReturn(List.of(targetLine));
target.setCountedPackageCount(0);
when(repository.lockAllocation(plan.getId(), target.getId())).thenReturn(target);
when(repository.line(plan.getId(), targetLine.getId())).thenReturn(targetLine);
@@ -249,7 +241,9 @@ class InboundPdaWorkflowServiceTest {
@Test
void overPickReturnsConfirmationWithoutChangingAllocationOrPallet() {
targetLine.setFbaWarehouseCode("ONT8");
activeTask();
when(repository.lines(plan.getId())).thenReturn(List.of(targetLine));
when(repository.lockAllocation(plan.getId(), target.getId())).thenReturn(target);
when(repository.line(plan.getId(), targetLine.getId())).thenReturn(targetLine);
candidateQueries();
@@ -546,6 +540,8 @@ class InboundPdaWorkflowServiceTest {
@Test
void rejectsReuseOfIdempotencyKeyWithDifferentActionOrPayload() {
targetLine.setFbaWarehouseCode("ONT8");
when(repository.lines(plan.getId())).thenReturn(List.of(targetLine));
InboundPdaOperation existing = new InboundPdaOperation();
existing.setAction("COUNT");
existing.setRequestHash("different");
@@ -557,7 +553,9 @@ class InboundPdaWorkflowServiceTest {
@Test
void completedTaskStillReplaysAnAlreadyPersistedPick() {
targetLine.setFbaWarehouseCode("ONT8");
activeTask();
when(repository.lines(plan.getId())).thenReturn(List.of(targetLine));
when(repository.lockAllocation(plan.getId(), target.getId())).thenReturn(target);
when(repository.line(plan.getId(), targetLine.getId())).thenReturn(targetLine);
when(repository.lockPallet(plan.getId(), targetPallet.getId())).thenReturn(targetPallet);