From 1a7e4800f5c6d46be3579bf4ecdbf34d9713a09c Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 30 Sep 2026 15:28:50 +0800 Subject: [PATCH] fix(firstleg): derive remainder pallet labels from frozen allocation facts --- .../mapper/LoadingPalletLabelMapper.java | 12 ++ .../mapper/LoadingPalletLabelMapperTest.java | 143 ++++++++++++++++++ 2 files changed, 155 insertions(+) create mode 100644 firstleg/src/test/java/com/baiyue/firstleg/mapper/LoadingPalletLabelMapperTest.java diff --git a/firstleg/src/main/java/com/baiyue/firstleg/mapper/LoadingPalletLabelMapper.java b/firstleg/src/main/java/com/baiyue/firstleg/mapper/LoadingPalletLabelMapper.java index 6b05deda6..faca5e4c3 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/mapper/LoadingPalletLabelMapper.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/mapper/LoadingPalletLabelMapper.java @@ -11,12 +11,24 @@ import java.util.List; @Mapper public interface LoadingPalletLabelMapper { + // Remainder slices inherit bound slots, not new weight records. Their slot/weight + // identity survives release or transfer; preserved empty slots are not pallets. @Select(""" SELECT p.id AS plan_id,p.plan_no,p.plan_status,b.container_no,b.cabinet_no,b.warehouse_id, i.id AS item_id,i.account_id,i.inbound_id,i.inbound_code,i.destination_warehouse_id, i.destination_warehouse_code,i.destination_warehouse_name, COALESCE(NULLIF(i.pallet_count,0), CEIL(COALESCE(NULLIF(o.pallet_count,0), + NULLIF((SELECT COUNT(DISTINCT weight.id) + FROM fl_inbound_pallet_allocation allocation + JOIN fl_inbound_weight_result weight + ON weight.id=allocation.weight_result_id AND weight.tenant_id=allocation.tenant_id + AND weight.deleted=0 AND weight.recognition_status='CONFIRMED' + AND (weight.inbound_id=o.id OR weight.inbound_id=o.root_inbound_id) + WHERE allocation.tenant_id=i.account_id AND allocation.inbound_id=i.inbound_id + AND allocation.block_id>0 AND allocation.slot_no>0 AND allocation.reservation_id>0 + AND allocation.allocated_package_count>0 + AND allocation.allocation_status IN ('CONFIRMED','TRANSFERRED','RELEASED')),0), (SELECT COUNT(*) FROM fl_inbound_weight_result weight WHERE weight.tenant_id=i.account_id AND weight.inbound_id=i.inbound_id AND weight.deleted=0 AND weight.recognition_status='CONFIRMED'),0) diff --git a/firstleg/src/test/java/com/baiyue/firstleg/mapper/LoadingPalletLabelMapperTest.java b/firstleg/src/test/java/com/baiyue/firstleg/mapper/LoadingPalletLabelMapperTest.java new file mode 100644 index 000000000..c5e09cb6d --- /dev/null +++ b/firstleg/src/test/java/com/baiyue/firstleg/mapper/LoadingPalletLabelMapperTest.java @@ -0,0 +1,143 @@ +package com.baiyue.firstleg.mapper; + +import org.apache.ibatis.datasource.unpooled.UnpooledDataSource; +import org.apache.ibatis.mapping.Environment; +import org.apache.ibatis.session.Configuration; +import org.apache.ibatis.session.SqlSession; +import org.apache.ibatis.session.SqlSessionFactoryBuilder; +import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +/** Execute the production MyBatis query, including inherited weight/slot joins. */ +class LoadingPalletLabelMapperTest { + private SqlSession session; + + @BeforeEach + void schema() throws Exception { + var dataSource = new UnpooledDataSource("org.h2.Driver", + "jdbc:h2:mem:" + UUID.randomUUID() + ";MODE=MySQL", "sa", ""); + var configuration = new Configuration(new Environment("test", new JdbcTransactionFactory(), dataSource)); + configuration.setMapUnderscoreToCamelCase(true); + configuration.addMapper(LoadingPalletLabelMapper.class); + session = new SqlSessionFactoryBuilder().build(configuration).openSession(); + execute("CREATE TABLE fl_loading_plan(id BIGINT PRIMARY KEY,plan_no VARCHAR,plan_status VARCHAR,container_booking_id BIGINT)"); + execute("CREATE TABLE fl_container_booking(id BIGINT PRIMARY KEY,container_no VARCHAR,cabinet_no VARCHAR,warehouse_id BIGINT,record_status VARCHAR)"); + // MySQL '/' preserves fractional results for integer operands; H2's integer + // division truncates. Decimal scale zero models MySQL's existing CEIL formula. + execute("CREATE TABLE fl_loading_plan_item(id BIGINT PRIMARY KEY,loading_plan_id BIGINT,account_id BIGINT,inbound_id BIGINT,inbound_code VARCHAR,destination_warehouse_id BIGINT,destination_warehouse_code VARCHAR,destination_warehouse_name VARCHAR,pallet_count INT,package_count DECIMAL(10,0),load_sequence INT,deleted INT)"); + execute("CREATE TABLE fl_inbound_order(id BIGINT PRIMARY KEY,tenant_id BIGINT,root_inbound_id BIGINT,pallet_count INT,total_package_count DECIMAL(10,0),fulfillment_type VARCHAR,destination_type VARCHAR,deleted INT)"); + execute("CREATE TABLE fl_inbound_weight_result(id BIGINT PRIMARY KEY,tenant_id BIGINT,inbound_id BIGINT,pallet_id BIGINT,recognition_status VARCHAR,deleted INT)"); + execute("CREATE TABLE fl_inbound_pallet_allocation(id BIGINT PRIMARY KEY,tenant_id BIGINT,inbound_id BIGINT,block_id BIGINT,slot_no INT,reservation_id BIGINT,weight_result_id BIGINT,allocated_package_count INT,picked_package_count INT,allocation_status VARCHAR)"); + execute("INSERT INTO fl_loading_plan VALUES(1,'LP-1','LOADING',2)"); + execute("INSERT INTO fl_container_booking VALUES(2,'MSCU6639870','D0001',3,'ACTIVE')"); + execute("INSERT INTO fl_inbound_order VALUES(20,100,30,0,2,'STANDARD','FBA',0)"); + execute("INSERT INTO fl_loading_plan_item VALUES(10,1,100,20,'IN-CHILD',200,'ONT8','ONT8',0,2,1,0)"); + } + + @AfterEach + void close() { if (session != null) session.close(); } + + private void execute(String sql) throws Exception { + try (var statement = session.getConnection().createStatement()) { statement.execute(sql); } + session.clearCache(); + } + + private void inherited(long id, int slot, int boxes) throws Exception { + execute("INSERT INTO fl_inbound_weight_result VALUES(" + id + ",100,30,900,'CONFIRMED',0)"); + execute("INSERT INTO fl_inbound_pallet_allocation VALUES(" + id + ",100,20,600," + slot + + "," + (700 + slot) + "," + id + "," + boxes + ",0,'CONFIRMED')"); + } + + private int count() { + var rows = session.getMapper(LoadingPalletLabelMapper.class).lockSources(1L); + assertThat(rows).hasSize(1); + assertThat(rows.get(0).getInboundId()).isEqualTo(20L); + assertThat(rows.get(0).getAccountId()).isEqualTo(100L); + return rows.get(0).getPalletCount(); + } + + @Test + void childUsesInheritedConfirmedSlotWithoutCreatingItsOwnWeightAndKeepsReleasedSnapshot() throws Exception { + inherited(50, 1, 2); + assertThat(count()).isEqualTo(1); + execute("UPDATE fl_inbound_pallet_allocation SET picked_package_count=allocated_package_count,allocation_status='RELEASED'"); + assertThat(count()).isEqualTo(1); + execute("UPDATE fl_inbound_pallet_allocation SET allocation_status='TRANSFERRED'"); + assertThat(count()).isEqualTo(1); + try (var statement = session.getConnection().createStatement(); + var result = statement.executeQuery("SELECT COUNT(*) FROM fl_inbound_weight_result WHERE inbound_id=20")) { + result.next(); assertThat(result.getInt(1)).isZero(); + } + } + + @Test + void countsWeightSlotIdentityOnceAndDoesNotCollapseReusedPhysicalPalletIds() throws Exception { + inherited(50, 1, 1); + execute("INSERT INTO fl_inbound_pallet_allocation VALUES(51,100,20,601,1,702,50,1,0,'CONFIRMED')"); + assertThat(count()).isEqualTo(1); + inherited(52, 2, 1); + assertThat(count()).isEqualTo(2); + } + + @ParameterizedTest + @ValueSource(strings = {"empty", "no-weight", "wrong-customer", "wrong-child", "other-child-weight", + "unconfirmed", "deleted-weight", "invalid-block", "invalid-slot", "invalid-reservation", "reserved"}) + void invalidOrUnrelatedSnapshotsNeverInventOnePallet(String scenario) throws Exception { + inherited(50, 1, 2); + execute(switch (scenario) { + case "empty" -> "UPDATE fl_inbound_pallet_allocation SET allocated_package_count=0"; + case "no-weight" -> "UPDATE fl_inbound_pallet_allocation SET weight_result_id=NULL"; + case "wrong-customer" -> "UPDATE fl_inbound_weight_result SET tenant_id=101"; + case "wrong-child" -> "UPDATE fl_inbound_pallet_allocation SET inbound_id=21"; + case "other-child-weight" -> "UPDATE fl_inbound_weight_result SET inbound_id=21"; + case "unconfirmed" -> "UPDATE fl_inbound_weight_result SET recognition_status='PENDING'"; + case "deleted-weight" -> "UPDATE fl_inbound_weight_result SET deleted=1"; + case "invalid-block" -> "UPDATE fl_inbound_pallet_allocation SET block_id=0"; + case "invalid-slot" -> "UPDATE fl_inbound_pallet_allocation SET slot_no=0"; + case "invalid-reservation" -> "UPDATE fl_inbound_pallet_allocation SET reservation_id=0"; + case "reserved" -> "UPDATE fl_inbound_pallet_allocation SET allocation_status='RESERVED'"; + default -> throw new IllegalArgumentException(scenario); + }); + assertThat(count()).isZero(); + } + + @Test + void preservesPlanThenInboundPriorityAndRoundsActualSliceUp() throws Exception { + inherited(50, 1, 1); inherited(51, 2, 1); inherited(52, 3, 1); + execute("UPDATE fl_inbound_order SET total_package_count=8"); + execute("UPDATE fl_loading_plan_item SET package_count=3"); + assertThat(count()).isEqualTo(2); + execute("UPDATE fl_inbound_order SET pallet_count=4"); + assertThat(count()).isEqualTo(2); + execute("UPDATE fl_loading_plan_item SET pallet_count=5"); + assertThat(count()).isEqualTo(5); + } + + @ParameterizedTest + @ValueSource(strings = {"NULL", "0"}) + void rootWithoutRootPointerStillUsesItsOwnConfirmedWeight(String rootId) throws Exception { + execute("UPDATE fl_inbound_order SET root_inbound_id=" + rootId); + execute("INSERT INTO fl_inbound_weight_result VALUES(50,100,20,900,'CONFIRMED',0)"); + assertThat(count()).isEqualTo(1); + execute("INSERT INTO fl_inbound_pallet_allocation VALUES(50,100,20,600,1,701,50,2,0,'CONFIRMED')"); + assertThat(count()).isEqualTo(1); + } + + @Test + void anotherChildMayOnlyUseASourceWeightThroughItsOwnSnapshot() throws Exception { + inherited(50, 1, 2); + execute("INSERT INTO fl_inbound_order VALUES(21,100,30,0,2,'STANDARD','FBA',0)"); + execute("UPDATE fl_loading_plan_item SET inbound_id=21,inbound_code='OTHER-CHILD'"); + assertThat(session.getMapper(LoadingPalletLabelMapper.class).lockSources(1L).get(0).getPalletCount()).isZero(); + execute("INSERT INTO fl_inbound_pallet_allocation VALUES(51,100,21,600,1,701,50,2,0,'CONFIRMED')"); + assertThat(session.getMapper(LoadingPalletLabelMapper.class).lockSources(1L).get(0).getPalletCount()).isEqualTo(1); + } +}