优化
This commit is contained in:
+2
@@ -153,6 +153,8 @@ class ReceivingContainerContractTest {
|
||||
assertTrue(yaml.contains("enum: [LICENSE_PLATE, CONTAINER_SEAL, DOOR_OPEN]"));
|
||||
assertTrue(yaml.contains("photos: { type: array, minItems: 3, maxItems: 3"));
|
||||
assertTrue(yaml.contains("required: [role, fileId, downloadPath]"));
|
||||
assertTrue(yaml.contains("packageCount: { type: integer, minimum: 1, maximum: 66 }"));
|
||||
assertTrue(receivingPaths.contains("单托最多 66 箱"));
|
||||
assertTrue(yaml.contains("overseasWarehouseId: { type: string"));
|
||||
assertTrue(yaml.contains("overseasWarehouseName: { type: string"));
|
||||
assertFalse(receivingPaths.contains("photoFileIds"));
|
||||
|
||||
+73
@@ -49,6 +49,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
@@ -422,6 +423,55 @@ class ReceivingContainerServiceBehaviorTest {
|
||||
verifyNoInteractions(workflow);
|
||||
}
|
||||
|
||||
@Test
|
||||
void fbaPalletAccepts66PackagesAndRejects67BeforeReceivingStarts() {
|
||||
stubCurrent("FBA", "CONT-A");
|
||||
InboundPlanLine fbaLine = line("FBA");
|
||||
fbaLine.setRequiredPackageCount(66);
|
||||
when(lines.selectList(any())).thenReturn(List.of(fbaLine));
|
||||
when(orderDetails.selectList(any())).thenReturn(List.of(detail("SKU-1", 66)));
|
||||
when(skuReceipts.listLineSku("100", 10L, "SKU-1")).thenReturn(List.of());
|
||||
stubReservedFbaPallet();
|
||||
when(workflow.receiveBulkResolved(any(), any(), any(), any(), anyInt(), any(), any()))
|
||||
.thenAnswer(invocation -> invocation.getArgument(4));
|
||||
when(fbaPalletCounts.insert(any(ReceivingFbaPalletCount.class))).thenReturn(1);
|
||||
when(sessions.updateById(any(ReceivingContainerSession.class))).thenReturn(1);
|
||||
when(operations.insert(any(ReceivingContainerOperation.class))).thenReturn(1);
|
||||
|
||||
var accepted = service.receiveBulk("7", "receiver",
|
||||
new ReceivingContainerRequests.BulkReceive("IN-1", "PAL-1", 66, "capacity-66"));
|
||||
|
||||
assertThat(accepted.acceptedPackages()).isEqualTo(66);
|
||||
|
||||
org.mockito.Mockito.reset(sessions, workflow, warehouse);
|
||||
assertThatThrownBy(() -> service.receiveBulk("7", "receiver",
|
||||
new ReceivingContainerRequests.BulkReceive("IN-1", "PAL-1", 67, "capacity-67")))
|
||||
.isInstanceOf(ResponseStatusException.class)
|
||||
.hasMessageContaining("66箱");
|
||||
verifyNoInteractions(sessions, workflow, warehouse);
|
||||
}
|
||||
|
||||
@Test
|
||||
void overseasCartonIsRejectedWhenPhysicalPalletAlreadyContains66Packages() {
|
||||
stubCurrent("OVERSEAS", "CONT-A");
|
||||
when(cartons.selectOne(any())).thenReturn(carton(1L, "IN-1#67"));
|
||||
InboundPlanPallet reserved = planPallet();
|
||||
reserved.setPalletNo("PAL-1"); reserved.setWarehousePalletId(30L); reserved.setWarehouseLocationId(40L);
|
||||
reserved.setWarehouseResourceLockId(50L); reserved.setAssignmentStatus("RESERVED");
|
||||
when(pallets.selectList(any())).thenReturn(List.of(reserved));
|
||||
when(warehouse.resolveReservedPallet("PAL-1", "100", 100L, 30L, 40L, 50L))
|
||||
.thenReturn(resource(100L, null));
|
||||
when(cartons.selectCount(any())).thenReturn(66L);
|
||||
|
||||
assertThatThrownBy(() -> service.receiveCarton("7", "receiver",
|
||||
new ReceivingContainerRequests.CartonReceive("IN-1#67", "PAL-1", "carton-capacity")))
|
||||
.isInstanceOf(ResponseStatusException.class)
|
||||
.hasMessageContaining("66箱");
|
||||
|
||||
verify(workflow, never()).validateCartonResolved(any(), any(), any(), any(), any());
|
||||
verify(workflow, never()).assignResolvedPallet(any(), any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void crossContainerCartonAndMismatchedPalletAreRejectedBeforeReservation() {
|
||||
stubCurrent("OVERSEAS", "CONT-A");
|
||||
@@ -950,6 +1000,29 @@ class ReceivingContainerServiceBehaviorTest {
|
||||
verifyNoInteractions(warehouse);
|
||||
}
|
||||
|
||||
@Test
|
||||
void dropshipWholePalletOver66PackagesIsRejectedBeforePicking() {
|
||||
stubCurrent("OVERSEAS", "CONT-A");
|
||||
LastlegOrder dropship = order("OVERSEAS"); dropship.setOrderType("ONE_CLICK_DROPSHIP");
|
||||
when(orders.selectById(1L)).thenReturn(dropship);
|
||||
InboundPlanPallet pallet = planPallet();
|
||||
pallet.setPalletNo("DP-260909-0067"); pallet.setSourcePalletNo("DP-260909-0067");
|
||||
pallet.setWarehousePalletId(30L);
|
||||
InboundPlanPalletAllocation pending = new InboundPlanPalletAllocation();
|
||||
pending.setId(67L); pending.setCustomerId("100"); pending.setPlanId(2L);
|
||||
pending.setLineId(10L); pending.setPalletId(20L); pending.setPlannedPackageCount(67);
|
||||
when(pallets.selectList(any())).thenReturn(List.of(pallet));
|
||||
when(allocations.selectList(any())).thenReturn(List.of(pending));
|
||||
stubCompleteManifest("DP-260909-0067", 67);
|
||||
|
||||
assertThatThrownBy(() -> service.receiveWholePallet("7", "receiver",
|
||||
new ReceivingContainerRequests.WholePalletReceive("DP-260909-0067", "whole-capacity")))
|
||||
.isInstanceOf(ResponseStatusException.class)
|
||||
.hasMessageContaining("66箱");
|
||||
|
||||
verify(workflow, never()).pickWholePallet(any(), any(), anyLong(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void domesticLoadingPhotoDownloadIsScopedToTheCurrentPhysicalContainer() {
|
||||
ReceivingContainerSession current = session("CONT-A", "SIGNED");
|
||||
|
||||
+16
@@ -85,6 +85,22 @@ class ContainerInboundPlanPreviewTest {
|
||||
assertFalse(result.ready());
|
||||
}
|
||||
|
||||
@Test
|
||||
void heldHistoricalOrderRemainsReadyBecauseHoldOnlyBlocksOutbound() {
|
||||
when(orderMapper.containerPreviewOrders("BOOKING-HELD")).thenReturn(List.of(
|
||||
new InboundPlanOrderMapper.ContainerPreviewOrderRow("100", 1L, "LM-HELD", "FO-1",
|
||||
"测试客户", "FIRST_LEG", "FBA", "IN-1", "PENDING_ASSIGNMENT", null,
|
||||
"MSCU6639870", 1, 10, new BigDecimal("1.0000"), "193000000000200003", "US-LAX-01",
|
||||
"洛杉矶仓", "ACY2", null, null, "Amazon ACY2", 0, true, 1,
|
||||
null, null, null, null, 1)));
|
||||
|
||||
var result = service.previewContainer("BOOKING-HELD");
|
||||
|
||||
assertEquals(true, result.ready());
|
||||
assertEquals("READY", result.groups().get(0).orders().get(0).readiness());
|
||||
assertEquals(List.of(), result.groups().get(0).orders().get(0).blockers());
|
||||
}
|
||||
|
||||
@Test
|
||||
void dropshipPreviewKeepsUnderlyingFbaFactSeparateFromItsDisplayCategory() {
|
||||
when(orderMapper.containerPreviewOrders("BOOKING-DROPSHIP")).thenReturn(List.of(
|
||||
|
||||
+9
-7
@@ -352,21 +352,23 @@ class InboundPlanServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void refusesManualPlanForHeldOrderBeforeWriting() {
|
||||
void createsManualPlanForHeldHistoricalOrderBecauseHoldOnlyBlocksOutbound() {
|
||||
SourceOrderSnapshot held = new SourceOrderSnapshot(2L, "100", "MSCU6639870", "RK-2",
|
||||
"PENDING_ASSIGNMENT", null, new BigDecimal("1.0000"), 10, 0,
|
||||
"193000000000200003", "US-LAX-01", "洛杉矶仓", null,
|
||||
null, "WH-A", "仓A", 1, 0, true, 5);
|
||||
"WH-A", "WH-A", "仓A", 1, 0, true, 5);
|
||||
when(orderMapper.lockOrder("100", 2L)).thenReturn(List.of(held));
|
||||
when(orderMapper.markPlannedManual(eq("100"), eq(2L), any(), eq(1), eq(false), eq(5), any()))
|
||||
.thenReturn(1);
|
||||
|
||||
ResponseStatusException error = assertThrows(ResponseStatusException.class, () -> service.convertManual(
|
||||
var result = service.convertManual(
|
||||
new ManualInboundPlanRequest("100", 2L, 5,
|
||||
1, "STANDARD", false, false, false, null),
|
||||
"manual-held", "900"));
|
||||
"manual-held", "900");
|
||||
|
||||
assertEquals(409, error.getStatusCode().value());
|
||||
verify(planMapper, never()).insert(any(InboundPlan.class));
|
||||
verify(commandMapper, never()).insert(any(com.baiyue.lastleg.entity.LastlegOrderCommandRecord.class));
|
||||
assertEquals(2L, result.sourceOrderId());
|
||||
verify(planMapper).insert(any(InboundPlan.class));
|
||||
verify(orderMapper).markPlannedManual(eq("100"), eq(2L), any(), eq(1), eq(false), eq(5), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+17
@@ -280,6 +280,23 @@ class InventoryCommandServiceTest {
|
||||
verify(balances, never()).updateById(any(InventoryBalance.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void holdsAllAvailablePositionsForOneInboundOrder() {
|
||||
InventoryBalance balance = balance(200L, 5);
|
||||
InventoryPosition position = position(201L, 200L, 5);
|
||||
when(commands.lockInboundAvailablePositions("100", 101L)).thenReturn(List.of(position));
|
||||
when(commands.lockPosition("100", 201L)).thenReturn(position);
|
||||
when(commands.lockBalance("100", 200L)).thenReturn(balance);
|
||||
|
||||
service.holdInboundOrderInventory("100", "7", 101L, "ORD-1", "质检暂扣");
|
||||
|
||||
assertEquals(0L, position.getAvailablePackages());
|
||||
assertEquals(5L, position.getHoldPackages());
|
||||
assertEquals(0L, balance.getAvailablePackages());
|
||||
assertEquals(5L, balance.getHoldPackages());
|
||||
verify(ledgers).insert(any(InventoryLedger.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void replayUsesPersistedResultWithoutMutatingInventoryAgain() throws Exception {
|
||||
var request = stockIn("IN-1", "stock-in-1", 12, 24);
|
||||
|
||||
+23
@@ -16,6 +16,15 @@ class MerchantOverseasOpenApiContractTest {
|
||||
}
|
||||
|
||||
for (String path : new String[] {
|
||||
"/api/merchant/v1/last-leg/orders:",
|
||||
"/api/merchant/v1/last-leg/orders/{id}:",
|
||||
"/api/merchant/v1/last-leg/warehouse-transfers:",
|
||||
"/api/merchant/v1/last-leg/warehouse-transfers/{transferNo}:",
|
||||
"/api/merchant/v1/last-leg/warehouse-transfers/{transferNo}/confirm-measurement:",
|
||||
"/api/merchant/v1/last-leg/cargo-discards:",
|
||||
"/api/merchant/v1/last-leg/cargo-discards/inventory-candidates:",
|
||||
"/api/merchant/v1/last-leg/cargo-discards/{discardNo}:",
|
||||
"/api/merchant/v1/last-leg/cargo-discards/{discardNo}/cancel:",
|
||||
"/api/merchant/v1/last-leg/overseas-procurements:",
|
||||
"/api/merchant/v1/last-leg/overseas-procurements/{orderNo}:",
|
||||
"/api/merchant/v1/last-leg/us-truck-orders:",
|
||||
@@ -35,6 +44,20 @@ class MerchantOverseasOpenApiContractTest {
|
||||
.contains("merchant:cross-border:pallet-shipping:list")
|
||||
.contains("merchant:cross-border:pallet-shipping:create")
|
||||
.contains("merchant:cross-border:pallet-shipping:edit")
|
||||
.contains("merchant:overseas-warehouse:last-mile-order:list")
|
||||
.contains("merchant:overseas-warehouse:transfer:list")
|
||||
.contains("merchant:overseas-warehouse:transfer:create")
|
||||
.contains("merchant:overseas-warehouse:transfer:confirm")
|
||||
.contains("merchant:overseas-warehouse:discard:list")
|
||||
.contains("merchant:overseas-warehouse:discard:create")
|
||||
.contains("merchant:overseas-warehouse:discard:cancel")
|
||||
.contains("merchant:overseas-warehouse:relabel:list")
|
||||
.contains("merchant:overseas-warehouse:relabel:create")
|
||||
.contains("merchant:overseas-warehouse:relabel:files")
|
||||
.contains("merchant:overseas-warehouse:relabel:confirm")
|
||||
.contains("merchant:overseas-warehouse:return-order:list")
|
||||
.contains("merchant:overseas-warehouse:return-order:create")
|
||||
.contains("merchant:overseas-warehouse:return-order:relabel")
|
||||
.contains("merchant:warehouse:inbound:list")
|
||||
.contains("#/components/schemas/ProcurementDetailResponse")
|
||||
.contains("#/components/schemas/TruckDetailResponse")
|
||||
|
||||
+37
@@ -12,6 +12,7 @@ import com.baiyue.lastleg.mapper.LastlegOrderManualNodeHistoryMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderManualNodeMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderOperationLogMapper;
|
||||
import com.baiyue.lastleg.inventory.command.service.InventoryCommandService;
|
||||
import com.fasterxml.jackson.databind.json.JsonMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -48,6 +49,7 @@ class LastlegOrderCommandServiceTest {
|
||||
@Mock LastlegOrderManualNodeHistoryMapper manualNodeHistory;
|
||||
@Mock LastlegOrderOperationLogMapper operationLogs;
|
||||
@Mock InboundOrderDetainedOutboxService detainedOutbox;
|
||||
@Mock InventoryCommandService inventory;
|
||||
LastlegOrderCommandService service;
|
||||
|
||||
@BeforeEach
|
||||
@@ -60,12 +62,14 @@ class LastlegOrderCommandServiceTest {
|
||||
ReflectionTestUtils.setField(service, "manualNodeHistory", manualNodeHistory);
|
||||
ReflectionTestUtils.setField(service, "operationLogs", operationLogs);
|
||||
ReflectionTestUtils.setField(service, "detainedOutbox", detainedOutbox);
|
||||
ReflectionTestUtils.setField(service, "inventory", inventory);
|
||||
ReflectionTestUtils.setField(service, "objectMapper", JsonMapper.builder().findAndAddModules().build());
|
||||
}
|
||||
|
||||
@Test
|
||||
void holdsOnlySelectedOrderWithVersionAndImmutableHistory() {
|
||||
LastlegOrder order = order(11L, 3);
|
||||
order.setCurrentStatus("INBOUND_COMPLETED");
|
||||
when(orders.selectById(11L)).thenReturn(order);
|
||||
when(orders.lockById("100", 11L)).thenReturn(order);
|
||||
when(orders.updateHold(eq("100"), eq(11L), eq(true), eq(true), eq("海关查验"), eq(3),
|
||||
@@ -80,9 +84,42 @@ class LastlegOrderCommandServiceTest {
|
||||
verify(orders).updateHold(eq("100"), eq(11L), eq(true), eq(true), eq("海关查验"), eq(3), any());
|
||||
verify(holdHistory).insert(any(LastlegOrderHoldHistory.class));
|
||||
verify(commands).insert(any(LastlegOrderCommandRecord.class));
|
||||
verify(inventory).holdInboundOrderInventory("100", "900", 11L, "LL-11", "海关查验");
|
||||
verify(orders, never()).lockContainerOrders(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void releasingHoldMovesOrderInventoryBackToAvailable() {
|
||||
LastlegOrder order = order(11L, 3);
|
||||
order.setCurrentStatus("INBOUND_COMPLETED");
|
||||
order.setDetained(true);
|
||||
when(orders.selectById(11L)).thenReturn(order);
|
||||
when(orders.lockById("100", 11L)).thenReturn(order);
|
||||
when(orders.updateHold(eq("100"), eq(11L), eq(false), eq(false), eq("质检通过"), eq(3),
|
||||
any(LocalDateTime.class))).thenReturn(1);
|
||||
|
||||
service.hold("100", 11L, "release-11",
|
||||
new LastlegOrderCommandDtos.HoldRequest(false, false, "质检通过", 3), "900", "管理员");
|
||||
|
||||
verify(inventory).releaseInboundOrderHold("100", "900", 11L, "LL-11", "质检通过");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsNewHoldBeforeInboundCompletion() {
|
||||
LastlegOrder order = order(11L, 3);
|
||||
when(orders.selectById(11L)).thenReturn(order);
|
||||
when(orders.lockById("100", 11L)).thenReturn(order);
|
||||
|
||||
ResponseStatusException error = assertThrows(ResponseStatusException.class, () -> service.hold("100", 11L,
|
||||
"hold-before-inbound", new LastlegOrderCommandDtos.HoldRequest(true, false, "待入库", 3),
|
||||
"900", "管理员"));
|
||||
|
||||
assertEquals(409, error.getStatusCode().value());
|
||||
assertEquals("货物完成入库后才能暂扣", error.getReason());
|
||||
verify(orders, never()).updateHold(anyString(), anyLong(), anyBoolean(), anyBoolean(),
|
||||
nullable(String.class), anyInt(), any(LocalDateTime.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsStaleHoldWithoutAnyWrite() {
|
||||
LastlegOrder order = order(11L, 4);
|
||||
|
||||
@@ -49,7 +49,6 @@ import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
@@ -69,7 +68,6 @@ class LastlegOrderServiceTest {
|
||||
@Mock InboundPlanCartonMapper cartons;
|
||||
@Mock FbaWarehouseValidator fbaWarehouses;
|
||||
@Mock InventoryCommandService inventory;
|
||||
@Mock InboundOrderDetainedOutboxService detainedOutbox;
|
||||
@Mock ReceivingWarehouseOperationMapper receivingWarehouseOperations;
|
||||
@Mock ContainerArrivalTimeOperationMapper containerArrivalTimeOperations;
|
||||
@Mock WarehouseResourceGateway warehouseResourceGateway;
|
||||
@@ -116,7 +114,6 @@ class LastlegOrderServiceTest {
|
||||
Long.parseLong(invocation.getArgument(0)), invocation.getArgument(1), invocation.getArgument(2),
|
||||
"US", "CA"));
|
||||
ReflectionTestUtils.setField(service, "inventory", inventory);
|
||||
ReflectionTestUtils.setField(service, "detainedOutbox", detainedOutbox);
|
||||
ReflectionTestUtils.setField(service, "receivingWarehouseOperations", receivingWarehouseOperations);
|
||||
ReflectionTestUtils.setField(service, "containerArrivalTimeOperations", containerArrivalTimeOperations);
|
||||
ReflectionTestUtils.setField(service, "warehouseResourceGateway", warehouseResourceGateway);
|
||||
@@ -559,40 +556,6 @@ class LastlegOrderServiceTest {
|
||||
assertEquals("NYEXX2609060063", result.records().get(2).inboundCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void holdingBeforeInboundPersistsTheOrderMarkerAndOperationRemark() {
|
||||
LastlegOrder order = order(11L, "100", "PENDING_INBOUND", null);
|
||||
when(orders.selectOne(any())).thenReturn(order);
|
||||
when(orders.lockById("100", 11L)).thenReturn(order);
|
||||
|
||||
service.setDetained("100", 11L, new LastlegOrderRequests.SetDetained(true, "等待质检"), "operator");
|
||||
|
||||
assertTrue(order.getDetained());
|
||||
verify(orders).updateById(order);
|
||||
ArgumentCaptor<com.baiyue.lastleg.entity.LastlegOrderOperationLog> operation =
|
||||
ArgumentCaptor.forClass(com.baiyue.lastleg.entity.LastlegOrderOperationLog.class);
|
||||
verify(operations).insert(operation.capture());
|
||||
assertEquals("暂扣", operation.getValue().getOperationNode());
|
||||
assertEquals("等待质检", operation.getValue().getResult());
|
||||
verify(detainedOutbox).publish(eq(order), eq(true), eq("等待质检"), isNull(), eq("operator"), any(), anyLong());
|
||||
verify(inventory, never()).releaseInboundOrderHold(any(), any(), anyLong(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void releasingAfterInboundMovesAllOrderHeldInventoryToAvailable() {
|
||||
LastlegOrder order = order(11L, "100", "INBOUND_COMPLETED", "IP-1");
|
||||
order.setOrderNo("LM-1");
|
||||
order.setDetained(true);
|
||||
when(orders.selectOne(any())).thenReturn(order);
|
||||
when(orders.lockById("100", 11L)).thenReturn(order);
|
||||
|
||||
service.setDetained("100", 11L, new LastlegOrderRequests.SetDetained(false, "质检通过"), "operator");
|
||||
|
||||
verify(inventory).releaseInboundOrderHold("100", "operator", 11L, "LM-1", "质检通过");
|
||||
assertEquals(false, order.getDetained());
|
||||
verify(detainedOutbox).publish(eq(order), eq(false), eq("质检通过"), isNull(), eq("operator"), any(), anyLong());
|
||||
}
|
||||
|
||||
private static LastlegOrder order(long id, String customerId, String status, String inboundOrderNo) {
|
||||
LastlegOrder order = new LastlegOrder();
|
||||
order.setId(id);
|
||||
|
||||
Reference in New Issue
Block a user