fix: preserve dropship pallets in container plans
This commit is contained in:
+4
-2
@@ -12,9 +12,11 @@ import java.util.List;
|
|||||||
|
|
||||||
@Mapper
|
@Mapper
|
||||||
public interface InboundPlanOrderMapper {
|
public interface InboundPlanOrderMapper {
|
||||||
record ContainerOrderRef(String customerId, Long orderId, String currentStatus, String containerNo) { }
|
record ContainerOrderRef(String customerId, Long orderId, String currentStatus, String containerNo,
|
||||||
|
String orderType) { }
|
||||||
|
|
||||||
@Select("SELECT customer_id AS customerId,id AS orderId,current_status AS currentStatus,container_no AS containerNo " +
|
@Select("SELECT customer_id AS customerId,id AS orderId,current_status AS currentStatus,container_no AS containerNo," +
|
||||||
|
"order_type AS orderType " +
|
||||||
"FROM ll_order WHERE source_container_booking_id=#{sourceContainerBookingId} ORDER BY customer_id,id FOR UPDATE")
|
"FROM ll_order WHERE source_container_booking_id=#{sourceContainerBookingId} ORDER BY customer_id,id FOR UPDATE")
|
||||||
List<ContainerOrderRef> lockContainerOrders(@Param("sourceContainerBookingId") String sourceContainerBookingId);
|
List<ContainerOrderRef> lockContainerOrders(@Param("sourceContainerBookingId") String sourceContainerBookingId);
|
||||||
|
|
||||||
|
|||||||
+22
-1
@@ -20,6 +20,8 @@ import com.baiyue.lastleg.inboundplan.mapper.InboundPlanPalletMapper;
|
|||||||
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanQueryMapper;
|
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanQueryMapper;
|
||||||
import com.baiyue.lastleg.inboundplan.model.SourceOrderSnapshot;
|
import com.baiyue.lastleg.inboundplan.model.SourceOrderSnapshot;
|
||||||
import com.baiyue.lastleg.inventory.mapper.InventoryBalanceMapper;
|
import com.baiyue.lastleg.inventory.mapper.InventoryBalanceMapper;
|
||||||
|
import com.baiyue.lastleg.dropship.model.DropshipPalletSnapshot;
|
||||||
|
import com.baiyue.lastleg.dropship.service.DropshipOrderService;
|
||||||
import com.baiyue.common.event.ContainerNumbers;
|
import com.baiyue.common.event.ContainerNumbers;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
@@ -47,6 +49,7 @@ public class InboundPlanService {
|
|||||||
@Resource private InboundPlanOrderMapper orderMapper;
|
@Resource private InboundPlanOrderMapper orderMapper;
|
||||||
@Resource private InboundPlanQueryMapper queryMapper;
|
@Resource private InboundPlanQueryMapper queryMapper;
|
||||||
@Resource private InventoryBalanceMapper inventoryBalanceMapper;
|
@Resource private InventoryBalanceMapper inventoryBalanceMapper;
|
||||||
|
@Resource private DropshipOrderService dropshipOrders;
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public PageResult<InboundPlanListResponse> page(String customerId, InboundPlanRequests.Query query) {
|
public PageResult<InboundPlanListResponse> page(String customerId, InboundPlanRequests.Query query) {
|
||||||
@@ -206,6 +209,7 @@ public class InboundPlanService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<List<SourceOrderSnapshot>> snapshots = new ArrayList<>(refs.size());
|
List<List<SourceOrderSnapshot>> snapshots = new ArrayList<>(refs.size());
|
||||||
|
List<List<WholePalletInput>> wholePallets = new ArrayList<>(refs.size());
|
||||||
for (InboundPlanOrderMapper.ContainerOrderRef ref : refs) {
|
for (InboundPlanOrderMapper.ContainerOrderRef ref : refs) {
|
||||||
List<SourceOrderSnapshot> orders = orderMapper.lockOrder(ref.customerId(), ref.orderId());
|
List<SourceOrderSnapshot> orders = orderMapper.lockOrder(ref.customerId(), ref.orderId());
|
||||||
if (orders == null || orders.isEmpty()) {
|
if (orders == null || orders.isEmpty()) {
|
||||||
@@ -217,16 +221,33 @@ public class InboundPlanService {
|
|||||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "来源柜内尾程订单柜号不一致");
|
throw new ResponseStatusException(HttpStatus.CONFLICT, "来源柜内尾程订单柜号不一致");
|
||||||
}
|
}
|
||||||
snapshots.add(orders);
|
snapshots.add(orders);
|
||||||
|
List<WholePalletInput> sourcePallets = "ONE_CLICK_DROPSHIP".equals(ref.orderType())
|
||||||
|
? dropshipOrders.palletSnapshots(ref.customerId(), ref.orderId()).stream()
|
||||||
|
.map(this::wholePallet).toList()
|
||||||
|
: null;
|
||||||
|
if (sourcePallets != null) {
|
||||||
|
if (sourcePallets.isEmpty()) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.CONFLICT,
|
||||||
|
"一件代发订单缺少不可变来源托盘快照");
|
||||||
|
}
|
||||||
|
validateWholePallets(orders, sourcePallets);
|
||||||
|
}
|
||||||
|
wholePallets.add(sourcePallets);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<InboundPlanResponse> created = new ArrayList<>(refs.size());
|
List<InboundPlanResponse> created = new ArrayList<>(refs.size());
|
||||||
for (int index = 0; index < refs.size(); index++) {
|
for (int index = 0; index < refs.size(); index++) {
|
||||||
InboundPlanOrderMapper.ContainerOrderRef ref = refs.get(index);
|
InboundPlanOrderMapper.ContainerOrderRef ref = refs.get(index);
|
||||||
created.add(createPlan(ref.customerId(), ref.orderId(), snapshots.get(index), List.of(), null));
|
created.add(createPlan(ref.customerId(), ref.orderId(), snapshots.get(index), List.of(),
|
||||||
|
wholePallets.get(index)));
|
||||||
}
|
}
|
||||||
return List.copyOf(created);
|
return List.copyOf(created);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private WholePalletInput wholePallet(DropshipPalletSnapshot pallet) {
|
||||||
|
return new WholePalletInput(pallet.sourcePalletNo(), Math.toIntExact(pallet.packageCount()));
|
||||||
|
}
|
||||||
|
|
||||||
private String requiredContainerNo(String value) {
|
private String requiredContainerNo(String value) {
|
||||||
try {
|
try {
|
||||||
return ContainerNumbers.requireCanonical(value);
|
return ContainerNumbers.requireCanonical(value);
|
||||||
|
|||||||
+41
-12
@@ -13,6 +13,8 @@ import com.baiyue.lastleg.inboundplan.mapper.InboundPlanPalletAllocationMapper;
|
|||||||
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanPalletMapper;
|
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanPalletMapper;
|
||||||
import com.baiyue.lastleg.inboundplan.model.SourceOrderSnapshot;
|
import com.baiyue.lastleg.inboundplan.model.SourceOrderSnapshot;
|
||||||
import com.baiyue.lastleg.inventory.mapper.InventoryBalanceMapper;
|
import com.baiyue.lastleg.inventory.mapper.InventoryBalanceMapper;
|
||||||
|
import com.baiyue.lastleg.dropship.model.DropshipPalletSnapshot;
|
||||||
|
import com.baiyue.lastleg.dropship.service.DropshipOrderService;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
@@ -44,6 +46,7 @@ class InboundPlanServiceTest {
|
|||||||
@Mock InboundPlanPalletAllocationMapper allocationMapper;
|
@Mock InboundPlanPalletAllocationMapper allocationMapper;
|
||||||
@Mock InboundPlanOrderMapper orderMapper;
|
@Mock InboundPlanOrderMapper orderMapper;
|
||||||
@Mock InventoryBalanceMapper inventoryBalanceMapper;
|
@Mock InventoryBalanceMapper inventoryBalanceMapper;
|
||||||
|
@Mock DropshipOrderService dropshipOrders;
|
||||||
|
|
||||||
private InboundPlanService service;
|
private InboundPlanService service;
|
||||||
|
|
||||||
@@ -56,6 +59,7 @@ class InboundPlanServiceTest {
|
|||||||
ReflectionTestUtils.setField(service, "allocationMapper", allocationMapper);
|
ReflectionTestUtils.setField(service, "allocationMapper", allocationMapper);
|
||||||
ReflectionTestUtils.setField(service, "orderMapper", orderMapper);
|
ReflectionTestUtils.setField(service, "orderMapper", orderMapper);
|
||||||
ReflectionTestUtils.setField(service, "inventoryBalanceMapper", inventoryBalanceMapper);
|
ReflectionTestUtils.setField(service, "inventoryBalanceMapper", inventoryBalanceMapper);
|
||||||
|
ReflectionTestUtils.setField(service, "dropshipOrders", dropshipOrders);
|
||||||
ReflectionTestUtils.setField(service, "orderNoGenerator", new com.baiyue.lastleg.service.LastlegOrderNoGenerator());
|
ReflectionTestUtils.setField(service, "orderNoGenerator", new com.baiyue.lastleg.service.LastlegOrderNoGenerator());
|
||||||
org.mockito.Mockito.lenient().when(lineMapper.selectList(any())).thenReturn(List.of());
|
org.mockito.Mockito.lenient().when(lineMapper.selectList(any())).thenReturn(List.of());
|
||||||
org.mockito.Mockito.lenient().when(palletMapper.selectList(any())).thenReturn(List.of());
|
org.mockito.Mockito.lenient().when(palletMapper.selectList(any())).thenReturn(List.of());
|
||||||
@@ -278,8 +282,8 @@ class InboundPlanServiceTest {
|
|||||||
assertNotNull(InboundPlanService.class.getMethod("convertContainer", String.class)
|
assertNotNull(InboundPlanService.class.getMethod("convertContainer", String.class)
|
||||||
.getAnnotation(Transactional.class));
|
.getAnnotation(Transactional.class));
|
||||||
when(orderMapper.lockContainerOrders("BOOKING-1")).thenReturn(List.of(
|
when(orderMapper.lockContainerOrders("BOOKING-1")).thenReturn(List.of(
|
||||||
new InboundPlanOrderMapper.ContainerOrderRef("100", 1L, "PENDING_ASSIGNMENT", "MSCU6639870"),
|
new InboundPlanOrderMapper.ContainerOrderRef("100", 1L, "PENDING_ASSIGNMENT", "MSCU6639870", "STANDARD"),
|
||||||
new InboundPlanOrderMapper.ContainerOrderRef("200", 2L, "DESTINATION_ASSIGNED", "MSCU6639870")));
|
new InboundPlanOrderMapper.ContainerOrderRef("200", 2L, "DESTINATION_ASSIGNED", "MSCU6639870", "STANDARD")));
|
||||||
when(orderMapper.lockOrder("100", 1L)).thenReturn(List.of(order("100", 1L, "RK-1", "WH-A")));
|
when(orderMapper.lockOrder("100", 1L)).thenReturn(List.of(order("100", 1L, "RK-1", "WH-A")));
|
||||||
when(orderMapper.lockOrder("200", 2L)).thenReturn(List.of(order("200", 2L, "RK-2", "WH-B")));
|
when(orderMapper.lockOrder("200", 2L)).thenReturn(List.of(order("200", 2L, "RK-2", "WH-B")));
|
||||||
when(orderMapper.markPlanned(any(), any(), any(), org.mockito.ArgumentMatchers.anyInt(), any()))
|
when(orderMapper.markPlanned(any(), any(), any(), org.mockito.ArgumentMatchers.anyInt(), any()))
|
||||||
@@ -296,11 +300,36 @@ class InboundPlanServiceTest {
|
|||||||
verify(orderMapper).lockOrder("200", 2L);
|
verify(orderMapper).lockOrder("200", 2L);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void containerConversionPreservesDropshipSourcePallets() {
|
||||||
|
when(orderMapper.lockContainerOrders("BOOKING-1")).thenReturn(List.of(
|
||||||
|
new InboundPlanOrderMapper.ContainerOrderRef("100", 1L, "PENDING_ASSIGNMENT",
|
||||||
|
"MSCU6639870", "ONE_CLICK_DROPSHIP")));
|
||||||
|
when(orderMapper.lockOrder("100", 1L)).thenReturn(List.of(order(1L, "RK-1", "1.0000", "WH-A")));
|
||||||
|
when(dropshipOrders.palletSnapshots("100", 1L)).thenReturn(List.of(
|
||||||
|
new DropshipPalletSnapshot("SOURCE-P-1", 3, List.of()),
|
||||||
|
new DropshipPalletSnapshot("SOURCE-P-2", 7, List.of())));
|
||||||
|
when(orderMapper.markPlanned(any(), any(), any(), org.mockito.ArgumentMatchers.anyInt(), any()))
|
||||||
|
.thenReturn(1);
|
||||||
|
|
||||||
|
service.convertContainer("BOOKING-1");
|
||||||
|
|
||||||
|
ArgumentCaptor<InboundPlanPallet> pallets = ArgumentCaptor.forClass(InboundPlanPallet.class);
|
||||||
|
verify(palletMapper, times(2)).insert(pallets.capture());
|
||||||
|
assertEquals(List.of("SOURCE-P-1", "SOURCE-P-2"),
|
||||||
|
pallets.getAllValues().stream().map(InboundPlanPallet::getPalletNo).toList());
|
||||||
|
ArgumentCaptor<InboundPlanPalletAllocation> allocations =
|
||||||
|
ArgumentCaptor.forClass(InboundPlanPalletAllocation.class);
|
||||||
|
verify(allocationMapper, times(2)).insert(allocations.capture());
|
||||||
|
assertEquals(List.of(3, 7), allocations.getAllValues().stream()
|
||||||
|
.map(InboundPlanPalletAllocation::getPlannedPackageCount).toList());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void invalidLaterOrderCausesNoWritesBecauseWholeContainerIsPrevalidated() {
|
void invalidLaterOrderCausesNoWritesBecauseWholeContainerIsPrevalidated() {
|
||||||
when(orderMapper.lockContainerOrders("BOOKING-1")).thenReturn(List.of(
|
when(orderMapper.lockContainerOrders("BOOKING-1")).thenReturn(List.of(
|
||||||
new InboundPlanOrderMapper.ContainerOrderRef("100", 1L, "PENDING_ASSIGNMENT", "MSCU6639870"),
|
new InboundPlanOrderMapper.ContainerOrderRef("100", 1L, "PENDING_ASSIGNMENT", "MSCU6639870", "STANDARD"),
|
||||||
new InboundPlanOrderMapper.ContainerOrderRef("100", 2L, "PENDING_ASSIGNMENT", "MSCU6639870")));
|
new InboundPlanOrderMapper.ContainerOrderRef("100", 2L, "PENDING_ASSIGNMENT", "MSCU6639870", "STANDARD")));
|
||||||
when(orderMapper.lockOrder("100", 1L)).thenReturn(List.of(order(1L, "RK-1", "1", "WH-A")));
|
when(orderMapper.lockOrder("100", 1L)).thenReturn(List.of(order(1L, "RK-1", "1", "WH-A")));
|
||||||
when(orderMapper.lockOrder("100", 2L)).thenReturn(List.of(new SourceOrderSnapshot(2L, "100", "MSCU6639870", "RK-2",
|
when(orderMapper.lockOrder("100", 2L)).thenReturn(List.of(new SourceOrderSnapshot(2L, "100", "MSCU6639870", "RK-2",
|
||||||
"PENDING_ASSIGNMENT", null, BigDecimal.ONE, 1, null, null, null, 1, 0)));
|
"PENDING_ASSIGNMENT", null, BigDecimal.ONE, 1, null, null, null, 1, 0)));
|
||||||
@@ -315,8 +344,8 @@ class InboundPlanServiceTest {
|
|||||||
@Test
|
@Test
|
||||||
void completeContainerReplayReturnsStableExistingPlansWithoutWriting() {
|
void completeContainerReplayReturnsStableExistingPlansWithoutWriting() {
|
||||||
var refs = List.of(
|
var refs = List.of(
|
||||||
new InboundPlanOrderMapper.ContainerOrderRef("100", 1L, "INBOUND_PLANNED", "MSCU6639870"),
|
new InboundPlanOrderMapper.ContainerOrderRef("100", 1L, "INBOUND_PLANNED", "MSCU6639870", "STANDARD"),
|
||||||
new InboundPlanOrderMapper.ContainerOrderRef("200", 2L, "INBOUND_COMPLETED", "MSCU6639870"));
|
new InboundPlanOrderMapper.ContainerOrderRef("200", 2L, "INBOUND_COMPLETED", "MSCU6639870", "STANDARD"));
|
||||||
when(orderMapper.lockContainerOrders("BOOKING-1")).thenReturn(refs);
|
when(orderMapper.lockContainerOrders("BOOKING-1")).thenReturn(refs);
|
||||||
when(planMapper.findBySourceOrder("100", 1L)).thenReturn(existingPlan("100", 1L, "LIP-1"));
|
when(planMapper.findBySourceOrder("100", 1L)).thenReturn(existingPlan("100", 1L, "LIP-1"));
|
||||||
when(planMapper.findBySourceOrder("200", 2L)).thenReturn(existingPlan("200", 2L, "LIP-2"));
|
when(planMapper.findBySourceOrder("200", 2L)).thenReturn(existingPlan("200", 2L, "LIP-2"));
|
||||||
@@ -331,8 +360,8 @@ class InboundPlanServiceTest {
|
|||||||
@Test
|
@Test
|
||||||
void partialExistingPlansAreRejectedBeforeAnyWrite() {
|
void partialExistingPlansAreRejectedBeforeAnyWrite() {
|
||||||
when(orderMapper.lockContainerOrders("BOOKING-1")).thenReturn(List.of(
|
when(orderMapper.lockContainerOrders("BOOKING-1")).thenReturn(List.of(
|
||||||
new InboundPlanOrderMapper.ContainerOrderRef("100", 1L, "INBOUND_PLANNED", "MSCU6639870"),
|
new InboundPlanOrderMapper.ContainerOrderRef("100", 1L, "INBOUND_PLANNED", "MSCU6639870", "STANDARD"),
|
||||||
new InboundPlanOrderMapper.ContainerOrderRef("100", 2L, "PENDING_ASSIGNMENT", "MSCU6639870")));
|
new InboundPlanOrderMapper.ContainerOrderRef("100", 2L, "PENDING_ASSIGNMENT", "MSCU6639870", "STANDARD")));
|
||||||
when(planMapper.findBySourceOrder("100", 1L)).thenReturn(existingPlan("100", 1L, "LIP-1"));
|
when(planMapper.findBySourceOrder("100", 1L)).thenReturn(existingPlan("100", 1L, "LIP-1"));
|
||||||
|
|
||||||
assertThrows(ResponseStatusException.class, () -> service.convertContainer("BOOKING-1"));
|
assertThrows(ResponseStatusException.class, () -> service.convertContainer("BOOKING-1"));
|
||||||
@@ -344,8 +373,8 @@ class InboundPlanServiceTest {
|
|||||||
@Test
|
@Test
|
||||||
void rejectsContainerDriftAcrossCustomersBeforeWritingAnyPlan() {
|
void rejectsContainerDriftAcrossCustomersBeforeWritingAnyPlan() {
|
||||||
when(orderMapper.lockContainerOrders("BOOKING-1")).thenReturn(List.of(
|
when(orderMapper.lockContainerOrders("BOOKING-1")).thenReturn(List.of(
|
||||||
new InboundPlanOrderMapper.ContainerOrderRef("100", 1L, "PENDING_ASSIGNMENT", "MSCU6639870"),
|
new InboundPlanOrderMapper.ContainerOrderRef("100", 1L, "PENDING_ASSIGNMENT", "MSCU6639870", "STANDARD"),
|
||||||
new InboundPlanOrderMapper.ContainerOrderRef("200", 2L, "PENDING_ASSIGNMENT", "CSQU3054383")));
|
new InboundPlanOrderMapper.ContainerOrderRef("200", 2L, "PENDING_ASSIGNMENT", "CSQU3054383", "STANDARD")));
|
||||||
|
|
||||||
ResponseStatusException error = assertThrows(ResponseStatusException.class,
|
ResponseStatusException error = assertThrows(ResponseStatusException.class,
|
||||||
() -> service.convertContainer("BOOKING-1"));
|
() -> service.convertContainer("BOOKING-1"));
|
||||||
@@ -358,8 +387,8 @@ class InboundPlanServiceTest {
|
|||||||
@Test
|
@Test
|
||||||
void rejectsReplayWhenExistingPlanContainerSnapshotHasDrifted() {
|
void rejectsReplayWhenExistingPlanContainerSnapshotHasDrifted() {
|
||||||
when(orderMapper.lockContainerOrders("BOOKING-1")).thenReturn(List.of(
|
when(orderMapper.lockContainerOrders("BOOKING-1")).thenReturn(List.of(
|
||||||
new InboundPlanOrderMapper.ContainerOrderRef("100", 1L, "INBOUND_PLANNED", "MSCU6639870"),
|
new InboundPlanOrderMapper.ContainerOrderRef("100", 1L, "INBOUND_PLANNED", "MSCU6639870", "STANDARD"),
|
||||||
new InboundPlanOrderMapper.ContainerOrderRef("200", 2L, "INBOUND_PLANNED", "MSCU6639870")));
|
new InboundPlanOrderMapper.ContainerOrderRef("200", 2L, "INBOUND_PLANNED", "MSCU6639870", "STANDARD")));
|
||||||
InboundPlan first = existingPlan("100", 1L, "LIP-1");
|
InboundPlan first = existingPlan("100", 1L, "LIP-1");
|
||||||
InboundPlan drifted = existingPlan("200", 2L, "LIP-2");
|
InboundPlan drifted = existingPlan("200", 2L, "LIP-2");
|
||||||
drifted.setContainerNo("CSQU3054383");
|
drifted.setContainerNo("CSQU3054383");
|
||||||
|
|||||||
Reference in New Issue
Block a user