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 ;
2026-10-02 10:34:00 +08:00
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 ;
2026-10-03 04:18:52 +08:00
import static org.junit.jupiter.api.Assertions.assertFalse ;
2026-10-02 10:34:00 +08:00
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" , "> 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 ( ">" , ">" ). replace ( "<" , "<" );
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 );
}
2026-10-15 21:05:34 +08:00
@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 ());
}
}
}
}
2026-10-10 10:38:06 +08:00
@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 );
}
2026-10-15 16:55:57 +08:00
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" ), "部分批次确认也不能代表整票" );
}
2026-10-10 10:38:06 +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 ( DispatchCandidateMapper . CONFIRMED_APPOINTMENT_AT + " AS appointmentAt,NULL AS expectedLoadingAt" ));
assertFalse ( sql . contains ( "expected_loading_at" ));
assertFalse ( sql . contains ( "fi.line_id" ));
}
}
}
2026-10-05 07:24:53 +08:00
@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" ));
}
}
2026-10-02 10:34:00 +08:00
@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 > 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
2026-10-03 04:18:52 +08:00
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" ));
2026-10-03 04:18:52 +08:00
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 ;
}
}
}