merge: integrate TEST updates and verify appointment release
This commit is contained in:
+10
@@ -672,6 +672,16 @@ class DeliveryTransferServiceTest {
|
||||
assertEquals(4, detail.pallets().get(0).packageCount());
|
||||
}
|
||||
|
||||
@org.junit.jupiter.params.ParameterizedTest
|
||||
@org.junit.jupiter.params.provider.ValueSource(strings = {"DISCARD_OUTBOUND", "ONE_CLICK_DROPSHIP"})
|
||||
void discardDoesNotExposeHistoricInboundCodeAsLxButDropshipKeepsRealLx(String type) {
|
||||
OutboundOrder outbound = outbound(301L, null, null, "OUT-301", "outbound-301");
|
||||
outbound.setOutboundType(type); outbound.setLxCodes("LX-REAL-001");
|
||||
when(outboundOrders.findGlobalByNo("OUT-301")).thenReturn(outbound);
|
||||
var detail = service.outboundDetail("OUT-301");
|
||||
assertEquals("DISCARD_OUTBOUND".equals(type) ? null : "LX-REAL-001", detail.outbound().lxCodes());
|
||||
}
|
||||
|
||||
@Test
|
||||
void dispatchOutboundStillRequiresItsDeliveryDocument() {
|
||||
OutboundOrder outbound = outbound(301L, 101L, "DSP-101", "OUT-301", "dispatch-301");
|
||||
|
||||
+41
@@ -3,10 +3,51 @@ package com.baiyue.lastleg.discard;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.SQLException;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonItemMapper;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
class CargoDiscardSchemaContractTest {
|
||||
@Test
|
||||
void returnCartonDeductionAllowsZeroButRejectsNegativeStock() throws Exception {
|
||||
String baseline;
|
||||
try (var input = getClass().getResourceAsStream("/db/lastleg.sql")) {
|
||||
baseline = new String(input.readAllBytes(), StandardCharsets.UTF_8);
|
||||
}
|
||||
var constraint = java.util.regex.Pattern.compile(
|
||||
"CONSTRAINT chk_ll_return_inventory_carton_item_quantity CHECK\\(([^)]+)\\)")
|
||||
.matcher(baseline);
|
||||
assertTrue(constraint.find());
|
||||
String deduction = ReturnInventoryCartonItemMapper.class.getMethod("deduct", long.class, long.class)
|
||||
.getAnnotation(Update.class).value()[0]
|
||||
.replace("#{quantity}", "?").replace("#{id}", "?");
|
||||
try (var connection = DriverManager.getConnection("jdbc:h2:mem:discard_zero_quantity;MODE=MySQL")) {
|
||||
try (var statement = connection.createStatement()) {
|
||||
statement.execute("CREATE TABLE ll_return_inventory_carton_item (id BIGINT PRIMARY KEY, "
|
||||
+ "quantity BIGINT NOT NULL, CONSTRAINT chk_ll_return_inventory_carton_item_quantity CHECK("
|
||||
+ constraint.group(1) + "))");
|
||||
statement.execute("INSERT INTO ll_return_inventory_carton_item VALUES (1, 3)");
|
||||
try (var update = connection.prepareStatement(deduction)) {
|
||||
update.setLong(1, 3); update.setLong(2, 1); update.setLong(3, 3);
|
||||
assertEquals(1, update.executeUpdate());
|
||||
update.setLong(1, 1); update.setLong(3, 1);
|
||||
assertEquals(0, update.executeUpdate());
|
||||
}
|
||||
try (var result = statement.executeQuery("SELECT quantity FROM ll_return_inventory_carton_item WHERE id=1")) {
|
||||
assertTrue(result.next());
|
||||
assertEquals(0, result.getLong(1));
|
||||
}
|
||||
assertThrows(SQLException.class, () -> statement.executeUpdate(
|
||||
"UPDATE ll_return_inventory_carton_item SET quantity=-1 WHERE id=1"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void baselineContainsDiscardApplicationReservationAndHistoryTables() throws Exception {
|
||||
String sql;
|
||||
|
||||
+16
@@ -15,6 +15,22 @@ import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class MerchantCargoDiscardControllerTest {
|
||||
@Test
|
||||
void completionUsesAuthenticatedCustomerAndIndependentPermission() throws Exception {
|
||||
controller.confirmCompletion(user(100L, "MERCHANT"), "CD001");
|
||||
verify(service).confirmCompletion("100", "CD001", "1");
|
||||
assertThrows(ResponseStatusException.class,
|
||||
() -> controller.confirmCompletion(user(0L, "PLATFORM"), "CD001"));
|
||||
var guard = MerchantCargoDiscardController.class.getMethod("confirmCompletion", AuthenticatedUser.class, String.class)
|
||||
.getAnnotation(org.springframework.security.access.prepost.PreAuthorize.class);
|
||||
org.junit.jupiter.api.Assertions.assertEquals(
|
||||
"hasAuthority('merchant:overseas-warehouse:discard:confirm')", guard.value());
|
||||
org.junit.jupiter.api.Assertions.assertFalse(java.util.Arrays.stream(CargoDiscardController.class.getDeclaredMethods())
|
||||
.filter(method -> method.isAnnotationPresent(org.springframework.web.bind.annotation.PostMapping.class))
|
||||
.flatMap(method -> java.util.Arrays.stream(method.getAnnotation(org.springframework.web.bind.annotation.PostMapping.class).value()))
|
||||
.anyMatch(path -> path.endsWith("/complete") || path.endsWith("/confirm-completion")));
|
||||
}
|
||||
|
||||
private final CargoDiscardService service = mock(CargoDiscardService.class);
|
||||
private final MerchantCargoDiscardController controller = controller();
|
||||
private final CargoDiscardRequests.Query query = new CargoDiscardRequests.Query(
|
||||
|
||||
+56
-2
@@ -42,6 +42,7 @@ class PdaCargoDiscardServiceTest {
|
||||
|
||||
@BeforeEach void setup() {
|
||||
service = new PdaCargoDiscardService();
|
||||
ReflectionTestUtils.setField(service, "outboundService", org.mockito.Mockito.mock(com.baiyue.lastleg.discard.service.CargoDiscardOutboundService.class));
|
||||
ReflectionTestUtils.setField(service, "photoService", photoService);
|
||||
ReflectionTestUtils.setField(service, "applications", applications);
|
||||
ReflectionTestUtils.setField(service, "items", items);
|
||||
@@ -122,8 +123,9 @@ class PdaCargoDiscardServiceTest {
|
||||
@Test void loadedForeignCustomerItemCannotRecover() { finished("COMPLETED"); item.setCustomerId("OTHER"); assertThrows(ResponseStatusException.class, () -> service.confirm(identity, "CD-1", "PALLET-1", request(1))); noWrites(); }
|
||||
@Test void loadedDifferentApplicationItemCannotRecover() { finished("COMPLETED"); item.setApplicationId(2L); assertThrows(ResponseStatusException.class, () -> service.confirm(identity, "CD-1", "PALLET-1", request(1))); noWrites(); }
|
||||
|
||||
@ParameterizedTest @ValueSource(ints = {3, 1, 0})
|
||||
void threeLocalOrdersUploadConfirmAndReplayWithFullPartialAndZeroActualDisposal(int actual) {
|
||||
@Test
|
||||
void fullDisposalUploadsConfirmsAndReplaysWithoutDoubleDeduction() {
|
||||
int actual = 3;
|
||||
var realPhotos = new com.baiyue.lastleg.discard.service.CargoDiscardPhotoService();
|
||||
var photoMapper = mock(com.baiyue.lastleg.discard.mapper.CargoDiscardPhotoMapper.class);
|
||||
var photoFiles = mock(com.baiyue.lastleg.discard.gateway.CargoDiscardPhotoFileGateway.class);
|
||||
@@ -151,4 +153,56 @@ class PdaCargoDiscardServiceTest {
|
||||
verify(photoMapper, times(1)).updateById(any(com.baiyue.lastleg.discard.entity.CargoDiscardPhoto.class));
|
||||
assertThrows(ResponseStatusException.class, () -> service.uploadPhoto(identity, "CD-1", "PALLET-1", 9L, new org.springframework.mock.web.MockMultipartFile("file", new byte[]{1})));
|
||||
}
|
||||
@ParameterizedTest @ValueSource(ints = {0, 1, 2})
|
||||
void partialAndZeroDisposalDoNotWriteInventoryOrPhotos(int actual) {
|
||||
item.setDiscardPackages(3);
|
||||
assertThrows(ResponseStatusException.class, () -> service.confirm(identity, "CD-1", "PALLET-1", request(actual)));
|
||||
noWrites(); verifyNoInteractions(photoService);
|
||||
}
|
||||
private void looseTask() {
|
||||
var stockService = mock(com.baiyue.lastleg.discard.service.CargoDiscardStockService.class);
|
||||
ReflectionTestUtils.setField(service, "stockService", stockService);
|
||||
item.setSourceType("INVENTORY"); item.setInventoryPositionId(42L); item.setDiscardPackages(2);
|
||||
var position = new com.baiyue.lastleg.inventory.entity.InventoryPosition();
|
||||
position.setId(42L); position.setCustomerId("CUSTOMER"); position.setOverseasWarehouseId("99");
|
||||
when(stockService.position("CUSTOMER", 42L)).thenReturn(position);
|
||||
when(stockService.cartonMarks(item)).thenReturn(List.of("BOX-1", "BOX-2"));
|
||||
}
|
||||
private PdaCargoDiscardDtos.Confirm looseRequest(List<String> marks, boolean bulk) {
|
||||
return new PdaCargoDiscardDtos.Confirm(List.of(new PdaCargoDiscardDtos.ActualLine(11L, 2)), List.of(101L), marks, bulk);
|
||||
}
|
||||
@Test void looseTaskExposesAllRealMarksAndRejectsMissingDuplicateAndUnknownScans() {
|
||||
looseTask(); when(applications.selectList(any())).thenReturn(List.of(application));
|
||||
var task = service.resolve(identity, "BOX-1", "CD-1");
|
||||
assertEquals("CARTON", task.scanMode()); assertEquals(List.of("BOX-1", "BOX-2"), task.cartonMarks());
|
||||
for (var marks : List.of(List.of("BOX-1"), List.of("BOX-1", "BOX-1"), List.of("BOX-1", "OTHER")))
|
||||
assertThrows(ResponseStatusException.class, () -> service.confirm(identity, "CD-1", "散箱", looseRequest(marks, false)));
|
||||
noWrites(); verifyNoInteractions(photoService);
|
||||
}
|
||||
@ParameterizedTest @ValueSource(booleans = {false, true})
|
||||
void looseTaskCompletesAfterAllScansOrExplicitBulkAndReplays(boolean bulk) {
|
||||
looseTask(); var request = looseRequest(bulk ? List.of() : List.of("BOX-2", "BOX-1"), bulk);
|
||||
assertEquals("COMPLETED", service.confirm(identity, "CD-1", "散箱", request).status());
|
||||
assertEquals(CargoDiscardService.PDA_COMPLETED, application.getStatus());
|
||||
service.confirm(identity, "CD-1", "散箱", request);
|
||||
verify(adjustments, times(1)).discardPalletItem(eq("CUSTOMER"), same(application), same(item), anyList(), eq(2), eq("OP"), any());
|
||||
verify(photoService).confirm(same(application), eq("散箱"), eq(List.of(101L)), eq(false));
|
||||
}
|
||||
|
||||
@Test void partialBoundPalletOnlyExposesSelectedBoxesAndRequiresTheirScan() {
|
||||
looseTask(); item.setCartonScanRequired(true);
|
||||
var stockService = (com.baiyue.lastleg.discard.service.CargoDiscardStockService) ReflectionTestUtils.getField(service, "stockService");
|
||||
var position = new com.baiyue.lastleg.inventory.entity.InventoryPosition(); position.setOverseasWarehouseId("99"); position.setPalletNo("P-42");
|
||||
when(stockService.position("CUSTOMER", 42L)).thenReturn(position);
|
||||
when(applications.selectList(any())).thenReturn(List.of(application));
|
||||
var task = service.resolve(identity, "P-42", "CD-1");
|
||||
assertEquals("CARTON", task.scanMode()); assertEquals("P-42", task.palletNo());
|
||||
assertEquals(List.of("BOX-1", "BOX-2"), task.cartonMarks());
|
||||
assertThrows(ResponseStatusException.class, () -> service.confirm(identity, "CD-1", "P-42", looseRequest(List.of("BOX-1"), false)));
|
||||
verifyNoInteractions(adjustments);
|
||||
service.confirm(identity, "CD-1", "P-42", looseRequest(List.of("BOX-1", "BOX-2"), false));
|
||||
service.confirm(identity, "CD-1", "P-42", looseRequest(List.of("BOX-1", "BOX-2"), false));
|
||||
verify(adjustments, times(1)).discardPalletItem(eq("CUSTOMER"), same(application), same(item), anyList(), eq(2), eq("OP"), any());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
package com.baiyue.lastleg.discard.service;
|
||||
|
||||
import com.baiyue.lastleg.delivery.entity.*;
|
||||
import com.baiyue.lastleg.delivery.mapper.*;
|
||||
import com.baiyue.lastleg.discard.entity.*;
|
||||
import com.baiyue.lastleg.discard.mapper.*;
|
||||
import com.baiyue.lastleg.dropship.entity.DropshipOrderProfile;
|
||||
import com.baiyue.lastleg.dropship.mapper.DropshipOrderProfileMapper;
|
||||
import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCarton;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper;
|
||||
import com.baiyue.lastleg.service.LastlegOrderNoGenerator;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import java.util.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
|
||||
class CargoDiscardOutboundServiceTest {
|
||||
private final OutboundOrderMapper outbounds = mock(OutboundOrderMapper.class);
|
||||
private final OutboundPalletItemMapper pallets = mock(OutboundPalletItemMapper.class);
|
||||
private final CargoDiscardItemMapper items = mock(CargoDiscardItemMapper.class);
|
||||
private final CargoDiscardCartonMapper cartons = mock(CargoDiscardCartonMapper.class);
|
||||
private final DropshipOrderProfileMapper profiles = mock(DropshipOrderProfileMapper.class);
|
||||
private final ReturnInventoryCartonMapper returnCartons = mock(ReturnInventoryCartonMapper.class);
|
||||
private CargoDiscardOutboundService service;
|
||||
private CargoDiscardApplication application;
|
||||
private CargoDiscardItem first, second;
|
||||
private OutboundOrder document;
|
||||
private final List<OutboundPalletItem> snapshots = new ArrayList<>();
|
||||
|
||||
@BeforeEach void setup() {
|
||||
service = new CargoDiscardOutboundService();
|
||||
ReflectionTestUtils.setField(service, "outbounds", outbounds);
|
||||
ReflectionTestUtils.setField(service, "pallets", pallets);
|
||||
ReflectionTestUtils.setField(service, "items", items);
|
||||
ReflectionTestUtils.setField(service, "cartons", cartons);
|
||||
ReflectionTestUtils.setField(service, "profiles", profiles);
|
||||
ReflectionTestUtils.setField(service, "returnCartons", returnCartons);
|
||||
ReflectionTestUtils.setField(service, "orderNoGenerator", new LastlegOrderNoGenerator());
|
||||
application = new CargoDiscardApplication(); application.setId(1L); application.setCustomerId("C1");
|
||||
application.setDiscardNo("CD-TEST"); application.setOverseasWarehouseId("99");
|
||||
application.setTotalPackages(5); application.setStatus("APPROVED");
|
||||
application.setInboundCodes("RT-123,RK-456");
|
||||
first = line(11L, 21L, 3); second = line(12L, 22L, 2);
|
||||
when(items.listApplication("C1", 1L)).thenReturn(List.of(first, second));
|
||||
when(cartons.listApplication("C1", 1L)).thenReturn(List.of());
|
||||
for (long id : List.of(21L, 22L)) {
|
||||
var profile = new DropshipOrderProfile(); profile.setId(id); profile.setCustomerId("C1"); profile.setPalletNo("P-" + id);
|
||||
when(profiles.selectById(id)).thenReturn(profile);
|
||||
}
|
||||
when(outbounds.findByIdempotency("C1", "CARGO_DISCARD:1")).thenAnswer(i -> document);
|
||||
when(outbounds.lockByIdempotency("C1", "CARGO_DISCARD:1")).thenAnswer(i -> document);
|
||||
when(outbounds.insert(any(OutboundOrder.class))).thenAnswer(i -> { document = i.getArgument(0); return 1; });
|
||||
when(pallets.insert(any(OutboundPalletItem.class))).thenAnswer(i -> { snapshots.add(i.getArgument(0)); return 1; });
|
||||
when(pallets.findByOutbound(eq("C1"), anyLong())).thenAnswer(i -> snapshots);
|
||||
}
|
||||
private CargoDiscardItem line(long id, long profile, int quantity) {
|
||||
var line = new CargoDiscardItem(); line.setId(id); line.setApplicationId(1L); line.setCustomerId("C1");
|
||||
line.setProfileId(profile); line.setDiscardPackages(quantity); line.setSkuCode("SKU-" + id); return line;
|
||||
}
|
||||
@Test void approvalCreatesExactlyOneDocumentAndImmutablePlannedLines() {
|
||||
service.create(application, "OP"); service.create(application, "OP");
|
||||
assertEquals("DISCARD_OUTBOUND", document.getOutboundType()); assertEquals("WAIT_PICK", document.getStatus());
|
||||
assertNull(document.getLxCodes());
|
||||
assertEquals(5, document.getPackageCount()); assertEquals(2, document.getPalletCount());
|
||||
assertTrue(service.outboundNo(application).startsWith("OB-"));
|
||||
assertEquals(List.of(3, 2), snapshots.stream().map(OutboundPalletItem::getPackageCount).toList());
|
||||
verify(outbounds, times(1)).insert(any(OutboundOrder.class)); verify(pallets, times(2)).insert(any(OutboundPalletItem.class));
|
||||
}
|
||||
@Test void partialThenFinalCompletionTracksActualsWithoutChangingPlannedQuantities() {
|
||||
service.create(application, "OP"); first.setPdaStatus("COMPLETED"); first.setActualDiscardPackages(2);
|
||||
service.sync(application); assertEquals("PICKING", document.getStatus());
|
||||
assertEquals(2, snapshots.get(0).getPickedPackageCount()); assertEquals(0, snapshots.get(1).getPickedPackageCount());
|
||||
second.setPdaStatus("COMPLETED"); second.setActualDiscardPackages(0);
|
||||
application.setStatus("PDA_COMPLETED"); application.setTotalPackages(2); service.sync(application);
|
||||
assertEquals("OUTBOUNDED", document.getStatus()); assertEquals(2, document.getPackageCount());
|
||||
assertEquals(3, snapshots.get(0).getPackageCount()); assertEquals("PICKED", snapshots.get(1).getStatus());
|
||||
application.setStatus("COMPLETED"); service.sync(application); assertEquals("OUTBOUNDED", document.getStatus());
|
||||
}
|
||||
@Test void cancellationSynchronizesDocumentAndEveryLine() {
|
||||
service.create(application, "OP"); application.setStatus("CANCELLED"); service.sync(application);
|
||||
assertEquals("CANCELLED", document.getStatus()); assertTrue(snapshots.stream().allMatch(s -> "CANCELLED".equals(s.getStatus())));
|
||||
}
|
||||
@Test void syncClearsHistoricallyMislabelledInboundCodesWithoutChangingSourceSnapshots() {
|
||||
first.setInboundCode("RK-456");
|
||||
service.create(application, "OP"); document.setLxCodes("RT-123,RK-456");
|
||||
service.sync(application);
|
||||
assertNull(document.getLxCodes()); assertEquals("RK-456", snapshots.get(0).getInboundCode());
|
||||
assertEquals("RT-123,RK-456", application.getInboundCodes());
|
||||
}
|
||||
@Test void returnSourceKeepsRealCartonMarkWithoutInventingPalletNumber() {
|
||||
first.setReturnCartonId(31L); first.setSourceType("RETURN_CARTON");
|
||||
var carton = new ReturnInventoryCarton(); carton.setId(31L); carton.setCustomerId("C1");
|
||||
carton.setOverseasWarehouseId("99"); carton.setCartonMark("BOX-31");
|
||||
when(returnCartons.selectById(31L)).thenReturn(carton);
|
||||
service.create(application, "OP"); assertNull(snapshots.get(0).getPalletNo()); assertEquals("BOX-31", snapshots.get(0).getCartonMarks());
|
||||
assertEquals(3L, snapshots.get(0).getReturnSkuQuantity());
|
||||
}
|
||||
@Test void invalidSourcePreventsAnyOutboundInsert() {
|
||||
when(profiles.selectById(22L)).thenReturn(null);
|
||||
assertThrows(ResponseStatusException.class, () -> service.create(application, "OP"));
|
||||
verify(outbounds, never()).insert(any(OutboundOrder.class)); verify(pallets, never()).insert(any(OutboundPalletItem.class));
|
||||
}
|
||||
}
|
||||
+76
-3
@@ -47,6 +47,36 @@ import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CargoDiscardServiceTest {
|
||||
@Test
|
||||
void customerConfirmationClosesPdaCompletedApplicationOnceWithoutAnotherDeduction() {
|
||||
CargoDiscardApplication application = new CargoDiscardApplication();
|
||||
application.setId(1001L); application.setCustomerId("100"); application.setDiscardNo("CD001");
|
||||
application.setTotalPackages(1); application.setOverseasWarehouseName("测试仓"); application.setStatus(CargoDiscardService.PDA_COMPLETED);
|
||||
storedApplication.set(application);
|
||||
when(applications.lockNo("100", "CD001")).thenReturn(application);
|
||||
|
||||
service.confirmCompletion("100", "CD001", "CUSTOMER");
|
||||
var confirmedAt = application.getCustomerConfirmedAt();
|
||||
org.junit.jupiter.api.Assertions.assertNotNull(confirmedAt);
|
||||
assertEquals(CargoDiscardService.COMPLETED, application.getStatus());
|
||||
assertEquals("客户确认废弃完成", storedHistories.get(0).getRemark());
|
||||
service.confirmCompletion("100", "CD001", "CUSTOMER");
|
||||
assertEquals(confirmedAt, application.getCustomerConfirmedAt());
|
||||
assertEquals(1, storedHistories.size());
|
||||
verify(applications, times(1)).updateById(application);
|
||||
org.mockito.Mockito.verifyNoInteractions(adjustments, inventory, returnCartonItems);
|
||||
}
|
||||
|
||||
@org.junit.jupiter.params.ParameterizedTest
|
||||
@org.junit.jupiter.params.provider.ValueSource(strings = {"PENDING_REVIEW", "APPROVED", "REJECTED", "CANCELLED"})
|
||||
void customerCannotConfirmBeforePdaCompletion(String status) {
|
||||
CargoDiscardApplication application = new CargoDiscardApplication(); application.setStatus(status);
|
||||
when(applications.lockNo("100", "CD001")).thenReturn(application);
|
||||
org.junit.jupiter.api.Assertions.assertThrows(org.springframework.web.server.ResponseStatusException.class,
|
||||
() -> service.confirmCompletion("100", "CD001", "CUSTOMER"));
|
||||
verify(applications, never()).updateById(any(CargoDiscardApplication.class));
|
||||
}
|
||||
|
||||
@Mock CargoDiscardApplicationMapper applications;
|
||||
@Mock CargoDiscardItemMapper items;
|
||||
@Mock CargoDiscardCartonMapper discardCartons;
|
||||
@@ -57,8 +87,10 @@ class CargoDiscardServiceTest {
|
||||
@Mock LastlegOrderStatusHistoryMapper orderHistories;
|
||||
@Mock ReturnInventoryCartonMapper returnCartons;
|
||||
@Mock ReturnInventoryCartonItemMapper returnCartonItems;
|
||||
@Mock com.baiyue.lastleg.client.DomesticWarehouseDirectoryClient warehouseDirectory;
|
||||
|
||||
private CargoDiscardService service;
|
||||
private final CargoDiscardOutboundService outboundService = org.mockito.Mockito.mock(CargoDiscardOutboundService.class);
|
||||
private final AtomicReference<CargoDiscardApplication> storedApplication = new AtomicReference<>();
|
||||
private final List<CargoDiscardItem> storedItems = new ArrayList<>();
|
||||
private final List<CargoDiscardCarton> storedLinks = new ArrayList<>();
|
||||
@@ -67,6 +99,11 @@ class CargoDiscardServiceTest {
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new CargoDiscardService();
|
||||
ReflectionTestUtils.setField(service, "warehouseDirectory", warehouseDirectory);
|
||||
lenient().when(warehouseDirectory.receivingWarehouses(any())).thenReturn(com.baiyue.common.core.api.ApiResponse.success(List.of(
|
||||
new com.baiyue.lastleg.client.DomesticWarehouseDirectoryClient.ReceivingWarehouse(99L, "LAX", "洛杉矶仓", "US", "ACTIVE"))));
|
||||
ReflectionTestUtils.setField(service, "outboundService", outboundService);
|
||||
ReflectionTestUtils.setField(service, "stockService", org.mockito.Mockito.mock(CargoDiscardStockService.class));
|
||||
ReflectionTestUtils.setField(service, "photoService", org.mockito.Mockito.mock(CargoDiscardPhotoService.class));
|
||||
ReflectionTestUtils.setField(service, "applications", applications);
|
||||
ReflectionTestUtils.setField(service, "items", items);
|
||||
@@ -94,18 +131,20 @@ class CargoDiscardServiceTest {
|
||||
@Test
|
||||
void createReservesExactSkuCartonsWithoutDeductingInventory() {
|
||||
LastlegOrder order = order();
|
||||
order.setOverseasWarehouseId("99"); order.setOverseasWarehouseName(null);
|
||||
DropshipSkuInventory sku = skuInventory();
|
||||
when(orders.selectById(101L)).thenReturn(order);
|
||||
when(inventory.countAvailable("100", 101L, "SKU-1")).thenReturn(3L);
|
||||
when(inventory.countAvailable("100", 101L, "SKU-1")).thenReturn(2L);
|
||||
when(inventory.lockAvailable("100", 101L, "SKU-1", 2)).thenReturn(List.of(carton(1L), carton(2L)));
|
||||
when(inventory.lockSkuInventoryDimension("100", 101L, 201L, "SKU-1")).thenReturn(sku);
|
||||
when(inventory.countAvailableProfile("100", 101L, 201L, "SKU-1")).thenReturn(3L);
|
||||
when(inventory.countAvailableProfile("100", 101L, 201L, "SKU-1")).thenReturn(2L);
|
||||
when(inventory.changeCartonStatus(eq("100"), anyLong(), eq("AVAILABLE"), eq("DISCARD_PENDING"))).thenReturn(1);
|
||||
|
||||
var detail = service.create("100", "7", new CargoDiscardRequests.Create(
|
||||
List.of(new CargoDiscardRequests.Item(101L, null, "SKU-1", 2)), "discard-create-1", "damaged"));
|
||||
|
||||
assertEquals(CargoDiscardService.PENDING_REVIEW, detail.summary().status());
|
||||
assertEquals("洛杉矶仓", storedApplication.get().getOverseasWarehouseName());
|
||||
assertEquals(2, detail.summary().totalPackages());
|
||||
assertEquals(new BigDecimal("10.00"), detail.summary().totalFee());
|
||||
assertEquals(2, storedLinks.size());
|
||||
@@ -113,6 +152,38 @@ class CargoDiscardServiceTest {
|
||||
verify(inventory, never()).deductSkuInventory(any(), anyLong(), anyInt());
|
||||
}
|
||||
|
||||
@Test void historicalDiscardDetailResolvesMissingWarehouseNameByOperatingWarehouseId() {
|
||||
CargoDiscardApplication application = pendingApplication();
|
||||
application.setOverseasWarehouseId("99"); application.setOverseasWarehouseName(null);
|
||||
storedApplication.set(application);
|
||||
assertEquals("洛杉矶仓", service.detail("100", application.getDiscardNo()).summary().overseasWarehouseName());
|
||||
verify(applications, never()).updateById(any(CargoDiscardApplication.class));
|
||||
}
|
||||
|
||||
|
||||
@Test void selectedBoxesCreatePartialApplicationWithoutReservingUnselectedStock() {
|
||||
when(orders.selectById(101L)).thenReturn(order());
|
||||
when(inventory.lockSelected("100", 101L, "SKU-1", List.of(2L))).thenReturn(List.of(carton(2L)));
|
||||
when(inventory.lockSkuInventoryDimension("100", 101L, 201L, "SKU-1")).thenReturn(skuInventory());
|
||||
when(inventory.countAvailableProfile("100", 101L, 201L, "SKU-1")).thenReturn(3L);
|
||||
when(inventory.availableOrderCartons("100", 101L)).thenReturn(List.of(carton(1L), carton(2L), carton(3L)));
|
||||
when(inventory.changeCartonStatus("100", 2L, "AVAILABLE", "DISCARD_PENDING")).thenReturn(1);
|
||||
var detail = service.create("100", "7", new CargoDiscardRequests.Create(
|
||||
List.of(new CargoDiscardRequests.Item(101L, null, "SKU-1", 1, null, List.of(2L))), "partial-create", "damage"));
|
||||
assertEquals(1, detail.summary().totalPackages()); assertEquals(new BigDecimal("5.00"), detail.summary().totalFee());
|
||||
assertEquals(List.of("C-2"), detail.items().get(0).cartonMarks());
|
||||
assertEquals(1, storedLinks.size()); assertEquals(2L, storedLinks.get(0).getCartonId());
|
||||
org.junit.jupiter.api.Assertions.assertTrue(storedItems.get(0).getCartonScanRequired());
|
||||
verify(inventory, never()).changeCartonStatus(eq("100"), eq(1L), any(), any());
|
||||
verify(inventory, never()).changeCartonStatus(eq("100"), eq(3L), any(), any());
|
||||
}
|
||||
@Test void duplicatePhysicalBoxAcrossSkuLinesIsRejectedBeforeCreation() {
|
||||
when(orders.selectById(101L)).thenReturn(order());
|
||||
org.junit.jupiter.api.Assertions.assertThrows(org.springframework.web.server.ResponseStatusException.class, () -> service.create("100", "7", new CargoDiscardRequests.Create(
|
||||
List.of(new CargoDiscardRequests.Item(101L, null, "SKU-A", 1, null, List.of(2L)),
|
||||
new CargoDiscardRequests.Item(101L, null, "SKU-B", 1, null, List.of(2L))), "duplicate-box", null)));
|
||||
verify(applications, never()).insert(any(CargoDiscardApplication.class));
|
||||
}
|
||||
@Test
|
||||
void candidateTotalCountsPhysicalCartonsInsteadOfSkuRows() {
|
||||
CargoDiscardCandidateRow skuA = candidate("SKU-A", 20L);
|
||||
@@ -132,7 +203,7 @@ class CargoDiscardServiceTest {
|
||||
ReturnInventoryCarton carton = new ReturnInventoryCarton();
|
||||
carton.setId(901L);
|
||||
carton.setCustomerId("100");
|
||||
carton.setOverseasWarehouseId("WH-1");
|
||||
carton.setOverseasWarehouseId("99");
|
||||
carton.setSourceReturnNo("RT-20260928-001");
|
||||
carton.setCartonMark("RC-LAX-001");
|
||||
carton.setStatus("AVAILABLE");
|
||||
@@ -149,6 +220,7 @@ class CargoDiscardServiceTest {
|
||||
|
||||
assertEquals(1, result.size());
|
||||
assertEquals("RETURN_CARTON", result.get(0).sourceType());
|
||||
assertEquals("洛杉矶仓", result.get(0).overseasWarehouseName());
|
||||
assertEquals(901L, result.get(0).returnCartonId());
|
||||
assertEquals("RC-LAX-001", result.get(0).cartonMark());
|
||||
assertEquals(4, result.get(0).availablePackages());
|
||||
@@ -167,6 +239,7 @@ class CargoDiscardServiceTest {
|
||||
var detail = service.review("100", "FH-1", "9", new CargoDiscardRequests.Review("APPROVE", "ok"));
|
||||
|
||||
assertEquals(CargoDiscardService.APPROVED, detail.summary().status());
|
||||
verify(outboundService).create(application, "9");
|
||||
verify(adjustments, never()).discard(any(), any(), any(), any(), any(), any());
|
||||
verify(inventory, never()).deductSkuInventory(any(), anyLong(), anyInt());
|
||||
assertEquals("ok", storedHistories.get(storedHistories.size() - 1).getRemark());
|
||||
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
package com.baiyue.lastleg.discard.service;
|
||||
|
||||
import com.baiyue.lastleg.discard.dto.CargoDiscardRequests;
|
||||
import com.baiyue.lastleg.discard.entity.*;
|
||||
import com.baiyue.lastleg.discard.mapper.*;
|
||||
import com.baiyue.lastleg.inventory.command.dto.InventoryCommandRequests;
|
||||
import com.baiyue.lastleg.inventory.command.service.InventoryCommandService;
|
||||
import com.baiyue.lastleg.inventory.entity.*;
|
||||
import com.baiyue.lastleg.inventory.mapper.*;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
|
||||
class CargoDiscardStockServiceTest {
|
||||
final CargoDiscardInventoryMapper stock = mock(CargoDiscardInventoryMapper.class);
|
||||
final InventoryPositionMapper positions = mock(InventoryPositionMapper.class);
|
||||
final InventoryBalanceMapper balances = mock(InventoryBalanceMapper.class);
|
||||
final CargoDiscardCartonMapper links = mock(CargoDiscardCartonMapper.class);
|
||||
final CargoDiscardItemMapper items = mock(CargoDiscardItemMapper.class);
|
||||
final InventoryCommandService commands = mock(InventoryCommandService.class);
|
||||
final CargoDiscardStockService service = new CargoDiscardStockService();
|
||||
final InventoryPosition position = new InventoryPosition();
|
||||
final InventoryBalance balance = new InventoryBalance();
|
||||
final CargoDiscardApplication application = new CargoDiscardApplication();
|
||||
CargoDiscardRequests.Item request(int count) { return new CargoDiscardRequests.Item(null, null, "SKU", count, 42L); }
|
||||
@BeforeEach void setup() {
|
||||
ReflectionTestUtils.setField(service, "stock", stock);
|
||||
ReflectionTestUtils.setField(service, "positions", positions);
|
||||
ReflectionTestUtils.setField(service, "balances", balances);
|
||||
ReflectionTestUtils.setField(service, "commands", commands);
|
||||
ReflectionTestUtils.setField(service, "items", items);
|
||||
ReflectionTestUtils.setField(service, "links", links);
|
||||
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper());
|
||||
position.setId(42L); position.setCustomerId("C1"); position.setBalanceId(51L);
|
||||
position.setOverseasWarehouseId("99"); position.setPalletNo("P-42"); position.setAvailablePackages(100L);
|
||||
balance.setId(51L); balance.setCustomerId("C1"); balance.setOverseasWarehouseId("99");
|
||||
balance.setInboundCode("WT-WH-1"); balance.setInboundOrderNo("WT-WH-1"); balance.setSkuCode("SKU"); balance.setAvailablePackages(100L);
|
||||
application.setId(11L); application.setDiscardNo("CD-1"); application.setCustomerId("C1"); application.setCreatedBy("OP");
|
||||
when(positions.findTenantPosition("C1", 42L)).thenReturn(position);
|
||||
when(stock.lockGenericPosition("C1", 42L)).thenReturn(position);
|
||||
when(balances.selectById(51L)).thenReturn(balance);
|
||||
}
|
||||
@Test void warehouseTransferStockIsEligibleAndKeepsTrueSourcePosition() {
|
||||
when(stock.genericPositions("C1", "WT")).thenReturn(List.of(position));
|
||||
var candidate = service.candidates("C1", "WT").get(0);
|
||||
assertEquals("INVENTORY", candidate.sourceType()); assertNull(candidate.orderId());
|
||||
assertEquals(42L, candidate.inventoryPositionId()); assertEquals("WT-WH-1", candidate.inboundCode());
|
||||
service.reserve("C1", "OP", application, request(100), LocalDateTime.now());
|
||||
var capture = ArgumentCaptor.forClass(CargoDiscardItem.class); verify(items).insert(capture.capture());
|
||||
var line = capture.getValue(); assertEquals(42L, line.getInventoryPositionId()); assertEquals(100, line.getDiscardPackages());
|
||||
var move = ArgumentCaptor.forClass(InventoryCommandRequests.Move.class); verify(commands).hold(eq("C1"), eq("OP"), move.capture());
|
||||
assertEquals(100, move.getValue().packages()); assertEquals("CARGO_DISCARD:11:" + line.getId(), move.getValue().sourceEventId());
|
||||
service.release("C1", application, line); verify(commands).release(eq("C1"), eq("OP"), any());
|
||||
service.discard("C1", "OP", application, line, 100); verify(commands).discardHeld(eq("C1"), eq("OP"), any());
|
||||
}
|
||||
@Test void partialForeignSkuAndDropshipBypassAreRejectedBeforeHolding() {
|
||||
assertThrows(ResponseStatusException.class, () -> service.reserve("C1", "OP", application, request(99), LocalDateTime.now()));
|
||||
assertThrows(ResponseStatusException.class, () -> service.validate("C2", request(100)));
|
||||
assertThrows(ResponseStatusException.class, () -> service.validate("C1", new CargoDiscardRequests.Item(null, null, "OTHER", 100, 42L)));
|
||||
when(stock.isDropshipPosition("C1", 42L)).thenReturn(1L);
|
||||
assertThrows(ResponseStatusException.class, () -> service.validate("C1", request(100)));
|
||||
verifyNoInteractions(commands, items);
|
||||
}
|
||||
@Test void looseStockRequiresCompleteRealMarksAndSnapshotsThem() {
|
||||
position.setPalletNo(null); position.setAvailablePackages(2L); balance.setAvailablePackages(2L);
|
||||
when(stock.looseCartonMarks("C1", 42L)).thenReturn(List.of("BOX-1"));
|
||||
assertThrows(ResponseStatusException.class, () -> service.reserve("C1", "OP", application, request(2), LocalDateTime.now()));
|
||||
verifyNoInteractions(commands, items);
|
||||
when(stock.looseCartonMarks("C1", 42L)).thenReturn(List.of("BOX-1", "BOX-2"));
|
||||
service.reserve("C1", "OP", application, request(2), LocalDateTime.now());
|
||||
var capture = ArgumentCaptor.forClass(CargoDiscardItem.class); verify(items).insert(capture.capture());
|
||||
assertEquals(List.of("BOX-1", "BOX-2"), service.cartonMarks(capture.getValue()));
|
||||
}
|
||||
private com.baiyue.lastleg.discard.model.CargoDiscardStockCartonRow carton(long id, String mark) {
|
||||
var carton = new com.baiyue.lastleg.discard.model.CargoDiscardStockCartonRow();
|
||||
carton.setSourceType("INBOUND"); carton.setId(id); carton.setCartonMark(mark); carton.setSkuCode("SKU"); return carton;
|
||||
}
|
||||
@Test void partialDisposalFreezesExactlySelectedBoxesAndLeavesTheOthersUntouched() {
|
||||
position.setAvailablePackages(3L); balance.setAvailablePackages(3L);
|
||||
when(stock.genericCartons("C1", 42L)).thenReturn(List.of(carton(1L, "BOX-1"), carton(2L, "BOX-2"), carton(3L, "BOX-3")));
|
||||
when(stock.changeGenericCartonStatus("C1", 2L, "AVAILABLE", "DISCARD_PENDING")).thenReturn(1);
|
||||
var request = new CargoDiscardRequests.Item(null, null, "SKU", 1, 42L, List.of(2L));
|
||||
service.reserve("C1", "OP", application, request, LocalDateTime.now());
|
||||
var capture = ArgumentCaptor.forClass(CargoDiscardItem.class); verify(items).insert(capture.capture());
|
||||
var line = capture.getValue(); assertEquals(1, line.getDiscardPackages()); assertTrue(line.getCartonScanRequired());
|
||||
assertEquals(List.of("BOX-2"), service.cartonMarks(line));
|
||||
var boxLink = ArgumentCaptor.forClass(CargoDiscardCarton.class); verify(links).insert(boxLink.capture());
|
||||
assertEquals(2L, boxLink.getValue().getCartonId());
|
||||
verify(stock, never()).changeGenericCartonStatus(eq("C1"), eq(1L), anyString(), anyString());
|
||||
verify(stock, never()).changeGenericCartonStatus(eq("C1"), eq(3L), anyString(), anyString());
|
||||
when(links.listApplication("C1", 11L)).thenReturn(List.of(boxLink.getValue()));
|
||||
when(stock.changeGenericCartonStatus("C1", 2L, "DISCARD_PENDING", "AVAILABLE")).thenReturn(1);
|
||||
service.release("C1", application, line);
|
||||
verify(stock).changeGenericCartonStatus("C1", 2L, "DISCARD_PENDING", "AVAILABLE");
|
||||
}
|
||||
@Test void partialBoxSelectionRejectsForeignRepeatedAndMismatchedQuantity() {
|
||||
position.setAvailablePackages(3L); balance.setAvailablePackages(3L);
|
||||
when(stock.genericCartons("C1", 42L)).thenReturn(List.of(carton(1L, "BOX-1"), carton(2L, "BOX-2"), carton(3L, "BOX-3")));
|
||||
for (var request : List.of(new CargoDiscardRequests.Item(null, null, "SKU", 1, 42L, List.of(9L)),
|
||||
new CargoDiscardRequests.Item(null, null, "SKU", 2, 42L, List.of(1L, 1L)),
|
||||
new CargoDiscardRequests.Item(null, null, "SKU", 2, 42L, List.of(1L)))) {
|
||||
assertThrows(ResponseStatusException.class, () -> service.validate("C1", request));
|
||||
}
|
||||
verifyNoInteractions(commands, items, links);
|
||||
}
|
||||
|
||||
@Test void singlePositionTransferBoxesReleaseBackToTheirOriginalExpectedStatus() {
|
||||
position.setAvailablePackages(2L); balance.setAvailablePackages(2L);
|
||||
var first = carton(1L, "WT-1#2-1"); first.setSourceType("TRANSFER");
|
||||
var second = carton(2L, "WT-1#2-2"); second.setSourceType("TRANSFER");
|
||||
when(stock.genericCartons("C1", 42L)).thenReturn(List.of(first, second));
|
||||
when(stock.changeTransferCartonStatus("C1", 2L, "EXPECTED", "DISCARD_PENDING")).thenReturn(1);
|
||||
service.reserve("C1", "OP", application, new CargoDiscardRequests.Item(null, null, "SKU", 1, 42L, List.of(2L)), LocalDateTime.now());
|
||||
var capture = ArgumentCaptor.forClass(CargoDiscardCarton.class); verify(links).insert(capture.capture());
|
||||
assertEquals("TRANSFER", capture.getValue().getSourceType());
|
||||
when(links.listApplication("C1", 11L)).thenReturn(List.of(capture.getValue()));
|
||||
when(stock.changeTransferCartonStatus("C1", 2L, "DISCARD_PENDING", "EXPECTED")).thenReturn(1);
|
||||
var line = ArgumentCaptor.forClass(CargoDiscardItem.class); verify(items).insert(line.capture());
|
||||
service.release("C1", application, line.getValue());
|
||||
verify(stock).changeTransferCartonStatus("C1", 2L, "DISCARD_PENDING", "EXPECTED");
|
||||
verify(stock, never()).changeGenericCartonStatus(anyString(), anyLong(), anyString(), anyString());
|
||||
}
|
||||
|
||||
}
|
||||
+29
@@ -594,6 +594,35 @@ class InventoryCommandServiceTest {
|
||||
"return-1", List.of("FILE-1"));
|
||||
}
|
||||
|
||||
|
||||
@Test void discardOnlyConsumesOwnedHoldAndKeepsAvailableStockConserved() {
|
||||
var balance = balance(200L, 7); balance.setHoldPackages(3L);
|
||||
var position = position(201L, 200L, 7); position.setHoldPackages(3L);
|
||||
when(commands.lockPosition("100", 201L)).thenReturn(position);
|
||||
when(commands.lockBalance("100", 200L)).thenReturn(balance);
|
||||
var hold = new InventoryLedger(); hold.setPositionId(201L); hold.setBusinessOrderNo("CD-1"); hold.setDeltaPackages(3L);
|
||||
when(ledgers.orderHoldCycle("100", 101L, "CARGO_DISCARD:1:2")).thenReturn(List.of(hold));
|
||||
when(commands.palletPackages(300L)).thenReturn(7L);
|
||||
service.discardHeld("100", "OP", new InventoryCommandRequests.Move(201L, 3, "CD-1", "OB-1", "CARGO_DISCARD:1:2", "DISCARD-1", null));
|
||||
assertEquals(7L, position.getAvailablePackages()); assertEquals(7L, balance.getAvailablePackages());
|
||||
assertEquals(0L, position.getHoldPackages()); assertEquals(0L, balance.getHoldPackages());
|
||||
var capture = ArgumentCaptor.forClass(InventoryLedger.class); verify(ledgers).insert(capture.capture());
|
||||
assertEquals("DISCARD", capture.getValue().getBusinessType()); assertEquals(-3L, capture.getValue().getDeltaPackages());
|
||||
assertEquals("OB-1", capture.getValue().getOutboundOrderNo());
|
||||
verify(warehouseGateway, never()).scrapPallet(any(), org.mockito.ArgumentMatchers.anyLong());
|
||||
}
|
||||
@Test void foreignDiscardHoldCannotConsumeAnotherApplicationsStock() {
|
||||
var balance = balance(200L, 0); balance.setHoldPackages(3L);
|
||||
var position = position(201L, 200L, 0); position.setHoldPackages(3L);
|
||||
when(commands.lockPosition("100", 201L)).thenReturn(position);
|
||||
when(commands.lockBalance("100", 200L)).thenReturn(balance);
|
||||
var hold = new InventoryLedger(); hold.setPositionId(201L); hold.setBusinessOrderNo("OTHER-CD"); hold.setDeltaPackages(3L);
|
||||
when(ledgers.orderHoldCycle("100", 101L, "CARGO_DISCARD:1:2")).thenReturn(List.of(hold));
|
||||
assertThrows(ResponseStatusException.class, () -> service.discardHeld("100", "OP", new InventoryCommandRequests.Move(201L, 3, "CD-1", null, "CARGO_DISCARD:1:2", "DISCARD-1", null)));
|
||||
assertEquals(3L, position.getHoldPackages());
|
||||
verify(positions, never()).updateById(any(InventoryPosition.class)); verify(ledgers, never()).insert(any(InventoryLedger.class));
|
||||
}
|
||||
|
||||
private InventoryBalance balance(Long id, long available) {
|
||||
InventoryBalance value = new InventoryBalance();
|
||||
value.setId(id); value.setCustomerId("100"); value.setCustomerName("Customer 100");
|
||||
|
||||
+34
@@ -53,6 +53,7 @@ class PdaRelabelServiceTest {
|
||||
@Mock RelabelApplicationService applicationService;
|
||||
@Mock RelabelPhotoFileGateway photoFiles;
|
||||
@Mock com.baiyue.lastleg.relabel.service.ReturnSkuConversionService skuConversions;
|
||||
@Mock com.baiyue.lastleg.relabel.service.RelabelPalletMeasurementService palletMeasurements;
|
||||
PdaRelabelService service;
|
||||
RelabelApplication application;
|
||||
RelabelItem item;
|
||||
@@ -62,6 +63,7 @@ class PdaRelabelServiceTest {
|
||||
void setUp() {
|
||||
service = new PdaRelabelService();
|
||||
ReflectionTestUtils.setField(service, "skuConversions", skuConversions);
|
||||
ReflectionTestUtils.setField(service, "palletMeasurements", palletMeasurements);
|
||||
ReflectionTestUtils.setField(service, "applications", applications);
|
||||
ReflectionTestUtils.setField(service, "bindingGuard", org.mockito.Mockito.mock(com.baiyue.lastleg.relabel.service.RelabelBindingGuard.class));
|
||||
ReflectionTestUtils.setField(service, "items", items);
|
||||
@@ -452,12 +454,44 @@ class PdaRelabelServiceTest {
|
||||
new PdaRelabelRequests.Complete(List.of(501L), 1, "complete-1", "done"));
|
||||
|
||||
verify(photoFiles).validate("100", 20L, 501L);
|
||||
verify(palletMeasurements).requireMeasured(application);
|
||||
verify(skuConversions).apply(eq(application), org.mockito.ArgumentMatchers.anyList(), org.mockito.ArgumentMatchers.anyList(), anyString(), org.mockito.ArgumentMatchers.anyList());
|
||||
assertEquals(PdaRelabelService.WAIT_REVIEW, result.taskStatus());
|
||||
assertEquals("CUSTOMER_CONFIRM_PENDING", result.applicationStatus());
|
||||
verify(applicationService).changeStatus(eq("100"), eq("FBR-1"), eq("7"), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void missingOriginalRemainderMeasurementBlocksNormalAndManualCompletionBeforeWrites() {
|
||||
application.setStatus("PROCESSING");
|
||||
PdaRelabelTask task = task("PROCESSING", 1);
|
||||
task.setRelabelledPackages(1);
|
||||
persistedFacts.add(fact("RELABELLED"));
|
||||
when(applications.lockAnyByRelabelNo("FBR-1")).thenReturn(application);
|
||||
when(tasks.lock("100", 1L)).thenReturn(task);
|
||||
var measurements = org.mockito.Mockito.spy(new com.baiyue.lastleg.relabel.service.RelabelPalletMeasurementService());
|
||||
var measuredNew = new com.baiyue.lastleg.relabel.service.RelabelPalletMeasurementService.Measurement(
|
||||
200L, "NEW", 1, "2:1:1", java.math.BigDecimal.TEN, java.math.BigDecimal.TEN,
|
||||
java.math.BigDecimal.TEN, java.math.BigDecimal.ONE, new java.math.BigDecimal("0.001"), false);
|
||||
var unmeasuredRemainder = new com.baiyue.lastleg.relabel.service.RelabelPalletMeasurementService.Measurement(
|
||||
100L, "OLD", 2, "1:3:2", null, null, null, null, null, false);
|
||||
org.mockito.Mockito.doReturn(List.of(measuredNew, unmeasuredRemainder)).when(measurements).snapshots(application);
|
||||
ReflectionTestUtils.setField(service, "palletMeasurements", measurements);
|
||||
|
||||
for (boolean manual : List.of(false, true)) {
|
||||
var error = assertThrows(ResponseStatusException.class, () -> service.complete("7", "picker", 900L, "FBR-1",
|
||||
new PdaRelabelRequests.Complete(List.of(501L), 1, "complete-" + manual, "done", manual)));
|
||||
assertTrue(error.getReason().contains("托盘 OLD"));
|
||||
assertEquals("PROCESSING", task.getStatus());
|
||||
assertEquals("PROCESSING", application.getStatus());
|
||||
}
|
||||
verify(tasks, never()).updateWithVersion(any(), anyInt());
|
||||
verify(unitFacts, never()).insert(any(PdaRelabelUnitFact.class));
|
||||
org.mockito.Mockito.verifyNoInteractions(applicationService, photoFiles, skuConversions);
|
||||
verify(histories, never()).insert(any(com.baiyue.lastleg.relabel.pda.entity.PdaRelabelHistory.class));
|
||||
verify(operations, never()).insert(any(PdaRelabelOperation.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void approvalUsesExistingApplicationCompletionAndReworkDoesNot() {
|
||||
assertThrows(ResponseStatusException.class, () -> service.review("8", "reviewer", 900L, "FBR-1",
|
||||
|
||||
+41
@@ -373,6 +373,47 @@ class ReturnInventoryServiceTest {
|
||||
verify(balances, never()).adjustCartonQuantity(anyString(), anyString(), anyString(), anyLong());
|
||||
}
|
||||
|
||||
@org.junit.jupiter.params.ParameterizedTest
|
||||
@org.junit.jupiter.params.provider.ValueSource(longs = {3L, 6L})
|
||||
void mergeCoalescesTrackedAndOriginalRemainderOwnedByTheSameReturn(long quantity) {
|
||||
var source = prepareConcurrentSource();
|
||||
source.setTotalQuantity(6L);
|
||||
when(cartonItems.lockCarton(31L)).thenReturn(List.of(item(41L, "SKU-A", 6L)));
|
||||
when(packingTrace.ownership("10001", "RC-SOURCE", "SKU-A")).thenReturn(List.of(
|
||||
new com.baiyue.lastleg.returnreceipt.mapper.ReturnPackingTraceMapper.Ownership("RT-1", 3L)));
|
||||
when(numbers.next(anyString(), anyString(), anyLong())).thenReturn("RC-TARGET");
|
||||
when(balances.adjustCartonQuantity(anyString(), anyString(), anyString(), anyLong())).thenReturn(1);
|
||||
|
||||
service.merge(new PdaWarehouseIdentity("operator-2", "操作员2", 42L), mergeRequest(quantity));
|
||||
|
||||
verify(packingTrace).insert(anyLong(), eq("10001"), anyLong(), eq("RC-SOURCE"),
|
||||
eq("RC-TARGET"), eq("RT-1"), eq("SKU-A"), eq(quantity), any());
|
||||
verify(packingTrace, org.mockito.Mockito.times(1)).insert(anyLong(), anyString(), anyLong(),
|
||||
anyString(), anyString(), anyString(), anyString(), anyLong(), any());
|
||||
assertThat(source.getTotalQuantity()).isEqualTo(6L - quantity);
|
||||
}
|
||||
|
||||
@Test
|
||||
void partialMergeStillRejectsActuallyDifferentReturnOwners() {
|
||||
var source = prepareConcurrentSource();
|
||||
source.setSourceReturnNo("RT-1,RT-2"); source.setTotalQuantity(6L);
|
||||
var otherOrder = completedOrder(); otherOrder.setId(12L); otherOrder.setReturnNo("RT-2");
|
||||
org.mockito.Mockito.reset(orders);
|
||||
when(orders.findByNos("42", List.of("RT-1", "RT-2"))).thenReturn(List.of(completedOrder(), otherOrder));
|
||||
when(cartonItems.lockCarton(31L)).thenReturn(List.of(item(41L, "SKU-A", 6L)));
|
||||
when(packingTrace.ownership("10001", "RC-SOURCE", "SKU-A")).thenReturn(List.of(
|
||||
new com.baiyue.lastleg.returnreceipt.mapper.ReturnPackingTraceMapper.Ownership("RT-1", 3L),
|
||||
new com.baiyue.lastleg.returnreceipt.mapper.ReturnPackingTraceMapper.Ownership("RT-2", 3L)));
|
||||
var error = assertThrows(ResponseStatusException.class, () -> service.merge(
|
||||
new PdaWarehouseIdentity("operator-2", "操作员2", 42L),
|
||||
new ReturnInventoryRequests.Merge(List.of("RT-1", "RT-2"),
|
||||
List.of(new ReturnInventoryRequests.ValidItem("SKU-A", 3L)),
|
||||
BigDecimal.ONE, BigDecimal.ONE, BigDecimal.ONE, BigDecimal.ONE, "mixed-owner-key")));
|
||||
assertThat(error.getReason()).contains("不能自动猜测来源归属");
|
||||
verify(packingTrace, never()).insert(anyLong(), anyString(), anyLong(), anyString(),
|
||||
anyString(), anyString(), anyString(), anyLong(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void partiallyConsumedSourceUsesCurrentLockedSkuQuantityNotEarlierSnapshot() {
|
||||
ReturnInventoryCarton locked = prepareConcurrentSource();
|
||||
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
package com.baiyue.lastleg.returnreceipt.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnPackingTraceMapper;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mybatis.spring.SqlSessionTemplate;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.datasource.DriverManagerDataSource;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.tuple;
|
||||
|
||||
public class ReturnPackingTracePersistenceTest {
|
||||
private JdbcTemplate jdbc;
|
||||
private ReturnPackingTraceMapper mapper;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
var ds = new DriverManagerDataSource("jdbc:h2:mem:packing-trace-" + UUID.randomUUID()
|
||||
+ ";MODE=MySQL;DB_CLOSE_DELAY=-1", "sa", "");
|
||||
jdbc = new JdbcTemplate(ds);
|
||||
jdbc.execute("CREATE ALIAS JSON_EXTRACT FOR '" + getClass().getName() + ".jsonExtract'");
|
||||
jdbc.execute("CREATE ALIAS JSON_UNQUOTE FOR '" + getClass().getName() + ".jsonUnquote'");
|
||||
jdbc.execute("CREATE TABLE ll_return_inventory_operation (id BIGINT PRIMARY KEY, operation_type VARCHAR(40), result_json VARCHAR(1000))");
|
||||
jdbc.execute("CREATE TABLE ll_return_packing_trace (id BIGINT PRIMARY KEY, customer_id VARCHAR(80), operation_id BIGINT, "
|
||||
+ "source_carton_mark VARCHAR(80), target_carton_mark VARCHAR(80), return_no VARCHAR(80), sku_code VARCHAR(80), quantity BIGINT, created_at TIMESTAMP)");
|
||||
var config = new MybatisConfiguration();
|
||||
config.setMapUnderscoreToCamelCase(true);
|
||||
config.addMapper(ReturnPackingTraceMapper.class);
|
||||
var factory = new MybatisSqlSessionFactoryBean();
|
||||
factory.setDataSource(ds); factory.setConfiguration(config);
|
||||
mapper = new SqlSessionTemplate(factory.getObject()).getMapper(ReturnPackingTraceMapper.class);
|
||||
operation(1, "PLAN");
|
||||
trace(1, 1, "C", "SOURCE", "TARGET", "RETURN", "SKU", 3);
|
||||
trace(2, 1, "C", "SOURCE", "TARGET", "RETURN", "SKU", 3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void allDetailQueriesSumHistoricalFragmentsWithoutChangingRawTrace() {
|
||||
trace(3, 1, "C", "SOURCE-B", "TARGET", "RETURN-B", "SKU-B", 15);
|
||||
assertThat(mapper.planHistory("C", "PLAN"))
|
||||
.extracting(row -> row.skuCode(), row -> row.quantity())
|
||||
.containsExactly(tuple("SKU", 6L), tuple("SKU-B", 15L));
|
||||
assertThat(mapper.returnHistory("C", "RETURN")).singleElement()
|
||||
.satisfies(row -> assertThat(row.quantity()).isEqualTo(6));
|
||||
assertThat(mapper.listCartonHistory("C", "TARGET"))
|
||||
.extracting(row -> row.skuCode(), row -> row.quantity())
|
||||
.containsExactly(tuple("SKU-B", 15L), tuple("SKU", 6L));
|
||||
assertThat(mapper.listReturn("C", "RETURN")).hasSize(2);
|
||||
assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM ll_return_packing_trace", Long.class)).isEqualTo(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void groupingKeepsOperationReturnSkuAndCartonBoundariesAndCustomerIsolation() {
|
||||
operation(2, "PLAN"); operation(3, "OTHER-PLAN");
|
||||
trace(3, 2, "C", "SOURCE", "TARGET", "RETURN", "SKU", 2);
|
||||
trace(4, 3, "C", "SOURCE", "TARGET", "RETURN", "SKU", 1);
|
||||
trace(5, 1, "C", "SOURCE", "TARGET", "RETURN-B", "SKU", 4);
|
||||
trace(6, 1, "C", "SOURCE-B", "TARGET", "RETURN", "SKU", 5);
|
||||
trace(7, 1, "C", "SOURCE", "TARGET-B", "RETURN", "SKU", 7);
|
||||
trace(8, 1, "C", "SOURCE", "TARGET", "RETURN", "SKU-B", 8);
|
||||
trace(9, 1, "OTHER-CUSTOMER", "SOURCE", "TARGET", "RETURN", "SKU", 99);
|
||||
trace(10, 1, "C", "", "TARGET", "RETURN", "SKU", 100);
|
||||
|
||||
assertThat(mapper.planHistory("C", "PLAN"))
|
||||
.extracting(row -> row.sourceCartonMark(), row -> row.targetCartonMark(), row -> row.returnNo(), row -> row.skuCode(), row -> row.quantity())
|
||||
.containsExactly(tuple("SOURCE", "TARGET", "RETURN", "SKU", 6L),
|
||||
tuple("SOURCE", "TARGET", "RETURN", "SKU", 2L),
|
||||
tuple("SOURCE", "TARGET", "RETURN-B", "SKU", 4L),
|
||||
tuple("SOURCE-B", "TARGET", "RETURN", "SKU", 5L),
|
||||
tuple("SOURCE", "TARGET-B", "RETURN", "SKU", 7L),
|
||||
tuple("SOURCE", "TARGET", "RETURN", "SKU-B", 8L));
|
||||
assertThat(mapper.returnHistory("C", "RETURN")).extracting(row -> row.quantity())
|
||||
.containsExactly(6L, 2L, 1L, 5L, 7L, 8L);
|
||||
assertThat(mapper.listCartonHistory("C", "TARGET")).extracting(row -> row.quantity())
|
||||
.containsExactly(8L, 5L, 4L, 1L, 2L, 6L);
|
||||
assertThat(mapper.planHistory("MISSING", "PLAN")).isEmpty();
|
||||
assertThat(mapper.returnHistory("MISSING", "RETURN")).isEmpty();
|
||||
assertThat(mapper.listCartonHistory("MISSING", "TARGET")).isEmpty();
|
||||
}
|
||||
|
||||
private void operation(long id, String plan) {
|
||||
jdbc.update("INSERT INTO ll_return_inventory_operation VALUES(?, 'MERGE', ?)", id, "{\"planNo\":\"" + plan + "\"}");
|
||||
}
|
||||
|
||||
private void trace(long id, long operation, String customer, String source, String target, String owner, String sku, long quantity) {
|
||||
jdbc.update("INSERT INTO ll_return_packing_trace VALUES(?,?,?,?,?,?,?,?,TIMESTAMP '2026-10-15 16:13:36.447')",
|
||||
id, customer, operation, source, target, owner, sku, quantity);
|
||||
}
|
||||
|
||||
public static String jsonExtract(String json, String path) throws Exception {
|
||||
return new ObjectMapper().readTree(json).path(path.substring(2)).asText(null);
|
||||
}
|
||||
|
||||
public static String jsonUnquote(String value) { return value; }
|
||||
}
|
||||
+10
@@ -18,6 +18,16 @@ class WarehouseTransferCargoValidationTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void skuUnitsAndExpectedCartonsAreIndependent() {
|
||||
try (var factory = Validation.buildDefaultValidatorFactory()) {
|
||||
var validator = factory.getValidator();
|
||||
assertTrue(validator.validate(new WarehouseTransferRequests.Item("SKU-A", "货物", 3, null, 120L)).isEmpty());
|
||||
assertTrue(!validator.validate(new WarehouseTransferRequests.Item("SKU-A", "货物", 3, null, 0L)).isEmpty());
|
||||
assertTrue(!validator.validate(new WarehouseTransferRequests.Item("SKU-A", "货物", 0, null, 120L)).isEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void cartonRelabelAllowsOriginalSkuToBeOmitted() {
|
||||
try (var factory = Validation.buildDefaultValidatorFactory()) {
|
||||
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
package com.baiyue.lastleg.transfer.service;
|
||||
|
||||
import com.baiyue.lastleg.transfer.dto.WarehouseTransferRequests;
|
||||
import com.baiyue.lastleg.transfer.entity.*;
|
||||
import com.baiyue.lastleg.transfer.mapper.*;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
|
||||
class WarehouseTransferCartonCountTest {
|
||||
WarehouseTransferService service;
|
||||
WarehouseTransferOrderMapper orders = mock(WarehouseTransferOrderMapper.class);
|
||||
WarehouseTransferCartonMapper cartons = mock(WarehouseTransferCartonMapper.class);
|
||||
WarehouseTransferItemMapper items = mock(WarehouseTransferItemMapper.class);
|
||||
WarehouseTransferPalletMapper palletMapper = mock(WarehouseTransferPalletMapper.class);
|
||||
WarehouseTransferPallet pallet;
|
||||
WarehouseTransferOrder order;
|
||||
WarehouseTransferCarton a, b;
|
||||
@BeforeEach void setup() {
|
||||
service = spy(new WarehouseTransferService());
|
||||
ReflectionTestUtils.setField(service, "transfers", orders);
|
||||
ReflectionTestUtils.setField(service, "transferCartons", cartons);
|
||||
ReflectionTestUtils.setField(service, "transferItems", items);
|
||||
ReflectionTestUtils.setField(service, "transferPallets", palletMapper);
|
||||
pallet = new WarehouseTransferPallet(); pallet.setId(200L); pallet.setStatus("GENERATED"); pallet.setWarehousePalletId(900L);
|
||||
when(palletMapper.listOrder("C1", 1L)).thenReturn(List.of(pallet));
|
||||
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper());
|
||||
doReturn(null).when(service).detail(anyString(), anyString());
|
||||
order = new WarehouseTransferOrder(); order.setId(1L); order.setCustomerId("C1"); order.setTransferNo("WT1"); order.setStatus("WAIT_PDA_RECEIVING"); order.setPackageCount(2);
|
||||
a = box("BOX1", 10L); b = box("BOX2", 11L);
|
||||
when(orders.lockForPda("WT1", "77")).thenReturn(order);
|
||||
when(cartons.listOrder("C1", 1L)).thenReturn(List.of(a,b));
|
||||
var ia = new WarehouseTransferItem(); ia.setId(10L); ia.setSkuCode("A");
|
||||
var ib = new WarehouseTransferItem(); ib.setId(11L); ib.setSkuCode("B");
|
||||
when(items.selectList(any())).thenReturn(List.of(ia,ib));
|
||||
}
|
||||
WarehouseTransferCarton box(String mark, long item) { var box = new WarehouseTransferCarton(); box.setCartonMark(mark); box.setCartonIndex((int)item); box.setTransferItemId(item); return box; }
|
||||
WarehouseTransferRequests.CountCarton input(String mark, String sku, long quantity) { return new WarehouseTransferRequests.CountCarton(mark, null, 200L, new BigDecimal("20"),new BigDecimal("30"),new BigDecimal("40"),new BigDecimal("2.5"),sku,quantity); }
|
||||
WarehouseTransferRequests.CountCartons request(String key, WarehouseTransferRequests.CountCarton... rows) { return new WarehouseTransferRequests.CountCartons(key,List.of(rows),null); }
|
||||
@Test void multiSkuBoxesPersistDimensionsPiecesAndReplayWithoutWriting() {
|
||||
var request = request("K1",input("BOX1","A",12),input("BOX2","B",7));
|
||||
service.countCartons("OP1",77L,"WT1",request);
|
||||
assertEquals(12L,a.getCountedQuantity()); assertEquals(7L,b.getCountedQuantity()); assertNotNull(a.getCountedAt()); assertEquals("OP1",a.getCountedBy()); assertEquals(new BigDecimal("2.5"),b.getGrossWeightKg());
|
||||
assertEquals(200L,a.getTransferPalletId()); assertEquals(2,pallet.getCartonCount());
|
||||
assertEquals(new BigDecimal("0.048000"),pallet.getVolumeCbm()); assertEquals(new BigDecimal("5.0"),pallet.getGrossWeightKg());
|
||||
clearInvocations(cartons); order.setStatus("WAIT_CUSTOMER_CONFIRMATION");
|
||||
service.countCartons("OP1",77L,"WT1",request);
|
||||
verify(cartons,never()).updateById(any(WarehouseTransferCarton.class));
|
||||
assertThrows(ResponseStatusException.class,()->service.countCartons("OP1",77L,"WT1",request("K1",input("BOX1","A",13),input("BOX2","B",7))));
|
||||
}
|
||||
@Test void missingDuplicateForeignSkuAndInvalidQuantityRejectBeforeWrites() {
|
||||
var invalid = List.of(request("K",input("BOX1","A",1)),request("K",input("BOX1","A",1),input("BOX1","A",2)),request("K",input("OTHER","A",1),input("BOX2","B",2)),request("K",input("BOX1","B",1),input("BOX2","B",2)),request("K",input("BOX1","A",0),input("BOX2","B",2)));
|
||||
for(var request:invalid) assertThrows(ResponseStatusException.class,()->service.countCartons("OP1",77L,"WT1",request));
|
||||
verify(cartons,never()).updateById(any(WarehouseTransferCarton.class));
|
||||
}
|
||||
@Test void shortReceiptNeedsReasonAndPreservesMissingBoxFact() {
|
||||
var request = new WarehouseTransferRequests.CountCartons("SHORT",List.of(input("BOX1","A",3)),"少到一箱");
|
||||
service.countCartons("OP1",77L,"WT1",request);
|
||||
assertEquals("COUNTED",a.getStatus()); assertEquals("NOT_RECEIVED",b.getStatus());
|
||||
assertEquals("少到一箱",b.getDiscrepancyReason()); assertNull(b.getCountedQuantity());
|
||||
}
|
||||
@Test void extraReceiptGeneratesOwnedBoxAndReconcilesToDeclaredSku() {
|
||||
var extra = new WarehouseTransferRequests.CountCarton(null,11L,200L,new BigDecimal("10"),new BigDecimal("20"),new BigDecimal("30"),BigDecimal.ONE,"B",4);
|
||||
service.countCartons("OP1",77L,"WT1",new WarehouseTransferRequests.CountCartons("EXTRA",List.of(input("BOX1","A",1),input("BOX2","B",2),extra),"多到一箱"));
|
||||
var captured = org.mockito.ArgumentCaptor.forClass(WarehouseTransferCarton.class);
|
||||
verify(cartons).insert(captured.capture());
|
||||
assertEquals("WT1#EX-12",captured.getValue().getCartonMark()); assertEquals("C1",captured.getValue().getCustomerId()); assertEquals(4L,captured.getValue().getCountedQuantity());
|
||||
}
|
||||
@Test void cartonsRequireGeneratedPalletOwnedByThisOrder() {
|
||||
when(palletMapper.listOrder("C1",1L)).thenReturn(List.of());
|
||||
assertThrows(ResponseStatusException.class,()->service.countCartons("OP1",77L,"WT1",request("K",input("BOX1","A",1),input("BOX2","B",2))));
|
||||
verify(cartons,never()).updateById(any(WarehouseTransferCarton.class));
|
||||
when(palletMapper.listOrder("C1",1L)).thenReturn(List.of(pallet)); pallet.setStatus("DRAFT");
|
||||
assertThrows(ResponseStatusException.class,()->service.countCartons("OP1",77L,"WT1",request("K",input("BOX1","A",1),input("BOX2","B",2))));
|
||||
}
|
||||
@Test void multiSkuPalletCreatesOneInboundLineAllocationWithSummedBoxes() {
|
||||
var plans = mock(com.baiyue.lastleg.inboundplan.mapper.InboundPlanMapper.class);
|
||||
var lines = mock(com.baiyue.lastleg.inboundplan.mapper.InboundPlanLineMapper.class);
|
||||
var pallets = mock(com.baiyue.lastleg.inboundplan.mapper.InboundPlanPalletMapper.class);
|
||||
var allocations = mock(com.baiyue.lastleg.inboundplan.mapper.InboundPlanPalletAllocationMapper.class);
|
||||
ReflectionTestUtils.setField(service,"inboundPlans",plans); ReflectionTestUtils.setField(service,"inboundPlanLines",lines);
|
||||
ReflectionTestUtils.setField(service,"inboundPlanPallets",pallets); ReflectionTestUtils.setField(service,"inboundPlanAllocations",allocations);
|
||||
order.setLastlegOrderId(9L); order.setInboundCode("WT1");
|
||||
var plan = new com.baiyue.lastleg.inboundplan.entity.InboundPlan(); plan.setId(100L);
|
||||
var line = new com.baiyue.lastleg.inboundplan.entity.InboundPlanLine(); line.setId(101L);
|
||||
when(plans.findBySourceOrder("C1",9L)).thenReturn(plan); when(lines.selectOne(any())).thenReturn(line);
|
||||
var pallet = new WarehouseTransferPallet(); pallet.setId(200L); pallet.setCartonCount(3); pallet.setVolumeCbm(BigDecimal.ONE); pallet.setSequenceNo(1);
|
||||
var first = new WarehouseTransferPalletItem(); first.setTransferPalletId(200L); first.setTransferItemId(10L); first.setPackageCount(1);
|
||||
var second = new WarehouseTransferPalletItem(); second.setTransferPalletId(200L); second.setTransferItemId(11L); second.setPackageCount(2);
|
||||
ReflectionTestUtils.invokeMethod(service,"completeTransferInboundPlan","C1",order,List.of(pallet),List.of(first,second),java.time.LocalDateTime.now());
|
||||
var result=org.mockito.ArgumentCaptor.forClass(com.baiyue.lastleg.inboundplan.entity.InboundPlanPalletAllocation.class);
|
||||
verify(allocations,times(1)).insert(result.capture()); assertEquals(3,result.getValue().getPickedPackageCount()); assertEquals(3,result.getValue().getCountedPackageCount());
|
||||
}
|
||||
@Test void otherWarehouseCannotReadOrWriteAndCompletedCountsCannotBeOverwritten() {
|
||||
assertThrows(ResponseStatusException.class,()->service.countCartons("OP1",88L,"WT1",request("K",input("BOX1","A",1))));
|
||||
service.countCartons("OP1",77L,"WT1",request("K",input("BOX1","A",1),input("BOX2","B",2)));
|
||||
clearInvocations(cartons);
|
||||
assertThrows(ResponseStatusException.class,()->service.countCartons("OP2",77L,"WT1",request("NEW",input("BOX1","A",9),input("BOX2","B",2))));
|
||||
verify(cartons,never()).updateById(any(WarehouseTransferCarton.class));
|
||||
}
|
||||
}
|
||||
+11
-13
@@ -48,19 +48,17 @@ class WarehouseTransferPalletAllocationTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void persistedOverReceiptUsesTheSameStableAllocationRule() {
|
||||
WarehouseTransferPallet first = persistedPallet(4);
|
||||
WarehouseTransferPallet second = persistedPallet(3);
|
||||
|
||||
var allocations = WarehouseTransferService.derivePersistedPalletAllocations(
|
||||
List.of(item(11L, 3), item(12L, 2)), List.of(first, second));
|
||||
|
||||
assertEquals(7, allocations.stream().mapToInt(value -> value.packageCount()).sum());
|
||||
assertEquals(3, allocations.stream().filter(value -> value.transferItemId() == 11L)
|
||||
.mapToInt(value -> value.packageCount()).sum());
|
||||
assertEquals(4, allocations.stream().filter(value -> value.transferItemId() == 12L)
|
||||
.mapToInt(value -> value.packageCount()).sum());
|
||||
assertEquals(List.of(0, 0, 1), allocations.stream().map(value -> value.palletIndex()).toList());
|
||||
void countedBoxesDetermineSkuPlacementRatherThanDeclarationOrder() {
|
||||
WarehouseTransferPallet first = persistedPallet(2); first.setId(100L);
|
||||
WarehouseTransferPallet second = persistedPallet(1); second.setId(200L);
|
||||
var a = new com.baiyue.lastleg.transfer.entity.WarehouseTransferCarton(); a.setTransferItemId(12L); a.setTransferPalletId(100L);
|
||||
var b = new com.baiyue.lastleg.transfer.entity.WarehouseTransferCarton(); b.setTransferItemId(12L); b.setTransferPalletId(100L);
|
||||
var c = new com.baiyue.lastleg.transfer.entity.WarehouseTransferCarton(); c.setTransferItemId(11L); c.setTransferPalletId(200L);
|
||||
var allocations = WarehouseTransferService.deriveCountedPalletAllocations(List.of(a,b,c),List.of(first,second));
|
||||
assertEquals(List.of(new WarehouseTransferService.DerivedPalletAllocation(0,12L,2),new WarehouseTransferService.DerivedPalletAllocation(1,11L,1)),allocations);
|
||||
c.setTransferPalletId(999L);
|
||||
org.junit.jupiter.api.Assertions.assertThrows(org.springframework.web.server.ResponseStatusException.class,
|
||||
() -> WarehouseTransferService.deriveCountedPalletAllocations(List.of(a,b,c),List.of(first,second)));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+8
@@ -322,6 +322,10 @@ class WarehouseTransferServiceTest {
|
||||
WarehouseTransferOrder transfer = transfer(WarehouseTransferService.WAIT_PDA_RECEIVING);
|
||||
transfer.setOperatingWarehouseId("77");
|
||||
WarehouseTransferPallet pallet = measuredPallet("GENERATED");
|
||||
var counted = java.util.stream.IntStream.rangeClosed(1, 5).mapToObj(index -> {
|
||||
var box = new WarehouseTransferCarton(); box.setStatus("COUNTED"); box.setCountedAt(LocalDateTime.now()); return box;
|
||||
}).toList();
|
||||
when(transferCartons.listOrder("TENANT-A", transfer.getId())).thenReturn(counted);
|
||||
when(transfers.lockForPda("WTR-1", "77")).thenReturn(transfer);
|
||||
when(transferItems.selectList(any())).thenReturn(List.of(transferItem(51L, "SKU-1", 5)));
|
||||
WarehouseTransferMeasurementPhoto photo = new WarehouseTransferMeasurementPhoto();
|
||||
@@ -366,6 +370,10 @@ class WarehouseTransferServiceTest {
|
||||
new InventoryCommandResponse(1L, "STOCK_IN", 801L, 901L, 5, 0, 0, 0, false, false));
|
||||
when(sourceOrders.selectList(any())).thenReturn(List.of());
|
||||
|
||||
var counted = java.util.stream.IntStream.rangeClosed(1, 5).mapToObj(index -> {
|
||||
var box = new WarehouseTransferCarton(); box.setTransferPalletId(pallet.getId()); box.setTransferItemId(item.getId()); box.setCartonIndex(index); box.setTotalCartons(5); box.setCartonMark("BOX-" + index); box.setStatus("COUNTED"); box.setCountedAt(LocalDateTime.now()); return box;
|
||||
}).toList();
|
||||
when(transferCartons.listOrder("TENANT-A", transfer.getId())).thenReturn(counted);
|
||||
var result = service.palletize("OP-1", 77L, "WTR-1",
|
||||
new WarehouseTransferRequests.Palletize("DONE-1", List.of(9001L)));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user