feat: add dropship fulfillment producer chain
This commit is contained in:
+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,
|
||||
|
||||
Reference in New Issue
Block a user