Merge remote-tracking branch 'origin/test' into dev

This commit is contained in:
2026-09-04 12:55:52 +08:00
15 changed files with 343 additions and 107 deletions
@@ -10450,8 +10450,22 @@ DELIMITER ;
CALL assert_phase3_final_schema();
DROP PROCEDURE IF EXISTS assert_phase3_final_schema;
-- PDA 部分拣货释放策略:NULL/AUTO 按比例释放,MANUAL + [] 表示人工明确不释放。
-- PDA 整票库位块直接使用 Firstleg 托盘分配快照;旧明细级释放策略字段不再保留。
USE baiyue_firstleg;
SET @phase3_pda_drop_release_locations = IF((SELECT COUNT(*) FROM information_schema.columns
WHERE table_schema=DATABASE() AND table_name='fl_loading_pick_item'
AND column_name='release_location_ids') > 0,
'ALTER TABLE fl_loading_pick_item DROP COLUMN release_location_ids', 'SELECT 1');
PREPARE phase3_pda_drop_release_locations_stmt FROM @phase3_pda_drop_release_locations;
EXECUTE phase3_pda_drop_release_locations_stmt;
DEALLOCATE PREPARE phase3_pda_drop_release_locations_stmt;
SET @phase3_pda_drop_release_mode = IF((SELECT COUNT(*) FROM information_schema.columns
WHERE table_schema=DATABASE() AND table_name='fl_loading_pick_item'
AND column_name='release_mode') > 0,
'ALTER TABLE fl_loading_pick_item DROP COLUMN release_mode', 'SELECT 1');
PREPARE phase3_pda_drop_release_mode_stmt FROM @phase3_pda_drop_release_mode;
EXECUTE phase3_pda_drop_release_mode_stmt;
DEALLOCATE PREPARE phase3_pda_drop_release_mode_stmt;
DROP PROCEDURE IF EXISTS phase3_pda_add_column;
DELIMITER $$
CREATE PROCEDURE phase3_pda_add_column(
@@ -10471,14 +10485,6 @@ BEGIN
END IF;
END$$
DELIMITER ;
CALL phase3_pda_add_column(
'fl_loading_pick_item', 'release_mode',
"VARCHAR(10) NULL COMMENT 'AUTO按比例释放,MANUAL按人工选择释放' AFTER item_status"
);
CALL phase3_pda_add_column(
'fl_loading_pick_item', 'release_location_ids',
"JSON NULL COMMENT '人工选择释放库位ID;空数组表示明确释放0个' AFTER release_mode"
);
CALL phase3_pda_add_column(
'fl_inbound_pallet_allocation', 'block_id',
"BIGINT UNSIGNED NOT NULL COMMENT 'Warehouse整票连续库位块ID' AFTER allocation_no"
@@ -13971,8 +13977,6 @@ CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_item', 'loading_pl
CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_item', 'pick_task_id', 'PDA拣货任务ID');
CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_item', 'picked_package_count', '已拣货箱数');
CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_item', 'planned_package_count', '计划拣货箱数;上层拆分按业务规则向上取整');
CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_item', 'release_location_ids', '人工选择释放库位ID;空数组表示明确释放0个');
CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_item', 'release_mode', 'AUTO按比例释放,MANUAL按人工选择释放');
CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_item', 'updated_at', 'PDA拣货明细最后更新时间');
CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_item', 'version', '乐观锁版本号');
CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_item_assignment', 'assigned_by', '执行领取或分配操作的用户ID');
@@ -14991,17 +14995,24 @@ CALL queue_column_comment('baiyue_warehouse', 'wh_pallet_location_transfer_opera
USE baiyue_warehouse;
CREATE TABLE IF NOT EXISTS wh_location_block (
id BIGINT UNSIGNED NOT NULL COMMENT '整票库位块ID', tenant_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
warehouse_id BIGINT UNSIGNED NOT NULL, source_service VARCHAR(40) NOT NULL,
business_id BIGINT UNSIGNED NOT NULL, inbound_code VARCHAR(40) NOT NULL,
slot_count INT UNSIGNED NOT NULL, idempotency_key VARCHAR(128) NOT NULL,
block_status VARCHAR(20) NOT NULL, expires_at DATETIME(3) NOT NULL,
last_operation_key VARCHAR(128) NULL, created_by BIGINT UNSIGNED NOT NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
version INT NOT NULL DEFAULT 0,
id BIGINT UNSIGNED NOT NULL COMMENT '整票库位块ID',
tenant_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '平台公共仓储资源域,固定为0',
warehouse_id BIGINT UNSIGNED NOT NULL COMMENT '仓库ID',
source_service VARCHAR(40) NOT NULL COMMENT '来源服务',
business_id BIGINT UNSIGNED NOT NULL COMMENT '当前归属入库单业务ID',
inbound_code VARCHAR(40) NOT NULL COMMENT '当前归属入仓码',
slot_count INT UNSIGNED NOT NULL COMMENT '整票独占槽位数',
idempotency_key VARCHAR(128) NOT NULL COMMENT '预占幂等键',
block_status VARCHAR(20) NOT NULL COMMENT '整票块状态:RESERVED、OCCUPIED或RELEASED',
expires_at DATETIME(3) NOT NULL COMMENT '预占过期时间',
last_operation_key VARCHAR(128) NULL COMMENT '最近确认、转移或释放幂等键',
created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人或系统主体ID',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
active_business_block TINYINT GENERATED ALWAYS AS
(CASE WHEN block_status IN ('RESERVED','OCCUPIED') THEN 1 ELSE NULL END) STORED,
(CASE WHEN block_status IN ('RESERVED','OCCUPIED') THEN 1 ELSE NULL END) STORED
COMMENT '同一来源入库单仅允许一个活动整票块',
PRIMARY KEY (id),
UNIQUE KEY uk_location_block_idempotency (tenant_id, source_service, idempotency_key),
UNIQUE KEY uk_location_block_active_business (tenant_id, source_service, business_id, active_business_block),
@@ -15011,15 +15022,19 @@ CREATE TABLE IF NOT EXISTS wh_location_block (
) ENGINE=InnoDB COMMENT='一票入仓码原子持有的稳定库位块';
CREATE TABLE IF NOT EXISTS wh_location_block_slot (
id BIGINT UNSIGNED NOT NULL COMMENT '槽位预占ID,对外作为reservationId',
tenant_id BIGINT UNSIGNED NOT NULL DEFAULT 0, block_id BIGINT UNSIGNED NOT NULL,
slot_no INT UNSIGNED NOT NULL, location_id BIGINT UNSIGNED NOT NULL,
location_code VARCHAR(50) NOT NULL, shared_overflow TINYINT(1) NOT NULL DEFAULT 0,
resource_status VARCHAR(20) NOT NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
version INT NOT NULL DEFAULT 0,
tenant_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '平台公共仓储资源域,固定为0',
block_id BIGINT UNSIGNED NOT NULL COMMENT '整票库位块ID',
slot_no INT UNSIGNED NOT NULL COMMENT '块内稳定槽位序号,从1开始',
location_id BIGINT UNSIGNED NOT NULL COMMENT '库位ID',
location_code VARCHAR(50) NOT NULL COMMENT '库位编码快照',
shared_overflow TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否落入同仓共享A0',
resource_status VARCHAR(20) NOT NULL COMMENT '资源状态:RESERVED、OCCUPIED或RELEASED',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
active_normal_slot TINYINT GENERATED ALWAYS AS
(CASE WHEN resource_status IN ('RESERVED','OCCUPIED') AND shared_overflow = 0 THEN 1 ELSE NULL END) STORED,
(CASE WHEN resource_status IN ('RESERVED','OCCUPIED') AND shared_overflow = 0 THEN 1 ELSE NULL END) STORED
COMMENT '普通库位活动块槽唯一标记;A0返回NULL',
PRIMARY KEY (id), UNIQUE KEY uk_location_block_slot_no (tenant_id, block_id, slot_no),
UNIQUE KEY uk_location_block_active_normal (tenant_id, location_id, active_normal_slot),
KEY idx_location_block_slot_block (tenant_id, block_id, resource_status),
@@ -46,13 +46,6 @@ public interface WarehousePalletLocationClient {
ApiResponse<BlockResult> releaseBlock(@RequestHeader("X-Internal-Service-Token") String token,
@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,
String inboundCode, int requiredSlotCount, String idempotencyKey,
long operatorId, LocalDateTime expiresAt) {}
@@ -65,9 +58,6 @@ public interface WarehousePalletLocationClient {
record BlockResource(long blockId, long reservationId, long locationId, String locationCode,
int slotNo, boolean sharedOverflow, String resourceStatus) {}
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,
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,
String orderNo, String inboundCode, String cargoName, List<String> boxCodes,
List<InventoryResource> inventoryResources,
Integer plannedPackageCount, Integer pickedPackageCount,
String itemStatus, Integer version) {
}
@@ -131,9 +132,10 @@ public final class LoadingPickDtos {
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,
Integer availablePackageCount) {
String slotStatus, Integer availablePackageCount) {
}
public record InboundInventory(Long inboundId, Integer availablePackageCount,
@@ -1,9 +1,7 @@
package com.baiyue.firstleg.service;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.common.core.api.ResultCode;
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.PickRequest;
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.LoadingPlanHistoryEntity;
import com.baiyue.firstleg.entity.InboundStatusHistoryEntity;
import com.baiyue.firstleg.entity.InboundPalletAllocationEntity;
import com.baiyue.firstleg.entity.ContainerBookingEntity;
import com.baiyue.firstleg.loading.mapper.LoadingInboundMapper;
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;
import com.baiyue.firstleg.repository.LoadingPickRepository;
import com.baiyue.firstleg.repository.InboundPalletAllocationRepository;
import com.baiyue.firstleg.repository.LoadingCompletionCommandRepository;
import com.baiyue.firstleg.repository.SmartLoadingRepository;
import com.baiyue.firstleg.oceanbooking.repository.OceanBookingRepository;
import jakarta.annotation.Resource;
import feign.FeignException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate;
@@ -80,8 +78,6 @@ import com.baomidou.mybatisplus.core.toolkit.IdWorker;
@Service
public class LoadingPickService {
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
private LoadingPickRepository repository;
@Resource
@@ -107,9 +103,7 @@ public class LoadingPickService {
@Resource
private PdaPickSettlementService settlementService;
@Resource
private WarehousePalletLocationClient warehouse;
@Value("${baiyue.security.internal-service-token:test-internal-service-token}")
private String internalToken;
private InboundPalletAllocationRepository palletAllocations;
/** Opens the field task after planning and ocean release; the real container number may be filled later. */
@Transactional
@@ -908,14 +902,19 @@ public class LoadingPickService {
? repository.items(task.getId()) : repository.items(task.getId(), warehouseId);
Map<Long, List<CartonLabelRow>> cartonsByItem = repository.cartonLabels(task.getId()).stream()
.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<CartonLabelRow> cartons = cartonsByItem.getOrDefault(item.getId(), List.of());
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.getInboundDetailId(), first == null ? null : first.getOrderNo(),
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());
}).toList();
LoadingPickAssignmentEntity assigned = repository.assignment(task.getId());
@@ -1038,17 +1037,11 @@ public class LoadingPickService {
private InventorySnapshot buildInventorySnapshot(LoadingPickTaskEntity task, long warehouseId,
List<LoadingPickItemEntity> items) {
Map<Long, List<InventoryResource>> inventoryByInbound = inventoryByInbound(
warehouseId, items, tenantByInbound(task.getId()));
List<InboundInventory> inbounds = items.stream().map(LoadingPickItemEntity::getInboundId)
.filter(Objects::nonNull).distinct().sorted().map(inboundId -> {
List<WarehousePalletLocationClient.OccupiedResource> current = occupiedResources(inboundId);
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();
List<InventoryResource> resources = inventoryByInbound.getOrDefault(inboundId, List.of());
int available = resources.stream().mapToInt(InventoryResource::availablePackageCount).sum();
return new InboundInventory(inboundId, available, resources);
}).toList();
@@ -1059,20 +1052,54 @@ public class LoadingPickService {
LocalDateTime.now(), token, inbounds);
}
private List<WarehousePalletLocationClient.OccupiedResource> occupiedResources(long inboundId) {
ApiResponse<List<WarehousePalletLocationClient.OccupiedResource>> response;
try {
response = warehouse.occupiedResources(internalToken, PLATFORM_WAREHOUSE_RESOURCE_TENANT_ID,
SOURCE_SERVICE, inboundId);
} catch (FeignException exception) {
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "仓储实时库存查询失败,请刷新后重试");
private Map<Long, Long> tenantByInbound(long taskId) {
return repository.inboundOwners(taskId).stream().collect(Collectors.toMap(
LoadingPickItemMapper.InboundOwner::inboundId,
LoadingPickItemMapper.InboundOwner::tenantId,
(left, right) -> {
if (!Objects.equals(left, right)) throw conflict("PDA任务入库单客户归属冲突");
return left;
}));
}
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) {
ResultCode code = response == null ? ResultCode.SERVICE_UNAVAILABLE : ResultCode.of(response.code());
throw new BizException(code, response == null || response.message() == null
? "仓储实时库存查询失败,请刷新后重试" : response.message());
return result;
}
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,
@@ -1082,8 +1109,9 @@ public class LoadingPickService {
for (InboundInventory inbound : inbounds) {
canonical.append('|').append(inbound.inboundId()).append('|').append(inbound.availablePackageCount());
for (InventoryResource resource : inbound.resources()) {
canonical.append('|').append(resource.reservationId()).append('|').append(resource.occupancyId())
.append('|').append(resource.locationId()).append('|').append(resource.availablePackageCount());
canonical.append('|').append(resource.blockId()).append('|').append(resource.slotNo())
.append('|').append(resource.reservationId()).append('|').append(resource.locationId())
.append('|').append(resource.availablePackageCount());
}
}
return hash(canonical.toString());
@@ -294,7 +294,7 @@ components:
items: {$ref: '#/components/schemas/PickItem'}
PickItem:
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:
id: {type: integer, format: int64}
loadingPlanItemId: {type: integer, format: int64}
@@ -308,6 +308,10 @@ components:
type: array
description: Stable carton label codes; one successful scan represents one carton
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}
pickedPackageCount: {type: integer, minimum: 0}
itemStatus: {type: string, enum: [PENDING, PICKING, PICKED, PARTIAL, SKIPPED]}
@@ -427,12 +431,16 @@ components:
items: {$ref: '#/components/schemas/InventoryResource'}
InventoryResource:
type: object
required: [reservationId, occupancyId, palletId, locationId, sharedOverflow, availablePackageCount]
required: [blockId, slotNo, reservationId, locationId, sharedOverflow, slotStatus, availablePackageCount]
properties:
blockId: {type: integer, format: int64}
slotNo: {type: integer, minimum: 1}
reservationId: {type: integer, format: int64}
occupancyId: {type: integer, format: int64}
palletId: {type: integer, format: int64}
weightResultId: {type: integer, format: int64, nullable: true}
palletSequence: {type: integer, minimum: 1, nullable: true}
palletId: {type: integer, format: int64, nullable: true}
locationId: {type: integer, format: int64}
locationCode: {type: string, nullable: true}
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(
yaml.indexOf(" BatchItemRequest:"),
yaml.indexOf(" ReviewRequest:"));
String inventoryResource = yaml.substring(
yaml.indexOf(" InventoryResource:"));
assertThat(taskLookup)
.contains("name: cabinetNo", "name: loadingPlanId")
.doesNotContain("name: containerNo");
@@ -64,5 +66,10 @@ class PdaConvergenceOpenApiContractTest {
.doesNotContain("containerNo:");
assertThat(batchItemRequest)
.doesNotContain("releaseMode:", "reservationIds:", "locationIds:");
assertThat(inventoryResource)
.contains("blockId:", "slotNo:", "reservationId:", "weightResultId:", "slotStatus:")
.doesNotContain("occupancyId:");
assertThat(yaml).contains("inventoryResources:",
"never projected from legacy occupancy rows");
}
}
@@ -92,7 +92,7 @@ class LoadingPickItemMapperContractTest {
void pdaWritesAndReadsRequireWarehouseScope() throws Exception {
Method list = LoadingPickItemMapper.class.getDeclaredMethod("selectTaskItemsWithAccount", long.class, long.class);
Method pick = LoadingPickItemMapper.class.getDeclaredMethod("pick", long.class, int.class,
String.class, String.class, int.class, long.class);
int.class, long.class);
assertThat(list.getAnnotation(Select.class).value()[0])
.contains("booking.warehouse_id = #{warehouseId}")
@@ -157,11 +157,22 @@ class PdaSplitSettlementSqlContractTest {
.contains("allocation_status = 'CONFIRMED'")
.doesNotContain("THEN 'RELEASED'");
String release = InboundPalletAllocationMapper.class.getMethod("markReleased",
String release = InboundPalletAllocationMapper.class.getMethod("markBlockReleased",
long.class, long.class, long.class)
.getAnnotation(Update.class).value()[0];
assertThat(release)
.contains("allocation_status = 'RELEASED'")
.contains("allocation_status = 'CONFIRMED'");
.contains("allocation_status='RELEASED'")
.contains("slot_status='RELEASED'")
.contains("block_id=#{blockId}")
.contains("allocation_status='CONFIRMED'");
String transfer = InboundPalletAllocationMapper.class.getMethod("markBlockTransferred",
long.class, long.class, long.class)
.getAnnotation(Update.class).value()[0];
assertThat(transfer)
.contains("allocation_status='TRANSFERRED'")
.contains("slot_status='TRANSFERRED'")
.contains("block_id=#{blockId}")
.contains("allocation_status='CONFIRMED'");
}
}
@@ -252,6 +252,21 @@ class Phase3FinalUnifiedSqlContractTest {
.contains("CREATE PROCEDURE phase3_pda_replace_operation_check");
}
@Test
void unifiedPdaBlockDropsLegacyPerItemReleaseColumns() throws Exception {
String sql = normalise(Files.readString(resolveUnified()));
assertThat(sql)
.contains("column_name='release_location_ids'")
.contains("ALTER TABLE fl_loading_pick_item DROP COLUMN release_location_ids")
.contains("column_name='release_mode'")
.contains("ALTER TABLE fl_loading_pick_item DROP COLUMN release_mode")
.doesNotContain("'fl_loading_pick_item', 'release_mode'")
.doesNotContain("'fl_loading_pick_item', 'release_location_ids'")
.doesNotContain("CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_item', 'release_mode'")
.doesNotContain("CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_item', 'release_location_ids'");
}
@Test
void legacySplitIndexesAreOnlyDroppedAndNeverRecreated() throws Exception {
String sql = normalise(Files.readString(resolveUnified()));
@@ -11,8 +11,7 @@ import com.baiyue.firstleg.dto.LoadingPickDtos.ClaimRequest;
import com.baiyue.firstleg.dto.LoadingPickDtos.AssignRequest;
import com.baiyue.firstleg.dto.LoadingPickDtos.CompleteLoadingRequest;
import com.baiyue.firstleg.dto.LoadingPickDtos.Task;
import com.baiyue.firstleg.client.WarehousePalletLocationClient;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.firstleg.entity.InboundPalletAllocationEntity;
import com.baiyue.firstleg.entity.LoadingPickAssignmentEntity;
import com.baiyue.firstleg.entity.LoadingPickHistoryEntity;
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.LoadingPickItemMapper;
import com.baiyue.firstleg.repository.LoadingPickRepository;
import com.baiyue.firstleg.repository.InboundPalletAllocationRepository;
import com.baiyue.firstleg.repository.LoadingCompletionCommandRepository;
import com.baiyue.firstleg.repository.SmartLoadingRepository;
import com.baiyue.firstleg.oceanbooking.repository.OceanBookingRepository;
@@ -100,7 +100,7 @@ class LoadingPickServiceTest {
@MockBean
private PdaPickSettlementService settlementService;
@MockBean
private WarehousePalletLocationClient warehouse;
private InboundPalletAllocationRepository palletAllocations;
@Resource
private LoadingPickService service;
@@ -122,6 +122,10 @@ class LoadingPickServiceTest {
when(completionCommands.insert(any())).thenReturn(1);
when(completionCommands.renew(anyLong(), anyString(), any(java.time.LocalDateTime.class))).thenReturn(true);
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
@@ -437,6 +441,7 @@ class LoadingPickServiceTest {
when(repository.allTaskItemsResolved(20L)).thenReturn(false);
when(repository.inboundOwners(20L)).thenReturn(List.of(
new LoadingPickItemMapper.InboundOwner(101L, 50L)));
when(palletAllocations.list(101L, 50L)).thenReturn(List.of(allocation(50L, 2)));
when(repository.inbound(101L, 50L)).thenReturn(
new LoadingInboundMapper.Snapshot(50L, 101L, "LOADING", 7));
var result = service.task(20L, 101L);
@@ -449,6 +454,14 @@ class LoadingPickServiceTest {
assertThat(result.sourceType()).isEqualTo("SIMULATION");
assertThat(result.recordStatus()).isEqualTo("ACTIVE");
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);
}
@@ -458,7 +471,9 @@ class LoadingPickServiceTest {
when(repository.task(20L)).thenReturn(task);
when(repository.items(20L, 101L)).thenReturn(List.of(item()));
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"))
.when(completionCommands).latest(20L);
@@ -1150,10 +1165,17 @@ class LoadingPickServiceTest {
when(repository.allTaskItemsResolved(20L)).thenReturn(true);
when(repository.touchTask(20L, 0)).thenReturn(1);
when(repository.allPlanItemsPicked(10L)).thenReturn(true);
when(warehouse.occupiedResources(any(), any(Long.class), any(), any(Long.class))).thenReturn(
ApiResponse.success(List.of(new WarehousePalletLocationClient.OccupiedResource(
71L, 72L, 0, 101L, 73L, 74L, "A-01", false, 2))));
String snapshotToken = service.inventorySnapshot(20L, 101L).snapshotToken();
when(repository.inboundOwners(20L)).thenReturn(List.of(
new LoadingPickItemMapper.InboundOwner(1L, 50L),
new LoadingPickItemMapper.InboundOwner(1L, 51L)));
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(
new BatchItemRequest(30L, "PICK", 2, null, 0, null),
new BatchItemRequest(31L, "SKIP", null, null, 0, "damaged carton")));
@@ -1191,9 +1213,10 @@ class LoadingPickServiceTest {
when(repository.touchTask(20L, 0)).thenReturn(1);
when(smartLoadingRepository.startPicking(10L, 9L, "operator")).thenReturn(1);
when(smartLoadingRepository.operation("PDA-START-20")).thenReturn(existingStart);
when(warehouse.occupiedResources(any(), any(Long.class), any(), any(Long.class))).thenReturn(
ApiResponse.success(List.of(new WarehousePalletLocationClient.OccupiedResource(
71L, 72L, 0, 101L, 73L, 74L, "A-01", false, 100))));
when(repository.inboundOwners(20L)).thenReturn(List.of(
new LoadingPickItemMapper.InboundOwner(1L, 50L)));
when(palletAllocations.list(anyLong(), anyLong())).thenAnswer(invocation ->
List.of(allocation(invocation.getArgument(1), 100)));
String snapshotToken = service.inventorySnapshot(20L, 101L).snapshotToken();
BatchRequest request = new BatchRequest(101L, 10L, 99L, 0, snapshotToken, List.of(
new BatchItemRequest(30L, "PICK", 100, null, 0, null)));
@@ -1242,9 +1265,10 @@ class LoadingPickServiceTest {
when(repository.assignment(20L)).thenReturn(assignment());
when(repository.items(20L)).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(
ApiResponse.success(List.of(new WarehousePalletLocationClient.OccupiedResource(
71L, 72L, 0, 101L, 73L, 74L, "A-01", false, 2))));
when(repository.inboundOwners(20L)).thenReturn(List.of(
new LoadingPickItemMapper.InboundOwner(1L, 50L)));
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(
new BatchItemRequest(30L, "PICK", 2, null, 0, null)));
@@ -1285,9 +1309,10 @@ class LoadingPickServiceTest {
when(repository.pick(30L, 2, 0, 101L)).thenReturn(1);
when(repository.allTaskItemsResolved(20L)).thenReturn(true);
when(repository.touchTask(20L, 0)).thenReturn(1);
when(warehouse.occupiedResources(any(), any(Long.class), any(), any(Long.class))).thenReturn(
ApiResponse.success(List.of(new WarehousePalletLocationClient.OccupiedResource(
71L, 72L, 0, 101L, 73L, 74L, "A-01", false, 2))));
when(repository.inboundOwners(20L)).thenReturn(List.of(
new LoadingPickItemMapper.InboundOwner(1L, 50L)));
when(palletAllocations.list(anyLong(), anyLong())).thenAnswer(invocation ->
List.of(allocation(invocation.getArgument(1), 2)));
String snapshotToken = service.inventorySnapshot(20L, 101L).snapshotToken();
BatchItemRequest line = new BatchItemRequest(30L, "PICK", 2, null, 0, null);
@@ -1405,6 +1430,27 @@ class LoadingPickServiceTest {
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() {
LoadingPickAssignmentEntity assignment = new LoadingPickAssignmentEntity();
assignment.setPickTaskId(20L);
+2
View File
@@ -288,6 +288,7 @@ function usage() {
--wait-split-package-count <n> 单票指定总箱数,1-1000;省略时使用既有轮换
--wait-split-sequence <n> 单票全局序号,正整数,默认1
--wait-split-random-value <n> 单票随机选择值,正整数,默认使用全局序号
--verify-pallet-location-guide <true> 验证整票库位块、图片槽绑定与删除后 RESERVED_EMPTY
--route-mode <mode> 线路选择:fixed 或 random,默认fixed
--manual-split-fixture-report <path> 人工分柜准备报告路径,仅只读核验模块使用
--manual-split-plan-id <id> 人工操作产生的智能装柜计划 ID,仅只读核验模块使用
@@ -348,6 +349,7 @@ export function parseArguments(argv) {
waitSplitPackageCount: Number(values.get('wait-split-package-count') || 0),
waitSplitSequence: Number(values.get('wait-split-sequence') || 1),
waitSplitRandomValue: Number(values.get('wait-split-random-value') || values.get('wait-split-sequence') || 1),
verifyPalletLocationGuide: values.has('verify-pallet-location-guide'),
routeMode: values.get('route-mode') || 'fixed',
quiet: values.get('quiet') === 'true',
manualSplitFixtureReport: values.get('manual-split-fixture-report') || '',
+6
View File
@@ -128,6 +128,12 @@ node --env-file="$env:USERPROFILE\.codex\chongchong-global-dev-acceptance.env" s
node scripts/admin-test-env-acceptance.mjs --modules merchant-qualification-fixture,orders,inbound-measurement,phase3-container-loading,phase3-loading,phase3-container-overview,phase3-pda-convergence,phase3-evidence-gate,phase3-customs,phase3-loading-finalize
```
整票库位块专项可在只跑 `orders,inbound-measurement` 时追加
`--verify-pallet-location-guide true`。它在测量提交后核对
`palletLocationGuide` 计数、每槽 `blockId/slotNo/reservationId/locationCode/slotStatus`、
称重结果按 `weightResultId` 显式绑定槽位,并真实验证删除图片后原 reservation/location
保留且转为 `RESERVED_EMPTY`。脚本只观测自然出现的共享 A0;不会为制造普通位不足批量停用库位。
只验证真实集装箱号晚补门禁时,使用正式可选参数 `LATE_CONTAINER_GATE_ONLY=1` 并仍从总入口执行 `phase3-loading`。该模式使用本轮 marker 独立造数,固定验证无号 push、订舱关联、整份预装确认、PDA claim/batch、无号 complete `A0409`、LOADING 阶段首次补号和补号后 complete 受理;随后允许后台命令停在 `CABINET_EVIDENCE/A0409`,以证明柜号门禁已经消失且下一门禁来自未上传的五类柜级证据。该模式不继续完整报关或 LOADED 链,也不得与完整链同时解释为同一终态验收。
```powershell
@@ -69,6 +69,16 @@ test('demo package keeps pallet pages bounded and historical snapshots read-only
assert.match(measurementTests, /detailUsesStoredPalletSnapshotAfterCandidateBecomesUnavailable/)
})
test('opt-in HTTP acceptance verifies whole-ticket slot identity and delete-to-empty semantics', () => {
assert.match(source, /verifyPalletLocationGuide/)
assert.match(source, /expectedWeightResultIds\.every/)
assert.match(source, /asId\(row\.reservationId\) === asId\(slot\.reservationId\)/)
assert.match(source, /\['RESERVED_EMPTY', 'BOUND', 'TRANSFERRED', 'RELEASED'\]/)
assert.match(source, /deleteSucceeded/)
assert.match(source, /retainedSlot\?\.slotStatus === 'RESERVED_EMPTY'/)
assert.match(source, /不批量停用普通库位制造容量不足/)
})
test('approval freezes measurement and requires quantity weight and volume conservation', () => {
assert.match(source, /审核及合托完成后箱码尺寸与图片快照不可再覆盖/)
assert.match(measurementTests, /approvedMeasurementCannotBeResubmittedOrMutateFrozenTotals/)
@@ -271,7 +271,10 @@ async function createConfirmedWeightEvidence(context, {
staleConfirmResponse.http === 200 && String(staleConfirmResponse.body?.code) === 'A0409',
`HTTP=${staleConfirmResponse.http}, code=${staleConfirmResponse.body?.code ?? 'missing'}, requestId=${staleConfirmResponse.requestId || 'missing'}`,
OPTIONS)
confirmedResults.push({ id: asId(created.id), palletId, netWeightKg, grossWeightKg })
confirmedResults.push({
id: asId(created.id), palletId, netWeightKg, grossWeightKg,
version: Number(confirmed?.version),
})
}
const workspaceResponse = await context.request('GET',
@@ -292,7 +295,41 @@ async function createConfirmedWeightEvidence(context, {
context.assert(MODULE, '图片张数决定托数且净重汇总已落地', workspaceResponse, persisted,
`confirmedImages=${confirmedRows.length}, expectedPallets=${palletIds.length}, grossWeight=${grossWeightKg}, netWeight=${netWeightKg}, actualGross=${workspace?.actualGrossWeightKg ?? 'missing'}, actualNet=${workspace?.actualNetWeightKg ?? 'missing'}`,
OPTIONS)
return persisted ? { count: confirmedRows.length, grossWeightKg, netWeightKg } : null
return persisted ? { count: confirmedRows.length, grossWeightKg, netWeightKg, confirmedResults } : null
}
function verifyPalletLocationGuide(context, response, workspace, expectedWeightResultIds, name) {
const guide = workspace?.palletLocationGuide
const slots = Array.isArray(guide?.slots) ? guide.slots : []
const weights = Array.isArray(workspace?.weightResults) ? workspace.weightResults : []
const boundSlots = slots.filter((slot) => asId(slot?.boundWeightResultId))
const emptySlots = slots.filter((slot) => slot?.slotStatus === 'RESERVED_EMPTY')
const identityComplete = asId(guide?.blockId)
&& slots.length === Number(guide?.reservedSlotCount)
&& Number(guide?.actualPalletCount) === boundSlots.length
&& Number(guide?.reservedEmptySlotCount) === emptySlots.length
&& slots.every((slot) => Number.isInteger(Number(slot?.slotNo)) && Number(slot.slotNo) > 0
&& asId(slot?.reservationId) && asId(slot?.locationId)
&& String(slot?.locationCode || '').trim() && typeof slot?.sharedOverflow === 'boolean'
&& ['RESERVED_EMPTY', 'BOUND', 'TRANSFERRED', 'RELEASED'].includes(String(slot?.slotStatus || '')))
context.assert(MODULE, `${name}整票库位块计数与槽身份完整`, response, Boolean(identityComplete),
`blockId=${asId(guide?.blockId) || 'missing'}, reserved=${guide?.reservedSlotCount ?? 'missing'}, actual=${guide?.actualPalletCount ?? 'missing'}, empty=${guide?.reservedEmptySlotCount ?? 'missing'}, slots=${slots.length}`,
OPTIONS)
const bindingsStable = expectedWeightResultIds.every((resultId) => {
const row = weights.find((item) => asId(item?.id) === resultId)
const slot = slots.find((item) => asId(item?.boundWeightResultId) === resultId)
return row && slot && asId(row.blockId) === asId(guide?.blockId)
&& Number(row.slotNo) === Number(slot.slotNo)
&& asId(row.reservationId) === asId(slot.reservationId)
&& asId(row.locationId) === asId(slot.locationId)
&& String(row.locationCode || '') === String(slot.locationCode || '')
&& Boolean(row.sharedOverflow) === Boolean(slot.sharedOverflow)
})
context.assert(MODULE, `${name}称重图片按 resultId 显式绑定槽位`, response, bindingsStable,
`expectedResultIds=${expectedWeightResultIds.join('|') || 'none'}, boundResultIds=${boundSlots.map((slot) => asId(slot.boundWeightResultId)).join('|') || 'none'}`,
OPTIONS)
return { guide, slots, weights, identityComplete: Boolean(identityComplete), bindingsStable }
}
function findIsolatedInbound(context) {
@@ -708,6 +745,62 @@ export async function run(context) {
submitted?.processStatus === 'MEASURE_PENDING' && submitted?.measureReviewStatus === 'PENDING',
`processStatus=${submitted?.processStatus ?? 'missing'}, reviewStatus=${submitted?.measureReviewStatus ?? 'missing'}`, OPTIONS)
if (context.options?.verifyPalletLocationGuide) {
const guideResponse = await context.request('GET', measurementPath)
const guideWorkspace = context.apiSuccess(MODULE, '提交后读取整票库位块与称重图片槽绑定', guideResponse,
`inboundId=${inboundId}`, OPTIONS)
const beforeDelete = verifyPalletLocationGuide(context, guideResponse, guideWorkspace,
weightEvidence.confirmedResults.map((item) => item.id), '删除前')
const overflowSlots = beforeDelete.slots.filter((slot) => slot?.sharedOverflow === true)
context.addStep(MODULE, '正式数据 A0 共享溢出观测', guideResponse, true,
overflowSlots.length > 0
? `sharedOverflow=true, slots=${overflowSlots.map((slot) => `${slot.slotNo}:${slot.reservationId}:${slot.locationCode}`).join('|')}`
: '本票未触发 A0;为保护共享测试仓,不批量停用普通库位制造容量不足',
{ scenario: SCENARIO })
const deletedWeight = weightEvidence.confirmedResults[0]
const deleteResponse = await context.request('DELETE',
`/api/platform/v1/first-leg/inbounds/${inboundId}/weight-results/${deletedWeight.id}?${query(tenantId)}&version=${deletedWeight.version}`)
const deleteSucceeded = deleteResponse.http === 200 && String(deleteResponse.body?.code) === '200'
context.assert(MODULE, '删除称重图片后保留原 reservation/location 并释放为空槽', deleteResponse,
deleteSucceeded,
`HTTP=${deleteResponse.http}, code=${deleteResponse.body?.code ?? 'missing'}, message=${deleteResponse.body?.message ?? 'missing'}, requestId=${deleteResponse.requestId || 'missing'}, traceId=${deleteResponse.traceId || 'missing'}`,
OPTIONS)
if (deleteSucceeded) {
const afterResponse = await context.request('GET', measurementPath)
const afterWorkspace = context.apiSuccess(MODULE, '删除后回查整票库位块', afterResponse,
`deletedWeightResultId=${deletedWeight.id}`, OPTIONS)
const afterDelete = verifyPalletLocationGuide(context, afterResponse, afterWorkspace,
weightEvidence.confirmedResults.slice(1).map((item) => item.id), '删除后')
const previousSlot = beforeDelete.slots.find((slot) => asId(slot?.boundWeightResultId) === deletedWeight.id)
const retainedSlot = afterDelete.slots.find((slot) =>
asId(slot?.reservationId) === asId(previousSlot?.reservationId)
&& asId(slot?.locationId) === asId(previousSlot?.locationId))
context.assert(MODULE, '删除只解绑图片且原槽变为 RESERVED_EMPTY', afterResponse,
Boolean(previousSlot) && retainedSlot?.slotStatus === 'RESERVED_EMPTY'
&& !asId(retainedSlot?.boundWeightResultId)
&& Number(afterDelete.guide?.reservedSlotCount) === Number(beforeDelete.guide?.reservedSlotCount),
`reservationId=${asId(retainedSlot?.reservationId) || 'missing'}, locationId=${asId(retainedSlot?.locationId) || 'missing'}, slotStatus=${retainedSlot?.slotStatus || 'missing'}`,
OPTIONS)
context.createdResources.inboundMeasurementLocationGuide = {
marker: context.marker, inboundId, tenantId, blockId: asId(afterDelete.guide?.blockId),
deletedWeightResultId: deletedWeight.id, deleteSucceeded: true,
sharedOverflowObserved: afterDelete.slots.some((slot) => slot?.sharedOverflow === true),
}
finishScenario(context, start)
return
}
context.createdResources.inboundMeasurementLocationGuide = {
marker: context.marker, inboundId, tenantId, blockId: asId(beforeDelete.guide?.blockId),
deletedWeightResultId: deletedWeight.id, deleteSucceeded: false,
deleteCode: deleteResponse.body?.code || null,
deleteMessage: deleteResponse.body?.message || null,
deleteRequestId: deleteResponse.requestId || null,
deleteTraceId: deleteResponse.traceId || null,
sharedOverflowObserved: overflowSlots.length > 0,
}
}
const rejectedScanResponse = await multipartRequest(context, scanPath, {
boxCode, ...replacement, machineNo: 'MJ-ADMIN-E2E-03', measuredAt: new Date().toISOString().slice(0, 19),
})
@@ -124,8 +124,10 @@ function inventorySignature(snapshot) {
inboundId: id(inbound?.inboundId),
availablePackageCount: Number(inbound?.availablePackageCount),
resources: (inbound?.resources || []).map((resource) => ({
reservationId: id(resource?.reservationId), occupancyId: id(resource?.occupancyId),
palletId: id(resource?.palletId), locationId: id(resource?.locationId),
blockId: id(resource?.blockId), slotNo: Number(resource?.slotNo),
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,
availablePackageCount: Number(resource?.availablePackageCount),
})),
@@ -622,8 +624,9 @@ export async function run(context) {
&& id(inventory?.warehouseId) === warehouseId
&& Boolean(inventory?.capturedAt) && typeof inventory?.snapshotToken === 'string'
&& inventory.snapshotToken.length === 64 && Number(inventoryInbound?.availablePackageCount) >= 1
&& (inventoryInbound?.resources || []).every((resource) => id(resource?.reservationId)
&& id(resource?.occupancyId) && id(resource?.palletId) && id(resource?.locationId)
&& (inventoryInbound?.resources || []).every((resource) => id(resource?.blockId)
&& Number(resource?.slotNo) >= 1 && id(resource?.reservationId) && id(resource?.locationId)
&& ['RESERVED_EMPTY', 'BOUND'].includes(resource?.slotStatus)
&& Number(resource?.availablePackageCount) >= 0)
&& Number(inventoryInbound?.availablePackageCount) === (inventoryInbound?.resources || [])
.reduce((total, resource) => total + Number(resource?.availablePackageCount || 0), 0)