366 lines
27 KiB
Java
366 lines
27 KiB
Java
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;
|
|
import java.nio.file.Files;
|
|
import java.nio.file.Path;
|
|
import java.sql.DriverManager;
|
|
import java.util.ArrayList;
|
|
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;
|
|
|
|
class DispatchCandidateMapperContractTest {
|
|
/** 执行正式计划/实收判定与离港门禁,覆盖自动预留、人工覆盖、混合来源及库存耗尽。 */
|
|
@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"));
|
|
}
|
|
}
|
|
}
|
|
/** 执行正式仓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());
|
|
int start = sql.indexOf("CAST(CASE WHEN NOT " + DispatchCandidateMapper.PLAN_ONLY + " THEN (SELECT MIN(pos.overseas_warehouse_id)");
|
|
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)");
|
|
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)");
|
|
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)");
|
|
statement.execute("INSERT INTO ll_inbound_plan_pallet(id,plan_id,warehouse_pallet_id) VALUES (1,1,10),(2,2,NULL),(3,3,NULL),"
|
|
+ "(4,4,40),(5,5,50),(6,6,NULL),(7,7,NULL),(8,8,80),(9,9,NULL)");
|
|
statement.execute("INSERT INTO ll_inbound_plan_pallet VALUES (10,1,1000,'RESERVED',0)");
|
|
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));
|
|
assertWarehouseRows(prepared, "193000000000200002", List.of(8L, 10L));
|
|
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());
|
|
}
|
|
}
|
|
}
|
|
private static final List<String> CONSTRUCTOR_ALIASES = List.of(
|
|
"planId", "planPalletId", "warehousePalletId", "orderId", "customerId", "customerName",
|
|
"planNo", "orderType", "volumeWeightKg", "grossWeightKg", "containerNo", "appointmentAt", "expectedLoadingAt",
|
|
"overseasWarehouseId", "overseasWarehouseCode", "overseasWarehouseName", "orderNo", "orderRefs", "inboundCode", "palletNo",
|
|
"destinationWarehouseId", "destinationWarehouseCode", "destinationWarehouseName", "fbaWarehouseCode",
|
|
"packageCount", "volumeCbm", "sourceStage", "cartonMarks", "relabelCartonMarks",
|
|
"destinationType", "deliveryAddress", "postalCode", "contactName", "contactPhone"
|
|
);
|
|
|
|
@Test
|
|
void candidateQueriesKeepRecordConstructorColumnOrder() throws Exception {
|
|
assertConstructorColumnOrder(DispatchCandidateMapper.class.getMethod("available",
|
|
String.class, List.class, String.class, String.class, String.class, String.class));
|
|
assertConstructorColumnOrder(DispatchCandidateMapper.class.getMethod("lockSelected",
|
|
String.class, List.class));
|
|
assertConstructorColumnOrder(DispatchCandidateMapper.class.getMethod("relabelAvailable",
|
|
String.class, String.class, String.class));
|
|
}
|
|
|
|
@Test
|
|
void selectedReceivedPalletMustStillHaveAvailableInventory() throws Exception {
|
|
Method method = DispatchCandidateMapper.class.getMethod("lockSelected", String.class, List.class);
|
|
String sql = String.join(" ", method.getAnnotation(Select.class).value());
|
|
|
|
assertTrue(sql.contains("AND (" + DispatchCandidateMapper.PLAN_ONLY + " OR EXISTS"));
|
|
assertTrue(sql.contains("pos.available_packages > 0"));
|
|
}
|
|
|
|
@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},'%')"));
|
|
}
|
|
|
|
@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());
|
|
assertTrue(sql.contains("CASE WHEN NOT " + DispatchCandidateMapper.PLAN_ONLY));
|
|
assertTrue(sql.contains("pos.overseas_warehouse_id"));
|
|
assertTrue(sql.contains(method.getName().equals("available")
|
|
? "NULLIF(CAST(ip.overseas_warehouse_id AS CHAR(128)),'')"
|
|
: "NULLIF(CAST(ip.overseas_warehouse_id AS CHAR),'')"));
|
|
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"));
|
|
assertTrue(sql.contains("ll_inventory_balance"));
|
|
}
|
|
}
|
|
|
|
@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"));
|
|
}
|
|
}
|
|
|
|
@Test
|
|
void relabelCandidatesReadWeightFromWarehouseTransferPalletSnapshot() throws Exception {
|
|
String sql=String.join(" ",DispatchCandidateMapper.class.getMethod("relabelAvailable",String.class,String.class,String.class).getAnnotation(Select.class).value());
|
|
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"));
|
|
}
|
|
|
|
@Test
|
|
void candidateGrossWeightUsesPhysicalSourcesAndNeverBillingVolumeWeight() 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("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"));
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|