This commit is contained in:
2026-08-13 14:37:51 +08:00
parent bc62650ca9
commit 65cd7e0a39
63 changed files with 380 additions and 226 deletions
@@ -29,7 +29,7 @@ public class MerchantInboundBillingController {
@GetMapping("/{orderId}/inbound-billing")
@PreAuthorize("hasAuthority('merchant:order:view')")
@Operation(summary = "查询订单入库结果和最新费用",
description = "客户只读查看已达到 INBOUNDED + APPROVED + CONFIRMED 的二期费用,不需要确认费用。")
description = "客户只读查看已达到 WAIT_SPLIT + APPROVED + CONFIRMED 的二期费用,不需要确认费用。")
public ApiResponse<MerchantInboundBillingResponse> find(
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable long orderId) {
return ApiResponse.success(service.find(FirstlegScopeGuard.merchant(user), orderId));
@@ -51,7 +51,7 @@ public class PlatformInboundMeasurementReviewController {
@Valid @RequestBody MeasurementReviewRequest request) {
validateScope(user, tenantId);
service.approve(tenantId, user.userId(), user.username(), inboundId, idempotencyKey, request);
// 统一订单主线要求测量审核后直接进入 INBOUNDED,不把计费、库位或其他资料
// 统一订单主线要求测量审核后直接进入 WAIT_SPLIT,不把计费、库位或其他资料
// 作为本版本的强制门禁。保留原 FeeSnapshotResponse 泛型以兼容既有客户端。
return ApiResponse.success(null);
}
@@ -10,8 +10,8 @@ public enum InboundListTab {
WAIT_REVIEW(List.of("MEASURE_PENDING")),
/** 待发送费用:等待计费、费用确认或正在完成入库。 */
WAIT_FEE_SEND(List.of("WAIT_PRICING", "FEE_CONFIRMING", "INBOUNDING")),
/** 已入库:费用已发送且入库完成。 */
INBOUNDED(List.of("INBOUNDED"));
/** 待分柜:入库和费用发布完成,等待形成有效分柜方案。 */
WAIT_SPLIT(List.of("WAIT_SPLIT"));
private final List<String> processStatuses;
@@ -18,8 +18,6 @@ public final class InboundProcessStatus {
public static final String FEE_CONFIRMING = "FEE_CONFIRMING";
/** 费用已确认,正在执行托盘、库位占用和入库收口。 */
public static final String INBOUNDING = "INBOUNDING";
/** 历史已入库终态;存量数据仍允许进入分柜。 */
public static final String INBOUNDED = "INBOUNDED";
/** 入库和费用发布完成,等待分柜。 */
public static final String WAIT_SPLIT = "WAIT_SPLIT";
/** 已进入装柜作业,PDA 可执行拣货。 */
@@ -33,7 +31,7 @@ public final class InboundProcessStatus {
public static final Set<String> VALUES = Set.of(
WAIT_MEASURE, MEASURING, MEASURE_PENDING, MEASURE_REJECTED,
WAIT_PRICING, FEE_CONFIRMING, INBOUNDING, INBOUNDED, WAIT_SPLIT,
WAIT_PRICING, FEE_CONFIRMING, INBOUNDING, WAIT_SPLIT,
LOADING, LOADED, DELIVERED, VOIDED
);
@@ -14,7 +14,7 @@ public record UnifiedInboundQuery(
Long tenantId,
@Size(max = 64) String keyword,
@Size(max = 64) String orderNo,
@Schema(allowableValues = {"PENDING_REVIEW", "WAIT_MEASURE", "INBOUNDED", "WAIT_SPLIT",
@Schema(allowableValues = {"PENDING_REVIEW", "WAIT_MEASURE", "WAIT_SPLIT",
"LOADING", "LOADED", "DELIVERED"})
@Size(max = 32) String mainStatus,
Long customerId,
@@ -62,7 +62,7 @@ public interface LoadingInboundMapper {
AND loaded.process_status<>'LOADED' AND loaded.deleted=0
SET root.process_status='LOADED', root.updated_by=#{operatorId}, root.version=root.version+1
WHERE root.tenant_id=#{tenantId} AND root.id=#{rootInboundId} AND root.deleted=0
AND root.inbound_role='MASTER' AND root.process_status IN ('INBOUNDED','LOADING')
AND root.inbound_role='MASTER' AND root.process_status IN ('WAIT_SPLIT','LOADING')
AND remainder.id IS NULL AND loaded.id IS NULL
""")
int aggregateRootLoaded(@Param("tenantId") long tenantId,
@@ -46,7 +46,7 @@ public interface InboundSplitSettlementMapper {
express_tracking_no,public_remark,created_by,created_by_name,created_at,updated_by,updated_at,deleted,version,
inbound_role,parent_inbound_id,root_inbound_id,split_sequence,split_status,split_purpose)
SELECT #{childId},tenant_id,task_id,#{childOrderId},#{childOrderNo},#{childInboundCode},
CASE WHEN #{splitPurpose}='LOADED' THEN 'LOADING' ELSE 'INBOUNDED' END,order_status_snapshot,
CASE WHEN #{splitPurpose}='LOADED' THEN 'LOADING' ELSE 'WAIT_SPLIT' END,order_status_snapshot,
'APPROVED',fee_status,fee_review_status,notification_status,expected_arrival_at,customer_id,customer_name,
company_id,company_name,warehouse_id,warehouse_name,delivery_warehouse_id,delivery_warehouse_name,business_type,
channel_code,channel_name,carrier_id,carrier_name,tax_mode,pricing_tax_mode,container_mode,customs_inspection,
@@ -151,7 +151,7 @@ public interface InboundSplitSettlementMapper {
SET package_count = GREATEST(COALESCE(package_count, total_package_count) - #{packageCount}, 0),
split_status = 'PARTIALLY_SPLIT', updated_by = #{operatorId}, version = version + 1
WHERE id = #{parentInboundId} AND tenant_id = #{accountId} AND deleted = 0
AND inbound_role = 'MASTER' AND process_status = 'INBOUNDED' AND version = #{expectedVersion}
AND inbound_role = 'MASTER' AND process_status = 'WAIT_SPLIT' AND version = #{expectedVersion}
""")
int markParentPartiallySplit(@Param("accountId") long accountId, @Param("parentInboundId") long parentInboundId,
@Param("packageCount") int packageCount, @Param("operatorId") long operatorId,
@@ -160,7 +160,7 @@ public interface InboundSplitSettlementMapper {
@Update("UPDATE fl_inbound_order SET package_count=#{loadedPackageCount},process_status='LOADING',"
+ "split_status='FULLY_SPLIT',updated_by=#{operatorId},version=version+1 "
+ "WHERE id=#{sourceInboundId} AND tenant_id=#{accountId} AND deleted=0 "
+ "AND process_status IN ('INBOUNDED','LOADING') AND split_status='NOT_SPLIT' "
+ "AND process_status IN ('WAIT_SPLIT','LOADING') AND split_status='NOT_SPLIT' "
+ "AND COALESCE(package_count,total_package_count)>=#{loadedPackageCount} AND version=#{expectedVersion}")
int markSourceLoaded(@Param("accountId") long accountId, @Param("sourceInboundId") long sourceInboundId,
@Param("loadedPackageCount") int loadedPackageCount,
@@ -23,7 +23,7 @@ import com.baiyue.firstleg.dto.SmartLoadingDtos.Candidate; import org.apache.iba
AND m.id=(SELECT MAX(m2.id) FROM fl_inbound_measurement m2
WHERE m2.tenant_id=o.tenant_id AND m2.inbound_id=o.id AND m2.status='APPROVED')
JOIN fl_inbound_measurement_item i ON i.tenant_id=o.tenant_id AND i.measurement_id=m.id
WHERE o.warehouse_id=#{warehouseId} AND o.process_status IN ('WAIT_SPLIT','INBOUNDED')
WHERE o.warehouse_id=#{warehouseId} AND o.process_status='WAIT_SPLIT'
AND (o.inbound_role='MASTER' OR (o.split_purpose='REMAINDER' AND o.split_status='NOT_SPLIT'))
AND i.inbound_detail_id IS NOT NULL
AND i.package_count>COALESCE((SELECT SUM(li.package_count) FROM fl_loading_plan_item li
@@ -61,7 +61,7 @@ import com.baiyue.firstleg.dto.SmartLoadingDtos.Candidate; import org.apache.iba
AND m.id=(SELECT MAX(m2.id) FROM fl_inbound_measurement m2
WHERE m2.tenant_id=o.tenant_id AND m2.inbound_id=o.id AND m2.status='APPROVED')
JOIN fl_inbound_measurement_item i ON i.tenant_id=o.tenant_id AND i.measurement_id=m.id
WHERE o.warehouse_id=#{warehouseId} AND o.id=#{inboundId} AND o.process_status IN ('WAIT_SPLIT','INBOUNDED')
WHERE o.warehouse_id=#{warehouseId} AND o.id=#{inboundId} AND o.process_status='WAIT_SPLIT'
AND (o.inbound_role='MASTER' OR (o.split_purpose='REMAINDER' AND o.split_status='NOT_SPLIT'))
AND i.inbound_detail_id=#{detailId}
ORDER BY m.id DESC LIMIT 1 FOR UPDATE
@@ -209,7 +209,7 @@ public class FbaPricingRepository {
.eq(InboundPricingEntity::getProcessStatus, processStatus)
.in(InboundPricingEntity::getFeeStatus, List.of("UNPRICED", "CALCULATED"))
.set(InboundPricingEntity::getFeeStatus, "CALCULATED");
if (!List.of("WAIT_SPLIT", "INBOUNDED").contains(processStatus)) {
if (!"WAIT_SPLIT".equals(processStatus)) {
update.set(InboundPricingEntity::getProcessStatus, "FEE_CONFIRMING");
}
if ("KG".equals(chargeMode)) update.set(InboundPricingEntity::getChargeableWeightKg, chargeable);
@@ -229,7 +229,7 @@ public class FbaPricingRepository {
public int markInboundConfirmed(long tenantId, long inboundId) {
return inbounds.update(null, new LambdaUpdateWrapper<InboundPricingEntity>()
.eq(InboundPricingEntity::getTenantId, tenantId).eq(InboundPricingEntity::getId, inboundId)
.in(InboundPricingEntity::getProcessStatus, List.of("FEE_CONFIRMING", "WAIT_SPLIT", "INBOUNDED"))
.in(InboundPricingEntity::getProcessStatus, List.of("FEE_CONFIRMING", "WAIT_SPLIT"))
.eq(InboundPricingEntity::getFeeStatus, "CALCULATED")
.set(InboundPricingEntity::getFeeStatus, "CONFIRMED"));
}
@@ -58,7 +58,7 @@ public class AutomaticInboundCompletionService {
}
private static boolean completed(InboundOrderEntity order) {
return ("WAIT_SPLIT".equals(order.getProcessStatus()) || "INBOUNDED".equals(order.getProcessStatus()))
return "WAIT_SPLIT".equals(order.getProcessStatus())
&& "SENT".equals(order.getFeeStatus());
}
@@ -209,7 +209,7 @@ public class FbaPricingService {
private InboundPricingEntity requireEligibleInbound(long tenantId, long inboundId) {
InboundPricingEntity inbound = requireApprovedInbound(tenantId, inboundId);
if (!List.of("WAIT_PRICING", "FEE_CONFIRMING", "WAIT_SPLIT", "INBOUNDED").contains(inbound.getProcessStatus())
if (!List.of("WAIT_PRICING", "FEE_CONFIRMING", "WAIT_SPLIT").contains(inbound.getProcessStatus())
|| !List.of("UNPRICED", "CALCULATED").contains(inbound.getFeeStatus())) {
throw conflict("当前入库单状态不允许费用试算");
}
@@ -235,7 +235,7 @@ public class FbaPricingService {
}
private static boolean isCompletedInbound(String processStatus) {
return "WAIT_SPLIT".equals(processStatus) || "INBOUNDED".equals(processStatus);
return "WAIT_SPLIT".equals(processStatus);
}
private static PricingDestinationType destinationType(InboundPricingEntity inbound) {
@@ -114,7 +114,7 @@ public class MerchantInboundBillingService {
}
private static boolean isPublished(InboundOrderEntity inbound) {
return (InboundProcessStatus.INBOUNDED.equals(inbound.getProcessStatus())
return (InboundProcessStatus.WAIT_SPLIT.equals(inbound.getProcessStatus())
|| "WAIT_SPLIT".equals(inbound.getProcessStatus()))
&& "APPROVED".equals(inbound.getMeasureReviewStatus())
&& "CONFIRMED".equals(inbound.getFeeStatus());
@@ -98,7 +98,7 @@ public class PdaPickSettlementService {
ParentRow parent = rootInboundId == parentInboundId ? root : splits.lockParent(parentInboundId);
if (parent == null || parent.tenantId() == null || parent.tenantId() <= 0
|| !("MASTER".equals(parent.inboundRole()) || "CHILD".equals(parent.inboundRole()))
|| !("INBOUNDED".equals(parent.processStatus()) || "LOADING".equals(parent.processStatus()))
|| !("WAIT_SPLIT".equals(parent.processStatus()) || "LOADING".equals(parent.processStatus()))
|| ("MASTER".equals(parent.inboundRole()) && "FULLY_SPLIT".equals(parent.splitStatus()))) {
throw conflict("待拆分主入库单不在可拆分状态");
}
@@ -28,7 +28,7 @@ import java.util.stream.Collectors;
@Service
public class UnifiedInboundQueryService {
private static final Set<String> MAIN_STATUSES = Set.of(
"PENDING_REVIEW", "WAIT_MEASURE", "INBOUNDED", "WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED");
"PENDING_REVIEW", "WAIT_MEASURE", "WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED");
private static final Set<String> REVIEW_STATUSES = Set.of("UNRECORDED", "PENDING", "APPROVED", "REJECTED");
@Resource private InboundOrderRepository inbounds;
@@ -140,7 +140,8 @@ public class UnifiedInboundQueryService {
return switch (status) {
case "WAIT_MEASURE", "MEASURING", "MEASURE_PENDING", "MEASURE_REJECTED",
"WAIT_PRICING", "FEE_CONFIRMING", "INBOUNDING" -> "WAIT_MEASURE";
case "INBOUNDED", "WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED" -> status;
case "WAIT_SPLIT" -> "WAIT_SPLIT";
case "LOADING", "LOADED", "DELIVERED" -> status;
default -> null;
};
}
@@ -150,7 +151,7 @@ public class UnifiedInboundQueryService {
return switch (mainStatus) {
case "PENDING_REVIEW" -> "APPROVE_ORDER";
case "WAIT_MEASURE" -> "MEASURE_PENDING".equals(processStatus) ? "CONFIRM_MEASUREMENT" : null;
case "INBOUNDED", "WAIT_SPLIT" -> !("INBOUNDED".equals(processStatus) || "WAIT_SPLIT".equals(processStatus)) ? null
case "WAIT_SPLIT" -> !"WAIT_SPLIT".equals(processStatus) ? null
: "CONFIRMED".equals(preloadStatus) ? null
: preloadStatus == null ? "SMART_SPLIT" : "CONFIRM_PRELOAD";
case "LOADING" -> "COMPLETE_LOADING";
@@ -169,7 +170,6 @@ public class UnifiedInboundQueryService {
return switch (mainStatus) {
case "WAIT_MEASURE" -> List.of("WAIT_MEASURE", "MEASURING", "MEASURE_PENDING", "MEASURE_REJECTED",
"WAIT_PRICING", "FEE_CONFIRMING", "INBOUNDING");
case "INBOUNDED" -> List.of("INBOUNDED");
case "WAIT_SPLIT" -> List.of("WAIT_SPLIT");
case "LOADING", "LOADED", "DELIVERED" -> List.of(mainStatus);
default -> List.of("__NO_MATCH__");
@@ -13,7 +13,7 @@ public interface SplitInboundMapper {
@Update("UPDATE fl_inbound_order SET process_status='LOADING', updated_by=#{operatorId}, "
+ "version=version+1 WHERE tenant_id=#{tenantId} AND id=#{inboundId} AND deleted=0 "
+ "AND process_status IN ('WAIT_SPLIT','INBOUNDED') AND version=#{expectedVersion}")
+ "AND process_status='WAIT_SPLIT' AND version=#{expectedVersion}")
int beginLoading(@Param("tenantId") long tenantId, @Param("inboundId") long inboundId,
@Param("operatorId") long operatorId, @Param("expectedVersion") int expectedVersion);
@@ -177,7 +177,7 @@ public class SplitPreloadService {
}
private static boolean isWaitingSplit(String status) {
return "WAIT_SPLIT".equals(status) || "INBOUNDED".equals(status);
return "WAIT_SPLIT".equals(status);
}
private void addHistory(long tenantId, long inboundId, String from, String to, String action,
@@ -11,7 +11,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_order (
order_id BIGINT UNSIGNED NOT NULL COMMENT '订单服务订单ID,不建立跨库外键',
order_no VARCHAR(32) NOT NULL COMMENT '订单号快照',
inbound_code VARCHAR(40) NOT NULL COMMENT '客户内唯一入仓码',
process_status VARCHAR(24) NOT NULL DEFAULT 'WAIT_MEASURE' COMMENT '入库流程状态:WAIT_MEASURE待测量、MEASURING测量中、MEASURE_PENDING待审核、MEASURE_REJECTED测量驳回、WAIT_PRICING待计费、FEE_CONFIRMING费用确认中、INBOUNDING入库收口中、INBOUNDED已入库、WAIT_SPLIT历史待分柜',
process_status VARCHAR(24) NOT NULL DEFAULT 'WAIT_MEASURE' COMMENT '入库流程状态:WAIT_MEASURE待测量、MEASURING测量中、MEASURE_PENDING待审核、MEASURE_REJECTED测量驳回、WAIT_PRICING待计费、FEE_CONFIRMING费用确认中、INBOUNDING入库收口中、WAIT_SPLIT待分柜',
order_status_snapshot VARCHAR(24) NOT NULL COMMENT '订单状态快照',
measure_review_status VARCHAR(24) NOT NULL DEFAULT 'NOT_SUBMITTED' COMMENT '测量审核状态:NOT_SUBMITTED未提交、PENDING待审核、APPROVED已通过、REJECTED已驳回',
fee_status VARCHAR(24) NOT NULL DEFAULT 'UNPRICED' COMMENT '费用状态:UNPRICED未计费、CALCULATED已计算、CONFIRMED已确认、SENT已发送、VOIDED已作废',
@@ -1,10 +1,9 @@
-- 新完成入库记录使用 WAIT_SPLIT,存量 INBOUNDED 继续作为可分柜兼容状态。
-- 本脚本不改写业务主表或历史状态,避免影响既有审计记录。
-- 入库完成后的唯一状态为 WAIT_SPLIT。
USE baiyue_firstleg;
DROP PROCEDURE IF EXISTS assert_inbounded_compatibility;
DROP PROCEDURE IF EXISTS assert_wait_split_status;
DELIMITER $$
CREATE PROCEDURE assert_inbounded_compatibility()
CREATE PROCEDURE assert_wait_split_status()
BEGIN
IF (SELECT COUNT(*) FROM information_schema.columns
WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_inbound_order'
@@ -13,9 +12,9 @@ BEGIN
WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_inbound_status_history'
AND column_name IN ('from_status', 'to_status')) <> 2 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'WAIT_SPLIT/INBOUNDED 兼容校验失败:入库状态字段不完整';
SET MESSAGE_TEXT = 'WAIT_SPLIT 状态校验失败:入库状态字段不完整';
END IF;
END$$
DELIMITER ;
CALL assert_inbounded_compatibility();
DROP PROCEDURE assert_inbounded_compatibility;
CALL assert_wait_split_status();
DROP PROCEDURE assert_wait_split_status;
@@ -130,14 +130,14 @@ paths:
/api/platform/v1/inbounds/{inboundId}/fees/calculate:
post:
summary: 按审核通过测量快照试算 FBA 费用
description: 仅匹配当前唯一 ACTIVE 船期价格表中的 ACTIVE 价格行,按起运仓、渠道、目的仓、pricingTaxMode 和计费单位估价;不再以 carrierId 或客户计费规则作为前置。海派还需按FBA区域和重量阶梯匹配;测量审核后即使主流程已到 INBOUNDED,也可独立试算且不回退主流程状态;需要 firstleg:fba-pricing:calculate 权限。
description: 仅匹配当前唯一 ACTIVE 船期价格表中的 ACTIVE 价格行,按起运仓、渠道、目的仓、pricingTaxMode 和计费单位估价;不再以 carrierId 或客户计费规则作为前置。海派还需按FBA区域和重量阶梯匹配;测量审核后即使主流程已到 WAIT_SPLIT,也可独立试算且不回退主流程状态;需要 firstleg:fba-pricing:calculate 权限。
parameters: [{$ref: '#/components/parameters/TenantId'}, {$ref: '#/components/parameters/InboundId'}, {$ref: '#/components/parameters/IdempotencyKey'}]
requestBody: {content: {application/json: {schema: {$ref: '#/components/schemas/CalculateRequest'}}}}
responses: {'200': {description: 成功或业务失败}, '403': {description: 无权限}}
/api/platform/v1/inbounds/{inboundId}/fees/confirm:
post:
summary: 确认最新费用快照
description: 确认费用只更新费用状态;主流程已到 INBOUNDED 时保持不变。
description: 确认费用只更新费用状态;主流程已到 WAIT_SPLIT 时保持不变。
parameters: [{$ref: '#/components/parameters/TenantId'}, {$ref: '#/components/parameters/InboundId'}, {$ref: '#/components/parameters/IdempotencyKey'}]
responses: {'200': {description: 成功或业务失败}, '403': {description: 无权限}}
/api/platform/v1/inbounds/{inboundId}/fees/send:
@@ -154,7 +154,7 @@ paths:
/api/platform/v1/inbounds/{inboundId}/fees/published:
put:
summary: 调整已发布费用
description: 需要 firstleg:fba-pricing:adjust 权限;生成新 SENT 版本并将旧版本标记 SUPERSEDED,不回退 INBOUNDED 状态。
description: 需要 firstleg:fba-pricing:adjust 权限;生成新 SENT 版本并将旧版本标记 SUPERSEDED,不回退 WAIT_SPLIT 状态。
parameters: [{$ref: '#/components/parameters/TenantId'}, {$ref: '#/components/parameters/InboundId'}, {$ref: '#/components/parameters/IdempotencyKey'}]
requestBody:
required: true
@@ -68,7 +68,7 @@ paths:
post:
summary: 按统一订单条件分页查询履约摘要(Order 内部)
description: >-
mainStatus 接受七态产品状态;WAIT_SPLIT 为正式待分柜状态,INBOUNDED 仅兼容存量已入库记录。
mainStatus 接受六态产品状态;WAIT_SPLIT 为入库完成后的唯一待分柜状态。
计费与入库处理中状态在完成前仍归并为 WAIT_MEASURE,不提前开放分柜。
security:
- internalServiceToken: []
@@ -102,7 +102,7 @@ paths:
/internal/firstleg/v1/inbounds/unified-orders/detail:
post:
summary: 按订单 ID 查询统一履约详情(Order 内部)
description: 时间线保留 Firstleg 内部 fromStatus/toStatus;summary.mainStatus 使用七态产品状态。
description: 时间线保留 Firstleg 内部 fromStatus/toStatus;summary.mainStatus 使用六态产品状态。
security:
- internalServiceToken: []
requestBody:
@@ -390,7 +390,7 @@ paths:
description: 需要 platform:firstleg:inbound:list 权限;默认查询全部企业,可选 tenantId 筛选;仅使用 V2 四个页签
parameters:
- {name: tenantId, in: query, required: false, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
- {name: tab, in: query, schema: {type: string, enum: [WAIT_MEASURE, WAIT_REVIEW, WAIT_FEE_SEND, INBOUNDED]}}
- {name: tab, in: query, schema: {type: string, enum: [WAIT_MEASURE, WAIT_REVIEW, WAIT_FEE_SEND, WAIT_SPLIT]}}
responses:
'200':
description: 成功或业务失败,按响应体 code 判断;每条记录携带后续客户隔离操作所需的 tenantId,该字段当前表示客户 ID
@@ -565,7 +565,7 @@ paths:
/api/merchant/v1/first-leg/orders/{orderId}/inbound-billing:
get:
summary: 客户查询已完成入库结果和最新已发布费用
description: 需要 merchant:order:view 权限;客户只读查看,不需要确认费用,仅 INBOUNDED + APPROVED + CONFIRMED 的二期终态可见。
description: 需要 merchant:order:view 权限;客户只读查看,不需要确认费用,仅 WAIT_SPLIT + APPROVED + CONFIRMED 的二期终态可见。
parameters:
- {name: orderId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
responses:
@@ -656,8 +656,8 @@ components:
schemas:
UnifiedMainStatus:
type: string
enum: [PENDING_REVIEW, WAIT_MEASURE, INBOUNDED, WAIT_SPLIT, LOADING, LOADED, DELIVERED]
description: 七态产品主状态;WAIT_SPLIT 为正式待分柜状态,INBOUNDED 仅兼容存量已入库记录
enum: [PENDING_REVIEW, WAIT_MEASURE, WAIT_SPLIT, LOADING, LOADED, DELIVERED]
description: 六态产品主状态;WAIT_SPLIT 为入库完成后的唯一待分柜状态
UnifiedInboundQuery:
type: object
properties:
@@ -666,9 +666,9 @@ components:
orderNo: {type: string, maxLength: 64, nullable: true}
mainStatus:
type: string
enum: [PENDING_REVIEW, WAIT_MEASURE, INBOUNDED, LOADING, LOADED, DELIVERED, WAIT_SPLIT]
enum: [PENDING_REVIEW, WAIT_MEASURE, WAIT_SPLIT, LOADING, LOADED, DELIVERED]
nullable: true
description: WAIT_SPLIT 与 INBOUNDED 为独立筛选值
description: WAIT_SPLIT 为唯一待分柜筛选值
customerId: {type: integer, format: int64, nullable: true}
receivingWarehouseId: {type: integer, format: int64, nullable: true}
destinationWarehouseId: {type: integer, format: int64, nullable: true}
@@ -767,7 +767,7 @@ components:
orderId: {type: integer, format: int64, nullable: true}
orderNo: {type: string, nullable: true}
inboundCode: {type: string, nullable: true}
processStatus: {type: string, nullable: true, description: Firstleg 内部状态;新入库完成记录进入 WAIT_SPLIT,历史记录可能为 INBOUNDED}
processStatus: {type: string, nullable: true, description: Firstleg 内部状态;入库完成记录进入 WAIT_SPLIT}
orderStatusSnapshot: {type: string, nullable: true}
measureReviewStatus: {type: string, nullable: true}
feeStatus: {type: string, nullable: true}
@@ -48,9 +48,9 @@ class FbaPricingContractTest {
.contains("/api/platform/v1/inbounds/{inboundId}/fees/send:")
.contains("/api/platform/v1/inbounds/{inboundId}/fees/published:")
.contains("Idempotency-Key")
.contains("主流程已到 INBOUNDED,也可独立试算且不回退主流程状态")
.contains("主流程已到 WAIT_SPLIT,也可独立试算且不回退主流程状态")
.contains("该动作涉及后续仓储流程,本轮二期验收不调用")
.contains("不回退 INBOUNDED 状态")
.contains("不回退 WAIT_SPLIT 状态")
.doesNotContain("应收")
.doesNotContain("应付")
.doesNotContain("待客户确认");
@@ -42,7 +42,7 @@ class InboundOrderControllerContractTest {
openapi.indexOf("/api/platform/v1/first-leg/tasks:"),
openapi.indexOf("/api/platform/v1/first-leg/tasks/{taskId}:"));
assertThat(inboundList)
.doesNotContain("WAIT_SPLIT");
.contains("WAIT_SPLIT");
assertThat(openapi)
.contains("/api/platform/inbounds:")
.contains("/api/platform/inbounds/{inboundId}:")
@@ -53,7 +53,7 @@ class InboundOrderControllerContractTest {
.contains("required: [id, tenantId]")
.contains("pricingTaxMode: {type: string, enum: [TAX_INCLUDED, TAX_EXCLUDED]")
.contains("tenantId: {type: string, pattern: '^[1-9][0-9]*$', description:")
.contains("[WAIT_MEASURE, WAIT_REVIEW, WAIT_FEE_SEND, INBOUNDED]")
.contains("[WAIT_MEASURE, WAIT_REVIEW, WAIT_FEE_SEND, WAIT_SPLIT]")
.contains("platform:firstleg:inbound:list")
.doesNotContain("WAIT_CUSTOMER_CONFIRM");
assertThat(inboundList).contains("#/components/schemas/InboundOrderPageEnvelope");
@@ -54,7 +54,7 @@ class InternalInboundOrderReferenceControllerTest {
@Test
void unifiedQueryRequiresInternalTokenAndForwardsBatchRequest() {
UnifiedInboundQuery request = new UnifiedInboundQuery(
10001L, null, null, "INBOUNDED", null, null, null,
10001L, null, null, "WAIT_SPLIT", null, null, null,
null, null, null, null, null, null, null, null, null, null, null, null,
null, null, 1, 20);
when(unifiedQueryService.page(request)).thenReturn(new PageResult<>(List.of(), 1, 20, 0, 0));
@@ -38,7 +38,7 @@ class MerchantInboundBillingControllerContractTest {
String openapi = new ClassPathResource("openapi/firstleg-v1.yaml")
.getContentAsString(StandardCharsets.UTF_8);
assertThat(openapi).contains("/api/merchant/v1/first-leg/orders/{orderId}/inbound-billing:")
.contains("INBOUNDED + APPROVED + CONFIRMED");
.contains("WAIT_SPLIT + APPROVED + CONFIRMED");
}
@Test
@@ -12,7 +12,7 @@ import static org.assertj.core.api.Assertions.assertThat;
class UnifiedInboundOpenApiContractTest {
@Test
void documentsInternalUnifiedOrderEndpointsAndSevenStatuses() throws Exception {
void documentsInternalUnifiedOrderEndpointsAndSixStatuses() throws Exception {
String yaml = new ClassPathResource("openapi/firstleg-v1.yaml")
.getContentAsString(StandardCharsets.UTF_8);
Map<String, Object> document = stringMap(new Yaml().load(yaml));
@@ -25,13 +25,13 @@ class UnifiedInboundOpenApiContractTest {
Map<String, Object> schemas = stringMap(stringMap(document.get("components")).get("schemas"));
Map<String, Object> responseStatus = stringMap(schemas.get("UnifiedMainStatus"));
assertThat(stringList(responseStatus.get("enum"))).containsExactly(
"PENDING_REVIEW", "WAIT_MEASURE", "INBOUNDED", "WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED");
assertThat(responseStatus.get("description").toString()).contains("WAIT_SPLIT", "INBOUNDED");
"PENDING_REVIEW", "WAIT_MEASURE", "WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED");
assertThat(responseStatus.get("description").toString()).contains("WAIT_SPLIT", "唯一待分柜状态");
Map<String, Object> query = stringMap(schemas.get("UnifiedInboundQuery"));
Map<String, Object> queryStatus = stringMap(stringMap(query.get("properties")).get("mainStatus"));
assertThat(stringList(queryStatus.get("enum"))).contains("WAIT_SPLIT");
assertThat(queryStatus.get("description").toString()).contains("WAIT_SPLIT", "INBOUNDED", "独立");
assertThat(queryStatus.get("description").toString()).contains("WAIT_SPLIT", "唯一待分柜筛选值");
Map<String, Object> inbound = stringMap(schemas.get("UnifiedInboundOrderSummary"));
assertThat(stringMap(inbound.get("properties"))).containsKeys(
@@ -86,7 +86,7 @@ class LoadingDeliveryServiceTest {
@Test
void completesLoadedFulfillmentSliceBeforeAggregatingRoot() {
when(repository.claimOperation(anyLong(), anyLong(), anyLong(), anyString(), anyString(), anyString())).thenReturn(true);
when(repository.findInbound(10, 20)).thenReturn(new Snapshot(20L, 10L, "INBOUNDED", 3));
when(repository.findInbound(10, 20)).thenReturn(new Snapshot(20L, 10L, "WAIT_SPLIT", 3));
when(repository.completionTarget(10, 20)).thenReturn(new CompletionTarget(
20L, null, 20L, "LOADING", 3, "LOADING", 3));
LoadingPickItemMapper.InboundProgress progress = mock(LoadingPickItemMapper.InboundProgress.class);
@@ -180,7 +180,7 @@ class LoadingDeliveryServiceTest {
@Test
void keepsHistoricalLoadedChildAsCompletionTarget() {
CompletionTarget historical = new CompletionTarget(
20L, 21L, 20L, "INBOUNDED", 3, "LOADING", 1);
20L, 21L, 20L, "WAIT_SPLIT", 3, "LOADING", 1);
assertThat(historical.completionInboundId()).isEqualTo(21L);
}
@@ -156,7 +156,7 @@ class FbaPricingRepositoryRuleListTest {
when(pricing.update(org.mockito.ArgumentMatchers.isNull(), org.mockito.ArgumentMatchers.any()))
.thenReturn(1);
assertThat(repository.markInboundFeeSent(10001L, 88L, "INBOUNDED")).isEqualTo(1);
assertThat(repository.markInboundFeeSent(10001L, 88L, "WAIT_SPLIT")).isEqualTo(1);
@SuppressWarnings("unchecked")
ArgumentCaptor<LambdaUpdateWrapper<InboundPricingEntity>> wrapper =
@@ -164,7 +164,7 @@ class FbaPricingRepositoryRuleListTest {
verify(pricing).update(org.mockito.ArgumentMatchers.isNull(), wrapper.capture());
assertThat(wrapper.getValue().getSqlSet()).contains("feeStatus").doesNotContain("processStatus");
assertThat(wrapper.getValue().getCustomSqlSegment()).contains("tenantId", "id", "processStatus", "feeStatus");
assertThat(params(wrapper.getValue()).values()).contains(10001L, 88L, "INBOUNDED", "CONFIRMED", "SENT");
assertThat(params(wrapper.getValue()).values()).contains(10001L, 88L, "WAIT_SPLIT", "CONFIRMED", "SENT");
}
@Test
@@ -11,23 +11,23 @@ import static org.assertj.core.api.Assertions.assertThat;
class InboundStatusUnificationSqlContractTest {
@Test
void preservesHistoricalWaitSplitWithoutDataRewrite() throws Exception {
void enforcesCanonicalWaitSplitStatusWithoutCompatibilityRewrite() throws Exception {
String module = normalize(new ClassPathResource("db/phase3-inbounded-status-unification.sql")
.getContentAsString(StandardCharsets.UTF_8)).trim();
String unified = normalize(Files.readString(unifiedPath(), StandardCharsets.UTF_8));
assertThat(module)
.contains("CREATE PROCEDURE assert_inbounded_compatibility()")
.contains("新完成入库记录使用 WAIT_SPLIT")
.contains("存量 INBOUNDED")
.contains("CREATE PROCEDURE assert_wait_split_status()")
.contains("入库完成后的唯一状态为 WAIT_SPLIT")
.doesNotContain("兼容")
.doesNotContain("UPDATE fl_inbound_order")
.doesNotContain("UPDATE fl_inbound_status_history");
assertThat(unified)
.contains("新完成入库记录使用 WAIT_SPLIT")
.contains("存量 INBOUNDED")
.doesNotContain("SET process_status = 'INBOUNDED'")
.doesNotContain("SET from_status = 'INBOUNDED'")
.doesNotContain("SET to_status = 'INBOUNDED'");
.contains("入库完成后的唯一状态为 WAIT_SPLIT")
.doesNotContain("历史 WAIT_SPLIT")
.doesNotContain("SET process_status = 'WAIT_SPLIT'")
.doesNotContain("SET from_status = 'WAIT_SPLIT'")
.doesNotContain("SET to_status = 'WAIT_SPLIT'");
}
private static Path unifiedPath() {
@@ -76,7 +76,7 @@ class PdaSplitSettlementSqlContractTest {
.getAnnotation(Insert.class).value()[0];
assertThat(clone)
.contains("#{childOrderId},#{childOrderNo}")
.contains("CASE WHEN #{splitPurpose}='LOADED' THEN 'LOADING' ELSE 'INBOUNDED' END");
.contains("CASE WHEN #{splitPurpose}='LOADED' THEN 'LOADING' ELSE 'WAIT_SPLIT' END");
}
@Test
@@ -89,7 +89,7 @@ class PdaSplitSettlementSqlContractTest {
long.class, long.class, int.class, long.class, int.class)
.getAnnotation(Update.class).value()[0];
assertThat(markSource)
.contains("process_status IN ('INBOUNDED','LOADING')")
.contains("process_status IN ('WAIT_SPLIT','LOADING')")
.contains("package_count=#{loadedPackageCount}")
.contains("split_status='FULLY_SPLIT'");
}
@@ -84,7 +84,7 @@ class Phase3FinalUnifiedSqlContractTest {
"-- 来源:iam/src/main/resources/db/phase3-warehouse-roles.sql",
"firstleg/src/main/resources/db/phase3-carrier-schedule-query-schema.sql");
assertBlock(unified, "-- 来源:iam/src/main/resources/db/phase3-warehouse-roles.sql",
"-- 新完成入库记录使用 WAIT_SPLIT;存量 INBOUNDED 继续作为可分柜兼容状态,统一迁移不改写主表或历史状态。",
"-- 入库完成后的唯一状态为 WAIT_SPLIT。",
"iam/src/main/resources/db/phase3-warehouse-roles.sql");
}
@@ -21,7 +21,7 @@ class SmartLoadingCandidateMapperContractTest {
.contains("m2.status='APPROVED'")
.contains("i.inbound_detail_id IS NOT NULL")
.contains("o.warehouse_id=#{warehouseId}")
.contains("o.process_status IN ('WAIT_SPLIT','INBOUNDED')")
.contains("o.process_status='WAIT_SPLIT'")
.contains("SELECT SUM(li.package_count)")
.contains("li.deleted=0")
.contains("ORDER BY o.inbounded_at ASC, o.id ASC, i.id ASC");
@@ -38,7 +38,7 @@ class SmartLoadingCandidateMapperContractTest {
.contains("m.status='APPROVED'")
.contains("SELECT MAX(m2.id)")
.contains("o.warehouse_id=#{warehouseId}")
.contains("o.process_status IN ('WAIT_SPLIT','INBOUNDED')")
.contains("o.process_status='WAIT_SPLIT'")
.contains("SELECT SUM(li.package_count)")
.contains("FOR UPDATE");
}
@@ -32,7 +32,7 @@ class AutomaticInboundCompletionServiceTest {
order("FEE_CONFIRMING", "CALCULATED", 5),
order("FEE_CONFIRMING", "CONFIRMED", 6),
order("INBOUNDING", "CONFIRMED", 7),
order("INBOUNDED", "SENT", 8));
order("WAIT_SPLIT", "SENT", 8));
FeeSnapshotResponse result = service.completeApproved(10001L, 88L, 9L, "客服", 30L, 4);
@@ -48,7 +48,7 @@ class AutomaticInboundCompletionServiceTest {
@Test
void shouldHandleCase02() {
FeeSnapshotResponse fee = fee(501L);
when(workflow.findOwned(10001L, 88L)).thenReturn(order("INBOUNDED", "SENT", 8));
when(workflow.findOwned(10001L, 88L)).thenReturn(order("WAIT_SPLIT", "SENT", 8));
when(pricing.latest(10001L, 88L)).thenReturn(fee);
assertThat(service.completeApproved(10001L, 88L, 9L, "客服", 30L, 4)).isSameAs(fee);
@@ -65,7 +65,7 @@ class AutomaticInboundCompletionServiceTest {
FeeSnapshotResponse fee = fee(501L);
when(pricing.latest(10001L, 88L)).thenReturn(fee);
when(workflow.findOwned(10001L, 88L)).thenReturn(
order("INBOUNDING", "CONFIRMED", 7), order("INBOUNDED", "SENT", 8));
order("INBOUNDING", "CONFIRMED", 7), order("WAIT_SPLIT", "SENT", 8));
service.completeApproved(10001L, 88L, 9L, "客服", 30L, 4);
@@ -185,7 +185,7 @@ class FbaPricingServiceTest {
void returnsOriginalSnapshotForRepeatedCalculation() {
InboundPricingIdempotencyEntity completed = completed("FEE_CALCULATE", 501L);
InboundPricingEntity inbound = inbound("10", "1");
inbound.setProcessStatus("INBOUNDED");
inbound.setProcessStatus("WAIT_SPLIT");
when(repository.findIdempotency(TENANT, OPERATOR, "FEE_CALCULATE", "same")).thenReturn(completed);
when(repository.findFee(TENANT, 501L)).thenReturn(savedFee("CALCULATED"));
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
@@ -195,7 +195,7 @@ class FbaPricingServiceTest {
FeeSnapshotResponse response = service.calculate(TENANT, OPERATOR, INBOUND, NOW, "same");
assertThat(response.feeId()).isEqualTo(501L);
assertThat(response.nextProcessStatus()).isEqualTo("INBOUNDED");
assertThat(response.nextProcessStatus()).isEqualTo("WAIT_SPLIT");
verify(repository, never()).matchRules(any(), any());
verify(repository, never()).insertFee(any());
}
@@ -236,7 +236,7 @@ class FbaPricingServiceTest {
@DisplayName("已入库费用发送只发布费用且不回退主流程状态")
void sendsFeeAfterInboundWithoutRollingBackProcessStatus() {
InboundPricingEntity inbound = inbound("10", "1");
inbound.setProcessStatus("INBOUNDED");
inbound.setProcessStatus("WAIT_SPLIT");
inbound.setFeeStatus("CONFIRMED");
InboundFeeEntity confirmed = savedFee("CONFIRMED");
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
@@ -245,14 +245,14 @@ class FbaPricingServiceTest {
doAnswer(invocation -> { ((InboundPricingIdempotencyEntity) invocation.getArgument(0)).setId(900L); return null; })
.when(repository).insertIdempotency(any());
when(repository.sendFee(TENANT, 501L, OPERATOR, NOW)).thenReturn(1);
when(repository.markInboundFeeSent(TENANT, INBOUND, "INBOUNDED")).thenReturn(1);
when(repository.markInboundFeeSent(TENANT, INBOUND, "WAIT_SPLIT")).thenReturn(1);
when(repository.completeIdempotency(any(), eq(501L), eq(NOW))).thenReturn(1);
when(repository.findFee(TENANT, 501L)).thenReturn(confirmed);
FeeSnapshotResponse response = service.send(TENANT, OPERATOR, INBOUND, "send-inbounded");
assertThat(response.nextProcessStatus()).isEqualTo("INBOUNDED");
verify(repository).markInboundFeeSent(TENANT, INBOUND, "INBOUNDED");
assertThat(response.nextProcessStatus()).isEqualTo("WAIT_SPLIT");
verify(repository).markInboundFeeSent(TENANT, INBOUND, "WAIT_SPLIT");
verify(financeOutbox).publishInbound(eq(FinanceFeeSourceOutboxService.PUBLISHED),
eq(TENANT), eq(INBOUND), eq(501L), eq(1), eq(NOW));
}
@@ -274,7 +274,7 @@ class FbaPricingServiceTest {
@DisplayName("已发布费用可生成新版本且不回退已入库状态")
void adjustsPublishedFeeWithoutRollingBackInbound() {
InboundPricingEntity inbound = inbound("10", "1");
inbound.setProcessStatus("INBOUNDED");
inbound.setProcessStatus("WAIT_SPLIT");
inbound.setFeeStatus("SENT");
InboundFeeEntity current = savedFee("SENT");
AtomicReference<InboundFeeEntity> inserted = new AtomicReference<>();
@@ -301,8 +301,8 @@ class FbaPricingServiceTest {
assertThat(response.feeVersion()).isEqualTo(2);
assertThat(response.feeStatus()).isEqualTo("SENT");
assertThat(response.totalAmount()).isEqualByComparingTo("120.00");
assertThat(response.nextProcessStatus()).isEqualTo("INBOUNDED");
assertThat(inbound.getProcessStatus()).isEqualTo("INBOUNDED");
assertThat(response.nextProcessStatus()).isEqualTo("WAIT_SPLIT");
assertThat(inbound.getProcessStatus()).isEqualTo("WAIT_SPLIT");
verify(repository).supersedeSent(TENANT, 501L);
verify(financeOutbox).publishInbound(eq(FinanceFeeSourceOutboxService.ADJUSTED),
eq(TENANT), eq(INBOUND), eq(502L), eq(2), eq(NOW));
@@ -506,7 +506,7 @@ class InboundMeasurementServiceTest {
@Test
void editingAndResubmittingInboundMeasurementDoesNotRollBackMainStatus() {
InboundOrderEntity inbound = inbound("INBOUNDED");
InboundOrderEntity inbound = inbound("WAIT_SPLIT");
inbound.setMeasureReviewStatus("APPROVED");
InboundMeasurementEntity measurement = new InboundMeasurementEntity();
measurement.setId(30L);
@@ -524,7 +524,7 @@ class InboundMeasurementServiceTest {
InboundMeasurementResponses.MeasurementSubmitted result = service.submit(1L, 10L, 9L,
new InboundMeasurementRequests.SubmitRequest(30L, 2));
assertThat(result.processStatus()).isEqualTo("INBOUNDED");
assertThat(result.processStatus()).isEqualTo("WAIT_SPLIT");
assertThat(result.measureReviewStatus()).isEqualTo("APPROVED");
assertThat(measurement.getStatus()).isEqualTo("APPROVED");
verify(repository, never()).updateMeasurement(any());
@@ -42,7 +42,7 @@ class InboundOrderQueryServiceTest {
@Test
void shouldAcceptInboundedAsThePhase2TerminalStatusFilter() {
InboundOrderQuery query = new InboundOrderQuery(null, null, "INBOUNDED", null,
InboundOrderQuery query = new InboundOrderQuery(null, null, "WAIT_SPLIT", null,
"APPROVED", "CONFIRMED", null, null, null, null, null, null, null,
null, null, null, null, 1, 200);
PageResult<InboundOrderSummary> expected = new PageResult<>(List.of(), 1, 200, 0, 0);
@@ -59,7 +59,7 @@ class InboundOrderQueryServiceTest {
assertThat(InboundListTab.WAIT_REVIEW.processStatuses()).containsExactly("MEASURE_PENDING");
assertThat(InboundListTab.WAIT_FEE_SEND.processStatuses())
.containsExactly("WAIT_PRICING", "FEE_CONFIRMING", "INBOUNDING");
assertThat(InboundListTab.INBOUNDED.processStatuses()).containsExactly("INBOUNDED");
assertThat(InboundListTab.WAIT_SPLIT.processStatuses()).containsExactly("WAIT_SPLIT");
assertThat(List.of(InboundListTab.values())).extracting(Enum::name)
.doesNotContain("WAIT_CUSTOMER_CONFIRM");
}
@@ -36,13 +36,13 @@ class MerchantInboundBillingServiceTest {
@Test
void shouldHandleCase01() {
InboundOrderEntity inbound = inbound("INBOUNDED", "APPROVED", "CONFIRMED");
InboundOrderEntity inbound = inbound("WAIT_SPLIT", "APPROVED", "CONFIRMED");
InboundOrderDetail detail = new InboundOrderDetail(mock(com.baiyue.firstleg.dto.InboundOrderSummary.class),
"客户备注", List.of(), List.of(
new InboundOrderDetail.FileSummary(1L, "RELATED", "public.pdf", "PUBLIC", 1, 1L, null),
new InboundOrderDetail.FileSummary(2L, "INTERNAL", "internal.pdf", "INTERNAL", 1, 1L, null)),
List.of(new InboundOrderDetail.InternalNoteSummary(1L, "内部备注", 1L, "客服", null)),
List.of(new InboundOrderDetail.StatusHistorySummary(null, "INBOUNDED", "COMPLETE",
List.of(new InboundOrderDetail.StatusHistorySummary(null, "WAIT_SPLIT", "COMPLETE",
"内部原因", 1L, "客服", null)));
InboundMeasurementResponses.Detail measurement = mock(InboundMeasurementResponses.Detail.class);
FeeSnapshotResponse fee = mock(FeeSnapshotResponse.class);
@@ -142,7 +142,7 @@ class MerchantInboundBillingServiceTest {
@Test
void shouldRejectHistoricalPublishedStatusCombination() {
InboundOrderEntity inbound = inbound("INBOUNDED", "APPROVED", "SENT");
InboundOrderEntity inbound = inbound("WAIT_SPLIT", "APPROVED", "SENT");
when(orders.findByOrderId(10001L, 77L)).thenReturn(inbound);
assertThatThrownBy(() -> service.find(10001L, 77L))
@@ -180,7 +180,7 @@ class PdaPickSettlementServiceTest {
void usesTotalPackageCountWhenUnsplitMeasuredInboundHasZeroAvailableColumn() {
LoadingPickItemEntity item = item(11L, 21L, 31L, 41L, 20, 20, "PICKED");
resolved(List.of(item));
when(splits.lockParent(31L)).thenReturn(parent(20, 0, "INBOUNDED", "NOT_SPLIT"));
when(splits.lockParent(31L)).thenReturn(parent(20, 0, "WAIT_SPLIT", "NOT_SPLIT"));
when(splits.lockLatestApprovedItemsForInbound(10001L, 31L))
.thenReturn(List.of(new MeasurementItemRow(51L, 61L, 41L, 20)));
when(allocations.lockConfirmed(10001L, 31L))
@@ -336,7 +336,7 @@ class PdaPickSettlementServiceTest {
"IN-001", "MASTER", 31L, "LOADING", 5, 5, "FULLY_SPLIT", 4));
when(splits.lockParent(inboundId)).thenReturn(new ParentRow(inboundId, 10001L, 902L,
"ORDER-REMAINDER", "IN-001-" + (sourceVersion - 1), "CHILD", 31L,
"INBOUNDED", packageCount, packageCount, "NOT_SPLIT", sourceVersion));
"WAIT_SPLIT", packageCount, packageCount, "NOT_SPLIT", sourceVersion));
when(splits.lockLatestApprovedItemsForInbound(10001L, inboundId))
.thenReturn(List.of(new MeasurementItemRow(measurementItemId, measurementId, detailId, packageCount)));
when(allocations.lockConfirmed(10001L, inboundId)).thenAnswer(invocation ->
@@ -408,7 +408,7 @@ class PdaPickSettlementServiceTest {
}
private static ParentRow parent(int packageCount) {
return parent(packageCount, packageCount, "INBOUNDED", "NOT_SPLIT");
return parent(packageCount, packageCount, "WAIT_SPLIT", "NOT_SPLIT");
}
private static ParentRow parent(int totalPackageCount, int packageCount, String processStatus, String splitStatus) {
@@ -32,7 +32,6 @@ class UnifiedInboundQueryServiceTest {
assertThat(UnifiedInboundQueryService.mainStatus("FEE_CONFIRMING")).isEqualTo("WAIT_MEASURE");
assertThat(UnifiedInboundQueryService.mainStatus("INBOUNDING")).isEqualTo("WAIT_MEASURE");
assertThat(UnifiedInboundQueryService.mainStatus("WAIT_SPLIT")).isEqualTo("WAIT_SPLIT");
assertThat(UnifiedInboundQueryService.mainStatus("INBOUNDED")).isEqualTo("INBOUNDED");
assertThat(UnifiedInboundQueryService.mainStatus("LOADING")).isEqualTo("LOADING");
assertThat(UnifiedInboundQueryService.mainStatus("LOADED")).isEqualTo("LOADED");
assertThat(UnifiedInboundQueryService.mainStatus("DELIVERED")).isEqualTo("DELIVERED");
@@ -44,25 +43,21 @@ class UnifiedInboundQueryServiceTest {
assertThat(UnifiedInboundQueryService.nextAction("WAIT_MEASURE", "MEASURING", null)).isNull();
assertThat(UnifiedInboundQueryService.nextAction("WAIT_MEASURE", "MEASURE_PENDING", null))
.isEqualTo("CONFIRM_MEASUREMENT");
assertThat(UnifiedInboundQueryService.nextAction("INBOUNDED", "INBOUNDED", null))
.isEqualTo("SMART_SPLIT");
assertThat(UnifiedInboundQueryService.nextAction("INBOUNDED", "INBOUNDED", "PENDING"))
.isEqualTo("CONFIRM_PRELOAD");
assertThat(UnifiedInboundQueryService.nextAction("INBOUNDED", "INBOUNDING", null)).isNull();
assertThat(UnifiedInboundQueryService.nextAction("INBOUNDED", "INBOUNDED", "CONFIRMED")).isNull();
assertThat(UnifiedInboundQueryService.nextAction("WAIT_SPLIT", "WAIT_SPLIT", null))
.isEqualTo("SMART_SPLIT");
assertThat(UnifiedInboundQueryService.nextAction("WAIT_SPLIT", "WAIT_SPLIT", "PENDING"))
.isEqualTo("CONFIRM_PRELOAD");
assertThat(UnifiedInboundQueryService.nextAction("WAIT_SPLIT", "INBOUNDING", null)).isNull();
assertThat(UnifiedInboundQueryService.nextAction("WAIT_SPLIT", "WAIT_SPLIT", "CONFIRMED")).isNull();
assertThat(UnifiedInboundQueryService.nextAction("LOADED", "LOADED", null)).isNull();
assertThat(UnifiedInboundQueryService.nextAction("DELIVERED", "DELIVERED", null)).isNull();
}
@Test
void inboundedAndWaitSplitFiltersRemainDistinct() throws Exception {
void waitSplitFilterUsesOnlyCanonicalStatus() throws Exception {
var method = UnifiedInboundQueryService.class.getDeclaredMethod("internalStatuses", String.class);
method.setAccessible(true);
assertThat(method.invoke(new UnifiedInboundQueryService(), "INBOUNDED"))
.isEqualTo(java.util.List.of("INBOUNDED"));
assertThat(method.invoke(new UnifiedInboundQueryService(), "WAIT_SPLIT"))
.isEqualTo(java.util.List.of("WAIT_SPLIT"));
}
@@ -70,14 +65,14 @@ class UnifiedInboundQueryServiceTest {
@Test
void phaseTwoMeasurementStatusesDoNotRequireUndeployedPhaseThreeTables() {
assertThat(UnifiedInboundQueryService.requiresPhaseThreeSummary("WAIT_MEASURE")).isFalse();
assertThat(UnifiedInboundQueryService.requiresPhaseThreeSummary("INBOUNDED")).isFalse();
assertThat(UnifiedInboundQueryService.requiresPhaseThreeSummary("WAIT_SPLIT")).isFalse();
assertThat(UnifiedInboundQueryService.requiresPhaseThreeSummary("LOADING")).isTrue();
assertThat(UnifiedInboundQueryService.requiresPhaseThreeSummary("LOADED")).isTrue();
assertThat(UnifiedInboundQueryService.requiresPhaseThreeSummary("DELIVERED")).isTrue();
}
@ParameterizedTest
@ValueSource(strings = {"WAIT_MEASURE", "INBOUNDED", "WAIT_SPLIT"})
@ValueSource(strings = {"WAIT_MEASURE", "WAIT_SPLIT"})
void phaseTwoDetailDoesNotQueryPhaseThreeTables(String processStatus) {
InboundOrderRepository inbounds = mock(InboundOrderRepository.class);
UnifiedInboundReadRepository operations = mock(UnifiedInboundReadRepository.class);