fix(lastleg): expose and validate individual return carton picks
This commit is contained in:
+48
-16
@@ -420,9 +420,11 @@ public class DeliveryTransferService {
|
||||
outbound.setRecipientInfo(request.recipient().trim()); outbound.setPackageCount(marks.size()); outbound.setPalletCount(0);
|
||||
outbound.setOverseasWarehouseId(warehouseId); outbound.setIdempotencyKey(request.idempotencyKey().trim());
|
||||
outbound.setCreatedBy(operatorId); outbound.setCreatedAt(now); outbound.setUpdatedAt(now); outboundOrders.insert(outbound);
|
||||
OutboundPalletItem item = new OutboundPalletItem(); item.setId(IdWorker.getId()); item.setCustomerId(customerId);
|
||||
item.setOutboundOrderId(outbound.getId()); item.setPackageCount(marks.size()); item.setPickedPackageCount(0); item.setStatus("WAIT_PICK");
|
||||
item.setCartonMarks(String.join(",", marks)); item.setInboundCode(request.returnNo().trim()); item.setCreatedAt(now); outboundPallets.insert(item);
|
||||
for (ReturnInventoryCarton carton : locked) {
|
||||
OutboundPalletItem item = new OutboundPalletItem(); item.setId(IdWorker.getId()); item.setCustomerId(customerId);
|
||||
item.setOutboundOrderId(outbound.getId()); item.setPackageCount(1); item.setPickedPackageCount(0); item.setStatus("WAIT_PICK");
|
||||
item.setCartonMarks(carton.getCartonMark()); item.setInboundCode(carton.getSourceReturnNo()); item.setCreatedAt(now); outboundPallets.insert(item);
|
||||
}
|
||||
OutboundPdaTask task = new OutboundPdaTask(); task.setId(IdWorker.getId()); task.setCustomerId(customerId); task.setOutboundOrderId(outbound.getId());
|
||||
task.setTaskNo("PDA" + now.format(DateTimeFormatter.BASIC_ISO_DATE) + task.getId()); task.setStatus("PENDING"); task.setPushedAt(now); task.setCreatedAt(now); task.setUpdatedAt(now); pdaTasks.insert(task);
|
||||
return taskResult(outbound, task);
|
||||
@@ -646,7 +648,12 @@ public class DeliveryTransferService {
|
||||
|| !request.packages().equals(replay.getPackages())) {
|
||||
throw conflict("Idempotency key was reused with a different outbound pick request");
|
||||
}
|
||||
return taskResult(requireOutbound(task), task);
|
||||
OutboundOrder replayOutbound = requireOutbound(task);
|
||||
if ("RETURN_OUTBOUND".equals(replayOutbound.getOutboundType())) {
|
||||
OutboundPalletItem replayItem = outboundPallets.selectById(replay.getOutboundPalletItemId());
|
||||
requireReturnScan(replayOutbound, replayItem, request);
|
||||
}
|
||||
return taskResult(replayOutbound, task);
|
||||
}
|
||||
OutboundOrder existingOutbound = requireOutbound(task);
|
||||
OutboundOrder outbound = outboundOrders.lockByNo(customerId, existingOutbound.getOutboundNo());
|
||||
@@ -660,17 +667,13 @@ public class DeliveryTransferService {
|
||||
|| !task.getOutboundOrderId().equals(item.getOutboundOrderId())) throw notFound("Outbound pallet item not found");
|
||||
if (request.packages() > item.getPackageCount() - value(item.getPickedPackageCount())) throw conflict("Picked quantity exceeds outbound pallet quantity");
|
||||
if ("RETURN_OUTBOUND".equals(outbound.getOutboundType())) {
|
||||
if (request.inventoryPositionId() != null) throw conflict("退货出库无需扫描库存仓位");
|
||||
List<String> marks = Arrays.stream(Optional.ofNullable(item.getCartonMarks()).orElse("").split(","))
|
||||
.map(String::trim).filter(StringUtils::hasText).toList();
|
||||
if (request.packages() != marks.size() || value(item.getPickedPackageCount()) > 0) throw conflict("退货出库必须一次扫描本次箱唛且不可重复");
|
||||
for (String mark : marks) {
|
||||
ReturnInventoryCarton carton = returnCartons.lockMark(mark);
|
||||
if (carton == null || !customerId.equals(carton.getCustomerId()) || !warehouseId.equals(carton.getOverseasWarehouseId()) || !"AVAILABLE".equals(carton.getStatus())) {
|
||||
throw conflict("退货箱唛不可出库:" + mark);
|
||||
}
|
||||
carton.setStatus("OUTBOUNDED"); carton.setUpdatedAt(LocalDateTime.now()); returnCartons.updateById(carton);
|
||||
}
|
||||
requireReturnScan(outbound, item, request);
|
||||
if (value(item.getPickedPackageCount()) > 0) throw conflict("退货箱唛已完成拣货");
|
||||
ReturnInventoryCarton carton = returnCartons.lockMark(item.getCartonMarks());
|
||||
requireReturnSource(outbound, item, carton);
|
||||
if (!"AVAILABLE".equals(carton.getStatus())) throw conflict("退货箱唛不可出库:" + item.getCartonMarks());
|
||||
carton.setStatus("OUTBOUNDED"); carton.setUpdatedAt(LocalDateTime.now());
|
||||
if (returnCartons.updateById(carton) != 1) throw conflict("退货箱唛状态已变化,请重新核对");
|
||||
OutboundPdaPick pick = new OutboundPdaPick(); pick.setId(IdWorker.getId()); pick.setCustomerId(customerId); pick.setTaskId(task.getId()); pick.setOutboundPalletItemId(item.getId()); pick.setInventoryPositionId(null); pick.setPackages(request.packages()); pick.setIdempotencyKey(request.idempotencyKey().trim()); pick.setPickedAt(LocalDateTime.now()); pdaPicks.insert(pick);
|
||||
item.setPickedPackageCount(request.packages().intValue()); item.setStatus("PICKED"); outboundPallets.updateById(item);
|
||||
updateOutboundPickingStatus(customerId, outbound);
|
||||
@@ -1037,7 +1040,8 @@ public class DeliveryTransferService {
|
||||
task.getOutboundOrderId()).stream().map(item -> new DeliveryResponses.PdaTaskItem(item.getId(),
|
||||
item.getPalletNo(), item.getSkuCode(), item.getInboundCode(), value(item.getPackageCount()),
|
||||
value(item.getPickedPackageCount()), item.getStatus(),
|
||||
pdaPickPositions(task.getCustomerId(), boundWarehouseId, item))).toList();
|
||||
"RETURN_OUTBOUND".equals(outbound.getOutboundType()) ? returnPickPositions(outbound, task, item)
|
||||
: pdaPickPositions(task.getCustomerId(), boundWarehouseId, item))).toList();
|
||||
return new DeliveryResponses.PdaTaskDetail(taskResult(outbound, task), items);
|
||||
}
|
||||
|
||||
@@ -1071,6 +1075,34 @@ public class DeliveryTransferService {
|
||||
throw notFound("Outbound or dispatch task not found in the bound warehouse");
|
||||
}
|
||||
|
||||
private void requireReturnScan(OutboundOrder outbound, OutboundPalletItem item, DeliveryRequests.PdaPick request) {
|
||||
if (item == null || !outbound.getId().equals(item.getOutboundOrderId())
|
||||
|| !outbound.getCustomerId().equals(item.getCustomerId())) throw notFound("退货出库明细不存在");
|
||||
if (request.inventoryPositionId() != null || !Long.valueOf(1).equals(request.packages()) || value(item.getPackageCount()) != 1)
|
||||
throw conflict("退货出库须逐箱扫描,无需库存仓位");
|
||||
if (request.cartonMarks() == null || request.cartonMarks().size() != 1 || request.cartonMarks().get(0) == null
|
||||
|| !Objects.equals(item.getCartonMarks(), request.cartonMarks().get(0).trim()))
|
||||
throw conflict("请扫描当前退货出库明细指定的箱唛");
|
||||
}
|
||||
|
||||
private void requireReturnSource(OutboundOrder outbound, OutboundPalletItem item, ReturnInventoryCarton carton) {
|
||||
if (carton == null || !outbound.getCustomerId().equals(carton.getCustomerId())
|
||||
|| !outbound.getOverseasWarehouseId().equals(carton.getOverseasWarehouseId())
|
||||
|| !Objects.equals(item.getInboundCode(), carton.getSourceReturnNo())
|
||||
|| !Objects.equals(item.getCartonMarks(), carton.getCartonMark()))
|
||||
throw conflict("退货箱唛与本次出库的客户、仓库或来源退货单不一致");
|
||||
}
|
||||
|
||||
private List<DeliveryResponses.PdaPickPosition> returnPickPositions(OutboundOrder outbound, OutboundPdaTask task,
|
||||
OutboundPalletItem item) {
|
||||
ReturnInventoryCarton carton = returnCartons.findMark(item.getCartonMarks());
|
||||
requireReturnSource(outbound, item, carton);
|
||||
boolean pending = Set.of("PENDING", "IN_PROGRESS").contains(task.getStatus()) && value(item.getPickedPackageCount()) == 0;
|
||||
long available = pending && "AVAILABLE".equals(carton.getStatus()) ? 1 : 0;
|
||||
return List.of(new DeliveryResponses.PdaPickPosition(null, null, item.getCartonMarks(), item.getSkuCode(),
|
||||
item.getInboundCode(), available, value(item.getPickedPackageCount())));
|
||||
}
|
||||
|
||||
private List<DeliveryResponses.PdaPickPosition> pdaPickPositions(String customerId, String warehouseId,
|
||||
OutboundPalletItem item) {
|
||||
if (item.getWarehousePalletId() == null) return List.of();
|
||||
|
||||
@@ -865,16 +865,16 @@ components:
|
||||
remark: { type: string, maxLength: 1000, nullable: true }
|
||||
OutboundPick:
|
||||
type: object
|
||||
required: [outboundPalletItemId, inventoryPositionId, packages, idempotencyKey]
|
||||
required: [outboundPalletItemId, packages, idempotencyKey]
|
||||
properties:
|
||||
outboundPalletItemId: { type: string, pattern: '^[0-9]+$' }
|
||||
inventoryPositionId: { type: string, pattern: '^[0-9]+$' }
|
||||
inventoryPositionId: { type: string, nullable: true, pattern: '^[0-9]+$', description: 普通库存使用真实仓位键;退货箱库存没有该仓位,提交 null,不提交空字符串或退货库存内部ID }
|
||||
packages: { type: integer, format: int64, minimum: 1 }
|
||||
idempotencyKey: { type: string, minLength: 1, maxLength: 255 }
|
||||
cartonMarks:
|
||||
type: array
|
||||
maxItems: 500
|
||||
description: 本次实际扫描的箱唛。一件代发指定箱唛任务分批拣货时必须提交本次子集,数量与 packages 一致;换标任务仍须提交全部指定箱唛。
|
||||
description: 本次实际扫描的箱唛。退货出库每条明细对应一个真实退货箱,必须提交该箱的 cartonMark,packages 为 1;相同幂等键改传其他箱唛同样拒绝,执行时重新核对客户、仓库及来源退货单。一件代发指定箱唛任务分批拣货时必须提交本次子集,数量与 packages 一致;换标任务仍须提交全部指定箱唛。
|
||||
items: { type: string, minLength: 1, maxLength: 255 }
|
||||
PdaOutboundTaskResponse:
|
||||
type: object
|
||||
@@ -901,25 +901,26 @@ components:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
required: [outboundPalletItemId, palletNo, skuCode, inboundCode, packageCount, pickedPackageCount, status, positions]
|
||||
required: [outboundPalletItemId, inboundCode, packageCount, pickedPackageCount, status, positions]
|
||||
properties:
|
||||
outboundPalletItemId: { type: string, description: pick请求提交键,不作为用户展示编号 }
|
||||
palletNo: { type: string, description: Warehouse 权威实物托盘码 }
|
||||
skuCode: { type: string }
|
||||
palletNo: { type: string, nullable: true, description: Warehouse 权威实物托盘码;退货箱明细不伪造托盘码 }
|
||||
skuCode: { type: string, nullable: true }
|
||||
inboundCode: { type: string }
|
||||
packageCount: { type: integer }
|
||||
pickedPackageCount: { type: integer }
|
||||
status: { type: string }
|
||||
positions:
|
||||
type: array
|
||||
description: 普通出库为真实库存仓位;退货出库为已冻结真实箱的扫码行,inventoryPositionId 与 palletNo 为空,cartonMark 为扫码业务码、inboundCode 为退货单号,按箱计数。已拣或已完成时 availablePackages 为 0,详情查询不写库存。
|
||||
items:
|
||||
type: object
|
||||
required: [inventoryPositionId, palletNo, cartonMark, skuCode, inboundCode, availablePackages, pickedPackages]
|
||||
required: [inboundCode, availablePackages, pickedPackages]
|
||||
properties:
|
||||
inventoryPositionId: { type: string, description: pick请求提交键,不作为用户展示编号 }
|
||||
palletNo: { type: string, description: Warehouse 权威实物托盘码 }
|
||||
inventoryPositionId: { type: string, nullable: true, description: 普通库存的pick提交键;退货箱无仓位键,不得以其内部ID替代 }
|
||||
palletNo: { type: string, nullable: true, description: Warehouse 权威实物托盘码;退货箱不提供伪造托盘号 }
|
||||
cartonMark: { type: string, nullable: true }
|
||||
skuCode: { type: string }
|
||||
skuCode: { type: string, nullable: true }
|
||||
inboundCode: { type: string }
|
||||
availablePackages: { type: integer, format: int64, minimum: 0 }
|
||||
pickedPackages: { type: integer, format: int64, minimum: 0 }
|
||||
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
package com.baiyue.lastleg.delivery.service;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
|
||||
import com.baiyue.lastleg.delivery.dto.DeliveryRequests;
|
||||
import com.baiyue.lastleg.delivery.entity.*;
|
||||
import com.baiyue.lastleg.delivery.mapper.*;
|
||||
import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCarton;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper;
|
||||
import com.baiyue.lastleg.service.LastlegOrderNoGenerator;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mybatis.spring.SqlSessionTemplate;
|
||||
import org.springframework.aop.framework.ProxyFactory;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
|
||||
import org.springframework.jdbc.datasource.DriverManagerDataSource;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource;
|
||||
import org.springframework.transaction.interceptor.TransactionInterceptor;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class ReturnOutboundPdaPersistenceTest {
|
||||
private JdbcTemplate jdbc;
|
||||
private DeliveryTransferService service;
|
||||
|
||||
@BeforeEach void setUp() throws Exception {
|
||||
var ds = new DriverManagerDataSource("jdbc:h2:mem:return-pda-" + UUID.randomUUID() + ";MODE=MySQL;DB_CLOSE_DELAY=-1", "sa", "");
|
||||
jdbc = new JdbcTemplate(ds);
|
||||
// Persist production entities through production MyBatis mappers, with real transactions.
|
||||
for (Class<?> entity : List.of(OutboundOrder.class, OutboundPalletItem.class, OutboundPdaTask.class,
|
||||
OutboundPdaPick.class, ReturnInventoryCarton.class)) {
|
||||
String columns = Arrays.stream(entity.getDeclaredFields()).filter(field -> !Modifier.isStatic(field.getModifiers()))
|
||||
.map(field -> field.getName().replaceAll("([A-Z])", "_$1").toLowerCase() + " "
|
||||
+ (field.getType() == String.class ? "VARCHAR(2000)" : field.getType() == LocalDateTime.class ? "TIMESTAMP"
|
||||
: field.getType() == java.math.BigDecimal.class ? "DECIMAL(20,6)" : "BIGINT")
|
||||
+ (field.getName().equals("id") ? " PRIMARY KEY" : "")).collect(Collectors.joining(","));
|
||||
jdbc.execute("CREATE TABLE " + entity.getAnnotation(TableName.class).value() + " (" + columns + ")");
|
||||
}
|
||||
var configuration = new MybatisConfiguration(); configuration.setMapUnderscoreToCamelCase(true);
|
||||
for (Class<?> mapper : List.of(OutboundOrderMapper.class, OutboundPalletItemMapper.class,
|
||||
OutboundPdaTaskMapper.class, OutboundPdaPickMapper.class, ReturnInventoryCartonMapper.class)) configuration.addMapper(mapper);
|
||||
var factory = new MybatisSqlSessionFactoryBean(); factory.setDataSource(ds); factory.setConfiguration(configuration);
|
||||
var session = new SqlSessionTemplate(factory.getObject());
|
||||
var target = new DeliveryTransferService();
|
||||
ReflectionTestUtils.setField(target, "outboundOrders", session.getMapper(OutboundOrderMapper.class));
|
||||
ReflectionTestUtils.setField(target, "outboundPallets", session.getMapper(OutboundPalletItemMapper.class));
|
||||
ReflectionTestUtils.setField(target, "pdaTasks", session.getMapper(OutboundPdaTaskMapper.class));
|
||||
ReflectionTestUtils.setField(target, "pdaPicks", session.getMapper(OutboundPdaPickMapper.class));
|
||||
ReflectionTestUtils.setField(target, "returnCartons", session.getMapper(ReturnInventoryCartonMapper.class));
|
||||
var numbers = mock(LastlegOrderNoGenerator.class);
|
||||
when(numbers.next(eq("RETURN_OUTBOUND"), eq("WH-1"), anyLong())).thenAnswer(call -> "RETU-" + call.getArgument(2));
|
||||
ReflectionTestUtils.setField(target, "orderNoGenerator", numbers);
|
||||
var proxy = new ProxyFactory(target); proxy.setProxyTargetClass(true);
|
||||
proxy.addAdvice(new TransactionInterceptor(new DataSourceTransactionManager(ds), new AnnotationTransactionAttributeSource()));
|
||||
service = (DeliveryTransferService) proxy.getProxy();
|
||||
carton(1, "C1", "WH-1", "RT-1", "BOX-A"); carton(2, "C1", "WH-1", "RT-1", "BOX-B");
|
||||
carton(3, "C2", "WH-1", "RT-2", "OTHER-CUSTOMER"); carton(4, "C1", "WH-2", "RT-1", "OTHER-WAREHOUSE");
|
||||
}
|
||||
private void carton(long id, String customer, String warehouse, String returnNo, String mark) {
|
||||
jdbc.update("INSERT INTO ll_return_inventory_carton(id,customer_id,overseas_warehouse_id,source_return_no,carton_mark,status,total_quantity) VALUES(?,?,?,?,?,'AVAILABLE',5)", id, customer, warehouse, returnNo, mark);
|
||||
}
|
||||
private com.baiyue.lastleg.delivery.dto.DeliveryResponses.PdaTask create(String key, String... marks) {
|
||||
return service.createReturnOutbound("C1", "operator", new DeliveryRequests.ReturnOutbound("recipient", "WH-1", "RT-1", List.of(marks), "fixture", key));
|
||||
}
|
||||
@Test void singleExistingShapeHasRealCartonScanRowWithoutInventedPalletOrPosition() {
|
||||
var task = create("single", "BOX-A");
|
||||
var detail = service.pdaTaskDetail("WH-1", task.taskNo());
|
||||
assertEquals(1, detail.items().size());
|
||||
assertEquals(1, detail.items().get(0).positions().size());
|
||||
var row = detail.items().get(0).positions().get(0);
|
||||
assertNull(row.inventoryPositionId()); assertNull(row.palletNo());
|
||||
assertEquals("BOX-A", row.cartonMark()); assertEquals("RT-1", row.inboundCode());
|
||||
assertEquals(1, row.availablePackages()); assertEquals(0, row.pickedPackages());
|
||||
assertEquals(0, jdbc.queryForObject("SELECT COUNT(*) FROM ll_outbound_pda_pick", Integer.class));
|
||||
}
|
||||
@Test void multiplePhysicalCartonsPersistSeparateIndependentlyScannableItems() {
|
||||
var task = create("multi", "BOX-A", "BOX-B");
|
||||
var detail = service.pdaTaskDetail("WH-1", task.taskNo());
|
||||
assertEquals(2, detail.items().size());
|
||||
assertTrue(detail.items().stream().allMatch(item -> item.packageCount() == 1 && item.positions().size() == 1));
|
||||
var a = detail.items().stream().filter(item -> item.positions().get(0).cartonMark().equals("BOX-A")).findFirst().orElseThrow();
|
||||
service.pdaPick("WH-1", "operator", task.taskNo(), new DeliveryRequests.PdaPick(a.outboundPalletItemId(), null, 1L, "pick-a", List.of("BOX-A")));
|
||||
var after = service.pdaTaskDetail("WH-1", task.taskNo());
|
||||
assertEquals(1, after.items().stream().mapToInt(item -> item.pickedPackageCount()).sum());
|
||||
assertEquals(1, after.items().stream().flatMap(item -> item.positions().stream()).mapToLong(row -> row.availablePackages()).sum());
|
||||
}
|
||||
@Test void wrongScanAndWrongReplayPayloadLeaveStoredInventoryUnchanged() {
|
||||
var task = create("scan", "BOX-A"); var item = service.pdaTaskDetail("WH-1", task.taskNo()).items().get(0);
|
||||
assertThrows(ResponseStatusException.class, () -> service.pdaPick("WH-1", "operator", task.taskNo(),
|
||||
new DeliveryRequests.PdaPick(item.outboundPalletItemId(), null, 1L, "bad", List.of("BOX-B"))));
|
||||
assertEquals("AVAILABLE", jdbc.queryForObject("SELECT status FROM ll_return_inventory_carton WHERE id=1", String.class));
|
||||
var valid = new DeliveryRequests.PdaPick(item.outboundPalletItemId(), null, 1L, "good", List.of("BOX-A"));
|
||||
service.pdaPick("WH-1", "operator", task.taskNo(), valid);
|
||||
service.pdaPick("WH-1", "operator", task.taskNo(), valid);
|
||||
assertThrows(ResponseStatusException.class, () -> service.pdaPick("WH-1", "operator", task.taskNo(),
|
||||
new DeliveryRequests.PdaPick(item.outboundPalletItemId(), null, 1L, "good", List.of("BOX-B"))));
|
||||
assertEquals(1, jdbc.queryForObject("SELECT COUNT(*) FROM ll_outbound_pda_pick", Integer.class));
|
||||
}
|
||||
@Test void customerWarehouseAndSourceReturnMismatchNeverAppearAsPickableRows() {
|
||||
var task = create("scope", "BOX-A");
|
||||
assertThrows(ResponseStatusException.class, () -> service.pdaTaskDetail("WH-2", task.taskNo()));
|
||||
for (String mark : List.of("OTHER-CUSTOMER", "OTHER-WAREHOUSE")) {
|
||||
assertThrows(ResponseStatusException.class, () -> create("bad-" + mark, mark));
|
||||
}
|
||||
for (String column : List.of("customer_id", "overseas_warehouse_id", "source_return_no")) {
|
||||
String original = jdbc.queryForObject("SELECT " + column + " FROM ll_return_inventory_carton WHERE id=1", String.class);
|
||||
jdbc.update("UPDATE ll_return_inventory_carton SET " + column + "='FOREIGN' WHERE id=1");
|
||||
assertThrows(ResponseStatusException.class, () -> service.pdaTaskDetail("WH-1", task.taskNo()));
|
||||
jdbc.update("UPDATE ll_return_inventory_carton SET " + column + "=? WHERE id=1", original);
|
||||
}
|
||||
}
|
||||
@Test void completedTaskDetailIsReadOnlyAndContainsNoPickableStock() {
|
||||
var task = create("complete", "BOX-A"); var item = service.pdaTaskDetail("WH-1", task.taskNo()).items().get(0);
|
||||
var pick = new DeliveryRequests.PdaPick(item.outboundPalletItemId(), null, 1L, "pick", List.of("BOX-A"));
|
||||
service.pdaPick("WH-1", "operator", task.taskNo(), pick);
|
||||
service.completePda("WH-1", task.taskNo(), new DeliveryRequests.PdaComplete("complete-key"));
|
||||
for (int i = 0; i < 2; i++) {
|
||||
var detail = service.pdaTaskDetail("WH-1", task.taskNo());
|
||||
assertEquals("COMPLETED", detail.task().status());
|
||||
assertTrue(detail.items().stream().flatMap(row -> row.positions().stream()).allMatch(row -> row.availablePackages() == 0));
|
||||
}
|
||||
assertThrows(ResponseStatusException.class, () -> service.pdaPick("WH-1", "operator", task.taskNo(),
|
||||
new DeliveryRequests.PdaPick(item.outboundPalletItemId(), null, 1L, "new-key", List.of("BOX-A"))));
|
||||
assertEquals(1, jdbc.queryForObject("SELECT COUNT(*) FROM ll_outbound_pda_pick", Integer.class));
|
||||
}
|
||||
|
||||
@Test void sourceChangedAfterDetailReadCannotBeShippedByTheOldRequest() {
|
||||
var task = create("source-change", "BOX-A");
|
||||
var item = service.pdaTaskDetail("WH-1", task.taskNo()).items().get(0);
|
||||
jdbc.update("UPDATE ll_return_inventory_carton SET source_return_no='RT-OTHER' WHERE id=1");
|
||||
assertThrows(ResponseStatusException.class, () -> service.pdaPick("WH-1", "operator", task.taskNo(),
|
||||
new DeliveryRequests.PdaPick(item.outboundPalletItemId(), null, 1L, "stale-source", List.of("BOX-A"))));
|
||||
assertEquals("AVAILABLE", jdbc.queryForObject("SELECT status FROM ll_return_inventory_carton WHERE id=1", String.class));
|
||||
assertEquals(0, jdbc.queryForObject("SELECT COUNT(*) FROM ll_outbound_pda_pick", Integer.class));
|
||||
assertEquals(0, jdbc.queryForObject("SELECT picked_package_count FROM ll_outbound_pallet_item", Integer.class));
|
||||
}
|
||||
|
||||
@Test void failedPickJournalRollsBackCartonAndTaskThenAllowsAnExactRetry() {
|
||||
var task = create("rollback", "BOX-A");
|
||||
var item = service.pdaTaskDetail("WH-1", task.taskNo()).items().get(0);
|
||||
var request = new DeliveryRequests.PdaPick(item.outboundPalletItemId(), null, 1L, "rollback-pick", List.of("BOX-A"));
|
||||
jdbc.execute("ALTER TABLE ll_outbound_pda_pick ADD CONSTRAINT fail_pick CHECK(idempotency_key <> 'rollback-pick')");
|
||||
assertThrows(RuntimeException.class, () -> service.pdaPick("WH-1", "operator", task.taskNo(), request));
|
||||
assertEquals("AVAILABLE", jdbc.queryForObject("SELECT status FROM ll_return_inventory_carton WHERE id=1", String.class));
|
||||
assertEquals(0, jdbc.queryForObject("SELECT COUNT(*) FROM ll_outbound_pda_pick", Integer.class));
|
||||
var unchanged = service.pdaTaskDetail("WH-1", task.taskNo());
|
||||
assertEquals("PENDING", unchanged.task().status());
|
||||
assertEquals(1, unchanged.items().get(0).positions().get(0).availablePackages());
|
||||
jdbc.execute("ALTER TABLE ll_outbound_pda_pick DROP CONSTRAINT fail_pick");
|
||||
service.pdaPick("WH-1", "operator", task.taskNo(), request);
|
||||
assertEquals("OUTBOUNDED", jdbc.queryForObject("SELECT status FROM ll_return_inventory_carton WHERE id=1", String.class));
|
||||
assertEquals(1, jdbc.queryForObject("SELECT COUNT(*) FROM ll_outbound_pda_pick", Integer.class));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user