fix(lastleg): expose and validate individual return carton picks

This commit is contained in:
2026-10-02 16:59:16 +08:00
parent 195446e532
commit e15bec3052
3 changed files with 224 additions and 26 deletions
@@ -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 }
@@ -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));
}
}