提交
This commit is contained in:
+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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
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");
|
||||
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());
|
||||
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 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));
|
||||
}
|
||||
}
|
||||
+30
-2
@@ -59,6 +59,7 @@ class CargoDiscardServiceTest {
|
||||
@Mock ReturnInventoryCartonItemMapper returnCartonItems;
|
||||
|
||||
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 +68,8 @@ class CargoDiscardServiceTest {
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new CargoDiscardService();
|
||||
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);
|
||||
@@ -96,10 +99,10 @@ class CargoDiscardServiceTest {
|
||||
LastlegOrder order = order();
|
||||
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(
|
||||
@@ -113,6 +116,30 @@ class CargoDiscardServiceTest {
|
||||
verify(inventory, never()).deductSkuInventory(any(), anyLong(), anyInt());
|
||||
}
|
||||
|
||||
|
||||
@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);
|
||||
@@ -167,6 +194,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",
|
||||
|
||||
Reference in New Issue
Block a user