Merge remote-tracking branch 'origin/test' into dev
# Conflicts: # AGENTS.md # scripts/agents-index-contract.ps1
This commit is contained in:
+4
-4
@@ -19,12 +19,12 @@
|
||||
| 箱唛箱码生成、打印与测量绑定 | 审核建单生成钩子 `controller/InternalInboundOrderController.java`;生成/解析 `service/InboundCartonLabelService.java`、`repository/InboundCartonLabelRepository.java`;平台与客户打印 `controller/PlatformInboundOrderController.java`、`MerchantInboundCartonLabelController.java`、`service/InboundCartonLabelPrintService.java`。`fl_inbound_carton_label.sku_code` 是箱唛的 SKU 冻结快照,生成时经 `client/OrderCargoClient.java` 获取;既有记录按 `inbound_detail_id` 回填。`ONE_CLICK_DROPSHIP` 缺 SKU 时禁止生成箱唛,打印面显示 SKU;`box_code` 与二维码内容仍只携带稳定箱码。基线/升级 SQL 为 `src/main/resources/db/phase2-inbound-carton-label.sql`、`phase2-carton-label-versioning-migration.sql`、`../deploy/sql/头程数据库统一增量-MySQL8.sql` |
|
||||
| 一件代发测量后 PDA 合托 | `controller/PlatformDropshipPalletizationController.java`、`service/DropshipPalletizationService.java`、`mapper/DropshipPalletizationMapper.java`;测量通过进入 `WAIT_PALLETIZE` 并下发独立任务,不创建国内库存,封托生成 `DP` 托盘号,全部箱数完成后进入 `WAIT_SPLIT`。契约 `src/main/resources/openapi/dropship-palletization-v1.yaml` |
|
||||
| 墨家设备测量上传 | `controller/OpenApiInboundMeasurementController.java`、`dto/MojiaMeasurementDtos.java`、`service/MojiaMeasurementOpenApiService.java`、`security/MeasurementRequestAuthenticator.java`;接口契约见 `src/main/resources/openapi/firstleg-v1.yaml` 与 `../../docs/墨家设备测量上传接口说明-V3.md` |
|
||||
| 称重确认与整票库位块 | `service/MeasurementLocationOccupationService.java`、`InboundMeasurementService.java`、`MeasurementSubmissionService.java`、`client/WarehousePalletLocationClient.java`;Warehouse 块服务见 `../warehouse/src/main/java/com/baiyue/warehouse/service/LocationBlockService.java`,接口与上架引导见 `src/main/resources/openapi/firstleg-v1.yaml` |
|
||||
| 审核与初始计价 | `controller/PlatformInboundMeasurementReviewController.java`、`service/MeasurementReviewService.java`、`service/InitialInboundPricingService.java`、`dto/MeasurementApprovalResponse.java` |
|
||||
| 客户四档箱规与默认算法 | `controller/PlatformCustomerDimensionConfigController.java`、`service/CustomerDimensionConfigService.java`、`repository/CustomerDimensionConfigRepository.java`;固定 `SMALL/MEDIUM/LARGE/EXTRA_LARGE`,默认算法只允许 `ADD/MULTIPLY` |
|
||||
| FBA 主数据/计费与客户规则 | `controller/PlatformFbaMasterDataController.java`、`PlatformFbaPricingController.java`、`service/FbaPricingService.java`、`repository/FbaPricingRepository.java`;客户规则可保存多个版本,但每个客户仅一条默认规则,计算请求可提交已确认的重量/体积选择值 |
|
||||
| 入库费用明细 | `controller/PlatformInboundFeeDetailController.java`、`service/InboundFeeDetailService.java`、`dto/InboundFeeDetailDtos.java`;Admin 消费端见 `../../apps/admin-web/src/modules/unified-orders/api/inboundFeeDetails.ts` |
|
||||
| 入库/柜次直接已付快照与凭证校验 | `service/FeePaymentSnapshotValidator.java`、`client/FeePaymentVoucherFileClient.java`;写入入口分别为 `InboundFeeDetailService.java`、`ContainerFeeDetailService.java`,File 业务类型固定 `FINANCE_VOUCHER` |
|
||||
| 客户费用查询 | `controller/MerchantInboundBillingController.java`、`service/MerchantInboundBillingService.java` |
|
||||
| 客户运营统计/在途柜量 | `customerstatistics/controller/PlatformCustomerTransitStatisticsController.java`、`customerstatistics/service/CustomerTransitStatisticsService.java`;契约见 `src/main/resources/openapi/customer-statistics-v1.yaml` |
|
||||
| 费用项目与价格簿 | `controller/PlatformFeeItemController.java`、`PlatformPriceBookController.java`、`MerchantPriceBookController.java` |
|
||||
| 客户入库结果与头程费用确认 | `controller/MerchantInboundBillingController.java`、`service/MerchantInboundBillingService.java`、`FbaPricingService.java`;查询与确认接口为 `/api/merchant/v1/first-leg/orders/{orderId}/inbound-billing`、`/confirm`,契约 `src/main/resources/openapi/firstleg-v1.yaml` |
|
||||
@@ -80,14 +80,14 @@
|
||||
|
||||
## 国内货柜履约装柜与 PDA 资源结算
|
||||
|
||||
- 智能装柜全局工作台由服务端把业务渠道分组映射为实际渠道,并分别分页查询候选与 `WAIT_LOADING/EDITING/LOADING` 柜次;候选状态筛选使用最多四项的 `orderStatuses`,未传表示 `PENDING_REVIEW/WAIT_MEASURE/WAIT_SPLIT/LOADING` 全部未完成状态。状态只用于搜索和展示,能否加入以响应 `canAssign` 为准,由有效柜次占用、剩余箱数、国内中转仓、容量、组锁和版本等事实决定;人工加入允许订单渠道与柜次渠道不同,智能推荐仍只选择柜次渠道。容积和重量均允许达到柜型额定值的 150%,超过即拒绝。待审核事实必须经 `PendingReviewOrderClient` 重新核验 Order 权威 `SUBMITTED` 订单,待审核或待测量可先占位,但确认预装和启动 PDA 前仍必须进入 `WAIT_SPLIT`。同柜不可拆组只有全部成员命中状态集合时才返回,且必须成员完整、满箱并作为单个分页与分配单元,不能跨页或拆分。一次性移除未拣明细时,普通未拣项直接移除;同柜组仅全组未拣时整组移除,任一成员已拣则整组保留,单事务只递增一次计划版本。推荐顺序按入库时间先进先出。装柜计划状态为 `EDITING -> LOADING -> COMPLETED`,PDA 全部明细处理完成后自动完成。
|
||||
- 智能装柜全局工作台由服务端把业务渠道分组映射为实际渠道,并分别分页查询候选与 `WAIT_LOADING/EDITING/LOADING` 柜次;候选状态筛选使用最多四项的 `orderStatuses`,未传表示 `PENDING_REVIEW/WAIT_MEASURE/WAIT_SPLIT/LOADING` 全部未完成状态。状态只用于搜索和展示,能否加入以响应 `canAssign` 为准,由有效柜次占用、剩余箱数、国内中转仓、容量、组锁和版本等事实决定;人工加入允许订单渠道与柜次渠道不同,智能推荐仍只选择柜次渠道。容积和重量均允许达到柜型额定值的 150%,超过即拒绝。待审核事实必须经 `PendingReviewOrderClient` 重新核验 Order 权威 `SUBMITTED` 订单,待审核或待测量可先占位,但确认预装和启动 PDA 前仍必须进入 `WAIT_SPLIT`。计划明细通过 `totalPackageCount/remainingPackageCount` 返回可调总量和余量:测量前取 Order 原始申报箱数,进入 `WAIT_SPLIT/LOADING` 后取最新已审核测量箱数;只读补充失败时保守回退当前已分配数,调整写入仍重新查询并校验权威数量。同柜不可拆组只有全部成员命中状态集合时才返回,且必须成员完整、满箱并作为单个分页与分配单元,不能跨页或拆分。一次性移除未拣明细时,普通未拣项直接移除;同柜组仅全组未拣时整组移除,任一成员已拣则整组保留,单事务只递增一次计划版本。推荐顺序按入库时间先进先出。装柜计划状态为 `EDITING -> LOADING -> COMPLETED`,PDA 全部明细处理完成后自动完成。
|
||||
- 客户入库费用只在登录客户范围、测量审核通过、`feeStatus=SENT` 且存在已发布明细时可见;确认必须具备 `merchant:order:edit`,携带当前 `expectedFeeVersion` 与幂等键。成功后记录确认成员和时间并将该费用版本永久锁定,平台不得再调整;旧版本、未发送或重复异参确认均返回冲突,不做兼容覆盖。
|
||||
- PDA 拣货以 `fl_inbound_carton_label.box_code` 对应的稳定箱唛箱码为扫描身份;审核建单、重复打印、手工测量和墨家测量复用同一映射,版本字段只用于并发控制,不进入扫码文本。一个有效且未重复的箱码扫描表示拣出一箱,不得以 SKU、商品件数或页面展示字段替代箱码。任务下发与提交契约见 `src/main/resources/openapi/phase3-pda-convergence-v1.yaml`。
|
||||
- PDA 部分拣货生成二级子入库单时,数量必须守恒:`已拣数量 + 子单数量 = 拣货前主单可用数量`。子单只允许一层,不能继续拆分。
|
||||
- 首张称重图片确认时按 `max(已确认称重图片数, ceil(实测体积 / 默认托盘容量))` 预留并确认整票库位块;后续确认绑定最小空槽,所需槽数增长时通过 Warehouse `/blocks/expand` 原子追加,既有 `blockId/slotNo/reservationId/locationId` 不得变化。测量提交只校验并复用已存在权威块,不得首次分配。Firstleg 必须显式保存 `weightResultId -> blockId/slotNo/reservationId/locationId/locationCode`,按 `confirmedAt + weightResultId` 稳定顺序绑定,禁止按列表下标推断;普通连续位不足的新增槽由 Warehouse 放入共享 A0,不阻断确认。
|
||||
- 首张称重图片确认时按 `max(已确认称重图片数, ceil(有效体积 / 默认托盘容量))` 预留并确认整票库位块;有效体积严格依次取墨家/人工测量实测体积、测量批次冻结申报体积、Order 最新客户申报体积,全部无效才拒绝,不得把后两级回退冒充实测。后续确认绑定最小空槽,所需槽数增长时通过 Warehouse `/blocks/expand` 原子追加,既有 `blockId/slotNo/reservationId/locationId` 不得变化。测量提交只校验并复用已存在权威块,不得首次分配。Firstleg 必须显式保存 `weightResultId -> blockId/slotNo/reservationId/locationId/locationCode`,按 `confirmedAt + weightResultId` 稳定顺序绑定,禁止按列表下标推断;普通连续位不足的新增槽由 Warehouse 放入共享 A0,不阻断确认。
|
||||
- 删除称重图片只解除图片和托盘快照绑定,将槽位恢复为 `RESERVED_EMPTY`;槽位和整票块继续归当前入仓码。解绑失败时不得调用文件删除,文件删除失败时本地解绑和重量结果删除必须回滚。
|
||||
- 头程快照必须保存 Warehouse `blockId + slotNo + reservationId`;A0 可被多个整票块共享 `locationId`,业务资源身份始终使用 `reservationId`,不得把 `locationId` 当作唯一标识。
|
||||
- PDA 部分拣货不得释放块内单槽;存在余量子单时通过 `/internal/warehouse/v1/pallet-locations/blocks/transfer` 将整段转移给子单。整票全部拣完时只通过 `/internal/warehouse/v1/pallet-locations/blocks/release` 一次性释放全部普通位和 A0 预约,禁止恢复逐槽 `settlements/release-selected` 协议。
|
||||
- PDA 部分拣货不得释放块内单槽;存在余量子单时通过 `/internal/warehouse/v1/pallet-locations/blocks/transfer` 将整段转移给子单,源单与余量子单可各自保留同一块槽及称重快照,因此 `fl_inbound_pallet_allocation` 的槽位与称重唯一键必须包含 `inbound_id`。整票全部拣完时只通过 `/internal/warehouse/v1/pallet-locations/blocks/release` 一次性释放全部普通位和 A0 预约,禁止恢复逐槽 `settlements/release-selected` 协议。
|
||||
- 数据库字段变更直接维护当前模块基线与 `../deploy/sql/头程数据库统一增量-MySQL8.sql`;已有 `phase3-pda-release-allocation-migration.sql` 只作为当前结构来源,不为历史开发数据再新增兼容迁移。
|
||||
|
||||
## 全量验收导航
|
||||
|
||||
@@ -401,6 +401,21 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ba
|
||||
Candidate candidate(@Param("planId") long planId,@Param("warehouseId") long warehouseId,
|
||||
@Param("inboundId") long inboundId,@Param("detailId") long detailId);
|
||||
|
||||
@Select("""
|
||||
SELECT SUM(i.package_count)
|
||||
FROM fl_inbound_measurement m
|
||||
JOIN fl_inbound_measurement_item i
|
||||
ON i.tenant_id=m.tenant_id AND i.measurement_id=m.id
|
||||
WHERE m.tenant_id=#{accountId} AND m.inbound_id=#{inboundId} AND m.status='APPROVED'
|
||||
AND m.id=(SELECT MAX(m2.id) FROM fl_inbound_measurement m2
|
||||
WHERE m2.tenant_id=m.tenant_id AND m2.inbound_id=m.inbound_id
|
||||
AND m2.status='APPROVED')
|
||||
AND i.inbound_detail_id=#{detailId} AND i.package_count>0
|
||||
""")
|
||||
Integer measuredPackageCount(@Param("accountId") long accountId,
|
||||
@Param("inboundId") long inboundId,
|
||||
@Param("detailId") long detailId);
|
||||
|
||||
@lombok.Getter @lombok.Setter class ChannelContainerCountRow {
|
||||
private String channelCode;
|
||||
private Long availableContainerCount;
|
||||
|
||||
@@ -65,6 +65,9 @@ public class SmartLoadingRepository {
|
||||
public Candidate candidate(long planId, long warehouseId, long inboundId, long detailId) {
|
||||
return candidates.candidate(planId, warehouseId, inboundId, detailId);
|
||||
}
|
||||
public Integer measuredPackageCount(long accountId, long inboundId, long detailId) {
|
||||
return candidates.measuredPackageCount(accountId, inboundId, detailId);
|
||||
}
|
||||
public InboundOrderEntity inbound(long accountId, long inboundId) {
|
||||
return inbounds.selectOne(new LambdaQueryWrapper<InboundOrderEntity>()
|
||||
.eq(InboundOrderEntity::getTenantId, accountId)
|
||||
|
||||
@@ -1429,7 +1429,7 @@ public class SmartLoadingService {
|
||||
int picked = task.pickedByPlanItem().getOrDefault(item.getId(), 0);
|
||||
item.setPickedPackageCount(picked);
|
||||
item.setMinPackageCount(Math.max(1, picked));
|
||||
int total = adjustmentTotal(p.getId(), b.getWarehouseId(), item);
|
||||
int total = adjustmentTotal(item);
|
||||
item.setTotalPackageCount(total);
|
||||
item.setRemainingPackageCount(Math.max(0, total - item.getPackageCount()));
|
||||
item.setCanAdjust(mutable);
|
||||
@@ -1441,18 +1441,21 @@ public class SmartLoadingService {
|
||||
b.getContainerNo(),b.getVoyageNo(),p.getVersion(),mutable,mutable,blockedReason,finalStatus,
|
||||
task.taskId(),task.taskStatus(),task.taskVersion(),planCoLoadGroups(p.getId()),items);
|
||||
}
|
||||
private int adjustmentTotal(long planId, long warehouseId, LoadingPlanItemEntity item) {
|
||||
private int adjustmentTotal(LoadingPlanItemEntity item) {
|
||||
int current = item.getPackageCount() == null ? 0 : item.getPackageCount();
|
||||
Candidate measured = repository.candidate(planId, warehouseId, item.getInboundId(), item.getInboundDetailId());
|
||||
if (measured != null) {
|
||||
return current + Math.max(0, measured.availablePackageCount() == null
|
||||
? 0 : measured.availablePackageCount());
|
||||
}
|
||||
InboundOrderEntity inbound = repository.inbound(item.getAccountId(), item.getInboundId());
|
||||
if (inbound == null || !Set.of("PREPARING", "WAIT_MEASURE", "MEASURING", "MEASURE_PENDING",
|
||||
"MEASURE_REJECTED", "WAIT_PRICING", "FEE_CONFIRMING", "INBOUNDING")
|
||||
.contains(inbound.getProcessStatus())) return current;
|
||||
try {
|
||||
if (item.getAccountId() == null || item.getInboundId() == null
|
||||
|| item.getInboundDetailId() == null) return current;
|
||||
InboundOrderEntity inbound = repository.inbound(item.getAccountId(), item.getInboundId());
|
||||
if (inbound == null) return current;
|
||||
if (Set.of("WAIT_SPLIT", "LOADING").contains(inbound.getProcessStatus())) {
|
||||
Integer measured = repository.measuredPackageCount(item.getAccountId(), item.getInboundId(),
|
||||
item.getInboundDetailId());
|
||||
return Math.max(current, measured == null ? 0 : measured);
|
||||
}
|
||||
if (!Set.of("PREPARING", "WAIT_MEASURE", "MEASURING", "MEASURE_PENDING",
|
||||
"MEASURE_REJECTED", "WAIT_PRICING", "FEE_CONFIRMING", "INBOUNDING")
|
||||
.contains(inbound.getProcessStatus()) || item.getOrderId() == null) return current;
|
||||
OrderCargoClient.CargoBinding cargo = loadCargo(item.getAccountId(), item.getOrderId(), item.getInboundDetailId());
|
||||
return Math.max(current, cargo.originalPackageCount() == null ? 0 : cargo.originalPackageCount());
|
||||
} catch (RuntimeException ignored) {
|
||||
|
||||
@@ -738,7 +738,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_pallet_allocation (
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_pallet_allocation_no (tenant_id, inbound_id, allocation_no),
|
||||
UNIQUE KEY uk_pallet_allocation_slot (tenant_id, inbound_id, block_id, slot_no),
|
||||
UNIQUE KEY uk_pallet_allocation_weight (tenant_id, weight_result_id),
|
||||
UNIQUE KEY uk_pallet_allocation_weight (tenant_id, inbound_id, weight_result_id),
|
||||
KEY idx_allocation_location (tenant_id, warehouse_id, location_id, allocation_status),
|
||||
KEY idx_allocation_reservation (reservation_id),
|
||||
CONSTRAINT fk_allocation_inbound FOREIGN KEY (inbound_id) REFERENCES fl_inbound_order (id)
|
||||
|
||||
@@ -2210,7 +2210,7 @@ components:
|
||||
items: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
SmartLoadingPlanItem:
|
||||
type: object
|
||||
required: [id, packageCount, version, pickedPackageCount, minPackageCount, canAdjust, canRemove]
|
||||
required: [id, packageCount, totalPackageCount, remainingPackageCount, version, pickedPackageCount, minPackageCount, canAdjust, canRemove]
|
||||
properties:
|
||||
id: {type: string}
|
||||
inboundId: {type: string}
|
||||
@@ -2222,6 +2222,8 @@ components:
|
||||
internalRemark: {type: string, nullable: true, readOnly: true, description: 来源为订单最新内部备注,不使用 cargoRemark}
|
||||
warehouseRemark: {type: string, nullable: true, description: 仓库人员逐条维护,可在 EDITING/LOADING 修改}
|
||||
packageCount: {type: integer, minimum: 1}
|
||||
totalPackageCount: {type: integer, minimum: 1, description: 当前入仓明细可在计划内分配的总箱数,包含已用与未用箱数}
|
||||
remainingPackageCount: {type: integer, minimum: 0, description: 当前入仓明细尚未分配到本计划的箱数}
|
||||
version: {type: integer, minimum: 0, description: itemVersion,调整/删除写请求使用此值}
|
||||
pickedPackageCount: {type: integer, minimum: 0}
|
||||
minPackageCount: {type: integer, minimum: 1}
|
||||
|
||||
+4
-1
@@ -185,9 +185,12 @@ class PdaSplitSettlementSqlContractTest {
|
||||
|
||||
assertThat(moduleSql)
|
||||
.contains("UNIQUE KEY uk_pallet_allocation_slot (tenant_id, inbound_id, block_id, slot_no)")
|
||||
.contains("UNIQUE KEY uk_pallet_allocation_weight (tenant_id, inbound_id, weight_result_id)")
|
||||
.doesNotContain("UNIQUE KEY uk_pallet_allocation_slot (tenant_id, block_id, slot_no)");
|
||||
assertThat(unifiedSql)
|
||||
.contains("'tenant_id,inbound_id,block_id,slot_no'")
|
||||
.contains("ADD UNIQUE KEY uk_pallet_allocation_slot (tenant_id,inbound_id,block_id,slot_no)");
|
||||
.contains("ADD UNIQUE KEY uk_pallet_allocation_slot (tenant_id,inbound_id,block_id,slot_no)")
|
||||
.contains("'tenant_id,inbound_id,weight_result_id'")
|
||||
.contains("ADD UNIQUE KEY uk_pallet_allocation_weight (tenant_id,inbound_id,weight_result_id)");
|
||||
}
|
||||
}
|
||||
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.firstleg.client.OrderCargoClient;
|
||||
import com.baiyue.firstleg.client.PendingReviewOrderClient;
|
||||
import com.baiyue.firstleg.dto.OrderApprovedInboundCommand;
|
||||
import com.baiyue.firstleg.dto.SmartLoadingDtos.AdjustRequest;
|
||||
import com.baiyue.firstleg.dto.SmartLoadingDtos.Plan;
|
||||
import com.baiyue.firstleg.entity.ContainerBookingEntity;
|
||||
import com.baiyue.firstleg.entity.InboundOrderEntity;
|
||||
import com.baiyue.firstleg.entity.LoadingPlanEntity;
|
||||
import com.baiyue.firstleg.entity.LoadingPlanItemEntity;
|
||||
import com.baiyue.firstleg.loading.bridge.service.LoadingModelBridgeService;
|
||||
import com.baiyue.firstleg.loading.coload.port.ColoadGroupAssignmentPort;
|
||||
import com.baiyue.firstleg.pricebookquery.application.port.in.PriceBookQueryUseCase;
|
||||
import com.baiyue.firstleg.repository.SmartLoadingRepository;
|
||||
import com.baiyue.firstleg.split.service.SplitPreloadService;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Spy;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.dao.DataAccessResourceFailureException;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.nullable;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class SmartLoadingProvisionalAdjustmentTest {
|
||||
@Mock private SmartLoadingRepository repository;
|
||||
@Mock private OrderCargoClient orderCargoClient;
|
||||
@Mock private PendingReviewOrderClient pendingReviewOrders;
|
||||
@Mock private InboundOrderCreationService inboundOrderCreationService;
|
||||
@Mock private LoadingPickService loadingPickService;
|
||||
@Mock private LoadingModelBridgeService loadingModelBridge;
|
||||
@Mock private SplitPreloadService splitPreloadService;
|
||||
@Mock private ColoadGroupAssignmentPort coloadGroups;
|
||||
@Mock private PriceBookQueryUseCase priceBooks;
|
||||
@Mock private FirstlegDocumentNumberGenerator documentNumbers;
|
||||
@Spy private ObjectMapper objectMapper = new ObjectMapper();
|
||||
@InjectMocks private SmartLoadingService service;
|
||||
|
||||
private LoadingPlanItemEntity item;
|
||||
private InboundOrderEntity inbound;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
item = new LoadingPlanItemEntity();
|
||||
item.setId(8L);
|
||||
item.setLoadingPlanId(1L);
|
||||
item.setAccountId(9L);
|
||||
item.setInboundId(10L);
|
||||
item.setInboundDetailId(20L);
|
||||
item.setOrderId(30L);
|
||||
item.setInboundCode("I10");
|
||||
item.setPackageCount(20);
|
||||
item.setVersion(0);
|
||||
|
||||
inbound = new InboundOrderEntity();
|
||||
inbound.setTenantId(9L);
|
||||
inbound.setId(10L);
|
||||
inbound.setProcessStatus("PREPARING");
|
||||
|
||||
when(loadingPickService.mutationSnapshot(anyLong())).thenReturn(
|
||||
new LoadingPickService.MutationSnapshot(null, null, null, Map.of()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void adjustsProvisionalInboundFromTwentyToFiftyWithinDeclaredHundredBoxes() {
|
||||
LoadingPlanEntity plan = plan(103);
|
||||
ContainerBookingEntity booking = booking();
|
||||
OrderCargoClient.CargoBinding cargo = new OrderCargoClient.CargoBinding(
|
||||
20L, 30L, 1, "货物", 100, new BigDecimal("200"), new BigDecimal("50"));
|
||||
OrderApprovedInboundCommand snapshot = new OrderApprovedInboundCommand(
|
||||
9L, 30L, "O30", "I10", 9L, "客户", 100L, "仓库",
|
||||
null, null, null, "FIRST_LEG", "FBA", "FBA渠道", null, null, null,
|
||||
null, null, false, "FBA", 200L, "FBA1", "FBA仓", null, null, null, 100);
|
||||
|
||||
when(repository.plan(1L)).thenReturn(plan, plan(104));
|
||||
when(repository.item(1L, 8L)).thenReturn(item);
|
||||
when(repository.booking(99L)).thenReturn(booking);
|
||||
when(repository.inbound(9L, 10L)).thenReturn(inbound);
|
||||
when(orderCargoClient.detail(nullable(String.class), eq(9L), eq(30L), eq(20L)))
|
||||
.thenReturn(ApiResponse.success(cargo));
|
||||
when(orderCargoClient.inboundSnapshot(nullable(String.class), eq(9L), eq(30L)))
|
||||
.thenReturn(ApiResponse.success(snapshot));
|
||||
when(repository.items(1L)).thenReturn(List.of(item));
|
||||
when(repository.touch(1L, 103, 7L, "u")).thenReturn(1);
|
||||
when(repository.update(item)).thenReturn(1);
|
||||
|
||||
Plan result = service.adjust(1L, 8L,
|
||||
new AdjustRequest(50, 103, 0, "adjust-provisional"), 7L, "u");
|
||||
|
||||
assertThat(item.getPackageCount()).isEqualTo(50);
|
||||
assertThat(result.items()).singleElement().satisfies(updated -> {
|
||||
assertThat(updated.getTotalPackageCount()).isEqualTo(100);
|
||||
assertThat(updated.getRemainingPackageCount()).isEqualTo(50);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void planReadFallsBackToAssignedCountWhenQuantityEnrichmentFails() {
|
||||
LoadingPlanEntity plan = plan(103);
|
||||
when(repository.planByBooking(99L)).thenReturn(plan);
|
||||
when(repository.booking(99L)).thenReturn(booking());
|
||||
when(repository.items(1L)).thenReturn(List.of(item));
|
||||
when(repository.inbound(9L, 10L))
|
||||
.thenThrow(new DataAccessResourceFailureException("read query failed"));
|
||||
|
||||
Plan result = service.draftByBooking(99L);
|
||||
|
||||
assertThat(result.items()).singleElement().satisfies(updated -> {
|
||||
assertThat(updated.getTotalPackageCount()).isEqualTo(20);
|
||||
assertThat(updated.getRemainingPackageCount()).isZero();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void planReadUsesApprovedMeasurementWithoutCallingTheLockedAdjustmentQuery() {
|
||||
inbound.setProcessStatus("WAIT_SPLIT");
|
||||
when(repository.planByBooking(99L)).thenReturn(plan(103));
|
||||
when(repository.booking(99L)).thenReturn(booking());
|
||||
when(repository.items(1L)).thenReturn(List.of(item));
|
||||
when(repository.inbound(9L, 10L)).thenReturn(inbound);
|
||||
when(repository.measuredPackageCount(9L, 10L, 20L)).thenReturn(100);
|
||||
|
||||
Plan result = service.draftByBooking(99L);
|
||||
|
||||
assertThat(result.items()).singleElement().satisfies(updated -> {
|
||||
assertThat(updated.getTotalPackageCount()).isEqualTo(100);
|
||||
assertThat(updated.getRemainingPackageCount()).isEqualTo(80);
|
||||
});
|
||||
}
|
||||
|
||||
private static LoadingPlanEntity plan(int version) {
|
||||
LoadingPlanEntity value = new LoadingPlanEntity();
|
||||
value.setId(1L);
|
||||
value.setContainerBookingId(99L);
|
||||
value.setPlanStatus("EDITING");
|
||||
value.setVersion(version);
|
||||
return value;
|
||||
}
|
||||
|
||||
private static ContainerBookingEntity booking() {
|
||||
ContainerBookingEntity value = new ContainerBookingEntity();
|
||||
value.setId(99L);
|
||||
value.setWarehouseId(100L);
|
||||
value.setChannelCode("FBA");
|
||||
value.setDestinationWarehouseCode("FBA1");
|
||||
value.setCapacityCbm(new BigDecimal("60"));
|
||||
value.setMaxWeightKg(new BigDecimal("20000"));
|
||||
return value;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user