Merge branch 'test' of http://8.163.30.128:10082/java-project/baiyue-firstleg into test
This commit is contained in:
+2
-1
@@ -67,6 +67,7 @@ class DeliveryLongIdJsonContractTest {
|
||||
null, null, "PS-1", null, null, null, null, "CREATED", null,
|
||||
null, null, null, null, null, null, null, null, null, null, null,
|
||||
0, 0, null, null, null, null, null, null, null, null, 0, java.util.List.of(), null,
|
||||
com.baiyue.lastleg.delivery.model.DeliveryDocumentSnapshot.summary(null), null, null, null);
|
||||
com.baiyue.lastleg.delivery.model.DeliveryDocumentSnapshot.summary(null), null, null, null,
|
||||
null, null, null, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
+18
@@ -19,6 +19,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
|
||||
class DeliveryCompletedOutboxServiceTest {
|
||||
@Test
|
||||
@@ -48,6 +49,23 @@ class DeliveryCompletedOutboxServiceTest {
|
||||
.containsExactly("ONT8", "ACY2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void skipsRelabelApplicationWithoutDomesticSourceOrder() {
|
||||
LastlegEventOutboxMapper mapper = mock(LastlegEventOutboxMapper.class);
|
||||
DeliveryCompletedOutboxService service = new DeliveryCompletedOutboxService();
|
||||
ReflectionTestUtils.setField(service, "outbox", mapper);
|
||||
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper().registerModule(new JavaTimeModule()));
|
||||
DeliveryOrder delivery = new DeliveryOrder();
|
||||
delivery.setId(1L); delivery.setDeliveryNo("DL-RELABEL"); delivery.setVersion(2L);
|
||||
LastlegOrder relabel = new LastlegOrder();
|
||||
relabel.setId(21L); relabel.setOrderType("RELABEL_APPLICATION");
|
||||
relabel.setCustomerId("88"); relabel.setSourceInboundId("RL-1:WH-1");
|
||||
|
||||
service.publish(delivery, List.of(relabel), Instant.parse("2026-09-15T08:00:00Z"));
|
||||
|
||||
verifyNoInteractions(mapper);
|
||||
}
|
||||
|
||||
private LastlegOrder order(long id, String inboundId, String orderId, String inboundCode, String fba) {
|
||||
LastlegOrder value = new LastlegOrder();
|
||||
value.setId(id); value.setOrderNo("LM-" + id); value.setCustomerId("88");
|
||||
|
||||
+25
-1
@@ -15,7 +15,8 @@ class DispatchCandidateMapperContractTest {
|
||||
"planNo", "orderType", "volumeWeightKg", "containerNo", "appointmentAt", "expectedLoadingAt",
|
||||
"overseasWarehouseId", "overseasWarehouseName", "orderNo", "orderRefs", "inboundCode", "palletNo",
|
||||
"destinationWarehouseId", "destinationWarehouseCode", "destinationWarehouseName", "fbaWarehouseCode",
|
||||
"packageCount", "volumeCbm", "sourceStage", "cartonMarks", "relabelCartonMarks"
|
||||
"packageCount", "volumeCbm", "sourceStage", "cartonMarks", "relabelCartonMarks",
|
||||
"destinationType", "deliveryAddress", "postalCode", "contactName", "contactPhone"
|
||||
);
|
||||
|
||||
@Test
|
||||
@@ -37,6 +38,29 @@ class DispatchCandidateMapperContractTest {
|
||||
assertTrue(sql.contains("pos.available_packages > 0"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void candidatesWithoutFbaWarehouseAreExcludedFromReadAndWriteQueries() throws Exception {
|
||||
for (Method method : List.of(
|
||||
DispatchCandidateMapper.class.getMethod("available", String.class, List.class, String.class,
|
||||
String.class, String.class, String.class),
|
||||
DispatchCandidateMapper.class.getMethod("lockSelected", String.class, List.class))) {
|
||||
String sql = String.join(" ", method.getAnnotation(Select.class).value());
|
||||
assertTrue(sql.contains("NULLIF(TRIM(l.fba_warehouse_code),'') IS NOT NULL"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void candidateKeywordSearchIncludesSourceInboundCode() throws Exception {
|
||||
Method available = DispatchCandidateMapper.class.getMethod("available", String.class, List.class,
|
||||
String.class, String.class, String.class, String.class);
|
||||
String availableSql = String.join(" ", available.getAnnotation(Select.class).value());
|
||||
assertTrue(availableSql.contains("o.order_no LIKE CONCAT('%',#{orderNo},'%') OR l.inbound_code LIKE CONCAT('%',#{orderNo},'%')"));
|
||||
|
||||
Method relabel = DispatchCandidateMapper.class.getMethod("relabelAvailable", String.class, String.class, String.class);
|
||||
String relabelSql = String.join(" ", relabel.getAnnotation(Select.class).value());
|
||||
assertTrue(relabelSql.contains("ri.source_inbound_code LIKE CONCAT('%',#{orderNo},'%')"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void candidateWarehouseComesFromPhysicalInventoryOrFrozenInboundPlan() throws Exception {
|
||||
for (Method method : List.of(
|
||||
|
||||
+24
@@ -190,6 +190,22 @@ class DispatchServiceTest {
|
||||
verify(orders, never()).insert(any(DispatchOrder.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void preArrangeRejectsPalletWithoutFbaWarehouse() {
|
||||
DispatchPalletCandidate missingFba = new DispatchPalletCandidate(1L, 10L, 20L, 2L,
|
||||
"WH-1", "Overseas warehouse", "LMO1", "LMO1", "RK1", "LIP1-P001",
|
||||
null, null, null, null, 12, new BigDecimal("1.2000"), "RECEIVED");
|
||||
when(candidates.lockSelected("100", List.of(10L))).thenReturn(List.of(missingFba));
|
||||
|
||||
ResponseStatusException exception = assertThrows(ResponseStatusException.class,
|
||||
() -> service.preArrange("100", "7",
|
||||
new DispatchRequests.PreArrange(List.of(10L), null, null, null, null)));
|
||||
|
||||
assertEquals(409, exception.getStatusCode().value());
|
||||
assertEquals("所选托盘尚未设置 FBA 仓,不能智能排车", exception.getReason());
|
||||
verify(orders, never()).insert(any(DispatchOrder.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void removingPalletFromOwnedDraftImmediatelyReleasesLock() {
|
||||
when(candidates.lockSelected("100", List.of(10L))).thenReturn(List.of(candidate(10L, null, "ONT8")));
|
||||
@@ -307,6 +323,14 @@ class DispatchServiceTest {
|
||||
verify(candidates).available(null, null, null, null, "RECEIVED", "draft-1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void relabelSourceDoesNotRequireOrQueryAnInboundPlan() {
|
||||
when(candidates.relabelAvailable(null, "RL-1", null)).thenReturn(List.of());
|
||||
org.junit.jupiter.api.Assertions.assertTrue(service.platformCandidates("RL-1", null, "RELABEL", "draft-1").isEmpty());
|
||||
verify(candidates).relabelAvailable(null, "RL-1", null);
|
||||
verify(candidates, never()).available(any(), any(), any(), any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolvesCustomerFromSelectedPlansAndPalletsInMultiCustomerDeployment() {
|
||||
when(candidates.findCustomerScopesByPlanIds(List.of(1L, 2L))).thenReturn(List.of("100"));
|
||||
|
||||
+2
-1
@@ -29,7 +29,8 @@ class InboundPlanSchemaContractTest {
|
||||
assertTrue(schema.contains("unique key uk_ll_fba_appointment_no (customer_id, appointment_no)"));
|
||||
assertTrue(schema.contains("expected_loading_at"));
|
||||
assertTrue(schema.contains("planned_arrival_date"));
|
||||
assertTrue(schema.contains("creation_mode"));
|
||||
assertTrue(schema.contains("creation_mode varchar(32)"));
|
||||
assertTrue(schema.contains("modify column creation_mode varchar(32)"));
|
||||
assertTrue(schema.contains("pallet_type"));
|
||||
assertTrue(schema.contains("create table if not exists ll_order_command_record"));
|
||||
assertTrue(schema.contains("create table if not exists ll_order_hold_history"));
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ class PdaPalletOperationControllerContractTest {
|
||||
.filter(PostMapping.class::isInstance).map(PostMapping.class::cast)
|
||||
.flatMap(annotation -> Arrays.stream(annotation.value())).collect(Collectors.toSet());
|
||||
assertEquals(Set.of("/pallets/resolve", "/relabel-pallet-label", "/assemble", "/disassemble",
|
||||
"/relabel-disassemble", "/swap", "/unbind"), paths);
|
||||
"/relabel-disassemble", "/relabel-sources", "/swap", "/unbind"), paths);
|
||||
}
|
||||
|
||||
@Test void palletContentAnnotationUsesNativeGreaterThanOperator() throws Exception {
|
||||
|
||||
+107
-1
@@ -42,6 +42,17 @@ import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class PdaPalletOperationServiceTest {
|
||||
@Test void relabelRebindingRejectsDispatchAndOutboundOccupation() {
|
||||
RelabelApplication app = new RelabelApplication();
|
||||
for (String status : List.of("PRE_DISPATCH_CREATED", "OUTBOUND_CREATED", "OUTBOUNDED")) {
|
||||
app.setFulfillmentStatus(status);
|
||||
assertThrows(ResponseStatusException.class, () -> ReflectionTestUtils.invokeMethod(service, "requireUnallocatedRelabel", app));
|
||||
}
|
||||
app.setFulfillmentStatus("WAIT_PRE_DISPATCH");
|
||||
assertDoesNotThrow(() -> ReflectionTestUtils.invokeMethod(service, "requireUnallocatedRelabel", app));
|
||||
app.setOutboundOrderNo("OUT-1");
|
||||
assertThrows(ResponseStatusException.class, () -> ReflectionTestUtils.invokeMethod(service, "requireUnallocatedRelabel", app));
|
||||
}
|
||||
@Mock private PdaPalletOperationMapper mapper;
|
||||
@Mock private InventoryPositionMapper positions;
|
||||
@Mock private InventoryBalanceMapper balances;
|
||||
@@ -185,6 +196,13 @@ class PdaPalletOperationServiceTest {
|
||||
}
|
||||
|
||||
@Test void relabelDisassembleMovesOnlySelectedHeldCartonWithoutRequiringLocations() {
|
||||
var tasks = mock(com.baiyue.lastleg.relabel.pda.mapper.PdaRelabelTaskMapper.class);
|
||||
var histories = mock(com.baiyue.lastleg.relabel.pda.mapper.PdaRelabelHistoryMapper.class);
|
||||
ReflectionTestUtils.setField(service, "relabelWorkTasks", tasks);
|
||||
ReflectionTestUtils.setField(service, "relabelWorkHistory", histories);
|
||||
var task = new com.baiyue.lastleg.relabel.pda.entity.PdaRelabelTask();
|
||||
task.setId(900L); task.setStatus("COMPLETED");
|
||||
when(tasks.find("100", 600L)).thenReturn(task);
|
||||
var sourcePallet = new PalletPhysicalGateway.PalletReference(20L, "SOURCE", 88L, null,
|
||||
"OCCUPIED", null, 1);
|
||||
var targetPallet = new PalletPhysicalGateway.PalletReference(10L, "TARGET", 88L, null,
|
||||
@@ -196,7 +214,7 @@ class PdaPalletOperationServiceTest {
|
||||
|
||||
RelabelApplication application = new RelabelApplication(); application.setId(600L);
|
||||
application.setCustomerId("100"); application.setRelabelNo("RL-1"); application.setStatus("READY_FOR_OUTBOUND");
|
||||
application.setOverseasWarehouseId("88"); application.setOutboundOrderNo("OUT-1");
|
||||
application.setOverseasWarehouseId("88"); application.setFulfillmentStatus("WAIT_PRE_DISPATCH");
|
||||
when(relabelApplications.findAnyByRelabelNo("RL-1")).thenReturn(application);
|
||||
when(relabelApplications.lockAnyByRelabelNo("RL-1")).thenReturn(application);
|
||||
RelabelItem selected = relabelItem(701L, "CARTON-A", 100L);
|
||||
@@ -266,6 +284,94 @@ class PdaPalletOperationServiceTest {
|
||||
relabelPalletMutations);
|
||||
}
|
||||
|
||||
@Test void actualSourceReconcilesOnlySelectedHoldAndKeepsOriginalInboundBalance() {
|
||||
var tasks = mock(com.baiyue.lastleg.relabel.pda.mapper.PdaRelabelTaskMapper.class);
|
||||
var histories = mock(com.baiyue.lastleg.relabel.pda.mapper.PdaRelabelHistoryMapper.class);
|
||||
ReflectionTestUtils.setField(service, "relabelWorkTasks", tasks);
|
||||
ReflectionTestUtils.setField(service, "relabelWorkHistory", histories);
|
||||
var task = new com.baiyue.lastleg.relabel.pda.entity.PdaRelabelTask(); task.setId(900L); task.setStatus("COMPLETED");
|
||||
when(tasks.find("100", 600L)).thenReturn(task);
|
||||
RelabelApplication app = new RelabelApplication(); app.setId(600L); app.setRelabelNo("RL-1");
|
||||
app.setCustomerId("100"); app.setOverseasWarehouseId("88"); app.setStatus("READY_FOR_OUTBOUND");
|
||||
when(relabelApplications.findAnyByRelabelNo("RL-1")).thenReturn(app);
|
||||
when(relabelApplications.lockAnyByRelabelNo("RL-1")).thenReturn(app);
|
||||
RelabelItem item = relabelItem(701L, "CARTON-A", 101L);
|
||||
when(relabelItems.selectList(any())).thenReturn(List.of(item));
|
||||
when(relabelItems.selectById(701L)).thenReturn(item);
|
||||
InventoryPosition anchor = position(101L, 30L, "ACCOUNTING", 2L, 3L);
|
||||
InventoryPosition actual = position(100L, 20L, "SOURCE", 0L, 4L); actual.setAvailablePackages(5L);
|
||||
AtomicReference<InventoryPosition> target = new AtomicReference<>();
|
||||
when(positions.selectById(anyLong())).thenAnswer(invocation -> {
|
||||
long id = invocation.getArgument(0); return id == 101L ? anchor : id == 100L ? actual : target.get();
|
||||
});
|
||||
doAnswer(invocation -> { target.set(invocation.getArgument(0)); return 1; }).when(positions).insert(any(InventoryPosition.class));
|
||||
when(mapper.lockPosition(eq("88"), anyLong())).thenAnswer(invocation -> {
|
||||
InventoryPosition p = ((Long) invocation.getArgument(1)) == 101L ? anchor : actual;
|
||||
PalletInventoryRow row = row(p.getId(), p.getPalletId(), p.getVersion(), p.getAvailablePackages());
|
||||
row.setHoldPackages(p.getHoldPackages()); return row;
|
||||
});
|
||||
var src = new PalletPhysicalGateway.PalletReference(20L, "SOURCE", 88L, null, "OCCUPIED", null, 1);
|
||||
var dst = new PalletPhysicalGateway.PalletReference(10L, "TARGET", 88L, null, "IDLE", null, 2);
|
||||
when(physical.resolve("SOURCE")).thenReturn(src); when(physical.resolve("TARGET")).thenReturn(dst);
|
||||
when(physical.apply(eq(88L), any(), anyString(), isNull(), anyString(), any()))
|
||||
.thenAnswer(invocation -> transition(invocation.getArgument(1), invocation.getArgument(2)));
|
||||
when(mapper.content("88", 10L)).thenReturn(List.of());
|
||||
when(mapper.palletPackages("88", 20L)).thenAnswer(invocation -> actual.getAvailablePackages() + actual.getHoldPackages());
|
||||
when(mapper.palletPackages("88", 10L)).thenAnswer(invocation -> target.get() == null ? 0L : target.get().getHoldPackages());
|
||||
InventoryBalance balance = new InventoryBalance(); balance.setId(200L); balance.setCustomerId("100");
|
||||
balance.setOverseasWarehouseId("88"); balance.setInboundCode("ORIGINAL-INBOUND");
|
||||
when(balances.selectById(200L)).thenReturn(balance);
|
||||
AtomicReference<PdaPalletOperation> op = new AtomicReference<>();
|
||||
doAnswer(invocation -> { op.set(invocation.getArgument(0)); return 1; }).when(mapper).insert(any(PdaPalletOperation.class));
|
||||
when(mapper.selectById(anyLong())).thenAnswer(invocation -> op.get());
|
||||
var result = service.relabelDisassemble(identity(), new PalletOperationRequests.RelabelDisassemble(
|
||||
"qty-1", "RL-1", new PalletOperationRequests.PalletExpectation("SOURCE", 1),
|
||||
new PalletOperationRequests.PalletExpectation("TARGET", 2), List.of("CARTON-A"), "quantity",
|
||||
new PalletOperationRequests.MoveLine(100L, 1L, 4L)));
|
||||
assertEquals("COMPLETED", result.status());
|
||||
assertEquals(1L, anchor.getHoldPackages()); // Other order's hold is not moved.
|
||||
assertEquals(1L, anchor.getAvailablePackages());
|
||||
assertEquals(4L, actual.getAvailablePackages()); assertEquals(0L, actual.getHoldPackages());
|
||||
assertEquals(1L, target.get().getHoldPackages()); assertEquals(200L, target.get().getBalanceId());
|
||||
assertEquals(7L, anchor.getAvailablePackages() + anchor.getHoldPackages() + actual.getAvailablePackages()
|
||||
+ actual.getHoldPackages() + target.get().getHoldPackages());
|
||||
verify(relabelPalletMutations).moveSelectedItems(eq(app), argThat(values -> values.get(100L).equals(List.of(item))), anyMap());
|
||||
ArgumentCaptor<InventoryLedger> entries = ArgumentCaptor.forClass(InventoryLedger.class);
|
||||
verify(ledgers, times(4)).insert(entries.capture());
|
||||
assertTrue(entries.getAllValues().stream().allMatch(entry -> "ORIGINAL-INBOUND".equals(entry.getInboundCode())));
|
||||
verify(balances, never()).updateById(any(InventoryBalance.class));
|
||||
}
|
||||
|
||||
@Test void boundMarksSurviveReloadButDoNotBlockANewReworkRound() {
|
||||
var tasks = mock(com.baiyue.lastleg.relabel.pda.mapper.PdaRelabelTaskMapper.class);
|
||||
var histories = mock(com.baiyue.lastleg.relabel.pda.mapper.PdaRelabelHistoryMapper.class);
|
||||
ReflectionTestUtils.setField(service, "relabelWorkTasks", tasks);
|
||||
ReflectionTestUtils.setField(service, "relabelWorkHistory", histories);
|
||||
RelabelApplication app = new RelabelApplication(); app.setId(600L); app.setCustomerId("100");
|
||||
var task = new com.baiyue.lastleg.relabel.pda.entity.PdaRelabelTask(); task.setId(900L);
|
||||
when(tasks.find("100", 600L)).thenReturn(task);
|
||||
var history = new com.baiyue.lastleg.relabel.pda.entity.PdaRelabelHistory();
|
||||
history.setDetailJson("{\"round\":\"INITIAL\",\"cartonMarks\":[\"CARTON-A\"]}");
|
||||
when(histories.selectList(any())).thenReturn(List.of(history));
|
||||
assertEquals(java.util.Set.of("CARTON-A"), ReflectionTestUtils.invokeMethod(service, "boundRelabelMarks", app));
|
||||
task.setReviewedAt(LocalDateTime.of(2026, 9, 15, 12, 0));
|
||||
assertEquals(java.util.Set.of(), ReflectionTestUtils.invokeMethod(service, "boundRelabelMarks", app));
|
||||
}
|
||||
|
||||
@Test void createsPalletBeforeCustomerConfirmationOnlyForClaimingOperator() {
|
||||
var tasks = mock(com.baiyue.lastleg.relabel.pda.mapper.PdaRelabelTaskMapper.class);
|
||||
ReflectionTestUtils.setField(service, "relabelWorkTasks", tasks);
|
||||
RelabelApplication app = new RelabelApplication(); app.setId(600L); app.setRelabelNo("RL-1");
|
||||
app.setCustomerId("100"); app.setOverseasWarehouseId("88"); app.setStatus("PROCESSING");
|
||||
when(relabelApplications.findAnyByRelabelNo("RL-1")).thenReturn(app);
|
||||
var task = new com.baiyue.lastleg.relabel.pda.entity.PdaRelabelTask(); task.setStatus("PROCESSING"); task.setOperatorKey("7");
|
||||
when(tasks.find("100", 600L)).thenReturn(task);
|
||||
when(physical.ensureRelabelPallet(88L, "RL-1:before-confirm")).thenReturn(pallet(30L, "NEW", 88L, 0));
|
||||
assertEquals("NEW", service.createRelabelPallet(identity(), new PalletOperationRequests.NewRelabelPallet("RL-1", "before-confirm")).palletCode());
|
||||
task.setOperatorKey("other");
|
||||
assertThrows(ResponseStatusException.class, () -> service.createRelabelPallet(identity(), new PalletOperationRequests.NewRelabelPallet("RL-1", "different")));
|
||||
}
|
||||
|
||||
private static PdaWarehouseIdentity identity() { return new PdaWarehouseIdentity("7", "picker", 88L); }
|
||||
private static PalletPhysicalGateway.PalletReference pallet(long id, String code, long warehouse, int version) {
|
||||
return new PalletPhysicalGateway.PalletReference(id, code, warehouse, 1000L + id,
|
||||
|
||||
+32
-1
@@ -61,6 +61,7 @@ class PdaRelabelServiceTest {
|
||||
void setUp() {
|
||||
service = new PdaRelabelService();
|
||||
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);
|
||||
ReflectionTestUtils.setField(service, "tasks", tasks);
|
||||
ReflectionTestUtils.setField(service, "unitFacts", unitFacts);
|
||||
@@ -103,7 +104,7 @@ class PdaRelabelServiceTest {
|
||||
PdaRelabelTask active = task("PROCESSING", 0);
|
||||
active.setApplicationId(99L);
|
||||
active.setRelabelNo("FBR-ACTIVE");
|
||||
when(tasks.lockUnfinishedByOperator("7")).thenReturn(active);
|
||||
when(tasks.lockProcessingByOperator("7")).thenReturn(active);
|
||||
|
||||
ResponseStatusException error = assertThrows(ResponseStatusException.class,
|
||||
() -> service.claim("7", "picker", 900L, "FBR-1", new PdaRelabelRequests.Claim("claim-other")));
|
||||
@@ -205,6 +206,36 @@ class PdaRelabelServiceTest {
|
||||
assertEquals(1, result.version());
|
||||
}
|
||||
|
||||
@org.junit.jupiter.params.ParameterizedTest
|
||||
@org.junit.jupiter.params.provider.ValueSource(strings = {"SKU_LABEL", "CARTON_AND_SKU"})
|
||||
void oldSkuOnlyScanPersistsEachRowAndCompletesBoxOnlyWhenRequiredStagesFinish(String type) {
|
||||
application.setStatus("PROCESSING");
|
||||
item.setOperationType(type); item.setCartonMark("BOX-1"); item.setFbaCartonMark("FBA-1");
|
||||
item.setNotifiedPackageCount(1);
|
||||
item.setSkuRelabelItemsJson("[{\"skuCode\":\"OLD-A\",\"newSkuCode\":\"NEW-A\",\"quantity\":3},{\"skuCode\":\"OLD-B\",\"newSkuCode\":\"NEW-B\",\"quantity\":2}]");
|
||||
var task = task("PROCESSING", 0);
|
||||
var saved = new ArrayList<com.baiyue.lastleg.relabel.pda.entity.PdaRelabelHistory>();
|
||||
when(histories.selectList(any())).thenAnswer(call -> List.copyOf(saved));
|
||||
when(histories.insert(any(com.baiyue.lastleg.relabel.pda.entity.PdaRelabelHistory.class))).thenAnswer(call -> { saved.add(call.getArgument(0)); return 1; });
|
||||
when(applications.lockAnyByRelabelNo("FBR-1")).thenReturn(application);
|
||||
lenient().when(applications.findAnyByRelabelNo("FBR-1")).thenReturn(application);
|
||||
when(tasks.lock("100", 1L)).thenReturn(task);
|
||||
when(items.selectOne(any())).thenReturn(item);
|
||||
var first = service.scanSku("7", "picker", 900L, "FBR-1", new PdaRelabelRequests.SkuScan(10L, 0, "OLD-A", 0, "sku-a"));
|
||||
assertEquals(0, first.relabelledPackages()); assertEquals(1, first.version());
|
||||
assertTrue(saved.stream().anyMatch(entry -> "SKU_SCAN".equals(entry.getAction()) && entry.getDetailJson().contains("NEW-A")));
|
||||
assertThrows(ResponseStatusException.class, () -> service.scanSku("7", "picker", 900L, "FBR-1", new PdaRelabelRequests.SkuScan(10L, 0, "OLD-A", 1, "duplicate")));
|
||||
assertThrows(ResponseStatusException.class, () -> service.scanSku("7", "picker", 900L, "FBR-1", new PdaRelabelRequests.SkuScan(10L, 1, "WRONG", 1, "wrong")));
|
||||
var second = service.scanSku("7", "picker", 900L, "FBR-1", new PdaRelabelRequests.SkuScan(10L, 1, "OLD-B", 1, "sku-b"));
|
||||
assertEquals(1, second.items().get(0).skuProcessedPackages());
|
||||
if ("CARTON_AND_SKU".equals(type)) {
|
||||
assertEquals(0, second.relabelledPackages());
|
||||
var last = service.scanCarton("7", "picker", 900L, "FBR-1", new PdaRelabelRequests.CartonScan("BOX-1", 2, "carton-last"));
|
||||
assertEquals(1, last.relabelledPackages());
|
||||
} else assertEquals(1, second.relabelledPackages());
|
||||
assertEquals(1, persistedFacts.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void duplicateOldLabelAndStaleVersionFailBeforeWritingAnotherFact() {
|
||||
application.setStatus("PROCESSING");
|
||||
|
||||
+136
-20
@@ -117,6 +117,7 @@ class RelabelApplicationServiceTest {
|
||||
lenient().when(feeSources.publishApproved(any(), any(), any())).thenReturn(new OverseasFeeSource());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void createBuildsPendingRelabelTailOrderItemsAndBothHistories() {
|
||||
RelabelRequests.Create request = request("key-1", 10, 6);
|
||||
@@ -144,7 +145,7 @@ class RelabelApplicationServiceTest {
|
||||
verify(orderHistories).insert(tailHistory.capture());
|
||||
|
||||
assertEquals("RELABEL_APPLICATION", tailOrder.getValue().getOrderType());
|
||||
assertEquals("PENDING_ASSIGNMENT", tailOrder.getValue().getCurrentStatus());
|
||||
assertEquals("WAIT_RELABEL", tailOrder.getValue().getCurrentStatus());
|
||||
assertEquals("RELABEL:PENDING_PROCESSING", tailOrder.getValue().getSubOrderStatus());
|
||||
assertEquals("CUSTOMER", tailOrder.getValue().getSourceRegion());
|
||||
assertTrue(tailOrder.getValue().getOrderNo().matches("RL-WH-\\d{8}-\\d{6}"));
|
||||
@@ -155,10 +156,36 @@ class RelabelApplicationServiceTest {
|
||||
assertEquals(6, application.getValue().getNotifiedPackageCount());
|
||||
assertEquals("SKU-A", relabelItem.getValue().getSkuCode());
|
||||
assertEquals("PENDING_PROCESSING", relabelHistory.getValue().getToStatus());
|
||||
assertEquals("PENDING_ASSIGNMENT", tailHistory.getValue().getToStatus());
|
||||
assertEquals("WAIT_RELABEL", tailHistory.getValue().getToStatus());
|
||||
assertEquals("FBR-1", result.application().relabelNo());
|
||||
}
|
||||
|
||||
@Test
|
||||
void warehouseTransferRelabelReusesExistingTailOrder() {
|
||||
RelabelRequests.Create request = request("key-transfer", 10, 6);
|
||||
RelabelApplication persisted = application(11L, "FBR-TRANSFER", "10001", "key-transfer", null,
|
||||
RelabelApplicationService.PENDING_PROCESSING);
|
||||
persisted.setLastlegOrderId(21L);
|
||||
LastlegOrder transferOrder = order(21L, "10001", "WT-WH-20260916-560449", "INBOUND_COMPLETED");
|
||||
transferOrder.setOrderType("WAREHOUSE_TRANSFER");
|
||||
when(applications.findNo(eq("10001"), any())).thenReturn(persisted);
|
||||
when(items.selectList(any())).thenReturn(List.of(item(31L, 11L, "SKU-A", 10, 6)));
|
||||
when(histories.selectList(any())).thenReturn(List.of());
|
||||
when(orders.selectById(21L)).thenReturn(transferOrder);
|
||||
|
||||
service.createForExistingTailOrder("10001", "operator-1", 21L, request);
|
||||
|
||||
ArgumentCaptor<RelabelApplication> application = ArgumentCaptor.forClass(RelabelApplication.class);
|
||||
verify(applications).insert(application.capture());
|
||||
verify(orders, never()).insert(any(LastlegOrder.class));
|
||||
verify(orderHistories, never()).insert(any(LastlegOrderStatusHistory.class));
|
||||
assertEquals(21L, application.getValue().getLastlegOrderId());
|
||||
assertEquals(application.getValue().getRelabelNo(), transferOrder.getRelabelOrderNo());
|
||||
assertEquals(RelabelApplicationService.PENDING_PROCESSING, transferOrder.getRelabelStatus());
|
||||
assertEquals("WAREHOUSE_TRANSFER", transferOrder.getOrderType());
|
||||
assertEquals("INBOUND_COMPLETED", transferOrder.getCurrentStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void createWithoutChargeSkipsWalletFreeze() {
|
||||
RelabelRequests.Create charged = request("key-free", 10, 6);
|
||||
@@ -242,27 +269,19 @@ class RelabelApplicationServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void createKeepsOneOperationWarehouseWhenItemsHaveDifferentFbaDestinations() {
|
||||
void createRejectsDifferentFbaDestinationsInOneApplication() {
|
||||
RelabelRequests.Item west = carton("SKU-A", "BOX-1", "FBA-1", "ONT8");
|
||||
RelabelRequests.Item east = carton("SKU-B", "BOX-2", "FBA-2", "ABE8");
|
||||
RelabelRequests.Create request = new RelabelRequests.Create("CUSTOMER-1", "WH-1",
|
||||
LocalDateTime.of(2026, 8, 20, 10, 0), "Li", "13800000000", BigDecimal.ONE, null, "key-multi",
|
||||
List.of(west, east));
|
||||
RelabelApplication persisted = application(11L, "FBR-1", "10001", "key-multi", null,
|
||||
RelabelApplicationService.PENDING_PROCESSING);
|
||||
persisted.setLastlegOrderId(21L);
|
||||
when(applications.findNo(eq("10001"), any())).thenReturn(persisted);
|
||||
when(items.selectList(any())).thenReturn(List.of(item(31L, 11L, "SKU-A", 1, 1)));
|
||||
when(histories.selectList(any())).thenReturn(List.of());
|
||||
when(orders.selectById(21L)).thenReturn(order(21L, "10001", "LMO-RELABEL-1",
|
||||
RelabelApplicationService.PENDING_PROCESSING));
|
||||
ResponseStatusException exception = assertThrows(ResponseStatusException.class,
|
||||
() -> service.create("10001", "operator-1", request));
|
||||
|
||||
service.create("10001", "operator-1", request);
|
||||
|
||||
ArgumentCaptor<LastlegOrder> created = ArgumentCaptor.forClass(LastlegOrder.class);
|
||||
verify(orders).insert(created.capture());
|
||||
assertEquals("WH-1", created.getValue().getOverseasWarehouseId());
|
||||
assertEquals(2, created.getValue().getPackageCount());
|
||||
assertEquals(400, exception.getStatusCode().value());
|
||||
assertEquals("同一张贴标申请只能选择一个 FBA 目的仓", exception.getReason());
|
||||
verify(orders, never()).insert(any(LastlegOrder.class));
|
||||
verify(applications, never()).insert(any(RelabelApplication.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -363,6 +382,9 @@ class RelabelApplicationServiceTest {
|
||||
application.setLastlegOrderId(21L);
|
||||
when(applications.lockNo("10001", "FBR-1")).thenReturn(application);
|
||||
stubDetail(application);
|
||||
LastlegOrder relabelOrder = order(21L, "10001", "LMO-RELABEL-1", "PENDING_ASSIGNMENT");
|
||||
relabelOrder.setOrderType("RELABEL_APPLICATION");
|
||||
when(orders.selectById(21L)).thenReturn(relabelOrder);
|
||||
|
||||
RelabelResponses.Detail result = service.changeStatus("10001", "FBR-1", "operator-2",
|
||||
new RelabelRequests.ChangeStatus("processing", "started"));
|
||||
@@ -372,12 +394,33 @@ class RelabelApplicationServiceTest {
|
||||
verify(applications).updateById(application);
|
||||
ArgumentCaptor<LastlegOrder> updatedOrder = ArgumentCaptor.forClass(LastlegOrder.class);
|
||||
verify(orders).updateById(updatedOrder.capture());
|
||||
assertEquals("PENDING_ASSIGNMENT", updatedOrder.getValue().getCurrentStatus());
|
||||
assertEquals("WAIT_RELABEL", updatedOrder.getValue().getCurrentStatus());
|
||||
assertEquals("RELABEL:PROCESSING", updatedOrder.getValue().getSubOrderStatus());
|
||||
verify(histories).insert(any(RelabelStatusHistory.class));
|
||||
verify(orderHistories).insert(any(LastlegOrderStatusHistory.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void reviewStageIsReportedAsCompletedOnLinkedCustomerAndPlatformOrder() {
|
||||
RelabelApplication application = application(11L, "FBR-REVIEW", "10001", "key-review", "hash",
|
||||
"PROCESSING");
|
||||
application.setLastlegOrderId(21L);
|
||||
LastlegOrder order = order(21L, "10001", "LMO-RELABEL-1", "PENDING_ASSIGNMENT");
|
||||
when(applications.lockNo("10001", "FBR-REVIEW")).thenReturn(application);
|
||||
when(applications.findNo("10001", "FBR-REVIEW")).thenReturn(application);
|
||||
when(items.selectList(any())).thenReturn(List.of(item(31L, application.getId(), "SKU-A", 1, 1)));
|
||||
when(histories.selectList(any())).thenReturn(List.of());
|
||||
when(orders.selectById(21L)).thenReturn(order);
|
||||
|
||||
service.changeStatus("10001", "FBR-REVIEW", "operator-2",
|
||||
new RelabelRequests.ChangeStatus(RelabelApplicationService.CUSTOMER_CONFIRM_PENDING, "submitted"));
|
||||
|
||||
assertEquals(RelabelApplicationService.CUSTOMER_CONFIRM_PENDING, application.getStatus());
|
||||
assertEquals("WAIT_CUSTOMER_CONFIRMATION", application.getFulfillmentStatus());
|
||||
assertEquals("COMPLETED", order.getRelabelStatus());
|
||||
assertEquals("RELABEL:CUSTOMER_CONFIRM_PENDING", order.getSubOrderStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void completedApplicationCannotReturnToProcessingAndOtherTenantCannotReadIt() {
|
||||
RelabelApplication completed = application(11L, "FBR-1", "10001", "key-1", "hash", "COMPLETED");
|
||||
@@ -466,7 +509,7 @@ class RelabelApplicationServiceTest {
|
||||
var review = ArgumentCaptor.forClass(com.baiyue.lastleg.relabel.pda.entity.PdaRelabelTask.class);
|
||||
verify(pdaTasks).updateWithVersion(review.capture(), eq(1));
|
||||
assertEquals("COMPLETED", review.getValue().getStatus());
|
||||
assertEquals("CUSTOMER_CONFIRMED", review.getValue().getReviewDecision());
|
||||
assertEquals("CONFIRMED", review.getValue().getReviewDecision());
|
||||
assertNull(application.getOutboundOrderNo());
|
||||
verify(outboundOrders, never()).insert(any(OutboundOrder.class));
|
||||
|
||||
@@ -475,12 +518,36 @@ class RelabelApplicationServiceTest {
|
||||
verify(applications, times(1)).updateById(application);
|
||||
}
|
||||
|
||||
@Test
|
||||
void skuOnlyCustomerConfirmationCompletesWithoutPalletOrOutboundFulfillment() {
|
||||
RelabelApplication application = confirmationApplication(null, false);
|
||||
RelabelItem skuOnly = item(31L, application.getId(), "SKU-A", 1, 1);
|
||||
skuOnly.setOperationType("SKU_LABEL");
|
||||
LastlegOrder linked = order(21L, "10001", "LMO-RELABEL-1", "WAIT_RELABEL");
|
||||
when(applications.lockNo("10001", "FBR-1")).thenReturn(application);
|
||||
when(applications.findNo("10001", "FBR-1")).thenReturn(application);
|
||||
when(items.selectList(any())).thenReturn(List.of(skuOnly));
|
||||
when(histories.selectList(any())).thenReturn(List.of());
|
||||
when(orders.selectById(21L)).thenReturn(linked);
|
||||
|
||||
service.confirmCompletion("10001", "FBR-1", "customer-1",
|
||||
new RelabelRequests.CustomerConfirm("confirm-sku", "accepted"));
|
||||
|
||||
assertEquals("COMPLETED", application.getStatus());
|
||||
assertEquals("NOT_REQUIRED", application.getFulfillmentStatus());
|
||||
assertEquals("COMPLETED", linked.getCurrentStatus());
|
||||
assertNull(application.getOutboundOrderNo());
|
||||
verify(outboundOrders, never()).insert(any(OutboundOrder.class));
|
||||
verify(outboundItems, never()).insert(any(OutboundPalletItem.class));
|
||||
verify(outboundTasks, never()).insert(any(OutboundPdaTask.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void transferRelabelConfirmationReleasesTransferForDispatch() {
|
||||
RelabelApplication application = confirmationApplication("TRUCK", false);
|
||||
WarehouseTransferOrder transfer = new WarehouseTransferOrder();
|
||||
transfer.setId(77L); transfer.setCustomerId("10001"); transfer.setRelabelNo("FBR-1");
|
||||
transfer.setStatus("WAIT_RELABEL"); transfer.setLastlegOrderId(88L);
|
||||
transfer.setStatus("WAIT_RELABEL"); transfer.setLastlegOrderId(88L); transfer.setFbaWarehouseCode("ONT8");
|
||||
LastlegOrder transferOrder = order(88L, "10001", "LMO-TRANSFER-1", "WAIT_RELABEL");
|
||||
when(applications.lockNo("10001", "FBR-1")).thenReturn(application);
|
||||
stubDetail(application);
|
||||
@@ -498,6 +565,55 @@ class RelabelApplicationServiceTest {
|
||||
verify(orders).updateById(transferOrder);
|
||||
}
|
||||
|
||||
@Test
|
||||
void transferRelabelConfirmationWithoutDestinationWaitsForFbaInsteadOfDispatch() {
|
||||
RelabelApplication application = confirmationApplication("TRUCK", false);
|
||||
WarehouseTransferOrder transfer = new WarehouseTransferOrder();
|
||||
transfer.setId(77L); transfer.setCustomerId("10001"); transfer.setRelabelNo("FBR-1");
|
||||
transfer.setStatus("WAIT_RELABEL"); transfer.setLastlegOrderId(88L);
|
||||
LastlegOrder transferOrder = order(88L, "10001", "LMO-TRANSFER-1", "WAIT_RELABEL");
|
||||
when(applications.lockNo("10001", "FBR-1")).thenReturn(application);
|
||||
stubDetail(application);
|
||||
when(warehouseTransfers.lockByRelabelNo("10001", "FBR-1")).thenReturn(transfer);
|
||||
when(orders.selectById(88L)).thenReturn(transferOrder);
|
||||
|
||||
service.confirmCompletion("10001", "FBR-1", "customer-1",
|
||||
new RelabelRequests.CustomerConfirm("confirm-transfer-no-fba", "accepted"));
|
||||
|
||||
assertEquals("WAIT_FBA_DESTINATION", transfer.getStatus());
|
||||
assertEquals("WAIT_FBA_DESTINATION", transferOrder.getCurrentStatus());
|
||||
assertEquals("WAREHOUSE_TRANSFER:WAIT_FBA_DESTINATION;RELABEL:COMPLETED",
|
||||
transferOrder.getSubOrderStatus());
|
||||
assertEquals("WAIT_FBA_DESTINATION", application.getFulfillmentStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void transferRelabelConfirmationDerivesUniqueDestinationFromRelabelItems() {
|
||||
RelabelApplication application = confirmationApplication("TRUCK", false);
|
||||
WarehouseTransferOrder transfer = new WarehouseTransferOrder();
|
||||
transfer.setId(77L); transfer.setCustomerId("10001"); transfer.setRelabelNo("FBR-1");
|
||||
transfer.setStatus("WAIT_RELABEL"); transfer.setLastlegOrderId(88L);
|
||||
LastlegOrder transferOrder = order(88L, "10001", "LMO-TRANSFER-1", "WAIT_RELABEL");
|
||||
RelabelItem relabelItem = item(31L, application.getId(), "SKU-A", 10, 6);
|
||||
relabelItem.setDestinationFbaWarehouseCode("ONT8");
|
||||
when(applications.lockNo("10001", "FBR-1")).thenReturn(application);
|
||||
when(applications.findNo("10001", "FBR-1")).thenReturn(application);
|
||||
when(items.selectList(any())).thenReturn(List.of(relabelItem));
|
||||
when(histories.selectList(any())).thenReturn(List.of());
|
||||
when(warehouseTransfers.lockByRelabelNo("10001", "FBR-1")).thenReturn(transfer);
|
||||
when(orders.selectById(88L)).thenReturn(transferOrder);
|
||||
when(orders.selectById(application.getLastlegOrderId())).thenReturn(
|
||||
order(application.getLastlegOrderId(), "10001", "LMO-RELABEL-1", application.getStatus()));
|
||||
|
||||
service.confirmCompletion("10001", "FBR-1", "customer-1",
|
||||
new RelabelRequests.CustomerConfirm("confirm-transfer-derived-fba", "accepted"));
|
||||
|
||||
assertEquals("ONT8", transfer.getFbaWarehouseCode());
|
||||
assertEquals("READY_FOR_DISPATCH", transfer.getStatus());
|
||||
assertEquals("ONT8", transferOrder.getFbaWarehouseCode());
|
||||
assertEquals("READY_FOR_DISPATCH", transferOrder.getCurrentStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void expressCustomerConfirmationRequiresShippingLabelWhenWarehousePrintingIsSelected() {
|
||||
RelabelApplication application = confirmationApplication("EXPRESS", true);
|
||||
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package com.baiyue.lastleg.relabel.service;
|
||||
|
||||
import com.baiyue.lastleg.relabel.entity.*;
|
||||
import com.baiyue.lastleg.relabel.mapper.RelabelItemMapper;
|
||||
import com.baiyue.lastleg.relabel.pda.entity.PdaRelabelHistory;
|
||||
import com.baiyue.lastleg.relabel.pda.mapper.PdaRelabelHistoryMapper;
|
||||
import com.baiyue.lastleg.inventory.entity.InventoryPosition;
|
||||
import com.baiyue.lastleg.inventory.mapper.InventoryPositionMapper;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import java.util.List;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class RelabelBindingGuardTest {
|
||||
@Test void physicalBindingMustMatchLatestTargetAndCannotSurviveRework() {
|
||||
var guard = new RelabelBindingGuard();
|
||||
var items = mock(RelabelItemMapper.class);
|
||||
var history = mock(PdaRelabelHistoryMapper.class);
|
||||
var positions = mock(InventoryPositionMapper.class);
|
||||
ReflectionTestUtils.setField(guard, "items", items);
|
||||
ReflectionTestUtils.setField(guard, "histories", history);
|
||||
ReflectionTestUtils.setField(guard, "positions", positions);
|
||||
ReflectionTestUtils.setField(guard, "objectMapper", new ObjectMapper());
|
||||
var app = new RelabelApplication(); app.setId(1L); app.setCustomerId("2");
|
||||
var item = new RelabelItem(); item.setCartonMark("A"); item.setOperationType("CARTON_LABEL");
|
||||
item.setSourceInventoryPositionId(3L); item.setNotifiedPackageCount(1);
|
||||
when(items.selectList(any())).thenReturn(List.of(item));
|
||||
var position = new InventoryPosition(); position.setPalletNo("OLD"); position.setHoldPackages(1L);
|
||||
when(positions.findTenantPosition("2", 3L)).thenReturn(position);
|
||||
when(history.selectList(any())).thenReturn(List.of());
|
||||
assertThrows(ResponseStatusException.class, () -> guard.requireBound(app));
|
||||
var binding = new PdaRelabelHistory(); binding.setAction("PALLET_BIND");
|
||||
binding.setDetailJson("{\"cartonMarks\":[\"A\"],\"targetPallets\":[\"NEW\"]}");
|
||||
when(history.selectList(any())).thenReturn(List.of(binding));
|
||||
assertThrows(ResponseStatusException.class, () -> guard.requireBound(app));
|
||||
position.setPalletNo("NEW");
|
||||
assertDoesNotThrow(() -> guard.requireBound(app));
|
||||
var rework = new PdaRelabelHistory(); rework.setAction("REWORK_ARCHIVE");
|
||||
when(history.selectList(any())).thenReturn(List.of(binding, rework));
|
||||
assertThrows(ResponseStatusException.class, () -> guard.requireBound(app));
|
||||
item.setOperationType("SKU_LABEL");
|
||||
assertDoesNotThrow(() -> guard.requireBound(app));
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package com.baiyue.lastleg.relabel.service;
|
||||
|
||||
import com.baiyue.lastleg.inventory.mapper.InventoryBalanceMapper;
|
||||
import com.baiyue.lastleg.inventory.mapper.InventoryPositionMapper;
|
||||
import com.baiyue.lastleg.relabel.dto.RelabelResponses;
|
||||
import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCarton;
|
||||
import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCartonItem;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonItemMapper;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class RelabelReturnCartonCandidateTest {
|
||||
@Test
|
||||
void candidatesIncludeAvailableMergedReturnCartonByWarehouseMark() {
|
||||
RelabelApplicationService service = new RelabelApplicationService();
|
||||
InventoryBalanceMapper balances = mock(InventoryBalanceMapper.class);
|
||||
InventoryPositionMapper positions = mock(InventoryPositionMapper.class);
|
||||
ReturnInventoryCartonMapper cartons = mock(ReturnInventoryCartonMapper.class);
|
||||
ReturnInventoryCartonItemMapper cartonItems = mock(ReturnInventoryCartonItemMapper.class);
|
||||
ReflectionTestUtils.setField(service, "inventoryBalances", balances);
|
||||
ReflectionTestUtils.setField(service, "inventoryPositions", positions);
|
||||
ReflectionTestUtils.setField(service, "returnCartons", cartons);
|
||||
ReflectionTestUtils.setField(service, "returnCartonItems", cartonItems);
|
||||
|
||||
ReturnInventoryCarton carton = new ReturnInventoryCarton();
|
||||
carton.setId(9001L); carton.setCustomerId("10001"); carton.setOverseasWarehouseId("WH-1");
|
||||
carton.setCartonMark("RC-WH-0001"); carton.setSourceReturnNo("RT-1,RT-2");
|
||||
carton.setStatus("AVAILABLE"); carton.setTotalQuantity(12L);
|
||||
ReturnInventoryCartonItem cartonItem = new ReturnInventoryCartonItem();
|
||||
cartonItem.setCartonId(9001L); cartonItem.setSkuCode("SKU-RETURN");
|
||||
cartonItem.setProductNameZh("退货商品"); cartonItem.setQuantity(12L);
|
||||
when(cartons.findAvailable("10001", "WH-1")).thenReturn(List.of(carton));
|
||||
when(cartonItems.listCarton(9001L)).thenReturn(List.of(cartonItem));
|
||||
|
||||
List<RelabelResponses.InventoryCandidate> result = service.candidates("10001", "WH-1", "RC-WH");
|
||||
|
||||
assertEquals(1, result.size());
|
||||
assertEquals("RETURN_CARTON", result.get(0).sourceType());
|
||||
assertEquals(9001L, result.get(0).returnCartonId());
|
||||
assertEquals("RC-WH-0001", result.get(0).cartonMark());
|
||||
assertEquals("SKU-RETURN", result.get(0).skuItems().get(0).skuCode());
|
||||
assertEquals(12, result.get(0).skuItems().get(0).quantity());
|
||||
}
|
||||
}
|
||||
+22
@@ -11,6 +11,8 @@ class WarehouseTransferSchemaContractTest {
|
||||
void baselineAndIncrementalContainTransferAndHistoricalOrderBindingTables() throws Exception {
|
||||
String baseline = Files.readString(Path.of("src", "main", "resources", "db", "lastleg.sql"), StandardCharsets.UTF_8).toLowerCase();
|
||||
String migration = Files.readString(Path.of("..", "scripts", "db", "incremental.sql"), StandardCharsets.UTF_8).toLowerCase();
|
||||
String deployment = Files.readString(Path.of("..", "deploy", "sql", "海外区域统一增量-MySQL8.sql"),
|
||||
StandardCharsets.UTF_8).toLowerCase();
|
||||
for (String table : new String[]{"ll_warehouse_transfer_order", "ll_warehouse_transfer_source_order",
|
||||
"ll_warehouse_transfer_item", "ll_warehouse_transfer_inventory_allocation",
|
||||
"ll_warehouse_transfer_receipt"}) {
|
||||
@@ -21,5 +23,25 @@ class WarehouseTransferSchemaContractTest {
|
||||
assertTrue(migration.contains("uk_ll_wh_transfer_source"));
|
||||
assertTrue(migration.contains("uk_ll_wh_transfer_active_position"));
|
||||
assertTrue(migration.contains("uk_ll_wh_transfer_receipt_source"));
|
||||
assertTrue(baseline.contains("pickup_contact_name varchar(128) null"));
|
||||
assertTrue(baseline.contains("pickup_contact_phone varchar(64) null"));
|
||||
assertTrue(migration.contains("column_name='pickup_contact_name'"));
|
||||
assertTrue(migration.contains("column_name='pickup_contact_phone'"));
|
||||
assertTrue(baseline.contains("create table if not exists ll_warehouse_transfer_measurement_photo"));
|
||||
assertTrue(migration.contains("create table if not exists ll_warehouse_transfer_measurement_photo"));
|
||||
assertTrue(baseline.contains("received_package_count int null comment 'pda实际入库箱数'"));
|
||||
assertTrue(migration.contains("column_name='received_package_count'"));
|
||||
assertTrue(migration.contains("o.current_status='inbound_completed'"));
|
||||
for (String field : new String[]{"pickup_contact_name", "review_decision", "destination_type",
|
||||
"destination_binding_idempotency_key", "private_destination_address",
|
||||
"private_destination_postal_code", "draft_idempotency_key", "bind_idempotency_key"}) {
|
||||
assertTrue(deployment.contains(field), () -> "deployment SQL must include " + field);
|
||||
}
|
||||
for (String table : new String[]{"ll_warehouse_transfer_measurement_photo", "ll_inbound_plan_line",
|
||||
"ll_inbound_plan_pallet", "ll_dispatch_item", "ll_delivery_order"}) {
|
||||
assertTrue(deployment.contains(table), () -> "deployment SQL must include " + table);
|
||||
}
|
||||
assertTrue(deployment.contains("assert_relabel_transfer_latest_schema_ready"));
|
||||
assertTrue(deployment.contains("贴标申请与仓储转运最新表结构已完整部署"));
|
||||
}
|
||||
}
|
||||
|
||||
+19
@@ -31,6 +31,25 @@ class MerchantWarehouseTransferControllerTest {
|
||||
verify(service).detail("100", "TR001");
|
||||
}
|
||||
|
||||
@Test
|
||||
void merchantSubmitUsesOnlyAuthenticatedTenant() {
|
||||
controller.submit(user(100L, "MERCHANT"), "TR001");
|
||||
verify(service).submit("100", "TR001", "1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void merchantUpdateUsesOnlyAuthenticatedTenant() {
|
||||
WarehouseTransferRequests.Create request = mock(WarehouseTransferRequests.Create.class);
|
||||
controller.updateDraft(user(100L, "MERCHANT"), "TR001", request);
|
||||
verify(service).updateDraft("100", "TR001", "1", request);
|
||||
}
|
||||
|
||||
@Test
|
||||
void merchantPrintsSubmittedCartonLabelsWithinAuthenticatedTenant() {
|
||||
controller.cartonLabel(user(100L, "MERCHANT"), "TR001");
|
||||
verify(service).cartonLabel("100", "TR001");
|
||||
}
|
||||
|
||||
@Test
|
||||
void merchantEndpointRejectsPlatformIdentity() {
|
||||
assertThrows(ResponseStatusException.class,
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package com.baiyue.lastleg.transfer.controller;
|
||||
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import com.baiyue.lastleg.transfer.dto.WarehouseTransferRequests;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class PdaWarehouseTransferControllerContractTest {
|
||||
@Test
|
||||
void detailUsesOnlyAuthenticatedPdaWarehouseScopeAndTransferNumber() throws Exception {
|
||||
var method = PdaWarehouseTransferController.class
|
||||
.getMethod("detail", AuthenticatedUser.class, String.class);
|
||||
|
||||
assertThat(method.getAnnotation(GetMapping.class).value()).containsExactly("/{transferNo}");
|
||||
assertThat(method.getParameterCount()).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void measurementPhotoUploadUsesThePublicPdaRouteAndFileMultipartPart() throws Exception {
|
||||
var method = PdaWarehouseTransferController.class.getMethod("uploadMeasurementPhoto",
|
||||
AuthenticatedUser.class, String.class, MultipartFile.class);
|
||||
|
||||
var mapping = method.getAnnotation(PostMapping.class);
|
||||
assertThat(mapping.value()).containsExactly("/{transferNo}/measurement-photos/files");
|
||||
assertThat(mapping.consumes()).containsExactly("multipart/form-data");
|
||||
assertThat(method.getParameters()[2].getAnnotation(RequestParam.class).value()).isEqualTo("file");
|
||||
}
|
||||
|
||||
@Test
|
||||
void palletLabelUsesThePdaScopedTransferAndGeneratedWarehousePallet() throws Exception {
|
||||
var method = PdaWarehouseTransferController.class.getMethod("palletLabel",
|
||||
AuthenticatedUser.class, String.class, long.class, String.class);
|
||||
|
||||
assertThat(method.getAnnotation(GetMapping.class).value())
|
||||
.containsExactly("/{transferNo}/pallets/{warehousePalletId}/label");
|
||||
assertThat(method.getParameters()[3].getAnnotation(RequestHeader.class).value()).isEqualTo("X-Request-Id");
|
||||
}
|
||||
|
||||
@Test
|
||||
void palletLifecycleExposesDraftGenerateBindAndDraftDeleteRoutes() throws Exception {
|
||||
var draft = PdaWarehouseTransferController.class.getMethod("savePalletDraft",
|
||||
AuthenticatedUser.class, String.class, WarehouseTransferRequests.PalletDraft.class);
|
||||
var generate = PdaWarehouseTransferController.class.getMethod("generatePallet",
|
||||
AuthenticatedUser.class, String.class, long.class, WarehouseTransferRequests.PalletAction.class);
|
||||
var bind = PdaWarehouseTransferController.class.getMethod("bindPallet",
|
||||
AuthenticatedUser.class, String.class, long.class, WarehouseTransferRequests.PalletBind.class);
|
||||
var delete = PdaWarehouseTransferController.class.getMethod("deletePalletDraft",
|
||||
AuthenticatedUser.class, String.class, long.class);
|
||||
|
||||
assertThat(draft.getAnnotation(PostMapping.class).value()).containsExactly("/{transferNo}/pallets/drafts");
|
||||
assertThat(generate.getAnnotation(PostMapping.class).value()).containsExactly("/{transferNo}/pallets/{palletId}/generate");
|
||||
assertThat(bind.getAnnotation(PostMapping.class).value()).containsExactly("/{transferNo}/pallets/{palletId}/bind");
|
||||
assertThat(delete.getAnnotation(DeleteMapping.class).value()).containsExactly("/{transferNo}/pallets/{palletId}");
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package com.baiyue.lastleg.transfer.dto;
|
||||
|
||||
import jakarta.validation.Validation;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class WarehouseTransferCargoValidationTest {
|
||||
@Test
|
||||
void transferCargoAllowsSkuAndProductNameToBeOmitted() {
|
||||
try (var factory = Validation.buildDefaultValidatorFactory()) {
|
||||
var violations = factory.getValidator().validate(
|
||||
new WarehouseTransferRequests.Item(null, null, 1, null));
|
||||
|
||||
assertTrue(violations.isEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void cartonRelabelAllowsOriginalSkuToBeOmitted() {
|
||||
try (var factory = Validation.buildDefaultValidatorFactory()) {
|
||||
var violations = factory.getValidator().validate(new WarehouseTransferRequests.RelabelItem(
|
||||
"CARTON_LABEL", List.of("CUSTOMER-CARTON-1"), null, null, 1,
|
||||
"FBA-CARTON-1", "ONT8", null));
|
||||
|
||||
assertTrue(violations.isEmpty());
|
||||
}
|
||||
}
|
||||
}
|
||||
+19
-7
@@ -11,12 +11,12 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class WarehouseTransferPalletValidationTest {
|
||||
@Test
|
||||
void palletAcceptsDirectTotalVolumeAndWeightWithoutCartonDimensions() {
|
||||
void palletAcceptsMeasuredDimensionsVolumeAndWeight() {
|
||||
try (var factory = Validation.buildDefaultValidatorFactory()) {
|
||||
var validator = factory.getValidator();
|
||||
var pallet = new WarehouseTransferRequests.Pallet("PALLET-1", 5,
|
||||
new BigDecimal("1.250000"), new BigDecimal("420.50"),
|
||||
List.of(new WarehouseTransferRequests.PalletItem(101L, 5)));
|
||||
var pallet = new WarehouseTransferRequests.Pallet(5,
|
||||
new BigDecimal("100"), new BigDecimal("100"), new BigDecimal("125"),
|
||||
new BigDecimal("1.250000"), new BigDecimal("420.50"));
|
||||
|
||||
assertTrue(validator.validate(pallet).isEmpty());
|
||||
assertEquals(new BigDecimal("1.250000"), pallet.volumeCbm());
|
||||
@@ -28,11 +28,23 @@ class WarehouseTransferPalletValidationTest {
|
||||
void palletRejectsNonPositiveTotalVolume() {
|
||||
try (var factory = Validation.buildDefaultValidatorFactory()) {
|
||||
var validator = factory.getValidator();
|
||||
var pallet = new WarehouseTransferRequests.Pallet("PALLET-1", 5,
|
||||
BigDecimal.ZERO, new BigDecimal("420.50"),
|
||||
List.of(new WarehouseTransferRequests.PalletItem(101L, 5)));
|
||||
var pallet = new WarehouseTransferRequests.Pallet(5,
|
||||
new BigDecimal("100"), new BigDecimal("100"), new BigDecimal("125"),
|
||||
BigDecimal.ZERO, new BigDecimal("420.50"));
|
||||
|
||||
assertEquals(1, validator.validate(pallet).size());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void palletRejectsMissingOrNonPositiveDimensions() {
|
||||
try (var factory = Validation.buildDefaultValidatorFactory()) {
|
||||
var validator = factory.getValidator();
|
||||
var pallet = new WarehouseTransferRequests.Pallet(5,
|
||||
BigDecimal.ZERO, null, new BigDecimal("125"),
|
||||
new BigDecimal("1.250000"), new BigDecimal("420.50"));
|
||||
|
||||
assertEquals(2, validator.validate(pallet).size());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package com.baiyue.lastleg.transfer.gateway;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class HttpTransferMeasurementPhotoFileGatewayContractTest {
|
||||
@Test
|
||||
void defaultsToTheFileServicesActualLocalPort() throws Exception {
|
||||
var field = HttpTransferMeasurementPhotoFileGateway.class.getDeclaredField("baseUrl");
|
||||
|
||||
assertThat(field.getAnnotation(Value.class).value())
|
||||
.isEqualTo("${baiyue.clients.file.url:http://127.0.0.1:9206}");
|
||||
}
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
package com.baiyue.lastleg.transfer.service;
|
||||
|
||||
import com.baiyue.lastleg.transfer.dto.WarehouseTransferRequests;
|
||||
import com.baiyue.lastleg.transfer.entity.WarehouseTransferItem;
|
||||
import com.baiyue.lastleg.transfer.entity.WarehouseTransferPallet;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class WarehouseTransferPalletAllocationTest {
|
||||
@Test
|
||||
void allocatesMultipleDeclaredSkusAcrossPalletsInStableDeclaredOrder() {
|
||||
WarehouseTransferItem first = item(11L, 3);
|
||||
WarehouseTransferItem second = item(12L, 4);
|
||||
var allocations = WarehouseTransferService.derivePalletAllocations(
|
||||
List.of(first, second), List.of(pallet(2), pallet(5)));
|
||||
|
||||
assertEquals(List.of(
|
||||
new WarehouseTransferService.DerivedPalletAllocation(0, 11L, 2),
|
||||
new WarehouseTransferService.DerivedPalletAllocation(1, 11L, 1),
|
||||
new WarehouseTransferService.DerivedPalletAllocation(1, 12L, 4)), allocations);
|
||||
}
|
||||
|
||||
@Test
|
||||
void underReceiptConsumesDeclaredSkusInStableOrder() {
|
||||
var allocations = WarehouseTransferService.derivePalletAllocations(
|
||||
List.of(item(11L, 3), item(12L, 2)), List.of(pallet(4)));
|
||||
|
||||
assertEquals(List.of(
|
||||
new WarehouseTransferService.DerivedPalletAllocation(0, 11L, 3),
|
||||
new WarehouseTransferService.DerivedPalletAllocation(0, 12L, 1)), allocations);
|
||||
assertEquals(4, allocations.stream().mapToInt(value -> value.packageCount()).sum());
|
||||
}
|
||||
|
||||
@Test
|
||||
void overReceiptAssignsExcessToFinalDeclaredSkuAcrossPallets() {
|
||||
var allocations = WarehouseTransferService.derivePalletAllocations(
|
||||
List.of(item(11L, 3), item(12L, 2)), List.of(pallet(4), pallet(3)));
|
||||
|
||||
assertEquals(List.of(
|
||||
new WarehouseTransferService.DerivedPalletAllocation(0, 11L, 3),
|
||||
new WarehouseTransferService.DerivedPalletAllocation(0, 12L, 1),
|
||||
new WarehouseTransferService.DerivedPalletAllocation(1, 12L, 3)), allocations);
|
||||
assertEquals(7, allocations.stream().mapToInt(value -> value.packageCount()).sum());
|
||||
}
|
||||
|
||||
@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());
|
||||
}
|
||||
|
||||
@Test
|
||||
void requestsFreshPalletNumbersAndCarriesOnlyTheKnownFbaDestination() {
|
||||
var known = WarehouseTransferService.newTransferPalletRequests(81L, "WTR-1", "ONT8", 2);
|
||||
var unknown = WarehouseTransferService.newTransferPalletRequests(81L, "WTR-2", null, 1);
|
||||
|
||||
assertEquals(List.of("WTR-1:1", "WTR-1:2"), known.stream().map(value -> value.businessKey()).toList());
|
||||
assertEquals(List.of("ONT8", "ONT8"), known.stream().map(value -> value.fbaWarehouseCode()).toList());
|
||||
assertEquals(true, known.stream().allMatch(value -> value.palletCode() == null));
|
||||
assertEquals(null, unknown.get(0).fbaWarehouseCode());
|
||||
assertEquals(null, unknown.get(0).palletCode());
|
||||
}
|
||||
|
||||
private static WarehouseTransferItem item(long id, int packages) {
|
||||
WarehouseTransferItem item = new WarehouseTransferItem();
|
||||
item.setId(id); item.setPackageCount(packages);
|
||||
return item;
|
||||
}
|
||||
|
||||
private static WarehouseTransferRequests.Pallet pallet(int cartons) {
|
||||
return new WarehouseTransferRequests.Pallet(cartons, BigDecimal.ONE, BigDecimal.ONE,
|
||||
BigDecimal.ONE, new BigDecimal("0.000001"), BigDecimal.ONE);
|
||||
}
|
||||
|
||||
private static com.baiyue.lastleg.transfer.entity.WarehouseTransferPallet persistedPallet(int cartons) {
|
||||
var pallet = new com.baiyue.lastleg.transfer.entity.WarehouseTransferPallet();
|
||||
pallet.setCartonCount(cartons);
|
||||
return pallet;
|
||||
}
|
||||
}
|
||||
+393
-12
@@ -1,6 +1,7 @@
|
||||
package com.baiyue.lastleg.transfer.service;
|
||||
|
||||
import com.baiyue.lastleg.entity.LastlegOrder;
|
||||
import com.baiyue.lastleg.entity.LastlegOrderDetail;
|
||||
import com.baiyue.lastleg.entity.LastlegInboundOrder;
|
||||
import com.baiyue.lastleg.relabel.entity.RelabelApplication;
|
||||
import com.baiyue.lastleg.inventory.command.dto.InventoryCommandResponse;
|
||||
@@ -19,11 +20,27 @@ import com.baiyue.lastleg.transfer.entity.WarehouseTransferOrder;
|
||||
import com.baiyue.lastleg.transfer.entity.WarehouseTransferCarton;
|
||||
import com.baiyue.lastleg.transfer.entity.WarehouseTransferInventoryAllocation;
|
||||
import com.baiyue.lastleg.transfer.entity.WarehouseTransferSourceOrder;
|
||||
import com.baiyue.lastleg.transfer.entity.WarehouseTransferMeasurementPhoto;
|
||||
import com.baiyue.lastleg.transfer.entity.WarehouseTransferPallet;
|
||||
import com.baiyue.lastleg.transfer.entity.WarehouseTransferItem;
|
||||
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferPalletMapper;
|
||||
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferPalletItemMapper;
|
||||
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferRelabelBindingMapper;
|
||||
import com.baiyue.lastleg.inboundplan.pda.gateway.WarehouseResourceGateway;
|
||||
import com.baiyue.lastleg.inboundplan.pda.mapper.InboundStockReceiptMapper;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlan;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlanLine;
|
||||
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanMapper;
|
||||
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanLineMapper;
|
||||
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanPalletMapper;
|
||||
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanPalletAllocationMapper;
|
||||
import com.baiyue.lastleg.transfer.mapper.InboundSnapshotMapper;
|
||||
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferItemMapper;
|
||||
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferCartonMapper;
|
||||
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferInventoryAllocationMapper;
|
||||
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferOrderMapper;
|
||||
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferMeasurementPhotoMapper;
|
||||
import com.baiyue.lastleg.transfer.gateway.TransferMeasurementPhotoFileGateway;
|
||||
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferReceiptMapper;
|
||||
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferSourceOrderMapper;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
@@ -62,14 +79,36 @@ import static org.mockito.Mockito.times;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class WarehouseTransferServiceTest {
|
||||
@Test
|
||||
void customerMainStatusFiltersExpandToInternalWorkflowStates() {
|
||||
assertEquals(List.of(WarehouseTransferService.APPROVED, WarehouseTransferService.WAIT_PDA_RECEIVING),
|
||||
WarehouseTransferService.customerMainStatuses(WarehouseTransferService.INBOUND_MEASURING));
|
||||
assertTrue(WarehouseTransferService.customerMainStatuses(WarehouseTransferService.INBOUND_COMPLETED)
|
||||
.containsAll(List.of(WarehouseTransferService.WAIT_CUSTOMER_CONFIRMATION,
|
||||
WarehouseTransferService.WAIT_FBA_DESTINATION, WarehouseTransferService.WAIT_RELABEL,
|
||||
WarehouseTransferService.READY_FOR_DISPATCH, WarehouseTransferService.OUTBOUNDED)));
|
||||
assertNull(WarehouseTransferService.customerMainStatuses(WarehouseTransferService.DRAFT));
|
||||
}
|
||||
|
||||
@Mock WarehouseTransferOrderMapper transfers;
|
||||
@Mock WarehouseTransferItemMapper transferItems;
|
||||
@Mock WarehouseTransferCartonMapper transferCartons;
|
||||
@Mock WarehouseTransferSourceOrderMapper sourceOrders;
|
||||
@Mock WarehouseTransferInventoryAllocationMapper inventoryAllocations;
|
||||
@Mock WarehouseTransferReceiptMapper transferReceipts;
|
||||
@Mock WarehouseTransferMeasurementPhotoMapper measurementPhotos;
|
||||
@Mock WarehouseTransferPalletMapper transferPallets;
|
||||
@Mock WarehouseTransferPalletItemMapper transferPalletItems;
|
||||
@Mock InboundStockReceiptMapper stockReceipts;
|
||||
@Mock WarehouseTransferRelabelBindingMapper relabelBindings;
|
||||
@Mock WarehouseResourceGateway warehouseResources;
|
||||
@Mock TransferMeasurementPhotoFileGateway measurementPhotoFiles;
|
||||
@Mock InboundSnapshotMapper inboundSnapshots;
|
||||
@Mock LastlegInboundOrderMapper inboundOrders;
|
||||
@Mock InboundPlanMapper inboundPlans;
|
||||
@Mock InboundPlanLineMapper inboundPlanLines;
|
||||
@Mock InboundPlanPalletMapper inboundPlanPallets;
|
||||
@Mock InboundPlanPalletAllocationMapper inboundPlanAllocations;
|
||||
@Mock LastlegOrderMapper orders;
|
||||
@Mock LastlegOrderDetailMapper orderDetails;
|
||||
@Mock LastlegOrderStatusHistoryMapper orderHistories;
|
||||
@@ -88,8 +127,19 @@ class WarehouseTransferServiceTest {
|
||||
ReflectionTestUtils.setField(service, "sourceOrders", sourceOrders);
|
||||
ReflectionTestUtils.setField(service, "inventoryAllocations", inventoryAllocations);
|
||||
ReflectionTestUtils.setField(service, "transferReceipts", transferReceipts);
|
||||
ReflectionTestUtils.setField(service, "measurementPhotos", measurementPhotos);
|
||||
ReflectionTestUtils.setField(service, "transferPallets", transferPallets);
|
||||
ReflectionTestUtils.setField(service, "transferPalletItems", transferPalletItems);
|
||||
ReflectionTestUtils.setField(service, "stockReceipts", stockReceipts);
|
||||
ReflectionTestUtils.setField(service, "relabelBindings", relabelBindings);
|
||||
ReflectionTestUtils.setField(service, "warehouseResources", warehouseResources);
|
||||
ReflectionTestUtils.setField(service, "measurementPhotoFiles", measurementPhotoFiles);
|
||||
ReflectionTestUtils.setField(service, "inboundSnapshots", inboundSnapshots);
|
||||
ReflectionTestUtils.setField(service, "inboundOrders", inboundOrders);
|
||||
ReflectionTestUtils.setField(service, "inboundPlans", inboundPlans);
|
||||
ReflectionTestUtils.setField(service, "inboundPlanLines", inboundPlanLines);
|
||||
ReflectionTestUtils.setField(service, "inboundPlanPallets", inboundPlanPallets);
|
||||
ReflectionTestUtils.setField(service, "inboundPlanAllocations", inboundPlanAllocations);
|
||||
ReflectionTestUtils.setField(service, "orders", orders);
|
||||
ReflectionTestUtils.setField(service, "orderDetails", orderDetails);
|
||||
ReflectionTestUtils.setField(service, "orderHistories", orderHistories);
|
||||
@@ -103,6 +153,116 @@ class WarehouseTransferServiceTest {
|
||||
lenient().when(inventoryAllocations.selectList(any())).thenReturn(List.of());
|
||||
lenient().when(transferReceipts.selectList(any())).thenReturn(List.of());
|
||||
lenient().when(transferCartons.listOrder(anyString(), anyLong())).thenReturn(List.of());
|
||||
lenient().when(measurementPhotos.listOrder(anyString(), anyLong())).thenReturn(List.of());
|
||||
lenient().when(measurementPhotos.listSubmitted(anyString(), anyLong())).thenReturn(List.of());
|
||||
lenient().when(transferPalletItems.listOrder(anyString(), anyLong())).thenReturn(List.of());
|
||||
lenient().when(relabelBindings.listOrder(anyString(), anyLong())).thenReturn(List.of());
|
||||
}
|
||||
|
||||
@Test
|
||||
void generatedPalletRetryWithSameKeyDoesNotGenerateAnotherPhysicalPallet() {
|
||||
WarehouseTransferOrder transfer = transfer(WarehouseTransferService.WAIT_PDA_RECEIVING);
|
||||
transfer.setOperatingWarehouseId("77");
|
||||
WarehouseTransferPallet pallet = measuredPallet("GENERATED");
|
||||
pallet.setGenerateIdempotencyKey("GEN-1");
|
||||
when(transfers.lockForPda("WTR-1", "77")).thenReturn(transfer);
|
||||
when(transferPallets.lockOne("TENANT-A", transfer.getId(), pallet.getId())).thenReturn(pallet);
|
||||
when(transfers.findNo("TENANT-A", "WTR-1")).thenReturn(transfer);
|
||||
when(transferItems.selectList(any())).thenReturn(List.of());
|
||||
when(sourceOrders.selectList(any())).thenReturn(List.of());
|
||||
when(transferPallets.listOrder("TENANT-A", transfer.getId())).thenReturn(List.of(pallet));
|
||||
|
||||
service.generatePallet(77L, "WTR-1", pallet.getId(), new WarehouseTransferRequests.PalletAction("GEN-1"));
|
||||
|
||||
verify(warehouseResources, never()).ensurePalletBatch(any());
|
||||
verify(warehouseResources, never()).confirmPalletBatch(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void scanBindingRejectsCodeOtherThanThisGeneratedPallet() {
|
||||
WarehouseTransferOrder transfer = transfer(WarehouseTransferService.WAIT_PDA_RECEIVING);
|
||||
WarehouseTransferPallet pallet = measuredPallet("GENERATED");
|
||||
when(transfers.lockForPda("WTR-1", "77")).thenReturn(transfer);
|
||||
when(transferPallets.lockOne("TENANT-A", transfer.getId(), pallet.getId())).thenReturn(pallet);
|
||||
|
||||
ResponseStatusException exception = assertThrows(ResponseStatusException.class, () -> service.bindPallet(
|
||||
77L, "WTR-1", pallet.getId(), new WarehouseTransferRequests.PalletBind("OTHER-PALLET", "BIND-1")));
|
||||
|
||||
assertEquals(409, exception.getStatusCode().value());
|
||||
verify(warehouseResources, never()).resolvePallet(anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void palletizeRejectsWhenAnyPersistedPalletIsNotBound() {
|
||||
WarehouseTransferOrder transfer = transfer(WarehouseTransferService.WAIT_PDA_RECEIVING);
|
||||
transfer.setOperatingWarehouseId("77");
|
||||
WarehouseTransferPallet pallet = measuredPallet("GENERATED");
|
||||
when(transfers.lockForPda("WTR-1", "77")).thenReturn(transfer);
|
||||
when(transferItems.selectList(any())).thenReturn(List.of(transferItem(51L, "SKU-1", 5)));
|
||||
WarehouseTransferMeasurementPhoto photo = new WarehouseTransferMeasurementPhoto();
|
||||
photo.setFileId(9001L);
|
||||
when(measurementPhotos.findFile("TENANT-A", transfer.getId(), 9001L)).thenReturn(photo);
|
||||
when(transferPallets.listOrder("TENANT-A", transfer.getId())).thenReturn(List.of(pallet));
|
||||
|
||||
ResponseStatusException exception = assertThrows(ResponseStatusException.class, () -> service.palletize(
|
||||
"OP-1", 77L, "WTR-1", new WarehouseTransferRequests.Palletize("DONE-1", List.of(9001L))));
|
||||
|
||||
assertEquals(409, exception.getStatusCode().value());
|
||||
verify(warehouseResources, never()).ensurePalletBatch(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void palletizeCompletesAfterEveryPalletIsBoundAndDoesNotGenerateAgain() {
|
||||
WarehouseTransferOrder transfer = transfer(WarehouseTransferService.WAIT_PDA_RECEIVING);
|
||||
transfer.setOperatingWarehouseId("77");
|
||||
transfer.setInboundCode("WTR-1");
|
||||
WarehouseTransferPallet pallet = measuredPallet("BOUND");
|
||||
WarehouseTransferItem item = transferItem(51L, "SKU-1", 5);
|
||||
LastlegOrder tail = tailOrder();
|
||||
LastlegInboundOrder inbound = new LastlegInboundOrder();
|
||||
inbound.setId(transfer.getInboundOrderId()); inbound.setCustomerId("TENANT-A");
|
||||
InboundPlan inboundPlan = new InboundPlan(); inboundPlan.setId(201L); inboundPlan.setStatus("PDA_PENDING");
|
||||
InboundPlanLine inboundLine = new InboundPlanLine(); inboundLine.setId(202L);
|
||||
when(transfers.lockForPda("WTR-1", "77")).thenReturn(transfer);
|
||||
when(transfers.findNo("TENANT-A", "WTR-1")).thenReturn(transfer);
|
||||
when(transferItems.selectList(any())).thenReturn(List.of(item));
|
||||
when(transferPallets.listOrder("TENANT-A", transfer.getId())).thenReturn(List.of(pallet));
|
||||
WarehouseTransferMeasurementPhoto photo = new WarehouseTransferMeasurementPhoto();
|
||||
photo.setFileId(9001L);
|
||||
when(measurementPhotos.findFile("TENANT-A", transfer.getId(), 9001L)).thenReturn(photo);
|
||||
when(measurementPhotos.markSubmitted(eq("TENANT-A"), eq(transfer.getId()), any(), any())).thenReturn(1);
|
||||
when(orders.selectById(transfer.getLastlegOrderId())).thenReturn(tail);
|
||||
when(inboundOrders.selectById(transfer.getInboundOrderId())).thenReturn(inbound);
|
||||
when(inboundPlans.findBySourceOrder("TENANT-A", transfer.getLastlegOrderId())).thenReturn(inboundPlan);
|
||||
when(inboundPlanLines.selectOne(any())).thenReturn(inboundLine);
|
||||
when(warehouseResources.requireActiveSharedWarehouse(77L)).thenReturn(
|
||||
new WarehouseResourceGateway.WarehouseReference(77L, "WH-77", "Warehouse 77", "ACTIVE"));
|
||||
when(inventory.stockIn(eq("TENANT-A"), eq("OP-1"), any())).thenReturn(
|
||||
new InventoryCommandResponse(1L, "STOCK_IN", 801L, 901L, 5, 0, 0, 0, false, false));
|
||||
when(sourceOrders.selectList(any())).thenReturn(List.of());
|
||||
|
||||
var result = service.palletize("OP-1", 77L, "WTR-1",
|
||||
new WarehouseTransferRequests.Palletize("DONE-1", List.of(9001L)));
|
||||
|
||||
assertEquals(WarehouseTransferService.WAIT_CUSTOMER_CONFIRMATION, result.status());
|
||||
assertEquals("COMPLETED", pallet.getStatus());
|
||||
assertEquals(WarehouseTransferService.INBOUND_COMPLETED, tail.getCurrentStatus());
|
||||
assertEquals(5, tail.getReceivedPackageCount());
|
||||
assertEquals("RECEIVED", inbound.getStatus());
|
||||
assertEquals(5, inbound.getPackageCount());
|
||||
assertEquals("COMPLETED", inboundPlan.getStatus());
|
||||
verify(transferPalletItems).insert(any(com.baiyue.lastleg.transfer.entity.WarehouseTransferPalletItem.class));
|
||||
verify(inventory).stockIn(eq("TENANT-A"), eq("OP-1"), any());
|
||||
verify(stockReceipts).insert(any(com.baiyue.lastleg.inboundplan.pda.entity.InboundStockReceipt.class));
|
||||
verify(inboundOrders).updateById(inbound);
|
||||
verify(inboundPlans).updateById(inboundPlan);
|
||||
verify(inboundPlanPallets).insert(any(com.baiyue.lastleg.inboundplan.entity.InboundPlanPallet.class));
|
||||
ArgumentCaptor<com.baiyue.lastleg.inboundplan.entity.InboundPlanPalletAllocation> allocation =
|
||||
ArgumentCaptor.forClass(com.baiyue.lastleg.inboundplan.entity.InboundPlanPalletAllocation.class);
|
||||
verify(inboundPlanAllocations).insert(allocation.capture());
|
||||
assertEquals(5, allocation.getValue().getPickedPackageCount());
|
||||
verify(warehouseResources, never()).ensurePalletBatch(any());
|
||||
verify(warehouseResources, never()).confirmPalletBatch(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -111,7 +271,7 @@ class WarehouseTransferServiceTest {
|
||||
LastlegOrder historical = historicalOrder();
|
||||
when(inboundSnapshots.findId("TENANT-A", "IN-1")).thenReturn(101L);
|
||||
when(orders.selectList(any())).thenReturn(List.of(historical));
|
||||
when(transfers.findNo(eq("TENANT-A"), org.mockito.ArgumentMatchers.anyString())).thenAnswer(invocation -> persistedTransfer());
|
||||
when(transfers.findNo(eq("TENANT-A"), org.mockito.ArgumentMatchers.anyString())).thenAnswer(invocation -> transfer(WarehouseTransferService.DRAFT));
|
||||
when(transferItems.selectList(any())).thenReturn(List.of());
|
||||
when(sourceOrders.selectList(any())).thenReturn(List.of());
|
||||
|
||||
@@ -120,23 +280,23 @@ class WarehouseTransferServiceTest {
|
||||
ArgumentCaptor<LastlegOrder> tailOrder = ArgumentCaptor.forClass(LastlegOrder.class);
|
||||
verify(orders).insert(tailOrder.capture());
|
||||
assertEquals(WarehouseTransferService.ORDER_TYPE, tailOrder.getValue().getOrderType());
|
||||
assertEquals(WarehouseTransferService.PENDING_REVIEW, tailOrder.getValue().getCurrentStatus());
|
||||
assertEquals("WAREHOUSE_TRANSFER:PENDING_REVIEW;RELABEL:PENDING_BINDING", tailOrder.getValue().getSubOrderStatus());
|
||||
assertEquals(WarehouseTransferService.DRAFT, tailOrder.getValue().getCurrentStatus());
|
||||
assertEquals("WAREHOUSE_TRANSFER:DRAFT;RELABEL:PENDING_BINDING", tailOrder.getValue().getSubOrderStatus());
|
||||
assertTrue(tailOrder.getValue().getOrderNo().matches("WT-WH-\\d{8}-\\d{6}"));
|
||||
assertNull(tailOrder.getValue().getTransferWarehouseId());
|
||||
verify(sourceOrders).insert(any(WarehouseTransferSourceOrder.class));
|
||||
verify(relabelApplications, never()).insert(any(RelabelApplication.class));
|
||||
assertNull(tailOrder.getValue().getRelabelOrderNo());
|
||||
assertEquals("PENDING_BINDING", tailOrder.getValue().getRelabelStatus());
|
||||
assertEquals(WarehouseTransferService.PENDING_REVIEW, result.status());
|
||||
assertEquals(WarehouseTransferService.PENDING_REVIEW, result.orderStatus());
|
||||
assertEquals(WarehouseTransferService.DRAFT, result.status());
|
||||
assertEquals(WarehouseTransferService.DRAFT, result.orderStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void transferWithoutRelabelDoesNotCreateRelabelDocument() {
|
||||
when(inboundSnapshots.findId("TENANT-A", "IN-1")).thenReturn(101L);
|
||||
when(orders.selectList(any())).thenReturn(List.of(historicalOrder()));
|
||||
when(transfers.findNo(eq("TENANT-A"), org.mockito.ArgumentMatchers.anyString())).thenAnswer(invocation -> persistedTransfer());
|
||||
when(transfers.findNo(eq("TENANT-A"), org.mockito.ArgumentMatchers.anyString())).thenAnswer(invocation -> transfer(WarehouseTransferService.DRAFT));
|
||||
when(transferItems.selectList(any())).thenReturn(List.of());
|
||||
when(sourceOrders.selectList(any())).thenReturn(List.of());
|
||||
|
||||
@@ -146,6 +306,27 @@ class WarehouseTransferServiceTest {
|
||||
assertNull(result.relabelNo());
|
||||
}
|
||||
|
||||
@Test
|
||||
void transferRelabelRejectsMultipleDestinationWarehouses() {
|
||||
WarehouseTransferRequests.Create request = new WarehouseTransferRequests.Create(
|
||||
null, null, "WH-1", null, "NONE", null, null, null,
|
||||
null, null, null, null, "123 Pickup Road", null, null, 2,
|
||||
new BigDecimal("0.1000"), "CUSTOMER_DELIVERY", LocalDateTime.of(2026, 9, 18, 10, 0),
|
||||
null, null, null, "BOX-1,BOX-2", null, true,
|
||||
LocalDateTime.of(2026, 9, 19, 10, 0), "Contact", "13800000000", null, "TRUCK",
|
||||
"single-destination-check", List.of(new WarehouseTransferRequests.Item("SKU-1", "Product", 2, null)),
|
||||
List.of(
|
||||
new WarehouseTransferRequests.RelabelItem("CARTON_LABEL", List.of("BOX-1"), "SKU-1", null, 1, "FBA-1", "ONT8", null),
|
||||
new WarehouseTransferRequests.RelabelItem("CARTON_LABEL", List.of("BOX-2"), "SKU-1", null, 1, "FBA-2", "ACY2", null)));
|
||||
|
||||
ResponseStatusException exception = assertThrows(ResponseStatusException.class,
|
||||
() -> service.create("TENANT-A", "USER-1", request));
|
||||
|
||||
assertEquals(400, exception.getStatusCode().value());
|
||||
assertEquals("Warehouse transfer relabel only supports one destination warehouse per order", exception.getReason());
|
||||
verifyNoInteractions(transfers);
|
||||
}
|
||||
|
||||
@Test
|
||||
void transferCanEnterTheTransitWarehouseBeforeFbaDestinationIsKnown() {
|
||||
WarehouseTransferRequests.Create request = new WarehouseTransferRequests.Create(
|
||||
@@ -155,7 +336,7 @@ class WarehouseTransferServiceTest {
|
||||
List.of(new WarehouseTransferRequests.Item("SKU-1", "Product", 5, "MARK-1")));
|
||||
when(inboundSnapshots.findId("TENANT-A", "IN-1")).thenReturn(101L);
|
||||
when(orders.selectList(any())).thenReturn(List.of(historicalOrder()));
|
||||
when(transfers.findNo(eq("TENANT-A"), org.mockito.ArgumentMatchers.anyString())).thenAnswer(invocation -> persistedTransfer());
|
||||
when(transfers.findNo(eq("TENANT-A"), org.mockito.ArgumentMatchers.anyString())).thenAnswer(invocation -> transfer(WarehouseTransferService.DRAFT));
|
||||
when(transferItems.selectList(any())).thenReturn(List.of());
|
||||
when(sourceOrders.selectList(any())).thenReturn(List.of());
|
||||
|
||||
@@ -179,7 +360,7 @@ class WarehouseTransferServiceTest {
|
||||
null, "TRACK-MUST-NOT-BE-SAVED", null, "Incoming transit cargo", true, "key-incoming",
|
||||
List.of(new WarehouseTransferRequests.Item("SKU-1", "Product A", 2, null),
|
||||
new WarehouseTransferRequests.Item("SKU-2", "Product B", 3, null)));
|
||||
when(transfers.findNo(eq("TENANT-A"), anyString())).thenAnswer(invocation -> persistedTransfer());
|
||||
when(transfers.findNo(eq("TENANT-A"), anyString())).thenAnswer(invocation -> transfer(WarehouseTransferService.DRAFT));
|
||||
when(transferItems.selectList(any())).thenReturn(List.of());
|
||||
when(sourceOrders.selectList(any())).thenReturn(List.of());
|
||||
|
||||
@@ -199,7 +380,7 @@ class WarehouseTransferServiceTest {
|
||||
assertEquals(transfer.getValue().getTransferNo(), transfer.getValue().getInboundCode());
|
||||
assertEquals(transfer.getValue().getTransferNo(), inbound.getValue().getInboundCode());
|
||||
assertEquals("OVERSEAS", inbound.getValue().getDestinationType());
|
||||
assertEquals(WarehouseTransferService.PENDING_REVIEW, inbound.getValue().getStatus());
|
||||
assertEquals(WarehouseTransferService.DRAFT, inbound.getValue().getStatus());
|
||||
assertNull(transfer.getValue().getTrackingNo());
|
||||
assertEquals("PENDING_BINDING", tailOrder.getValue().getRelabelStatus());
|
||||
assertEquals(List.of(1, 2, 3, 4, 5), cartons.getAllValues().stream()
|
||||
@@ -209,7 +390,7 @@ class WarehouseTransferServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void approvedTransferWithoutRelabelEntersPendingAssignment() {
|
||||
void approvedTransferWithoutRelabelEntersInboundMeasuring() {
|
||||
WarehouseTransferOrder transfer = persistedTransfer();
|
||||
transfer.setStatus(WarehouseTransferService.PENDING_REVIEW);
|
||||
transfer.setNeedRelabel(false);
|
||||
@@ -225,14 +406,134 @@ class WarehouseTransferServiceTest {
|
||||
new WarehouseTransferRequests.Review("APPROVE", null));
|
||||
|
||||
assertEquals(WarehouseTransferService.APPROVED, transfer.getStatus());
|
||||
assertEquals(WarehouseTransferService.PENDING_ASSIGNMENT, tailOrder.getCurrentStatus());
|
||||
assertEquals(WarehouseTransferService.INBOUND_MEASURING, tailOrder.getCurrentStatus());
|
||||
assertEquals(WarehouseTransferService.APPROVED, result.status());
|
||||
assertEquals(WarehouseTransferService.PENDING_ASSIGNMENT, result.orderStatus());
|
||||
assertEquals(WarehouseTransferService.INBOUND_MEASURING, result.orderStatus());
|
||||
verify(transfers).updateById(transfer);
|
||||
verify(orders).updateById(tailOrder);
|
||||
verifyNoInteractions(inventory);
|
||||
}
|
||||
|
||||
@Test
|
||||
void approvedIncomingTransferCreatesPendingInboundPlan() {
|
||||
WarehouseTransferOrder transfer = persistedTransfer();
|
||||
transfer.setInboundCode("WTR-1"); transfer.setOperatingWarehouseId("77");
|
||||
transfer.setEstimatedArrivalAt(LocalDateTime.of(2026, 9, 17, 10, 0));
|
||||
LastlegOrder tailOrder = tailOrder(); tailOrder.setCurrentStatus(WarehouseTransferService.PENDING_REVIEW);
|
||||
LastlegInboundOrder inbound = new LastlegInboundOrder();
|
||||
inbound.setId(101L); inbound.setCustomerId("TENANT-A"); inbound.setStatus(WarehouseTransferService.PENDING_REVIEW);
|
||||
when(transfers.lockNo("TENANT-A", "WTR-1")).thenReturn(transfer);
|
||||
when(transfers.findNo("TENANT-A", "WTR-1")).thenReturn(transfer);
|
||||
when(orders.selectById(31L)).thenReturn(tailOrder);
|
||||
when(inboundOrders.selectById(101L)).thenReturn(inbound);
|
||||
when(transferItems.selectList(any())).thenReturn(List.of());
|
||||
when(sourceOrders.selectList(any())).thenReturn(List.of());
|
||||
|
||||
service.review("TENANT-A", "WTR-1", "USER-1", new WarehouseTransferRequests.Review("APPROVE", null));
|
||||
|
||||
ArgumentCaptor<InboundPlan> plan = ArgumentCaptor.forClass(InboundPlan.class);
|
||||
ArgumentCaptor<InboundPlanLine> line = ArgumentCaptor.forClass(InboundPlanLine.class);
|
||||
verify(inboundPlans).insert(plan.capture());
|
||||
verify(inboundPlanLines).insert(line.capture());
|
||||
assertEquals("PDA_PENDING", plan.getValue().getStatus());
|
||||
assertEquals("WT-WTR-1", plan.getValue().getPlanNo());
|
||||
assertEquals("WTR-1", line.getValue().getInboundCode());
|
||||
assertEquals(5, line.getValue().getRequiredPackageCount());
|
||||
assertEquals("PENDING_INBOUND", inbound.getStatus());
|
||||
assertEquals(WarehouseTransferService.WAIT_PDA_RECEIVING, transfer.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void merchantSubmitsDraftForReviewBeforePrintingLabels() {
|
||||
WarehouseTransferOrder transfer = persistedTransfer();
|
||||
transfer.setStatus(WarehouseTransferService.DRAFT);
|
||||
transfer.setInboundCode("WTR-1");
|
||||
LastlegOrder tailOrder = historicalOrder();
|
||||
tailOrder.setId(31L); tailOrder.setCustomerId("TENANT-A"); tailOrder.setRelabelStatus("NOT_REQUIRED");
|
||||
LastlegInboundOrder inbound = new LastlegInboundOrder();
|
||||
inbound.setId(101L); inbound.setCustomerId("TENANT-A"); inbound.setStatus(WarehouseTransferService.DRAFT);
|
||||
when(transfers.lockNo("TENANT-A", "WTR-1")).thenReturn(transfer);
|
||||
when(transfers.findNo("TENANT-A", "WTR-1")).thenReturn(transfer);
|
||||
when(orders.selectById(31L)).thenReturn(tailOrder);
|
||||
when(inboundOrders.selectById(101L)).thenReturn(inbound);
|
||||
when(transferItems.selectList(any())).thenReturn(List.of());
|
||||
when(sourceOrders.selectList(any())).thenReturn(List.of());
|
||||
|
||||
var result = service.submit("TENANT-A", "WTR-1", "USER-1");
|
||||
|
||||
assertEquals(WarehouseTransferService.PENDING_REVIEW, transfer.getStatus());
|
||||
assertEquals(WarehouseTransferService.PENDING_REVIEW, tailOrder.getCurrentStatus());
|
||||
assertEquals(WarehouseTransferService.PENDING_REVIEW, inbound.getStatus());
|
||||
assertEquals(WarehouseTransferService.PENDING_REVIEW, result.status());
|
||||
verify(transfers).updateById(transfer);
|
||||
verify(orders).updateById(tailOrder);
|
||||
verify(inboundOrders).updateById(inbound);
|
||||
}
|
||||
|
||||
@Test
|
||||
void merchantUpdatesDraftAndRebuildsEditableSnapshots() {
|
||||
WarehouseTransferOrder transfer = persistedTransfer();
|
||||
transfer.setStatus(WarehouseTransferService.DRAFT);
|
||||
LastlegOrder tailOrder = historicalOrder();
|
||||
tailOrder.setId(31L); tailOrder.setCustomerId("TENANT-A"); tailOrder.setCurrentStatus(WarehouseTransferService.DRAFT);
|
||||
when(transfers.lockNo("TENANT-A", "WTR-1")).thenReturn(transfer);
|
||||
when(transfers.findNo("TENANT-A", "WTR-1")).thenReturn(transfer);
|
||||
when(orders.selectById(31L)).thenReturn(tailOrder);
|
||||
when(transferItems.selectList(any())).thenReturn(List.of());
|
||||
when(sourceOrders.selectList(any())).thenReturn(List.of());
|
||||
|
||||
var result = service.updateDraft("TENANT-A", "WTR-1", "USER-1", request(false));
|
||||
|
||||
assertEquals(WarehouseTransferService.DRAFT, result.status());
|
||||
assertEquals("WH-1", transfer.getOperatingWarehouseId());
|
||||
assertEquals(new BigDecimal("0.2500"), tailOrder.getVolumeCbm());
|
||||
verify(transfers).updateById(transfer);
|
||||
verify(orders).updateById(tailOrder);
|
||||
verify(transferCartons).delete(any());
|
||||
verify(transferItems).delete(any());
|
||||
verify(relabelBindings).delete(any());
|
||||
verify(orderDetails).delete(any());
|
||||
verify(transferItems).insert(any(WarehouseTransferItem.class));
|
||||
verify(transferCartons, times(5)).insert(any(WarehouseTransferCarton.class));
|
||||
verify(orderDetails).insert(any(LastlegOrderDetail.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void merchantCannotUpdateSubmittedTransfer() {
|
||||
WarehouseTransferOrder transfer = persistedTransfer();
|
||||
when(transfers.lockNo("TENANT-A", "WTR-1")).thenReturn(transfer);
|
||||
|
||||
ResponseStatusException error = assertThrows(ResponseStatusException.class,
|
||||
() -> service.updateDraft("TENANT-A", "WTR-1", "USER-1", request(false)));
|
||||
|
||||
assertEquals(409, error.getStatusCode().value());
|
||||
verify(transfers, never()).updateById(any(WarehouseTransferOrder.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void merchantDraftUpdateIsTenantScoped() {
|
||||
when(transfers.lockNo("TENANT-B", "WTR-1")).thenReturn(null);
|
||||
|
||||
ResponseStatusException error = assertThrows(ResponseStatusException.class,
|
||||
() -> service.updateDraft("TENANT-B", "WTR-1", "USER-2", request(false)));
|
||||
|
||||
assertEquals(404, error.getStatusCode().value());
|
||||
verify(transfers, never()).updateById(any(WarehouseTransferOrder.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void draftCannotPrintCartonLabelsBeforeSubmission() {
|
||||
WarehouseTransferOrder transfer = persistedTransfer();
|
||||
transfer.setStatus(WarehouseTransferService.DRAFT);
|
||||
when(transfers.findNo("TENANT-A", "WTR-1")).thenReturn(transfer);
|
||||
|
||||
ResponseStatusException error = assertThrows(ResponseStatusException.class,
|
||||
() -> service.cartonLabel("TENANT-A", "WTR-1"));
|
||||
|
||||
assertEquals(409, error.getStatusCode().value());
|
||||
verify(transferCartons, never()).listOrder(anyString(), anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectionRequiresNonBlankReasonBeforeAnyStateOrInventoryChange() {
|
||||
for (String decision : List.of("REJECT", "REJECTED")) {
|
||||
@@ -378,6 +679,67 @@ class WarehouseTransferServiceTest {
|
||||
assertEquals("RESERVED", allocation.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void outboundRejectsTransferWithoutFbaDestinationBeforeInventoryMutation() {
|
||||
WarehouseTransferOrder transfer = transfer(WarehouseTransferService.ALLOCATED);
|
||||
transfer.setFbaWarehouseCode(null);
|
||||
when(transfers.lockNo("TENANT-A", "WTR-1")).thenReturn(transfer);
|
||||
|
||||
ResponseStatusException exception = assertThrows(ResponseStatusException.class, () ->
|
||||
service.outbound("TENANT-A", "WTR-1", "USER-1",
|
||||
new WarehouseTransferRequests.Outbound("OUT-NO-FBA", null)));
|
||||
|
||||
assertEquals(409, exception.getStatusCode().value());
|
||||
verifyNoInteractions(inventory);
|
||||
verify(inventoryAllocations, never()).lockTransfer(anyString(), anyLong());
|
||||
verify(transfers, never()).updateById(any(WarehouseTransferOrder.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void merchantMeasurementPhotosExposeOnlySubmittedSelection() {
|
||||
WarehouseTransferOrder transfer = persistedTransfer();
|
||||
WarehouseTransferMeasurementPhoto selected = new WarehouseTransferMeasurementPhoto();
|
||||
selected.setFileId(101L); selected.setFileName("selected.jpg"); selected.setContentType("image/jpeg");
|
||||
selected.setSizeBytes(12L); selected.setUploadedAt(LocalDateTime.of(2026, 9, 16, 10, 0));
|
||||
when(transfers.findNo("TENANT-A", "WTR-1")).thenReturn(transfer);
|
||||
when(measurementPhotos.listSubmitted("TENANT-A", 21L)).thenReturn(List.of(selected));
|
||||
|
||||
var result = service.measurementPhotos("TENANT-A", "WTR-1");
|
||||
|
||||
assertEquals(List.of(101L), result.stream().map(value -> value.fileId()).toList());
|
||||
verify(measurementPhotos, never()).listOrder("TENANT-A", 21L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void merchantCannotDownloadUploadedButUnsubmittedMeasurementPhoto() {
|
||||
WarehouseTransferOrder transfer = persistedTransfer();
|
||||
when(transfers.findNo("TENANT-A", "WTR-1")).thenReturn(transfer);
|
||||
when(measurementPhotos.findSubmittedFile("TENANT-A", 21L, 102L)).thenReturn(null);
|
||||
|
||||
ResponseStatusException exception = assertThrows(ResponseStatusException.class,
|
||||
() -> service.downloadMeasurementPhoto("TENANT-A", "WTR-1", 102L));
|
||||
|
||||
assertEquals(404, exception.getStatusCode().value());
|
||||
verifyNoInteractions(measurementPhotoFiles);
|
||||
}
|
||||
|
||||
@Test
|
||||
void pdaCanDownloadOwnWarehouseUploadedPhotoBeforeSubmission() {
|
||||
WarehouseTransferOrder transfer = persistedTransfer();
|
||||
WarehouseTransferMeasurementPhoto draft = new WarehouseTransferMeasurementPhoto();
|
||||
draft.setFileId(102L);
|
||||
var downloaded = new TransferMeasurementPhotoFileGateway.DownloadedFile(
|
||||
new byte[]{1}, "draft.jpg", "image/jpeg");
|
||||
when(transfers.findNoByWarehouse("WTR-1", "92")).thenReturn(transfer);
|
||||
when(measurementPhotos.findFile("TENANT-A", 21L, 102L)).thenReturn(draft);
|
||||
when(measurementPhotoFiles.download("TENANT-A", 21L, 102L)).thenReturn(downloaded);
|
||||
|
||||
var result = service.downloadPdaMeasurementPhoto(92L, "WTR-1", 102L);
|
||||
|
||||
assertEquals("draft.jpg", result.fileName());
|
||||
verify(measurementPhotos, never()).findSubmittedFile(anyString(), anyLong(), anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
void allocationRejectsPositionOutsideSelectedInboundOrder() {
|
||||
WarehouseTransferOrder transfer = transfer("APPROVED");
|
||||
@@ -427,6 +789,7 @@ class WarehouseTransferServiceTest {
|
||||
transfer.setInboundOrderId(101L); transfer.setInboundCode("IN-1"); transfer.setLastlegOrderId(31L);
|
||||
transfer.setLastlegOrderNo("LMO-TRANSFER-1"); transfer.setNeedRelabel(false);
|
||||
transfer.setStatus(WarehouseTransferService.PENDING_REVIEW); transfer.setPackageCount(5);
|
||||
transfer.setFbaWarehouseCode("ONT8");
|
||||
return transfer;
|
||||
}
|
||||
|
||||
@@ -436,6 +799,24 @@ class WarehouseTransferServiceTest {
|
||||
return transfer;
|
||||
}
|
||||
|
||||
private static WarehouseTransferPallet measuredPallet(String status) {
|
||||
WarehouseTransferPallet pallet = new WarehouseTransferPallet();
|
||||
pallet.setId(61L); pallet.setTransferOrderId(21L); pallet.setCustomerId("TENANT-A");
|
||||
pallet.setSequenceNo(1); pallet.setWarehousePalletId(701L); pallet.setPalletNo("NEW-PALLET-1");
|
||||
pallet.setCartonCount(5); pallet.setLengthCm(new BigDecimal("100.00"));
|
||||
pallet.setWidthCm(new BigDecimal("100.00")); pallet.setHeightCm(new BigDecimal("100.00"));
|
||||
pallet.setVolumeCbm(new BigDecimal("1.000000")); pallet.setGrossWeightKg(new BigDecimal("50.000"));
|
||||
pallet.setStatus(status);
|
||||
return pallet;
|
||||
}
|
||||
|
||||
private static WarehouseTransferItem transferItem(long id, String sku, int packages) {
|
||||
WarehouseTransferItem item = new WarehouseTransferItem();
|
||||
item.setId(id); item.setTransferOrderId(21L); item.setCustomerId("TENANT-A");
|
||||
item.setSkuCode(sku); item.setProductNameZh("Product"); item.setPackageCount(packages);
|
||||
return item;
|
||||
}
|
||||
|
||||
private static LastlegOrder tailOrder() {
|
||||
LastlegOrder order = historicalOrder();
|
||||
order.setId(31L); order.setCurrentStatus("PENDING_ASSIGNMENT");
|
||||
|
||||
Reference in New Issue
Block a user