fix: bind received dispatches to current per-order inventory
This commit is contained in:
+4
@@ -38,6 +38,8 @@ class AppointmentWorkPreArrangementPersistenceTest {
|
||||
doReturn(List.of(new AppointmentWorkDispatchMapper.Candidate(101L,"P-B",11L,"IP-B","B","RECEIVED",false),new AppointmentWorkDispatchMapper.Candidate(102L,"P-D",12L,"IP-D","D","RECEIVED",false))).when(mapper).candidates(batch.id());
|
||||
doReturn(List.of(new AppointmentWorkDispatchMapper.Physical(1L,"B","FBA:ONT8",60,60))).when(mapper).physical(101L,"B");
|
||||
doReturn(List.of(new AppointmentWorkDispatchMapper.Physical(2L,"D","FBA:ONT8",40,40))).when(mapper).physical(102L,"D");
|
||||
doReturn(List.of(new AppointmentWorkDispatchMapper.Physical(1L,"B","FBA:ONT8",null,60))).when(mapper).receivedPhysical(101L,"B");
|
||||
doReturn(List.of(new AppointmentWorkDispatchMapper.Physical(2L,"D","FBA:ONT8",null,40))).when(mapper).receivedPhysical(102L,"D");
|
||||
var jdbc=new JdbcTemplate(context.getBean(DataSource.class));jdbc.update("INSERT INTO ll_dispatch_order(id,dispatch_no,record_version,status) VALUES(20,'DP-B',0,'ACTIVE')");
|
||||
jdbc.update("INSERT INTO ll_dispatch_item(id,customer_id,dispatch_order_id,plan_pallet_id,source_stage,package_count) VALUES(201,'B',20,101,'RECEIVED',60)");
|
||||
var candidates=new AppointmentWorkDispatchCandidates(mapper,batches,sources,new AppointmentDispatchAvailability(store));
|
||||
@@ -59,10 +61,12 @@ class AppointmentWorkPreArrangementPersistenceTest {
|
||||
assertTrue(assertThrows(ResponseStatusException.class,()->binding.requireDeparture("DP-B")).getReason().contains("缺少真实逐票分配"));
|
||||
// Unchanged real-source guards still reject an unverified pallet and an allocation owned by another customer.
|
||||
doReturn(List.of(new AppointmentWorkDispatchMapper.Physical(1L,"B","FBA:ONT8",60,null))).when(mapper).physical(101L,"B");
|
||||
doReturn(List.of(new AppointmentWorkDispatchMapper.Physical(1L,"B","FBA:ONT8",null,null))).when(mapper).receivedPhysical(101L,"B");
|
||||
assertFalse(candidates.read(batch.batchKey()).candidates().get(0).blockReasons().isEmpty());
|
||||
assertThrows(ResponseStatusException.class,()->tx.executeWithoutResult(s->binding.bindItems(20,List.of(new AppointmentWorkDispatchPort.PhysicalItem(201L,"B",101L,"RECEIVED",60)),after.allocations())));
|
||||
assertThrows(ResponseStatusException.class,()->tx.executeWithoutResult(s->binding.bindItems(20,List.of(new AppointmentWorkDispatchPort.PhysicalItem(201L,"D",101L,"RECEIVED",60)),after.allocations())));
|
||||
doReturn(List.of(new AppointmentWorkDispatchMapper.Physical(1L,"B","FBA:ONT8",61,61))).when(mapper).physical(101L,"B");
|
||||
doReturn(List.of(new AppointmentWorkDispatchMapper.Physical(1L,"B","FBA:ONT8",null,61))).when(mapper).receivedPhysical(101L,"B");
|
||||
var excessive=candidates.read(batch.batchKey()).candidates().get(0);assertFalse(excessive.blockReasons().isEmpty());
|
||||
assertThrows(ResponseStatusException.class,()->tx.executeWithoutResult(s->binding.bindItems(20,List.of(new AppointmentWorkDispatchPort.PhysicalItem(201L,"B",101L,"RECEIVED",61)),excessive.allocations())));
|
||||
assertTrue(mapper.byDispatch(20).isEmpty());
|
||||
|
||||
+24
-2
@@ -58,7 +58,7 @@ class AppointmentWorkDispatchBindingTest {
|
||||
when(mapper.pickedPhysical(30,"C")).thenReturn(List.of(new AppointmentWorkDispatchMapper.Physical(1L,"C","FBA:ONT8",null,4)));
|
||||
when(sources.canonicalOrders(anyList())).thenReturn(List.of(new AppointmentCoverageService.SourceRef("C","ORDER:1",null)));
|
||||
assertDoesNotThrow(()->service.requireDeparture("D-20"));
|
||||
when(mapper.physical(10,"C")).thenReturn(List.of(new AppointmentWorkDispatchMapper.Physical(1L,"C","FBA:ONT8",4,4)));
|
||||
when(mapper.receivedPhysical(10,"C")).thenReturn(List.of(new AppointmentWorkDispatchMapper.Physical(1L,"C","FBA:ONT8",null,4)));
|
||||
var departure=java.time.Instant.parse("2026-10-18T12:00:00Z");
|
||||
var arrival=java.time.Instant.parse("2026-10-18T17:00:00Z");
|
||||
clearInvocations(mapper);
|
||||
@@ -109,7 +109,7 @@ class AppointmentWorkDispatchBindingTest {
|
||||
@Test void privateDispatchBindsOriginalPhysicalSourceAndDepartureRequiresActualPickedQuantity() {
|
||||
var mapper=mock(AppointmentWorkDispatchMapper.class);var appointments=mock(InboundFbaAppointmentService.class);var sources=mock(AppointmentSourceService.class);var changes=mock(AppointmentWorkChanges.class);
|
||||
var service=new AppointmentWorkDispatchBinding(mapper,appointments,sources,changes,mock(com.baiyue.lastleg.inboundplan.receiving.FbaReceivingStore.class),mock(AppointmentWorkDispatchCandidates.class));
|
||||
when(mapper.physical(10,"C")).thenReturn(List.of(new AppointmentWorkDispatchMapper.Physical(1L,"C","CUSTOMER:1",4,4)));
|
||||
when(mapper.receivedPhysical(10,"C")).thenReturn(List.of(new AppointmentWorkDispatchMapper.Physical(1L,"C","CUSTOMER:1",null,4)));
|
||||
when(sources.canonicalOrders(anyList())).thenReturn(List.of(new AppointmentCoverageService.SourceRef("C","ORDER:1",null)));
|
||||
var physical=new AppointmentWorkDispatchMapper.DispatchPhysical(30,10L,"C","RECEIVED",4);
|
||||
when(mapper.dispatchPhysicalById(20)).thenReturn(List.of(physical));when(mapper.dispatchPhysical("D-20")).thenReturn(List.of(physical));
|
||||
@@ -138,6 +138,7 @@ class AppointmentWorkDispatchBindingTest {
|
||||
var service=new AppointmentWorkDispatchBinding(mapper,appointments,sources,changes,mock(com.baiyue.lastleg.inboundplan.receiving.FbaReceivingStore.class),mock(AppointmentWorkDispatchCandidates.class));
|
||||
var part=new AppointmentWorkDispatchMapper.Physical(1L,"C","FBA:ONT8",4,4);
|
||||
when(mapper.physical(10,"C")).thenReturn(List.of(part));
|
||||
when(mapper.receivedPhysical(10,"C")).thenReturn(List.of(part));
|
||||
when(sources.canonicalOrders(anyList())).thenReturn(List.of(new AppointmentCoverageService.SourceRef("C","ORDER:1",null)));
|
||||
var batch=mock(FbaAppointmentResponse.class);when(batch.appointmentStatus()).thenReturn("SUCCESS");when(batch.replacementBatchKeys()).thenReturn(List.of());
|
||||
when(appointments.relatedBatchSnapshots(anyList())).thenReturn(List.of(batch));
|
||||
@@ -152,4 +153,25 @@ class AppointmentWorkDispatchBindingTest {
|
||||
verify(mapper,times(3)).insert(new AppointmentWorkDispatchMapper.Bound(20,30,10,"DISPATCH:20","C","ORDER:1","FBA:ONT8",4));
|
||||
verify(changes,times(3)).requirementsChanged();
|
||||
}
|
||||
|
||||
@Test void receivedBindingUsesCurrentPerOrderInventoryAndRejectsTheOldReceiptTotal() {
|
||||
var mapper=mock(AppointmentWorkDispatchMapper.class);var sources=mock(AppointmentSourceService.class);
|
||||
var service=new AppointmentWorkDispatchBinding(mapper,mock(InboundFbaAppointmentService.class),sources,
|
||||
mock(AppointmentWorkChanges.class),mock(com.baiyue.lastleg.inboundplan.receiving.FbaReceivingStore.class),mock(AppointmentWorkDispatchCandidates.class));
|
||||
when(mapper.receivedPhysical(10,"C")).thenReturn(List.of(
|
||||
new AppointmentWorkDispatchMapper.Physical(1L,"C","FBA:ONT8",null,1),
|
||||
new AppointmentWorkDispatchMapper.Physical(2L,"C","FBA:ONT8",null,2)));
|
||||
when(sources.canonicalOrders(anyList())).thenReturn(List.of(
|
||||
new AppointmentCoverageService.SourceRef("C","ORDER:1",null),new AppointmentCoverageService.SourceRef("C","ORDER:2",null)));
|
||||
when(mapper.dispatchPhysicalById(20)).thenReturn(List.of(new AppointmentWorkDispatchMapper.DispatchPhysical(30,10L,"C","RECEIVED",3)));
|
||||
service.bindItems(20,List.of(new AppointmentWorkDispatchPort.PhysicalItem(30L,"C",10L,"RECEIVED",3)),List.of());
|
||||
verify(mapper).insert(new AppointmentWorkDispatchMapper.Bound(20,30,10,"DISPATCH:20","C","ORDER:1","FBA:ONT8",1));
|
||||
verify(mapper).insert(new AppointmentWorkDispatchMapper.Bound(20,30,10,"DISPATCH:20","C","ORDER:2","FBA:ONT8",2));
|
||||
verify(mapper,never()).physical(anyLong(),anyString());
|
||||
when(mapper.dispatchPhysicalById(20)).thenReturn(List.of(new AppointmentWorkDispatchMapper.DispatchPhysical(30,10L,"C","RECEIVED",4)));
|
||||
clearInvocations(mapper);
|
||||
assertThrows(org.springframework.web.server.ResponseStatusException.class,()->service.bindItems(20,
|
||||
List.of(new AppointmentWorkDispatchPort.PhysicalItem(30L,"C",10L,"RECEIVED",4)),List.of()));
|
||||
verify(mapper,never()).insert(any());
|
||||
}
|
||||
}
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package com.baiyue.lastleg.inboundplan.work;
|
||||
|
||||
import java.util.UUID;
|
||||
import org.apache.ibatis.mapping.Environment;
|
||||
import org.apache.ibatis.session.Configuration;
|
||||
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
|
||||
import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.datasource.DriverManagerDataSource;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class AppointmentWorkReceivedPhysicalTest {
|
||||
@Test void currentPalletSourcesKeepActualOrderCountsAfterMovesAndIncludeReservedOrPickedStock() {
|
||||
var ds = new DriverManagerDataSource("jdbc:h2:mem:" + UUID.randomUUID() + ";MODE=MySQL;DB_CLOSE_DELAY=-1", "sa", "");
|
||||
var db = new JdbcTemplate(ds);
|
||||
db.execute("CREATE TABLE ll_inbound_plan_pallet(id BIGINT,customer_id VARCHAR,warehouse_pallet_id BIGINT)");
|
||||
db.execute("CREATE TABLE ll_inventory_position(id BIGINT,customer_id VARCHAR,pallet_id BIGINT,balance_id BIGINT,available_packages INT,allocated_packages INT,picked_packages INT,hold_packages INT)");
|
||||
db.execute("CREATE TABLE ll_inventory_balance(id BIGINT,customer_id VARCHAR,inbound_code VARCHAR)");
|
||||
db.execute("CREATE TABLE ll_order(id BIGINT,customer_id VARCHAR,inbound_code VARCHAR,destination_type VARCHAR,fba_warehouse_code VARCHAR)");
|
||||
db.update("INSERT INTO ll_inbound_plan_pallet VALUES(10,'C',99)");
|
||||
db.update("INSERT INTO ll_inventory_balance VALUES(1,'C','IN1'),(2,'C','IN2'),(3,'OTHER','IN1')");
|
||||
db.update("INSERT INTO ll_order VALUES(101,'C','IN1','FBA','ONT8'),(102,'C','IN2','PRIVATE',NULL),(201,'OTHER','IN1','FBA','ONT8')");
|
||||
db.update("INSERT INTO ll_inventory_position VALUES(1,'C',99,1,0,1,0,5),(2,'C',99,2,1,0,1,0),(3,'OTHER',99,3,99,0,0,0),(4,'C',100,1,99,0,0,0)");
|
||||
var config = new Configuration(new Environment("test", new JdbcTransactionFactory(), ds));
|
||||
config.setMapUnderscoreToCamelCase(true); config.addMapper(AppointmentWorkDispatchMapper.class);
|
||||
try (var session = new SqlSessionFactoryBuilder().build(config).openSession()) {
|
||||
var mapper = session.getMapper(AppointmentWorkDispatchMapper.class);
|
||||
var rows = mapper.receivedPhysical(10,"C");
|
||||
assertEquals(2,rows.size());
|
||||
assertEquals(1,rows.stream().filter(r->r.orderId()==101).findFirst().orElseThrow().countedPackages());
|
||||
assertEquals(2,rows.stream().filter(r->r.orderId()==102).findFirst().orElseThrow().countedPackages());
|
||||
assertEquals("CUSTOMER:102",rows.stream().filter(r->r.orderId()==102).findFirst().orElseThrow().destinationKey());
|
||||
assertTrue(rows.stream().allMatch(r->r.plannedPackages()==null && r.customerId().equals("C")));
|
||||
assertTrue(mapper.receivedPhysical(10,"OTHER").isEmpty());
|
||||
db.update("UPDATE ll_inventory_position SET available_packages=0,picked_packages=0 WHERE id=2");
|
||||
session.clearCache(); rows=mapper.receivedPhysical(10,"C");
|
||||
assertEquals(1,rows.size()); assertEquals(1,rows.get(0).countedPackages());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user