feat: add dropship fulfillment producer chain

This commit is contained in:
2026-08-25 08:40:27 +08:00
parent c218bfad6b
commit 4839243f21
35 changed files with 389 additions and 53 deletions
@@ -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")
@@ -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();
}
}
@@ -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}");
@@ -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);
@@ -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;
}
@@ -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,