fix(dropship): preserve carton quantities and shared pallet outbound ownership

This commit is contained in:
2026-09-14 20:55:09 +08:00
parent 6dbcb2128c
commit 4f13ee2ec3
24 changed files with 302 additions and 99 deletions
@@ -27,7 +27,7 @@ class DeliverySchemaContractTest {
assertTrue(schema.contains("unique key uk_ll_outbound_idempotency (customer_id,idempotency_key)"));
assertTrue(schema.contains("unique key uk_ll_outbound_dispatch (customer_id,dispatch_order_id)"));
assertTrue(schema.contains("create table if not exists ll_outbound_pallet_item"));
assertTrue(schema.contains("unique key uk_ll_outbound_pallet (customer_id,outbound_order_id,warehouse_pallet_id)"));
assertTrue(schema.contains("unique key uk_ll_outbound_pallet (customer_id,outbound_order_id,warehouse_pallet_id,inbound_code,sku_code)"));
assertTrue(schema.contains("destination_warehouse_code varchar(128) null"));
assertTrue(schema.contains("order_refs varchar(2000) null"));
assertTrue(schema.contains("inbound_code varchar(128) null"));
@@ -279,6 +279,31 @@ class DeliveryTransferServiceTest {
verify(outboundOrders, never()).insert((OutboundOrder) any());
}
@Test
void dedicatedOutboundDetailDoesNotRequireADeliveryDocument() {
OutboundOrder outbound = outbound(301L, null, null, "OUT-301", "dropship-301");
outbound.setOutboundType("ONE_CLICK_DROPSHIP");
outbound.setStatus("OUTBOUNDED");
when(outboundOrders.findGlobalByNo("OUT-301")).thenReturn(outbound);
when(outboundPallets.findByOutbound("TENANT-A", 301L)).thenReturn(List.of(
pallet(501L, "DP-260914-0001", "ONT8", "Ontario", "IN-A", 4)));
DeliveryResponses.OutboundDetail detail = service.outboundDetail("OUT-301");
assertEquals("OUTBOUNDED", detail.outbound().status());
org.junit.jupiter.api.Assertions.assertNull(detail.delivery());
assertEquals("IN-A", detail.pallets().get(0).inboundCode());
assertEquals(4, detail.pallets().get(0).packageCount());
}
@Test
void dispatchOutboundStillRequiresItsDeliveryDocument() {
OutboundOrder outbound = outbound(301L, 101L, "DSP-101", "OUT-301", "dispatch-301");
outbound.setOutboundType("DISPATCH_DELIVERY");
when(outboundOrders.findGlobalByNo("OUT-301")).thenReturn(outbound);
assertThrows(IllegalStateException.class, () -> service.outboundDetail("OUT-301"));
}
@Test
void outboundDetailIncludesPalletCodesForDisplay() {
OutboundOrder outbound = outbound(301L, 101L, "DSP-101", "OUT-301", "transfer-101");
@@ -406,6 +431,33 @@ class DeliveryTransferServiceTest {
verify(outboundPallets, never()).findByOutbound(any(), any());
}
@Test
void sharedPalletOutboundFreezesEachInboundAndCountsOnePhysicalPallet() {
for (long i = 1; i <= 2; i++) {
when(dropshipProfiles.lockOutboundCandidate("TENANT-A", 900L + i, "SKU-1"))
.thenReturn(dropshipCandidate(900L + i, 1000L + i, "WH-1", "DP-SHARED", "SKU-1", 1L));
when(dropshipCartons.lockAvailable("TENANT-A", 1000L + i, "SKU-1", 1L))
.thenReturn(List.of(new DropshipCarton()));
InventoryPosition position = new InventoryPosition();
position.setId(900L + i); position.setCustomerId("TENANT-A"); position.setBalanceId(800L + i);
position.setPalletId(501L); position.setPalletNo("DP-SHARED");
InventoryBalance balance = new InventoryBalance();
balance.setCustomerId("TENANT-A"); balance.setInboundCode("IN-" + i);
when(inventoryPositions.selectById(900L + i)).thenReturn(position);
when(inventoryBalances.selectById(800L + i)).thenReturn(balance);
}
service.createOneClick("TENANT-A", "U-1", new DeliveryRequests.OneClickOutbound("recipient", List.of(
new DeliveryRequests.OneClickItem(901L, 1L, "SKU-1"),
new DeliveryRequests.OneClickItem(902L, 1L, "SKU-1")), null, "shared-pallet"));
ArgumentCaptor<OutboundPalletItem> items = ArgumentCaptor.forClass(OutboundPalletItem.class);
verify(outboundPallets, org.mockito.Mockito.times(2)).insert(items.capture());
assertEquals(List.of("IN-1", "IN-2"), items.getAllValues().stream().map(OutboundPalletItem::getInboundCode).toList());
ArgumentCaptor<OutboundOrder> order = ArgumentCaptor.forClass(OutboundOrder.class);
verify(outboundOrders).updateById(order.capture());
assertEquals(1, order.getValue().getPalletCount());
assertEquals(2, order.getValue().getPackageCount());
}
@Test
void oneClickOutboundRejectsSummarySkuWithoutAvailableDropshipCartons() {
DropshipOutboundCandidate candidate = dropshipCandidate(901L, 1001L, "WH-1", "PLT-501", "MIXED", 1L);
@@ -539,17 +591,26 @@ class DeliveryTransferServiceTest {
DropshipOrderProfile profile = new DropshipOrderProfile(); profile.setId(1001L);
DropshipCarton first = new DropshipCarton(); first.setStatus("AVAILABLE"); first.setSkuCode("SKU-1"); first.setCartonMark("CTN-A");
DropshipCarton second = new DropshipCarton(); second.setStatus("AVAILABLE"); second.setSkuCode("SKU-1"); second.setCartonMark("CTN-B");
InventoryPosition otherInbound = new InventoryPosition();
otherInbound.setId(902L); otherInbound.setCustomerId("TENANT-A"); otherInbound.setBalanceId(802L);
otherInbound.setOverseasWarehouseId("WH-1"); otherInbound.setPalletId(501L);
otherInbound.setAvailablePackages(2L);
InventoryBalance otherBalance = new InventoryBalance();
otherBalance.setId(802L); otherBalance.setCustomerId("TENANT-A"); otherBalance.setInboundCode("IN-B");
otherBalance.setSkuCode("SKU-1");
when(pdaTasks.findByNoAndWarehouse("PDA-701", "WH-1")).thenReturn(task);
when(outboundOrders.selectById(301L)).thenReturn(outbound);
when(outboundPallets.findByOutbound("TENANT-A", 301L)).thenReturn(List.of(item));
when(inventoryPositions.listPallet("TENANT-A", 501L)).thenReturn(List.of(position));
when(inventoryPositions.listPallet("TENANT-A", 501L)).thenReturn(List.of(position, otherInbound));
when(inventoryBalances.selectById(801L)).thenReturn(balance);
when(inventoryBalances.selectById(802L)).thenReturn(otherBalance);
when(dropshipProfiles.findPosition("TENANT-A", 901L)).thenReturn(profile);
when(dropshipCartons.listProfile("TENANT-A", 1001L)).thenReturn(List.of(first, second));
DeliveryResponses.PdaTaskDetail detail = service.pdaTaskDetail("WH-1", "PDA-701");
assertEquals(2, detail.items().get(0).positions().size());
verify(dropshipProfiles, never()).findPosition("TENANT-A", 902L);
assertEquals(List.of("CTN-A", "CTN-B"), detail.items().get(0).positions().stream()
.map(DeliveryResponses.PdaPickPosition::cartonMark).toList());
DeliveryResponses.PdaPickPosition projected = detail.items().get(0).positions().get(0);
@@ -588,6 +649,41 @@ class DeliveryTransferServiceTest {
assertEquals("CTN-A", detail.items().get(0).positions().get(0).cartonMark());
}
@Test
void dropshipPartialPicksDeductExactCartonProductQuantities() {
var commands = org.mockito.Mockito.mock(com.baiyue.lastleg.inventory.command.service.InventoryCommandService.class);
ReflectionTestUtils.setField(service, "inventoryCommands", commands);
OutboundOrder outbound = outbound(301L, null, null, "OUT-301", "quantity-test");
outbound.setOverseasWarehouseId("WH-1"); outbound.setOutboundType("ONE_CLICK_DROPSHIP");
OutboundPdaTask task = new OutboundPdaTask(); task.setId(701L); task.setCustomerId("TENANT-A");
task.setOutboundOrderId(301L); task.setTaskNo("PDA-701"); task.setStatus("PENDING");
OutboundPalletItem item = pallet(501L, "DP-1", "ONT8", "Ontario", "IN-A", 2);
item.setSkuCode("SKU-1");
InventoryPosition position = new InventoryPosition(); position.setId(901L); position.setCustomerId("TENANT-A");
position.setBalanceId(801L); position.setPalletId(501L); position.setOverseasWarehouseId("WH-1");
InventoryBalance balance = new InventoryBalance(); balance.setCustomerId("TENANT-A"); balance.setInboundCode("IN-A");
DropshipOrderProfile profile = new DropshipOrderProfile(); profile.setId(1001L);
DropshipCarton first = new DropshipCarton(); first.setId(1101L); first.setProductQuantity(7L);
DropshipCarton second = new DropshipCarton(); second.setId(1102L); second.setProductQuantity(13L);
when(pdaTasks.lockByNoAndWarehouse("PDA-701", "WH-1")).thenReturn(task);
when(outboundOrders.selectById(301L)).thenReturn(outbound);
when(outboundOrders.lockByNo("TENANT-A", "OUT-301")).thenReturn(outbound);
when(outboundPallets.selectById(item.getId())).thenReturn(item);
when(inventoryPositions.selectById(901L)).thenReturn(position);
when(inventoryBalances.selectById(801L)).thenReturn(balance);
when(dropshipProfiles.findPosition("TENANT-A", 901L)).thenReturn(profile);
when(dropshipCartons.lockAvailable("TENANT-A", 1001L, "SKU-1", 1L)).thenReturn(List.of(first), List.of(second));
when(dropshipSkuInventory.deduct("TENANT-A", 1001L, "SKU-1", 1L, 7L)).thenReturn(1);
when(dropshipSkuInventory.deduct("TENANT-A", 1001L, "SKU-1", 1L, 13L)).thenReturn(1);
when(outboundPallets.findByOutbound("TENANT-A", 301L)).thenReturn(List.of(item));
service.pdaPick("WH-1", "U-1", "PDA-701", new DeliveryRequests.PdaPick(item.getId(), 901L, 1L, "pick-7"));
service.pdaPick("WH-1", "U-1", "PDA-701", new DeliveryRequests.PdaPick(item.getId(), 901L, 1L, "pick-13"));
verify(dropshipSkuInventory).deduct("TENANT-A", 1001L, "SKU-1", 1L, 7L);
verify(dropshipSkuInventory).deduct("TENANT-A", 1001L, "SKU-1", 1L, 13L);
assertEquals("SHIPPED", first.getStatus()); assertEquals("SHIPPED", second.getStatus());
assertEquals(2, item.getPickedPackageCount());
}
@Test
void outboundPdaPickReplayDoesNotDeductInventoryAgainAndRejectsChangedRequest() {
OutboundOrder outbound = outbound(301L, null, null, "OUT-301", "one-click-key");