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

160 lines
9.9 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;
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.assertEquals;
2026-08-21 17:21:26 +08:00
class DispatchCandidateMapperContractTest {
@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());
assertTrue(sql.contains("p.warehouse_pallet_id IS NULL OR EXISTS"));
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());
assertTrue(sql.contains("CASE WHEN p.warehouse_pallet_id IS NOT NULL"));
assertTrue(sql.contains("pos.overseas_warehouse_id"));
2026-09-18 11:41:04 +08:00
assertTrue(sql.contains("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"));
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-09-18 11:41:04 +08:00
@Test
void candidateGrossWeightIsAllocatedByPalletAndFallsBackToVolumeWeight() 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"));
assertTrue(sql.contains("MIN(o.volume_weight_kg)) AS grossWeightKg"));
}
}
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;
}
}
}