Test #1
+2
-2
@@ -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,
|
||||
|
||||
+36
-16
@@ -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 {
|
||||
|
||||
+13
-15
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user