This commit is contained in:
2026-09-16 17:49:20 +08:00
parent 392f5b7386
commit 8488d4d989
10 changed files with 148 additions and 60 deletions
@@ -216,12 +216,19 @@ public interface DropshipPalletizationMapper {
""") List<CartonRow> cartons(@Param("palletId") long palletId);
@Select("SELECT c.*,p.pallet_no FROM fl_dropship_pallet_carton c LEFT JOIN fl_dropship_pallet p ON p.id=c.pallet_id WHERE c.task_id=#{taskId} AND c.pallet_id IS NULL ORDER BY c.counted_at,c.id") List<CartonRow> unboundCartons(@Param("taskId") long taskId);
@Select("""
SELECT COALESCE(SUM(o.actual_gross_weight_kg/NULLIF(o.total_package_count,0)),0) weight_kg,
COALESCE(SUM(o.actual_volume_cbm/NULLIF(o.total_package_count,0)),0) volume_cbm
FROM fl_dropship_pallet_carton c
JOIN fl_inbound_order o ON o.id=c.inbound_id AND o.tenant_id=c.tenant_id
WHERE c.pallet_id=#{palletId}""")
PalletMetrics metrics(@Param("palletId") long palletId);
<script>
SELECT c.pallet_id,COUNT(DISTINCT c.id) carton_count,
COALESCE(SUM(o.actual_gross_weight_kg/NULLIF(o.total_package_count,0)),0) gross_weight_kg,
COALESCE(SUM(o.actual_net_weight_kg/NULLIF(o.total_package_count,0)),0) net_weight_kg,
COALESCE(SUM(o.actual_volume_cbm/NULLIF(o.total_package_count,0)),0) volume_cbm
FROM fl_dropship_pallet_carton c
JOIN fl_inbound_order o ON o.id=c.inbound_id AND o.tenant_id=c.tenant_id
WHERE c.pallet_id IN
<foreach collection="palletIds" item="palletId" open="(" separator="," close=")">#{palletId}</foreach>
GROUP BY c.pallet_id
</script>
""")
List<PalletMetrics> physicalMetricsByPalletIds(@Param("palletIds") Collection<Long> palletIds);
@Select("""
SELECT c.tenant_id,c.inbound_id,o.inbound_code,
@@ -340,7 +347,8 @@ public interface DropshipPalletizationMapper {
@Getter @Setter class LabelSkuRow { private Long inboundDetailId; private String skuCode,cargoName; private Integer expectedQuantity,lineNo; }
@Getter @Setter class CartonRow { private Long id,taskId,palletId,inboundId,inboundDetailId; private String palletNo,inboundCode,skuCode,cargoName,boxCode,discrepancyStatus,discrepancyRemark; private Integer expectedSkuQuantity,actualSkuQuantity; }
@Getter @Setter class CartonSkuRow { private Long inboundDetailId; private String skuCode,cargoName,discrepancyStatus,discrepancyRemark; private Integer expectedQuantity,actualQuantity,lineNo; }
record PalletMetrics(BigDecimal weightKg,BigDecimal volumeCbm) {}
record PalletMetrics(Long palletId,Integer cartonCount,BigDecimal grossWeightKg,
BigDecimal netWeightKg,BigDecimal volumeCbm) {}
@Getter @Setter class PrintInboundRow { private Long tenantId,inboundId; private String inboundCode,destinationWarehouse; private Integer cartonCount,itemQuantity; }
@Getter @Setter class PrintSkuRow { private String skuCode; private Integer cartonCount,itemQuantity,minUnitsPerCarton,maxUnitsPerCarton; }
@Getter @Setter class PalletLine { private Long palletId,tenantId,accountId,inboundId,orderId,warehouseId,destinationWarehouseId,inboundDetailId; private String palletNo,palletStatus,orderNo,inboundCode,channelCode,channelName,destinationWarehouseCode,destinationWarehouseName,customerName,skuCode,cargoName; private Integer cartonCount,actualSkuQuantity; private BigDecimal unitGrossWeightKg,unitNetWeightKg,unitVolumeCbm; }
@@ -45,10 +45,33 @@ public interface LoadingListQueryMapper {
(SELECT pick_task.task_status FROM fl_loading_pick_task pick_task
WHERE pick_task.loading_plan_id = p.id LIMIT 1) AS pda_task_status,
COUNT(DISTINCT NULLIF(i.sku_code, '')) AS sku_count,
COALESCE(SUM(i.package_count), 0) AS total_package_count,
COALESCE(SUM(i.gross_weight_kg), 0) AS total_gross_weight_kg,
COALESCE(SUM(i.net_weight_kg), 0) AS total_net_weight_kg,
COALESCE(SUM(i.volume_cbm), 0) AS total_volume_cbm,
COALESCE(SUM(CASE WHEN i.pallet_id IS NULL THEN i.package_count ELSE 0 END), 0)
+ COALESCE((SELECT COUNT(DISTINCT carton.id)
FROM fl_dropship_pallet_carton carton
WHERE EXISTS (SELECT 1 FROM fl_loading_plan_item pallet_item
WHERE pallet_item.loading_plan_id=p.id AND pallet_item.deleted=0
AND pallet_item.pallet_id=carton.pallet_id)), 0) AS total_package_count,
COALESCE(SUM(CASE WHEN i.pallet_id IS NULL THEN i.gross_weight_kg ELSE 0 END), 0)
+ COALESCE((SELECT SUM(inbound.actual_gross_weight_kg/NULLIF(inbound.total_package_count,0))
FROM fl_dropship_pallet_carton carton
JOIN fl_inbound_order inbound ON inbound.id=carton.inbound_id AND inbound.tenant_id=carton.tenant_id
WHERE EXISTS (SELECT 1 FROM fl_loading_plan_item pallet_item
WHERE pallet_item.loading_plan_id=p.id AND pallet_item.deleted=0
AND pallet_item.pallet_id=carton.pallet_id)), 0) AS total_gross_weight_kg,
COALESCE(SUM(CASE WHEN i.pallet_id IS NULL THEN i.net_weight_kg ELSE 0 END), 0)
+ COALESCE((SELECT SUM(inbound.actual_net_weight_kg/NULLIF(inbound.total_package_count,0))
FROM fl_dropship_pallet_carton carton
JOIN fl_inbound_order inbound ON inbound.id=carton.inbound_id AND inbound.tenant_id=carton.tenant_id
WHERE EXISTS (SELECT 1 FROM fl_loading_plan_item pallet_item
WHERE pallet_item.loading_plan_id=p.id AND pallet_item.deleted=0
AND pallet_item.pallet_id=carton.pallet_id)), 0) AS total_net_weight_kg,
COALESCE(SUM(CASE WHEN i.pallet_id IS NULL THEN i.volume_cbm ELSE 0 END), 0)
+ COALESCE((SELECT SUM(inbound.actual_volume_cbm/NULLIF(inbound.total_package_count,0))
FROM fl_dropship_pallet_carton carton
JOIN fl_inbound_order inbound ON inbound.id=carton.inbound_id AND inbound.tenant_id=carton.tenant_id
WHERE EXISTS (SELECT 1 FROM fl_loading_plan_item pallet_item
WHERE pallet_item.loading_plan_id=p.id AND pallet_item.deleted=0
AND pallet_item.pallet_id=carton.pallet_id)), 0) AS total_volume_cbm,
COALESCE(SUM(i.product_quantity), 0) AS total_quantity
""";
@@ -158,11 +158,12 @@ public class LoadingListRepository {
java.util.ArrayList<String> blockers = new java.util.ArrayList<>();
if (!editing) blockers.add("LOADING_PLAN_NOT_EDITING");
if (linkCount == 0) blockers.add("PRELOAD_LINK_MISSING");
if (!containerReady) blockers.add("CONTAINER_NO_MISSING");
if (!oceanLinked) blockers.add("OCEAN_BOOKING_NOT_LINKED");
else if (!released) blockers.add("OCEAN_BOOKING_NOT_RELEASED");
if (taskCreated) blockers.add("PDA_TASK_ALREADY_CREATED");
boolean allConfirmed = linkCount > 0 && confirmedCount == linkCount;
boolean prerequisitesReady = editing && linkCount > 0 && released;
boolean prerequisitesReady = editing && linkCount > 0 && containerReady && released;
boolean canConfirm = prerequisitesReady && !taskCreated && !allConfirmed;
if (allConfirmed) blockers.add("PRELOAD_ALREADY_CONFIRMED");
String confirmationStatus = allConfirmed ? "CONFIRMED"
@@ -231,10 +231,10 @@ public class DropshipPalletizationService {
values.stream().mapToInt(v->v.line().getActualQuantity()==null?0:v.line().getActualQuantity()).sum(),
values.stream().map(v->v.carton().getBoxCode()).toList());}).toList();
int discrepancies=(int)cartons.stream().filter(c->"ACCEPTED_VARIANCE".equals(c.getDiscrepancyStatus())).count();
PalletMetrics metrics=mapper.metrics(row.getId());
PalletMetrics metrics=mapper.physicalMetricsByPalletIds(List.of(row.getId())).stream().findFirst().orElse(null);
int skuCount=(int)items.stream().map(SkuItem::skuCode).distinct().count();
return new Pallet(row.getId(),row.getPalletNo(),row.getPalletStatus(),cartons.size(),skuCount,discrepancies,
metrics==null?BigDecimal.ZERO:metrics.weightKg(),metrics==null?BigDecimal.ZERO:metrics.volumeCbm(),row.getVersion(),items,
metrics==null?BigDecimal.ZERO:metrics.grossWeightKg(),metrics==null?BigDecimal.ZERO:metrics.volumeCbm(),row.getVersion(),items,
cartons.stream().map(c->cartonView(c,c.getInboundCode(),false,null)).toList(),row.getCompletedAt());
}
@@ -116,6 +116,7 @@ public class LoadingPickService {
public void beginIfReady(long containerBookingId, long operatorId, String operatorName) {
var booking = smartLoadingRepository.booking(containerBookingId);
if (booking == null) return;
if (!StringUtils.hasText(booking.getContainerNo())) return;
var plan = smartLoadingRepository.planByBooking(containerBookingId);
if (plan == null || !"EDITING".equals(plan.getPlanStatus())) return;
inboundHoldGuard.requirePlanNotHeld(plan.getId());
@@ -137,6 +138,10 @@ public class LoadingPickService {
if (!Set.of("PENDING", "CONFIRMING").contains(link.preloadStatus())) {
throw conflict("预装清单已确认或当前状态不允许确认");
}
var booking = smartLoadingRepository.booking(link.containerBookingId());
if (booking == null || !StringUtils.hasText(booking.getContainerNo())) {
throw conflict("确认预装前必须录入真实集装箱号");
}
var allocation = oceanBookingRepository.allocationByContainer(link.containerBookingId());
if (allocation == null) throw conflict("确认预装前必须关联真实订舱柜");
var oceanBooking = oceanBookingRepository.find(allocation.getOceanBookingId());
@@ -476,19 +476,28 @@ public class SmartLoadingService {
var first = lines.get(0);
inboundHoldGuard.requireInboundNotHeld(first.getAccountId(), first.getInboundId());
if (!repository.activeItemsByPallet(request.palletId()).isEmpty()) throw conflict("托盘已被有效柜次占用");
com.baiyue.firstleg.mapper.DropshipPalletizationMapper.PalletMetrics physical =
dropshipPallets.physicalMetricsByPalletIds(List.of(request.palletId())).stream().findFirst()
.orElseThrow(() -> conflict("托盘缺少物理箱明细,不能加入预装清单"));
int sequence = 0;
for (var line : lines) {
boolean primaryPalletLine = sequence == 0;
BigDecimal lineBoxes = BigDecimal.valueOf(line.getCartonCount());
LoadingPlanEntity current = repository.plan(planId);
Candidate candidate = new Candidate(line.getAccountId(), line.getInboundId(), line.getInboundDetailId(),
line.getOrderId(), line.getOrderNo(), line.getInboundCode(), line.getWarehouseId(), line.getChannelCode(),
line.getChannelName(), line.getDestinationWarehouseId(), line.getDestinationWarehouseCode(),
line.getDestinationWarehouseName(), line.getCustomerName(), line.getCargoName(), line.getCartonCount(),
line.getUnitGrossWeightKg(), line.getUnitNetWeightKg(), line.getUnitVolumeCbm(), null, null, null, null,
primaryPalletLine ? unit(physical.grossWeightKg()).divide(lineBoxes, 6, RoundingMode.HALF_UP) : BigDecimal.ZERO,
primaryPalletLine ? unit(physical.netWeightKg()).divide(lineBoxes, 6, RoundingMode.HALF_UP) : BigDecimal.ZERO,
primaryPalletLine ? unit(physical.volumeCbm()).divide(lineBoxes, 6, RoundingMode.HALF_UP) : BigDecimal.ZERO,
null, null, null, null,
false, null, "WAIT_SPLIT", null);
String childOperation = "PAL-" + sha256(operation + '|' + line.getInboundDetailId()).substring(0, 48);
assign(planId, new AssignRequest(line.getInboundId(), line.getInboundDetailId(), line.getCartonCount(),
current.getVersion(), childOperation), operatorId, operatorName, true, request.palletId(),
line.getPalletNo(), line.getActualSkuQuantity(), sequence++ == 0, candidate, false);
line.getPalletNo(), line.getActualSkuQuantity(), primaryPalletLine, candidate, false);
sequence++;
}
LoadingPlanEntity assigned = repository.plan(planId);
history(planId, null, "ADD_PALLET", original.getPlanStatus(), assigned.getPlanStatus(), operation,
@@ -1689,19 +1698,26 @@ public class SmartLoadingService {
Map<Long,List<LoadingPlanItemEntity>> pallets = items.stream().filter(i -> i.getPalletId() != null)
.collect(java.util.stream.Collectors.groupingBy(LoadingPlanItemEntity::getPalletId,
LinkedHashMap::new, java.util.stream.Collectors.toList()));
Map<Long,com.baiyue.firstleg.mapper.DropshipPalletizationMapper.PalletMetrics> physicalByPallet =
pallets.isEmpty() ? Map.of() : dropshipPallets.physicalMetricsByPalletIds(pallets.keySet()).stream()
.collect(java.util.stream.Collectors.toMap(
com.baiyue.firstleg.mapper.DropshipPalletizationMapper.PalletMetrics::palletId,
java.util.function.Function.identity()));
items.stream().filter(i -> i.getPalletId() == null).forEach(result::add);
for (List<LoadingPlanItemEntity> lines : pallets.values()) {
LoadingPlanItemEntity unit = lines.get(0);
unit.setPackageCount(lines.stream().map(LoadingPlanItemEntity::getPackageCount)
.filter(Objects::nonNull).mapToInt(Integer::intValue).sum());
var physical = physicalByPallet.get(unit.getPalletId());
if (physical == null) throw conflict("托盘缺少物理箱明细,不能汇总预装清单");
unit.setPackageCount(physical.cartonCount());
unit.setProductQuantity(lines.stream().map(LoadingPlanItemEntity::getProductQuantity)
.filter(Objects::nonNull).mapToInt(Integer::intValue).sum());
unit.setPalletCount(1);
unit.setGrossWeightKg(sumDecimal(lines, LoadingPlanItemEntity::getGrossWeightKg));
unit.setNetWeightKg(sumDecimal(lines, LoadingPlanItemEntity::getNetWeightKg));
unit.setVolumeCbm(sumDecimal(lines, LoadingPlanItemEntity::getVolumeCbm));
unit.setPickedPackageCount(lines.stream().map(LoadingPlanItemEntity::getPickedPackageCount)
.filter(Objects::nonNull).mapToInt(Integer::intValue).sum());
unit.setGrossWeightKg(physical.grossWeightKg());
unit.setNetWeightKg(physical.netWeightKg());
unit.setVolumeCbm(physical.volumeCbm());
unit.setPickedPackageCount(Math.min(physical.cartonCount(), lines.stream()
.map(LoadingPlanItemEntity::getPickedPackageCount).filter(Objects::nonNull)
.mapToInt(Integer::intValue).sum()));
unit.setTotalPackageCount(unit.getPackageCount());
unit.setRemainingPackageCount(0);
unit.setCanAdjust(false);
@@ -1713,10 +1729,6 @@ public class SmartLoadingService {
.thenComparing(LoadingPlanItemEntity::getId, Comparator.nullsLast(Long::compareTo)))
.toList();
}
private static BigDecimal sumDecimal(List<LoadingPlanItemEntity> lines,
java.util.function.Function<LoadingPlanItemEntity,BigDecimal> getter) {
return lines.stream().map(getter).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add);
}
private int adjustmentTotal(LoadingPlanItemEntity item) {
int current = item.getPackageCount() == null ? 0 : item.getPackageCount();
try {
@@ -74,7 +74,7 @@ class LoadingListRepositoryTest {
}
@Test
void missingContainerNumberDoesNotBlockPreloadConfirmationOrPdaReadiness() {
void missingContainerNumberBlocksPreloadConfirmationAndPdaReadiness() {
AggregateRow row = new AggregateRow();
row.setPlanId(41L);
row.setContainerBookingId(61L);
@@ -90,12 +90,12 @@ class LoadingListRepositoryTest {
var pending = repository.preloadDetail(row).summary().readiness();
assertThat(pending.containerNoReady()).isFalse();
assertThat(pending.canConfirm()).isTrue();
assertThat(pending.blockingReasons()).doesNotContain("CONTAINER_NO_MISSING");
assertThat(pending.canConfirm()).isFalse();
assertThat(pending.blockingReasons()).contains("CONTAINER_NO_MISSING");
row.setConfirmedPreloadCount(2L);
var confirmed = repository.preloadDetail(row).summary().readiness();
assertThat(confirmed.readyForPda()).isTrue();
assertThat(confirmed.readyForPda()).isFalse();
}
@Test
@@ -158,30 +158,15 @@ class LoadingPickServiceTest {
}
@Test
void opensTaskAfterReleasedBookingWithoutWaitingForRealContainerNumber() {
void doesNotOpenTaskWithoutRealContainerNumber() {
var booking = new com.baiyue.firstleg.entity.ContainerBookingEntity();
booking.setId(40L);
var plan = new com.baiyue.firstleg.entity.LoadingPlanEntity();
plan.setId(10L); plan.setContainerBookingId(40L); plan.setPlanStatus("EDITING");
var allocation = new OceanBookingContainerAllocationEntity(); allocation.setOceanBookingId(90L);
var ocean = new OceanBookingEntity(); ocean.setId(90L); ocean.setStatus("RELEASED");
when(smartLoadingRepository.booking(40L)).thenReturn(booking);
when(smartLoadingRepository.planByBooking(40L)).thenReturn(plan);
when(smartLoadingRepository.plan(10L)).thenReturn(plan);
when(loadingModelBridge.byLoadingPlan(10L)).thenReturn(List.of(
new Link(1L, 1L, 50L, 2L, "CONFIRMED", 1, 10L, 40L, null, "EDITING", 1, 1, null)));
when(oceanBookingRepository.allocationByContainer(40L)).thenReturn(allocation);
when(oceanBookingRepository.find(90L)).thenReturn(ocean);
when(repository.taskByPlan(10L)).thenReturn(null);
when(smartLoadingRepository.items(10L)).thenReturn(List.of());
doAnswer(invocation -> {
invocation.getArgument(0, LoadingPickTaskEntity.class).setId(20L);
return null;
}).when(repository).insert(any(LoadingPickTaskEntity.class));
service.beginIfReady(40L, 7L, "admin");
verify(repository).insert(any(LoadingPickTaskEntity.class));
verify(repository, never()).insert(any(LoadingPickTaskEntity.class));
verify(smartLoadingRepository, never()).planByBooking(40L);
}
@Test
@@ -222,11 +207,11 @@ class LoadingPickServiceTest {
}
@Test
void planConfirmationGateRequiresReleasedBookingButNotRealContainerNumber() {
void planConfirmationGateRequiresReleasedBookingAndRealContainerNumber() {
Link link = new Link(1L, 1L, 50L, 2L, "PENDING", 1,
10L, 40L, "MSCU-001", "EDITING", 3, 2, null);
var booking = new com.baiyue.firstleg.entity.ContainerBookingEntity();
booking.setId(40L);
booking.setId(40L); booking.setContainerNo("MSCU-001");
var allocation = new OceanBookingContainerAllocationEntity();
allocation.setOceanBookingId(90L);
var ocean = new OceanBookingEntity();
@@ -240,21 +225,17 @@ class LoadingPickServiceTest {
}
@Test
void planConfirmationGateAllowsMissingRealContainerNumber() {
void planConfirmationGateRejectsMissingRealContainerNumber() {
Link link = new Link(1L, 1L, 50L, 2L, "PENDING", 1,
10L, 40L, null, "EDITING", 3, 2, null);
var booking = new com.baiyue.firstleg.entity.ContainerBookingEntity();
booking.setId(40L);
var allocation = new OceanBookingContainerAllocationEntity();
allocation.setOceanBookingId(90L);
var ocean = new OceanBookingEntity();
ocean.setId(90L); ocean.setStatus("RELEASED");
when(loadingModelBridge.requireAllocatedLinksForConfirmation(10L)).thenReturn(List.of(link));
when(smartLoadingRepository.booking(40L)).thenReturn(booking);
when(oceanBookingRepository.allocationByContainer(40L)).thenReturn(allocation);
when(oceanBookingRepository.find(90L)).thenReturn(ocean);
assertThat(service.requirePreloadConfirmationReady(10L)).containsExactly(link);
assertThatThrownBy(() -> service.requirePreloadConfirmationReady(10L))
.isInstanceOf(BizException.class).hasMessageContaining("真实集装箱号");
verify(oceanBookingRepository, never()).allocationByContainer(40L);
}
@Test
@@ -9,6 +9,7 @@ import com.baiyue.firstleg.client.PendingReviewOrderClient;
import com.baiyue.firstleg.dto.SmartLoadingDtos.AssignRequest;
import com.baiyue.firstleg.dto.SmartLoadingDtos.AssignInboundRequest;
import com.baiyue.firstleg.dto.SmartLoadingDtos.AssignPendingReviewRequest;
import com.baiyue.firstleg.dto.SmartLoadingDtos.AssignPalletRequest;
import com.baiyue.firstleg.dto.SmartLoadingDtos.AdjustRequest;
import com.baiyue.firstleg.dto.SmartLoadingDtos.Candidate;
import com.baiyue.firstleg.dto.SmartLoadingDtos.CandidateOrderStatus;
@@ -62,6 +63,7 @@ import java.util.List;
import java.util.Map;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.concurrent.atomic.AtomicInteger;
import static org.assertj.core.api.Assertions.assertThat;
@@ -76,6 +78,7 @@ import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
@@ -119,6 +122,11 @@ class SmartLoadingServiceTest {
when(fbaPricingService.recalculateUnlockedContainerCustomerFees(anyLong(), anyLong(), anyString()))
.thenReturn(new FbaPricingService.ContainerFeeRecalculationResult(
FbaPricingService.ContainerFeeRecalculationStatus.NOT_READY, 0));
when(dropshipPallets.physicalMetricsByPalletIds(any())).thenAnswer(invocation -> {
Collection<Long> palletIds = invocation.getArgument(0);
return palletIds.stream().map(palletId -> new DropshipPalletizationMapper.PalletMetrics(
palletId, 10, new BigDecimal("20"), new BigDecimal("15"), new BigDecimal("5"))).toList();
});
}
@Test
@@ -181,6 +189,9 @@ class SmartLoadingServiceTest {
secondMember.setCustomerName("客户"); secondMember.setAvailablePackageCount(5);
when(dropshipPallets.palletInboundMembersByPalletIds(List.of(801L)))
.thenReturn(List.of(firstMember,secondMember));
doReturn(List.of(new DropshipPalletizationMapper.PalletMetrics(
801L, 11, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO)))
.when(dropshipPallets).physicalMetricsByPalletIds(any());
Plan result=service.detail(1L);
@@ -887,6 +898,53 @@ class SmartLoadingServiceTest {
eq(99L), eq(9L), anyString());
}
@Test
void multiSkuPalletKeepsSkuLinesButCountsPhysicalCartonsAndVolumeOnce() {
AtomicInteger version = new AtomicInteger();
List<LoadingPlanItemEntity> saved = new ArrayList<>();
DropshipPalletizationMapper.PalletLine first = palletLine(801L, "DP-260916-0009", 20);
DropshipPalletizationMapper.PalletLine second = palletLine(801L, "DP-260916-0009", 20);
second.setInboundDetailId(21L);
when(repository.plan(1L)).thenAnswer(invocation -> plan("EDITING", version.get()));
when(repository.planByBooking(99L)).thenAnswer(invocation -> plan("EDITING", version.get()));
when(repository.booking(99L)).thenReturn(booking());
when(repository.items(1L)).thenAnswer(invocation -> List.copyOf(saved));
when(repository.activeItemsByPallet(801L)).thenReturn(List.of());
when(repository.activePalletItem(eq(1L), eq(801L), anyLong())).thenReturn(null);
when(repository.inbound(9L, 10L)).thenReturn(inbound(9L, 10L));
when(dropshipPallets.sealedPalletLines(801L)).thenReturn(List.of(first, second));
doReturn(List.of(
new DropshipPalletizationMapper.PalletMetrics(801L, 20, new BigDecimal("197"),
new BigDecimal("180"), new BigDecimal("3.365"))))
.when(dropshipPallets).physicalMetricsByPalletIds(any());
when(repository.touch(eq(1L), anyInt(), eq(7L), eq("u"))).thenAnswer(invocation -> {
int expected = invocation.getArgument(1);
if (expected != version.get()) return 0;
version.incrementAndGet();
return 1;
});
doAnswer(invocation -> {
LoadingPlanItemEntity item = invocation.getArgument(0);
item.setId(800L + saved.size());
saved.add(item);
return null;
}).when(repository).insert(any(LoadingPlanItemEntity.class));
Plan result = service.assignPallet(1L,
new AssignPalletRequest(801L, 0, "assign-multi-sku-pallet"), 7L, "u");
assertThat(saved).hasSize(2);
assertThat(saved).extracting(LoadingPlanItemEntity::getInboundDetailId)
.containsExactly(20L, 21L);
assertThat(saved).extracting(LoadingPlanItemEntity::getVolumeCbm)
.containsExactly(new BigDecimal("3.365000"), new BigDecimal("0.000000"));
assertThat(result.items()).singleElement().satisfies(item -> {
assertThat(item.getPackageCount()).isEqualTo(20);
assertThat(item.getGrossWeightKg()).isEqualByComparingTo("197");
assertThat(item.getVolumeCbm()).isEqualByComparingTo("3.365");
});
}
@Test
void recommendSkipsStalePalletCandidateInsteadOfFailingTheWholeRecommendation() {
Candidate dropship = candidate(9L, 10L, 20L, 30L, 10);