Merge test fixes into dev dropship flow
This commit is contained in:
+33
@@ -1,20 +1,52 @@
|
||||
package com.baiyue.firstleg.controller;
|
||||
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.firstleg.service.ContainerOverviewService;
|
||||
import com.baiyue.firstleg.service.ContainerService;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@DisplayName("货柜基础资料接口契约")
|
||||
class ContainerControllerContractTest {
|
||||
@Test
|
||||
@DisplayName("PDA照片作为历史证据下载时不受国内工作台分区限制")
|
||||
void downloadsPdaPhotoWithoutDomesticWorkbenchGate() {
|
||||
ContainerService containers = mock(ContainerService.class);
|
||||
ContainerOverviewService overview = mock(ContainerOverviewService.class);
|
||||
PlatformContainerController controller = new PlatformContainerController();
|
||||
ReflectionTestUtils.setField(controller, "service", containers);
|
||||
ReflectionTestUtils.setField(controller, "overview", overview);
|
||||
when(overview.downloadPhoto(41L, 101L)).thenReturn(new ContainerOverviewService.PhotoDownload(
|
||||
"door.jpg", MediaType.IMAGE_JPEG, new byte[]{1, 2, 3}));
|
||||
AuthenticatedUser user = new AuthenticatedUser(7L, 0L, 0L, 8L,
|
||||
"admin", "PLATFORM", "WEB", Set.of("platform:firstleg:container:list"));
|
||||
|
||||
var response = controller.downloadPdaPhoto(user, 41L, 101L);
|
||||
|
||||
assertThat(response.getStatusCode().is2xxSuccessful()).isTrue();
|
||||
assertThat(response.getHeaders().getContentType()).isEqualTo(MediaType.IMAGE_JPEG);
|
||||
assertThat(response.getBody()).containsExactly(1, 2, 3);
|
||||
verify(overview).downloadPhoto(41L, 101L);
|
||||
verifyNoInteractions(containers);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("查询、新增和编辑按钮权限彼此独立")
|
||||
void separatesButtonPermissions() {
|
||||
@@ -50,6 +82,7 @@ class ContainerControllerContractTest {
|
||||
"/pda/photos/{fileId}/download:", "downloadPath:", "workflowStatus:",
|
||||
"AWAITING_REVIEW", "version: {type: integer, minimum: 0}",
|
||||
"downloadPath: {type: string, nullable: true",
|
||||
"PDA完成及国内离仓后仍可读取ACTIVE历史照片",
|
||||
"仅 ACTIVE 照片提供鉴权下载路径;VOID 历史照片为空")
|
||||
.doesNotContain("?containerNo", "containerNo in: query");
|
||||
}
|
||||
|
||||
+5
-5
@@ -33,15 +33,15 @@ class CustomerDimensionConfigControllerContractTest {
|
||||
|
||||
@Test
|
||||
void internalInitializationRequiresTokenAndReturnsCreationIdentity() {
|
||||
InitializeRequest request = new InitializeRequest(8L, 100L, "registration-1");
|
||||
when(service.initializeDefault(8L, 100L, "registration-1"))
|
||||
.thenReturn(new InitializeResponse(91L, true));
|
||||
InitializeRequest request = new InitializeRequest(8L, 100L, "registration-1", "柏悦物流");
|
||||
when(service.initializeDefault(8L, 100L, "registration-1", "柏悦物流"))
|
||||
.thenReturn(new InitializeResponse(91L, 191L, true));
|
||||
|
||||
assertThatThrownBy(() -> internalController.initializeDefault("wrong", request))
|
||||
.isInstanceOf(BizException.class);
|
||||
assertThat(internalController.initializeDefault("test-token", request).data())
|
||||
.isEqualTo(new InitializeResponse(91L, true));
|
||||
verify(service).initializeDefault(8L, 100L, "registration-1");
|
||||
.isEqualTo(new InitializeResponse(91L, 191L, true));
|
||||
verify(service).initializeDefault(8L, 100L, "registration-1", "柏悦物流");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+16
@@ -61,6 +61,22 @@ class InboundMeasurementEvidenceOpenApiContractTest {
|
||||
assertThat(properties(schemas, "InboundMeasurementItemRequest")).containsKeys(
|
||||
"inboundDetailId", "itemNo", "boxSpecification", "packageCount", "lengthCm",
|
||||
"widthCm", "heightCm", "intelligentImageFileId", "remark");
|
||||
String strategyPath = "/api/platform/v1/first-leg/inbounds/{inboundId}/measurement/calculation-strategy";
|
||||
assertThat(paths).containsKey(strategyPath);
|
||||
Map<String, Object> strategyOperation = stringMap(stringMap(paths.get(strategyPath)).get("put"));
|
||||
assertThat(strategyOperation.get("description").toString())
|
||||
.contains("不覆盖整批测量数据", "不改变测量提交或审核状态", "客户已确认费用且该票已装柜完成");
|
||||
assertThat(properties(schemas, "InboundMeasurementCalculationStrategyRequest"))
|
||||
.containsKeys("measurementId", "version", "coefficientMode", "chargeRuleId");
|
||||
String outerDimensionsPath = "/api/platform/v1/first-leg/inbounds/{inboundId}"
|
||||
+ "/measurement/items/{itemId}/outer-dimensions";
|
||||
assertThat(paths).containsKey(outerDimensionsPath);
|
||||
Map<String, Object> outerDimensionsOperation = stringMap(
|
||||
stringMap(paths.get(outerDimensionsPath)).get("put"));
|
||||
assertThat(outerDimensionsOperation.get("description").toString())
|
||||
.contains("不覆盖整批测量数据", "不改变测量提交或审核状态", "客户已确认费用且该票已装柜完成");
|
||||
assertThat(properties(schemas, "InboundMeasurementOuterDimensionsRequest"))
|
||||
.containsKeys("measurementId", "version", "outerLengthCm", "outerWidthCm", "outerHeightCm");
|
||||
|
||||
Map<String, Object> itemProperties = properties(schemas, "InboundMeasurementItem");
|
||||
assertThat(itemProperties).containsKeys(
|
||||
|
||||
+3
@@ -22,6 +22,8 @@ class InboundOrderControllerContractTest {
|
||||
AuthenticatedUser.class, long.class, long.class);
|
||||
assertAuthority("printCartonLabels", "hasAuthority('platform:firstleg:inbound:view')",
|
||||
AuthenticatedUser.class, long.class, long.class, List.class, String.class);
|
||||
assertAuthority("printCartonLabelsBatch", "hasAuthority('platform:firstleg:inbound:view')",
|
||||
AuthenticatedUser.class, List.class, String.class);
|
||||
|
||||
Method page = PlatformInboundOrderController.class.getDeclaredMethod(
|
||||
"page", AuthenticatedUser.class, Long.class, InboundOrderQuery.class);
|
||||
@@ -47,6 +49,7 @@ class InboundOrderControllerContractTest {
|
||||
.contains("/api/platform/inbounds:")
|
||||
.contains("/api/platform/inbounds/{inboundId}:")
|
||||
.contains("/api/platform/inbounds/{inboundId}/carton-label-pdf:")
|
||||
.contains("/api/platform/inbounds/carton-label-pdf:")
|
||||
.contains("/api/platform/v1/first-leg/inbounds/{inboundId}/measurement/carton-scans:")
|
||||
.contains("required: false")
|
||||
.contains("InboundOrderSummary:")
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ class ContainerCustomsControllerContractTest {
|
||||
String yaml=new ClassPathResource("openapi/customs-v1.yaml").getContentAsString(java.nio.charset.StandardCharsets.UTF_8);
|
||||
new Yaml().load(yaml);
|
||||
assertThat(yaml).contains("ContainerView:","DocumentView:","FileView:","InboundView:","ActionRequest:","ScopeRequest:")
|
||||
.contains("merchantStatus:","WAITING_UPLOAD","CUSTOMS_COMPLETED","loadingPlanId:")
|
||||
.contains("merchantStatus:","WAITING_UPLOAD","CUSTOMS_COMPLETED","loadingPlanId:","hasActualCargo:","nullable: true")
|
||||
.contains("LOADING_LIST_BUY_DECLARATION_ITEMS")
|
||||
.doesNotContain("/cases/","CUSTOMS_DECLARATION");
|
||||
}
|
||||
|
||||
+3
@@ -23,6 +23,9 @@ class ContainerCustomsMapperContractTest {
|
||||
@Test void actualCompletedLoadingItemsAndTaxModeAreTheOnlyTicketFacts() throws Exception {
|
||||
String plan=sql("planInbounds",long.class);
|
||||
assertThat(plan).contains("fl_loading_plan_item","fl_inbound_order","p.id=#{planId}","item.deleted=0");
|
||||
String planIdentity=sql("plan",long.class);
|
||||
assertThat(planIdentity).contains("fl_loading_plan","fl_container_booking","p.id=#{planId}")
|
||||
.doesNotContain("fl_loading_plan_item","fl_inbound_order");
|
||||
String missing=sql("missingRefundCount",long.class,long.class);
|
||||
assertThat(missing).contains("i.tax_mode='TAX_REFUND'","fl_customs_customer_document_inbound","d.status='ACTIVE'");
|
||||
}
|
||||
|
||||
+39
@@ -44,6 +44,45 @@ class ContainerCustomsServiceTest {
|
||||
assertMerchantOrderNotFound(7L,22L);
|
||||
}
|
||||
|
||||
@Test void completedPlanWithoutActualCargoReturnsEmptyViewInsteadOfNotFound(){
|
||||
var plan=new ContainerCustomsMapper.PlanRow(9L,"LP9",8L,"COMPLETED","CAB","C");
|
||||
when(mapper.planInbounds(9L)).thenReturn(List.of());
|
||||
when(mapper.plan(9L)).thenReturn(plan);
|
||||
when(mapper.container(9L)).thenReturn(container(ContainerCustomsService.READY,0));
|
||||
|
||||
var view=service.platformView(9L);
|
||||
|
||||
assertThat(view.hasActualCargo()).isFalse();
|
||||
assertThat(view.planNo()).isEqualTo("LP9");
|
||||
assertThat(view.inbounds()).isEmpty();
|
||||
assertThat(view.documents()).isEmpty();
|
||||
assertThat(view.taxRefundCount()).isZero();
|
||||
verify(mapper).insertContainer(anyLong(),eq(9L),eq(8L),eq(ContainerCustomsService.READY));
|
||||
}
|
||||
|
||||
@Test void editingPlanReturnsNoCustomsAggregateInsteadOfConflict(){
|
||||
when(mapper.planInbounds(9L)).thenReturn(List.of(new ContainerCustomsMapper.InboundRow(
|
||||
9L,"LP9",8L,"EDITING","CAB","C",11L,21L,"O","I",7L,"客户","TAX_REFUND")));
|
||||
|
||||
assertThat(service.platformView(9L)).isNull();
|
||||
|
||||
verify(mapper,never()).insertContainer(anyLong(),anyLong(),anyLong(),anyString());
|
||||
verify(mapper,never()).container(9L);
|
||||
}
|
||||
|
||||
@Test void completedPlanWithoutActualCargoCannotStartCustoms(){
|
||||
when(mapper.claimCommand(anyLong(),eq(5L),eq("START_CONTAINER_CUSTOMS_9"),eq("empty-plan"),anyString())).thenReturn(1);
|
||||
when(mapper.planInbounds(9L)).thenReturn(List.of());
|
||||
when(mapper.plan(9L)).thenReturn(new ContainerCustomsMapper.PlanRow(9L,"LP9",8L,"COMPLETED","CAB","C"));
|
||||
when(mapper.container(9L)).thenReturn(container(ContainerCustomsService.READY,0));
|
||||
|
||||
assertThatThrownBy(()->service.start(9L,5L,"admin","empty-plan",new ActionRequest(0,true,null)))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("没有实际货物,无需报关");
|
||||
|
||||
verify(mapper,never()).lockContainer(9L);
|
||||
verify(mapper,never()).start(anyLong(),anyInt(),anyLong(),anyString());
|
||||
}
|
||||
|
||||
@Test void merchantCompletedTaxRefundOrderReturnsContainerView(){
|
||||
var row=new ContainerCustomsMapper.InboundRow(9L,"LP9",8L,"COMPLETED","CAB","C",11L,21L,"O","I",7L,"客户","TAX_REFUND");
|
||||
when(mapper.completedPlanForOrder(7L,21L)).thenReturn(9L);
|
||||
|
||||
-4
@@ -6,7 +6,6 @@ import com.baiyue.firstleg.loading.repository.LoadingRepository;
|
||||
import com.baiyue.firstleg.mapper.InboundStatusHistoryMapper;
|
||||
import com.baiyue.firstleg.mapper.LoadingPickItemMapper.InboundProgress;
|
||||
import com.baiyue.firstleg.repository.LoadingPickRepository;
|
||||
import com.baiyue.firstleg.service.FbaPricingService;
|
||||
import com.baiyue.firstleg.service.InboundHoldGuard;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
@@ -29,7 +28,6 @@ class LoadingDeliveryServiceTest {
|
||||
@MockBean LoadingCompletionGateService completionGate;
|
||||
@MockBean InboundStatusHistoryMapper histories;
|
||||
@MockBean CabinetLoadingEvidenceService cabinetEvidence;
|
||||
@MockBean FbaPricingService pricingService;
|
||||
@MockBean ObjectMapper objectMapper;
|
||||
@MockBean InboundHoldGuard inboundHoldGuard;
|
||||
@Resource LoadingDeliveryService service;
|
||||
@@ -50,7 +48,6 @@ class LoadingDeliveryServiceTest {
|
||||
new com.baiyue.firstleg.loading.dto.LoadingRequests.CompleteLoadingRequest(3, "done"));
|
||||
|
||||
verify(cabinetEvidence).requireComplete(99L);
|
||||
verify(pricingService).autoConfirmAfterLoading(10L, 20L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -91,7 +88,6 @@ class LoadingDeliveryServiceTest {
|
||||
|
||||
verify(repository).findInboundForUpdate(10L, 21L);
|
||||
verify(repository).completeLoadedChild(10L, 20L, 9L, 9);
|
||||
verify(pricingService).autoConfirmAfterLoading(10L, 21L);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+18
@@ -146,6 +146,24 @@ class OutboundDispatchServiceTest {
|
||||
verify(carrierSchedules, times(1)).ensureFromOutboundConfirm(7L, 66L, 22L, 9L, "admin");
|
||||
}
|
||||
|
||||
@Test
|
||||
void confirmCreatesPlatformScopedCarrierTaskForConsolidatedBooking() {
|
||||
CarrierScheduleQueryService carrierSchedules = mock(CarrierScheduleQueryService.class);
|
||||
OutboundDispatchService service = service(carrierSchedules);
|
||||
stubContainer();
|
||||
when(repository.ocean(22L)).thenReturn(new OutboundDispatchReadMapper.OceanBinding(66L, "RELEASED", null));
|
||||
when(repository.lockBindings(22L)).thenReturn(bindings());
|
||||
when(repository.dispatchLocked(7L, 11L, 22L)).thenReturn(state(31L, 11L, "READY_FOR_DISPATCH", 0));
|
||||
when(repository.dispatchLocked(7L, 12L, 22L)).thenReturn(state(32L, 12L, "READY_FOR_DISPATCH", 0));
|
||||
when(repository.claim(eq(9L), eq("CONFIRM_DISPATCH"), eq("confirm-platform-booking"), anyString())).thenReturn(true);
|
||||
when(repository.confirm(anyLong(), eq(0), eq(9L), eq("admin"), isNull())).thenReturn(1);
|
||||
when(repository.completeOperation(31L, 9L, "CONFIRM_DISPATCH", "confirm-platform-booking")).thenReturn(1);
|
||||
|
||||
service.confirm(22L, 9L, "admin", "confirm-platform-booking", new ActionRequest(0, null));
|
||||
|
||||
verify(carrierSchedules).ensureFromOutboundConfirm(null, 66L, 22L, 9L, "admin");
|
||||
}
|
||||
|
||||
@Test
|
||||
void confirmDoesNotWriteAnyRowWhenOneInboundGateFails() {
|
||||
OutboundDispatchService service = serviceWithoutCarrier();
|
||||
|
||||
-2
@@ -38,8 +38,6 @@ class CarrierScheduleQueryOpenApiContractTest {
|
||||
.contains("cabinetNo, orderNo, bookingNo and bill of lading are never fallbacks")
|
||||
.contains("latestActualNode:")
|
||||
.contains("latestActualAt:")
|
||||
.contains("manual actual node is equal to or earlier than latestActualNode")
|
||||
.contains("strictly greater sequence than latestActualNode")
|
||||
.contains("internal execution fields are intentionally absent")
|
||||
.doesNotContain("enum: [READY, MANUAL_REQUIRED, ARRIVED")
|
||||
.doesNotContain("enum: [BOOKING_NO, BILL_OF_LADING, CONTAINER_NO]")
|
||||
|
||||
+4
-3
@@ -21,7 +21,7 @@ class CarrierScheduleQuerySqlContractTest {
|
||||
.getAnnotation(Select.class).value());
|
||||
|
||||
assertThat(sql)
|
||||
.contains("container_booking_id IN")
|
||||
.contains("WHERE b.id IN")
|
||||
.contains("f.actual_at IS NOT NULL")
|
||||
.contains("MAX(CASE WHEN f.standard_node='DEPARTED_WAREHOUSE'")
|
||||
.contains("MAX(CASE WHEN f.standard_node='DEPARTED_ORIGIN_PORT'")
|
||||
@@ -29,9 +29,10 @@ class CarrierScheduleQuerySqlContractTest {
|
||||
.contains("MAX(CASE WHEN f.standard_node='PICKUP_READY'")
|
||||
.contains("MAX(CASE WHEN f.standard_node='PICKED_UP'")
|
||||
.contains("MAX(CASE WHEN f.standard_node='EMPTY_RETURNED'")
|
||||
.contains("ORDER BY q.container_booking_id")
|
||||
.contains("ORDER BY b.id")
|
||||
.doesNotContain("estimated_at");
|
||||
assertThat(count(sql, "fl_carrier_schedule_node_fact")).isEqualTo(1);
|
||||
assertThat(count(sql, "fl_carrier_schedule_node_fact")).isEqualTo(3);
|
||||
assertThat(count(sql, "SELECT f2.id FROM fl_carrier_schedule_node_fact f2")).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+18
@@ -93,6 +93,24 @@ class CarrierScheduleNodeOutboxServiceTest {
|
||||
verify(mapper, never()).insert(org.mockito.ArgumentMatchers.any(InboundOutboxEntity.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void directManualActualFactDoesNotRequireTrackingQuery() throws Exception {
|
||||
CarrierScheduleNodeFactEntity fact = fact();
|
||||
fact.setProviderCode("MANUAL"); fact.setProviderEventId("manual-event");
|
||||
fact.setSourceReference("MANUAL:7");
|
||||
|
||||
service.publishContainerNode(null, 2088000000000000003L,
|
||||
fact, "ZCSU7881970");
|
||||
|
||||
ArgumentCaptor<InboundOutboxEntity> captor = ArgumentCaptor.forClass(InboundOutboxEntity.class);
|
||||
verify(mapper).insert(captor.capture());
|
||||
EventEnvelope envelope = objectMapper.readValue(captor.getValue().getPayloadJson(), EventEnvelope.class);
|
||||
assertThat(envelope.payload().path("queryId").isNull()).isTrue();
|
||||
assertThat(envelope.payload().path("oceanBookingId").isNull()).isTrue();
|
||||
assertThat(envelope.payload().path("sourceContainerBookingId").asText())
|
||||
.isEqualTo("2088000000000000003");
|
||||
}
|
||||
|
||||
@Test
|
||||
void initialOutboundDepartureUsesVersionZeroWithoutEnvelopeFailure() throws Exception {
|
||||
CarrierScheduleQueryEntity query = query();
|
||||
|
||||
+20
-30
@@ -210,6 +210,24 @@ class CarrierScheduleQueryServiceTest {
|
||||
&& "国内确认出仓自动创建".equals(history.getReason())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void platformOwnedConsolidatedBookingCreatesContainerTaskInTechnicalScope() {
|
||||
OceanBookingEntity booking = new OceanBookingEntity();
|
||||
booking.setCustomerId(null); booking.setCarrierCode("SML"); booking.setShippingCompanyName("SML");
|
||||
var container = new com.baiyue.firstleg.entity.ContainerBookingEntity();
|
||||
container.setContainerNo("ZCSU7881970");
|
||||
when(portSchedules.oceanBooking(10L)).thenReturn(booking);
|
||||
when(portSchedules.containerBooking(20L)).thenReturn(container);
|
||||
when(portSchedules.allocation(10L, 20L)).thenReturn(new OceanBookingContainerAllocationEntity());
|
||||
when(repository.insertIfAbsent(any(CarrierScheduleNodeFactEntity.class))).thenReturn(true);
|
||||
|
||||
CarrierScheduleQueryEntity result = service.ensureFromOutboundConfirm(null, 10L, 20L, 0L, "SYSTEM");
|
||||
|
||||
assertThat(result.getCustomerId()).isZero();
|
||||
verify(repository).findByBusinessScope(0L, 10L, 20L);
|
||||
verify(repository).insert(argThat((CarrierScheduleQueryEntity task) -> task.getCustomerId() == 0L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void outboundUnknownCarrierCreatesManualTaskAndPreservesBookingCarrierIdentity() {
|
||||
OceanBookingEntity booking = new OceanBookingEntity(); booking.setCustomerId(10001L);
|
||||
@@ -298,7 +316,7 @@ class CarrierScheduleQueryServiceTest {
|
||||
@Test
|
||||
void outboundAndCarrierEnsureShareRequiredTransactionalRollbackBoundary() throws Exception {
|
||||
Transactional carrierBoundary = CarrierScheduleQueryService.class.getMethod("ensureFromOutboundConfirm",
|
||||
long.class, long.class, long.class, long.class, String.class).getAnnotation(Transactional.class);
|
||||
Long.class, long.class, long.class, long.class, String.class).getAnnotation(Transactional.class);
|
||||
Transactional outboundBoundary = OutboundDispatchService.class.getMethod("confirm",
|
||||
long.class, long.class, String.class, String.class, ActionRequest.class).getAnnotation(Transactional.class);
|
||||
|
||||
@@ -433,34 +451,6 @@ class CarrierScheduleQueryServiceTest {
|
||||
verify(repository).completeOperation(eq(1L), eq(7L), eq("manual-empty-1"), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void manualActualRejectsSameAndEarlierNodesIncludingSameKeyRetryWithoutDrift() {
|
||||
CarrierScheduleQueryEntity task = task(0, 3);
|
||||
task.setStatus("READY"); task.setLatestStandardNode("PICKED_UP");
|
||||
task.setLatestNodeObservedAt(NOW.minusDays(1));
|
||||
when(repository.lock(1L)).thenReturn(task);
|
||||
|
||||
ManualNodeRequest sameNode = new ManualNodeRequest(3, "PICKED_UP", NOW, null,
|
||||
NOW.plusDays(1), null, "Los Angeles", "重复确认提柜");
|
||||
assertThatThrownBy(() -> service.addManualNode(1L, 7L, "admin", "manual-picked-up-again", sameNode))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessage("人工实际节点必须严格晚于当前最新实际节点");
|
||||
assertThatThrownBy(() -> service.addManualNode(1L, 7L, "admin", "manual-picked-up-again", sameNode))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessage("人工实际节点必须严格晚于当前最新实际节点");
|
||||
assertThatThrownBy(() -> service.addManualNode(1L, 7L, "admin", "manual-pickup-ready-late",
|
||||
new ManualNodeRequest(3, "PICKUP_READY", NOW, null, NOW.plusDays(2), null,
|
||||
"Los Angeles", "倒序补录可提柜")))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessage("人工实际节点必须严格晚于当前最新实际节点");
|
||||
|
||||
verify(repository, never()).insertIfAbsent(any(CarrierScheduleNodeFactEntity.class));
|
||||
verify(repository, never()).updateState(any(), any(Integer.class), anyLong(), anyString());
|
||||
verify(repository, never()).insert(any(com.baiyue.firstleg.portschedule.entity.CarrierScheduleQueryHistoryEntity.class));
|
||||
verify(repository, never()).completeOperation(anyLong(), anyLong(), anyString(), anyString());
|
||||
verify(nodeOutbox, never()).publish(any(), any(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void manualEstimatedFactAdvancesVersionWithoutAdvancingActualProgress() {
|
||||
CarrierScheduleQueryEntity before = task(0, 3);
|
||||
@@ -531,7 +521,7 @@ class CarrierScheduleQueryServiceTest {
|
||||
existing.setEventKey("ZIM:evt-duplicate"); existing.setRawPayloadHash("a".repeat(64));
|
||||
when(repository.lock(1L)).thenReturn(task);
|
||||
when(repository.insertIfAbsent(any(CarrierScheduleNodeFactEntity.class))).thenReturn(false);
|
||||
when(repository.node(1L, "ZIM:evt-duplicate")).thenReturn(existing);
|
||||
when(repository.nodeByContainer(task.getContainerBookingId(), "ZIM:evt-duplicate")).thenReturn(existing);
|
||||
ProviderObservationRequest replay = new ProviderObservationRequest("ZIM", "evt-duplicate", "GATE_IN",
|
||||
"AT_ORIGIN_PORT", NOW, null, NOW.minusDays(1), null, null, "ZIM:evt", "a".repeat(64));
|
||||
|
||||
|
||||
+24
-2
@@ -210,20 +210,42 @@ class FbaPricingRepositoryRuleListTest {
|
||||
.thenReturn(1);
|
||||
|
||||
assertThat(repository.markInboundCalculated(
|
||||
10001L, 88L, "WAIT_PALLETIZE", "CBM", new java.math.BigDecimal("1.0000")))
|
||||
10001L, 88L, "WAIT_PALLETIZE", "CBM", new java.math.BigDecimal("10.000"),
|
||||
new java.math.BigDecimal("1.0000"), new java.math.BigDecimal("1.0000")))
|
||||
.isEqualTo(1);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
ArgumentCaptor<LambdaUpdateWrapper<InboundPricingEntity>> wrapper =
|
||||
ArgumentCaptor.forClass(LambdaUpdateWrapper.class);
|
||||
verify(pricing).update(org.mockito.ArgumentMatchers.isNull(), wrapper.capture());
|
||||
assertThat(wrapper.getValue().getSqlSet()).contains("feeStatus", "chargeableVolumeCbm")
|
||||
assertThat(wrapper.getValue().getSqlSet()).contains(
|
||||
"feeStatus", "chargeableWeightKg", "chargeableVolumeCbm")
|
||||
.doesNotContain("processStatus");
|
||||
assertThat(wrapper.getValue().getCustomSqlSegment())
|
||||
.contains("tenantId", "id", "processStatus", "feeStatus");
|
||||
assertThat(params(wrapper.getValue()).values()).contains(10001L, 88L, "WAIT_PALLETIZE");
|
||||
}
|
||||
|
||||
@Test
|
||||
void kilogramPricingStoresChargeableWeightAndSelectedVolumeTogether() {
|
||||
when(pricing.update(org.mockito.ArgumentMatchers.isNull(), org.mockito.ArgumentMatchers.any()))
|
||||
.thenReturn(1);
|
||||
|
||||
assertThat(repository.markInboundCalculated(
|
||||
10001L, 88L, "WAIT_SPLIT", "KG", new java.math.BigDecimal("297.000"),
|
||||
new java.math.BigDecimal("9.0000"), new java.math.BigDecimal("1503.000")))
|
||||
.isEqualTo(1);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
ArgumentCaptor<LambdaUpdateWrapper<InboundPricingEntity>> wrapper =
|
||||
ArgumentCaptor.forClass(LambdaUpdateWrapper.class);
|
||||
verify(pricing).update(org.mockito.ArgumentMatchers.isNull(), wrapper.capture());
|
||||
assertThat(wrapper.getValue().getSqlSet())
|
||||
.contains("chargeableWeightKg", "chargeableVolumeCbm");
|
||||
assertThat(params(wrapper.getValue()).values()).contains(
|
||||
new java.math.BigDecimal("1503.000"), new java.math.BigDecimal("9.0000"));
|
||||
}
|
||||
|
||||
private Collection<Object> queryValues(Long tenantId, String ruleCode) {
|
||||
when(rules.selectList(org.mockito.ArgumentMatchers.any())).thenReturn(List.of());
|
||||
|
||||
|
||||
+3
-3
@@ -89,16 +89,16 @@ class LoadingPickItemMapperContractTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void pdaWritesAndReadsRequireWarehouseScope() throws Exception {
|
||||
void pdaWritesAndReadsRelyOnUpstreamTaskScopeInsteadOfWarehouseScope() throws Exception {
|
||||
Method list = LoadingPickItemMapper.class.getDeclaredMethod("selectTaskItemsWithAccount", long.class, long.class);
|
||||
Method pick = LoadingPickItemMapper.class.getDeclaredMethod("pick", long.class, int.class,
|
||||
int.class, long.class);
|
||||
|
||||
assertThat(list.getAnnotation(Select.class).value()[0])
|
||||
.contains("booking.warehouse_id = #{warehouseId}")
|
||||
.doesNotContain("booking.warehouse_id = #{warehouseId}")
|
||||
.contains("plan_item.deleted = 0");
|
||||
assertThat(pick.getAnnotation(Update.class).value()[0])
|
||||
.contains("booking.warehouse_id = #{warehouseId}")
|
||||
.doesNotContain("booking.warehouse_id = #{warehouseId}")
|
||||
.contains("plan_item.deleted = 0");
|
||||
}
|
||||
}
|
||||
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.firstleg.entity.CustomerChargeRuleEntity;
|
||||
import com.baiyue.firstleg.repository.FbaPricingRepository;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@SpringJUnitConfig(CustomerChargeRuleInitializationService.class)
|
||||
class CustomerChargeRuleInitializationServiceTest {
|
||||
@MockBean private FbaPricingRepository repository;
|
||||
@Resource private CustomerChargeRuleInitializationService service;
|
||||
|
||||
@Test
|
||||
void initializesDefaultWeightCoefficientRuleForNewCustomer() {
|
||||
CustomerChargeRuleEntity result = service.initializeDefault(
|
||||
8L, 100L, "iam-customer-create:8", "柏悦物流");
|
||||
|
||||
ArgumentCaptor<CustomerChargeRuleEntity> captor =
|
||||
ArgumentCaptor.forClass(CustomerChargeRuleEntity.class);
|
||||
verify(repository).insertRule(captor.capture());
|
||||
CustomerChargeRuleEntity saved = captor.getValue();
|
||||
assertThat(result).isSameAs(saved);
|
||||
assertThat(saved.getTenantId()).isEqualTo(8L);
|
||||
assertThat(saved.getCustomerId()).isEqualTo(8L);
|
||||
assertThat(saved.getCustomerName()).isEqualTo("柏悦物流");
|
||||
assertThat(saved.getRuleCode()).isEqualTo("DEFAULT-DIMENSION-WEIGHT");
|
||||
assertThat(saved.getRuleVersion()).isEqualTo(1);
|
||||
assertThat(saved.getDefaultRule()).isTrue();
|
||||
assertThat(saved.getLengthFactor()).isEqualByComparingTo("1.100000");
|
||||
assertThat(saved.getWidthFactor()).isEqualByComparingTo("1.100000");
|
||||
assertThat(saved.getHeightFactor()).isEqualByComparingTo("1.100000");
|
||||
assertThat(saved.getConversionFactor()).isEqualByComparingTo("167.000000");
|
||||
assertThat(saved.getRemark()).contains(
|
||||
"SMALL:30,20,20;系数:1.1;重量:5",
|
||||
"EXTRA_LARGE:60,50,50;系数:1.1;重量:15");
|
||||
assertThat(saved.getInitializationKey()).isEqualTo("iam-customer-create:8");
|
||||
}
|
||||
|
||||
@Test
|
||||
void retryReturnsRuleOwnedBySameInitializationWithoutDuplicating() {
|
||||
CustomerChargeRuleEntity existing = new CustomerChargeRuleEntity();
|
||||
existing.setId(191L);
|
||||
when(repository.findRuleByInitializationKey(8L, "iam-customer-create:8"))
|
||||
.thenReturn(existing);
|
||||
|
||||
assertThat(service.initializeDefault(8L, 100L, "iam-customer-create:8", "柏悦物流"))
|
||||
.isSameAs(existing);
|
||||
verify(repository, never()).insertRule(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void compensationDeletesOnlyAnUnchangedInitializedRule() {
|
||||
CustomerChargeRuleEntity value = service.initializeDefault(
|
||||
8L, 100L, "iam-customer-create:8", "柏悦物流");
|
||||
value.setVersion(0);
|
||||
when(repository.findRuleByInitializationKeyForUpdate(8L, "iam-customer-create:8"))
|
||||
.thenReturn(value);
|
||||
when(repository.deleteUnchangedInitializedRule(8L, "iam-customer-create:8"))
|
||||
.thenReturn(1);
|
||||
|
||||
CustomerChargeRuleEntity locked = service.lockForCompensation(8L, "iam-customer-create:8");
|
||||
assertThat(service.isUnchangedDefault(locked)).isTrue();
|
||||
service.deleteUnchangedDefault(8L, "iam-customer-create:8");
|
||||
|
||||
verify(repository).deleteUnchangedInitializedRule(8L, "iam-customer-create:8");
|
||||
}
|
||||
}
|
||||
+25
-6
@@ -22,6 +22,7 @@ import java.util.List;
|
||||
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.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
@@ -32,6 +33,8 @@ import static org.mockito.Mockito.when;
|
||||
class CustomerDimensionConfigServiceTest {
|
||||
@MockBean
|
||||
private CustomerDimensionConfigRepository repository;
|
||||
@MockBean
|
||||
private CustomerChargeRuleInitializationService chargeRuleInitialization;
|
||||
@Resource
|
||||
private CustomerDimensionConfigService service;
|
||||
|
||||
@@ -40,10 +43,12 @@ class CustomerDimensionConfigServiceTest {
|
||||
List<CustomerDimensionConfigEntity> persisted = defaultConfigs("registration-1");
|
||||
when(repository.findAll(8L)).thenReturn(List.of(), persisted);
|
||||
when(repository.insertDefaults(any(), eq(100L))).thenReturn(4);
|
||||
when(chargeRuleInitialization.initializeDefault(8L, 100L, "registration-1", "柏悦物流"))
|
||||
.thenReturn(defaultChargeRule());
|
||||
|
||||
InitializeResponse result = service.initializeDefault(8L, 100L, "registration-1");
|
||||
InitializeResponse result = service.initializeDefault(8L, 100L, "registration-1", "柏悦物流");
|
||||
|
||||
assertThat(result).isEqualTo(new InitializeResponse(91L, true));
|
||||
assertThat(result).isEqualTo(new InitializeResponse(91L, 191L, true));
|
||||
@SuppressWarnings("unchecked")
|
||||
ArgumentCaptor<List<CustomerDimensionConfigEntity>> captor = ArgumentCaptor.forClass(List.class);
|
||||
verify(repository).insertDefaults(captor.capture(), eq(100L));
|
||||
@@ -67,10 +72,12 @@ class CustomerDimensionConfigServiceTest {
|
||||
@Test
|
||||
void retryAfterLostResponseKeepsCreationOwnershipForSameKey() {
|
||||
when(repository.findAll(8L)).thenReturn(defaultConfigs("registration-1"));
|
||||
when(chargeRuleInitialization.initializeDefault(8L, 100L, "registration-1", "柏悦物流"))
|
||||
.thenReturn(defaultChargeRule());
|
||||
|
||||
InitializeResponse result = service.initializeDefault(8L, 100L, "registration-1");
|
||||
InitializeResponse result = service.initializeDefault(8L, 100L, "registration-1", "柏悦物流");
|
||||
|
||||
assertThat(result).isEqualTo(new InitializeResponse(91L, true));
|
||||
assertThat(result).isEqualTo(new InitializeResponse(91L, 191L, true));
|
||||
verify(repository, never()).insertDefaults(any(), eq(100L));
|
||||
}
|
||||
|
||||
@@ -78,10 +85,11 @@ class CustomerDimensionConfigServiceTest {
|
||||
void differentInitializationKeyDoesNotTakeOwnershipOrOverwrite() {
|
||||
when(repository.findAll(8L)).thenReturn(defaultConfigs("registration-1"));
|
||||
|
||||
InitializeResponse result = service.initializeDefault(8L, 100L, "registration-2");
|
||||
InitializeResponse result = service.initializeDefault(8L, 100L, "registration-2", "柏悦物流");
|
||||
|
||||
assertThat(result).isEqualTo(new InitializeResponse(91L, false));
|
||||
assertThat(result).isEqualTo(new InitializeResponse(91L, null, false));
|
||||
verify(repository, never()).insertDefaults(any(), eq(100L));
|
||||
verify(chargeRuleInitialization, never()).initializeDefault(anyLong(), anyLong(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -173,10 +181,14 @@ class CustomerDimensionConfigServiceTest {
|
||||
when(repository.findDefaultsForCompensation(8L, "registration-1"))
|
||||
.thenReturn(defaultConfigs("registration-1"));
|
||||
when(repository.deleteUnchangedDefaults(8L, "registration-1")).thenReturn(4);
|
||||
when(chargeRuleInitialization.lockForCompensation(8L, "registration-1"))
|
||||
.thenReturn(defaultChargeRule());
|
||||
when(chargeRuleInitialization.isUnchangedDefault(any())).thenReturn(true);
|
||||
|
||||
service.compensateDefault(8L, "registration-1");
|
||||
|
||||
verify(repository).deleteUnchangedDefaults(8L, "registration-1");
|
||||
verify(chargeRuleInitialization).deleteUnchangedDefault(8L, "registration-1");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -228,4 +240,11 @@ class CustomerDimensionConfigServiceTest {
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
private static com.baiyue.firstleg.entity.CustomerChargeRuleEntity defaultChargeRule() {
|
||||
com.baiyue.firstleg.entity.CustomerChargeRuleEntity value =
|
||||
new com.baiyue.firstleg.entity.CustomerChargeRuleEntity();
|
||||
value.setId(191L);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,48 +77,6 @@ class FbaPricingServiceTest {
|
||||
verify(repository, never()).findLatestFee(TENANT, INBOUND);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("装柜完成后系统自动确认客户未确认的已发送费用")
|
||||
void automaticallyConfirmsSentFeeAfterLoading() {
|
||||
InboundFeeEntity sent = savedFee("SENT");
|
||||
when(repository.findLatestFeeForUpdate(TENANT, INBOUND)).thenReturn(sent);
|
||||
when(repository.confirmFeeByCustomer(TENANT, 501L, 1, 0L,
|
||||
"装柜完成自动确认", NOW)).thenReturn(1);
|
||||
|
||||
service.autoConfirmAfterLoading(TENANT, INBOUND);
|
||||
|
||||
verify(repository).confirmFeeByCustomer(TENANT, 501L, 1, 0L,
|
||||
"装柜完成自动确认", NOW);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("客户已经确认时装柜完成不覆盖原确认信息")
|
||||
void keepsExistingCustomerConfirmationAfterLoading() {
|
||||
InboundFeeEntity sent = savedFee("SENT");
|
||||
sent.setCustomerConfirmedBy(88L);
|
||||
sent.setCustomerConfirmedByName("客户成员");
|
||||
sent.setCustomerConfirmedAt(NOW.minusHours(1));
|
||||
when(repository.findLatestFeeForUpdate(TENANT, INBOUND)).thenReturn(sent);
|
||||
|
||||
service.autoConfirmAfterLoading(TENANT, INBOUND);
|
||||
|
||||
verify(repository, never()).confirmFeeByCustomer(anyLong(), anyLong(), anyInt(),
|
||||
anyLong(), anyString(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("没有已发送费用时装柜完成不受影响")
|
||||
void ignoresMissingOrUnsentFeeAfterLoading() {
|
||||
when(repository.findLatestFeeForUpdate(TENANT, INBOUND))
|
||||
.thenReturn(null, savedFee("CONFIRMED"));
|
||||
|
||||
service.autoConfirmAfterLoading(TENANT, INBOUND);
|
||||
service.autoConfirmAfterLoading(TENANT, INBOUND);
|
||||
|
||||
verify(repository, never()).confirmFeeByCustomer(anyLong(), anyLong(), anyInt(),
|
||||
anyLong(), anyString(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("包税KG按生效价格表计费且不依赖客户规则或承运商")
|
||||
void calculatesTaxIncludedKgWithoutCustomerRuleOrCarrier() {
|
||||
@@ -258,7 +216,7 @@ class FbaPricingServiceTest {
|
||||
assertThat(response.feeStatus()).isEqualTo("CALCULATED");
|
||||
assertThat(response.nextProcessStatus()).isEqualTo("WAIT_SPLIT");
|
||||
verify(repository).markInboundCalculated(TENANT, INBOUND, "WAIT_SPLIT", "KG",
|
||||
new BigDecimal("167.000"));
|
||||
new BigDecimal("100.000"), new BigDecimal("1.0000"), new BigDecimal("167.000"));
|
||||
verify(feeDetails).materializeCalculatedFreight(TENANT, INBOUND, 501L, OPERATOR, NOW);
|
||||
verify(financeOutbox, never()).publishInbound(any(), anyLong(), anyLong(), anyLong(), anyInt(), any());
|
||||
}
|
||||
@@ -289,17 +247,27 @@ class FbaPricingServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("装柜完成后禁止确认费用")
|
||||
void rejectsConfirmAfterLoadingCompleted() {
|
||||
@DisplayName("装柜完成但客户未确认时仍允许确认新费用版本")
|
||||
void confirmsNewFeeVersionAfterLoadingCompleted() {
|
||||
InboundPricingEntity inbound = inbound("10.000", "1.0000");
|
||||
inbound.setProcessStatus("LOADED");
|
||||
inbound.setFeeStatus("CALCULATED");
|
||||
InboundFeeEntity fee = savedFee("CALCULATED");
|
||||
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
|
||||
when(repository.findLatestFee(TENANT, INBOUND)).thenReturn(fee);
|
||||
when(repository.findIdempotency(TENANT, OPERATOR, "FEE_CONFIRM", "confirm-loaded"))
|
||||
.thenReturn(null);
|
||||
doAnswer(invocation -> { ((InboundPricingIdempotencyEntity) invocation.getArgument(0)).setId(899L); return null; })
|
||||
.when(repository).insertIdempotency(any());
|
||||
when(repository.confirmFee(TENANT, 501L, OPERATOR, NOW)).thenReturn(1);
|
||||
when(repository.markInboundConfirmed(TENANT, INBOUND)).thenReturn(1);
|
||||
when(repository.completeIdempotency(any(), eq(501L), eq(NOW))).thenReturn(1);
|
||||
when(repository.findFee(TENANT, 501L)).thenReturn(fee);
|
||||
|
||||
assertThatThrownBy(() -> service.confirm(TENANT, OPERATOR, INBOUND, "confirm-loaded"))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("装柜流程");
|
||||
verify(repository, never()).markInboundConfirmed(TENANT, INBOUND);
|
||||
FeeSnapshotResponse response = service.confirm(TENANT, OPERATOR, INBOUND, "confirm-loaded");
|
||||
|
||||
assertThat(response.nextProcessStatus()).isEqualTo("LOADED");
|
||||
verify(repository).markInboundConfirmed(TENANT, INBOUND);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -331,15 +299,15 @@ class FbaPricingServiceTest {
|
||||
|
||||
assertThat(response.nextProcessStatus()).isEqualTo("WAIT_PALLETIZE");
|
||||
verify(repository).markInboundCalculated(TENANT, INBOUND, "WAIT_PALLETIZE", "CBM",
|
||||
new BigDecimal("1.0000"));
|
||||
new BigDecimal("10.000"), new BigDecimal("1.0000"), new BigDecimal("1.0000"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("客户未确认时待装柜订单可重新计算并替换已发送费用")
|
||||
@DisplayName("客户未确认时装柜完成订单仍可重新计算并替换已发送费用")
|
||||
void recalculatesSentFeeBeforeCustomerConfirmation() {
|
||||
readyRepository();
|
||||
InboundPricingEntity inbound = inbound("10", "1");
|
||||
inbound.setProcessStatus("WAIT_PALLET");
|
||||
inbound.setProcessStatus("LOADED");
|
||||
inbound.setFeeStatus("SENT");
|
||||
InboundFeeEntity sent = savedFee("SENT");
|
||||
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
|
||||
@@ -354,18 +322,18 @@ class FbaPricingServiceTest {
|
||||
|
||||
assertThat(response.feeVersion()).isEqualTo(2);
|
||||
assertThat(response.feeStatus()).isEqualTo("CALCULATED");
|
||||
assertThat(response.nextProcessStatus()).isEqualTo("WAIT_PALLET");
|
||||
assertThat(response.nextProcessStatus()).isEqualTo("LOADED");
|
||||
verify(repository).supersedeCurrent(TENANT, 501L, "SENT");
|
||||
verify(repository).markInboundCalculated(TENANT, INBOUND, "WAIT_PALLET", "CBM",
|
||||
new BigDecimal("1.0000"));
|
||||
verify(repository).markInboundCalculated(TENANT, INBOUND, "LOADED", "CBM",
|
||||
new BigDecimal("10.000"), new BigDecimal("1.0000"), new BigDecimal("1.0000"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("客户已确认时拒绝重新计算已发送费用")
|
||||
@DisplayName("客户已确认且装柜完成时拒绝重新计算已发送费用")
|
||||
void rejectsRecalculationAfterCustomerConfirmation() {
|
||||
readyRepository();
|
||||
InboundPricingEntity inbound = inbound("10", "1");
|
||||
inbound.setProcessStatus("WAIT_PALLET");
|
||||
inbound.setProcessStatus("LOADED");
|
||||
inbound.setFeeStatus("SENT");
|
||||
InboundFeeEntity sent = savedFee("SENT");
|
||||
sent.setCustomerConfirmedAt(NOW);
|
||||
@@ -493,6 +461,7 @@ class FbaPricingServiceTest {
|
||||
inbound.setProcessStatus("WAIT_SPLIT");
|
||||
inbound.setFeeStatus("SENT");
|
||||
InboundFeeEntity current = savedFee("SENT");
|
||||
current.setCustomerConfirmedAt(NOW.minusHours(1));
|
||||
AtomicReference<InboundFeeEntity> inserted = new AtomicReference<>();
|
||||
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
|
||||
when(repository.findLatestFee(TENANT, INBOUND)).thenReturn(current);
|
||||
@@ -501,7 +470,8 @@ class FbaPricingServiceTest {
|
||||
.when(repository).insertIdempotency(any());
|
||||
when(repository.nextFeeVersion(TENANT, INBOUND)).thenReturn(2);
|
||||
when(repository.supersedeCurrent(TENANT, 501L, "SENT")).thenReturn(1);
|
||||
when(repository.markInboundAdjusted(eq(TENANT), eq(INBOUND), eq("SENT"), eq("KG"), any()))
|
||||
when(repository.markInboundAdjusted(eq(TENANT), eq(INBOUND), eq("WAIT_SPLIT"), eq("SENT"),
|
||||
any(), any(), eq("KG"), any()))
|
||||
.thenReturn(1);
|
||||
doAnswer(invocation -> {
|
||||
InboundFeeEntity fee = invocation.getArgument(0);
|
||||
@@ -561,10 +531,10 @@ class FbaPricingServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("客户已确认费用时拒绝平台再次修改")
|
||||
@DisplayName("客户已确认且装柜完成时拒绝平台再次修改")
|
||||
void rejectsAdjustmentAfterCustomerConfirmation() {
|
||||
InboundPricingEntity inbound = inbound("10", "1");
|
||||
inbound.setProcessStatus("WAIT_SPLIT");
|
||||
inbound.setProcessStatus("LOADED");
|
||||
InboundFeeEntity current = savedFee("SENT");
|
||||
current.setCustomerConfirmedAt(NOW);
|
||||
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
|
||||
@@ -596,7 +566,8 @@ class FbaPricingServiceTest {
|
||||
.when(repository).insertIdempotency(any());
|
||||
when(repository.nextFeeVersion(TENANT, INBOUND)).thenReturn(2);
|
||||
when(repository.supersedeCurrent(TENANT, 501L, "CALCULATED")).thenReturn(1);
|
||||
when(repository.markInboundAdjusted(eq(TENANT), eq(INBOUND), eq("CALCULATED"), eq("KG"), any()))
|
||||
when(repository.markInboundAdjusted(eq(TENANT), eq(INBOUND), eq("WAIT_SPLIT"), eq("CALCULATED"),
|
||||
any(), any(), eq("KG"), any()))
|
||||
.thenReturn(1);
|
||||
doAnswer(invocation -> {
|
||||
InboundFeeEntity fee = invocation.getArgument(0);
|
||||
@@ -655,7 +626,8 @@ class FbaPricingServiceTest {
|
||||
.when(repository).insertIdempotency(any());
|
||||
when(repository.nextFeeVersion(TENANT, INBOUND)).thenReturn(2);
|
||||
when(repository.supersedeCurrent(TENANT, 501L, "CALCULATED")).thenReturn(1);
|
||||
when(repository.markInboundAdjusted(eq(TENANT), eq(INBOUND), eq("CALCULATED"), eq("KG"), any()))
|
||||
when(repository.markInboundAdjusted(eq(TENANT), eq(INBOUND), eq("WAIT_SPLIT"), eq("CALCULATED"),
|
||||
any(), any(), eq("KG"), any()))
|
||||
.thenReturn(1);
|
||||
doAnswer(invocation -> { ((InboundFeeEntity) invocation.getArgument(0)).setId(504L); return null; })
|
||||
.when(repository).insertFee(any());
|
||||
@@ -803,7 +775,8 @@ class FbaPricingServiceTest {
|
||||
when(repository.nextFeeVersion(TENANT, 30002L)).thenReturn(2);
|
||||
when(repository.completeIdempotency(any(), eq(901L), eq(NOW))).thenReturn(1);
|
||||
when(repository.supersedeCurrent(eq(TENANT), anyLong(), anyString())).thenReturn(1);
|
||||
when(repository.markInboundContainerFeeRecalculated(eq(TENANT), anyLong(), anyString(), anyString(), any()))
|
||||
when(repository.markInboundContainerFeeRecalculated(eq(TENANT), anyLong(), anyString(),
|
||||
any(), any(), anyString(), any()))
|
||||
.thenReturn(1);
|
||||
when(priceBookEstimator.estimate(eq(TENANT), any())).thenReturn(
|
||||
regionEstimate("200", "8", "1600"), regionEstimate("200", "8", "1600"));
|
||||
@@ -855,7 +828,8 @@ class FbaPricingServiceTest {
|
||||
when(repository.nextFeeVersion(TENANT, 30002L)).thenReturn(2);
|
||||
when(repository.completeIdempotency(any(), eq(901L), eq(NOW))).thenReturn(1);
|
||||
when(repository.supersedeCurrent(TENANT, 502L, "CALCULATED")).thenReturn(1);
|
||||
when(repository.markInboundContainerFeeRecalculated(eq(TENANT), eq(30002L), anyString(), anyString(), any()))
|
||||
when(repository.markInboundContainerFeeRecalculated(eq(TENANT), eq(30002L), anyString(),
|
||||
any(), any(), anyString(), any()))
|
||||
.thenReturn(1);
|
||||
when(priceBookEstimator.estimate(eq(TENANT), any())).thenReturn(regionEstimate("180", "8", "1440"));
|
||||
when(containerTierAllocator.allocate(any(), eq(new BigDecimal("8")))).thenReturn(
|
||||
@@ -939,7 +913,8 @@ class FbaPricingServiceTest {
|
||||
doAnswer(invocation -> { ((InboundFeeEntity) invocation.getArgument(0)).setId(501L); return null; })
|
||||
.when(repository).insertFee(any());
|
||||
when(repository.nextFeeVersion(TENANT, INBOUND)).thenReturn(1);
|
||||
when(repository.markInboundCalculated(eq(TENANT), eq(INBOUND), any(), any(), any())).thenReturn(1);
|
||||
when(repository.markInboundCalculated(eq(TENANT), eq(INBOUND), any(), any(), any(), any(), any()))
|
||||
.thenReturn(1);
|
||||
when(repository.completeIdempotency(any(), eq(501L), eq(NOW))).thenReturn(1);
|
||||
}
|
||||
|
||||
|
||||
+34
@@ -21,6 +21,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class InboundCartonLabelPrintServiceTest {
|
||||
@@ -118,6 +119,39 @@ class InboundCartonLabelPrintServiceTest {
|
||||
org.mockito.ArgumentMatchers.eq("request-batch"), org.mockito.ArgumentMatchers.any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void platformBatchCombinesLabelsAcrossCustomers() {
|
||||
when(labels.list(10001L, 70001L, null)).thenReturn(batch());
|
||||
when(labels.list(10002L, 70002L, null)).thenReturn(batch());
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_PDF);
|
||||
when(print.generateInboundMarks(org.mockito.ArgumentMatchers.anyString(),
|
||||
org.mockito.ArgumentMatchers.eq("platform-batch"), org.mockito.ArgumentMatchers.any()))
|
||||
.thenReturn(new ResponseEntity<>(new byte[] {1, 2, 3}, headers,
|
||||
org.springframework.http.HttpStatus.OK));
|
||||
|
||||
ResponseEntity<byte[]> response = service.generateForPlatformInbounds(List.of(
|
||||
new InboundCartonLabels.PlatformBatchItem(10001L, 70001L),
|
||||
new InboundCartonLabels.PlatformBatchItem(10002L, 70002L)), "platform-batch");
|
||||
|
||||
assertThat(response.getHeaders().getFirst("X-Carton-Label-Order-Count")).isEqualTo("2");
|
||||
assertThat(response.getBody()).containsExactly(1, 2, 3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void platformBatchFailsWhenAnySelectedInboundCannotGenerateLabels() {
|
||||
when(labels.list(10001L, 70001L, null)).thenReturn(batch());
|
||||
when(labels.list(10002L, 70002L, null))
|
||||
.thenThrow(new BizException(ResultCode.NOT_FOUND, "箱唛入库单不存在"));
|
||||
|
||||
assertThatThrownBy(() -> service.generateForPlatformInbounds(List.of(
|
||||
new InboundCartonLabels.PlatformBatchItem(10001L, 70001L),
|
||||
new InboundCartonLabels.PlatformBatchItem(10002L, 70002L)), "platform-batch"))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessage("箱唛入库单不存在");
|
||||
verifyNoInteractions(print);
|
||||
}
|
||||
|
||||
private InboundCartonLabels.Batch batch() {
|
||||
InboundCartonLabels.Label label = new InboundCartonLabels.Label(
|
||||
1L, 2L, 1, "音箱", 19, "PCHK2608040174#19-1", 19, 1,
|
||||
|
||||
@@ -12,6 +12,7 @@ import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.cloud.client.circuitbreaker.NoFallbackAvailableException;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
@@ -233,6 +234,21 @@ class InboundCartonLabelServiceTest {
|
||||
.hasMessageContaining("订单服务暂不可用");
|
||||
}
|
||||
|
||||
@Test
|
||||
void convertsOrderCargoCircuitBreakerFailureToActionableBusinessError() {
|
||||
InboundOrderEntity inbound = inbound();
|
||||
when(repository.lockInbound(10001L, 70001L)).thenReturn(inbound);
|
||||
when(repository.findActiveByInbound(10001L, 70001L)).thenReturn(new ArrayList<>());
|
||||
when(repository.findByInbound(10001L, 70001L)).thenReturn(new ArrayList<>());
|
||||
when(orderCargoClient.listForCartonLabels("internal-token", 10001L, 60001L))
|
||||
.thenThrow(new NoFallbackAvailableException("order unavailable",
|
||||
new IllegalStateException("downstream failure")));
|
||||
|
||||
assertThatThrownBy(() -> service.getOrCreate(10001L, 70001L))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessage("订单服务暂不可用,无法生成唛头");
|
||||
}
|
||||
|
||||
@Test
|
||||
void repairsMissingHistoricalInboundFromOwnedOrderSnapshot() {
|
||||
var snapshot = new com.baiyue.firstleg.dto.OrderApprovedInboundCommand(
|
||||
|
||||
+112
-9
@@ -11,12 +11,14 @@ import com.baiyue.firstleg.dto.MeasurementBatchCommand;
|
||||
import com.baiyue.firstleg.dto.InboundMeasurementResponses;
|
||||
import com.baiyue.firstleg.entity.InboundMeasurementEntity;
|
||||
import com.baiyue.firstleg.entity.InboundMeasurementItemEntity;
|
||||
import com.baiyue.firstleg.entity.InboundFeeEntity;
|
||||
import com.baiyue.firstleg.entity.InboundOrderEntity;
|
||||
import com.baiyue.firstleg.entity.InboundPalletAllocationEntity;
|
||||
import com.baiyue.firstleg.entity.InboundWeightResultEntity;
|
||||
import com.baiyue.firstleg.entity.InboundWeightRevisionEntity;
|
||||
import com.baiyue.firstleg.repository.InboundMeasurementRepository;
|
||||
import com.baiyue.firstleg.repository.InboundPalletAllocationRepository;
|
||||
import com.baiyue.firstleg.repository.FbaPricingRepository;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
@@ -57,6 +59,7 @@ import static org.mockito.Mockito.when;
|
||||
class InboundMeasurementServiceTest {
|
||||
@MockBean private InboundMeasurementRepository repository;
|
||||
@MockBean private MeasurementBatchService batchService;
|
||||
@MockBean private FbaPricingRepository pricingRepository;
|
||||
@MockBean private FbaMasterDataService fbaMasterDataService;
|
||||
@MockBean private OrderCargoClient orderCargoClient;
|
||||
@MockBean private MeasurementImageClient imageClient;
|
||||
@@ -90,6 +93,85 @@ class InboundMeasurementServiceTest {
|
||||
verify(repository, never()).listWeights(anyLong(), anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
void updatesSubmittedCalculationStrategyWithoutReplacingMeasurementBatch() {
|
||||
InboundOrderEntity inbound = inbound("MEASURE_PENDING");
|
||||
InboundMeasurementEntity measurement = new InboundMeasurementEntity();
|
||||
measurement.setId(30L);
|
||||
measurement.setTenantId(1L);
|
||||
measurement.setInboundId(10L);
|
||||
measurement.setStatus("SUBMITTED");
|
||||
measurement.setVersion(5);
|
||||
measurement.setTotalVolumeCbm(new BigDecimal("2.5000"));
|
||||
measurement.setVolumeWeightKg(new BigDecimal("417.500"));
|
||||
InboundMeasurementItemEntity item = new InboundMeasurementItemEntity();
|
||||
item.setId(31L);
|
||||
item.setTenantId(1L);
|
||||
item.setMeasurementId(30L);
|
||||
when(repository.lockInbound(1L, 10L)).thenReturn(inbound);
|
||||
when(repository.lockMeasurement(1L, 10L, 30L)).thenReturn(measurement);
|
||||
when(repository.listItems(1L, 30L)).thenReturn(List.of(item));
|
||||
when(repository.updateMeasurement(measurement)).thenReturn(true);
|
||||
when(repository.updateInbound(inbound)).thenReturn(true);
|
||||
|
||||
var response = service.updateCalculationStrategy(1L, 10L,
|
||||
new InboundMeasurementRequests.UpdateCalculationStrategyRequest(30L, 5, "ADD", null));
|
||||
|
||||
assertThat(response.status()).isEqualTo("SUBMITTED");
|
||||
verify(batchService).recalculateStrategy(1L, measurement, List.of(item), "ADD", null);
|
||||
verify(repository, never()).replaceItems(anyLong(), anyLong(), any());
|
||||
assertThat(inbound.getActualVolumeCbm()).isEqualByComparingTo("2.5000");
|
||||
}
|
||||
|
||||
@Test
|
||||
void locksCalculationStrategyOnlyAfterCustomerConfirmationAndLoadingCompletion() {
|
||||
InboundOrderEntity inbound = inbound("LOADED");
|
||||
InboundFeeEntity fee = new InboundFeeEntity();
|
||||
fee.setCustomerConfirmedAt(LocalDateTime.now());
|
||||
when(repository.lockInbound(1L, 10L)).thenReturn(inbound);
|
||||
when(pricingRepository.findLatestFeeForUpdate(1L, 10L)).thenReturn(fee);
|
||||
|
||||
assertThatThrownBy(() -> service.updateCalculationStrategy(1L, 10L,
|
||||
new InboundMeasurementRequests.UpdateCalculationStrategyRequest(30L, 5, "ACTUAL", null)))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("客户已确认且该票已装柜完成");
|
||||
|
||||
verify(repository, never()).lockMeasurement(anyLong(), anyLong(), anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
void updatesSubmittedWarehouseOuterDimensionsWithoutReplacingMeasurementBatch() {
|
||||
InboundOrderEntity inbound = inbound("MEASURE_PENDING");
|
||||
InboundMeasurementEntity measurement = new InboundMeasurementEntity();
|
||||
measurement.setId(30L);
|
||||
measurement.setTenantId(1L);
|
||||
measurement.setInboundId(10L);
|
||||
measurement.setStatus("SUBMITTED");
|
||||
measurement.setVersion(5);
|
||||
measurement.setTotalVolumeCbm(new BigDecimal("1.8000"));
|
||||
measurement.setVolumeWeightKg(new BigDecimal("300.600"));
|
||||
InboundMeasurementItemEntity item = new InboundMeasurementItemEntity();
|
||||
item.setId(31L);
|
||||
item.setTenantId(1L);
|
||||
item.setMeasurementId(30L);
|
||||
when(repository.lockInbound(1L, 10L)).thenReturn(inbound);
|
||||
when(repository.lockMeasurement(1L, 10L, 30L)).thenReturn(measurement);
|
||||
when(repository.listItems(1L, 30L)).thenReturn(List.of(item));
|
||||
when(repository.updateMeasurement(measurement)).thenReturn(true);
|
||||
when(repository.updateInbound(inbound)).thenReturn(true);
|
||||
|
||||
var response = service.updateOuterDimensions(1L, 10L, 31L,
|
||||
new InboundMeasurementRequests.UpdateOuterDimensionsRequest(
|
||||
30L, 5, new BigDecimal("30"), new BigDecimal("20"), new BigDecimal("20")));
|
||||
|
||||
assertThat(response.status()).isEqualTo("SUBMITTED");
|
||||
assertThat(response.itemId()).isEqualTo(31L);
|
||||
verify(batchService).recalculateOuterDimensions(1L, measurement, List.of(item), 31L,
|
||||
new BigDecimal("30"), new BigDecimal("20"), new BigDecimal("20"));
|
||||
verify(repository, never()).replaceItems(anyLong(), anyLong(), any());
|
||||
assertThat(inbound.getActualVolumeCbm()).isEqualByComparingTo("1.8000");
|
||||
}
|
||||
|
||||
@Test
|
||||
void correctingWeightKeepsOcrOriginalAndWritesImmutableRevision() {
|
||||
InboundOrderEntity inbound = inbound("MEASURING");
|
||||
@@ -1089,7 +1171,7 @@ class InboundMeasurementServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void submitDoesNotBlockOnUnconfirmedWeight() {
|
||||
void submitRejectsWhenNoWeightImageHasBeenConfirmed() {
|
||||
InboundOrderEntity inbound = inbound("MEASURING");
|
||||
InboundMeasurementEntity measurement = new InboundMeasurementEntity();
|
||||
measurement.setId(30L);
|
||||
@@ -1103,16 +1185,37 @@ class InboundMeasurementServiceTest {
|
||||
when(repository.listItems(1L, 30L)).thenReturn(List.of(item));
|
||||
when(repository.listWeights(1L, 10L)).thenReturn(List.of(
|
||||
weight(20L, "20", "KG", "PENDING_CONFIRM", 0)));
|
||||
when(repository.updateMeasurement(measurement)).thenReturn(true);
|
||||
when(repository.updateInbound(inbound)).thenReturn(true);
|
||||
assertThatThrownBy(() -> service.submit(1L, 10L, 9L,
|
||||
new InboundMeasurementRequests.SubmitRequest(30L, 0)))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("至少上传并确认一张称重图片");
|
||||
|
||||
InboundMeasurementResponses.MeasurementSubmitted result = service.submit(1L, 10L, 9L,
|
||||
new InboundMeasurementRequests.SubmitRequest(30L, 0));
|
||||
verify(repository, never()).updateMeasurement(any());
|
||||
verify(repository, never()).updateInbound(any());
|
||||
}
|
||||
|
||||
assertThat(result.processStatus()).isEqualTo("MEASURE_PENDING");
|
||||
assertThat(inbound.getActualGrossWeightKg()).isNull();
|
||||
verify(repository).updateMeasurement(measurement);
|
||||
verify(repository).updateInbound(inbound);
|
||||
@Test
|
||||
void submitRejectsWhenThereIsNoWeightImage() {
|
||||
InboundOrderEntity inbound = inbound("MEASURING");
|
||||
InboundMeasurementEntity measurement = new InboundMeasurementEntity();
|
||||
measurement.setId(30L);
|
||||
measurement.setStatus("DRAFT");
|
||||
measurement.setVersion(0);
|
||||
InboundMeasurementItemEntity item = new InboundMeasurementItemEntity();
|
||||
item.setPackageCount(1);
|
||||
item.setSourceImageFileId(500L);
|
||||
when(repository.findInbound(1L, 10L)).thenReturn(inbound);
|
||||
when(repository.findMeasurement(1L, 10L, 30L)).thenReturn(measurement);
|
||||
when(repository.listItems(1L, 30L)).thenReturn(List.of(item));
|
||||
when(repository.listWeights(1L, 10L)).thenReturn(List.of());
|
||||
|
||||
assertThatThrownBy(() -> service.submit(1L, 10L, 9L,
|
||||
new InboundMeasurementRequests.SubmitRequest(30L, 0)))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("至少上传并确认一张称重图片");
|
||||
|
||||
verify(repository, never()).updateMeasurement(any());
|
||||
verify(repository, never()).updateInbound(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -521,16 +521,22 @@ class LoadingPickServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void taskDetailRejectsWarehouseThatDoesNotOwnTheContainer() {
|
||||
void taskDetailDoesNotRecheckWarehouseOwnership() {
|
||||
LoadingPickTaskEntity task = task();
|
||||
when(repository.task(20L)).thenReturn(task);
|
||||
when(repository.items(20L)).thenReturn(List.of(item()));
|
||||
when(repository.items(20L, 999L)).thenReturn(List.of(item()));
|
||||
when(repository.cartonLabels(20L)).thenReturn(List.of());
|
||||
when(repository.inboundOwners(20L)).thenReturn(List.of(
|
||||
new LoadingPickItemMapper.InboundOwner(101L, 50L)));
|
||||
when(palletAllocations.list(101L, 50L)).thenReturn(List.of(allocation(50L, 2, 202L)));
|
||||
|
||||
assertThatThrownBy(() -> service.task(20L, 999L))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("不属于当前仓库");
|
||||
var result = service.task(20L, 999L);
|
||||
|
||||
verify(repository, never()).items(20L, 999L);
|
||||
verify(repository, never()).cartonLabels(20L);
|
||||
verify(completionCommands, never()).latest(20L);
|
||||
assertThat(result.id()).isEqualTo(20L);
|
||||
assertThat(result.items()).hasSize(1);
|
||||
assertThat(result.items().get(0).inventoryResources()).singleElement()
|
||||
.extracting("availablePackageCount").isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -1412,6 +1418,40 @@ class LoadingPickServiceTest {
|
||||
verify(settlementService, never()).settle(20L, 9L, "operator");
|
||||
}
|
||||
|
||||
@Test
|
||||
void batchUsesConfirmedInboundInventoryWhenContainerLoadsAcrossWarehouses() {
|
||||
LoadingPickTaskEntity task = task();
|
||||
LoadingPickItemEntity item = item();
|
||||
item.setPlannedPackageCount(250);
|
||||
when(repository.taskForUpdate(20L)).thenReturn(task);
|
||||
when(repository.task(20L)).thenReturn(task);
|
||||
when(repository.assignment(20L)).thenReturn(assignment());
|
||||
when(repository.items(20L)).thenReturn(List.of(item));
|
||||
when(repository.items(20L, 101L)).thenReturn(List.of(item));
|
||||
when(repository.inboundOwners(20L)).thenReturn(List.of(
|
||||
new LoadingPickItemMapper.InboundOwner(1L, 50L)));
|
||||
when(palletAllocations.list(1L, 50L)).thenReturn(List.of(allocation(50L, 250, 202L)));
|
||||
when(repository.pick(30L, 250, 0, 101L)).thenReturn(1);
|
||||
when(repository.allTaskItemsResolved(20L)).thenReturn(true);
|
||||
when(repository.touchTask(20L, 0)).thenReturn(1);
|
||||
|
||||
var snapshot = service.inventorySnapshot(20L, 101L);
|
||||
assertThat(snapshot.warehouseId()).isEqualTo(101L);
|
||||
assertThat(snapshot.inbounds()).singleElement().satisfies(inbound -> {
|
||||
assertThat(inbound.availablePackageCount()).isEqualTo(250);
|
||||
assertThat(inbound.resources()).singleElement()
|
||||
.extracting("availablePackageCount")
|
||||
.isEqualTo(250);
|
||||
});
|
||||
|
||||
service.batch(20L, "batch-cross-warehouse", new BatchRequest(
|
||||
101L, 10L, 99L, 0, snapshot.snapshotToken(), List.of(
|
||||
new BatchItemRequest(30L, "PICK", 250, null, 0, null, null))), 9L, "operator");
|
||||
|
||||
verify(repository).pick(30L, 250, 0, 101L);
|
||||
verify(repository).touchTask(20L, 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsBatchTaskVersionConflictBeforeWriting() {
|
||||
LoadingPickTaskEntity task = task();
|
||||
@@ -1561,6 +1601,11 @@ class LoadingPickServiceTest {
|
||||
}
|
||||
|
||||
private static InboundPalletAllocationEntity allocation(long inboundId, int packageCount) {
|
||||
return allocation(inboundId, packageCount, 101L);
|
||||
}
|
||||
|
||||
private static InboundPalletAllocationEntity allocation(long inboundId, int packageCount,
|
||||
long warehouseId) {
|
||||
InboundPalletAllocationEntity allocation = new InboundPalletAllocationEntity();
|
||||
allocation.setId(72L);
|
||||
allocation.setTenantId(1L);
|
||||
@@ -1568,7 +1613,7 @@ class LoadingPickServiceTest {
|
||||
allocation.setBlockId(71L);
|
||||
allocation.setSlotNo(1);
|
||||
allocation.setAllocationNo(1);
|
||||
allocation.setWarehouseId(101L);
|
||||
allocation.setWarehouseId(warehouseId);
|
||||
allocation.setPalletId(73L);
|
||||
allocation.setLocationId(74L);
|
||||
allocation.setLocationCode("A-01");
|
||||
|
||||
@@ -26,6 +26,7 @@ import java.util.List;
|
||||
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.anyLong;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
@@ -97,6 +98,81 @@ class MeasurementBatchServiceTest {
|
||||
assertThat(items.getValue().get(0).getInboundDetailId()).isEqualTo(80001L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void recalculatesSubmittedItemsInPlaceWithoutChangingTheirIdentityOrStatus() {
|
||||
InboundMeasurementEntity measurement = new InboundMeasurementEntity();
|
||||
measurement.setId(100L);
|
||||
measurement.setTenantId(1L);
|
||||
measurement.setInboundId(10L);
|
||||
measurement.setSourceType("MANUAL");
|
||||
measurement.setStatus("SUBMITTED");
|
||||
measurement.setVersion(4);
|
||||
measurement.setCreatedBy(9L);
|
||||
InboundMeasurementItemEntity existing = new InboundMeasurementItemEntity();
|
||||
existing.setId(200L);
|
||||
existing.setTenantId(1L);
|
||||
existing.setMeasurementId(100L);
|
||||
existing.setInboundDetailId(80001L);
|
||||
existing.setBoxCode("RK-001#1");
|
||||
existing.setItemNo("001");
|
||||
existing.setBoxSpecification("40x30x20cm");
|
||||
existing.setPackageCount(2);
|
||||
existing.setLengthCm(new BigDecimal("40"));
|
||||
existing.setWidthCm(new BigDecimal("30"));
|
||||
existing.setHeightCm(new BigDecimal("20"));
|
||||
when(repository.updateItem(existing)).thenReturn(true);
|
||||
|
||||
service.recalculateStrategy(1L, measurement, List.of(existing), "ACTUAL", null);
|
||||
|
||||
assertThat(existing.getId()).isEqualTo(200L);
|
||||
assertThat(existing.getBoxCode()).isEqualTo("RK-001#1");
|
||||
assertThat(existing.getOuterLengthCm()).isEqualByComparingTo("40.000");
|
||||
assertThat(measurement.getStatus()).isEqualTo("SUBMITTED");
|
||||
assertThat(measurement.getTotalVolumeCbm()).isEqualByComparingTo("0.0480");
|
||||
verify(repository).updateItem(existing);
|
||||
verify(repository, never()).replaceItems(anyLong(), anyLong(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void recalculatesOneSubmittedWarehouseOuterDimensionRowInPlace() {
|
||||
InboundMeasurementEntity measurement = new InboundMeasurementEntity();
|
||||
measurement.setId(100L);
|
||||
measurement.setTenantId(1L);
|
||||
measurement.setInboundId(10L);
|
||||
measurement.setSourceType("MANUAL");
|
||||
measurement.setStatus("SUBMITTED");
|
||||
measurement.setVersion(4);
|
||||
measurement.setCreatedBy(9L);
|
||||
InboundMeasurementItemEntity existing = new InboundMeasurementItemEntity();
|
||||
existing.setId(200L);
|
||||
existing.setTenantId(1L);
|
||||
existing.setMeasurementId(100L);
|
||||
existing.setInboundDetailId(80001L);
|
||||
existing.setBoxCode("RK-001#1");
|
||||
existing.setItemNo("001");
|
||||
existing.setBoxSpecification("40x30x20cm");
|
||||
existing.setPackageCount(2);
|
||||
existing.setLengthCm(new BigDecimal("40"));
|
||||
existing.setWidthCm(new BigDecimal("30"));
|
||||
existing.setHeightCm(new BigDecimal("20"));
|
||||
when(repository.updateItem(existing)).thenReturn(true);
|
||||
|
||||
service.recalculateOuterDimensions(1L, measurement, List.of(existing), 200L,
|
||||
new BigDecimal("30"), new BigDecimal("20"), new BigDecimal("20"));
|
||||
|
||||
assertThat(existing.getId()).isEqualTo(200L);
|
||||
assertThat(existing.getBoxCode()).isEqualTo("RK-001#1");
|
||||
assertThat(existing.getManualOuterLengthCm()).isEqualByComparingTo("30");
|
||||
assertThat(existing.getManualOuterWidthCm()).isEqualByComparingTo("20");
|
||||
assertThat(existing.getManualOuterHeightCm()).isEqualByComparingTo("20");
|
||||
assertThat(existing.getOuterLengthCm()).isEqualByComparingTo("30.000");
|
||||
assertThat(existing.getUnitVolumeCbm()).isEqualByComparingTo("0.0120");
|
||||
assertThat(measurement.getStatus()).isEqualTo("SUBMITTED");
|
||||
assertThat(measurement.getTotalVolumeCbm()).isEqualByComparingTo("0.0240");
|
||||
verify(repository).updateItem(existing);
|
||||
verify(repository, never()).replaceItems(anyLong(), anyLong(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void automaticallyAppliesCustomerDefaultRuleToActualMeasurement() {
|
||||
CustomerChargeRuleEntity rule = new CustomerChargeRuleEntity();
|
||||
|
||||
+16
-1
@@ -1,6 +1,7 @@
|
||||
package com.baiyue.firstleg.terminaldrayage.controller;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
|
||||
import java.nio.file.Files;
|
||||
@@ -50,10 +51,24 @@ class TerminalDrayageOpenApiContractTest {
|
||||
"platform:firstleg:terminal-drayage:pickup:confirm",
|
||||
"platform:firstleg:terminal-drayage:empty-return:confirm",
|
||||
"expectedVersion", "Idempotency-Key", "estimatedEmptyReturnAt",
|
||||
"emptyReturnWarning", "actualFacts", "deadlineAt", "overdueHours", "overdueDays");
|
||||
"emptyReturnWarning", "actualFacts", "deadlineAt", "overdueHours", "overdueDays",
|
||||
"船节点跟踪任务尚未建立也返回WAITING_FOR_ARRIVAL空详情",
|
||||
"允许读取空邮件列表,不以船节点跟踪任务是否已经建立作为货柜存在性条件");
|
||||
assertThat(yaml).contains("description: 卸柜完成时间加72小时");
|
||||
}
|
||||
|
||||
@Test
|
||||
void terminalDrayageContextsUseConfirmedOutboundAsTheOverseasBoundary() throws Exception {
|
||||
var mapper = com.baiyue.firstleg.terminaldrayage.mapper.TerminalDrayageMapper.class;
|
||||
for (String methodName : java.util.List.of("context", "lockContext", "contexts")) {
|
||||
var method = java.util.Arrays.stream(mapper.getDeclaredMethods())
|
||||
.filter(value -> methodName.equals(value.getName())).findFirst().orElseThrow();
|
||||
String sql = String.join("\n", method.getAnnotation(Select.class).value());
|
||||
assertThat(sql).contains("FROM fl_outbound_dispatch", "d.confirmed_at IS NOT NULL",
|
||||
"d.reversed_at IS NULL", "d.status='WAITING_FOR_LOADING'");
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertPermission(String methodName, String permission) {
|
||||
var method = java.util.Arrays.stream(PlatformTerminalDrayageController.class.getDeclaredMethods())
|
||||
.filter(value -> methodName.equals(value.getName())).findFirst().orElseThrow();
|
||||
|
||||
+1
-1
@@ -61,7 +61,7 @@ class TerminalDrayageSqlContractTest {
|
||||
"UNIQUE KEY uk_terminal_drayage_operation(container_booking_id,idempotency_key)",
|
||||
"服务端按卸柜完成时间加72小时计算的预计还柜时间");
|
||||
assertThat(sql).doesNotContain("ON DELETE CASCADE");
|
||||
assertThat(mapper).contains("WHERE b.id=#{containerBookingId} FOR UPDATE",
|
||||
assertThat(mapper).contains("WHERE b.id=#{containerBookingId}", "FOR UPDATE",
|
||||
"WHERE container_booking_id=#{value.containerBookingId} AND version=#{expectedVersion}")
|
||||
.doesNotContain("fl_carrier_schedule_query", "fl_carrier_schedule_node_fact");
|
||||
}
|
||||
|
||||
+31
-2
@@ -84,6 +84,35 @@ class ContainerWorkbenchServiceTest {
|
||||
verifyNoInteractions(carrierSchedules);
|
||||
}
|
||||
|
||||
@Test
|
||||
void overseasDetailKeepsNewlyDepartedContainerBeforeCarrierTrackingExists() {
|
||||
ContainerBookingEntity container = container();
|
||||
container.setOutboundConfirmedAt(LocalDateTime.of(2026, 9, 10, 10, 50));
|
||||
Set<String> permissions = Set.of("platform:firstleg:terminal-drayage:list",
|
||||
"platform:firstleg:terminal-drayage:document:edit");
|
||||
when(repository.find(11L)).thenReturn(container);
|
||||
when(repository.departed(11L)).thenReturn(true);
|
||||
var waiting = waitingDrayage();
|
||||
when(terminalDrayages.views(List.of(11L))).thenReturn(List.of(waiting));
|
||||
when(terminalDrayages.authorize(List.of(waiting), permissions)).thenReturn(List.of(waiting));
|
||||
|
||||
var result = service.overseasDetail(11L, permissions);
|
||||
|
||||
assertThat(result.workbenchStage()).isEqualTo("DEPARTED_WAREHOUSE");
|
||||
assertThat(result.actualFacts()).isNull();
|
||||
assertThat(result.terminalDrayage()).isEqualTo(waiting);
|
||||
assertThat(result.allowedActions()).containsExactly("MANAGE_DESTINATION_DOCUMENTS");
|
||||
verify(terminalDrayages, never()).detail(11L);
|
||||
verifyNoInteractions(carrierSchedules);
|
||||
}
|
||||
|
||||
private static com.baiyue.firstleg.terminaldrayage.dto.TerminalDrayageDtos.View waitingDrayage() {
|
||||
return new com.baiyue.firstleg.terminaldrayage.dto.TerminalDrayageDtos.View(
|
||||
11L, "MSCU1234567", 21L, "WAITING_FOR_ARRIVAL", 0, null, null, null,
|
||||
null, null, null, null, new com.baiyue.firstleg.terminaldrayage.dto.TerminalDrayageDtos.Warning(
|
||||
"NOT_APPLICABLE", null, 0, 0), List.of(), null, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
void realOverseasListAndDetailProjectBothEmptyReturnActions() throws Exception {
|
||||
verifyRealEmptyReturnProjection(true, true);
|
||||
@@ -139,8 +168,8 @@ class ContainerWorkbenchServiceTest {
|
||||
var serialized = json.readTree(json.writeValueAsString(com.baiyue.common.core.api.ApiResponse.success(page)));
|
||||
assertThat(serialized.at("/data/records/0/allowedActions")).isEqualTo(json.valueToTree(expected));
|
||||
assertThat(serialized.at("/data/records/0/terminalDrayage/allowedActions")).isEqualTo(json.valueToTree(expected));
|
||||
verify(mapper).findAll(List.of(11L));
|
||||
verify(mapper).contexts(List.of(11L));
|
||||
verify(mapper, times(2)).findAll(List.of(11L));
|
||||
verify(mapper, times(2)).contexts(List.of(11L));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+24
-11
@@ -2,7 +2,7 @@ package com.baiyue.firstleg.terminaldrayage.service;
|
||||
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.client.WarehouseSupplierClient;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.ManualNodeRequest;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.ContainerManualNodeRequest;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.ContainerActualFacts;
|
||||
import com.baiyue.firstleg.portschedule.service.CarrierScheduleQueryService;
|
||||
import com.baiyue.firstleg.service.WarehouseSupplierSnapshotService;
|
||||
@@ -125,8 +125,8 @@ class TerminalDrayageServiceTest {
|
||||
|
||||
assertThat(result.status()).isEqualTo("PICKED_UP");
|
||||
assertThat(result.actualPickupAt()).isEqualTo(LocalDateTime.of(2026, 9, 2, 10, 0));
|
||||
ArgumentCaptor<ManualNodeRequest> node = ArgumentCaptor.forClass(ManualNodeRequest.class);
|
||||
verify(carrierSchedules).addManualNode(anyLong(), anyLong(), anyString(), anyString(), node.capture());
|
||||
ArgumentCaptor<ContainerManualNodeRequest> node = ArgumentCaptor.forClass(ContainerManualNodeRequest.class);
|
||||
verify(carrierSchedules).addContainerManualNode(anyLong(), anyLong(), anyString(), anyString(), node.capture());
|
||||
assertThat(node.getValue().standardNode()).isEqualTo("PICKED_UP");
|
||||
verify(mapper, times(1)).updateAggregate(any(), anyInt(), anyLong(), anyString());
|
||||
}
|
||||
@@ -144,7 +144,7 @@ class TerminalDrayageServiceTest {
|
||||
new ActualTimeRequest(3, LocalDateTime.of(2026, 9, 4, 10, 0), null)))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessage("海外卸柜收货完成后才能确认还柜");
|
||||
verify(carrierSchedules, never()).addManualNode(anyLong(), anyLong(), anyString(), anyString(), any());
|
||||
verify(carrierSchedules, never()).addContainerManualNode(anyLong(), anyLong(), anyString(), anyString(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -212,7 +212,7 @@ class TerminalDrayageServiceTest {
|
||||
assertThat(result.allowedActions()).doesNotContain("RECORD_EMPTY_RETURN");
|
||||
assertThat(service.recordEmptyReturn(11L, 7L, "admin", "return-new", request)).isEqualTo(result);
|
||||
verify(mapper, times(1)).insert(any(TerminalDrayageEntity.class));
|
||||
verify(carrierSchedules, times(1)).addManualNode(anyLong(), anyLong(), anyString(), anyString(), any());
|
||||
verify(carrierSchedules, times(1)).addContainerManualNode(anyLong(), anyLong(), anyString(), anyString(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -222,7 +222,7 @@ class TerminalDrayageServiceTest {
|
||||
new ActualTimeRequest(0, LocalDateTime.of(2026, 9, 2, 23, 0), null)))
|
||||
.isInstanceOf(BizException.class).hasMessage("实际还柜时间不能早于提柜或卸柜完成时间");
|
||||
verify(mapper, never()).insert(any(TerminalDrayageEntity.class));
|
||||
verify(carrierSchedules, never()).addManualNode(anyLong(), anyLong(), anyString(), anyString(), any());
|
||||
verify(carrierSchedules, never()).addContainerManualNode(anyLong(), anyLong(), anyString(), anyString(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -243,13 +243,26 @@ class TerminalDrayageServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void bulkViewOmitsContainerWhenActualFactPortHasNoTrackingRecord() {
|
||||
void readsDepartedContainerBeforeCarrierTrackingRecordExists() {
|
||||
when(mapper.contexts(List.of(11L))).thenReturn(List.of(
|
||||
new Context(11L, "MSCU1234567", 21L, "ACTIVE", null)));
|
||||
when(mapper.findAll(List.of(11L))).thenReturn(List.of());
|
||||
when(mapper.context(11L)).thenReturn(
|
||||
new Context(11L, "MSCU1234567", 21L, "ACTIVE", null));
|
||||
when(mapper.find(11L)).thenReturn(null);
|
||||
|
||||
assertThat(service.views(List.of(11L))).isEmpty();
|
||||
verify(carrierSchedules).containerActualFacts(List.of(11L));
|
||||
var bulk = service.views(List.of(11L));
|
||||
var detail = service.detail(11L);
|
||||
|
||||
assertThat(bulk).singleElement().satisfies(view -> {
|
||||
assertThat(view.status()).isEqualTo("WAITING_FOR_ARRIVAL");
|
||||
assertThat(view.actualFacts()).isNull();
|
||||
assertThat(view.allowedActions()).isEmpty();
|
||||
});
|
||||
assertThat(detail.status()).isEqualTo("WAITING_FOR_ARRIVAL");
|
||||
assertThat(detail.actualFacts()).isNull();
|
||||
assertThat(detail.allowedActions()).isEmpty();
|
||||
verify(carrierSchedules, times(2)).containerActualFacts(List.of(11L));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -338,8 +351,8 @@ class TerminalDrayageServiceTest {
|
||||
assertThat(result.status()).isEqualTo("EMPTY_RETURNED");
|
||||
assertThat(result.estimatedEmptyReturnAt()).isEqualTo(unloadedAt.plusHours(72));
|
||||
assertThat(result.emptyReturnWarning().status()).isEqualTo("RESOLVED");
|
||||
ArgumentCaptor<ManualNodeRequest> node = ArgumentCaptor.forClass(ManualNodeRequest.class);
|
||||
verify(carrierSchedules).addManualNode(anyLong(), anyLong(), anyString(), anyString(), node.capture());
|
||||
ArgumentCaptor<ContainerManualNodeRequest> node = ArgumentCaptor.forClass(ContainerManualNodeRequest.class);
|
||||
verify(carrierSchedules).addContainerManualNode(anyLong(), anyLong(), anyString(), anyString(), node.capture());
|
||||
assertThat(node.getValue().standardNode()).isEqualTo("EMPTY_RETURNED");
|
||||
assertThat(node.getValue().reason()).isEqualTo("已还柜");
|
||||
assertThat(node.getValue().actualAt()).isEqualTo(LocalDateTime.of(2026, 9, 4, 9, 0));
|
||||
|
||||
Reference in New Issue
Block a user