Files
baiyue-firstleg/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/DispatchCandidateMapperContractTest.java
T

366 lines
27 KiB
Java
Raw Normal View History

2026-08-21 17:21:26 +08:00
package com.baiyue.lastleg.dispatch;
import com.baiyue.lastleg.dispatch.mapper.DispatchCandidateMapper;
import org.apache.ibatis.annotations.Select;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Method;
2026-10-14 13:42:58 +08:00
import java.nio.file.Files;
import java.nio.file.Path;
import java.sql.DriverManager;
import java.util.ArrayList;
2026-08-21 17:21:26 +08:00
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertEquals;
2026-08-21 17:21:26 +08:00
class DispatchCandidateMapperContractTest {
2026-10-20 23:34:22 +08:00
/** 执行正式计划/实收判定与离港门禁,覆盖自动预留、人工覆盖、混合来源及库存耗尽。 */
@Test
void reservedPalletsCanBePlannedAfterDepartureWithoutInventingReceivedStock() throws Exception {
try (var connection = DriverManager.getConnection("jdbc:h2:mem:dispatch-prearrival;MODE=MySQL")) {
var db = connection.createStatement();
db.execute("CREATE TABLE ll_inbound_plan(id BIGINT,customer_id VARCHAR)");
db.execute("CREATE TABLE ll_inbound_plan_pallet(id BIGINT,plan_id BIGINT,warehouse_pallet_id BIGINT,assignment_status VARCHAR,actual_package_count INT)");
db.execute("CREATE TABLE ll_inbound_plan_line(id BIGINT,customer_id VARCHAR,order_id BIGINT)");
db.execute("CREATE TABLE ll_inbound_plan_pallet_allocation(pallet_id BIGINT,line_id BIGINT)");
db.execute("CREATE TABLE ll_order(id BIGINT,customer_id VARCHAR,order_type VARCHAR,manual_node_override BOOLEAN,source_container_booking_id BIGINT)");
db.execute("CREATE TABLE ll_order_manual_node(order_id BIGINT,customer_id VARCHAR,node_code VARCHAR,node_at TIMESTAMP)");
db.execute("CREATE TABLE ll_container_shipping_gate(container_booking_id BIGINT,node_sequence INT,actual_at TIMESTAMP)");
db.execute("CREATE TABLE ll_inventory_position(customer_id VARCHAR,pallet_id BIGINT,available_packages INT)");
for (int id = 1; id <= 9; id++) {
db.execute("INSERT INTO ll_inbound_plan VALUES (" + id + ",'A')");
db.execute("INSERT INTO ll_inbound_plan_pallet VALUES (" + id + "," + id + "," + (100 + id) + ",'RESERVED',0)");
db.execute("INSERT INTO ll_inbound_plan_line VALUES (" + id + ",'A'," + id + ")");
db.execute("INSERT INTO ll_inbound_plan_pallet_allocation VALUES (" + id + "," + id + ")");
db.execute("INSERT INTO ll_order VALUES (" + id + ",'A','FIRST_LEG',FALSE," + id + ")");
if (id != 2) db.execute("INSERT INTO ll_container_shipping_gate VALUES (" + id + ",40,CURRENT_TIMESTAMP)");
}
db.execute("UPDATE ll_inbound_plan_pallet SET assignment_status='ASSIGNED',actual_package_count=5 WHERE id IN (3,4)");
db.execute("INSERT INTO ll_inventory_position VALUES ('A',103,5),('A',104,0),('B',104,10)");
db.execute("UPDATE ll_inbound_plan_pallet SET actual_package_count=1 WHERE id=5");
db.execute("UPDATE ll_order SET manual_node_override=TRUE WHERE id IN (6,7,9)");
db.execute("INSERT INTO ll_order_manual_node VALUES (7,'A','ACTUAL_DEPARTURE',CURRENT_TIMESTAMP),(9,'B','ACTUAL_DEPARTURE',CURRENT_TIMESTAMP)");
db.execute("INSERT INTO ll_inbound_plan_line VALUES (20,'A',2)");
db.execute("INSERT INTO ll_inbound_plan_pallet_allocation VALUES (8,20)");
String plan = DispatchCandidateMapper.PLAN_ONLY;
String eligible = "AND (NOT " + plan + " OR (" + DispatchCandidateMapper.DEPARTED_PLAN + ")) "
+ "AND (" + plan + " OR EXISTS (SELECT 1 FROM ll_inventory_position pos "
+ "WHERE pos.customer_id=ip.customer_id AND pos.pallet_id=p.warehouse_pallet_id AND pos.available_packages > 0))";
var ids = new ArrayList<Long>();
try (var rows = db.executeQuery("SELECT p.id,CASE WHEN " + plan + " THEN NULL ELSE p.warehouse_pallet_id END AS physical,"
+ "CASE WHEN " + plan + " THEN 'PLAN' ELSE 'RECEIVED' END AS stage "
+ "FROM ll_inbound_plan_pallet p JOIN ll_inbound_plan ip ON ip.id=p.plan_id WHERE 1=1 " + eligible + " ORDER BY p.id")) {
while (rows.next()) {
long id = rows.getLong("id"); ids.add(id);
assertEquals(id == 3 ? "RECEIVED" : "PLAN", rows.getString("stage"));
assertEquals(id == 3 ? 103L : null, rows.getObject("physical", Long.class));
}
}
assertEquals(List.of(1L, 3L, 7L), ids);
// 查询和写入选中来源共用同一判定;不允许仅在列表展示在途但提交时又要求实收。
for (String sql : List.of(DispatchCandidateMapper.AVAILABLE_PLAN_BODY, DispatchCandidateMapper.SELECTED_PLAN_SQL)) {
assertTrue(sql.contains(eligible.replace("> 0", "&gt; 0").replace("pos.customer_id=ip.customer_id", sql.contains("WHERE pos.customer_id=#{customerId}") ? "pos.customer_id=#{customerId}" : "pos.customer_id=ip.customer_id")));
assertTrue(sql.contains("CASE WHEN " + plan + " THEN NULL ELSE p.warehouse_pallet_id END AS warehousePalletId"));
}
}
}
2026-10-20 10:35:52 +08:00
/** 执行正式仓ID的完整CASE及外层过滤,保留实库存优先、客户来源和长ID文本身份。 */
@Test
void wholeCandidateWarehouseProjectionExecutesAsTextWithoutChangingSourceOwnership() throws Exception {
String sql = String.join(" ", DispatchCandidateMapper.class.getMethod("wholeAvailable", String.class,
String.class, List.class, String.class, String.class, String.class, String.class).getAnnotation(Select.class).value());
2026-10-20 23:34:22 +08:00
int start = sql.indexOf("CAST(CASE WHEN NOT " + DispatchCandidateMapper.PLAN_ONLY + " THEN (SELECT MIN(pos.overseas_warehouse_id)");
2026-10-20 10:35:52 +08:00
int end = sql.indexOf(" AS overseasWarehouseId", start);
assertTrue(start >= 0 && end > start, "正式whole候选必须在整个仓CASE外统一为文本投影");
String expression = sql.substring(start, end).replace("&gt;", ">").replace("&lt;", "<");
assertTrue(expression.endsWith("END AS CHAR(128))"));
assertTrue(expression.contains("NULLIF(CAST(ip.overseas_warehouse_id AS CHAR(128)),'')"));
String warehouseFilter = sql.substring(sql.lastIndexOf(") selected WHERE ") + 1)
.replace("#{warehouseId}", "?").replace("</script>", "");
String query = "SELECT selected.planPalletId FROM (SELECT p.id AS planPalletId," + expression
+ " AS overseasWarehouseId FROM ll_inbound_plan_pallet p JOIN ll_inbound_plan ip ON ip.id=p.plan_id)"
+ warehouseFilter;
try (var connection = DriverManager.getConnection("jdbc:h2:mem:dispatch-whole-warehouse-text;MODE=MySQL")) {
var statement = connection.createStatement();
statement.execute("CREATE TABLE ll_inbound_plan(id BIGINT,customer_id VARCHAR(128),overseas_warehouse_id BIGINT,source_order_id BIGINT)");
2026-10-20 23:34:22 +08:00
statement.execute("CREATE TABLE ll_inbound_plan_pallet(id BIGINT,plan_id BIGINT,warehouse_pallet_id BIGINT,assignment_status VARCHAR DEFAULT 'ASSIGNED',actual_package_count INT DEFAULT 1)");
2026-10-20 10:35:52 +08:00
statement.execute("CREATE TABLE ll_inventory_position(customer_id VARCHAR(128),pallet_id BIGINT,available_packages BIGINT,overseas_warehouse_id VARCHAR(128))");
statement.execute("CREATE TABLE ll_order(id BIGINT,customer_id VARCHAR(128),overseas_warehouse_id VARCHAR(128))");
statement.execute("INSERT INTO ll_inbound_plan VALUES (1,'A',193000000000200002,NULL),"
+ "(2,'A',193000000000100001,NULL),(3,'B',NULL,20),(4,'A',193000000000100001,NULL),"
+ "(5,'B',193000000000200002,NULL),(6,'A',NULL,NULL),(7,'A',193000000000100001,21),"
+ "(8,'A',193000000000100001,NULL),(9,'A',NULL,21)");
2026-10-20 23:34:22 +08:00
statement.execute("INSERT INTO ll_inbound_plan_pallet(id,plan_id,warehouse_pallet_id) VALUES (1,1,10),(2,2,NULL),(3,3,NULL),"
2026-10-20 10:35:52 +08:00
+ "(4,4,40),(5,5,50),(6,6,NULL),(7,7,NULL),(8,8,80),(9,9,NULL)");
2026-10-20 23:34:22 +08:00
statement.execute("INSERT INTO ll_inbound_plan_pallet VALUES (10,1,1000,'RESERVED',0)");
2026-10-20 10:35:52 +08:00
statement.execute("INSERT INTO ll_inventory_position VALUES ('A',10,1,'193000000000100001'),"
+ "('B',40,1,'193000000000100001'),('A',40,0,'193000000000100001'),"
+ "('B',50,1,'193000000000100001'),('A',80,1,'193000000000200002')");
statement.execute("INSERT INTO ll_order VALUES (20,'A','193000000000200002'),"
+ "(20,'B','193000000000100001'),(21,'B','193000000000100001')");
try (var prepared = connection.prepareStatement(query)) {
assertWarehouseRows(prepared, "193000000000100001", List.of(1L, 2L, 3L, 5L, 7L));
2026-10-20 23:34:22 +08:00
assertWarehouseRows(prepared, "193000000000200002", List.of(8L, 10L));
2026-10-20 10:35:52 +08:00
for (String invalid : List.of("19300000000010000", "193000000000100001x", "0193000000000100001", "1.93e17", "wrong"))
assertWarehouseRows(prepared, invalid, List.of());
}
}
}
/** 每次实际执行同一参数化仓过滤,禁止用前缀、数字强转或其他客户库存替代身份。 */
private void assertWarehouseRows(java.sql.PreparedStatement prepared, String warehouse, List<Long> expected) throws Exception {
prepared.setString(1, warehouse);
var actual = new ArrayList<Long>();
try (var rows = prepared.executeQuery()) { while (rows.next()) actual.add(rows.getLong(1)); }
actual.sort(Long::compareTo);
assertEquals(expected, actual, warehouse);
}
@Test
void ordinaryAndReturnRelabelCandidatesExcludeDirectOutboundDeliveryModes() throws Exception {
for (String method : List.of("relabelAvailable", "nativeReturnRelabelAvailable")) {
String sql = String.join(" ", DispatchCandidateMapper.class
.getMethod(method, String.class, String.class, String.class).getAnnotation(Select.class).value());
String eligibility = sql.substring(sql.indexOf("WHERE a.delivery_method"), sql.indexOf("<if test='customerId"));
try (var connection = DriverManager.getConnection("jdbc:h2:mem:relabel-mode-" + method + ";MODE=MySQL")) {
var statement = connection.createStatement();
statement.execute("CREATE TABLE ll_relabel_application(delivery_method VARCHAR,status VARCHAR,fulfillment_status VARCHAR)");
statement.execute("INSERT INTO ll_relabel_application VALUES "
+ "('TRUCK','READY_FOR_OUTBOUND','WAIT_PRE_DISPATCH'),"
+ "('PICKUP','READY_FOR_OUTBOUND','WAIT_PRE_DISPATCH'),"
+ "('AMAZON_PICKUP','READY_FOR_OUTBOUND','WAIT_PRE_DISPATCH'),"
+ "('CUSTOMER_PICKUP','READY_FOR_OUTBOUND','WAIT_PRE_DISPATCH'),"
+ "('UNKNOWN','READY_FOR_OUTBOUND','WAIT_PRE_DISPATCH'),"
+ "(NULL,'READY_FOR_OUTBOUND','WAIT_PRE_DISPATCH'),"
+ "('EXPRESS','READY_FOR_OUTBOUND','WAIT_PRE_DISPATCH')");
// The return query also requires physical return-carton availability.
eligibility = eligibility.replace("AND c.status='RELABEL_PENDING' AND c.pallet_code IS NOT NULL", "");
try (var rows = statement.executeQuery("SELECT delivery_method FROM ll_relabel_application a " + eligibility)) {
assertTrue(rows.next()); assertEquals("TRUCK", rows.getString(1)); assertFalse(rows.next());
}
}
}
}
@Test
void confirmedAppointmentUsesOriginalCustomerOrderAndDestinationAndNeverInventsLoadingTime() throws Exception {
try (var connection = DriverManager.getConnection("jdbc:h2:mem:dispatch-confirmed-appointment;MODE=MySQL")) {
var statement = connection.createStatement();
statement.execute("CREATE TABLE ll_inbound_plan(id BIGINT, customer_id VARCHAR)");
statement.execute("CREATE TABLE ll_inbound_plan_pallet(id BIGINT, plan_id BIGINT,destination_warehouse_code VARCHAR)");
statement.execute("CREATE TABLE ll_inbound_plan_line(id BIGINT,plan_id BIGINT,customer_id VARCHAR,order_id BIGINT,fba_warehouse_code VARCHAR)");
statement.execute("CREATE TABLE ll_inbound_plan_pallet_allocation(pallet_id BIGINT,line_id BIGINT,customer_id VARCHAR)");
statement.execute("CREATE TABLE ll_inbound_fba_appointment(id BIGINT,appointment_at TIMESTAMP,planned_at TIMESTAMP,appointment_status VARCHAR,fba_warehouse_code VARCHAR)");
statement.execute("CREATE TABLE ll_inbound_fba_appointment_item(appointment_id BIGINT,order_id BIGINT,customer_id VARCHAR,active INT,fba_warehouse_code VARCHAR)");
for (int i = 1; i <= 8; i++) {
statement.execute("INSERT INTO ll_inbound_plan VALUES ("+i+",'9')");
statement.execute("INSERT INTO ll_inbound_plan_pallet VALUES ("+i+","+i+",'ACY2')");
statement.execute("INSERT INTO ll_inbound_plan_line VALUES ("+i+","+i+",'9',"+i+",'OLD')");
statement.execute("INSERT INTO ll_inbound_plan_pallet_allocation VALUES ("+i+","+i+",'9')");
statement.execute("INSERT INTO ll_inbound_fba_appointment VALUES ("+i+",'2026-10-12 10:00:00','2026-10-11 09:00:00','SUCCESS','ACY2')");
statement.execute("INSERT INTO ll_inbound_fba_appointment_item VALUES ("+i+","+i+",'9',1,'ACY2')");
}
statement.execute("UPDATE ll_inbound_fba_appointment SET appointment_status='PENDING' WHERE id=2");
statement.execute("UPDATE ll_inbound_fba_appointment_item SET active=0 WHERE appointment_id=3");
statement.execute("UPDATE ll_inbound_fba_appointment_item SET customer_id='OTHER' WHERE appointment_id=4");
statement.execute("UPDATE ll_inbound_plan_pallet SET destination_warehouse_code='ONT8' WHERE id=5");
statement.execute("UPDATE ll_inbound_plan_line SET customer_id='OTHER' WHERE id=6");
statement.execute("UPDATE ll_inbound_fba_appointment SET appointment_at=NULL WHERE id=7");
statement.execute("UPDATE ll_inbound_fba_appointment_item SET fba_warehouse_code='ONT8' WHERE appointment_id=8");
try (var rows = statement.executeQuery("SELECT p.id," + DispatchCandidateMapper.CONFIRMED_APPOINTMENT_AT
+ " AS appointment_at FROM ll_inbound_plan_pallet p JOIN ll_inbound_plan ip ON ip.id=p.plan_id ORDER BY p.id")) {
int count = 0;
while (rows.next()) {
count++;
assertEquals(count == 1 ? java.sql.Timestamp.valueOf("2026-10-12 10:00:00") : null, rows.getTimestamp("appointment_at"));
}
assertEquals(8, count);
}
statement.execute("INSERT INTO ll_inbound_fba_appointment VALUES (99,'2026-10-15 10:00:00',NULL,'SUCCESS','ACY2')");
statement.execute("INSERT INTO ll_inbound_fba_appointment_item VALUES (99,1,'9',1,'ACY2')");
try(var rows=statement.executeQuery("SELECT "+DispatchCandidateMapper.CONFIRMED_APPOINTMENT_AT+" AS appointment_at FROM ll_inbound_plan_pallet p JOIN ll_inbound_plan ip ON ip.id=p.plan_id WHERE p.id=1")) {
assertTrue(rows.next());assertEquals(null,rows.getTimestamp("appointment_at"),"一票多批时不得将最晚确认冒充整个托盘的预约时间");
}
statement.execute("UPDATE ll_inbound_fba_appointment SET appointment_status='PENDING' WHERE id=99");
try(var rows=statement.executeQuery("SELECT "+DispatchCandidateMapper.CONFIRMED_APPOINTMENT_AT+" AS appointment_at FROM ll_inbound_plan_pallet p JOIN ll_inbound_plan ip ON ip.id=p.plan_id WHERE p.id=1")) {
assertTrue(rows.next());assertEquals(null,rows.getTimestamp("appointment_at"),"部分批次确认也不能代表整票");
}
for (Method method : List.of(
DispatchCandidateMapper.class.getMethod("available", String.class, List.class, String.class, String.class, String.class, String.class),
DispatchCandidateMapper.class.getMethod("lockSelected", String.class, List.class))) {
String sql = String.join(" ", method.getAnnotation(Select.class).value());
assertTrue(sql.contains(DispatchCandidateMapper.CONFIRMED_APPOINTMENT_AT + " AS appointmentAt,NULL AS expectedLoadingAt"));
assertFalse(sql.contains("expected_loading_at"));
assertFalse(sql.contains("fi.line_id"));
}
}
}
@Test
void historicalTransferCreationModeDoesNotGrantReservationOwnership() throws Exception {
for (Method method : List.of(
DispatchCandidateMapper.class.getMethod("available", String.class, List.class, String.class, String.class, String.class, String.class),
DispatchCandidateMapper.class.getMethod("lockSelected", String.class, List.class))) {
String sql = String.join(" ", method.getAnnotation(Select.class).value());
assertTrue(sql.contains("AND NOT EXISTS (SELECT 1 FROM ll_warehouse_transfer_inventory_allocation ta"));
org.junit.jupiter.api.Assertions.assertFalse(sql.contains("ip.creation_mode='WAREHOUSE_TRANSFER' OR NOT EXISTS (SELECT 1 FROM ll_warehouse_transfer_inventory_allocation"));
}
}
@Test
void availableAndLockedPalletsApplyTheSameActiveRelabelBoundary() throws Exception {
try (var connection = DriverManager.getConnection("jdbc:h2:mem:dispatch-relabel-boundary;MODE=MySQL")) {
var statement = connection.createStatement();
statement.execute("CREATE TABLE ll_inbound_plan(id BIGINT, customer_id VARCHAR, creation_mode VARCHAR)");
statement.execute("CREATE TABLE ll_inbound_plan_pallet(id BIGINT, plan_id BIGINT)");
statement.execute("CREATE TABLE ll_order(id BIGINT)");
statement.execute("CREATE TABLE ll_inbound_plan_line(id BIGINT, order_id BIGINT)");
statement.execute("CREATE TABLE ll_inbound_plan_pallet_allocation(pallet_id BIGINT, line_id BIGINT)");
statement.execute("CREATE TABLE ll_relabel_application(id BIGINT, customer_id VARCHAR, status VARCHAR)");
statement.execute("CREATE TABLE ll_relabel_item(application_id BIGINT, customer_id VARCHAR, source_inbound_order_id BIGINT)");
statement.execute("INSERT INTO ll_inbound_plan VALUES (1,'100','AUTO'),(2,'100','AUTO'),(3,'100','WAREHOUSE_TRANSFER'),(4,'100','AUTO'),(5,'100','AUTO')");
statement.execute("INSERT INTO ll_inbound_plan_pallet VALUES (11,1),(12,2),(13,3),(14,4),(15,5)");
statement.execute("INSERT INTO ll_order VALUES (21),(22),(23),(24),(25),(26)");
statement.execute("INSERT INTO ll_inbound_plan_line VALUES (31,21),(32,22),(33,23),(34,24),(35,25),(36,26)");
statement.execute("INSERT INTO ll_inbound_plan_pallet_allocation VALUES (11,31),(12,32),(13,33),(14,34),(15,35),(15,36)");
statement.execute("INSERT INTO ll_relabel_application VALUES (41,'100','PENDING_REVIEW'),(42,'100','COMPLETED'),(43,'100','PENDING_REVIEW'),(44,'200','PENDING_REVIEW'),(45,'100','PENDING_REVIEW')");
statement.execute("INSERT INTO ll_relabel_item VALUES (41,'100',21),(42,'100',22),(43,'100',23),(44,'200',24),(45,'100',25)");
for (Method method : List.of(
DispatchCandidateMapper.class.getMethod("available", String.class, List.class, String.class, String.class, String.class, String.class),
DispatchCandidateMapper.class.getMethod("lockSelected", String.class, List.class))) {
String sql = String.join(" ", method.getAnnotation(Select.class).value());
String boundary = sql.substring(sql.lastIndexOf("AND (ip.creation_mode"), sql.lastIndexOf("GROUP BY"))
.replace("#{customerId}", "'100'");
var result = statement.executeQuery("SELECT p.id FROM ll_inbound_plan_pallet p JOIN ll_inbound_plan ip ON ip.id=p.plan_id "
+ "JOIN ll_inbound_plan_pallet_allocation a ON a.pallet_id=p.id JOIN ll_inbound_plan_line l ON l.id=a.line_id "
+ "JOIN ll_order o ON o.id=l.order_id WHERE 1=1 " + boundary + " ORDER BY p.id");
var palletIds = new ArrayList<Long>();
while (result.next()) palletIds.add(result.getLong(1));
assertEquals(List.of(12L, 13L, 14L), palletIds, method.getName());
}
}
}
2026-08-21 17:21:26 +08:00
private static final List<String> CONSTRUCTOR_ALIASES = List.of(
"planId", "planPalletId", "warehousePalletId", "orderId", "customerId", "customerName",
2026-09-18 11:41:04 +08:00
"planNo", "orderType", "volumeWeightKg", "grossWeightKg", "containerNo", "appointmentAt", "expectedLoadingAt",
"overseasWarehouseId", "overseasWarehouseCode", "overseasWarehouseName", "orderNo", "orderRefs", "inboundCode", "palletNo",
2026-08-21 17:21:26 +08:00
"destinationWarehouseId", "destinationWarehouseCode", "destinationWarehouseName", "fbaWarehouseCode",
2026-09-17 17:37:29 +08:00
"packageCount", "volumeCbm", "sourceStage", "cartonMarks", "relabelCartonMarks",
"destinationType", "deliveryAddress", "postalCode", "contactName", "contactPhone"
2026-08-21 17:21:26 +08:00
);
@Test
void candidateQueriesKeepRecordConstructorColumnOrder() throws Exception {
assertConstructorColumnOrder(DispatchCandidateMapper.class.getMethod("available",
2026-08-22 10:20:38 +08:00
String.class, List.class, String.class, String.class, String.class, String.class));
2026-08-21 17:21:26 +08:00
assertConstructorColumnOrder(DispatchCandidateMapper.class.getMethod("lockSelected",
String.class, List.class));
2026-09-14 09:58:30 +08:00
assertConstructorColumnOrder(DispatchCandidateMapper.class.getMethod("relabelAvailable",
String.class, String.class, String.class));
2026-08-21 17:21:26 +08:00
}
@Test
void selectedReceivedPalletMustStillHaveAvailableInventory() throws Exception {
Method method = DispatchCandidateMapper.class.getMethod("lockSelected", String.class, List.class);
String sql = String.join(" ", method.getAnnotation(Select.class).value());
2026-10-20 23:34:22 +08:00
assertTrue(sql.contains("AND (" + DispatchCandidateMapper.PLAN_ONLY + " OR EXISTS"));
2026-08-21 17:21:26 +08:00
assertTrue(sql.contains("pos.available_packages &gt; 0"));
}
2026-09-17 17:37:29 +08:00
@Test
void candidatesWithoutFbaWarehouseAreExcludedFromReadAndWriteQueries() throws Exception {
for (Method method : List.of(
DispatchCandidateMapper.class.getMethod("available", String.class, List.class, String.class,
String.class, String.class, String.class),
DispatchCandidateMapper.class.getMethod("lockSelected", String.class, List.class))) {
String sql = String.join(" ", method.getAnnotation(Select.class).value());
assertTrue(sql.contains("NULLIF(TRIM(l.fba_warehouse_code),'') IS NOT NULL"));
}
}
@Test
void candidateKeywordSearchIncludesSourceInboundCode() throws Exception {
Method available = DispatchCandidateMapper.class.getMethod("available", String.class, List.class,
String.class, String.class, String.class, String.class);
String availableSql = String.join(" ", available.getAnnotation(Select.class).value());
assertTrue(availableSql.contains("o.order_no LIKE CONCAT('%',#{orderNo},'%') OR l.inbound_code LIKE CONCAT('%',#{orderNo},'%')"));
Method relabel = DispatchCandidateMapper.class.getMethod("relabelAvailable", String.class, String.class, String.class);
String relabelSql = String.join(" ", relabel.getAnnotation(Select.class).value());
assertTrue(relabelSql.contains("ri.source_inbound_code LIKE CONCAT('%',#{orderNo},'%')"));
}
2026-09-07 15:07:34 +08:00
@Test
void candidateWarehouseComesFromPhysicalInventoryOrFrozenInboundPlan() throws Exception {
for (Method method : List.of(
DispatchCandidateMapper.class.getMethod("available", String.class, List.class, String.class,
String.class, String.class, String.class),
DispatchCandidateMapper.class.getMethod("lockSelected", String.class, List.class))) {
String sql = String.join(" ", method.getAnnotation(Select.class).value());
2026-10-20 23:34:22 +08:00
assertTrue(sql.contains("CASE WHEN NOT " + DispatchCandidateMapper.PLAN_ONLY));
2026-09-07 15:07:34 +08:00
assertTrue(sql.contains("pos.overseas_warehouse_id"));
2026-10-20 10:35:52 +08:00
assertTrue(sql.contains(method.getName().equals("available")
? "NULLIF(CAST(ip.overseas_warehouse_id AS CHAR(128)),'')"
: "NULLIF(CAST(ip.overseas_warehouse_id AS CHAR),'')"));
2026-09-18 11:41:04 +08:00
assertTrue(sql.contains("source_order.id=ip.source_order_id"));
assertTrue(sql.contains("source_order.customer_id=ip.customer_id"));
assertTrue(sql.contains("source_order.overseas_warehouse_id"));
assertTrue(sql.contains("AS overseasWarehouseCode"));
assertTrue(sql.contains("b.overseas_warehouse_code"));
2026-09-07 15:07:34 +08:00
assertTrue(sql.contains("ll_inventory_balance"));
}
}
2026-09-14 09:58:30 +08:00
@Test
void candidateMarksFollowPdaRelabelPalletBinding() throws Exception {
for (Method method : List.of(
DispatchCandidateMapper.class.getMethod("available", String.class, List.class, String.class,
String.class, String.class, String.class),
DispatchCandidateMapper.class.getMethod("lockSelected", String.class, List.class))) {
String sql = String.join(" ", method.getAnnotation(Select.class).value());
assertTrue(sql.contains("rp.id=ri.source_inventory_position_id"));
assertTrue(sql.contains("rp.pallet_id=p.warehouse_pallet_id"));
assertTrue(sql.contains("ri.fba_carton_mark"));
assertTrue(sql.contains("AS relabelCartonMarks"));
}
}
2026-10-14 13:42:58 +08:00
@Test
void relabelCandidatesReadWeightFromWarehouseTransferPalletSnapshot() throws Exception {
2026-10-19 17:56:36 +08:00
String sql=String.join(" ",DispatchCandidateMapper.class.getMethod("relabelAvailable",String.class,String.class,String.class).getAnnotation(Select.class).value());
2026-10-14 13:42:58 +08:00
assertTrue(sql.contains("MAX(wtp.gross_weight_kg) AS grossWeightKg"));
assertTrue(sql.contains("MAX(wtp.volume_cbm) AS volumeCbm"));
assertTrue(sql.contains("wtp.warehouse_pallet_id=pos.pallet_id"));
}
2026-09-18 11:41:04 +08:00
@Test
void candidateGrossWeightUsesPhysicalSourcesAndNeverBillingVolumeWeight() throws Exception {
2026-09-18 11:41:04 +08:00
for (Method method : List.of(
DispatchCandidateMapper.class.getMethod("available", String.class, List.class, String.class,
String.class, String.class, String.class),
DispatchCandidateMapper.class.getMethod("lockSelected", String.class, List.class))) {
String sql = String.join(" ", method.getAnnotation(Select.class).value());
assertTrue(sql.contains("SUM(d.gross_weight_kg)"));
assertTrue(sql.contains("inbound.gross_weight_kg"));
assertTrue(sql.contains("ll_inbound_order inbound"));
assertTrue(sql.contains("a.allocated_volume_cbm"));
assertTrue(sql.contains("a2.line_id=a.line_id"));
assertFalse(sql.contains("MIN(o.volume_weight_kg)) AS grossWeightKg"));
2026-09-18 11:41:04 +08:00
}
}
2026-08-21 17:21:26 +08:00
private void assertConstructorColumnOrder(Method method) {
String sql = String.join(" ", method.getAnnotation(Select.class).value());
int previous = -1;
for (String alias : CONSTRUCTOR_ALIASES) {
int current = sql.indexOf(" AS " + alias, previous + 1);
assertTrue(current > previous,
() -> method.getName() + " must select alias " + alias + " in record constructor order");
previous = current;
}
}
}