fix(lastleg): complete stock transfer through linked relabel
This commit is contained in:
+22
@@ -76,6 +76,7 @@ class RelabelApplicationServiceTest {
|
||||
@Mock OutboundPalletItemMapper outboundItems;
|
||||
@Mock OutboundPdaTaskMapper outboundTasks;
|
||||
@Mock WarehouseTransferOrderMapper warehouseTransfers;
|
||||
@Mock com.baiyue.lastleg.transfer.service.WarehouseTransferRelabelCompletionService transferRelabelCompletion;
|
||||
@Mock OverseasFeeSourceService feeSources;
|
||||
@Mock ReturnOrderMapper returnOrders;
|
||||
@Mock com.baiyue.lastleg.relabel.pda.mapper.PdaRelabelTaskMapper pdaTasks;
|
||||
@@ -93,6 +94,7 @@ class RelabelApplicationServiceTest {
|
||||
});
|
||||
lenient().when(pdaTasks.updateWithVersion(any(), any())).thenReturn(1);
|
||||
ReflectionTestUtils.setField(service, "applications", applications);
|
||||
ReflectionTestUtils.setField(service, "transferRelabelCompletion", transferRelabelCompletion);
|
||||
ReflectionTestUtils.setField(service, "items", items);
|
||||
ReflectionTestUtils.setField(service, "histories", histories);
|
||||
ReflectionTestUtils.setField(service, "orders", orders);
|
||||
@@ -704,6 +706,26 @@ class RelabelApplicationServiceTest {
|
||||
assertEquals("WAIT_FBA_DESTINATION", application.getFulfillmentStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void existingStockTransferConfirmationFinalizesOriginalSourcesOnceWithoutIndependentOutbound() {
|
||||
var application=confirmationApplication("EXPRESS",false);application.setLastlegOrderId(88L);
|
||||
var transfer=new WarehouseTransferOrder();transfer.setId(77L);transfer.setCustomerId("10001");
|
||||
transfer.setRelabelNo("FBR-1");transfer.setStatus("WAIT_RELABEL");transfer.setLastlegOrderId(88L);
|
||||
transfer.setAllocationIdempotencyKey("ORIGINAL-SOURCE");transfer.setFbaWarehouseCode("ONT8");
|
||||
var transferOrder=order(88,"10001","LMO-TRANSFER-1","WAIT_RELABEL");
|
||||
when(applications.lockNo("10001","FBR-1")).thenReturn(application);stubDetail(application);
|
||||
when(warehouseTransfers.lockByRelabelNo("10001","FBR-1")).thenReturn(transfer);
|
||||
when(orders.selectById(88L)).thenReturn(transferOrder);
|
||||
var command=new RelabelRequests.CustomerConfirm("CONFIRM-ORIGINAL","accepted");
|
||||
service.confirmCompletion("10001","FBR-1","customer-1",command);
|
||||
assertEquals("ALLOCATED",transfer.getStatus());assertEquals("WAIT_TRANSFER_OUTBOUND",application.getFulfillmentStatus());
|
||||
assertEquals("ALLOCATED",transferOrder.getCurrentStatus());
|
||||
service.confirmCompletion("10001","FBR-1","customer-1",command);
|
||||
verify(transferRelabelCompletion).complete(eq(transfer),eq(application),any(),eq("customer-1"),any());
|
||||
verify(outboundOrders,never()).insert(any(OutboundOrder.class));
|
||||
assertNotNull(transfer.getRelabelCompletedAt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void transferRelabelConfirmationDerivesUniqueDestinationFromRelabelItems() {
|
||||
RelabelApplication application = confirmationApplication("TRUCK", false);
|
||||
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
package com.baiyue.lastleg.transfer.service;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
|
||||
import com.baiyue.lastleg.inventory.entity.*;
|
||||
import com.baiyue.lastleg.inventory.mapper.*;
|
||||
import com.baiyue.lastleg.inventory.command.mapper.InventoryCommandMapper;
|
||||
import com.baiyue.lastleg.inventory.command.service.InventoryCommandService;
|
||||
import com.baiyue.lastleg.inboundplan.pda.gateway.WarehouseResourceGateway;
|
||||
import com.baiyue.lastleg.relabel.entity.*;
|
||||
import com.baiyue.lastleg.transfer.entity.*;
|
||||
import com.baiyue.lastleg.transfer.mapper.*;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.*;
|
||||
import org.mybatis.spring.SqlSessionTemplate;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.datasource.*;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class WarehouseTransferRelabelPersistenceTest {
|
||||
JdbcTemplate jdbc; SqlSessionTemplate session; TransactionTemplate tx;
|
||||
WarehouseTransferRelabelCompletionService completion; InventoryCommandService inventory;
|
||||
WarehouseTransferOrder transfer; RelabelApplication application; RelabelItem item;
|
||||
@BeforeEach void setup() throws Exception {
|
||||
var ds = new DriverManagerDataSource("jdbc:h2:mem:"+UUID.randomUUID()+";MODE=MySQL;DB_CLOSE_DELAY=-1", "sa", "");
|
||||
jdbc = new JdbcTemplate(ds); tx = new TransactionTemplate(new DataSourceTransactionManager(ds));
|
||||
for (Class<?> type : List.of(InventoryBalance.class,InventoryPosition.class,InventoryLedger.class,
|
||||
InventoryOperation.class,WarehouseTransferInventoryAllocation.class,WarehouseTransferRelabelBinding.class,
|
||||
WarehouseTransferOrder.class,RelabelApplication.class,RelabelItem.class)) {
|
||||
var columns = new ArrayList<String>();
|
||||
for (var f : type.getDeclaredFields()) {
|
||||
if(java.lang.reflect.Modifier.isStatic(f.getModifiers()))continue;
|
||||
TableField field=f.getAnnotation(TableField.class);if(field!=null&&!field.exist())continue;
|
||||
String name=f.getName().replaceAll("([a-z])([A-Z])", "$1_$2").toLowerCase();
|
||||
String sqlType=f.getType()==String.class?"VARCHAR(2048)":f.getType()==LocalDateTime.class?"TIMESTAMP":f.getType()==Boolean.class?"BOOLEAN":"BIGINT";
|
||||
columns.add(name+" "+sqlType+(name.equals("id")?" PRIMARY KEY":""));
|
||||
}
|
||||
jdbc.execute("CREATE TABLE "+type.getAnnotation(TableName.class).value()+" ("+String.join(",",columns)+")");
|
||||
}
|
||||
jdbc.execute("CREATE TABLE ll_dispatch_pallet_lock(customer_id VARCHAR(100),warehouse_pallet_id BIGINT,status VARCHAR(40))");
|
||||
jdbc.execute("CREATE UNIQUE INDEX allocation_active ON ll_warehouse_transfer_inventory_allocation(customer_id,active_position_key)");
|
||||
jdbc.execute("CREATE INDEX idx_ll_wh_transfer_allocation_position ON ll_warehouse_transfer_inventory_allocation(transfer_order_id,inventory_position_id)");
|
||||
jdbc.execute("CREATE UNIQUE INDEX operation_key ON ll_inventory_operation(customer_id,idempotency_key)");
|
||||
var configuration=new MybatisConfiguration(); configuration.setMapUnderscoreToCamelCase(true);
|
||||
for(Class<?> mapper:List.of(InventoryBalanceMapper.class,InventoryPositionMapper.class,InventoryLedgerMapper.class,
|
||||
InventoryOperationMapper.class,InventoryCommandMapper.class,WarehouseTransferInventoryAllocationMapper.class,
|
||||
WarehouseTransferRelabelBindingMapper.class,WarehouseTransferOrderMapper.class,
|
||||
com.baiyue.lastleg.relabel.mapper.RelabelApplicationMapper.class,
|
||||
com.baiyue.lastleg.relabel.mapper.RelabelItemMapper.class))configuration.addMapper(mapper);
|
||||
var factory=new MybatisSqlSessionFactoryBean();factory.setDataSource(ds);factory.setConfiguration(configuration);
|
||||
session=new SqlSessionTemplate(factory.getObject());
|
||||
inventory=new InventoryCommandService();
|
||||
ReflectionTestUtils.setField(inventory,"balances",session.getMapper(InventoryBalanceMapper.class));
|
||||
ReflectionTestUtils.setField(inventory,"positions",session.getMapper(InventoryPositionMapper.class));
|
||||
ReflectionTestUtils.setField(inventory,"ledgers",session.getMapper(InventoryLedgerMapper.class));
|
||||
ReflectionTestUtils.setField(inventory,"operations",session.getMapper(InventoryOperationMapper.class));
|
||||
ReflectionTestUtils.setField(inventory,"commands",session.getMapper(InventoryCommandMapper.class));
|
||||
ReflectionTestUtils.setField(inventory,"warehouseGateway",mock(WarehouseResourceGateway.class));
|
||||
ReflectionTestUtils.setField(inventory,"objectMapper",new ObjectMapper().findAndRegisterModules());
|
||||
completion=new WarehouseTransferRelabelCompletionService();
|
||||
ReflectionTestUtils.setField(completion,"allocations",session.getMapper(WarehouseTransferInventoryAllocationMapper.class));
|
||||
ReflectionTestUtils.setField(completion,"bindings",session.getMapper(WarehouseTransferRelabelBindingMapper.class));
|
||||
ReflectionTestUtils.setField(completion,"inventoryRows",session.getMapper(InventoryCommandMapper.class));
|
||||
ReflectionTestUtils.setField(completion,"inventory",inventory);
|
||||
transfer=new WarehouseTransferOrder();transfer.setId(1L);transfer.setTransferNo("WT-ORIGINAL");transfer.setCustomerId("7");
|
||||
transfer.setPackageCount(1);transfer.setOperatingWarehouseId("9");transfer.setLastlegOrderId(10L);transfer.setRelabelApplicationId(11L);
|
||||
application=new RelabelApplication();application.setId(11L);application.setCustomerId("7");application.setLastlegOrderId(10L);
|
||||
application.setCustomerConfirmedAt(LocalDateTime.now());
|
||||
item=new RelabelItem();item.setId(12L);item.setApplicationId(11L);item.setCustomerId("7");item.setSkuCode("OLD");
|
||||
item.setSourceInventoryPositionId(202L);item.setSourceInboundOrderId(50L);item.setCartonMark("BOX-1");item.setNotifiedPackageCount(1);
|
||||
jdbc.update("INSERT INTO ll_inventory_balance(id,customer_id,customer_name,overseas_warehouse_id,overseas_warehouse_code,overseas_warehouse_name,destination_warehouse_code,inbound_order_id,inbound_order_no,inbound_code,sku_code,available_packages,hold_packages,allocated_packages,picked_packages,receiving_packages,in_transit_packages,procurement_packages,version) VALUES (60,'7','Synthetic customer','9','SRC','Source','ABE4',50,'IP-50','IN-50','OLD',2,2,0,0,0,0,0,0)");
|
||||
position(101,701,2,0,"SOURCE");position(202,702,0,1,"MOVED");position(99,799,0,1,"OTHER-HOLD");
|
||||
var allocation=new WarehouseTransferInventoryAllocation();allocation.setId(13L);allocation.setCustomerId("7");allocation.setTransferOrderId(1L);
|
||||
allocation.setInventoryPositionId(101L);allocation.setInventoryBalanceId(60L);allocation.setInboundOrderId(50L);
|
||||
allocation.setPackageCount(1);allocation.setStatus("RESERVED");allocation.setPalletId(701L);allocation.setActivePositionKey("7:101");
|
||||
allocation.setSkuCode("OLD");
|
||||
session.getMapper(WarehouseTransferInventoryAllocationMapper.class).insert(allocation);
|
||||
var binding=new WarehouseTransferRelabelBinding();binding.setId(14L);binding.setCustomerId("7");binding.setTransferOrderId(1L);
|
||||
binding.setSourceAllocationId(13L);binding.setRelabelItemId(12L);binding.setOriginalSku("OLD");binding.setNewSku("NEW");binding.setCustomerCartonMark("BOX-1");
|
||||
session.getMapper(WarehouseTransferRelabelBindingMapper.class).insert(binding);
|
||||
}
|
||||
void position(long id,long pallet,long available,long hold,String batch){jdbc.update("INSERT INTO ll_inventory_position(id,customer_id,balance_id,overseas_warehouse_id,destination_warehouse_code,pallet_id,pallet_no,batch_no,available_packages,hold_packages,allocated_packages,picked_packages,receiving_packages,version) VALUES (?,'7',60,'9','ABE4',?,?,?, ?,?,0,0,0,0)",id,pallet,"P-"+pallet,batch,available,hold);}
|
||||
@Test void confirmsMovedCartonIntoNewSkuAndPreservesOtherHoldWithRealCommandsAndMappers() {
|
||||
tx.executeWithoutResult(s->completion.complete(transfer,application,List.of(item),"operator",LocalDateTime.now()));
|
||||
assertEquals(4L,jdbc.queryForObject("SELECT SUM(available_packages+hold_packages+allocated_packages+picked_packages) FROM ll_inventory_position",Long.class));
|
||||
assertEquals(1L,jdbc.queryForObject("SELECT hold_packages FROM ll_inventory_position WHERE id=99",Long.class));
|
||||
assertEquals(2L,jdbc.queryForObject("SELECT available_packages FROM ll_inventory_position WHERE id=101",Long.class));
|
||||
var rows=session.getMapper(WarehouseTransferInventoryAllocationMapper.class).lockTransfer("7",1L);
|
||||
assertEquals(1,rows.size());assertEquals(702L,rows.get(0).getPalletId());assertEquals(1,rows.get(0).getPackageCount());
|
||||
assertEquals("NEW",rows.get(0).getSkuCode());
|
||||
assertEquals("NEW",jdbc.queryForObject("SELECT sku_code FROM ll_inventory_ledger WHERE business_type='STOCK_IN'",String.class));
|
||||
}
|
||||
@Test void missingExactCartonBindingRollsBackWithoutInventoryOrReservationMutation(){
|
||||
item.setId(999L);
|
||||
assertThrows(RuntimeException.class,()->tx.executeWithoutResult(s->completion.complete(transfer,application,List.of(item),"operator",LocalDateTime.now())));
|
||||
assertEquals(0L,jdbc.queryForObject("SELECT COUNT(*) FROM ll_inventory_ledger",Long.class));
|
||||
assertEquals("RESERVED",jdbc.queryForObject("SELECT status FROM ll_warehouse_transfer_inventory_allocation WHERE id=13",String.class));
|
||||
}
|
||||
@Test void lateAllocationFailureRollsBackRealReleaseSkuOutputAndOriginalReservation(){
|
||||
jdbc.update("INSERT INTO ll_warehouse_transfer_inventory_allocation(id,customer_id,transfer_order_id,inventory_position_id,active_position_key,status) VALUES (900,'7',999,901,'7:901','RESERVED')");
|
||||
// The target ID is allocated dynamically; inject a failure after real stock changes via the source mapper.
|
||||
var mapper=org.mockito.Mockito.spy(session.getMapper(WarehouseTransferInventoryAllocationMapper.class));
|
||||
org.mockito.Mockito.doThrow(new IllegalStateException("FORCED_FINAL_ALLOCATION_FAILURE")).when(mapper).insert(org.mockito.ArgumentMatchers.any(WarehouseTransferInventoryAllocation.class));
|
||||
ReflectionTestUtils.setField(completion,"allocations",mapper);
|
||||
assertThrows(RuntimeException.class,()->tx.executeWithoutResult(s->completion.complete(transfer,application,List.of(item),"operator",LocalDateTime.now())));
|
||||
assertEquals(0L,jdbc.queryForObject("SELECT COUNT(*) FROM ll_inventory_ledger",Long.class));
|
||||
assertEquals(1L,jdbc.queryForObject("SELECT hold_packages FROM ll_inventory_position WHERE id=202",Long.class));
|
||||
assertEquals("OLD",jdbc.queryForObject("SELECT sku_code FROM ll_inventory_balance WHERE id=60",String.class));
|
||||
assertEquals("RESERVED",jdbc.queryForObject("SELECT status FROM ll_warehouse_transfer_inventory_allocation WHERE id=13",String.class));
|
||||
}
|
||||
@Test void shipsActualNewSkuFromMixedBalanceAndOriginalKeysReplayWithoutSecondDecrement(){
|
||||
tx.executeWithoutResult(s->completion.complete(transfer,application,List.of(item),"operator",LocalDateTime.now()));
|
||||
var actual=session.getMapper(WarehouseTransferInventoryAllocationMapper.class).lockTransfer("7",1L).get(0);
|
||||
assertEquals("MIXED",jdbc.queryForObject("SELECT sku_code FROM ll_inventory_balance WHERE id=60",String.class));
|
||||
var allocate=new com.baiyue.lastleg.inventory.command.dto.InventoryCommandRequests.Move(actual.getInventoryPositionId(),1,"WT-ORIGINAL","WT-ORIGINAL","WAREHOUSE_TRANSFER_OUTBOUND:WT-ORIGINAL","WAREHOUSE_TRANSFER:WT-ORIGINAL:ALLOCATE","");
|
||||
var pick=new com.baiyue.lastleg.inventory.command.dto.InventoryCommandRequests.Move(actual.getInventoryPositionId(),1,"WT-ORIGINAL","WT-ORIGINAL","WAREHOUSE_TRANSFER_OUTBOUND:WT-ORIGINAL","WAREHOUSE_TRANSFER:WT-ORIGINAL:PICK","");
|
||||
var ship=new com.baiyue.lastleg.inventory.command.dto.InventoryCommandRequests.Move(actual.getInventoryPositionId(),1,"WT-ORIGINAL","WT-ORIGINAL","WAREHOUSE_TRANSFER_OUTBOUND:WT-ORIGINAL","WAREHOUSE_TRANSFER:WT-ORIGINAL:SHIP","");
|
||||
tx.executeWithoutResult(s->{inventory.allocateTransferSnapshot("7","operator",allocate,actual.getSkuCode());inventory.pickTransferSnapshot("7","operator",pick,actual.getSkuCode());inventory.shipTransferSnapshot("7","operator",ship,actual.getSkuCode());});
|
||||
tx.executeWithoutResult(s->{assertTrue(inventory.allocateTransferSnapshot("7","operator",allocate,"NEW").replayed());assertTrue(inventory.pickTransferSnapshot("7","operator",pick,"NEW").replayed());assertTrue(inventory.shipTransferSnapshot("7","operator",ship,"NEW").replayed());});
|
||||
assertEquals(3L,jdbc.queryForObject("SELECT SUM(available_packages+hold_packages+allocated_packages+picked_packages) FROM ll_inventory_position",Long.class));
|
||||
assertEquals(1L,jdbc.queryForObject("SELECT COUNT(*) FROM ll_inventory_ledger WHERE business_type='SHIP' AND sku_code='NEW'",Long.class));
|
||||
assertEquals(0L,jdbc.queryForObject("SELECT COUNT(*) FROM ll_inventory_ledger WHERE business_type IN ('SHIP','PICK','ALLOCATE') AND sku_code<>'NEW'",Long.class));
|
||||
assertThrows(RuntimeException.class,()->tx.executeWithoutResult(s->inventory.shipTransferSnapshot("7","operator",ship,"OLD")));
|
||||
assertEquals(1L,jdbc.queryForObject("SELECT hold_packages FROM ll_inventory_position WHERE id=99",Long.class));
|
||||
}
|
||||
@Test void cartonOnlyConfirmationKeepsSamePositionAndRetainsOriginalBindingHistory(){
|
||||
jdbc.update("UPDATE ll_warehouse_transfer_relabel_binding SET new_sku=NULL WHERE id=14");
|
||||
jdbc.update("UPDATE ll_inventory_position SET hold_packages=1 WHERE id=101");
|
||||
jdbc.update("UPDATE ll_inventory_position SET hold_packages=0 WHERE id=202");item.setSourceInventoryPositionId(101L);
|
||||
tx.executeWithoutResult(s->completion.complete(transfer,application,List.of(item),"operator",LocalDateTime.now()));
|
||||
var current=session.getMapper(WarehouseTransferInventoryAllocationMapper.class).lockTransfer("7",1L);
|
||||
assertEquals(1,current.size());assertEquals(101L,current.get(0).getInventoryPositionId());assertEquals("OLD",current.get(0).getSkuCode());
|
||||
assertEquals(2L,jdbc.queryForObject("SELECT COUNT(*) FROM ll_warehouse_transfer_inventory_allocation WHERE inventory_position_id=101",Long.class));
|
||||
assertEquals("RELABELLED",jdbc.queryForObject("SELECT status FROM ll_warehouse_transfer_inventory_allocation WHERE id=13",String.class));
|
||||
assertEquals(13L,jdbc.queryForObject("SELECT source_allocation_id FROM ll_warehouse_transfer_relabel_binding WHERE id=14",Long.class));
|
||||
assertEquals(12L,jdbc.queryForObject("SELECT relabel_item_id FROM ll_warehouse_transfer_relabel_binding WHERE id=14",Long.class));
|
||||
assertEquals(1L,jdbc.queryForObject("SELECT hold_packages FROM ll_inventory_position WHERE id=99",Long.class));
|
||||
}
|
||||
@Test void partiallyRelabeledTransferRetainsUnchangedOriginalRemainderAsSeparateCurrentAllocation(){
|
||||
transfer.setPackageCount(2);jdbc.update("UPDATE ll_warehouse_transfer_inventory_allocation SET package_count=2 WHERE id=13");
|
||||
jdbc.update("UPDATE ll_inventory_position SET available_packages=1,hold_packages=1 WHERE id=101");
|
||||
jdbc.update("UPDATE ll_inventory_balance SET available_packages=1,hold_packages=3 WHERE id=60");
|
||||
tx.executeWithoutResult(s->completion.complete(transfer,application,List.of(item),"operator",LocalDateTime.now()));
|
||||
var current=session.getMapper(WarehouseTransferInventoryAllocationMapper.class).lockTransfer("7",1L);
|
||||
assertEquals(2,current.size());assertEquals(2,current.stream().mapToInt(WarehouseTransferInventoryAllocation::getPackageCount).sum());
|
||||
assertEquals(Set.of("OLD","NEW"),new HashSet<>(current.stream().map(WarehouseTransferInventoryAllocation::getSkuCode).toList()));
|
||||
assertEquals(1L,jdbc.queryForObject("SELECT hold_packages FROM ll_inventory_position WHERE id=99",Long.class));
|
||||
assertEquals(4L,jdbc.queryForObject("SELECT SUM(available_packages+hold_packages+allocated_packages+picked_packages) FROM ll_inventory_position",Long.class));
|
||||
}
|
||||
@Test void customerConfirmationReplaysPersistedKeyWithoutSecondInventoryOrAllocationWrites(){
|
||||
transfer.setStatus("WAIT_RELABEL");transfer.setAllocationIdempotencyKey("SELECT-ORIGINAL");transfer.setRelabelNo("RL-OWN");
|
||||
transfer.setFbaWarehouseCode("ABE4");transfer.setDestinationType("FBA");transfer.setDestinationWarehouseCode("ABE4");
|
||||
application.setRelabelNo("RL-OWN");application.setStatus("CUSTOMER_CONFIRM_PENDING");application.setDeliveryMethod("TRUCK");
|
||||
application.setCustomerConfirmedAt(null);item.setOperationType("CARTON_AND_SKU");
|
||||
session.getMapper(WarehouseTransferOrderMapper.class).insert(transfer);
|
||||
var apps=session.getMapper(com.baiyue.lastleg.relabel.mapper.RelabelApplicationMapper.class);apps.insert(application);
|
||||
var items=session.getMapper(com.baiyue.lastleg.relabel.mapper.RelabelItemMapper.class);items.insert(item);
|
||||
var service=spy(new com.baiyue.lastleg.relabel.service.RelabelApplicationService());
|
||||
ReflectionTestUtils.setField(service,"applications",apps);ReflectionTestUtils.setField(service,"items",items);
|
||||
ReflectionTestUtils.setField(service,"warehouseTransfers",session.getMapper(WarehouseTransferOrderMapper.class));
|
||||
ReflectionTestUtils.setField(service,"transferRelabelCompletion",completion);
|
||||
var tasks=mock(com.baiyue.lastleg.relabel.pda.mapper.PdaRelabelTaskMapper.class);
|
||||
var task=new com.baiyue.lastleg.relabel.pda.entity.PdaRelabelTask();task.setStatus("WAIT_REVIEW");task.setVersion(1);
|
||||
when(tasks.lock("7",11L)).thenReturn(task);when(tasks.updateWithVersion(any(),any())).thenReturn(1);
|
||||
ReflectionTestUtils.setField(service,"pdaTasks",tasks);
|
||||
ReflectionTestUtils.setField(service,"returnOrders",mock(com.baiyue.lastleg.returnreceipt.mapper.ReturnOrderMapper.class));
|
||||
ReflectionTestUtils.setField(service,"histories",mock(com.baiyue.lastleg.relabel.mapper.RelabelStatusHistoryMapper.class));
|
||||
ReflectionTestUtils.setField(service,"orderHistories",mock(com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper.class));
|
||||
var orders=mock(com.baiyue.lastleg.mapper.LastlegOrderMapper.class);var order=new com.baiyue.lastleg.entity.LastlegOrder();
|
||||
order.setId(10L);order.setCustomerId("7");order.setCurrentStatus("WAIT_RELABEL");order.setOrderType("WAREHOUSE_TRANSFER");
|
||||
when(orders.selectList(any())).thenReturn(List.of(order));when(orders.selectById(10L)).thenReturn(order);
|
||||
ReflectionTestUtils.setField(service,"orders",orders);doReturn(null).when(service).detail("7","RL-OWN");
|
||||
var command=new com.baiyue.lastleg.relabel.dto.RelabelRequests.CustomerConfirm("CONFIRM-ORIGINAL","confirmed");
|
||||
tx.executeWithoutResult(s->service.confirmCompletion("7","RL-OWN","operator",command));
|
||||
long ledgers=jdbc.queryForObject("SELECT COUNT(*) FROM ll_inventory_ledger",Long.class);
|
||||
long allocations=jdbc.queryForObject("SELECT COUNT(*) FROM ll_warehouse_transfer_inventory_allocation",Long.class);
|
||||
tx.executeWithoutResult(s->service.confirmCompletion("7","RL-OWN","operator",command));
|
||||
assertEquals(ledgers,jdbc.queryForObject("SELECT COUNT(*) FROM ll_inventory_ledger",Long.class));
|
||||
assertEquals(allocations,jdbc.queryForObject("SELECT COUNT(*) FROM ll_warehouse_transfer_inventory_allocation",Long.class));
|
||||
assertEquals("ALLOCATED",jdbc.queryForObject("SELECT status FROM ll_warehouse_transfer_order WHERE id=1",String.class));
|
||||
assertEquals("CONFIRM-ORIGINAL",jdbc.queryForObject("SELECT customer_confirmation_key FROM ll_relabel_application WHERE id=11",String.class));
|
||||
assertEquals(1L,jdbc.queryForObject("SELECT hold_packages FROM ll_inventory_position WHERE id=99",Long.class));
|
||||
}
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
package com.baiyue.lastleg.transfer.service;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class WarehouseTransferRelabelSchemaContractTest {
|
||||
private List<String> baselines() throws Exception {
|
||||
return List.of(new ClassPathResource("db/lastleg.sql").getContentAsString(StandardCharsets.UTF_8),
|
||||
Files.readString(Path.of("..", "deploy", "sql", "海外区域统一增量-MySQL8.sql")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void bothBaselinesPersistExactAllocationSkuAndOriginalRelabelLinks() throws Exception {
|
||||
for (String sql : baselines()) {
|
||||
assertThat(table(sql, "ll_warehouse_transfer_inventory_allocation").toLowerCase())
|
||||
.contains("sku_code varchar(128)");
|
||||
assertThat(table(sql, "ll_warehouse_transfer_relabel_binding").toLowerCase())
|
||||
.contains("source_allocation_id bigint", "relabel_item_id bigint")
|
||||
.contains("uk_ll_transfer_relabel_mark");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void historyCanSharePositionWhileOnlyOneActiveAllocationOwnsIt() throws Exception {
|
||||
for (String sql : baselines()) {
|
||||
String allocation = table(sql, "ll_warehouse_transfer_inventory_allocation")
|
||||
.replaceAll("\\s+", "").toLowerCase();
|
||||
assertThat(allocation)
|
||||
.contains("keyidx_ll_wh_transfer_allocation_position(transfer_order_id,inventory_position_id)")
|
||||
.contains("uniquekeyuk_ll_wh_transfer_active_position(customer_id,active_position_key)")
|
||||
.doesNotContain("uniquekeyuk_ll_wh_transfer_allocation_position");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void controlledUpdateOnlyChangesTwoStructuresWithoutRepairingBusinessFacts() throws Exception {
|
||||
String sql = Files.readString(Path.of("..", "deploy", "sql", "migrations", "overseas",
|
||||
"baiyue_lastleg", "V8__bind_stock_transfer_relabel_sources.sql"))
|
||||
.replaceAll("(?m)^--.*$", "").toLowerCase();
|
||||
assertThat(sql.split(";")).filteredOn(part -> !part.isBlank()).hasSize(2);
|
||||
assertThat(sql)
|
||||
.contains("alter table ll_warehouse_transfer_inventory_allocation",
|
||||
"alter table ll_warehouse_transfer_relabel_binding", "add column sku_code varchar(128) null",
|
||||
"add column source_allocation_id bigint null", "add column relabel_item_id bigint null",
|
||||
"drop index uk_ll_wh_transfer_allocation_position",
|
||||
"add key idx_ll_wh_transfer_allocation_position")
|
||||
.doesNotContain("drop index uk_ll_wh_transfer_active_position", "drop table", "truncate",
|
||||
"insert into", "update ll_", "delete from", "disable keys");
|
||||
}
|
||||
|
||||
private static String table(String sql, String name) {
|
||||
int start = sql.indexOf("CREATE TABLE " + name + " (");
|
||||
if (start < 0) start = sql.indexOf("CREATE TABLE IF NOT EXISTS " + name + " (");
|
||||
assertThat(start).as(name).isGreaterThanOrEqualTo(0);
|
||||
int end = sql.indexOf(") ENGINE", start);
|
||||
assertThat(end).isGreaterThan(start);
|
||||
return sql.substring(start, end);
|
||||
}
|
||||
}
|
||||
+68
@@ -170,6 +170,7 @@ class WarehouseTransferServiceTest {
|
||||
@Spy LastlegOrderNoGenerator orderNoGenerator = new LastlegOrderNoGenerator();
|
||||
@Mock InventoryCommandService inventory;
|
||||
@Mock InventoryCommandMapper inventoryCommands;
|
||||
@Mock com.baiyue.lastleg.relabel.service.RelabelApplicationService relabelService;
|
||||
private WarehouseTransferService service;
|
||||
|
||||
@BeforeEach
|
||||
@@ -201,6 +202,7 @@ class WarehouseTransferServiceTest {
|
||||
ReflectionTestUtils.setField(service, "orderNoGenerator", orderNoGenerator);
|
||||
ReflectionTestUtils.setField(service, "inventory", inventory);
|
||||
ReflectionTestUtils.setField(service, "inventoryCommands", inventoryCommands);
|
||||
ReflectionTestUtils.setField(service, "relabelService", relabelService);
|
||||
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper().findAndRegisterModules());
|
||||
lenient().when(orderNoGenerator.next(anyString(), any(), anyLong()))
|
||||
.thenReturn("WT-WH-20260901-000001");
|
||||
@@ -791,6 +793,72 @@ class WarehouseTransferServiceTest {
|
||||
verifyNoInteractions(inventory);
|
||||
}
|
||||
|
||||
@Test
|
||||
void requiredRelabelCannotShipReservedOriginalStockBeforeCustomerConfirmation() {
|
||||
WarehouseTransferOrder transfer = transfer(WarehouseTransferService.ALLOCATED);
|
||||
transfer.setNeedRelabel(true);
|
||||
when(transfers.lockNo("TENANT-A", "WTR-1")).thenReturn(transfer);
|
||||
assertThrows(ResponseStatusException.class, () -> service.outbound("TENANT-A", "WTR-1", "USER-1",
|
||||
new WarehouseTransferRequests.Outbound("OUT-RELABEL", "must wait")));
|
||||
verifyNoInteractions(inventory);
|
||||
}
|
||||
|
||||
@Test
|
||||
void choosingOriginalStockStartsItsOwnRelabelAndReplaysWithoutSecondApplicationOrHold() {
|
||||
var transfer = transfer("APPROVED"); transfer.setNeedRelabel(true); transfer.setPackageCount(1);
|
||||
var binding = new com.baiyue.lastleg.transfer.entity.WarehouseTransferRelabelBinding();
|
||||
binding.setId(80L); binding.setCustomerCartonMark("BOX-1");binding.setOriginalSku("SKU-TRANSFER");
|
||||
binding.setNewSku("NEW");binding.setOperationType("CARTON_AND_SKU");binding.setQuantity(1);
|
||||
var cargo = new com.baiyue.lastleg.transfer.entity.WarehouseTransferItem();cargo.setId(90L);cargo.setSkuCode("SKU-TRANSFER");cargo.setPackageCount(1);
|
||||
var summary = org.mockito.Mockito.mock(com.baiyue.lastleg.relabel.dto.RelabelResponses.Summary.class);
|
||||
when(summary.id()).thenReturn(42L);when(summary.relabelNo()).thenReturn("RL-OWN");
|
||||
var carton = org.mockito.Mockito.mock(com.baiyue.lastleg.relabel.dto.RelabelResponses.Item.class);
|
||||
when(carton.id()).thenReturn(43L);when(carton.cartonMark()).thenReturn("BOX-1");
|
||||
var created = new com.baiyue.lastleg.relabel.dto.RelabelResponses.Detail(summary,"",List.of(carton),List.of(),List.of(),null);
|
||||
when(relabelService.createForExistingTailOrder(eq("TENANT-A"),eq("USER-1"),eq(31L),any())).thenReturn(created);
|
||||
when(transfers.lockNo("TENANT-A","WTR-1")).thenReturn(transfer);
|
||||
when(transfers.findNo("TENANT-A","WTR-1")).thenReturn(transfer);
|
||||
when(sourceOrders.selectList(any())).thenReturn(List.of(sourceOrder()));
|
||||
when(inventoryCommands.lockPosition("TENANT-A",501L)).thenReturn(position(501,601,701,3));
|
||||
when(inventoryCommands.lockBalance("TENANT-A",601L)).thenReturn(balance(601,101,"IN-1"));
|
||||
when(transferItems.selectList(any())).thenReturn(List.of(cargo));
|
||||
when(relabelBindings.listOrder("TENANT-A",21L)).thenReturn(List.of(binding));
|
||||
when(orders.selectById(31L)).thenReturn(tailOrder());
|
||||
var command=new WarehouseTransferRequests.Allocate("ORIGINAL-SELECT",List.of(new WarehouseTransferRequests.Allocation(501L,701L,1)));
|
||||
var response=service.allocate("TENANT-A","WTR-1","USER-1",command);
|
||||
assertEquals("WAIT_RELABEL",response.status());assertEquals("RL-OWN",transfer.getRelabelNo());
|
||||
assertEquals(43L,binding.getRelabelItemId());assertNotNull(binding.getSourceAllocationId());
|
||||
service.allocate("TENANT-A","WTR-1","USER-1",command);
|
||||
verify(relabelService).createForExistingTailOrder(eq("TENANT-A"),eq("USER-1"),eq(31L),any());
|
||||
verify(inventoryAllocations).insert(any(WarehouseTransferInventoryAllocation.class));
|
||||
verifyNoInteractions(inventory);
|
||||
assertThrows(ResponseStatusException.class,()->service.outbound("TENANT-A","WTR-1","USER-1",new WarehouseTransferRequests.Outbound("EARLY","")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void confirmedOwnRelabelShipsAndReceivesItsNewSkuAndCompletesLinkedFulfillment() {
|
||||
var transfer=transfer("ALLOCATED");transfer.setNeedRelabel(true);transfer.setRelabelNo("RL-OWN");
|
||||
transfer.setRelabelApplicationId(42L);transfer.setRelabelCompletedAt(LocalDateTime.now());
|
||||
var app=new RelabelApplication();app.setId(42L);app.setLastlegOrderId(31L);app.setCustomerConfirmedAt(LocalDateTime.now());
|
||||
var current=allocation();current.setSkuCode("NEW");
|
||||
when(transfers.lockNo("TENANT-A","WTR-1")).thenReturn(transfer);
|
||||
when(transfers.findNo("TENANT-A","WTR-1")).thenReturn(transfer);
|
||||
when(relabelApplications.lockNo("TENANT-A","RL-OWN")).thenReturn(app);
|
||||
when(inventoryAllocations.lockTransfer("TENANT-A",21L)).thenReturn(List.of(current));
|
||||
when(orders.selectById(31L)).thenReturn(tailOrder());when(transferItems.selectList(any())).thenReturn(List.of());
|
||||
when(sourceOrders.selectList(any())).thenReturn(List.of());
|
||||
service.outbound("TENANT-A","WTR-1","USER-1",new WarehouseTransferRequests.Outbound("OUT-NEW","departed"));
|
||||
service.outbound("TENANT-A","WTR-1","USER-1",new WarehouseTransferRequests.Outbound("OUT-NEW","departed"));
|
||||
verify(inventory).shipTransferSnapshot(eq("TENANT-A"),eq("USER-1"),any(),eq("NEW"));
|
||||
verify(inventory,never()).ship(anyString(),anyString(),any());assertEquals("OUTBOUNDED",app.getFulfillmentStatus());
|
||||
var mixed=balance(601,101,"IN-1");mixed.setSkuCode("MIXED");when(inventoryCommands.lockBalance("TENANT-A",601L)).thenReturn(mixed);
|
||||
when(inventory.stockIn(eq("TENANT-A"),eq("USER-1"),any())).thenReturn(new InventoryCommandResponse(1L,"STOCK_IN",801L,901L,5,0,0,0,false,false));
|
||||
service.receive("TENANT-A","WTR-1","USER-1",receiptRequest());
|
||||
var received=ArgumentCaptor.forClass(com.baiyue.lastleg.inventory.command.dto.InventoryCommandRequests.StockIn.class);
|
||||
verify(inventory).stockIn(eq("TENANT-A"),eq("USER-1"),received.capture());assertEquals("NEW",received.getValue().skuCode());
|
||||
assertEquals("RECEIVED",transfer.getStatus());assertEquals("COMPLETED",app.getStatus());assertEquals("COMPLETED",app.getFulfillmentStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void outboundFailureDoesNotAdvanceTransferOrPersistAllocationShipment() {
|
||||
WarehouseTransferOrder transfer = transfer(WarehouseTransferService.ALLOCATED);
|
||||
|
||||
Reference in New Issue
Block a user