fix: bind received dispatches to current per-order inventory

This commit is contained in:
2026-10-23 02:24:49 +08:00
parent 7e1703bde9
commit 85c7cde040
5 changed files with 88 additions and 5 deletions
@@ -133,7 +133,8 @@ public class AppointmentWorkDispatchBinding implements AppointmentWorkDispatchPo
return new Allocation(row.palletId(),row.batchKey(),version,row.customerId(),row.sourceKey(),row.destinationKey(),row.packages()); return new Allocation(row.palletId(),row.batchKey(),version,row.customerId(),row.sourceKey(),row.destinationKey(),row.packages());
} }
private void validatePhysical(String customer,PhysicalItem item,List<Allocation> selected,boolean received,boolean picked) { private void validatePhysical(String customer,PhysicalItem item,List<Allocation> selected,boolean received,boolean picked) {
var physical=picked?mapper.pickedPhysical(item.itemId(),customer):mapper.physical(item.planPalletId(),customer); var physical=picked?mapper.pickedPhysical(item.itemId(),customer)
:received?mapper.receivedPhysical(item.planPalletId(),customer):mapper.physical(item.planPalletId(),customer);
if(physical.isEmpty()) throw conflict("托盘缺少真实逐票分配,不能按整票或整托总数猜测批次"); if(physical.isEmpty()) throw conflict("托盘缺少真实逐票分配,不能按整票或整托总数猜测批次");
var canonical=AppointmentWorkPhysicalSources.canonical(physical,sources); var canonical=AppointmentWorkPhysicalSources.canonical(physical,sources);
var remaining=new HashMap<String,Long>(); var remaining=new HashMap<String,Long>();
@@ -259,8 +260,8 @@ public class AppointmentWorkDispatchBinding implements AppointmentWorkDispatchPo
} }
} }
private List<AppointmentWorkDispatchMapper.Bound> privateAllocation(String customer,long dispatch,PhysicalItem item) { private List<AppointmentWorkDispatchMapper.Bound> privateAllocation(String customer,long dispatch,PhysicalItem item) {
var physical=mapper.physical(item.planPalletId(),customer); var physical="RECEIVED".equals(item.sourceStage())?mapper.receivedPhysical(item.planPalletId(),customer):mapper.physical(item.planPalletId(),customer);
if(physical.isEmpty()) return List.of(); if(physical.isEmpty()) throw conflict("托盘缺少真实逐票库存归属,不能推算分配数量");
if(physical.stream().anyMatch(p->p.destinationKey()==null)) throw conflict("实物目的地尚未核实"); if(physical.stream().anyMatch(p->p.destinationKey()==null)) throw conflict("实物目的地尚未核实");
var canonical=AppointmentWorkPhysicalSources.canonical(physical,sources); var rows=new ArrayList<AppointmentWorkDispatchMapper.Bound>(); var canonical=AppointmentWorkPhysicalSources.canonical(physical,sources); var rows=new ArrayList<AppointmentWorkDispatchMapper.Bound>();
for(var part:physical) { for(var part:physical) {
@@ -122,6 +122,21 @@ public interface AppointmentWorkDispatchMapper {
List<Candidate> candidates(long id); List<Candidate> candidates(long id);
@Select("SELECT l.order_id,l.customer_id,CASE WHEN l.destination_type='PRIVATE' THEN CONCAT('CUSTOMER:',l.order_id) ELSE CONCAT('FBA:',l.fba_warehouse_code) END destination_key,CASE WHEN COUNT(a.planned_package_count)=COUNT(a.line_id) THEN SUM(a.planned_package_count) END planned_packages,CASE WHEN COUNT(a.counted_package_count)=COUNT(a.line_id) THEN SUM(a.counted_package_count) END counted_packages FROM ll_inbound_plan_pallet_allocation a JOIN ll_inbound_plan_line l ON l.id=a.line_id AND l.customer_id=a.customer_id WHERE a.pallet_id=#{pallet} AND a.customer_id=#{customer} GROUP BY l.order_id,l.customer_id,l.destination_type,l.fba_warehouse_code") @Select("SELECT l.order_id,l.customer_id,CASE WHEN l.destination_type='PRIVATE' THEN CONCAT('CUSTOMER:',l.order_id) ELSE CONCAT('FBA:',l.fba_warehouse_code) END destination_key,CASE WHEN COUNT(a.planned_package_count)=COUNT(a.line_id) THEN SUM(a.planned_package_count) END planned_packages,CASE WHEN COUNT(a.counted_package_count)=COUNT(a.line_id) THEN SUM(a.counted_package_count) END counted_packages FROM ll_inbound_plan_pallet_allocation a JOIN ll_inbound_plan_line l ON l.id=a.line_id AND l.customer_id=a.customer_id WHERE a.pallet_id=#{pallet} AND a.customer_id=#{customer} GROUP BY l.order_id,l.customer_id,l.destination_type,l.fba_warehouse_code")
List<Physical> physical(@Param("pallet") long pallet,@Param("customer") String customer); List<Physical> physical(@Param("pallet") long pallet,@Param("customer") String customer);
/** 已收货托盘按当前库位逐票归属读取;移托后的库存不能继续使用原收货分配箱数。 */
@Select("""
SELECT o.id order_id,o.customer_id,
CASE WHEN o.destination_type='PRIVATE' THEN CONCAT('CUSTOMER:',o.id) ELSE CONCAT('FBA:',o.fba_warehouse_code) END destination_key,
NULL planned_packages,
SUM(COALESCE(pos.available_packages,0)+COALESCE(pos.allocated_packages,0)+COALESCE(pos.picked_packages,0)) counted_packages
FROM ll_inbound_plan_pallet pp
JOIN ll_inventory_position pos ON pos.pallet_id=pp.warehouse_pallet_id AND pos.customer_id=pp.customer_id
JOIN ll_inventory_balance b ON b.id=pos.balance_id AND b.customer_id=pos.customer_id
JOIN ll_order o ON o.customer_id=b.customer_id AND o.inbound_code=b.inbound_code
WHERE pp.id=#{pallet} AND pp.customer_id=#{customer}
AND COALESCE(pos.available_packages,0)+COALESCE(pos.allocated_packages,0)+COALESCE(pos.picked_packages,0)>0
GROUP BY o.id,o.customer_id,o.destination_type,o.fba_warehouse_code
""")
List<Physical> receivedPhysical(@Param("pallet") long pallet,@Param("customer") String customer);
@Select("SELECT a.order_id,a.customer_id,CASE WHEN i.destination_type='PRIVATE' THEN CONCAT('CUSTOMER:',a.order_id) ELSE CONCAT('FBA:',i.fba_warehouse_code) END destination_key,NULL planned_packages,SUM(a.packages) counted_packages FROM ll_dispatch_pda_pick_order a JOIN ll_dispatch_pda_pick p ON p.id=a.pick_id AND p.customer_id=a.customer_id JOIN ll_dispatch_item i ON i.id=p.dispatch_item_id AND i.customer_id=p.customer_id WHERE i.id=#{item} AND a.customer_id=#{customer} GROUP BY a.order_id,a.customer_id,i.destination_type,i.fba_warehouse_code") @Select("SELECT a.order_id,a.customer_id,CASE WHEN i.destination_type='PRIVATE' THEN CONCAT('CUSTOMER:',a.order_id) ELSE CONCAT('FBA:',i.fba_warehouse_code) END destination_key,NULL planned_packages,SUM(a.packages) counted_packages FROM ll_dispatch_pda_pick_order a JOIN ll_dispatch_pda_pick p ON p.id=a.pick_id AND p.customer_id=a.customer_id JOIN ll_dispatch_item i ON i.id=p.dispatch_item_id AND i.customer_id=p.customer_id WHERE i.id=#{item} AND a.customer_id=#{customer} GROUP BY a.order_id,a.customer_id,i.destination_type,i.fba_warehouse_code")
List<Physical> pickedPhysical(@Param("item") long item,@Param("customer") String customer); List<Physical> pickedPhysical(@Param("item") long item,@Param("customer") String customer);
@Select("SELECT * FROM ll_inbound_fba_appointment WHERE id=#{id} FOR UPDATE") @Select("SELECT * FROM ll_inbound_fba_appointment WHERE id=#{id} FOR UPDATE")
@@ -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.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(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(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')"); 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)"); 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)); 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("缺少真实逐票分配")); 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. // 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",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()); 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,"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()))); 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",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()); 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()))); 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()); assertTrue(mapper.byDispatch(20).isEmpty());
@@ -58,7 +58,7 @@ class AppointmentWorkDispatchBindingTest {
when(mapper.pickedPhysical(30,"C")).thenReturn(List.of(new AppointmentWorkDispatchMapper.Physical(1L,"C","FBA:ONT8",null,4))); 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))); when(sources.canonicalOrders(anyList())).thenReturn(List.of(new AppointmentCoverageService.SourceRef("C","ORDER:1",null)));
assertDoesNotThrow(()->service.requireDeparture("D-20")); 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 departure=java.time.Instant.parse("2026-10-18T12:00:00Z");
var arrival=java.time.Instant.parse("2026-10-18T17:00:00Z"); var arrival=java.time.Instant.parse("2026-10-18T17:00:00Z");
clearInvocations(mapper); clearInvocations(mapper);
@@ -109,7 +109,7 @@ class AppointmentWorkDispatchBindingTest {
@Test void privateDispatchBindsOriginalPhysicalSourceAndDepartureRequiresActualPickedQuantity() { @Test void privateDispatchBindsOriginalPhysicalSourceAndDepartureRequiresActualPickedQuantity() {
var mapper=mock(AppointmentWorkDispatchMapper.class);var appointments=mock(InboundFbaAppointmentService.class);var sources=mock(AppointmentSourceService.class);var changes=mock(AppointmentWorkChanges.class); 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)); 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))); when(sources.canonicalOrders(anyList())).thenReturn(List.of(new AppointmentCoverageService.SourceRef("C","ORDER:1",null)));
var physical=new AppointmentWorkDispatchMapper.DispatchPhysical(30,10L,"C","RECEIVED",4); 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)); 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 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); var part=new AppointmentWorkDispatchMapper.Physical(1L,"C","FBA:ONT8",4,4);
when(mapper.physical(10,"C")).thenReturn(List.of(part)); 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))); 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()); var batch=mock(FbaAppointmentResponse.class);when(batch.appointmentStatus()).thenReturn("SUCCESS");when(batch.replacementBatchKeys()).thenReturn(List.of());
when(appointments.relatedBatchSnapshots(anyList())).thenReturn(List.of(batch)); 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(mapper,times(3)).insert(new AppointmentWorkDispatchMapper.Bound(20,30,10,"DISPATCH:20","C","ORDER:1","FBA:ONT8",4));
verify(changes,times(3)).requirementsChanged(); 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());
}
} }
@@ -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());
}
}
}