fix: align PDA inventory with pallet location blocks

This commit is contained in:
2026-09-04 12:28:47 +08:00
parent b697e4c9f0
commit 8099343fca
7 changed files with 153 additions and 72 deletions
@@ -46,13 +46,6 @@ public interface WarehousePalletLocationClient {
ApiResponse<BlockResult> releaseBlock(@RequestHeader("X-Internal-Service-Token") String token, ApiResponse<BlockResult> releaseBlock(@RequestHeader("X-Internal-Service-Token") String token,
@RequestBody BlockReleaseRequest request); @RequestBody BlockReleaseRequest request);
@GetMapping("/pallet-locations/occupied-resources")
ApiResponse<List<OccupiedResource>> occupiedResources(
@RequestHeader("X-Internal-Service-Token") String token,
@RequestParam("tenantId") long tenantId,
@RequestParam("sourceService") String sourceService,
@RequestParam("businessId") long businessId);
record BlockReserveRequest(long warehouseId, String sourceService, long businessId, record BlockReserveRequest(long warehouseId, String sourceService, long businessId,
String inboundCode, int requiredSlotCount, String idempotencyKey, String inboundCode, int requiredSlotCount, String idempotencyKey,
long operatorId, LocalDateTime expiresAt) {} long operatorId, LocalDateTime expiresAt) {}
@@ -65,9 +58,6 @@ public interface WarehousePalletLocationClient {
record BlockResource(long blockId, long reservationId, long locationId, String locationCode, record BlockResource(long blockId, long reservationId, long locationId, String locationCode,
int slotNo, boolean sharedOverflow, String resourceStatus) {} int slotNo, boolean sharedOverflow, String resourceStatus) {}
record BlockResult(long blockId, String blockStatus, List<BlockResource> resources) {} record BlockResult(long blockId, String blockStatus, List<BlockResource> resources) {}
record OccupiedResource(long reservationId, long occupancyId, int allocationNo, long warehouseId,
long palletId, long locationId, String locationCode, boolean sharedOverflow,
int allocatedPackageCount) {}
record PalletSnapshot(long palletId, String palletCode, String palletType, record PalletSnapshot(long palletId, String palletCode, String palletType,
BigDecimal capacityCbm, BigDecimal tareWeightKg) {} BigDecimal capacityCbm, BigDecimal tareWeightKg) {}
} }
@@ -92,6 +92,7 @@ public final class LoadingPickDtos {
public record PickItem(Long id, Long loadingPlanItemId, Long tenantId, Long inboundId, Long inboundDetailId, public record PickItem(Long id, Long loadingPlanItemId, Long tenantId, Long inboundId, Long inboundDetailId,
String orderNo, String inboundCode, String cargoName, List<String> boxCodes, String orderNo, String inboundCode, String cargoName, List<String> boxCodes,
List<InventoryResource> inventoryResources,
Integer plannedPackageCount, Integer pickedPackageCount, Integer plannedPackageCount, Integer pickedPackageCount,
String itemStatus, Integer version) { String itemStatus, Integer version) {
} }
@@ -131,9 +132,10 @@ public final class LoadingPickDtos {
Integer itemCount, Integer pendingItemCount) { Integer itemCount, Integer pendingItemCount) {
} }
public record InventoryResource(Long reservationId, Long occupancyId, Long palletId, public record InventoryResource(Long blockId, Integer slotNo, Long reservationId,
Long weightResultId, Integer palletSequence, Long palletId,
Long locationId, String locationCode, Boolean sharedOverflow, Long locationId, String locationCode, Boolean sharedOverflow,
Integer availablePackageCount) { String slotStatus, Integer availablePackageCount) {
} }
public record InboundInventory(Long inboundId, Integer availablePackageCount, public record InboundInventory(Long inboundId, Integer availablePackageCount,
@@ -1,9 +1,7 @@
package com.baiyue.firstleg.service; package com.baiyue.firstleg.service;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.common.core.api.ResultCode; import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException; import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.client.WarehousePalletLocationClient;
import com.baiyue.firstleg.dto.LoadingPickDtos.PickItem; import com.baiyue.firstleg.dto.LoadingPickDtos.PickItem;
import com.baiyue.firstleg.dto.LoadingPickDtos.PickRequest; import com.baiyue.firstleg.dto.LoadingPickDtos.PickRequest;
import com.baiyue.firstleg.dto.LoadingPickDtos.PartialPickRequest; import com.baiyue.firstleg.dto.LoadingPickDtos.PartialPickRequest;
@@ -34,6 +32,7 @@ import com.baiyue.firstleg.entity.LoadingPickTaskEntity;
import com.baiyue.firstleg.entity.LoadingPlanItemEntity; import com.baiyue.firstleg.entity.LoadingPlanItemEntity;
import com.baiyue.firstleg.entity.LoadingPlanHistoryEntity; import com.baiyue.firstleg.entity.LoadingPlanHistoryEntity;
import com.baiyue.firstleg.entity.InboundStatusHistoryEntity; import com.baiyue.firstleg.entity.InboundStatusHistoryEntity;
import com.baiyue.firstleg.entity.InboundPalletAllocationEntity;
import com.baiyue.firstleg.entity.ContainerBookingEntity; import com.baiyue.firstleg.entity.ContainerBookingEntity;
import com.baiyue.firstleg.loading.mapper.LoadingInboundMapper; import com.baiyue.firstleg.loading.mapper.LoadingInboundMapper;
import com.baiyue.firstleg.loading.service.LoadingDeliveryService; import com.baiyue.firstleg.loading.service.LoadingDeliveryService;
@@ -46,12 +45,11 @@ import com.baiyue.firstleg.mapper.InboundStatusHistoryMapper;
import com.baiyue.firstleg.mapper.LoadingPickItemMapper.CartonLabelRow; import com.baiyue.firstleg.mapper.LoadingPickItemMapper.CartonLabelRow;
import com.baiyue.firstleg.mapper.LoadingPickItemMapper; import com.baiyue.firstleg.mapper.LoadingPickItemMapper;
import com.baiyue.firstleg.repository.LoadingPickRepository; import com.baiyue.firstleg.repository.LoadingPickRepository;
import com.baiyue.firstleg.repository.InboundPalletAllocationRepository;
import com.baiyue.firstleg.repository.LoadingCompletionCommandRepository; import com.baiyue.firstleg.repository.LoadingCompletionCommandRepository;
import com.baiyue.firstleg.repository.SmartLoadingRepository; import com.baiyue.firstleg.repository.SmartLoadingRepository;
import com.baiyue.firstleg.oceanbooking.repository.OceanBookingRepository; import com.baiyue.firstleg.oceanbooking.repository.OceanBookingRepository;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import feign.FeignException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DuplicateKeyException; import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate; import org.springframework.transaction.support.TransactionTemplate;
@@ -80,8 +78,6 @@ import com.baomidou.mybatisplus.core.toolkit.IdWorker;
@Service @Service
public class LoadingPickService { public class LoadingPickService {
private static final Logger log = LoggerFactory.getLogger(LoadingPickService.class); private static final Logger log = LoggerFactory.getLogger(LoadingPickService.class);
private static final long PLATFORM_WAREHOUSE_RESOURCE_TENANT_ID = 0L;
private static final String SOURCE_SERVICE = "firstleg-service";
@Resource @Resource
private LoadingPickRepository repository; private LoadingPickRepository repository;
@Resource @Resource
@@ -107,9 +103,7 @@ public class LoadingPickService {
@Resource @Resource
private PdaPickSettlementService settlementService; private PdaPickSettlementService settlementService;
@Resource @Resource
private WarehousePalletLocationClient warehouse; private InboundPalletAllocationRepository palletAllocations;
@Value("${baiyue.security.internal-service-token:test-internal-service-token}")
private String internalToken;
/** Opens the field task after planning and ocean release; the real container number may be filled later. */ /** Opens the field task after planning and ocean release; the real container number may be filled later. */
@Transactional @Transactional
@@ -906,14 +900,19 @@ public class LoadingPickService {
? repository.items(task.getId()) : repository.items(task.getId(), warehouseId); ? repository.items(task.getId()) : repository.items(task.getId(), warehouseId);
Map<Long, List<CartonLabelRow>> cartonsByItem = repository.cartonLabels(task.getId()).stream() Map<Long, List<CartonLabelRow>> cartonsByItem = repository.cartonLabels(task.getId()).stream()
.collect(Collectors.groupingBy(CartonLabelRow::getPickItemId)); .collect(Collectors.groupingBy(CartonLabelRow::getPickItemId));
Map<Long, Long> tenantByInbound = tenantByInbound(task.getId());
Map<Long, List<InventoryResource>> inventoryByInbound = inventoryByInbound(
booking.getWarehouseId(), source, tenantByInbound);
List<PickItem> items = source.stream().map(item -> { List<PickItem> items = source.stream().map(item -> {
List<CartonLabelRow> cartons = cartonsByItem.getOrDefault(item.getId(), List.of()); List<CartonLabelRow> cartons = cartonsByItem.getOrDefault(item.getId(), List.of());
CartonLabelRow first = cartons.isEmpty() ? null : cartons.get(0); CartonLabelRow first = cartons.isEmpty() ? null : cartons.get(0);
return new PickItem(item.getId(), item.getLoadingPlanItemId(), first == null ? null : first.getTenantId(), Long tenantId = first == null ? tenantByInbound.get(item.getInboundId()) : first.getTenantId();
return new PickItem(item.getId(), item.getLoadingPlanItemId(), tenantId,
item.getInboundId(), item.getInboundId(),
item.getInboundDetailId(), first == null ? null : first.getOrderNo(), item.getInboundDetailId(), first == null ? null : first.getOrderNo(),
first == null ? null : first.getInboundCode(), first == null ? null : first.getCargoName(), first == null ? null : first.getInboundCode(), first == null ? null : first.getCargoName(),
cartons.stream().map(CartonLabelRow::getBoxCode).toList(), item.getPlannedPackageCount(), cartons.stream().map(CartonLabelRow::getBoxCode).toList(),
inventoryByInbound.getOrDefault(item.getInboundId(), List.of()), item.getPlannedPackageCount(),
item.getPickedPackageCount(), item.getItemStatus(), item.getVersion()); item.getPickedPackageCount(), item.getItemStatus(), item.getVersion());
}).toList(); }).toList();
LoadingPickAssignmentEntity assigned = repository.assignment(task.getId()); LoadingPickAssignmentEntity assigned = repository.assignment(task.getId());
@@ -1036,17 +1035,11 @@ public class LoadingPickService {
private InventorySnapshot buildInventorySnapshot(LoadingPickTaskEntity task, long warehouseId, private InventorySnapshot buildInventorySnapshot(LoadingPickTaskEntity task, long warehouseId,
List<LoadingPickItemEntity> items) { List<LoadingPickItemEntity> items) {
Map<Long, List<InventoryResource>> inventoryByInbound = inventoryByInbound(
warehouseId, items, tenantByInbound(task.getId()));
List<InboundInventory> inbounds = items.stream().map(LoadingPickItemEntity::getInboundId) List<InboundInventory> inbounds = items.stream().map(LoadingPickItemEntity::getInboundId)
.filter(Objects::nonNull).distinct().sorted().map(inboundId -> { .filter(Objects::nonNull).distinct().sorted().map(inboundId -> {
List<WarehousePalletLocationClient.OccupiedResource> current = occupiedResources(inboundId); List<InventoryResource> resources = inventoryByInbound.getOrDefault(inboundId, List.of());
List<InventoryResource> resources = current.stream()
.filter(resource -> resource.warehouseId() == warehouseId)
.sorted(Comparator.comparingLong(WarehousePalletLocationClient.OccupiedResource::reservationId)
.thenComparingLong(WarehousePalletLocationClient.OccupiedResource::occupancyId))
.map(resource -> new InventoryResource(resource.reservationId(), resource.occupancyId(),
resource.palletId(), resource.locationId(), resource.locationCode(),
resource.sharedOverflow(), resource.allocatedPackageCount()))
.toList();
int available = resources.stream().mapToInt(InventoryResource::availablePackageCount).sum(); int available = resources.stream().mapToInt(InventoryResource::availablePackageCount).sum();
return new InboundInventory(inboundId, available, resources); return new InboundInventory(inboundId, available, resources);
}).toList(); }).toList();
@@ -1057,20 +1050,51 @@ public class LoadingPickService {
LocalDateTime.now(), token, inbounds); LocalDateTime.now(), token, inbounds);
} }
private List<WarehousePalletLocationClient.OccupiedResource> occupiedResources(long inboundId) { private Map<Long, Long> tenantByInbound(long taskId) {
ApiResponse<List<WarehousePalletLocationClient.OccupiedResource>> response; return repository.inboundOwners(taskId).stream().collect(Collectors.toMap(
try { LoadingPickItemMapper.InboundOwner::inboundId,
response = warehouse.occupiedResources(internalToken, PLATFORM_WAREHOUSE_RESOURCE_TENANT_ID, LoadingPickItemMapper.InboundOwner::tenantId,
SOURCE_SERVICE, inboundId); (left, right) -> left));
} catch (FeignException exception) { }
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "仓储实时库存查询失败,请刷新后重试");
private Map<Long, List<InventoryResource>> inventoryByInbound(long warehouseId,
List<LoadingPickItemEntity> items,
Map<Long, Long> tenantByInbound) {
Map<Long, List<InventoryResource>> result = new HashMap<>();
for (Long inboundId : items.stream().map(LoadingPickItemEntity::getInboundId)
.filter(Objects::nonNull).distinct().toList()) {
Long tenantId = tenantByInbound.get(inboundId);
if (tenantId == null) throw conflict("PDA任务缺少入库单客户归属");
List<InventoryResource> resources = palletAllocations.list(tenantId, inboundId).stream()
.filter(resource -> Objects.equals(resource.getWarehouseId(), warehouseId))
.filter(resource -> "CONFIRMED".equals(resource.getAllocationStatus()))
.sorted(Comparator.comparing(InboundPalletAllocationEntity::getBlockId,
Comparator.nullsLast(Long::compareTo))
.thenComparing(InboundPalletAllocationEntity::getSlotNo,
Comparator.nullsLast(Integer::compareTo)))
.map(LoadingPickService::inventoryResource)
.toList();
result.put(inboundId, resources);
} }
if (response == null || !response.successful() || response.data() == null) { return result;
ResultCode code = response == null ? ResultCode.SERVICE_UNAVAILABLE : ResultCode.of(response.code()); }
throw new BizException(code, response == null || response.message() == null
? "仓储实时库存查询失败,请刷新后重试" : response.message()); private static InventoryResource inventoryResource(InboundPalletAllocationEntity resource) {
if (resource.getBlockId() == null || resource.getSlotNo() == null
|| resource.getReservationId() == null || resource.getLocationId() == null) {
throw conflict("入库单库位块快照不完整,请重新确认测量入库");
} }
return response.data(); return new InventoryResource(resource.getBlockId(), resource.getSlotNo(),
resource.getReservationId(), resource.getWeightResultId(), resource.getPalletSequence(),
resource.getPalletId(), resource.getLocationId(), resource.getLocationCode(),
Boolean.TRUE.equals(resource.getSharedOverflow()), resource.getSlotStatus(), available(resource));
}
private static int available(InboundPalletAllocationEntity resource) {
int allocated = count(resource.getAllocatedPackageCount());
int picked = count(resource.getPickedPackageCount());
if (picked > allocated) throw conflict("整票库位槽已拣箱数超过分配箱数");
return allocated - picked;
} }
private static String inventoryToken(LoadingPickTaskEntity task, long warehouseId, private static String inventoryToken(LoadingPickTaskEntity task, long warehouseId,
@@ -1080,8 +1104,9 @@ public class LoadingPickService {
for (InboundInventory inbound : inbounds) { for (InboundInventory inbound : inbounds) {
canonical.append('|').append(inbound.inboundId()).append('|').append(inbound.availablePackageCount()); canonical.append('|').append(inbound.inboundId()).append('|').append(inbound.availablePackageCount());
for (InventoryResource resource : inbound.resources()) { for (InventoryResource resource : inbound.resources()) {
canonical.append('|').append(resource.reservationId()).append('|').append(resource.occupancyId()) canonical.append('|').append(resource.blockId()).append('|').append(resource.slotNo())
.append('|').append(resource.locationId()).append('|').append(resource.availablePackageCount()); .append('|').append(resource.reservationId()).append('|').append(resource.locationId())
.append('|').append(resource.availablePackageCount());
} }
} }
return hash(canonical.toString()); return hash(canonical.toString());
@@ -201,7 +201,7 @@ components:
items: {$ref: '#/components/schemas/PickItem'} items: {$ref: '#/components/schemas/PickItem'}
PickItem: PickItem:
type: object type: object
required: [id, loadingPlanItemId, tenantId, inboundId, inboundDetailId, boxCodes, plannedPackageCount, pickedPackageCount, itemStatus, version] required: [id, loadingPlanItemId, tenantId, inboundId, inboundDetailId, boxCodes, inventoryResources, plannedPackageCount, pickedPackageCount, itemStatus, version]
properties: properties:
id: {type: integer, format: int64} id: {type: integer, format: int64}
loadingPlanItemId: {type: integer, format: int64} loadingPlanItemId: {type: integer, format: int64}
@@ -215,6 +215,10 @@ components:
type: array type: array
description: Stable carton label codes; one successful scan represents one carton description: Stable carton label codes; one successful scan represents one carton
items: {type: string} items: {type: string}
inventoryResources:
type: array
description: Current authoritative whole-block slot snapshots for this inbound; never projected from legacy occupancy rows
items: {$ref: '#/components/schemas/InventoryResource'}
plannedPackageCount: {type: integer, minimum: 1} plannedPackageCount: {type: integer, minimum: 1}
pickedPackageCount: {type: integer, minimum: 0} pickedPackageCount: {type: integer, minimum: 0}
itemStatus: {type: string, enum: [PENDING, PICKING, PICKED, PARTIAL, SKIPPED]} itemStatus: {type: string, enum: [PENDING, PICKING, PICKED, PARTIAL, SKIPPED]}
@@ -334,12 +338,16 @@ components:
items: {$ref: '#/components/schemas/InventoryResource'} items: {$ref: '#/components/schemas/InventoryResource'}
InventoryResource: InventoryResource:
type: object type: object
required: [reservationId, occupancyId, palletId, locationId, sharedOverflow, availablePackageCount] required: [blockId, slotNo, reservationId, locationId, sharedOverflow, slotStatus, availablePackageCount]
properties: properties:
blockId: {type: integer, format: int64}
slotNo: {type: integer, minimum: 1}
reservationId: {type: integer, format: int64} reservationId: {type: integer, format: int64}
occupancyId: {type: integer, format: int64} weightResultId: {type: integer, format: int64, nullable: true}
palletId: {type: integer, format: int64} palletSequence: {type: integer, minimum: 1, nullable: true}
palletId: {type: integer, format: int64, nullable: true}
locationId: {type: integer, format: int64} locationId: {type: integer, format: int64}
locationCode: {type: string, nullable: true} locationCode: {type: string, nullable: true}
sharedOverflow: {type: boolean} sharedOverflow: {type: boolean}
availablePackageCount: {type: integer, minimum: 1} slotStatus: {type: string, enum: [RESERVED_EMPTY, BOUND]}
availablePackageCount: {type: integer, minimum: 0}
@@ -52,6 +52,8 @@ class PdaConvergenceOpenApiContractTest {
String batchItemRequest = yaml.substring( String batchItemRequest = yaml.substring(
yaml.indexOf(" BatchItemRequest:"), yaml.indexOf(" BatchItemRequest:"),
yaml.indexOf(" ReviewRequest:")); yaml.indexOf(" ReviewRequest:"));
String inventoryResource = yaml.substring(
yaml.indexOf(" InventoryResource:"));
assertThat(taskLookup) assertThat(taskLookup)
.contains("name: cabinetNo", "name: loadingPlanId") .contains("name: cabinetNo", "name: loadingPlanId")
.doesNotContain("name: containerNo"); .doesNotContain("name: containerNo");
@@ -64,5 +66,10 @@ class PdaConvergenceOpenApiContractTest {
.doesNotContain("containerNo:"); .doesNotContain("containerNo:");
assertThat(batchItemRequest) assertThat(batchItemRequest)
.doesNotContain("releaseMode:", "reservationIds:", "locationIds:"); .doesNotContain("releaseMode:", "reservationIds:", "locationIds:");
assertThat(inventoryResource)
.contains("blockId:", "slotNo:", "reservationId:", "weightResultId:", "slotStatus:")
.doesNotContain("occupancyId:");
assertThat(yaml).contains("inventoryResources:",
"never projected from legacy occupancy rows");
} }
} }
@@ -11,8 +11,7 @@ import com.baiyue.firstleg.dto.LoadingPickDtos.ClaimRequest;
import com.baiyue.firstleg.dto.LoadingPickDtos.AssignRequest; import com.baiyue.firstleg.dto.LoadingPickDtos.AssignRequest;
import com.baiyue.firstleg.dto.LoadingPickDtos.CompleteLoadingRequest; import com.baiyue.firstleg.dto.LoadingPickDtos.CompleteLoadingRequest;
import com.baiyue.firstleg.dto.LoadingPickDtos.Task; import com.baiyue.firstleg.dto.LoadingPickDtos.Task;
import com.baiyue.firstleg.client.WarehousePalletLocationClient; import com.baiyue.firstleg.entity.InboundPalletAllocationEntity;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.firstleg.entity.LoadingPickAssignmentEntity; import com.baiyue.firstleg.entity.LoadingPickAssignmentEntity;
import com.baiyue.firstleg.entity.LoadingPickHistoryEntity; import com.baiyue.firstleg.entity.LoadingPickHistoryEntity;
import com.baiyue.firstleg.entity.LoadingPickItemEntity; import com.baiyue.firstleg.entity.LoadingPickItemEntity;
@@ -33,6 +32,7 @@ import com.baiyue.firstleg.loading.coload.model.ColoadGroupModels.Member;
import com.baiyue.firstleg.mapper.InboundStatusHistoryMapper; import com.baiyue.firstleg.mapper.InboundStatusHistoryMapper;
import com.baiyue.firstleg.mapper.LoadingPickItemMapper; import com.baiyue.firstleg.mapper.LoadingPickItemMapper;
import com.baiyue.firstleg.repository.LoadingPickRepository; import com.baiyue.firstleg.repository.LoadingPickRepository;
import com.baiyue.firstleg.repository.InboundPalletAllocationRepository;
import com.baiyue.firstleg.repository.LoadingCompletionCommandRepository; import com.baiyue.firstleg.repository.LoadingCompletionCommandRepository;
import com.baiyue.firstleg.repository.SmartLoadingRepository; import com.baiyue.firstleg.repository.SmartLoadingRepository;
import com.baiyue.firstleg.oceanbooking.repository.OceanBookingRepository; import com.baiyue.firstleg.oceanbooking.repository.OceanBookingRepository;
@@ -100,7 +100,7 @@ class LoadingPickServiceTest {
@MockBean @MockBean
private PdaPickSettlementService settlementService; private PdaPickSettlementService settlementService;
@MockBean @MockBean
private WarehousePalletLocationClient warehouse; private InboundPalletAllocationRepository palletAllocations;
@Resource @Resource
private LoadingPickService service; private LoadingPickService service;
@@ -122,6 +122,10 @@ class LoadingPickServiceTest {
when(completionCommands.insert(any())).thenReturn(1); when(completionCommands.insert(any())).thenReturn(1);
when(completionCommands.renew(anyLong(), anyString(), any(java.time.LocalDateTime.class))).thenReturn(true); when(completionCommands.renew(anyLong(), anyString(), any(java.time.LocalDateTime.class))).thenReturn(true);
when(repository.acceptCompletion(anyLong(), anyInt())).thenReturn(1); when(repository.acceptCompletion(anyLong(), anyInt())).thenReturn(1);
when(repository.inboundOwners(anyLong())).thenReturn(List.of(
new LoadingPickItemMapper.InboundOwner(101L, 50L)));
when(repository.inbound(anyLong(), anyLong())).thenAnswer(invocation ->
new LoadingInboundMapper.Snapshot(invocation.getArgument(1), invocation.getArgument(0), "LOADING", 1));
} }
@Test @Test
@@ -437,6 +441,7 @@ class LoadingPickServiceTest {
when(repository.allTaskItemsResolved(20L)).thenReturn(false); when(repository.allTaskItemsResolved(20L)).thenReturn(false);
when(repository.inboundOwners(20L)).thenReturn(List.of( when(repository.inboundOwners(20L)).thenReturn(List.of(
new LoadingPickItemMapper.InboundOwner(101L, 50L))); new LoadingPickItemMapper.InboundOwner(101L, 50L)));
when(palletAllocations.list(101L, 50L)).thenReturn(List.of(allocation(50L, 2)));
when(repository.inbound(101L, 50L)).thenReturn( when(repository.inbound(101L, 50L)).thenReturn(
new LoadingInboundMapper.Snapshot(50L, 101L, "LOADING", 7)); new LoadingInboundMapper.Snapshot(50L, 101L, "LOADING", 7));
var result = service.task(20L, 101L); var result = service.task(20L, 101L);
@@ -449,6 +454,14 @@ class LoadingPickServiceTest {
assertThat(result.sourceType()).isEqualTo("SIMULATION"); assertThat(result.sourceType()).isEqualTo("SIMULATION");
assertThat(result.recordStatus()).isEqualTo("ACTIVE"); assertThat(result.recordStatus()).isEqualTo("ACTIVE");
assertThat(result.mergedIntoContainerBookingId()).isNull(); assertThat(result.mergedIntoContainerBookingId()).isNull();
assertThat(result.items().get(0).inventoryResources()).singleElement().satisfies(resource -> {
assertThat(resource.blockId()).isEqualTo(71L);
assertThat(resource.slotNo()).isEqualTo(1);
assertThat(resource.reservationId()).isEqualTo(72L);
assertThat(resource.locationId()).isEqualTo(74L);
assertThat(resource.availablePackageCount()).isEqualTo(2);
});
verify(palletAllocations).list(101L, 50L);
verify(completionCommands, never()).latest(20L); verify(completionCommands, never()).latest(20L);
} }
@@ -458,7 +471,9 @@ class LoadingPickServiceTest {
when(repository.task(20L)).thenReturn(task); when(repository.task(20L)).thenReturn(task);
when(repository.items(20L, 101L)).thenReturn(List.of(item())); when(repository.items(20L, 101L)).thenReturn(List.of(item()));
when(repository.cartonLabels(20L)).thenReturn(List.of()); when(repository.cartonLabels(20L)).thenReturn(List.of());
when(repository.inboundOwners(20L)).thenReturn(List.of()); when(repository.inboundOwners(20L)).thenReturn(List.of(
new LoadingPickItemMapper.InboundOwner(101L, 50L)));
when(palletAllocations.list(101L, 50L)).thenReturn(List.of());
doThrow(new IllegalStateException("completion command schema unavailable")) doThrow(new IllegalStateException("completion command schema unavailable"))
.when(completionCommands).latest(20L); .when(completionCommands).latest(20L);
@@ -1123,10 +1138,17 @@ class LoadingPickServiceTest {
when(repository.allTaskItemsResolved(20L)).thenReturn(true); when(repository.allTaskItemsResolved(20L)).thenReturn(true);
when(repository.touchTask(20L, 0)).thenReturn(1); when(repository.touchTask(20L, 0)).thenReturn(1);
when(repository.allPlanItemsPicked(10L)).thenReturn(true); when(repository.allPlanItemsPicked(10L)).thenReturn(true);
when(warehouse.occupiedResources(any(), any(Long.class), any(), any(Long.class))).thenReturn( when(repository.inboundOwners(20L)).thenReturn(List.of(
ApiResponse.success(List.of(new WarehousePalletLocationClient.OccupiedResource( new LoadingPickItemMapper.InboundOwner(1L, 50L),
71L, 72L, 0, 101L, 73L, 74L, "A-01", false, 2)))); new LoadingPickItemMapper.InboundOwner(1L, 51L)));
String snapshotToken = service.inventorySnapshot(20L, 101L).snapshotToken(); when(palletAllocations.list(anyLong(), anyLong())).thenAnswer(invocation ->
List.of(allocation(invocation.getArgument(1), 2)));
var snapshot = service.inventorySnapshot(20L, 101L);
String snapshotToken = snapshot.snapshotToken();
assertThat(snapshot.inbounds()).hasSize(2);
assertThat(snapshot.inbounds().get(0).resources().get(0))
.extracting("blockId", "slotNo", "reservationId", "locationId", "availablePackageCount")
.containsExactly(71L, 1, 72L, 74L, 2);
BatchRequest request = new BatchRequest(101L, 10L, 99L, 0, snapshotToken, List.of( BatchRequest request = new BatchRequest(101L, 10L, 99L, 0, snapshotToken, List.of(
new BatchItemRequest(30L, "PICK", 2, null, 0, null), new BatchItemRequest(30L, "PICK", 2, null, 0, null),
new BatchItemRequest(31L, "SKIP", null, null, 0, "damaged carton"))); new BatchItemRequest(31L, "SKIP", null, null, 0, "damaged carton")));
@@ -1164,9 +1186,10 @@ class LoadingPickServiceTest {
when(repository.touchTask(20L, 0)).thenReturn(1); when(repository.touchTask(20L, 0)).thenReturn(1);
when(smartLoadingRepository.startPicking(10L, 9L, "operator")).thenReturn(1); when(smartLoadingRepository.startPicking(10L, 9L, "operator")).thenReturn(1);
when(smartLoadingRepository.operation("PDA-START-20")).thenReturn(existingStart); when(smartLoadingRepository.operation("PDA-START-20")).thenReturn(existingStart);
when(warehouse.occupiedResources(any(), any(Long.class), any(), any(Long.class))).thenReturn( when(repository.inboundOwners(20L)).thenReturn(List.of(
ApiResponse.success(List.of(new WarehousePalletLocationClient.OccupiedResource( new LoadingPickItemMapper.InboundOwner(1L, 50L)));
71L, 72L, 0, 101L, 73L, 74L, "A-01", false, 100)))); when(palletAllocations.list(anyLong(), anyLong())).thenAnswer(invocation ->
List.of(allocation(invocation.getArgument(1), 100)));
String snapshotToken = service.inventorySnapshot(20L, 101L).snapshotToken(); String snapshotToken = service.inventorySnapshot(20L, 101L).snapshotToken();
BatchRequest request = new BatchRequest(101L, 10L, 99L, 0, snapshotToken, List.of( BatchRequest request = new BatchRequest(101L, 10L, 99L, 0, snapshotToken, List.of(
new BatchItemRequest(30L, "PICK", 100, null, 0, null))); new BatchItemRequest(30L, "PICK", 100, null, 0, null)));
@@ -1215,9 +1238,10 @@ class LoadingPickServiceTest {
when(repository.assignment(20L)).thenReturn(assignment()); when(repository.assignment(20L)).thenReturn(assignment());
when(repository.items(20L)).thenReturn(List.of(picked)); when(repository.items(20L)).thenReturn(List.of(picked));
when(repository.items(20L, 101L)).thenReturn(List.of(picked)); when(repository.items(20L, 101L)).thenReturn(List.of(picked));
when(warehouse.occupiedResources(any(), any(Long.class), any(), any(Long.class))).thenReturn( when(repository.inboundOwners(20L)).thenReturn(List.of(
ApiResponse.success(List.of(new WarehousePalletLocationClient.OccupiedResource( new LoadingPickItemMapper.InboundOwner(1L, 50L)));
71L, 72L, 0, 101L, 73L, 74L, "A-01", false, 2)))); when(palletAllocations.list(anyLong(), anyLong())).thenAnswer(invocation ->
List.of(allocation(invocation.getArgument(1), 2)));
BatchRequest request = new BatchRequest(101L, 10L, 99L, 0, "0".repeat(64), List.of( BatchRequest request = new BatchRequest(101L, 10L, 99L, 0, "0".repeat(64), List.of(
new BatchItemRequest(30L, "PICK", 2, null, 0, null))); new BatchItemRequest(30L, "PICK", 2, null, 0, null)));
@@ -1258,9 +1282,10 @@ class LoadingPickServiceTest {
when(repository.pick(30L, 2, 0, 101L)).thenReturn(1); when(repository.pick(30L, 2, 0, 101L)).thenReturn(1);
when(repository.allTaskItemsResolved(20L)).thenReturn(true); when(repository.allTaskItemsResolved(20L)).thenReturn(true);
when(repository.touchTask(20L, 0)).thenReturn(1); when(repository.touchTask(20L, 0)).thenReturn(1);
when(warehouse.occupiedResources(any(), any(Long.class), any(), any(Long.class))).thenReturn( when(repository.inboundOwners(20L)).thenReturn(List.of(
ApiResponse.success(List.of(new WarehousePalletLocationClient.OccupiedResource( new LoadingPickItemMapper.InboundOwner(1L, 50L)));
71L, 72L, 0, 101L, 73L, 74L, "A-01", false, 2)))); when(palletAllocations.list(anyLong(), anyLong())).thenAnswer(invocation ->
List.of(allocation(invocation.getArgument(1), 2)));
String snapshotToken = service.inventorySnapshot(20L, 101L).snapshotToken(); String snapshotToken = service.inventorySnapshot(20L, 101L).snapshotToken();
BatchItemRequest line = new BatchItemRequest(30L, "PICK", 2, null, 0, null); BatchItemRequest line = new BatchItemRequest(30L, "PICK", 2, null, 0, null);
@@ -1378,6 +1403,27 @@ class LoadingPickServiceTest {
return item; return item;
} }
private static InboundPalletAllocationEntity allocation(long inboundId, int packageCount) {
InboundPalletAllocationEntity allocation = new InboundPalletAllocationEntity();
allocation.setId(72L);
allocation.setTenantId(1L);
allocation.setInboundId(inboundId);
allocation.setBlockId(71L);
allocation.setSlotNo(1);
allocation.setAllocationNo(1);
allocation.setWarehouseId(101L);
allocation.setPalletId(73L);
allocation.setLocationId(74L);
allocation.setLocationCode("A-01");
allocation.setSharedOverflow(false);
allocation.setReservationId(72L);
allocation.setSlotStatus("BOUND");
allocation.setAllocationStatus("CONFIRMED");
allocation.setAllocatedPackageCount(packageCount);
allocation.setPickedPackageCount(0);
return allocation;
}
private static LoadingPickAssignmentEntity assignment() { private static LoadingPickAssignmentEntity assignment() {
LoadingPickAssignmentEntity assignment = new LoadingPickAssignmentEntity(); LoadingPickAssignmentEntity assignment = new LoadingPickAssignmentEntity();
assignment.setPickTaskId(20L); assignment.setPickTaskId(20L);
@@ -124,8 +124,10 @@ function inventorySignature(snapshot) {
inboundId: id(inbound?.inboundId), inboundId: id(inbound?.inboundId),
availablePackageCount: Number(inbound?.availablePackageCount), availablePackageCount: Number(inbound?.availablePackageCount),
resources: (inbound?.resources || []).map((resource) => ({ resources: (inbound?.resources || []).map((resource) => ({
reservationId: id(resource?.reservationId), occupancyId: id(resource?.occupancyId), blockId: id(resource?.blockId), slotNo: Number(resource?.slotNo),
palletId: id(resource?.palletId), locationId: id(resource?.locationId), reservationId: id(resource?.reservationId), weightResultId: id(resource?.weightResultId),
palletSequence: Number(resource?.palletSequence), palletId: id(resource?.palletId),
locationId: id(resource?.locationId), slotStatus: resource?.slotStatus || null,
sharedOverflow: resource?.sharedOverflow === true, sharedOverflow: resource?.sharedOverflow === true,
availablePackageCount: Number(resource?.availablePackageCount), availablePackageCount: Number(resource?.availablePackageCount),
})), })),
@@ -622,8 +624,9 @@ export async function run(context) {
&& id(inventory?.warehouseId) === warehouseId && id(inventory?.warehouseId) === warehouseId
&& Boolean(inventory?.capturedAt) && typeof inventory?.snapshotToken === 'string' && Boolean(inventory?.capturedAt) && typeof inventory?.snapshotToken === 'string'
&& inventory.snapshotToken.length === 64 && Number(inventoryInbound?.availablePackageCount) >= 1 && inventory.snapshotToken.length === 64 && Number(inventoryInbound?.availablePackageCount) >= 1
&& (inventoryInbound?.resources || []).every((resource) => id(resource?.reservationId) && (inventoryInbound?.resources || []).every((resource) => id(resource?.blockId)
&& id(resource?.occupancyId) && id(resource?.palletId) && id(resource?.locationId) && Number(resource?.slotNo) >= 1 && id(resource?.reservationId) && id(resource?.locationId)
&& ['RESERVED_EMPTY', 'BOUND'].includes(resource?.slotStatus)
&& Number(resource?.availablePackageCount) >= 0) && Number(resource?.availablePackageCount) >= 0)
&& Number(inventoryInbound?.availablePackageCount) === (inventoryInbound?.resources || []) && Number(inventoryInbound?.availablePackageCount) === (inventoryInbound?.resources || [])
.reduce((total, resource) => total + Number(resource?.availablePackageCount || 0), 0) .reduce((total, resource) => total + Number(resource?.availablePackageCount || 0), 0)