feat: add dropship fulfillment producer chain
This commit is contained in:
@@ -583,6 +583,7 @@ CREATE TABLE IF NOT EXISTS ord_order (
|
||||
container_mode VARCHAR(24) NULL COMMENT '装运方式:FULL_CONTAINER整柜、LCL散货拼箱',
|
||||
customs_inspection TINYINT(1) NULL COMMENT '是否需要商检:0否、1是',
|
||||
destination_type VARCHAR(24) NULL COMMENT '目的仓类型:FBA亚马逊仓、OVERSEAS海外仓、PRIVATE私人地址',
|
||||
fulfillment_type VARCHAR(32) NOT NULL DEFAULT 'STANDARD' COMMENT '履约类型:STANDARD普通头程、ONE_CLICK_DROPSHIP一键代发',
|
||||
amazon_warehouse_code VARCHAR(32) NULL COMMENT 'Amazon目的仓编码',
|
||||
shipment_id VARCHAR(64) NULL COMMENT '订单级Amazon Shipment ID',
|
||||
reference_id VARCHAR(64) NULL COMMENT '订单级Amazon Reference ID',
|
||||
@@ -604,6 +605,26 @@ CREATE TABLE IF NOT EXISTS ord_order (
|
||||
KEY idx_order_tenant_warehouse (tenant_id, destination_warehouse_id)
|
||||
) ENGINE=InnoDB COMMENT='头程订单主表:保存商户委托、目的仓、运输方式和订单当前状态';
|
||||
|
||||
SET @order_fulfillment_type_sql = IF(
|
||||
EXISTS(
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'baiyue_order' AND table_name = 'ord_order'
|
||||
AND column_name = 'fulfillment_type'
|
||||
),
|
||||
'SELECT 1',
|
||||
'ALTER TABLE baiyue_order.ord_order ADD COLUMN fulfillment_type VARCHAR(32) NULL COMMENT ''履约类型:STANDARD普通头程、ONE_CLICK_DROPSHIP一键代发'' AFTER destination_type'
|
||||
);
|
||||
PREPARE order_fulfillment_type_stmt FROM @order_fulfillment_type_sql;
|
||||
EXECUTE order_fulfillment_type_stmt;
|
||||
DEALLOCATE PREPARE order_fulfillment_type_stmt;
|
||||
|
||||
UPDATE baiyue_order.ord_order
|
||||
SET fulfillment_type = 'STANDARD'
|
||||
WHERE fulfillment_type IS NULL OR fulfillment_type = '';
|
||||
ALTER TABLE baiyue_order.ord_order
|
||||
MODIFY COLUMN fulfillment_type VARCHAR(32) NOT NULL DEFAULT 'STANDARD'
|
||||
COMMENT '履约类型:STANDARD普通头程、ONE_CLICK_DROPSHIP一键代发';
|
||||
|
||||
-- 既有开发库可能仍保留草稿入仓码 NOT NULL 约束;必须先放宽再清理草稿入仓码。
|
||||
SET @order_inbound_code_nullable_sql = IF(
|
||||
EXISTS(
|
||||
@@ -2531,6 +2552,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_order (
|
||||
delivery_warehouse_id BIGINT UNSIGNED NULL COMMENT '交货仓逻辑ID',
|
||||
delivery_warehouse_name VARCHAR(128) NULL COMMENT '交货仓名称快照',
|
||||
business_type VARCHAR(32) NOT NULL COMMENT '业务类型',
|
||||
fulfillment_type VARCHAR(32) NOT NULL DEFAULT 'STANDARD' COMMENT '履约类型:STANDARD普通头程、ONE_CLICK_DROPSHIP一键代发',
|
||||
channel_code VARCHAR(64) NULL COMMENT '渠道编码',
|
||||
channel_name VARCHAR(128) NULL COMMENT '渠道名称快照',
|
||||
carrier_id BIGINT UNSIGNED NULL COMMENT '计费船司逻辑ID',
|
||||
@@ -2581,6 +2603,26 @@ CREATE TABLE IF NOT EXISTS fl_inbound_order (
|
||||
CONSTRAINT fk_inbound_task FOREIGN KEY (task_id) REFERENCES fl_fulfillment_task (id)
|
||||
) ENGINE=InnoDB COMMENT='二期头程入库单聚合根和列表查询快照';
|
||||
|
||||
SET @inbound_fulfillment_type_sql = IF(
|
||||
EXISTS(
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE() AND table_name = 'fl_inbound_order'
|
||||
AND column_name = 'fulfillment_type'
|
||||
),
|
||||
'SELECT 1',
|
||||
'ALTER TABLE fl_inbound_order ADD COLUMN fulfillment_type VARCHAR(32) NULL COMMENT ''履约类型:STANDARD普通头程、ONE_CLICK_DROPSHIP一键代发'' AFTER business_type'
|
||||
);
|
||||
PREPARE inbound_fulfillment_type_stmt FROM @inbound_fulfillment_type_sql;
|
||||
EXECUTE inbound_fulfillment_type_stmt;
|
||||
DEALLOCATE PREPARE inbound_fulfillment_type_stmt;
|
||||
|
||||
UPDATE fl_inbound_order
|
||||
SET fulfillment_type = 'STANDARD'
|
||||
WHERE fulfillment_type IS NULL OR fulfillment_type = '';
|
||||
ALTER TABLE fl_inbound_order
|
||||
MODIFY COLUMN fulfillment_type VARCHAR(32) NOT NULL DEFAULT 'STANDARD'
|
||||
COMMENT '履约类型:STANDARD普通头程、ONE_CLICK_DROPSHIP一键代发';
|
||||
|
||||
SET @inbound_global_code_index_exists := (
|
||||
SELECT COUNT(*) FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE() AND table_name = 'fl_inbound_order'
|
||||
@@ -4323,8 +4365,10 @@ CALL unified_assert_column('baiyue_order', 'ord_order', 'carrier_id');
|
||||
CALL unified_assert_column('baiyue_order', 'ord_order', 'carrier_name');
|
||||
CALL unified_assert_column('baiyue_order', 'ord_order', 'pricing_tax_mode');
|
||||
CALL unified_assert_column('baiyue_order', 'ord_order', 'inbound_code');
|
||||
CALL unified_assert_column('baiyue_order', 'ord_order', 'fulfillment_type');
|
||||
CALL unified_assert_column('baiyue_firstleg', 'fl_inbound_pallet_allocation', 'warehouse_resource_tenant_id');
|
||||
CALL unified_assert_column('baiyue_firstleg', 'fl_inbound_order', 'pricing_tax_mode');
|
||||
CALL unified_assert_column('baiyue_firstleg', 'fl_inbound_order', 'fulfillment_type');
|
||||
CALL unified_assert_column('baiyue_firstleg', 'fl_inbound_measurement_item', 'external_detail_no');
|
||||
CALL unified_assert_table('baiyue_firstleg', 'fl_price_channel_catalog');
|
||||
CALL unified_assert_table('baiyue_firstleg', 'fl_price_origin_alias');
|
||||
|
||||
+7
-1
@@ -59,6 +59,9 @@ public class InternalInboundOrderController {
|
||||
|| command.warehouseId() <= 0 || command.customerName() == null || command.customerName().isBlank()
|
||||
|| command.inboundCode() == null || command.inboundCode().isBlank()
|
||||
|| command.inboundCode().length() > 40
|
||||
|| command.fulfillmentType() == null
|
||||
|| (!"STANDARD".equals(command.fulfillmentType())
|
||||
&& !"ONE_CLICK_DROPSHIP".equals(command.fulfillmentType()))
|
||||
|| command.totalPackageCount() != null && command.totalPackageCount() < 1) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "订单入库快照不完整");
|
||||
}
|
||||
@@ -142,7 +145,10 @@ public class InternalInboundOrderController {
|
||||
|| command.customerName() == null || command.customerName().isBlank()
|
||||
|| command.warehouseId() == null || command.warehouseId() <= 0
|
||||
|| command.warehouseName() == null || command.warehouseName().isBlank()
|
||||
|| command.businessType() == null || command.businessType().isBlank()) {
|
||||
|| command.businessType() == null || command.businessType().isBlank()
|
||||
|| command.fulfillmentType() == null
|
||||
|| (!"STANDARD".equals(command.fulfillmentType())
|
||||
&& !"ONE_CLICK_DROPSHIP".equals(command.fulfillmentType()))) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "草稿预入库快照不完整");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ public record InboundOrderSummary(
|
||||
Long customerId, String customerName, Long companyId, String companyName,
|
||||
Long warehouseId, String warehouseName,
|
||||
Long deliveryWarehouseId, String deliveryWarehouseName,
|
||||
String businessType, String channelCode, String channelName,
|
||||
String businessType, String fulfillmentType, String channelCode, String channelName,
|
||||
String taxMode, String pricingTaxMode, String containerMode, Boolean customsInspection,
|
||||
String destinationType, Long destinationWarehouseId, String destinationCode, String destinationName,
|
||||
Boolean holdFlag, String currentHoldReason,
|
||||
@@ -56,6 +56,7 @@ public record InboundOrderSummary(
|
||||
order.getCustomerId(), order.getCustomerName(),
|
||||
order.getCompanyId(), order.getCompanyName(), order.getWarehouseId(), order.getWarehouseName(),
|
||||
order.getDeliveryWarehouseId(), order.getDeliveryWarehouseName(), order.getBusinessType(),
|
||||
order.getFulfillmentType(),
|
||||
order.getChannelCode(), order.getChannelName(), order.getTaxMode(), order.getPricingTaxMode(),
|
||||
order.getContainerMode(),
|
||||
order.getCustomsInspection(), order.getDestinationType(), order.getDestinationWarehouseId(),
|
||||
|
||||
@@ -33,7 +33,8 @@ public record OrderApprovedInboundCommand(
|
||||
String expressTrackingNo,
|
||||
Integer totalPackageCount,
|
||||
String pickupMethod,
|
||||
LocalDateTime requestedPickupAt
|
||||
LocalDateTime requestedPickupAt,
|
||||
String fulfillmentType
|
||||
) {
|
||||
public OrderApprovedInboundCommand(
|
||||
long tenantId, long orderId, String orderNo, String inboundCode, Long customerId, String customerName,
|
||||
@@ -48,7 +49,7 @@ public record OrderApprovedInboundCommand(
|
||||
deliveryWarehouseId, deliveryWarehouseName, expectedArrivalAt, businessType, channelCode, channelName,
|
||||
carrierId, carrierName, taxMode, pricingTaxMode, containerMode, customsInspection, destinationType,
|
||||
destinationWarehouseId, destinationCode, destinationName, publicRemark, expressCompanyName,
|
||||
expressTrackingNo, totalPackageCount, null, null);
|
||||
expressTrackingNo, totalPackageCount, null, null, "STANDARD");
|
||||
}
|
||||
/** 兼容尚未携带订单总箱数的历史事件。 */
|
||||
public OrderApprovedInboundCommand(
|
||||
@@ -63,7 +64,7 @@ public record OrderApprovedInboundCommand(
|
||||
deliveryWarehouseId, deliveryWarehouseName, expectedArrivalAt, businessType, channelCode, channelName,
|
||||
carrierId, carrierName, taxMode, pricingTaxMode, containerMode, customsInspection, destinationType,
|
||||
destinationWarehouseId, destinationCode, destinationName, publicRemark, expressCompanyName,
|
||||
expressTrackingNo, null, null, null);
|
||||
expressTrackingNo, null, null, null, "STANDARD");
|
||||
}
|
||||
|
||||
/** 兼容未携带报价包税维度的历史调用方。 */
|
||||
@@ -79,6 +80,6 @@ public record OrderApprovedInboundCommand(
|
||||
deliveryWarehouseId, deliveryWarehouseName, expectedArrivalAt, businessType, channelCode, channelName,
|
||||
carrierId, carrierName, taxMode, null, containerMode, customsInspection, destinationType,
|
||||
destinationWarehouseId, destinationCode, destinationName, publicRemark, expressCompanyName,
|
||||
expressTrackingNo, null, null, null);
|
||||
expressTrackingNo, null, null, null, "STANDARD");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ public class InboundOrderEntity extends BaseEntity {
|
||||
private Long deliveryWarehouseId;
|
||||
private String deliveryWarehouseName;
|
||||
private String businessType;
|
||||
private String fulfillmentType;
|
||||
private String channelCode;
|
||||
private String channelName;
|
||||
private Long carrierId;
|
||||
|
||||
@@ -38,7 +38,7 @@ public interface InboundSplitSettlementMapper {
|
||||
INSERT INTO fl_inbound_order (id,tenant_id,task_id,order_id,order_no,inbound_code,process_status,
|
||||
order_status_snapshot,measure_review_status,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,
|
||||
delivery_warehouse_id,delivery_warehouse_name,business_type,fulfillment_type,channel_code,channel_name,carrier_id,carrier_name,
|
||||
tax_mode,pricing_tax_mode,container_mode,customs_inspection,destination_type,destination_warehouse_id,
|
||||
destination_code,destination_name,hold_flag,current_hold_reason,total_package_count,package_count,
|
||||
actual_gross_weight_kg,chargeable_weight_kg,actual_volume_cbm,chargeable_volume_cbm,pallet_count,volume_weight_kg,
|
||||
@@ -48,7 +48,7 @@ public interface InboundSplitSettlementMapper {
|
||||
SELECT #{childId},tenant_id,task_id,#{childOrderId},#{childOrderNo},#{childInboundCode},
|
||||
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,
|
||||
company_id,company_name,warehouse_id,warehouse_name,delivery_warehouse_id,delivery_warehouse_name,business_type,fulfillment_type,
|
||||
channel_code,channel_name,carrier_id,carrier_name,tax_mode,pricing_tax_mode,container_mode,customs_inspection,
|
||||
destination_type,destination_warehouse_id,destination_code,destination_name,hold_flag,current_hold_reason,
|
||||
#{packageCount},#{packageCount},
|
||||
|
||||
@@ -19,7 +19,7 @@ public interface LoadingCompletedEventMapper {
|
||||
COALESCE(o.root_inbound_id, o.id) AS root_inbound_id,
|
||||
o.order_id AS source_order_id, o.order_no AS source_order_no,
|
||||
COALESCE(root_order.order_id, o.order_id) AS root_order_id,
|
||||
o.inbound_code, o.business_type, o.destination_type, o.destination_warehouse_id, o.destination_code AS destination_warehouse_code,
|
||||
o.inbound_code, o.business_type, o.fulfillment_type, o.destination_type, o.destination_warehouse_id, o.destination_code AS destination_warehouse_code,
|
||||
o.destination_name AS destination_warehouse_name,
|
||||
i.id AS loading_plan_item_id, i.inbound_detail_id AS source_inbound_detail_id, i.sku_code,
|
||||
i.product_name_zh, i.product_name_en, i.package_count, i.product_quantity,
|
||||
@@ -73,7 +73,7 @@ public interface LoadingCompletedEventMapper {
|
||||
private Integer productQuantity; private Integer palletCount; private BigDecimal grossWeightKg;
|
||||
private BigDecimal netWeightKg; private BigDecimal dimensionalWeightKg; private BigDecimal volumeCbm;
|
||||
private String fbaTrackingNo; private String shipmentNo; private String shippingMark;
|
||||
private String businessType; private String destinationType;
|
||||
private String businessType; private String fulfillmentType; private String destinationType;
|
||||
private String customerRemark; private String internalRemark; private String warehouseRemark;
|
||||
}
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@ public class InboundOrderCreationService {
|
||||
inbound.setDeliveryWarehouseId(command.deliveryWarehouseId());
|
||||
inbound.setDeliveryWarehouseName(command.deliveryWarehouseName());
|
||||
inbound.setBusinessType(command.businessType());
|
||||
inbound.setFulfillmentType(fulfillmentType(command.fulfillmentType()));
|
||||
inbound.setChannelCode(command.channelCode());
|
||||
inbound.setChannelName(command.channelName());
|
||||
inbound.setCarrierId(command.carrierId());
|
||||
@@ -160,6 +161,7 @@ public class InboundOrderCreationService {
|
||||
.set(InboundOrderEntity::getDeliveryWarehouseName, command.deliveryWarehouseName())
|
||||
.set(InboundOrderEntity::getExpectedArrivalAt, command.expectedArrivalAt())
|
||||
.set(InboundOrderEntity::getBusinessType, command.businessType())
|
||||
.set(InboundOrderEntity::getFulfillmentType, fulfillmentType(command.fulfillmentType()))
|
||||
.set(InboundOrderEntity::getChannelCode, command.channelCode())
|
||||
.set(InboundOrderEntity::getChannelName, command.channelName())
|
||||
.set(InboundOrderEntity::getDestinationType, command.destinationType())
|
||||
@@ -181,6 +183,7 @@ public class InboundOrderCreationService {
|
||||
.set(InboundOrderEntity::getProcessStatus, "WAIT_MEASURE")
|
||||
.set(InboundOrderEntity::getOrderStatusSnapshot, "APPROVED")
|
||||
.set(InboundOrderEntity::getTotalPackageCount, command.totalPackageCount())
|
||||
.set(InboundOrderEntity::getFulfillmentType, fulfillmentType(command.fulfillmentType()))
|
||||
.set(InboundOrderEntity::getUpdatedAt, LocalDateTime.now())
|
||||
.setSql("version = version + 1");
|
||||
if (orders.update(null, update) != 1) {
|
||||
@@ -217,6 +220,7 @@ public class InboundOrderCreationService {
|
||||
inbound.setDeliveryWarehouseId(command.deliveryWarehouseId());
|
||||
inbound.setDeliveryWarehouseName(command.deliveryWarehouseName());
|
||||
inbound.setBusinessType(command.businessType());
|
||||
inbound.setFulfillmentType(fulfillmentType(command.fulfillmentType()));
|
||||
inbound.setChannelCode(command.channelCode());
|
||||
inbound.setChannelName(command.channelName());
|
||||
inbound.setCarrierId(command.carrierId());
|
||||
@@ -241,6 +245,12 @@ public class InboundOrderCreationService {
|
||||
return inbound;
|
||||
}
|
||||
|
||||
private static String fulfillmentType(String value) {
|
||||
if ("STANDARD".equals(value) || "ONE_CLICK_DROPSHIP".equals(value)) return value;
|
||||
throw new BizException(ResultCode.BAD_REQUEST,
|
||||
"履约类型必须为 STANDARD 或 ONE_CLICK_DROPSHIP");
|
||||
}
|
||||
|
||||
private void addHistory(long inboundId, long tenantId, String from, String to,
|
||||
String action, LocalDateTime now) {
|
||||
InboundStatusHistoryEntity history = new InboundStatusHistoryEntity();
|
||||
|
||||
+17
-5
@@ -91,8 +91,9 @@ public class LoadingCompletedOutboxService {
|
||||
row.getCustomerRemark(), row.getInternalRemark(), row.getWarehouseRemark(), null,
|
||||
cartonMarks.getOrDefault(row.getLoadingPlanItemId(), List.of())))
|
||||
.toList();
|
||||
String orderType = dropship(first.getBusinessType()) ? "ONE_CLICK_DROPSHIP" : "FIRST_LEG";
|
||||
List<FirstLegLoadingPlanCompletedV2.PalletSnapshot> pallets = dropship(first.getBusinessType())
|
||||
String fulfillmentType = fulfillmentType(sorted);
|
||||
String orderType = dropship(fulfillmentType) ? "ONE_CLICK_DROPSHIP" : "FIRST_LEG";
|
||||
List<FirstLegLoadingPlanCompletedV2.PalletSnapshot> pallets = dropship(fulfillmentType)
|
||||
? palletSnapshots(sorted, palletRows) : List.of();
|
||||
if ("ONE_CLICK_DROPSHIP".equals(orderType) && pallets.isEmpty()) {
|
||||
throw new BizException(ResultCode.CONFLICT, "一键代发入仓单缺少托盘冻结快照");
|
||||
@@ -110,7 +111,7 @@ public class LoadingCompletedOutboxService {
|
||||
/** 国内按实装箱数从可用标签池稳定随机抽取箱唛,重复发布同一计划时结果不变。 */
|
||||
private Map<Long, List<String>> cartonLabelPools(List<Row> rows) {
|
||||
Map<Long, List<String>> result = new java.util.LinkedHashMap<>();
|
||||
Map<LabelPoolKey, List<Row>> byInbound = rows.stream().filter(row -> !dropship(row.getBusinessType()))
|
||||
Map<LabelPoolKey, List<Row>> byInbound = rows.stream().filter(row -> !dropship(row.getFulfillmentType()))
|
||||
.filter(row -> "OVERSEAS".equals(destinationType(List.of(row))))
|
||||
.collect(java.util.stream.Collectors.groupingBy(
|
||||
row -> new LabelPoolKey(row.getAccountId(), row.getSourceInboundId()),
|
||||
@@ -168,6 +169,18 @@ public class LoadingCompletedOutboxService {
|
||||
return values.iterator().next();
|
||||
}
|
||||
|
||||
private String fulfillmentType(List<Row> rows) {
|
||||
Set<String> values = rows.stream().map(Row::getFulfillmentType)
|
||||
.map(value -> value == null || value.isBlank() ? "STANDARD" : value)
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
if (values.size() != 1
|
||||
|| (!values.contains("STANDARD") && !values.contains("ONE_CLICK_DROPSHIP"))) {
|
||||
throw new BizException(ResultCode.CONFLICT,
|
||||
"装柜入仓单履约类型必须唯一且为 STANDARD 或 ONE_CLICK_DROPSHIP");
|
||||
}
|
||||
return values.iterator().next();
|
||||
}
|
||||
|
||||
private record LabelPoolKey(long accountId, long inboundId) { }
|
||||
|
||||
private List<FirstLegLoadingPlanCompletedV2.PalletSnapshot> palletSnapshots(List<Row> values, List<PalletRow> source) {
|
||||
@@ -223,8 +236,7 @@ public class LoadingCompletedOutboxService {
|
||||
}
|
||||
|
||||
private static boolean dropship(String value) {
|
||||
return value != null && ("ONE_CLICK_DROPSHIP".equalsIgnoreCase(value)
|
||||
|| value.toUpperCase(java.util.Locale.ROOT).contains("DROPSHIP"));
|
||||
return "ONE_CLICK_DROPSHIP".equals(value);
|
||||
}
|
||||
|
||||
private record DetailSnapshot(Long detailId, String skuCode, String productName,
|
||||
|
||||
@@ -27,6 +27,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_order (
|
||||
delivery_warehouse_id BIGINT UNSIGNED NULL COMMENT '交货仓逻辑ID',
|
||||
delivery_warehouse_name VARCHAR(128) NULL COMMENT '交货仓名称快照',
|
||||
business_type VARCHAR(32) NOT NULL COMMENT '业务类型',
|
||||
fulfillment_type VARCHAR(32) NOT NULL DEFAULT 'STANDARD' COMMENT '履约类型:STANDARD普通头程、ONE_CLICK_DROPSHIP一键代发',
|
||||
channel_code VARCHAR(64) NULL COMMENT '渠道编码',
|
||||
channel_name VARCHAR(128) NULL COMMENT '渠道名称快照',
|
||||
carrier_id BIGINT UNSIGNED NULL COMMENT '计费船司逻辑ID',
|
||||
@@ -77,6 +78,26 @@ CREATE TABLE IF NOT EXISTS fl_inbound_order (
|
||||
CONSTRAINT fk_inbound_task FOREIGN KEY (task_id) REFERENCES fl_fulfillment_task (id)
|
||||
) ENGINE=InnoDB COMMENT='二期头程入库单聚合根和列表查询快照';
|
||||
|
||||
SET @inbound_fulfillment_type_sql = IF(
|
||||
EXISTS(
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE() AND table_name = 'fl_inbound_order'
|
||||
AND column_name = 'fulfillment_type'
|
||||
),
|
||||
'SELECT 1',
|
||||
'ALTER TABLE fl_inbound_order ADD COLUMN fulfillment_type VARCHAR(32) NULL COMMENT ''履约类型:STANDARD普通头程、ONE_CLICK_DROPSHIP一键代发'' AFTER business_type'
|
||||
);
|
||||
PREPARE inbound_fulfillment_type_stmt FROM @inbound_fulfillment_type_sql;
|
||||
EXECUTE inbound_fulfillment_type_stmt;
|
||||
DEALLOCATE PREPARE inbound_fulfillment_type_stmt;
|
||||
|
||||
UPDATE fl_inbound_order
|
||||
SET fulfillment_type = 'STANDARD'
|
||||
WHERE fulfillment_type IS NULL OR fulfillment_type = '';
|
||||
ALTER TABLE fl_inbound_order
|
||||
MODIFY COLUMN fulfillment_type VARCHAR(32) NOT NULL DEFAULT 'STANDARD'
|
||||
COMMENT '履约类型:STANDARD普通头程、ONE_CLICK_DROPSHIP一键代发';
|
||||
|
||||
-- 既有开发库若存在跨客户重复码则严格失败,禁止自动覆盖或改写入仓码。
|
||||
SET @inbound_global_code_index_sql = IF(
|
||||
NOT EXISTS(
|
||||
|
||||
@@ -1122,6 +1122,7 @@ components:
|
||||
deliveryWarehouseId: {type: integer, format: int64, nullable: true}
|
||||
deliveryWarehouseName: {type: string, nullable: true}
|
||||
businessType: {type: string, nullable: true}
|
||||
fulfillmentType: {type: string, enum: [STANDARD, ONE_CLICK_DROPSHIP], nullable: true}
|
||||
channelCode: {type: string, nullable: true}
|
||||
channelName: {type: string, nullable: true}
|
||||
taxMode: {type: string, nullable: true}
|
||||
@@ -1466,7 +1467,7 @@ components:
|
||||
adjustmentReason: {type: string, minLength: 1, maxLength: 500, nullable: true}
|
||||
OrderApprovedInboundCommand:
|
||||
type: object
|
||||
required: [tenantId, orderId, inboundCode, customerName, warehouseId]
|
||||
required: [tenantId, orderId, inboundCode, customerName, warehouseId, fulfillmentType]
|
||||
properties:
|
||||
tenantId: {type: string, pattern: '^[1-9][0-9]*$', description: 当前客户隔离 ID}
|
||||
orderId: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
@@ -1480,6 +1481,7 @@ components:
|
||||
deliveryWarehouseName: {type: string, nullable: true}
|
||||
expectedArrivalAt: {type: string, format: date-time, nullable: true}
|
||||
businessType: {type: string, nullable: true}
|
||||
fulfillmentType: {type: string, enum: [STANDARD, ONE_CLICK_DROPSHIP], description: 独立履约类型;不得复用计费 businessType}
|
||||
channelCode: {type: string, nullable: true}
|
||||
channelName: {type: string, nullable: true}
|
||||
carrierId: {type: string, pattern: '^[1-9][0-9]*$', nullable: true, description: 历史兼容的内部运输供应商快照}
|
||||
@@ -1810,6 +1812,7 @@ components:
|
||||
deliveryWarehouseId: {type: string, nullable: true}
|
||||
deliveryWarehouseName: {type: string, nullable: true}
|
||||
businessType: {type: string, nullable: true}
|
||||
fulfillmentType: {type: string, enum: [STANDARD, ONE_CLICK_DROPSHIP], nullable: true}
|
||||
channelCode: {type: string, nullable: true}
|
||||
channelName: {type: string, nullable: true}
|
||||
containerMode: {type: string, nullable: true}
|
||||
|
||||
+6
-1
@@ -37,12 +37,17 @@ class UnifiedInboundOpenApiContractTest {
|
||||
assertThat(stringMap(inbound.get("properties"))).containsKeys(
|
||||
"id", "tenantId", "orderId", "inboundCode", "processStatus", "version",
|
||||
"customerId", "warehouseId", "deliveryWarehouseId", "totalPackageCount",
|
||||
"businessType", "channelCode", "pricingTaxMode", "containerMode", "destinationType",
|
||||
"businessType", "fulfillmentType", "channelCode", "pricingTaxMode", "containerMode", "destinationType",
|
||||
"holdFlag", "actualGrossWeightKg", "actualNetWeightKg", "actualVolumeCbm",
|
||||
"volumeWeightKg", "chargeableWeightKg", "chargeableVolumeCbm", "palletCount",
|
||||
"inboundedAt", "requestedPickupAt", "expressCompanyCode", "expressCompanyName", "containerNo",
|
||||
"cabinetNo", "estimatedSailingAt", "actualSailingAt", "estimatedPortArrivalAt",
|
||||
"actualPortArrivalAt", "createdByName", "createdAt", "updatedAt");
|
||||
|
||||
Map<String, Object> approval = stringMap(schemas.get("OrderApprovedInboundCommand"));
|
||||
assertThat(stringList(approval.get("required"))).contains("fulfillmentType");
|
||||
Map<String, Object> fulfillment = stringMap(stringMap(approval.get("properties")).get("fulfillmentType"));
|
||||
assertThat(stringList(fulfillment.get("enum"))).containsExactly("STANDARD", "ONE_CLICK_DROPSHIP");
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package com.baiyue.firstleg.repository;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class FulfillmentTypeSqlContractTest {
|
||||
@Test
|
||||
void baselinesAndUnifiedMigrationPersistIndependentFulfillmentType() throws Exception {
|
||||
String order = Files.readString(resolve("order/src/main/resources/db/order.sql"), StandardCharsets.UTF_8);
|
||||
String inbound = Files.readString(resolve("firstleg/src/main/resources/db/phase2-inbound-schema.sql"), StandardCharsets.UTF_8);
|
||||
String unified = Files.readString(resolve("deploy/sql/头程数据库统一增量-MySQL8.sql"), StandardCharsets.UTF_8);
|
||||
|
||||
assertFulfillmentMigration(order, "baiyue_order.ord_order");
|
||||
assertFulfillmentMigration(inbound, "fl_inbound_order");
|
||||
assertFulfillmentMigration(unified, "baiyue_order.ord_order");
|
||||
assertFulfillmentMigration(unified, "fl_inbound_order");
|
||||
assertThat(unified)
|
||||
.contains("unified_assert_column('baiyue_order', 'ord_order', 'fulfillment_type')")
|
||||
.contains("unified_assert_column('baiyue_firstleg', 'fl_inbound_order', 'fulfillment_type')");
|
||||
}
|
||||
|
||||
private static void assertFulfillmentMigration(String sql, String table) {
|
||||
assertThat(sql)
|
||||
.contains("fulfillment_type VARCHAR(32) NOT NULL DEFAULT 'STANDARD'")
|
||||
.contains("ADD COLUMN fulfillment_type VARCHAR(32) NULL")
|
||||
.contains("SET fulfillment_type = 'STANDARD'")
|
||||
.contains("MODIFY COLUMN fulfillment_type VARCHAR(32) NOT NULL DEFAULT 'STANDARD'")
|
||||
.contains(table);
|
||||
}
|
||||
|
||||
private static Path resolve(String path) {
|
||||
Path direct = Path.of(path);
|
||||
return Files.exists(direct) ? direct : Path.of("..").resolve(path).normalize();
|
||||
}
|
||||
}
|
||||
+1
@@ -100,6 +100,7 @@ class PdaSplitSettlementSqlContractTest {
|
||||
assertThat(clone)
|
||||
.contains("#{childOrderId},#{childOrderNo}")
|
||||
.contains("CASE WHEN #{splitPurpose}='LOADED' THEN 'LOADING' ELSE 'WAIT_SPLIT' END")
|
||||
.contains("business_type,fulfillment_type")
|
||||
.contains("actual_gross_weight_kg * #{packageCount}")
|
||||
.contains("actual_net_weight_kg * #{packageCount}");
|
||||
|
||||
|
||||
+24
@@ -72,11 +72,35 @@ class InboundOrderCreationServiceTest {
|
||||
assertThat(inboundCaptor.getValue().getCarrierId()).isNull();
|
||||
assertThat(inboundCaptor.getValue().getTaxMode()).isEqualTo("BUY_DECLARATION");
|
||||
assertThat(inboundCaptor.getValue().getPricingTaxMode()).isEqualTo("TAX_EXCLUDED");
|
||||
assertThat(inboundCaptor.getValue().getFulfillmentType()).isEqualTo("STANDARD");
|
||||
assertThat(inboundCaptor.getValue().getTotalPackageCount()).isEqualTo(4);
|
||||
assertThat(createdId).isEqualTo(inboundCaptor.getValue().getId());
|
||||
assertThat(replayedId).isEqualTo(900L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void persistsDropshipFulfillmentTypeWithoutChangingPricingBusinessType() {
|
||||
OrderApprovedInboundCommand base = approvedCommand("PCHK2608140001");
|
||||
OrderApprovedInboundCommand command = new OrderApprovedInboundCommand(
|
||||
base.tenantId(), base.orderId(), base.orderNo(), base.inboundCode(), base.customerId(),
|
||||
base.customerName(), base.warehouseId(), base.warehouseName(), base.deliveryWarehouseId(),
|
||||
base.deliveryWarehouseName(), base.expectedArrivalAt(), base.businessType(), base.channelCode(),
|
||||
base.channelName(), base.carrierId(), base.carrierName(), base.taxMode(), base.pricingTaxMode(),
|
||||
base.containerMode(), base.customsInspection(), base.destinationType(), base.destinationWarehouseId(),
|
||||
base.destinationCode(), base.destinationName(), base.publicRemark(), base.expressCompanyName(),
|
||||
base.expressTrackingNo(), base.totalPackageCount(), base.pickupMethod(), base.requestedPickupAt(),
|
||||
"ONE_CLICK_DROPSHIP");
|
||||
when(orders.selectOne(any(Wrapper.class))).thenReturn(null);
|
||||
when(fulfillmentTasks.provision(any(), any())).thenReturn(task(700L));
|
||||
|
||||
service.createFromApprovedOrder(command);
|
||||
|
||||
ArgumentCaptor<InboundOrderEntity> captor = ArgumentCaptor.forClass(InboundOrderEntity.class);
|
||||
verify(orders).insert(captor.capture());
|
||||
assertThat(captor.getValue().getBusinessType()).isEqualTo("FBA");
|
||||
assertThat(captor.getValue().getFulfillmentType()).isEqualTo("ONE_CLICK_DROPSHIP");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectReplayWithDifferentInboundCode() {
|
||||
InboundOrderEntity existing = inbound(900L, "RK-OLD", "UNPRICED", null, null);
|
||||
|
||||
+32
-2
@@ -146,9 +146,9 @@ class LoadingCompletedOutboxServiceTest {
|
||||
}
|
||||
|
||||
@Test void publishesDropshipPalletSkuAndCartonSnapshots() throws Exception {
|
||||
Row first = row(11, 100, 101); first.setBusinessType("ONE_CLICK_DROPSHIP");
|
||||
Row first = row(11, 100, 101); first.setFulfillmentType("ONE_CLICK_DROPSHIP");
|
||||
first.setPackageCount(2); first.setProductQuantity(20); first.setSkuCode("SKU-A");
|
||||
Row second = row(11, 100, 102); second.setBusinessType("ONE_CLICK_DROPSHIP");
|
||||
Row second = row(11, 100, 102); second.setFulfillmentType("ONE_CLICK_DROPSHIP");
|
||||
second.setPackageCount(1); second.setProductQuantity(5); second.setSkuCode("SKU-B");
|
||||
when(snapshots.rows(90L)).thenReturn(List.of(first, second));
|
||||
when(snapshots.palletRows(90L)).thenReturn(List.of(
|
||||
@@ -168,6 +168,35 @@ class LoadingCompletedOutboxServiceTest {
|
||||
assertThat(event.payload().at("/orders/0/palletCount").asInt()).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void pricingBusinessTypeCannotTurnAStandardOrderIntoDropship() throws Exception {
|
||||
Row value = row(11, 100, 101);
|
||||
value.setBusinessType("ONE_CLICK_DROPSHIP");
|
||||
value.setFulfillmentType("STANDARD");
|
||||
when(snapshots.rows(90L)).thenReturn(List.of(value));
|
||||
|
||||
service.publish(90L);
|
||||
|
||||
ArgumentCaptor<InboundOutboxEntity> captor = ArgumentCaptor.forClass(InboundOutboxEntity.class);
|
||||
verify(outbox).insert(captor.capture());
|
||||
EventEnvelope event = mapper.readValue(captor.getValue().getPayloadJson(), EventEnvelope.class);
|
||||
assertThat(event.payload().at("/orders/0/orderType").asText()).isEqualTo("FIRST_LEG");
|
||||
assertThat(event.payload().at("/orders/0/pallets")).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsMixedFulfillmentTypesWithinOneInbound() {
|
||||
Row first = row(11, 100, 101);
|
||||
Row second = row(11, 100, 102);
|
||||
second.setFulfillmentType("ONE_CLICK_DROPSHIP");
|
||||
when(snapshots.rows(90L)).thenReturn(List.of(first, second));
|
||||
|
||||
assertThatThrownBy(() -> service.publish(90L))
|
||||
.hasMessageContaining("履约类型必须唯一");
|
||||
verify(outbox, org.mockito.Mockito.never()).insert(
|
||||
org.mockito.ArgumentMatchers.any(InboundOutboxEntity.class));
|
||||
}
|
||||
|
||||
private Row row(long account,long inbound,long detail) {
|
||||
Row r=new Row(); r.setLoadingPlanId(90L); r.setPlanNo("P90"); r.setContainerBookingId(91L);
|
||||
r.setAggregateVersion(8); r.setContainerNo("CONT"); r.setCabinetNo("CAB"); r.setWarehouseId(92L);
|
||||
@@ -176,6 +205,7 @@ class LoadingCompletedOutboxServiceTest {
|
||||
r.setInboundCode("RK"+inbound); r.setLoadingPlanItemId(detail); r.setDestinationWarehouseId(93L); r.setDestinationWarehouseCode("ONT8");
|
||||
r.setDestinationWarehouseName("ONT8"); r.setSourceInboundDetailId(detail); r.setSkuCode("SKU"+detail);
|
||||
r.setDestinationType("OVERSEAS");
|
||||
r.setFulfillmentType("STANDARD");
|
||||
r.setPackageCount(1); r.setProductQuantity(10); r.setPalletCount(1); r.setGrossWeightKg(BigDecimal.ONE);
|
||||
r.setNetWeightKg(BigDecimal.ONE); r.setVolumeCbm(BigDecimal.ONE); return r;
|
||||
}
|
||||
|
||||
+9
-7
@@ -58,8 +58,8 @@ class TaillegRealProducerAcceptanceTest {
|
||||
BOOKING, MARKER + "-BOOK", MARKER + "-CNTR", MARKER + "-CAB", 92L, "真实验收海外仓", "40HQ");
|
||||
jdbc.update("INSERT IGNORE INTO fl_loading_plan(id,plan_no,container_booking_id,plan_status,created_by," +
|
||||
"updated_by,version) VALUES(?,?,?,'LOADING',1,1,0)", PLAN, MARKER + "-PLAN", BOOKING);
|
||||
seedInbound(FIRST_INBOUND, 990022221L, MARKER + "-FIRST-IN", "FBA_SEA", "LOADED", "ONT8");
|
||||
seedInbound(DROPSHIP_INBOUND, 990022222L, MARKER + "-DROP-IN", "ONE_CLICK_DROPSHIP", "LOADED", "LAX9");
|
||||
seedInbound(FIRST_INBOUND, 990022221L, MARKER + "-FIRST-IN", "FBA_SEA", "STANDARD", "LOADED", "ONT8");
|
||||
seedInbound(DROPSHIP_INBOUND, 990022222L, MARKER + "-DROP-IN", "FBA_SEA", "ONE_CLICK_DROPSHIP", "LOADED", "LAX9");
|
||||
seedItem(990022321L, FIRST_INBOUND, 990022421L, 990022221L, MARKER + "-FIRST-IN", "SKU-REAL-A", 3, "ONT8");
|
||||
seedItem(990022322L, DROPSHIP_INBOUND, 990022422L, 990022222L, MARKER + "-DROP-IN", "SKU-REAL-D", 2, "LAX9");
|
||||
for (int i = 1; i <= 3; i++) seedLabel(990022540L + i, FIRST_INBOUND, 990022221L, 990022421L,
|
||||
@@ -86,13 +86,15 @@ class TaillegRealProducerAcceptanceTest {
|
||||
id, 990022901L, planItemId, inboundId, detailId, packages, packages);
|
||||
}
|
||||
|
||||
private void seedInbound(long id, long orderId, String code, String type, String status, String destination) {
|
||||
private void seedInbound(long id, long orderId, String code, String businessType, String fulfillmentType,
|
||||
String status, String destination) {
|
||||
jdbc.update("INSERT IGNORE INTO fl_inbound_order(id,tenant_id,order_id,order_no,inbound_code,process_status," +
|
||||
"order_status_snapshot,customer_name,warehouse_id,warehouse_name,business_type,destination_type,destination_warehouse_id," +
|
||||
"order_status_snapshot,customer_name,warehouse_id,warehouse_name,business_type,fulfillment_type,destination_type,destination_warehouse_id," +
|
||||
"destination_code,destination_name,package_count,actual_volume_cbm,created_by,updated_by) " +
|
||||
"VALUES(?,?,?,?,?,?, 'IN_TRANSIT',?,92,'真实验收海外仓',?,'FBA',9001,?,?,?,0.4000,1,1)",
|
||||
id, CUSTOMER, orderId, MARKER + "-ORDER-" + id, code, status, "真实验收客户", type,
|
||||
destination, "FBA " + destination, type.equals("ONE_CLICK_DROPSHIP") ? 2 : 3);
|
||||
"VALUES(?,?,?,?,?,?, 'IN_TRANSIT',?,92,'真实验收海外仓',?,?,'FBA',9001,?,?,?,0.4000,1,1)",
|
||||
id, CUSTOMER, orderId, MARKER + "-ORDER-" + id, code, status, "真实验收客户", businessType,
|
||||
fulfillmentType, destination, "FBA " + destination,
|
||||
fulfillmentType.equals("ONE_CLICK_DROPSHIP") ? 2 : 3);
|
||||
}
|
||||
|
||||
private void seedItem(long id, long inboundId, long detailId, long orderId, String inboundCode,
|
||||
|
||||
+4
-2
@@ -210,9 +210,11 @@ public class LoadingCompletedConsumer {
|
||||
LastlegOrderStatusHistory history=new LastlegOrderStatusHistory(); history.setCustomerId(customerId);
|
||||
history.setOrderId(order.getId()); history.setToStatus("PENDING_ASSIGNMENT"); history.setEventId(event.eventId());
|
||||
history.setRemark("头程装柜完成自动生成"); history.setCreatedAt(LocalDateTime.now()); historyMapper.insert(history);
|
||||
if ("FIRST_LEG".equals(orderType)) {
|
||||
if ("FIRST_LEG".equals(orderType) || "ONE_CLICK_DROPSHIP".equals(orderType)) {
|
||||
InboundPlanResponse plan = inboundPlans.convert(customerId, order.getId());
|
||||
registerInTransit(event, payload, source, order, plan.planNo());
|
||||
if ("FIRST_LEG".equals(orderType)) {
|
||||
registerInTransit(event, payload, source, order, plan.planNo());
|
||||
}
|
||||
}
|
||||
if ("ONE_CLICK_DROPSHIP".equals(orderType)) {
|
||||
long warehouseId;
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ class LoadingCompletedConsumerTest {
|
||||
assertTrue(table.contains("customer_id")); assertFalse(table.contains("tenant_id"));
|
||||
}
|
||||
@Test void rejectsEventFromWrongRegion(){ EventEnvelope source=event(); EventEnvelope invalid=new EventEnvelope(source.eventId(),source.eventType(),source.eventVersion(),source.occurredAt(),RegionCode.OVERSEAS,RegionCode.CN,source.tenantId(),source.aggregateType(),source.aggregateId(),source.aggregateVersion(),source.handoverVersion(),source.traceId(),source.correlationId(),source.causationId(),source.payload(),source.payloadHash()); assertThrows(IllegalArgumentException.class,()->service.consume(invalid)); }
|
||||
@Test void consumesFormalDropshipPalletSnapshots(){ var sku=new FirstLegLoadingPlanCompletedV1.SkuPackageSnapshot("SKU-A","Product A",2,20,List.of("BOX-1","BOX-2")); var pallet=new FirstLegLoadingPlanCompletedV1.PalletSnapshot("PLT-1",List.of(sku)); var item=new FirstLegLoadingPlanCompletedV1.Item("d-1","SKU-A","Product A","Product A",2,20,1,BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE,"wh-1","ONT8","ONT8",null,null,"MARK",null,List.of()); var order=new FirstLegLoadingPlanCompletedV1.Order("in-d","root-d","ord-d","FL-D","root-od","RK-D","wh-1","ONT8","ONT8",2,20,1,BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE,List.of(item),"100","ONE_CLICK_DROPSHIP",List.of(pallet),"FBA"); var payload=new FirstLegLoadingPlanCompletedV1("plan-d","LP-D","book-d","CONT-D","CAB-D","92","Warehouse",List.of(order)); JsonNode node=json.valueToTree(payload); String hash=EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(node)); var event=new EventEnvelope("123e4567-e89b-12d3-a456-426614174010",EventTypes.FIRST_LEG_LOADING_PLAN_COMPLETED,1,Instant.now(),RegionCode.CN,RegionCode.OVERSEAS,"100","LOADING_PLAN","plan-d",2,1L,"123e4567-e89b-12d3-a456-426614174011","FIRSTLEG_LOADING_COMPLETED:plan-d:100:1",null,node,hash); assertEquals(1,service.consume(event)); ArgumentCaptor<DropshipRequests.Receive> captor=ArgumentCaptor.forClass(DropshipRequests.Receive.class); verify(dropshipOrders).receive(captor.capture()); assertEquals("100",captor.getValue().customerId()); assertEquals(1,captor.getValue().pallets().size()); assertEquals("PLT-1",captor.getValue().pallets().get(0).palletNo()); assertEquals(List.of("BOX-1","BOX-2"),captor.getValue().pallets().get(0).skuPackages().get(0).cartonMarks()); }
|
||||
@Test void consumesFormalDropshipPalletSnapshots(){ var sku=new FirstLegLoadingPlanCompletedV1.SkuPackageSnapshot("SKU-A","Product A",2,20,List.of("BOX-1","BOX-2")); var pallet=new FirstLegLoadingPlanCompletedV1.PalletSnapshot("PLT-1",List.of(sku)); var item=new FirstLegLoadingPlanCompletedV1.Item("d-1","SKU-A","Product A","Product A",2,20,1,BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE,"wh-1","ONT8","ONT8",null,null,"MARK",null,List.of()); var order=new FirstLegLoadingPlanCompletedV1.Order("in-d","root-d","ord-d","FL-D","root-od","RK-D","wh-1","ONT8","ONT8",2,20,1,BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE,List.of(item),"100","ONE_CLICK_DROPSHIP",List.of(pallet),"FBA"); var payload=new FirstLegLoadingPlanCompletedV1("plan-d","LP-D","book-d","CONT-D","CAB-D","92","Warehouse",List.of(order)); JsonNode node=json.valueToTree(payload); String hash=EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(node)); var event=new EventEnvelope("123e4567-e89b-12d3-a456-426614174010",EventTypes.FIRST_LEG_LOADING_PLAN_COMPLETED,1,Instant.now(),RegionCode.CN,RegionCode.OVERSEAS,"100","LOADING_PLAN","plan-d",2,1L,"123e4567-e89b-12d3-a456-426614174011","FIRSTLEG_LOADING_COMPLETED:plan-d:100:1",null,node,hash); assertEquals(1,service.consume(event)); ArgumentCaptor<DropshipRequests.Receive> captor=ArgumentCaptor.forClass(DropshipRequests.Receive.class); verify(dropshipOrders).receive(captor.capture()); verify(inboundPlans).convert(eq("100"),anyLong()); verify(inventory,never()).registerInTransit(anyString(),anyString(),any()); assertEquals("100",captor.getValue().customerId()); assertEquals(1,captor.getValue().pallets().size()); assertEquals("PLT-1",captor.getValue().pallets().get(0).palletNo()); assertEquals(List.of("BOX-1","BOX-2"),captor.getValue().pallets().get(0).skuPackages().get(0).cartonMarks()); }
|
||||
@Test void oneDropshipInboundWithMultipleSkusCreatesOneOrderAndMultipleSkuDetails(){
|
||||
var itemA=new FirstLegLoadingPlanCompletedV1.Item("d-a","SKU-A","产品A","Product A",2,20,1,BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE,"wh-1","ONT8","ONT8",null,"SHIP-A","MARK-A",null,List.of("BOX-A1","BOX-A2"));
|
||||
var itemB=new FirstLegLoadingPlanCompletedV1.Item("d-b","SKU-B","产品B","Product B",3,15,1,BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE,"wh-1","ONT8","ONT8",null,"SHIP-B","MARK-B",null,List.of("BOX-B1","BOX-B2","BOX-B3"));
|
||||
|
||||
@@ -45,7 +45,7 @@ public interface FirstlegInboundClient {
|
||||
Boolean customsInspection, String destinationType, Long destinationWarehouseId,
|
||||
String destinationCode, String destinationName, String publicRemark,
|
||||
String expressCompanyName, String expressTrackingNo, Integer totalPackageCount,
|
||||
String pickupMethod, LocalDateTime requestedPickupAt
|
||||
String pickupMethod, LocalDateTime requestedPickupAt, String fulfillmentType
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,8 +73,28 @@ public final class OrderRequests {
|
||||
@Size(max = 64) String shipmentId,
|
||||
@Size(max = 64) String referenceId,
|
||||
@Schema(description = "旧系统入仓码;仅全局 LEGACY_MANUAL 模式接受,AUTO 模式必须留空", example = "NYEXX2608140001", maxLength = 40)
|
||||
@Size(max = 40) @Pattern(regexp = "[A-Za-z0-9._:-]+") String inboundCode
|
||||
@Size(max = 40) @Pattern(regexp = "[A-Za-z0-9._:-]+") String inboundCode,
|
||||
@Schema(description = "Fulfillment type; omitted means STANDARD / 履约类型;省略时为普通头程", allowableValues = {"STANDARD", "ONE_CLICK_DROPSHIP"}, defaultValue = "STANDARD")
|
||||
@Pattern(regexp = "STANDARD|ONE_CLICK_DROPSHIP") String fulfillmentType
|
||||
) {
|
||||
public SaveRequest(Long customerId, String customerName, String transportMode, String tradeTerm,
|
||||
String destinationCountryCode, Long destinationWarehouseId,
|
||||
String destinationWarehouseName, LocalDate expectedShipDate, String customerRemark,
|
||||
Party sender, Party receiver, List<Cargo> cargoes, String pickupMethod,
|
||||
LocalDateTime requestedPickupAt, Long receivingWarehouseId,
|
||||
String receivingWarehouseName, String channelCode, String channelName,
|
||||
Long carrierId, String carrierName, String taxMode, String pricingTaxMode,
|
||||
String containerMode, Boolean customsInspection, String destinationType,
|
||||
String amazonWarehouseCode, String shipmentId, String referenceId,
|
||||
String inboundCode) {
|
||||
this(customerId, customerName, transportMode, tradeTerm, destinationCountryCode,
|
||||
destinationWarehouseId, destinationWarehouseName, expectedShipDate, customerRemark,
|
||||
sender, receiver, cargoes, pickupMethod, requestedPickupAt, receivingWarehouseId,
|
||||
receivingWarehouseName, channelCode, channelName, carrierId, carrierName, taxMode,
|
||||
pricingTaxMode, containerMode, customsInspection, destinationType, amazonWarehouseCode,
|
||||
shipmentId, referenceId, inboundCode, null);
|
||||
}
|
||||
|
||||
public SaveRequest(Long customerId, String customerName, String transportMode, String tradeTerm,
|
||||
String destinationCountryCode, Long destinationWarehouseId,
|
||||
String destinationWarehouseName, LocalDate expectedShipDate, String customerRemark,
|
||||
@@ -89,7 +109,7 @@ public final class OrderRequests {
|
||||
sender, receiver, cargoes, pickupMethod, requestedPickupAt, receivingWarehouseId,
|
||||
receivingWarehouseName, channelCode, channelName, carrierId, carrierName, taxMode,
|
||||
pricingTaxMode, containerMode, customsInspection, destinationType, amazonWarehouseCode,
|
||||
shipmentId, referenceId, null);
|
||||
shipmentId, referenceId, null, null);
|
||||
}
|
||||
|
||||
public SaveRequest(Long customerId, String customerName, String transportMode, String tradeTerm,
|
||||
@@ -106,7 +126,7 @@ public final class OrderRequests {
|
||||
sender, receiver, cargoes, pickupMethod, null, receivingWarehouseId,
|
||||
receivingWarehouseName, channelCode, channelName, carrierId, carrierName, taxMode,
|
||||
pricingTaxMode, containerMode, customsInspection, destinationType, amazonWarehouseCode,
|
||||
shipmentId, referenceId, null);
|
||||
shipmentId, referenceId, null, null);
|
||||
}
|
||||
|
||||
@AssertTrue(message = "承运商 ID 和名称必须同时填写或同时留空")
|
||||
|
||||
@@ -47,6 +47,7 @@ public class OrderEntity extends BaseEntity {
|
||||
private String containerMode;
|
||||
private Boolean customsInspection;
|
||||
private String destinationType;
|
||||
private String fulfillmentType;
|
||||
private String amazonWarehouseCode;
|
||||
private String shipmentId;
|
||||
private String referenceId;
|
||||
|
||||
@@ -462,6 +462,7 @@ public class OrderApplicationService {
|
||||
order.setPricingTaxMode(request.pricingTaxMode());
|
||||
order.setContainerMode(request.containerMode()); order.setCustomsInspection(request.customsInspection());
|
||||
order.setDestinationType(request.destinationType());
|
||||
order.setFulfillmentType(fulfillmentType(request.fulfillmentType()));
|
||||
order.setAmazonWarehouseCode("FBA".equals(request.destinationType()) && snapshots.destination() != null
|
||||
? snapshots.destination().warehouse().warehouseCode() : request.amazonWarehouseCode());
|
||||
order.setShipmentId(request.shipmentId()); order.setReferenceId(request.referenceId());
|
||||
@@ -486,6 +487,13 @@ public class OrderApplicationService {
|
||||
}
|
||||
order.setInboundCode(normalized);
|
||||
}
|
||||
|
||||
private static String fulfillmentType(String value) {
|
||||
if (value == null) return "STANDARD";
|
||||
if ("STANDARD".equals(value) || "ONE_CLICK_DROPSHIP".equals(value)) return value;
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED,
|
||||
"履约类型必须为 STANDARD 或 ONE_CLICK_DROPSHIP");
|
||||
}
|
||||
private boolean legacyManualMode() {
|
||||
return inboundCodeModeProvider.legacyManual();
|
||||
}
|
||||
@@ -599,7 +607,8 @@ public class OrderApplicationService {
|
||||
order.getContainerMode(), order.getCustomsInspection(), order.getDestinationType(),
|
||||
order.getDestinationWarehouseId(), order.getAmazonWarehouseCode(), order.getDestinationWarehouseName(),
|
||||
order.getCustomerRemark(), order.getExpressCompany(), order.getExpressTrackingNo(), totalPackageCount,
|
||||
order.getPickupMethod(), order.getRequestedPickupAt());
|
||||
order.getPickupMethod(), order.getRequestedPickupAt(),
|
||||
order.getFulfillmentType() == null ? "STANDARD" : order.getFulfillmentType());
|
||||
}
|
||||
private void prepareDraftInbound(OrderEntity order, OrderRequests.SaveRequest request) {
|
||||
FirstlegInboundClient client = firstlegProvider == null ? null : firstlegProvider.getIfAvailable();
|
||||
|
||||
@@ -141,6 +141,7 @@ public class OrderFulfillmentSplitService {
|
||||
child.setDestinationWarehouseName(root.getDestinationWarehouseName()); child.setReceivingWarehouseId(root.getReceivingWarehouseId());
|
||||
child.setReceivingWarehouseName(root.getReceivingWarehouseName()); child.setChannelCode(root.getChannelCode());
|
||||
child.setChannelName(root.getChannelName()); child.setDestinationType(root.getDestinationType());
|
||||
child.setFulfillmentType(root.getFulfillmentType());
|
||||
child.setInboundCode(remainderInboundCode(root, sequence));
|
||||
child.setCreatedBy(operatorId); child.setUpdatedBy(operatorId);
|
||||
if (orders.insert(child) != 1) throw new BizException(ResultCode.CONFLICT, "余量子订单创建失败");
|
||||
|
||||
@@ -35,6 +35,7 @@ CREATE TABLE IF NOT EXISTS ord_order (
|
||||
container_mode VARCHAR(24) NULL COMMENT '整柜或散货',
|
||||
customs_inspection TINYINT(1) NULL COMMENT '是否商检',
|
||||
destination_type VARCHAR(24) NULL COMMENT '目的仓类型:FBA亚马逊仓、OVERSEAS海外仓、PRIVATE私人地址',
|
||||
fulfillment_type VARCHAR(32) NOT NULL DEFAULT 'STANDARD' COMMENT '履约类型:STANDARD普通头程、ONE_CLICK_DROPSHIP一键代发',
|
||||
amazon_warehouse_code VARCHAR(32) NULL COMMENT '亚马逊仓库编码',
|
||||
shipment_id VARCHAR(64) NULL COMMENT '订单级Amazon Shipment ID',
|
||||
reference_id VARCHAR(64) NULL COMMENT '订单级Amazon Reference ID',
|
||||
@@ -61,6 +62,26 @@ CREATE TABLE IF NOT EXISTS ord_order (
|
||||
KEY idx_order_tenant_warehouse (tenant_id, destination_warehouse_id)
|
||||
) ENGINE=InnoDB COMMENT='头程订单主表:保存商户委托、目的仓、运输方式和订单当前状态';
|
||||
|
||||
SET @order_fulfillment_type_sql = IF(
|
||||
EXISTS(
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'baiyue_order' AND table_name = 'ord_order'
|
||||
AND column_name = 'fulfillment_type'
|
||||
),
|
||||
'SELECT 1',
|
||||
'ALTER TABLE baiyue_order.ord_order ADD COLUMN fulfillment_type VARCHAR(32) NULL COMMENT ''履约类型:STANDARD普通头程、ONE_CLICK_DROPSHIP一键代发'' AFTER destination_type'
|
||||
);
|
||||
PREPARE order_fulfillment_type_stmt FROM @order_fulfillment_type_sql;
|
||||
EXECUTE order_fulfillment_type_stmt;
|
||||
DEALLOCATE PREPARE order_fulfillment_type_stmt;
|
||||
|
||||
UPDATE baiyue_order.ord_order
|
||||
SET fulfillment_type = 'STANDARD'
|
||||
WHERE fulfillment_type IS NULL OR fulfillment_type = '';
|
||||
ALTER TABLE baiyue_order.ord_order
|
||||
MODIFY COLUMN fulfillment_type VARCHAR(32) NOT NULL DEFAULT 'STANDARD'
|
||||
COMMENT '履约类型:STANDARD普通头程、ONE_CLICK_DROPSHIP一键代发';
|
||||
|
||||
-- 既有开发库必须先确认不存在跨客户重复码;若存在则在此处失败,不自动改写业务数据。
|
||||
SET @order_global_inbound_index_sql = IF(
|
||||
NOT EXISTS(
|
||||
|
||||
@@ -646,6 +646,11 @@ components:
|
||||
type: object
|
||||
required: [pricingTaxMode, cargoes]
|
||||
properties:
|
||||
fulfillmentType:
|
||||
type: string
|
||||
enum: [STANDARD, ONE_CLICK_DROPSHIP]
|
||||
default: STANDARD
|
||||
description: 独立履约类型;省略为普通头程,不复用业务/计费 businessType
|
||||
inboundCode:
|
||||
type: string
|
||||
maxLength: 40
|
||||
@@ -657,7 +662,8 @@ components:
|
||||
minItems: 1
|
||||
items: {$ref: '#/components/schemas/FirstLegOrderCargo'}
|
||||
description: >-
|
||||
订单保存请求中的完整字段以服务端 FirstLegOrderSaveRequest 为准;pricingTaxMode
|
||||
订单保存请求中的完整字段以服务端 FirstLegOrderSaveRequest 为准;fulfillmentType
|
||||
省略时固定为 STANDARD,一键代发必须显式传 ONE_CLICK_DROPSHIP,且不复用计费 businessType;pricingTaxMode
|
||||
是新建和编辑订单的必填报价维度;customerId 不接受客户端指定,服务端始终使用认证企业 ID;
|
||||
发货单位和收货单位仅通过 sender/receiver 保存业务快照。历史存量订单的响应字段仍允许为空。
|
||||
InboundCodePolicy:
|
||||
|
||||
+3
@@ -37,6 +37,9 @@ class MerchantBatchDraftOpenApiContractTest {
|
||||
.contains("merchant:order:create")
|
||||
.contains("FirstLegBatchDraftResponse")
|
||||
.contains("FirstLegBatchDraftResult")
|
||||
.contains("fulfillmentType:")
|
||||
.contains("enum: [STANDARD, ONE_CLICK_DROPSHIP]")
|
||||
.contains("default: STANDARD")
|
||||
.contains("inboundCode:")
|
||||
.contains("errorCode:")
|
||||
.contains("errorMessage:");
|
||||
|
||||
@@ -54,6 +54,25 @@ class OrderRequestsValidationTest {
|
||||
assertThat(validator.validate(draft)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void fulfillmentTypeIsOptionalButRejectsPricingBusinessValues() {
|
||||
var standard = request(null, null, "TAX_INCLUDED");
|
||||
assertThat(validator.validate(standard)).isEmpty();
|
||||
|
||||
var invalid = new OrderRequests.SaveRequest(
|
||||
standard.customerId(), standard.customerName(), standard.transportMode(), standard.tradeTerm(),
|
||||
standard.destinationCountryCode(), standard.destinationWarehouseId(), standard.destinationWarehouseName(),
|
||||
standard.expectedShipDate(), standard.customerRemark(), standard.sender(), standard.receiver(),
|
||||
standard.cargoes(), standard.pickupMethod(), standard.requestedPickupAt(), standard.receivingWarehouseId(),
|
||||
standard.receivingWarehouseName(), standard.channelCode(), standard.channelName(), standard.carrierId(),
|
||||
standard.carrierName(), standard.taxMode(), standard.pricingTaxMode(), standard.containerMode(),
|
||||
standard.customsInspection(), standard.destinationType(), standard.amazonWarehouseCode(),
|
||||
standard.shipmentId(), standard.referenceId(), standard.inboundCode(), "FBA_SEA");
|
||||
|
||||
assertThat(validator.validate(invalid))
|
||||
.anyMatch(violation -> violation.getPropertyPath().toString().equals("fulfillmentType"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void validatesTheManuallyEnteredInboundCodeForSingleAndBatchCallers() {
|
||||
var source = request(null, null, "TAX_INCLUDED");
|
||||
|
||||
@@ -60,7 +60,7 @@ class OrderApplicationServiceStateTest {
|
||||
assertThat(orderId).isNotNull();
|
||||
verify(eligibility, never()).requireApproved(anyLong());
|
||||
verify(repository).insertOrder(argThat(order -> order.getStatus() == OrderStatus.DRAFT
|
||||
&& order.getInboundCode() == null));
|
||||
&& order.getInboundCode() == null && "STANDARD".equals(order.getFulfillmentType())));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -372,6 +372,31 @@ class OrderApplicationServiceStateTest {
|
||||
assertThat(command.taxMode()).isEqualTo("BUY_DECLARATION");
|
||||
assertThat(command.pricingTaxMode()).isEqualTo("TAX_EXCLUDED");
|
||||
assertThat(command.totalPackageCount()).isEqualTo(4);
|
||||
assertThat(command.fulfillmentType()).isEqualTo("STANDARD");
|
||||
}
|
||||
|
||||
@Test
|
||||
void explicitDropshipFulfillmentTypeIsPersistedAndSentIndependentlyFromPricingBusinessType() {
|
||||
OrderRepository repository = mock(OrderRepository.class);
|
||||
OrderMasterDataGuard masterData = mock(OrderMasterDataGuard.class);
|
||||
OrderRequests.SaveRequest source = request();
|
||||
OrderRequests.SaveRequest dropship = new OrderRequests.SaveRequest(
|
||||
source.customerId(), source.customerName(), source.transportMode(), source.tradeTerm(),
|
||||
source.destinationCountryCode(), source.destinationWarehouseId(), source.destinationWarehouseName(),
|
||||
source.expectedShipDate(), source.customerRemark(), source.sender(), source.receiver(), source.cargoes(),
|
||||
source.pickupMethod(), source.requestedPickupAt(), source.receivingWarehouseId(),
|
||||
source.receivingWarehouseName(), source.channelCode(), source.channelName(), source.carrierId(),
|
||||
source.carrierName(), source.taxMode(), source.pricingTaxMode(), source.containerMode(),
|
||||
source.customsInspection(), source.destinationType(), source.amazonWarehouseCode(), source.shipmentId(),
|
||||
source.referenceId(), source.inboundCode(), "ONE_CLICK_DROPSHIP");
|
||||
when(masterData.validate(10001L, dropship)).thenReturn(snapshots());
|
||||
when(repository.completeIdempotency(anyLong(), any(), anyLong())).thenReturn(1);
|
||||
|
||||
service(repository, mock(OrderEligibilityGuard.class), masterData)
|
||||
.createDraft(10001L, 2L, dropship, "dropship-fulfillment");
|
||||
|
||||
verify(repository).insertOrder(argThat(order -> "ONE_CLICK_DROPSHIP".equals(order.getFulfillmentType())
|
||||
&& "OVERSEAS".equals(order.getDestinationType())));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -921,10 +921,10 @@ export class AcceptanceRun {
|
||||
}
|
||||
}
|
||||
|
||||
async runPhase4ProducerVariant(label, destinationType) {
|
||||
async runPhase4ProducerVariant(label, destinationType, fulfillmentType = 'STANDARD') {
|
||||
const matrixMarker = this.marker
|
||||
const variantMarker = `${matrixMarker}-P4-${label}`
|
||||
const contextKeys = ['orderDestinationType', 'phase4ProducerMatrixMarker']
|
||||
const contextKeys = ['orderDestinationType', 'orderFulfillmentType', 'phase4ProducerMatrixMarker']
|
||||
const createdKeys = ['phase3Loading', 'phase3LoadingCompletedProducer', 'orchestrationFixtures', 'oceanBooking']
|
||||
const contextSnapshot = new Map(contextKeys.map((key) => [key, {
|
||||
present: Object.hasOwn(this.context, key), value: this.context[key],
|
||||
@@ -935,6 +935,7 @@ export class AcceptanceRun {
|
||||
let restoreFixture = null
|
||||
this.marker = variantMarker
|
||||
this.context.orderDestinationType = destinationType
|
||||
this.context.orderFulfillmentType = fulfillmentType
|
||||
this.context.phase4ProducerMatrixMarker = matrixMarker
|
||||
try {
|
||||
restoreFixture = await this.preparePhase3LoadingFixture()
|
||||
@@ -958,19 +959,22 @@ export class AcceptanceRun {
|
||||
|
||||
async ensurePhase4PdaProducerMatrix() {
|
||||
const readiness = () => this.createdResources.phase4InboundProducerFixtures?.readiness || {}
|
||||
if (Number(readiness().fbaContainerCount || 0) < 1) {
|
||||
await this.runPhase4ProducerVariant('FBA-PRIMARY', 'FBA')
|
||||
if (Number(readiness().dropshipContainerCount || 0) < 1) {
|
||||
await this.runPhase4ProducerVariant('DROPSHIP-PRIMARY', 'FBA', 'ONE_CLICK_DROPSHIP')
|
||||
}
|
||||
if (Number(readiness().fbaContainerCount || 0) < 2) {
|
||||
await this.runPhase4ProducerVariant('FBA-STANDARD', 'FBA')
|
||||
}
|
||||
if (Number(readiness().overseasContainerCount || 0) < 1) {
|
||||
await this.runPhase4ProducerVariant('OVERSEAS-PRIMARY', 'OVERSEAS')
|
||||
}
|
||||
if (Number(readiness().distinctContainerCount || 0) < 3) {
|
||||
if (Number(readiness().distinctContainerCount || 0) < 4) {
|
||||
await this.runPhase4ProducerVariant('SECONDARY', 'FBA')
|
||||
}
|
||||
const matrix = this.createdResources.phase4InboundProducerFixtures
|
||||
this.assert('phase4-overseas-pda-fixture', '本轮三期跨区生产矩阵已就绪', null,
|
||||
matrix?.marker === this.marker && matrix?.readiness?.readyForPhase4Pda === true,
|
||||
`marker=${matrix?.marker || 'missing'}, FBA=${matrix?.readiness?.fbaContainerCount ?? 0}, OVERSEAS=${matrix?.readiness?.overseasContainerCount ?? 0}, containers=${matrix?.readiness?.distinctContainerCount ?? 0}`,
|
||||
`marker=${matrix?.marker || 'missing'}, FBA=${matrix?.readiness?.fbaContainerCount ?? 0}, OVERSEAS=${matrix?.readiness?.overseasContainerCount ?? 0}, DROPSHIP=${matrix?.readiness?.dropshipContainerCount ?? 0}, containers=${matrix?.readiness?.distinctContainerCount ?? 0}`,
|
||||
{ scenario: 'phase3-phase4-producer-fixture', deploymentBlocker: true })
|
||||
}
|
||||
|
||||
|
||||
@@ -250,6 +250,7 @@ function buildPayload(context, profile, master, productImageFileId) {
|
||||
const referenceId = `REF-${context.marker}`.slice(0, 64)
|
||||
const companyName = profile.tenantName || `订单验收客户-${context.marker}`
|
||||
return {
|
||||
fulfillmentType: String(context.context?.orderFulfillmentType || 'STANDARD').trim().toUpperCase(),
|
||||
customerId: master.partner?.id ?? null,
|
||||
customerName: master.partner?.partnerName ?? companyName,
|
||||
transportMode: 'SEA', tradeTerm: 'FOB',
|
||||
@@ -397,6 +398,7 @@ export async function run(context) {
|
||||
if (!orderId) throw new Error('Client 订单创建失败,后续 Admin 链路停止')
|
||||
context.createdResources.orders = {
|
||||
marker: context.marker, tenantId, orderId, productImageFileId,
|
||||
fulfillmentType: payload.fulfillmentType,
|
||||
boxTypes: BOXES.map((box) => box.boxType), inboundId: null,
|
||||
receivingWarehouseId: id(master.receiving.id),
|
||||
destinationType: master.destinationType,
|
||||
|
||||
@@ -35,6 +35,7 @@ export function collectPhase4InboundProducerFixture(previous, producer) {
|
||||
destinationType,
|
||||
containers.filter((item) => item.destinationType === destinationType),
|
||||
]))
|
||||
const dropshipContainers = containers.filter((item) => item.fulfillmentType === 'ONE_CLICK_DROPSHIP')
|
||||
const distinctContainerCount = new Set(containers.map((item) => item.containerNo).filter(Boolean)).size
|
||||
return {
|
||||
marker: producer.marker,
|
||||
@@ -43,12 +44,15 @@ export function collectPhase4InboundProducerFixture(previous, producer) {
|
||||
readiness: {
|
||||
fbaContainerCount: byDestinationType.FBA.length,
|
||||
overseasContainerCount: byDestinationType.OVERSEAS.length,
|
||||
dropshipContainerCount: dropshipContainers.length,
|
||||
distinctContainerCount,
|
||||
readyForPhase4Pda: byDestinationType.FBA.length >= 1
|
||||
readyForPhase4Pda: byDestinationType.FBA.length >= 2
|
||||
&& byDestinationType.OVERSEAS.length >= 1
|
||||
&& distinctContainerCount >= 3,
|
||||
&& dropshipContainers.length >= 1
|
||||
&& distinctContainerCount >= 4,
|
||||
},
|
||||
requiredEntryOrchestration: [
|
||||
'DROPSHIP-FBA: public order fulfillmentType -> inbound-measurement -> phase3-loading -> phase3-loading-finalize -> overseas PDA',
|
||||
'FBA: orders -> inbound-measurement -> phase3-loading -> phase3-customs -> phase3-loading-finalize',
|
||||
'OVERSEAS-PRIMARY: orders -> inbound-measurement -> phase3-loading -> phase3-customs -> phase3-loading-finalize',
|
||||
'SECONDARY(FBA|OVERSEAS): orders -> inbound-measurement -> phase3-loading -> phase3-customs -> phase3-loading-finalize',
|
||||
@@ -367,6 +371,7 @@ export async function run(context) {
|
||||
sourceInboundId: inboundId,
|
||||
cartonMark: fixture.cartonMark || null,
|
||||
cartonMarks: Array.isArray(fixture.cartonMarks) ? fixture.cartonMarks : [],
|
||||
fulfillmentType: fixture.fulfillmentType || 'STANDARD',
|
||||
destinationType: fixture.destinationType || null,
|
||||
destinationWarehouseId: id(fixture.destinationWarehouseId),
|
||||
destinationWarehouseCode: fixture.destinationWarehouseCode || null,
|
||||
|
||||
@@ -1215,6 +1215,7 @@ export async function run(context) {
|
||||
cartonMark: source?.boxCode || source?.cartonMark || null,
|
||||
cartonMarks: [...new Set((stagedTask?.items || []).flatMap((item) => item?.boxCodes || [])
|
||||
.map((value) => String(value || '').trim()).filter(Boolean))],
|
||||
fulfillmentType: source?.fulfillmentType || 'STANDARD',
|
||||
destinationType: initial?.summary?.destinationType || source?.destinationType || null,
|
||||
destinationWarehouseId: id(initial?.summary?.destinationWarehouseId || source?.destinationWarehouseId),
|
||||
destinationWarehouseCode: initial?.summary?.destinationCode || source?.destinationWarehouseCode || null,
|
||||
|
||||
@@ -17,25 +17,32 @@ test('order fixture defaults to FBA and accepts only the explicit OVERSEAS varia
|
||||
const orders = read('orders.mjs')
|
||||
assert.match(orders, /warehouseType=\$\{encodeURIComponent\(destinationType\)\}/)
|
||||
assert.match(orders, /destinationType: master\.destinationType/)
|
||||
assert.match(orders, /fulfillmentType: String\(context\.context\?\.orderFulfillmentType \|\| 'STANDARD'\)/)
|
||||
assert.match(orders, /destinationType,\s*\n\s*\}\)/)
|
||||
assert.doesNotMatch(orders, /const fbaResponse =/)
|
||||
})
|
||||
|
||||
test('producer collector requires one FBA, one OVERSEAS and a third distinct container', () => {
|
||||
test('producer collector requires dropship FBA, standard FBA, OVERSEAS and a fourth distinct container', () => {
|
||||
const common = { marker: 'RUN-1', eventType: 'FIRST_LEG_LOADING_PLAN_COMPLETED', sourceStatus: 'LOADED' }
|
||||
const fba = { ...common, eventKey: 'FBA-EVENT', destinationType: 'FBA', containerNo: 'FBA-001' }
|
||||
const dropship = { ...common, eventKey: 'DROP-EVENT', destinationType: 'FBA',
|
||||
fulfillmentType: 'ONE_CLICK_DROPSHIP', containerNo: 'DROP-001' }
|
||||
const fba = { ...common, eventKey: 'FBA-EVENT', destinationType: 'FBA',
|
||||
fulfillmentType: 'STANDARD', containerNo: 'FBA-001' }
|
||||
const overseasPrimary = { ...common, eventKey: 'OS-EVENT-1', destinationType: 'OVERSEAS', containerNo: 'OS-001' }
|
||||
const secondary = { ...common, eventKey: 'FBA-EVENT-2', destinationType: 'FBA', containerNo: 'FBA-002' }
|
||||
const one = collectPhase4InboundProducerFixture(null, fba)
|
||||
const two = collectPhase4InboundProducerFixture(one, overseasPrimary)
|
||||
const ready = collectPhase4InboundProducerFixture(two, secondary)
|
||||
const one = collectPhase4InboundProducerFixture(null, dropship)
|
||||
const two = collectPhase4InboundProducerFixture(one, fba)
|
||||
const three = collectPhase4InboundProducerFixture(two, overseasPrimary)
|
||||
const ready = collectPhase4InboundProducerFixture(three, secondary)
|
||||
assert.equal(one.readiness.readyForPhase4Pda, false)
|
||||
assert.equal(two.readiness.readyForPhase4Pda, false)
|
||||
assert.equal(three.readiness.readyForPhase4Pda, false)
|
||||
assert.equal(ready.readiness.readyForPhase4Pda, true)
|
||||
assert.equal(ready.readiness.fbaContainerCount, 2)
|
||||
assert.equal(ready.readiness.fbaContainerCount, 3)
|
||||
assert.equal(ready.readiness.overseasContainerCount, 1)
|
||||
assert.equal(ready.readiness.distinctContainerCount, 3)
|
||||
assert.equal(ready.byDestinationType.FBA[1].eventKey, 'FBA-EVENT-2')
|
||||
assert.equal(ready.readiness.dropshipContainerCount, 1)
|
||||
assert.equal(ready.readiness.distinctContainerCount, 4)
|
||||
assert.equal(ready.byDestinationType.FBA[2].eventKey, 'FBA-EVENT-2')
|
||||
})
|
||||
|
||||
test('same event key is idempotently replaced and another marker starts a new matrix', () => {
|
||||
@@ -49,6 +56,7 @@ test('same event key is idempotently replaced and another marker starts a new ma
|
||||
assert.equal(nextRun.containers.length, 1)
|
||||
assert.equal(nextRun.marker, 'RUN-2')
|
||||
assert.deepEqual(nextRun.requiredEntryOrchestration, [
|
||||
'DROPSHIP-FBA: public order fulfillmentType -> inbound-measurement -> phase3-loading -> phase3-loading-finalize -> overseas PDA',
|
||||
'FBA: orders -> inbound-measurement -> phase3-loading -> phase3-customs -> phase3-loading-finalize',
|
||||
'OVERSEAS-PRIMARY: orders -> inbound-measurement -> phase3-loading -> phase3-customs -> phase3-loading-finalize',
|
||||
'SECONDARY(FBA|OVERSEAS): orders -> inbound-measurement -> phase3-loading -> phase3-customs -> phase3-loading-finalize',
|
||||
@@ -61,7 +69,14 @@ test('measurement and loading pass destination identity into the LOADED producer
|
||||
const finalize = read('phase3-loading-finalize.mjs')
|
||||
assert.match(measurement, /destinationType: context\.createdResources\?\.orders\?\.destinationType/)
|
||||
assert.match(loading, /destinationType: initial\?\.summary\?\.destinationType \|\| source\?\.destinationType/)
|
||||
assert.match(loading, /fulfillmentType: source\?\.fulfillmentType \|\| 'STANDARD'/)
|
||||
assert.match(finalize, /destinationType: fixture\.destinationType \|\| null/)
|
||||
assert.match(finalize, /fulfillmentType: fixture\.fulfillmentType \|\| 'STANDARD'/)
|
||||
assert.match(finalize, /resourceKey: `\$\{context\.marker\}:\$\{fixture\.destinationType \|\| 'UNKNOWN'\}:\$\{fixture\.containerNo \|\| containerBookingId\}`/)
|
||||
assert.match(finalize, /phase4InboundProducerFixtures = collectPhase4InboundProducerFixture/)
|
||||
const entry = read('../admin-test-env-acceptance.mjs')
|
||||
assert.match(entry, /runPhase4ProducerVariant\('DROPSHIP-PRIMARY', 'FBA', 'ONE_CLICK_DROPSHIP'\)/)
|
||||
assert.match(entry, /orderFulfillmentType/)
|
||||
const pda = read('phase4-overseas-pda.mjs')
|
||||
assert.match(pda, /find\(\(item\) => item\?\.fulfillmentType === 'ONE_CLICK_DROPSHIP'\)/)
|
||||
})
|
||||
|
||||
@@ -413,7 +413,8 @@ export async function run(context) {
|
||||
return
|
||||
}
|
||||
const matrix = context.createdResources?.phase4InboundProducerFixtures
|
||||
const producers = [matrix?.byDestinationType?.FBA?.[0], matrix?.byDestinationType?.OVERSEAS?.[0]]
|
||||
const dropship = matrix?.containers?.find((item) => item?.fulfillmentType === 'ONE_CLICK_DROPSHIP')
|
||||
const producers = [dropship || matrix?.byDestinationType?.FBA?.[0], matrix?.byDestinationType?.OVERSEAS?.[0]]
|
||||
.filter((item) => nonBlank(item?.containerNo))
|
||||
const allProducers = Array.isArray(matrix?.containers) ? matrix.containers : producers
|
||||
if (allProducers.length && !(await awaitProducerCandidates(context, primary.token, allProducers))) {
|
||||
|
||||
Reference in New Issue
Block a user