Merge origin/dev and resolve backend conflicts
This commit is contained in:
+7
@@ -27,6 +27,13 @@ class InboundPlanSchemaContractTest {
|
||||
assertTrue(schema.contains("create table if not exists ll_inbound_fba_appointment"));
|
||||
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("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"));
|
||||
assertTrue(schema.contains("create table if not exists ll_order_manual_node"));
|
||||
assertTrue(schema.contains("create table if not exists ll_order_manual_node_history"));
|
||||
assertTrue(schema.contains("create table if not exists ll_inbound_fba_appointment_item"));
|
||||
assertTrue(schema.contains("fba_warehouse_code"));
|
||||
assertFalse(schema.contains("cabinet_no"));
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package com.baiyue.lastleg.inboundplan;
|
||||
|
||||
import com.baiyue.lastleg.inboundplan.dto.ManualInboundPlanRequest;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class TailPlanCommandOpenApiContractTest {
|
||||
@Test
|
||||
void exposesOnlyLatestManualPlanHoldAndEightRowNodeContracts() throws Exception {
|
||||
try (var stream = getClass().getClassLoader()
|
||||
.getResourceAsStream("openapi/overseas-business-v1.yaml")) {
|
||||
assertTrue(stream != null);
|
||||
String api = new String(stream.readAllBytes(), StandardCharsets.UTF_8);
|
||||
assertTrue(api.contains("/api/platform/v1/last-leg/inbound-plans/manual:"));
|
||||
assertFalse(api.contains("/api/platform/v1/last-leg/inbound-plans/convert:"));
|
||||
assertFalse(api.contains("plannedWarehouseId"));
|
||||
assertFalse(api.contains("plannedWarehouseCode"));
|
||||
assertFalse(api.contains("plannedWarehouseName"));
|
||||
int manualSchemaStart = api.indexOf(" ManualInboundPlanRequest:");
|
||||
int manualSchemaEnd = api.indexOf("\n PageMetadata:", manualSchemaStart);
|
||||
assertTrue(manualSchemaStart >= 0 && manualSchemaEnd > manualSchemaStart);
|
||||
assertFalse(api.substring(manualSchemaStart, manualSchemaEnd).contains("inboundCode"));
|
||||
assertFalse(java.util.Arrays.stream(ManualInboundPlanRequest.class.getRecordComponents())
|
||||
.anyMatch(component -> "inboundCode".equals(component.getName())));
|
||||
assertTrue(api.contains("/api/platform/v1/last-leg/orders/{id}/hold:"));
|
||||
assertFalse(api.contains("/api/platform/v1/last-leg/orders/{id}/detained:"));
|
||||
assertTrue(api.contains("/api/platform/v1/last-leg/orders/{id}/manual-nodes:"));
|
||||
assertTrue(api.contains("minItems: 8"));
|
||||
assertTrue(api.contains("maxItems: 8"));
|
||||
assertTrue(api.contains("ACTUAL_ARRIVAL_WAITING_UNLOAD"));
|
||||
}
|
||||
}
|
||||
}
|
||||
+16
-22
@@ -3,8 +3,8 @@ package com.baiyue.lastleg.inboundplan.controller;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.lastleg.inboundplan.dto.InboundPlanRequests;
|
||||
import com.baiyue.lastleg.inboundplan.dto.ConvertInboundPlanRequest;
|
||||
import com.baiyue.lastleg.inboundplan.dto.ConvertContainerInboundPlanRequest;
|
||||
import com.baiyue.lastleg.inboundplan.dto.ManualInboundPlanRequest;
|
||||
import com.baiyue.lastleg.inboundplan.dto.UpdateFbaAppointmentRequest;
|
||||
import com.baiyue.lastleg.inboundplan.service.InboundFbaAppointmentService;
|
||||
import com.baiyue.lastleg.inboundplan.service.InboundPlanService;
|
||||
@@ -19,6 +19,7 @@ import org.springframework.web.server.ResponseStatusException;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalDate;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
@@ -61,31 +62,19 @@ class InboundPlanControllerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void platformConversionRequiresAnExplicitCustomer() {
|
||||
assertThatThrownBy(() -> controller.convert(platform(), new ConvertInboundPlanRequest(2L, null)))
|
||||
.isInstanceOf(ResponseStatusException.class)
|
||||
.hasMessageContaining("平台生成入库计划必须指定客户");
|
||||
verify(service, never()).convert(org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any());
|
||||
void platformManualPlanPassesVersionedIdempotentCommand() {
|
||||
ManualInboundPlanRequest request = manualRequest();
|
||||
controller.manual(platform(), "manual-key", request);
|
||||
verify(service).convertManual(request, "manual-key", "7");
|
||||
}
|
||||
|
||||
@Test
|
||||
void platformConversionPassesTheProvidedLongCustomerId() {
|
||||
ConvertInboundPlanRequest request = new ConvertInboundPlanRequest(9223372036854775807L,
|
||||
"193000000000200003");
|
||||
|
||||
controller.convert(platform(), request);
|
||||
|
||||
verify(service).convert("193000000000200003", 9223372036854775807L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void customerConversionIgnoresRequestedCustomerAndUsesAuthenticatedScope() {
|
||||
void merchantCannotCreateManualPlan() {
|
||||
AuthenticatedUser customer = new AuthenticatedUser(8L, 100L, 100L, 9L, "customer", "MERCHANT", "WEB", Set.of());
|
||||
ConvertInboundPlanRequest request = new ConvertInboundPlanRequest(2L, "200");
|
||||
|
||||
controller.convert(customer, request);
|
||||
|
||||
verify(service).convert("100", 2L);
|
||||
assertThatThrownBy(() -> controller.manual(customer, "manual-key", manualRequest()))
|
||||
.isInstanceOf(ResponseStatusException.class);
|
||||
verify(service, never()).convertManual(org.mockito.ArgumentMatchers.any(),
|
||||
org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -109,4 +98,9 @@ class InboundPlanControllerTest {
|
||||
return new AuthenticatedUser(7L, 0L, 0L, 8L, "operator", "PLATFORM", "WEB",
|
||||
Set.of("platform:overseas-warehouse:inbound-order:list"));
|
||||
}
|
||||
|
||||
private static ManualInboundPlanRequest manualRequest() {
|
||||
return new ManualInboundPlanRequest("193000000000200003", 9223372036854775807L, 0,
|
||||
LocalDate.of(2026, 9, 8), 2, "STANDARD", false, true, true, "加急入仓");
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ class InboundPlanPermissionContractTest {
|
||||
@Test
|
||||
void conversionUsesCanonicalInboundOrderCreateAuthority() {
|
||||
Method method = Arrays.stream(InboundPlanController.class.getDeclaredMethods())
|
||||
.filter(candidate -> candidate.getName().equals("convert"))
|
||||
.filter(candidate -> candidate.getName().equals("manual"))
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
|
||||
|
||||
+29
@@ -17,5 +17,34 @@ class InboundPlanOrderMapperContractTest {
|
||||
assertTrue(sql.contains("COALESCE(NULLIF(o.transfer_warehouse_code,''),MIN(NULLIF(d.destination_warehouse_code,'')))"));
|
||||
assertTrue(sql.contains("THEN 1 ELSE COUNT(DISTINCT"));
|
||||
assertTrue(sql.contains("0 AS destinationMismatchCount"));
|
||||
assertTrue(sql.contains("o.overseas_warehouse_id AS overseasWarehouseId"));
|
||||
assertTrue(sql.contains("o.overseas_warehouse_code AS overseasWarehouseCode"));
|
||||
assertTrue(sql.contains("o.overseas_warehouse_name AS overseasWarehouseName"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void receivingWarehouseSnapshotIsReadFromTenantScopedOrderFact() throws Exception {
|
||||
Select select = InboundPlanOrderMapper.class
|
||||
.getMethod("findReceivingWarehouse", String.class, Long.class)
|
||||
.getAnnotation(Select.class);
|
||||
String sql = String.join(" ", select.value());
|
||||
|
||||
assertTrue(sql.contains("overseas_warehouse_code AS overseasWarehouseCode"));
|
||||
assertTrue(sql.contains("overseas_warehouse_name AS overseasWarehouseName"));
|
||||
assertTrue(sql.contains("customer_id=#{customerId} AND id=#{orderId}"));
|
||||
assertTrue(!sql.contains("destination_warehouse"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void manualPlanLocksEveryOrderInTheSelectedStableContainer() throws Exception {
|
||||
Select select = InboundPlanOrderMapper.class
|
||||
.getMethod("lockContainerWarehouseFacts", String.class, Long.class)
|
||||
.getAnnotation(Select.class);
|
||||
String sql = String.join(" ", select.value());
|
||||
|
||||
assertTrue(sql.contains("c.source_container_booking_id=target.source_container_booking_id"));
|
||||
assertTrue(sql.contains("target.customer_id=#{customerId} AND target.id=#{orderId}"));
|
||||
assertTrue(sql.contains("c.overseas_warehouse_id AS overseasWarehouseId"));
|
||||
assertTrue(sql.contains("ORDER BY c.customer_id,c.id FOR UPDATE"));
|
||||
}
|
||||
}
|
||||
|
||||
+70
-1
@@ -11,6 +11,7 @@ import org.springframework.web.server.ResponseStatusException;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
|
||||
import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus;
|
||||
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
|
||||
|
||||
class HttpWarehouseResourceGatewayTest {
|
||||
@@ -32,7 +33,9 @@ class HttpWarehouseResourceGatewayTest {
|
||||
.andRespond(withSuccess("""
|
||||
{"code":"200","data":{"palletId":2092040891261427713,"palletCode":"PAL-1",
|
||||
"warehouseId":2092040869757231105,"locationId":2092040891076878337,
|
||||
"status":"IDLE","fbaWarehouseCode":"ABE3","version":3}}
|
||||
"status":"IDLE","fbaWarehouseCode":"ABE3","version":3,
|
||||
"locationType":"STORAGE","locationStatus":"IDLE","reservable":true,
|
||||
"reservationBlockReason":null}}
|
||||
""", MediaType.APPLICATION_JSON));
|
||||
|
||||
WarehouseResourceGateway.WarehousePalletReference result = gateway.resolvePallet("PAL-1");
|
||||
@@ -43,6 +46,40 @@ class HttpWarehouseResourceGatewayTest {
|
||||
server.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveRejectsPalletWhoseLocationIsNotReservable() {
|
||||
server.expect(requestTo("http://warehouse/internal/v1/warehouse-pallets/resolve/PAL-BLOCKED"))
|
||||
.andRespond(withSuccess("""
|
||||
{"code":"200","data":{"palletId":2092040891261427713,"palletCode":"PAL-BLOCKED",
|
||||
"warehouseId":2092040869757231105,"locationId":2092040891076878337,
|
||||
"status":"IDLE","fbaWarehouseCode":"ABE3","version":3,
|
||||
"locationType":"STORAGE","locationStatus":"OCCUPIED","reservable":false,
|
||||
"reservationBlockReason":"LOCATION_NOT_IDLE"}}
|
||||
""", MediaType.APPLICATION_JSON));
|
||||
|
||||
assertThatThrownBy(() -> gateway.resolvePallet("PAL-BLOCKED"))
|
||||
.isInstanceOf(ResponseStatusException.class)
|
||||
.hasMessageContaining("409")
|
||||
.hasMessageContaining("Pallet location is not idle");
|
||||
server.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveFailsClosedWhenWarehouseOmitsReservationFacts() {
|
||||
server.expect(requestTo("http://warehouse/internal/v1/warehouse-pallets/resolve/PAL-UNKNOWN"))
|
||||
.andRespond(withSuccess("""
|
||||
{"code":"200","data":{"palletId":2092040891261427713,"palletCode":"PAL-UNKNOWN",
|
||||
"warehouseId":2092040869757231105,"locationId":2092040891076878337,
|
||||
"status":"IDLE","fbaWarehouseCode":"ABE3","version":3}}
|
||||
""", MediaType.APPLICATION_JSON));
|
||||
|
||||
assertThatThrownBy(() -> gateway.resolvePallet("PAL-UNKNOWN"))
|
||||
.isInstanceOf(ResponseStatusException.class)
|
||||
.hasMessageContaining("409")
|
||||
.hasMessageContaining("availability could not be verified");
|
||||
server.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
void ensureDropshipStillMapsWarehouseEntityId() {
|
||||
server.expect(requestTo("http://warehouse/internal/v1/warehouse-pallets/ensure-dropship"))
|
||||
@@ -72,4 +109,36 @@ class HttpWarehouseResourceGatewayTest {
|
||||
server.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
void reserveMapsWarehouseLocationConflictToStableConflict() {
|
||||
server.expect(requestTo("http://warehouse/internal/v1/warehouse-resource-locks"))
|
||||
.andRespond(withStatus(org.springframework.http.HttpStatus.CONFLICT)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body("""
|
||||
{"code":"A0409","message":"Location is not idle","data":null}
|
||||
"""));
|
||||
|
||||
assertThatThrownBy(() -> gateway.reserve("1", 2L, 3L, 4L,
|
||||
"LASTLEG_INBOUND", "plan:5", "reservation-1"))
|
||||
.isInstanceOf(ResponseStatusException.class)
|
||||
.hasMessageContaining("409")
|
||||
.hasMessageContaining("Warehouse resource reservation was rejected");
|
||||
server.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
void reserveMapsBusinessConflictEnvelopeToStableConflict() {
|
||||
server.expect(requestTo("http://warehouse/internal/v1/warehouse-resource-locks"))
|
||||
.andRespond(withSuccess("""
|
||||
{"code":"A0409","message":"Location is not idle","data":null}
|
||||
""", MediaType.APPLICATION_JSON));
|
||||
|
||||
assertThatThrownBy(() -> gateway.reserve("1", 2L, 3L, 4L,
|
||||
"LASTLEG_INBOUND", "plan:5", "reservation-1"))
|
||||
.isInstanceOf(ResponseStatusException.class)
|
||||
.hasMessageContaining("409")
|
||||
.hasMessageContaining("Location is not idle");
|
||||
server.verify();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+37
@@ -167,6 +167,41 @@ class InboundPdaWorkflowServiceTest {
|
||||
targetPallet.getWarehouseLocationId(), "assign-wrong-warehouse")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolvedAssignmentReplacesPlanPalletNumberWithPhysicalWarehouseCode() {
|
||||
targetPallet.setPalletNo("IP-ACY2-20260907-597057-P001");
|
||||
targetPallet.setWarehousePalletId(null);
|
||||
targetPallet.setWarehouseLocationId(null);
|
||||
targetPallet.setWarehouseResourceLockId(null);
|
||||
targetPallet.setAssignmentStatus("PENDING_ASSIGNMENT");
|
||||
WarehouseResourceGateway.WarehousePalletReference resource =
|
||||
new WarehouseResourceGateway.WarehousePalletReference(188L, "A002", 88L, 288L,
|
||||
"IDLE", "ACY2", 1);
|
||||
when(warehouse.reserve("100", 88L, 188L, 288L, "INBOUND_CONTAINER_DESTINATION_PALLET",
|
||||
"CONT-1:WH-A", "INBOUND_CONTAINER_DESTINATION_PALLET:CONT-1:WH-A:188")).thenReturn(999L);
|
||||
|
||||
service.assignResolvedPallet("100", plan, targetPallet, resource);
|
||||
|
||||
assertEquals("A002", targetPallet.getPalletNo());
|
||||
assertEquals(188L, targetPallet.getWarehousePalletId());
|
||||
assertEquals(288L, targetPallet.getWarehouseLocationId());
|
||||
verify(repository).update(targetPallet);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolvedAssignmentRejectsPhysicalPalletWithoutBusinessCodeBeforeReservation() {
|
||||
targetPallet.setWarehouseResourceLockId(null);
|
||||
WarehouseResourceGateway.WarehousePalletReference resource =
|
||||
new WarehouseResourceGateway.WarehousePalletReference(188L, " ", 88L, 288L,
|
||||
"IDLE", "ACY2", 1);
|
||||
|
||||
ResponseStatusException exception = assertThrows(ResponseStatusException.class,
|
||||
() -> service.assignResolvedPallet("100", plan, targetPallet, resource));
|
||||
|
||||
assertEquals(409, exception.getStatusCode().value());
|
||||
verify(warehouse, never()).reserve(any(), anyLong(), anyLong(), anyLong(), any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void cartonScanBindsOnlyExpectedCartonAndRemoveMakesItScannableAgain() {
|
||||
InboundPlanCarton carton = carton("BOX-001", "UNASSIGNED", targetLine.getId(), "WH-A");
|
||||
@@ -522,6 +557,8 @@ class InboundPdaWorkflowServiceTest {
|
||||
@Test
|
||||
void dropshipTaskIsMarkedAndWholePalletPickReceivesThePlannedPalletAtOnce() {
|
||||
plan.setSourceOrderId(101L);
|
||||
targetPallet.setPalletNo("WH-PALLET-21");
|
||||
targetPallet.setSourcePalletNo("PLT-21");
|
||||
LastlegOrder order = new LastlegOrder();
|
||||
order.setId(101L); order.setOrderType("ONE_CLICK_DROPSHIP");
|
||||
when(repository.order(101L)).thenReturn(order);
|
||||
|
||||
+19
-2
@@ -14,6 +14,7 @@ import com.baiyue.lastleg.inboundplan.pda.mapper.ReceivingSkuReceiptMapper;
|
||||
import com.baiyue.lastleg.inventory.command.dto.InventoryCommandRequests;
|
||||
import com.baiyue.lastleg.inventory.command.dto.InventoryCommandResponse;
|
||||
import com.baiyue.lastleg.inventory.command.service.InventoryCommandService;
|
||||
import com.baiyue.lastleg.inboundplan.pda.gateway.WarehouseResourceGateway;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderDetailMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper;
|
||||
@@ -50,6 +51,7 @@ class InboundStockReceiptServiceTest {
|
||||
@Mock InboundStockReceiptMapper receipts;
|
||||
@Mock ReceivingSkuReceiptMapper receivingSkuReceipts;
|
||||
@Mock InventoryCommandService inventory;
|
||||
@Mock WarehouseResourceGateway warehouseGateway;
|
||||
@Mock DropshipOrderService dropshipOrders;
|
||||
@Mock LastlegOrderStatusHistoryMapper orderHistories;
|
||||
private InboundStockReceiptService service;
|
||||
@@ -62,6 +64,12 @@ class InboundStockReceiptServiceTest {
|
||||
ReflectionTestUtils.setField(service, "receipts", receipts);
|
||||
ReflectionTestUtils.setField(service, "receivingSkuReceipts", receivingSkuReceipts);
|
||||
ReflectionTestUtils.setField(service, "inventory", inventory);
|
||||
ReflectionTestUtils.setField(service, "warehouseGateway", warehouseGateway);
|
||||
org.mockito.Mockito.lenient().when(warehouseGateway.resolveLocation(eq(900L), anyLong())).thenAnswer(invocation -> {
|
||||
long locationId = invocation.getArgument(1);
|
||||
return new WarehouseResourceGateway.WarehouseLocationReference(locationId,
|
||||
"PDA-LAX-" + locationId, "STORAGE", BigDecimal.TEN, "OCCUPIED");
|
||||
});
|
||||
ReflectionTestUtils.setField(service, "dropshipOrders", dropshipOrders);
|
||||
ReflectionTestUtils.setField(service, "orderHistories", orderHistories);
|
||||
when(receipts.listTask("100", 2L)).thenReturn(List.of());
|
||||
@@ -86,11 +94,15 @@ class InboundStockReceiptServiceTest {
|
||||
assertEquals("SKU-1", request.getValue().skuCode());
|
||||
assertEquals("Customer", request.getValue().customerName());
|
||||
assertEquals("900", request.getValue().overseasWarehouseId());
|
||||
assertEquals("US-LAX-01", request.getValue().overseasWarehouseCode());
|
||||
assertEquals("Los Angeles", request.getValue().overseasWarehouseName());
|
||||
assertEquals("PDA-LAX-3001", request.getValue().warehouseLocationCode());
|
||||
assertEquals("PLT-21", request.getValue().palletNo());
|
||||
ArgumentCaptor<InboundStockReceipt> receipt = ArgumentCaptor.forClass(InboundStockReceipt.class);
|
||||
verify(receipts).insert(receipt.capture());
|
||||
assertEquals(5L, receipt.getValue().getReceivedPackages());
|
||||
assertEquals(10L, receipt.getValue().getInventoryPositionId());
|
||||
assertEquals("PLT-21", receipt.getValue().getPalletNo());
|
||||
assertEquals("INBOUND_COMPLETED", fixture.order.getCurrentStatus());
|
||||
assertEquals("INBOUND_PLAN:COMPLETED", fixture.order.getSubOrderStatus());
|
||||
ArgumentCaptor<LastlegOrderStatusHistory> history = ArgumentCaptor.forClass(LastlegOrderStatusHistory.class);
|
||||
@@ -216,6 +228,7 @@ class InboundStockReceiptServiceTest {
|
||||
void activatesDropshipAfterGenericStockInReceiptIsCreated() {
|
||||
Fixture fixture = fixture(5, 5, 5, 10);
|
||||
fixture.order.setOrderType("ONE_CLICK_DROPSHIP");
|
||||
fixture.pallet.setPalletNo("WH-PALLET-21");
|
||||
when(dropshipOrders.palletSnapshots("100", 101L)).thenReturn(List.of(dropshipPallet("PLT-21", 5)));
|
||||
when(orders.selectBatchIds(any())).thenReturn(List.of(fixture.order));
|
||||
when(inventory.stockIn(eq("100"), eq("operator"), any(InventoryCommandRequests.StockIn.class)))
|
||||
@@ -228,6 +241,8 @@ class InboundStockReceiptServiceTest {
|
||||
verify(dropshipOrders).activateAfterStockIn(eq("100"), eq(101L), eq(2L), activationReceipts.capture());
|
||||
assertEquals(1, activationReceipts.getValue().size());
|
||||
assertEquals(10L, activationReceipts.getValue().get(0).getInventoryPositionId());
|
||||
assertEquals("PLT-21", activationReceipts.getValue().get(0).getSourcePalletNo());
|
||||
assertEquals("WH-PALLET-21", activationReceipts.getValue().get(0).getPalletNo());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -292,7 +307,8 @@ class InboundStockReceiptServiceTest {
|
||||
line.setRequiredPackageCount(required); line.setVolumeCbm(new BigDecimal("1.0"));
|
||||
LastlegOrder order = new LastlegOrder(); order.setId(101L); order.setCustomerId("100");
|
||||
order.setOrderNo("LMO-1"); order.setCustomerName("Customer"); order.setSourceWarehouseId("100");
|
||||
order.setOverseasWarehouseId("900"); order.setOverseasWarehouseName("Los Angeles");
|
||||
order.setOverseasWarehouseId("900"); order.setOverseasWarehouseCode("US-LAX-01");
|
||||
order.setOverseasWarehouseName("Los Angeles");
|
||||
order.setSourceEventId("evt-1"); order.setFbaWarehouseCode("ONT8");
|
||||
order.setCurrentStatus("INBOUND_PLANNED");
|
||||
org.mockito.Mockito.lenient().when(orders.lockById("100", 101L)).thenReturn(order);
|
||||
@@ -309,7 +325,8 @@ class InboundStockReceiptServiceTest {
|
||||
|
||||
private InboundPlanPallet pallet(long id, long warehousePalletId) {
|
||||
InboundPlanPallet pallet = new InboundPlanPallet(); pallet.setId(id); pallet.setPlanId(1L);
|
||||
pallet.setPalletNo("PLT-" + id); pallet.setWarehousePalletId(warehousePalletId);
|
||||
pallet.setPalletNo("PLT-" + id); pallet.setSourcePalletNo("PLT-" + id);
|
||||
pallet.setWarehousePalletId(warehousePalletId);
|
||||
pallet.setWarehouseLocationId(3001L); return pallet;
|
||||
}
|
||||
|
||||
|
||||
+68
-6
@@ -112,9 +112,6 @@ class ReceivingContainerServiceBehaviorTest {
|
||||
when(sessions.insert(any(ReceivingContainerSession.class))).thenReturn(1);
|
||||
when(operations.insert(any(ReceivingContainerOperation.class))).thenReturn(1);
|
||||
when(warehouse.requireActiveSharedWarehouse(100L)).thenReturn(warehouseReference());
|
||||
when(plans.claimReceivingWarehouse("100", 2L, 100L)).thenReturn(1);
|
||||
InboundPlan claimed = plan("CONT-A"); claimed.setOverseasWarehouseId(100L);
|
||||
when(plans.selectById(2L)).thenReturn(claimed);
|
||||
var response = service.prepare("7", "receiver", 100L, "CONT-A",
|
||||
new ReceivingContainerRequests.Action("same-key"));
|
||||
assertThat(response.overseasWarehouseId()).isEqualTo(100L);
|
||||
@@ -179,6 +176,27 @@ class ReceivingContainerServiceBehaviorTest {
|
||||
.containsExactly("CONT-EARLY", "CONT-LATE", "CONT-NULL", "CONT-BLOCKED");
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidWarehouseSnapshotOnlyBlocksItsContainerWithoutFailingHealthyCandidates() {
|
||||
InboundPlan brokenPlan = plan("CONT-BROKEN");
|
||||
InboundPlan healthyPlan = plan("CONT-HEALTHY"); healthyPlan.setId(3L);
|
||||
InboundPlanLine brokenLine = line("FBA");
|
||||
InboundPlanLine healthyLine = line("FBA"); healthyLine.setId(11L); healthyLine.setOrderId(2L); healthyLine.setPlanId(3L);
|
||||
LastlegOrder broken = containerOrder(1L, "100", "CONT-BROKEN", "BOOKING-A", "IN-1");
|
||||
broken.setOverseasWarehouseCode(null);
|
||||
LastlegOrder healthy = containerOrder(2L, "100", "CONT-HEALTHY", "BOOKING-B", "IN-2");
|
||||
when(plans.selectList(any())).thenReturn(List.of(brokenPlan, healthyPlan));
|
||||
when(lines.selectList(any())).thenReturn(List.of(brokenLine, healthyLine));
|
||||
when(orders.selectList(any())).thenReturn(List.of(broken, healthy));
|
||||
|
||||
var result = service.candidates("7", 100L);
|
||||
|
||||
assertThat(result).extracting("containerNo").containsExactly("CONT-HEALTHY", "CONT-BROKEN");
|
||||
assertThat(result.get(0).operable()).isTrue();
|
||||
assertThat(result.get(1).operable()).isFalse();
|
||||
assertThat(result.get(1).blockReason()).isEqualTo("柜内接柜仓业务快照不一致");
|
||||
}
|
||||
|
||||
@Test
|
||||
void candidatesIncludeUnassignedAndSameWarehouseButHideOtherWarehouse() {
|
||||
InboundPlan unassigned = plan("CONT-A");
|
||||
@@ -198,6 +216,47 @@ class ReceivingContainerServiceBehaviorTest {
|
||||
.containsExactly("CONT-A", "CONT-B");
|
||||
}
|
||||
|
||||
@Test
|
||||
void visibleCandidateWithUnassignedPlanCanOpenDetailInTheSameWarehouse() {
|
||||
InboundPlan unassigned = plan("TSTU0000204");
|
||||
unassigned.setOverseasWarehouseId(null);
|
||||
InboundPlanLine planLine = line("FBA");
|
||||
LastlegOrder order = containerOrder(1L, "100", "TSTU0000204", "BOOKING-204", "IN-204");
|
||||
when(plans.selectList(any())).thenReturn(List.of(unassigned));
|
||||
when(lines.selectList(any())).thenReturn(List.of(planLine));
|
||||
when(orders.selectList(any())).thenReturn(List.of(order));
|
||||
when(orders.selectById(1L)).thenReturn(order);
|
||||
when(warehouse.requireActiveSharedWarehouse(100L)).thenReturn(warehouseReference());
|
||||
|
||||
var candidate = service.candidates("7", 100L).get(0);
|
||||
var detail = service.detail("7", 100L, "TSTU0000204");
|
||||
|
||||
assertThat(candidate.operable()).isTrue();
|
||||
assertThat(detail.overseasWarehouseId()).isEqualTo(100L);
|
||||
assertThat(detail.overseasWarehouseCode()).isEqualTo("SHARED-LAX");
|
||||
}
|
||||
|
||||
@Test
|
||||
void prepareAtomicallyBindsUnassignedPlansToThePickerWarehouse() {
|
||||
InboundPlan unassigned = plan("TSTU0195472");
|
||||
unassigned.setOverseasWarehouseId(null);
|
||||
InboundPlanLine planLine = line("FBA");
|
||||
LastlegOrder order = containerOrder(1L, "100", "TSTU0195472", "BOOKING-472", "IN-472");
|
||||
when(plans.selectList(any())).thenReturn(List.of(unassigned));
|
||||
when(plans.updateById(unassigned)).thenReturn(1);
|
||||
when(lines.selectList(any())).thenReturn(List.of(planLine));
|
||||
when(orders.selectList(any())).thenReturn(List.of(order));
|
||||
when(orders.selectById(1L)).thenReturn(order);
|
||||
when(warehouse.requireActiveSharedWarehouse(100L)).thenReturn(warehouseReference());
|
||||
|
||||
var result = service.prepare("7", "receiver", 100L, "TSTU0195472",
|
||||
new ReceivingContainerRequests.Action("bind-warehouse"));
|
||||
|
||||
assertThat(result.overseasWarehouseId()).isEqualTo(100L);
|
||||
assertThat(unassigned.getOverseasWarehouseId()).isEqualTo(100L);
|
||||
verify(plans).updateById(unassigned);
|
||||
}
|
||||
|
||||
@Test
|
||||
void candidatesHideContainersReservedByAnotherOperator() {
|
||||
InboundPlan available = plan("CONT-A");
|
||||
@@ -652,7 +711,7 @@ class ReceivingContainerServiceBehaviorTest {
|
||||
private static InboundPlan plan(String containerNo) {
|
||||
InboundPlan value = new InboundPlan();
|
||||
value.setId(2L); value.setPlanNo("PLAN-1"); value.setCustomerId("100"); value.setContainerNo(containerNo);
|
||||
value.setStatus("SIGNED");
|
||||
value.setStatus("SIGNED"); value.setOverseasWarehouseId(100L);
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -668,7 +727,9 @@ class ReceivingContainerServiceBehaviorTest {
|
||||
private static LastlegOrder order(String destinationType) {
|
||||
LastlegOrder value = new LastlegOrder();
|
||||
value.setId(1L); value.setCustomerId("100"); value.setDestinationType(destinationType);
|
||||
value.setOverseasWarehouseId("100"); value.setFbaWarehouseCode("FBA".equals(destinationType) ? "FBA-1" : null);
|
||||
value.setOverseasWarehouseId("100"); value.setOverseasWarehouseCode("SHARED-LAX");
|
||||
value.setOverseasWarehouseName("Shared LAX");
|
||||
value.setFbaWarehouseCode("FBA".equals(destinationType) ? "FBA-1" : null);
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -690,7 +751,8 @@ class ReceivingContainerServiceBehaviorTest {
|
||||
private static ReceivingContainerSession session(String containerNo, String status) {
|
||||
ReceivingContainerSession value = new ReceivingContainerSession();
|
||||
value.setId(8L); value.setCustomerId("100"); value.setContainerNo(containerNo); value.setOperatorKey("7");
|
||||
value.setOverseasWarehouseId(100L); value.setOverseasWarehouseName("Shared LAX");
|
||||
value.setOverseasWarehouseId(100L); value.setOverseasWarehouseCode("SHARED-LAX");
|
||||
value.setOverseasWarehouseName("Shared LAX");
|
||||
value.setStatus(status); value.setVersion(0);
|
||||
return value;
|
||||
}
|
||||
|
||||
+9
-3
@@ -48,9 +48,9 @@ class InboundPlanPaginationTest {
|
||||
null, "PENDING_ASSIGNMENT", null, null, "ONT", "ONT8", "UNAPPOINTED", "UNSCHEDULED",
|
||||
LocalDateTime.of(2026, 8, 1, 0, 0), LocalDateTime.of(2026, 8, 31, 23, 59));
|
||||
InboundPlanListResponse row = new InboundPlanListResponse(9007199254740993L, 2L, "LIP2", "100",
|
||||
3L, "IN-1", "FIRST_LEG", "W1", "ONT", "安大略仓", "ONT8", "客户A", 10,
|
||||
BigDecimal.ONE, 1, "PENDING_ASSIGNMENT", null, null, null, null, "UNSCHEDULED",
|
||||
LocalDateTime.of(2026, 8, 20, 10, 0));
|
||||
3L, "IN-1", null, "FIRST_LEG", "US-LAX-01", "洛杉矶仓", "W1", "ONT", "安大略仓",
|
||||
"ONT8", "客户A", 10, 0, BigDecimal.ONE, 1, "PENDING_ASSIGNMENT", null, null, null,
|
||||
null, "UNSCHEDULED", LocalDateTime.of(2026, 8, 20, 10, 0), 0);
|
||||
when(queryMapper.count("100", query)).thenReturn(11L);
|
||||
when(queryMapper.rows("100", query, 10, 10)).thenReturn(List.of(row));
|
||||
|
||||
@@ -59,6 +59,8 @@ class InboundPlanPaginationTest {
|
||||
assertThat(result.records()).containsExactly(row);
|
||||
assertThat(result.records().get(0).lineId()).isEqualTo(9007199254740993L);
|
||||
assertThat(result.records().get(0).appointmentStatus()).isNull();
|
||||
assertThat(result.records().get(0).overseasWarehouseCode()).isEqualTo("US-LAX-01");
|
||||
assertThat(result.records().get(0).overseasWarehouseName()).isEqualTo("洛杉矶仓");
|
||||
assertThat(result.pages()).isEqualTo(2);
|
||||
verify(queryMapper).count("100", query);
|
||||
verify(queryMapper).rows("100", query, 10, 10);
|
||||
@@ -71,6 +73,10 @@ class InboundPlanPaginationTest {
|
||||
.getAnnotation(org.apache.ibatis.annotations.Select.class).value()) + InboundPlanQueryMapper.FROM_AND_FILTER;
|
||||
assertThat(sql).contains("a2.customer_id=#{customerId}", "ORDER BY a2.created_at DESC,ai2.id DESC LIMIT 1");
|
||||
assertThat(sql).contains("di.customer_id=#{customerId} AND </if>di.order_id=l.order_id");
|
||||
assertThat(sql).contains("o.overseas_warehouse_code overseasWarehouseCode",
|
||||
"o.overseas_warehouse_name overseasWarehouseName",
|
||||
"o.overseas_warehouse_id=#{q.overseasWarehouseId}");
|
||||
assertThat(sql).doesNotContain("l.destination_warehouse_id=#{q.overseasWarehouseId}");
|
||||
assertThat(sql).contains("q.planNo", "q.inboundCode", "q.status", "q.overseasWarehouseId",
|
||||
"q.destinationWarehouseCode", "q.warehouse",
|
||||
"q.fbaWarehouseCode", "q.appointmentStatus", "q.dispatchStatus", "q.createdFrom", "q.createdTo");
|
||||
|
||||
+213
-10
@@ -3,6 +3,7 @@ package com.baiyue.lastleg.inboundplan.service;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlan;
|
||||
import com.baiyue.lastleg.inboundplan.dto.InboundPlanRequests;
|
||||
import com.baiyue.lastleg.inboundplan.dto.UpdateInboundPlanRequest;
|
||||
import com.baiyue.lastleg.inboundplan.dto.ManualInboundPlanRequest;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlanLine;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlanPallet;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlanPalletAllocation;
|
||||
@@ -15,6 +16,9 @@ import com.baiyue.lastleg.inboundplan.model.SourceOrderSnapshot;
|
||||
import com.baiyue.lastleg.inventory.mapper.InventoryBalanceMapper;
|
||||
import com.baiyue.lastleg.dropship.model.DropshipPalletSnapshot;
|
||||
import com.baiyue.lastleg.dropship.service.DropshipOrderService;
|
||||
import com.baiyue.lastleg.inboundplan.pda.gateway.WarehouseResourceGateway;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderCommandRecordMapper;
|
||||
import com.fasterxml.jackson.databind.json.JsonMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
@@ -28,11 +32,13 @@ import org.springframework.test.util.ReflectionTestUtils;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalDate;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
@@ -47,6 +53,8 @@ class InboundPlanServiceTest {
|
||||
@Mock InboundPlanOrderMapper orderMapper;
|
||||
@Mock InventoryBalanceMapper inventoryBalanceMapper;
|
||||
@Mock DropshipOrderService dropshipOrders;
|
||||
@Mock WarehouseResourceGateway warehouseGateway;
|
||||
@Mock LastlegOrderCommandRecordMapper commandMapper;
|
||||
|
||||
private InboundPlanService service;
|
||||
|
||||
@@ -60,10 +68,22 @@ class InboundPlanServiceTest {
|
||||
ReflectionTestUtils.setField(service, "orderMapper", orderMapper);
|
||||
ReflectionTestUtils.setField(service, "inventoryBalanceMapper", inventoryBalanceMapper);
|
||||
ReflectionTestUtils.setField(service, "dropshipOrders", dropshipOrders);
|
||||
ReflectionTestUtils.setField(service, "warehouseGateway", warehouseGateway);
|
||||
ReflectionTestUtils.setField(service, "commandMapper", commandMapper);
|
||||
ReflectionTestUtils.setField(service, "objectMapper", JsonMapper.builder().findAndAddModules().build());
|
||||
ReflectionTestUtils.setField(service, "orderNoGenerator", new com.baiyue.lastleg.service.LastlegOrderNoGenerator());
|
||||
org.mockito.Mockito.lenient().when(lineMapper.selectList(any())).thenReturn(List.of());
|
||||
org.mockito.Mockito.lenient().when(palletMapper.selectList(any())).thenReturn(List.of());
|
||||
org.mockito.Mockito.lenient().when(allocationMapper.selectList(any())).thenReturn(List.of());
|
||||
org.mockito.Mockito.lenient().when(orderMapper.findReceivingWarehouse(any(), any())).thenReturn(
|
||||
new InboundPlanOrderMapper.ReceivingWarehouseSnapshot(
|
||||
"193000000000200003", "US-LAX-01", "洛杉矶仓"));
|
||||
org.mockito.Mockito.lenient().when(warehouseGateway.requireActiveSharedWarehouse(193000000000200003L)).thenReturn(
|
||||
new WarehouseResourceGateway.WarehouseReference(
|
||||
193000000000200003L, "US-LAX-01", "洛杉矶仓", "ACTIVE"));
|
||||
org.mockito.Mockito.lenient().when(orderMapper.lockContainerWarehouseFacts(any(), any())).thenReturn(List.of(
|
||||
new InboundPlanOrderMapper.ContainerWarehouseRef(
|
||||
"100", 2L, "BOOKING-1", "193000000000200003")));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -99,18 +119,107 @@ class InboundPlanServiceTest {
|
||||
.filter(item -> "PENDING_COUNT".equals(item.getStatus())).count());
|
||||
verify(orderMapper).markPlanned(any(), org.mockito.ArgumentMatchers.eq(2L), any(), org.mockito.ArgumentMatchers.anyInt(), any());
|
||||
verify(inventoryBalanceMapper).bindInboundPlan(org.mockito.ArgumentMatchers.eq("100"),
|
||||
org.mockito.ArgumentMatchers.eq("193000000000200003"),
|
||||
org.mockito.ArgumentMatchers.eq(2L), org.mockito.ArgumentMatchers.eq("RK-2"),
|
||||
org.mockito.ArgumentMatchers.eq(result.planNo()), any());
|
||||
verify(orderMapper).insertHistory(any(), org.mockito.ArgumentMatchers.eq(2L), any(), any(), any());
|
||||
verify(orderMapper).lockOrder("100", 2L);
|
||||
assertEquals(2L, result.sourceOrderId());
|
||||
assertEquals("MSCU6639870", result.containerNo());
|
||||
assertEquals("US-LAX-01", result.overseasWarehouseCode());
|
||||
assertEquals("洛杉矶仓", result.overseasWarehouseName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void manuallyCreatesOneIndependentPlanWithFrozenMetadata() {
|
||||
SourceOrderSnapshot source = order(2L, "RK-2", "1.0000", "WH-A");
|
||||
when(orderMapper.lockOrder("100", 2L)).thenReturn(List.of(source));
|
||||
when(warehouseGateway.requireActiveSharedWarehouse(193000000000200003L)).thenReturn(
|
||||
new WarehouseResourceGateway.WarehouseReference(
|
||||
193000000000200003L, "US-LAX-01", "洛杉矶仓", "ACTIVE"));
|
||||
when(orderMapper.markPlannedManual(eq("100"), eq(2L), any(), eq(2), eq(true), eq(0), any()))
|
||||
.thenReturn(1);
|
||||
ManualInboundPlanRequest request = new ManualInboundPlanRequest("100", 2L, 0,
|
||||
LocalDate.of(2026, 9, 15), 2, "STANDARD", true, true, true, "优先收货");
|
||||
|
||||
var result = service.convertManual(request, "manual-2", "900");
|
||||
|
||||
ArgumentCaptor<InboundPlan> plan = ArgumentCaptor.forClass(InboundPlan.class);
|
||||
verify(planMapper).insert(plan.capture());
|
||||
assertEquals(2L, plan.getValue().getSourceOrderId());
|
||||
assertEquals(LocalDate.of(2026, 9, 15), plan.getValue().getPlannedArrivalDate());
|
||||
assertEquals("STANDARD", plan.getValue().getPalletType());
|
||||
assertEquals("MANUAL", plan.getValue().getCreationMode());
|
||||
assertEquals(2, result.palletCount());
|
||||
verify(palletMapper, times(2)).insert(any(InboundPlanPallet.class));
|
||||
verify(orderMapper).markPlannedManual(eq("100"), eq(2L), any(), eq(2), eq(true), eq(0), any());
|
||||
verify(commandMapper).insert(any(com.baiyue.lastleg.entity.LastlegOrderCommandRecord.class));
|
||||
verify(orderMapper, never()).lockContainerOrders(any());
|
||||
verify(orderMapper).lockContainerWarehouseFacts("100", 2L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void manuallyCreatesPlanWhenStableWarehouseIdExistsWithoutDisplaySnapshots() {
|
||||
SourceOrderSnapshot source = new SourceOrderSnapshot(2L, "100", "MSCU6639870", "RK-2",
|
||||
"PENDING_ASSIGNMENT", null, BigDecimal.ONE, 10, 0,
|
||||
"193000000000200003", null, null, null,
|
||||
"WH-A", "WH-A", "仓A", 1, 0, false, 0);
|
||||
when(orderMapper.lockOrder("100", 2L)).thenReturn(List.of(source));
|
||||
when(warehouseGateway.requireActiveSharedWarehouse(193000000000200003L)).thenReturn(
|
||||
new WarehouseResourceGateway.WarehouseReference(
|
||||
193000000000200003L, "US-LAX-01", "洛杉矶仓", "ACTIVE"));
|
||||
when(orderMapper.markPlannedManual(eq("100"), eq(2L), any(), eq(3), eq(false), eq(0), any()))
|
||||
.thenReturn(1);
|
||||
|
||||
var result = service.convertManual(new ManualInboundPlanRequest("100", 2L, 0,
|
||||
LocalDate.of(2026, 9, 15), 3, "STANDARD", true, false, false, null),
|
||||
"manual-id-only", "900");
|
||||
|
||||
assertEquals("US-LAX-01", result.overseasWarehouseCode());
|
||||
assertEquals("洛杉矶仓", result.overseasWarehouseName());
|
||||
verify(warehouseGateway).requireActiveSharedWarehouse(193000000000200003L);
|
||||
verify(planMapper).insert(any(InboundPlan.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void manualPlanRejectsMixedContainerWarehousesBeforeLockingOrderOrWriting() {
|
||||
when(orderMapper.lockContainerWarehouseFacts("100", 2L)).thenReturn(List.of(
|
||||
new InboundPlanOrderMapper.ContainerWarehouseRef("100", 2L, "BOOKING-1", "193000000000200003"),
|
||||
new InboundPlanOrderMapper.ContainerWarehouseRef("200", 3L, "BOOKING-1", "193000000000200004")));
|
||||
|
||||
ResponseStatusException error = assertThrows(ResponseStatusException.class, () -> service.convertManual(
|
||||
new ManualInboundPlanRequest("100", 2L, 0, LocalDate.of(2026, 9, 15),
|
||||
1, "STANDARD", false, false, false, null), "mixed-warehouse", "900"));
|
||||
|
||||
assertEquals(409, error.getStatusCode().value());
|
||||
verify(orderMapper, never()).lockOrder(any(), any());
|
||||
verify(planMapper, never()).insert(any(InboundPlan.class));
|
||||
verify(commandMapper, never()).insert(any(com.baiyue.lastleg.entity.LastlegOrderCommandRecord.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void refusesManualPlanForHeldOrderBeforeWriting() {
|
||||
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);
|
||||
when(orderMapper.lockOrder("100", 2L)).thenReturn(List.of(held));
|
||||
|
||||
ResponseStatusException error = assertThrows(ResponseStatusException.class, () -> service.convertManual(
|
||||
new ManualInboundPlanRequest("100", 2L, 5, LocalDate.of(2026, 9, 15),
|
||||
1, "STANDARD", false, false, false, null),
|
||||
"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));
|
||||
}
|
||||
|
||||
@Test
|
||||
void convertsAConsistentDestinationAssignedOrder() {
|
||||
SourceOrderSnapshot assigned = new SourceOrderSnapshot(2L, "100", "MSCU6639870", "RK-2",
|
||||
"DESTINATION_ASSIGNED", null, new BigDecimal("1.0000"), 10,
|
||||
"DESTINATION_ASSIGNED", null, new BigDecimal("1.0000"), 10, 0,
|
||||
"193000000000200003", "US-LAX-01", "洛杉矶仓", null,
|
||||
"2090000000000000001", "LAX-02", "Los Angeles 02", 1, 0);
|
||||
when(orderMapper.lockOrder("100", 2L)).thenReturn(List.of(assigned));
|
||||
when(orderMapper.markPlanned(any(), any(), any(), org.mockito.ArgumentMatchers.anyInt(), any()))
|
||||
@@ -145,7 +254,35 @@ class InboundPlanServiceTest {
|
||||
assertEquals(1L, result.sourceOrderId());
|
||||
verify(planMapper, never()).insert(any(InboundPlan.class));
|
||||
verify(orderMapper, never()).markPlanned(any(), any(), any(), org.mockito.ArgumentMatchers.anyInt(), any());
|
||||
verify(inventoryBalanceMapper, never()).bindInboundPlan(any(), any(), any(), any(), any());
|
||||
verify(inventoryBalanceMapper, never()).bindInboundPlan(any(), any(), any(), any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void detailReturnsReceivingWarehouseFromTenantScopedOrderFact() {
|
||||
InboundPlan plan = existingPlan("100", 1L, "LIP-1");
|
||||
when(planMapper.selectOne(any())).thenReturn(plan);
|
||||
when(orderMapper.findReceivingWarehouse("100", 1L)).thenReturn(
|
||||
new InboundPlanOrderMapper.ReceivingWarehouseSnapshot("88", "US-NY-01", "纽约仓"));
|
||||
when(warehouseGateway.requireActiveSharedWarehouse(88L)).thenReturn(
|
||||
new WarehouseResourceGateway.WarehouseReference(88L, "US-NY-01", "纽约仓", "ACTIVE"));
|
||||
|
||||
var result = service.detailByPlanNo("100", "LIP-1");
|
||||
|
||||
assertEquals("US-NY-01", result.overseasWarehouseCode());
|
||||
assertEquals("纽约仓", result.overseasWarehouseName());
|
||||
verify(orderMapper).findReceivingWarehouse("100", 1L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void detailFailsClosedWhenReceivingWarehouseFactIsMissing() {
|
||||
InboundPlan plan = existingPlan("100", 1L, "LIP-1");
|
||||
when(planMapper.selectOne(any())).thenReturn(plan);
|
||||
when(orderMapper.findReceivingWarehouse("100", 1L)).thenReturn(null);
|
||||
|
||||
ResponseStatusException error = assertThrows(ResponseStatusException.class,
|
||||
() -> service.detailByPlanNo("100", "LIP-1"));
|
||||
|
||||
assertEquals(409, error.getStatusCode().value());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -160,7 +297,9 @@ class InboundPlanServiceTest {
|
||||
@Test
|
||||
void rejectsMissingDestinationWarehouse() {
|
||||
SourceOrderSnapshot invalid = new SourceOrderSnapshot(1L, "100", "MSCU6639870", "RK-1",
|
||||
"PENDING_ASSIGNMENT", null, BigDecimal.ONE, 1, null, null, null, 1, 0);
|
||||
"PENDING_ASSIGNMENT", null, BigDecimal.ONE, 1, 0,
|
||||
"193000000000200003", "US-LAX-01", "洛杉矶仓", null,
|
||||
null, null, null, 1, 0);
|
||||
when(orderMapper.lockOrder("100", 1L)).thenReturn(List.of(invalid));
|
||||
|
||||
assertThrows(ResponseStatusException.class, () -> service.convert("100", 1L));
|
||||
@@ -190,7 +329,9 @@ class InboundPlanServiceTest {
|
||||
@Test
|
||||
void transferWarehouseOverridesOriginalCargoDestination() {
|
||||
SourceOrderSnapshot transferred = new SourceOrderSnapshot(1L, "100", "MSCU6639870", "RK-1",
|
||||
"DESTINATION_ASSIGNED", null, BigDecimal.ONE, 1, "WH-B", "WH-B", "Warehouse B", 1, 1);
|
||||
"DESTINATION_ASSIGNED", null, BigDecimal.ONE, 1, 0,
|
||||
"193000000000200003", "US-LAX-01", "洛杉矶仓", null,
|
||||
"WH-B", "WH-B", "Warehouse B", 1, 1);
|
||||
when(orderMapper.lockOrder("100", 1L)).thenReturn(List.of(transferred));
|
||||
when(orderMapper.markPlanned(any(), any(), any(), org.mockito.ArgumentMatchers.anyInt(), any()))
|
||||
.thenReturn(1);
|
||||
@@ -217,6 +358,8 @@ class InboundPlanServiceTest {
|
||||
verify(palletMapper, times(2)).insert(pallets.capture());
|
||||
assertEquals(List.of("SOURCE-P-1", "SOURCE-P-2"),
|
||||
pallets.getAllValues().stream().map(InboundPlanPallet::getPalletNo).toList());
|
||||
assertEquals(List.of("SOURCE-P-1", "SOURCE-P-2"),
|
||||
pallets.getAllValues().stream().map(InboundPlanPallet::getSourcePalletNo).toList());
|
||||
ArgumentCaptor<InboundPlanPalletAllocation> allocations =
|
||||
ArgumentCaptor.forClass(InboundPlanPalletAllocation.class);
|
||||
verify(allocationMapper, times(2)).insert(allocations.capture());
|
||||
@@ -318,6 +461,8 @@ class InboundPlanServiceTest {
|
||||
verify(palletMapper, times(2)).insert(pallets.capture());
|
||||
assertEquals(List.of("SOURCE-P-1", "SOURCE-P-2"),
|
||||
pallets.getAllValues().stream().map(InboundPlanPallet::getPalletNo).toList());
|
||||
assertEquals(List.of("SOURCE-P-1", "SOURCE-P-2"),
|
||||
pallets.getAllValues().stream().map(InboundPlanPallet::getSourcePalletNo).toList());
|
||||
ArgumentCaptor<InboundPlanPalletAllocation> allocations =
|
||||
ArgumentCaptor.forClass(InboundPlanPalletAllocation.class);
|
||||
verify(allocationMapper, times(2)).insert(allocations.capture());
|
||||
@@ -332,13 +477,35 @@ class InboundPlanServiceTest {
|
||||
new InboundPlanOrderMapper.ContainerOrderRef("100", 2L, "PENDING_ASSIGNMENT", "MSCU6639870", "STANDARD")));
|
||||
when(orderMapper.lockOrder("100", 1L)).thenReturn(List.of(order(1L, "RK-1", "1", "WH-A")));
|
||||
when(orderMapper.lockOrder("100", 2L)).thenReturn(List.of(new SourceOrderSnapshot(2L, "100", "MSCU6639870", "RK-2",
|
||||
"PENDING_ASSIGNMENT", null, BigDecimal.ONE, 1, null, null, null, 1, 0)));
|
||||
"PENDING_ASSIGNMENT", null, BigDecimal.ONE, 1, 0,
|
||||
"193000000000200003", "US-LAX-01", "洛杉矶仓", null,
|
||||
null, null, null, 1, 0)));
|
||||
|
||||
assertThrows(ResponseStatusException.class, () -> service.convertContainer("BOOKING-1"));
|
||||
|
||||
verify(planMapper, never()).insert(any(InboundPlan.class));
|
||||
verify(orderMapper, never()).markPlanned(any(), any(), any(), org.mockito.ArgumentMatchers.anyInt(), any());
|
||||
verify(inventoryBalanceMapper, never()).bindInboundPlan(any(), any(), any(), any(), any());
|
||||
verify(inventoryBalanceMapper, never()).bindInboundPlan(any(), any(), any(), any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void containerConversionRejectsMixedWarehouseSnapshotsBeforeCreatingAnyPlan() {
|
||||
when(orderMapper.lockContainerOrders("BOOKING-1")).thenReturn(List.of(
|
||||
new InboundPlanOrderMapper.ContainerOrderRef("100", 1L, "PENDING_ASSIGNMENT", "MSCU6639870", "STANDARD"),
|
||||
new InboundPlanOrderMapper.ContainerOrderRef("200", 2L, "PENDING_ASSIGNMENT", "MSCU6639870", "STANDARD")));
|
||||
when(orderMapper.lockOrder("100", 1L)).thenReturn(List.of(order("100", 1L, "RK-1", "WH-A")));
|
||||
SourceOrderSnapshot differentWarehouse = new SourceOrderSnapshot(2L, "200", "MSCU6639870", "RK-2",
|
||||
"PENDING_ASSIGNMENT", null, BigDecimal.ONE, 10, 0,
|
||||
"193000000000200004", null, null, null, "WH-B", "WH-B", "仓B", 1, 0, false, 0);
|
||||
when(orderMapper.lockOrder("200", 2L)).thenReturn(List.of(differentWarehouse));
|
||||
|
||||
ResponseStatusException error = assertThrows(ResponseStatusException.class,
|
||||
() -> service.convertContainer("BOOKING-1"));
|
||||
|
||||
assertEquals(409, error.getStatusCode().value());
|
||||
verify(planMapper, never()).insert(any(InboundPlan.class));
|
||||
verify(orderMapper, never()).markPlanned(any(), any(), any(), org.mockito.ArgumentMatchers.anyInt(), any());
|
||||
verify(inventoryBalanceMapper, never()).bindInboundPlan(any(), any(), any(), any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -358,16 +525,50 @@ class InboundPlanServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void partialExistingPlansAreRejectedBeforeAnyWrite() {
|
||||
void partialExistingPlansArePreservedWhileOnlyRemainingOrdersAreCreated() {
|
||||
when(orderMapper.lockContainerOrders("BOOKING-1")).thenReturn(List.of(
|
||||
new InboundPlanOrderMapper.ContainerOrderRef("100", 1L, "INBOUND_PLANNED", "MSCU6639870", "STANDARD"),
|
||||
new InboundPlanOrderMapper.ContainerOrderRef("100", 2L, "PENDING_ASSIGNMENT", "MSCU6639870", "STANDARD")));
|
||||
when(planMapper.findBySourceOrder("100", 1L)).thenReturn(existingPlan("100", 1L, "LIP-1"));
|
||||
when(orderMapper.lockOrder("100", 2L)).thenReturn(List.of(order("100", 2L, "RK-2", "WH-B")));
|
||||
when(orderMapper.markPlanned(eq("100"), eq(2L), any(), org.mockito.ArgumentMatchers.anyInt(), any()))
|
||||
.thenReturn(1);
|
||||
|
||||
assertThrows(ResponseStatusException.class, () -> service.convertContainer("BOOKING-1"));
|
||||
var result = service.convertContainer("BOOKING-1");
|
||||
|
||||
verify(orderMapper, never()).lockOrder(any(), any());
|
||||
assertEquals(2, result.size());
|
||||
assertEquals("LIP-1", result.get(0).planNo());
|
||||
assertEquals(2L, result.get(1).sourceOrderId());
|
||||
verify(orderMapper, never()).lockOrder("100", 1L);
|
||||
verify(orderMapper).lockOrder("100", 2L);
|
||||
verify(planMapper).insert(any(InboundPlan.class));
|
||||
verify(orderMapper, never()).markPlanned(eq("100"), eq(1L), any(),
|
||||
org.mockito.ArgumentMatchers.anyInt(), any());
|
||||
verify(orderMapper).markPlanned(eq("100"), eq(2L), any(),
|
||||
org.mockito.ArgumentMatchers.anyInt(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void partialExistingPlansWithMixedWarehouseFactsCauseZeroNewWrites() {
|
||||
when(orderMapper.lockContainerOrders("BOOKING-1")).thenReturn(List.of(
|
||||
new InboundPlanOrderMapper.ContainerOrderRef("100", 1L, "INBOUND_PLANNED", "MSCU6639870", "STANDARD"),
|
||||
new InboundPlanOrderMapper.ContainerOrderRef("100", 2L, "PENDING_ASSIGNMENT", "MSCU6639870", "STANDARD")));
|
||||
when(planMapper.findBySourceOrder("100", 1L)).thenReturn(existingPlan("100", 1L, "LIP-1"));
|
||||
when(orderMapper.findReceivingWarehouse("100", 1L)).thenReturn(
|
||||
new InboundPlanOrderMapper.ReceivingWarehouseSnapshot(
|
||||
"193000000000200003", "US-LAX-01", "洛杉矶仓"));
|
||||
SourceOrderSnapshot differentWarehouse = new SourceOrderSnapshot(2L, "100", "MSCU6639870", "RK-2",
|
||||
"PENDING_ASSIGNMENT", null, BigDecimal.ONE, 10, 0,
|
||||
"193000000000200004", null, null, null, "WH-B", "WH-B", "仓B", 1, 0, false, 0);
|
||||
when(orderMapper.lockOrder("100", 2L)).thenReturn(List.of(differentWarehouse));
|
||||
|
||||
ResponseStatusException error = assertThrows(ResponseStatusException.class,
|
||||
() -> service.convertContainer("BOOKING-1"));
|
||||
|
||||
assertEquals(409, error.getStatusCode().value());
|
||||
verify(planMapper, never()).insert(any(InboundPlan.class));
|
||||
verify(orderMapper, never()).markPlanned(any(), any(), any(), org.mockito.ArgumentMatchers.anyInt(), any());
|
||||
verify(inventoryBalanceMapper, never()).bindInboundPlan(any(), any(), any(), any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -419,12 +620,14 @@ class InboundPlanServiceTest {
|
||||
|
||||
private SourceOrderSnapshot order(String customerId, Long id, String inboundCode, String warehouse) {
|
||||
return new SourceOrderSnapshot(id, customerId, "MSCU6639870", inboundCode, "PENDING_ASSIGNMENT", null,
|
||||
BigDecimal.ONE, 10, warehouse, warehouse, warehouse, 1, 0);
|
||||
BigDecimal.ONE, 10, 0, "193000000000200003", "US-LAX-01", "洛杉矶仓", null,
|
||||
warehouse, warehouse, warehouse, 1, 0);
|
||||
}
|
||||
|
||||
private SourceOrderSnapshot order(Long id, String inboundCode, String volume, String warehouse) {
|
||||
return new SourceOrderSnapshot(id, "100", "MSCU6639870", inboundCode, "PENDING_ASSIGNMENT", null,
|
||||
new BigDecimal(volume), new BigDecimal(volume).multiply(BigDecimal.TEN).intValue(),
|
||||
0, "193000000000200003", "US-LAX-01", "洛杉矶仓", null,
|
||||
warehouse, warehouse, warehouse, 1, 0);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user