537 lines
28 KiB
Java
537 lines
28 KiB
Java
package com.baiyue.firstleg.service;
|
|
|
|
import com.baiyue.common.core.api.ApiResponse;
|
|
import com.baiyue.common.core.exception.BizException;
|
|
import com.baiyue.firstleg.client.WarehouseMasterDataClient;
|
|
import com.baiyue.firstleg.client.WarehouseSupplierClient;
|
|
import com.baiyue.firstleg.dto.ContainerDtos.FullUpdateRequest;
|
|
import com.baiyue.firstleg.dto.ContainerDtos.SaveRequest;
|
|
import com.baiyue.firstleg.dto.ContainerDtos.SimulationCreateRequest;
|
|
import com.baiyue.firstleg.dto.ContainerDtos.OceanBookingLinkRequest;
|
|
import com.baiyue.firstleg.entity.ContainerBookingEntity;
|
|
import com.baiyue.firstleg.repository.ContainerRepository;
|
|
import com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows.ChannelRow;
|
|
import com.baiyue.firstleg.pricebookquery.application.port.in.PriceBookQueryUseCase;
|
|
import com.baiyue.firstleg.oceanbooking.repository.OceanBookingRepository;
|
|
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingEntity;
|
|
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingContainerAllocationEntity;
|
|
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingContainerRequirementEntity;
|
|
import jakarta.annotation.Resource;
|
|
import org.junit.jupiter.api.DisplayName;
|
|
import org.junit.jupiter.api.BeforeEach;
|
|
import org.junit.jupiter.api.Test;
|
|
import org.junit.jupiter.params.ParameterizedTest;
|
|
import org.junit.jupiter.params.provider.ValueSource;
|
|
import org.mockito.ArgumentCaptor;
|
|
import org.springframework.boot.test.mock.mockito.MockBean;
|
|
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
|
|
|
import java.math.BigDecimal;
|
|
import java.time.LocalDateTime;
|
|
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.*;
|
|
import static org.mockito.Mockito.never;
|
|
import static org.mockito.Mockito.verify;
|
|
import static org.mockito.Mockito.verifyNoInteractions;
|
|
import static org.mockito.Mockito.when;
|
|
|
|
@DisplayName("货柜基础资料服务")
|
|
@SpringJUnitConfig(ContainerService.class)
|
|
class ContainerServiceTest {
|
|
@MockBean private ContainerRepository repository;
|
|
@MockBean private WarehouseMasterDataClient warehouses;
|
|
@MockBean private PriceBookQueryUseCase priceBooks;
|
|
@MockBean private OceanBookingRepository oceanBookings;
|
|
@MockBean private WarehouseSupplierSnapshotService suppliers;
|
|
@MockBean private FirstlegDocumentNumberGenerator documentNumbers;
|
|
@MockBean private SmartLoadingService smartLoadingService;
|
|
@Resource private ContainerService service;
|
|
|
|
@BeforeEach
|
|
void setUpWarehouse() {
|
|
when(documentNumbers.nextCabinetNo()).thenReturn("D0001");
|
|
when(smartLoadingService.createForContainer(anyLong(), anyLong(), anyString())).thenAnswer(invocation ->
|
|
new com.baiyue.firstleg.dto.SmartLoadingDtos.Plan(501L, "LP-260818-0001", "EDITING",
|
|
invocation.getArgument(0, Long.class), null, null, 0, true, true, null,
|
|
"NOT_STARTED", null, null, null, List.of(), List.of(), List.of()));
|
|
when(warehouses.active(anyString(), anyLong(), anyLong())).thenAnswer(invocation ->
|
|
activeWarehouse(invocation.getArgument(2, Long.class), "DOMESTIC", "上海中转仓"));
|
|
when(suppliers.overseasTrucking(202L, "拖车公司A"))
|
|
.thenReturn(new WarehouseSupplierClient.Snapshot(202L, "拖车公司A", "TRUCKING", "ACTIVE", List.of(), "OVERSEAS"));
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("新增模拟柜从有效渠道冻结快照并生成稳定内部编号")
|
|
void createsSimulationContainerBooking() {
|
|
ChannelRow channel = channel();
|
|
when(priceBooks.activeVersionId()).thenReturn(51L);
|
|
when(priceBooks.channels(51L, 101L)).thenReturn(java.util.List.of(channel));
|
|
ContainerBookingEntity created = service.createSimulation(9L, "admin", simulation());
|
|
|
|
ArgumentCaptor<ContainerBookingEntity> captor = ArgumentCaptor.forClass(ContainerBookingEntity.class);
|
|
verify(repository).insert(captor.capture());
|
|
ContainerBookingEntity saved = captor.getValue();
|
|
assertThat(created).isSameAs(saved);
|
|
assertThat(saved.getCabinetNo()).isEqualTo("D0001");
|
|
verify(documentNumbers).nextCabinetNo();
|
|
verify(documentNumbers, never()).next("CB");
|
|
assertThat(saved.getContainerNo()).isNull();
|
|
assertThat(saved.getOceanBookingId()).isNull();
|
|
assertThat(saved.getChannelGroupCode()).isEqualTo("MATSON_CLX");
|
|
assertThat(saved.getChannelCode()).isNull();
|
|
assertThat(saved.getChannelName()).isNull();
|
|
assertThat(saved.getWarehouseId()).isEqualTo(101L);
|
|
assertThat(saved.getWarehouseName()).isEqualTo("上海中转仓");
|
|
assertThat(saved.getCapacityCbm()).isEqualByComparingTo("68.0000");
|
|
assertThat(saved.getPlanId()).isEqualTo(501L);
|
|
assertThat(saved.getPlanNo()).isEqualTo("LP-260818-0001");
|
|
assertThat(saved.getPlanStatus()).isEqualTo("EDITING");
|
|
verify(smartLoadingService).createForContainer(saved.getId(), 9L, "admin");
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("新增模拟柜拒绝柜型与固定规格不匹配")
|
|
void rejectsSimulationWithNonCanonicalSpecification() {
|
|
SimulationCreateRequest invalid = new SimulationCreateRequest(
|
|
"MATSON_CLX", 101L, "40HQ", new BigDecimal("100"), new BigDecimal("19500"));
|
|
|
|
assertThatThrownBy(() -> service.createSimulation(9L, "admin", invalid))
|
|
.isInstanceOf(BizException.class)
|
|
.hasMessageContaining("容量和货重上限");
|
|
verify(repository, never()).insert(any());
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("智能装柜草稿阶段完整编辑时保留稳定柜次号")
|
|
void updatesAllFieldsWhileEditing() {
|
|
ContainerBookingEntity current = booking("EDITING", 2);
|
|
when(repository.find(11L)).thenReturn(current);
|
|
when(repository.updateDraft(any(), org.mockito.ArgumentMatchers.eq(2),
|
|
org.mockito.ArgumentMatchers.eq(9L), org.mockito.ArgumentMatchers.eq("admin"))).thenReturn(1);
|
|
|
|
service.updateDraft(11L, 9L, "admin", new FullUpdateRequest(2, request()));
|
|
|
|
assertThat(current.getCabinetNo()).isEqualTo("柜-001");
|
|
verify(repository).updateDraft(current, 2, 9L, "admin");
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("待装柜阶段完整编辑时保留稳定柜次号")
|
|
void updatesAllFieldsWhileWaitingForLoading() {
|
|
ContainerBookingEntity current = booking("WAIT_LOADING", 2);
|
|
when(repository.find(11L)).thenReturn(current);
|
|
when(repository.updateDraft(any(), org.mockito.ArgumentMatchers.eq(2),
|
|
org.mockito.ArgumentMatchers.eq(9L), org.mockito.ArgumentMatchers.eq("admin"))).thenReturn(1);
|
|
|
|
service.updateDraft(11L, 9L, "admin", new FullUpdateRequest(2, request()));
|
|
|
|
assertThat(current.getCabinetNo()).isEqualTo("柜-001");
|
|
verify(repository).updateDraft(current, 2, 9L, "admin");
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("进入预装柜中后拒绝完整编辑")
|
|
void rejectsFullUpdateAfterPreloadPush() {
|
|
when(repository.find(11L)).thenReturn(booking("LOADING", 2));
|
|
|
|
assertThatThrownBy(() -> service.updateDraft(
|
|
11L, 9L, "admin", new FullUpdateRequest(2, request())))
|
|
.isInstanceOf(BizException.class)
|
|
.hasMessageContaining("基础资料不可完整编辑");
|
|
|
|
verify(repository, never()).updateDraft(any(), any(Integer.class),
|
|
any(Long.class), any(String.class));
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("旧版本编辑请求触发乐观锁冲突")
|
|
void rejectsStaleVersion() {
|
|
when(repository.find(11L)).thenReturn(booking("EDITING", 3));
|
|
|
|
assertThatThrownBy(() -> service.updateDraft(
|
|
11L, 9L, "admin", new FullUpdateRequest(2, request())))
|
|
.isInstanceOf(BizException.class)
|
|
.hasMessageContaining("刷新后重试");
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("虚拟柜拒绝选择非国内中转仓")
|
|
void rejectsNonDomesticWarehouse() {
|
|
when(warehouses.active(anyString(), anyLong(), anyLong()))
|
|
.thenReturn(activeWarehouse(101L, "FBA", "美国目的仓"));
|
|
|
|
assertThatThrownBy(() -> service.createSimulation(9L, "admin", simulation()))
|
|
.isInstanceOf(BizException.class)
|
|
.hasMessageContaining("国内中转仓");
|
|
|
|
verify(repository, never()).insert(any());
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("虚拟柜仓库名称以仓库主数据为准")
|
|
void replacesRequestedWarehouseName() {
|
|
when(priceBooks.activeVersionId()).thenReturn(51L);
|
|
when(priceBooks.channels(51L, 101L)).thenReturn(java.util.List.of(channel()));
|
|
service.createSimulation(9L, "admin", simulation());
|
|
|
|
ArgumentCaptor<ContainerBookingEntity> captor = ArgumentCaptor.forClass(ContainerBookingEntity.class);
|
|
verify(repository).insert(captor.capture());
|
|
assertThat(captor.getValue().getWarehouseName()).isEqualTo("上海中转仓");
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("仓库主数据服务不可用时拒绝创建虚拟柜")
|
|
void rejectsUnavailableWarehouseService() {
|
|
when(warehouses.active(anyString(), anyLong(), anyLong()))
|
|
.thenThrow(new IllegalStateException("warehouse service unavailable"));
|
|
|
|
assertThatThrownBy(() -> service.createSimulation(9L, "admin", simulation()))
|
|
.isInstanceOf(BizException.class)
|
|
.hasMessageContaining("暂时不可用");
|
|
|
|
verify(repository, never()).insert(any());
|
|
}
|
|
|
|
@ParameterizedTest
|
|
@ValueSource(strings = {"WAIT_LOADING", "EDITING"})
|
|
@DisplayName("空计划及加入入仓单后的编辑计划均允许首次关联且保持清单身份")
|
|
void linksReleasedBookingAfterPreloading(String planStatus) {
|
|
ContainerBookingEntity container = booking(planStatus, 2);
|
|
container.setSourceType("SIMULATION");
|
|
container.setPlanId(501L);
|
|
container.setPlanNo("LP-260818-0001");
|
|
container.setContainerNo(null);
|
|
container.setWarehouseId(101L); container.setChannelGroupCode("EXX"); container.setChannelCode(null); container.setContainerType("40HQ");
|
|
container.setCapacityCbm(new BigDecimal("85.5000")); container.setMaxWeightKg(new BigDecimal("27500.000"));
|
|
OceanBookingEntity booking = new OceanBookingEntity();
|
|
booking.setId(21L); booking.setStatus("RELEASED"); booking.setWarehouseId(101L); booking.setChannelCode("EXX");
|
|
OceanBookingContainerRequirementEntity requirement = new OceanBookingContainerRequirementEntity();
|
|
requirement.setId(31L); requirement.setQuantity(1); requirement.setContainerType("40HQ"); requirement.setCapacityCbm(new BigDecimal("68.0000")); requirement.setMaxWeightKg(new BigDecimal("19500.000"));
|
|
when(repository.find(11L)).thenReturn(container, container);
|
|
when(oceanBookings.find(21L)).thenReturn(booking);
|
|
when(oceanBookings.requirement(21L, 31L)).thenReturn(requirement);
|
|
when(oceanBookings.activeLinkedCount(21L, 31L)).thenReturn(0L);
|
|
when(repository.linkReleasedBooking(eq(11L), eq(21L), eq(31L), eq("MSCU6639870"),
|
|
same(booking), eq(2), eq(9L), eq("admin"))).thenReturn(1);
|
|
|
|
ContainerBookingEntity result = service.linkOceanBooking(11L, 9L, "admin",
|
|
new OceanBookingLinkRequest(21L, 31L, " ms cu 663987 0 ", 2));
|
|
|
|
assertThat(result).isSameAs(container);
|
|
assertThat(container.getCabinetNo()).isEqualTo("柜-001");
|
|
assertThat(container.getPlanId()).isEqualTo(501L);
|
|
assertThat(container.getPlanNo()).isEqualTo("LP-260818-0001");
|
|
assertThat(container.getPlanStatus()).isEqualTo(planStatus);
|
|
assertThat(container.getSourceType()).isEqualTo("SIMULATION");
|
|
verify(repository, never()).hasActiveLoadingItems(11L);
|
|
verify(repository).linkReleasedBooking(eq(11L), eq(21L), eq(31L), eq("MSCU6639870"),
|
|
same(booking), eq(2), eq(9L), eq("admin"));
|
|
verify(oceanBookings).insert(any(OceanBookingContainerAllocationEntity.class));
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("模拟柜允许不带真实柜号原子关联已发布订舱需求")
|
|
void linksReleasedBookingWithoutRealContainerNumber() {
|
|
stubLinkContext();
|
|
ContainerBookingEntity container = repository.find(11L);
|
|
when(repository.find(11L)).thenReturn(container, container);
|
|
when(repository.linkReleasedBooking(eq(11L), eq(21L), eq(31L), isNull(),
|
|
any(OceanBookingEntity.class), eq(2), eq(9L), eq("admin"))).thenReturn(1);
|
|
|
|
ContainerBookingEntity result = service.linkOceanBooking(11L, 9L, "admin",
|
|
new OceanBookingLinkRequest(21L, 31L, null, 2));
|
|
|
|
assertThat(result.getContainerNo()).isNull();
|
|
assertThat(result.getCabinetNo()).isEqualTo("柜-001");
|
|
verify(repository, never()).containerNoExists(anyLong(), anyString(), anyLong());
|
|
verify(repository).linkReleasedBooking(eq(11L), eq(21L), eq(31L), isNull(),
|
|
any(OceanBookingEntity.class), eq(2), eq(9L), eq("admin"));
|
|
verify(oceanBookings).insert(any(OceanBookingContainerAllocationEntity.class));
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("预装已录入真实柜号时关联订舱保留原柜号")
|
|
void linksReleasedBookingWhilePreservingExistingRealContainerNumber() {
|
|
ContainerBookingEntity container = stubLinkContext();
|
|
container.setContainerNo("MSCU6639870");
|
|
when(repository.find(11L)).thenReturn(container, container);
|
|
when(repository.linkReleasedBooking(eq(11L), eq(21L), eq(31L), eq("MSCU6639870"),
|
|
any(OceanBookingEntity.class), eq(2), eq(9L), eq("admin"))).thenReturn(1);
|
|
|
|
ContainerBookingEntity result = service.linkOceanBooking(11L, 9L, "admin",
|
|
new OceanBookingLinkRequest(21L, 31L, null, 2));
|
|
|
|
assertThat(result.getContainerNo()).isEqualTo("MSCU6639870");
|
|
verify(repository).linkReleasedBooking(eq(11L), eq(21L), eq(31L), eq("MSCU6639870"),
|
|
any(OceanBookingEntity.class), eq(2), eq(9L), eq("admin"));
|
|
verify(oceanBookings).insert(any(OceanBookingContainerAllocationEntity.class));
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("关联订舱时拒绝用不同柜号覆盖预装已录入柜号")
|
|
void rejectsDifferentRealContainerNumberWhenLinkingReleasedBooking() {
|
|
ContainerBookingEntity container = stubLinkContext();
|
|
container.setContainerNo("MSCU6639870");
|
|
|
|
assertThatThrownBy(() -> service.linkOceanBooking(11L, 9L, "admin",
|
|
new OceanBookingLinkRequest(21L, 31L, "TSTU1503306", 2)))
|
|
.isInstanceOf(BizException.class).hasMessageContaining("与当前柜次已录入柜号不一致");
|
|
|
|
verify(repository, never()).linkReleasedBooking(anyLong(), anyLong(), anyLong(), anyString(),
|
|
any(), anyInt(), anyLong(), anyString());
|
|
verifyNoInteractions(oceanBookings);
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("提柜后按柜次版本原子补录真实柜号")
|
|
void assignsRealContainerNumberAfterPickup() {
|
|
ContainerBookingEntity linked = booking("EDITING", 3);
|
|
linked.setContainerNo(null); linked.setOceanBookingId(21L); linked.setOceanBookingRequirementId(31L);
|
|
ContainerBookingEntity assigned = booking("EDITING", 4);
|
|
assigned.setContainerNo("MSCU6639870"); assigned.setOceanBookingId(21L); assigned.setOceanBookingRequirementId(31L);
|
|
when(repository.find(11L)).thenReturn(linked, assigned);
|
|
when(repository.assignRealContainerNo(11L, "MSCU6639870", 3, 9L, "admin")).thenReturn(1);
|
|
|
|
ContainerBookingEntity result = service.assignRealContainerNo(
|
|
11L, " ms cu 663987 0 ", 3, 9L, "admin");
|
|
|
|
assertThat(result.getContainerNo()).isEqualTo("MSCU6639870");
|
|
verify(repository).assignRealContainerNo(11L, "MSCU6639870", 3, 9L, "admin");
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("提柜补录真实柜号拒绝同一订舱内重复号")
|
|
void rejectsDuplicateRealContainerNumberAfterPickup() {
|
|
ContainerBookingEntity linked = booking("EDITING", 3);
|
|
linked.setContainerNo(null); linked.setOceanBookingId(21L); linked.setOceanBookingRequirementId(31L);
|
|
when(repository.find(11L)).thenReturn(linked);
|
|
when(repository.containerNoExists(21L, "MSCU6639870", 11L)).thenReturn(true);
|
|
|
|
assertThatThrownBy(() -> service.assignRealContainerNo(
|
|
11L, "MSCU6639870", 3, 9L, "admin"))
|
|
.isInstanceOf(BizException.class).hasMessageContaining("已存在");
|
|
|
|
verify(repository, never()).assignRealContainerNo(anyLong(), anyString(), anyInt(), anyLong(), anyString());
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("首次关联拒绝错误ISO 6346校验位")
|
|
void rejectsInvalidContainerNumberCheckDigit() {
|
|
stubLinkContext();
|
|
|
|
assertThatThrownBy(() -> service.linkOceanBooking(11L, 9L, "admin",
|
|
new OceanBookingLinkRequest(21L, 31L, "MSCU6639871", 2)))
|
|
.isInstanceOf(BizException.class).hasMessageContaining("校验位");
|
|
|
|
verify(repository, never()).linkReleasedBooking(anyLong(), anyLong(), anyLong(), anyString(),
|
|
any(), anyInt(), anyLong(), anyString());
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("首次关联规范化后按同一订舱唯一真实柜号拒绝冲突")
|
|
void rejectsNormalizedContainerNumberConflict() {
|
|
stubLinkContext();
|
|
when(repository.containerNoExists(21L, "MSCU6639870", 11L)).thenReturn(true);
|
|
|
|
assertThatThrownBy(() -> service.linkOceanBooking(11L, 9L, "admin",
|
|
new OceanBookingLinkRequest(21L, 31L, "ms cu6639870", 2)))
|
|
.isInstanceOf(BizException.class).hasMessageContaining("已存在");
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("同一订舱箱型数量允许绑定多个不同真实柜号")
|
|
void linksMultipleRealContainersToTheSameReleasedBookingRequirement() {
|
|
ContainerBookingEntity first = booking("WAIT_LOADING", 2);
|
|
first.setId(11L); first.setContainerNo(null); first.setWarehouseId(101L);
|
|
first.setChannelGroupCode("MATSON_CLX"); first.setChannelCode(null); first.setContainerType("40HQ");
|
|
ContainerBookingEntity second = booking("WAIT_LOADING", 4);
|
|
second.setId(12L); second.setContainerNo(null); second.setWarehouseId(101L);
|
|
second.setChannelGroupCode("MATSON_CLX"); second.setChannelCode(null); second.setContainerType("40HQ");
|
|
OceanBookingEntity oceanBooking = new OceanBookingEntity();
|
|
oceanBooking.setId(21L); oceanBooking.setStatus("RELEASED");
|
|
oceanBooking.setWarehouseId(101L); oceanBooking.setChannelCode("MATSON_CLX");
|
|
OceanBookingContainerRequirementEntity requirement = new OceanBookingContainerRequirementEntity();
|
|
requirement.setId(31L); requirement.setQuantity(2); requirement.setContainerType("40HQ");
|
|
when(repository.find(11L)).thenReturn(first);
|
|
when(repository.find(12L)).thenReturn(second);
|
|
when(oceanBookings.find(21L)).thenReturn(oceanBooking);
|
|
when(oceanBookings.requirement(21L, 31L)).thenReturn(requirement);
|
|
when(oceanBookings.activeLinkedCount(21L, 31L)).thenReturn(0L, 1L);
|
|
when(repository.linkReleasedBooking(eq(11L), eq(21L), eq(31L), eq("MSCU6639870"),
|
|
same(oceanBooking), eq(2), eq(9L), eq("admin"))).thenReturn(1);
|
|
when(repository.linkReleasedBooking(eq(12L), eq(21L), eq(31L), eq("TSTU1503306"),
|
|
same(oceanBooking), eq(4), eq(9L), eq("admin"))).thenReturn(1);
|
|
|
|
service.linkOceanBooking(11L, 9L, "admin",
|
|
new OceanBookingLinkRequest(21L, 31L, "MSCU6639870", 2));
|
|
service.linkOceanBooking(12L, 9L, "admin",
|
|
new OceanBookingLinkRequest(21L, 31L, "TSTU1503306", 4));
|
|
|
|
ArgumentCaptor<OceanBookingContainerAllocationEntity> allocations =
|
|
ArgumentCaptor.forClass(OceanBookingContainerAllocationEntity.class);
|
|
verify(oceanBookings, org.mockito.Mockito.times(2)).insert(allocations.capture());
|
|
assertThat(allocations.getAllValues()).extracting(OceanBookingContainerAllocationEntity::getContainerBookingId)
|
|
.containsExactly(11L, 12L);
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("已经归属其他订舱的真实柜不能改绑")
|
|
void rejectsContainerAlreadyOwnedByAnotherBooking() {
|
|
ContainerBookingEntity container = booking("WAIT_LOADING", 2);
|
|
container.setContainerNo(null);
|
|
container.setOceanBookingId(22L);
|
|
when(repository.find(11L)).thenReturn(container);
|
|
|
|
assertThatThrownBy(() -> service.linkOceanBooking(11L, 9L, "admin",
|
|
new OceanBookingLinkRequest(21L, 31L, "MSCU6639870", 2)))
|
|
.isInstanceOf(BizException.class).hasMessageContaining("已关联真实订舱柜");
|
|
|
|
verifyNoInteractions(oceanBookings);
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("首次关联拒绝装柜仓库不一致")
|
|
void rejectsLinkWhenWarehouseDiffers() {
|
|
assertThreeFieldMismatch(102L, "MATSON_CLX", "40HQ");
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("首次关联拒绝物流渠道不一致")
|
|
void rejectsLinkWhenChannelDiffers() {
|
|
assertThreeFieldMismatch(101L, "HEDE", "40HQ");
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("首次关联拒绝柜型不一致")
|
|
void rejectsLinkWhenContainerTypeDiffers() {
|
|
assertThreeFieldMismatch(101L, "MATSON_CLX", "45HQ");
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("未发布订舱不可关联")
|
|
void rejectsDraftBooking() {
|
|
ContainerBookingEntity container = booking("WAIT_LOADING", 2);
|
|
container.setContainerNo(null);
|
|
OceanBookingEntity booking = new OceanBookingEntity();
|
|
booking.setId(21L); booking.setStatus("DRAFT");
|
|
when(repository.find(11L)).thenReturn(container);
|
|
when(oceanBookings.find(21L)).thenReturn(booking);
|
|
|
|
assertThatThrownBy(() -> service.linkOceanBooking(11L, 9L, "admin",
|
|
new OceanBookingLinkRequest(21L, 31L, "MSCU6639870", 2)))
|
|
.isInstanceOf(BizException.class).hasMessageContaining("已发布");
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("订舱箱型需求达到数量上限时拒绝关联")
|
|
void rejectsRequirementOverAllocation() {
|
|
ContainerBookingEntity container = booking("WAIT_LOADING", 2);
|
|
container.setContainerNo(null);
|
|
OceanBookingEntity booking = new OceanBookingEntity();
|
|
booking.setId(21L); booking.setStatus("RELEASED");
|
|
OceanBookingContainerRequirementEntity requirement = new OceanBookingContainerRequirementEntity();
|
|
requirement.setId(31L); requirement.setQuantity(1);
|
|
when(repository.find(11L)).thenReturn(container);
|
|
when(oceanBookings.find(21L)).thenReturn(booking);
|
|
when(oceanBookings.requirement(21L, 31L)).thenReturn(requirement);
|
|
when(oceanBookings.activeLinkedCount(21L, 31L)).thenReturn(1L);
|
|
|
|
assertThatThrownBy(() -> service.linkOceanBooking(11L, 9L, "admin",
|
|
new OceanBookingLinkRequest(21L, 31L, "MSCU6639870", 2)))
|
|
.isInstanceOf(BizException.class).hasMessageContaining("数量上限");
|
|
verify(repository, never()).linkReleasedBooking(anyLong(), anyLong(), anyLong(), anyString(),
|
|
any(), anyInt(), anyLong(), anyString());
|
|
}
|
|
|
|
private void assertThreeFieldMismatch(long bookingWarehouseId, String bookingChannel,
|
|
String requirementType) {
|
|
ContainerBookingEntity container = booking("WAIT_LOADING", 2);
|
|
container.setContainerNo(null);
|
|
container.setWarehouseId(101L); container.setChannelGroupCode("MATSON_CLX"); container.setChannelCode(null);
|
|
container.setContainerType("40HQ");
|
|
OceanBookingEntity oceanBooking = new OceanBookingEntity();
|
|
oceanBooking.setId(21L); oceanBooking.setStatus("RELEASED");
|
|
oceanBooking.setWarehouseId(bookingWarehouseId); oceanBooking.setChannelCode(bookingChannel);
|
|
OceanBookingContainerRequirementEntity requirement = new OceanBookingContainerRequirementEntity();
|
|
requirement.setId(31L); requirement.setQuantity(2); requirement.setContainerType(requirementType);
|
|
when(repository.find(11L)).thenReturn(container);
|
|
when(oceanBookings.find(21L)).thenReturn(oceanBooking);
|
|
when(oceanBookings.requirement(21L, 31L)).thenReturn(requirement);
|
|
when(oceanBookings.activeLinkedCount(21L, 31L)).thenReturn(0L);
|
|
|
|
assertThatThrownBy(() -> service.linkOceanBooking(11L, 9L, "admin",
|
|
new OceanBookingLinkRequest(21L, 31L, "MSCU6639870", 2)))
|
|
.isInstanceOf(BizException.class).hasMessageContaining("仓库、渠道分组或柜型不一致");
|
|
verify(repository, never()).linkReleasedBooking(anyLong(), anyLong(), anyLong(), anyString(),
|
|
any(), anyInt(), anyLong(), anyString());
|
|
}
|
|
|
|
private ContainerBookingEntity stubLinkContext() {
|
|
ContainerBookingEntity container = booking("WAIT_LOADING", 2);
|
|
container.setContainerNo(null);
|
|
container.setWarehouseId(101L); container.setChannelGroupCode("MATSON_CLX"); container.setChannelCode(null);
|
|
container.setContainerType("40HQ");
|
|
OceanBookingEntity oceanBooking = new OceanBookingEntity();
|
|
oceanBooking.setId(21L); oceanBooking.setStatus("RELEASED");
|
|
oceanBooking.setWarehouseId(101L); oceanBooking.setChannelCode("MATSON_CLX");
|
|
OceanBookingContainerRequirementEntity requirement = new OceanBookingContainerRequirementEntity();
|
|
requirement.setId(31L); requirement.setQuantity(2); requirement.setContainerType("40HQ");
|
|
when(repository.find(11L)).thenReturn(container);
|
|
when(oceanBookings.find(21L)).thenReturn(oceanBooking);
|
|
when(oceanBookings.requirement(21L, 31L)).thenReturn(requirement);
|
|
when(oceanBookings.activeLinkedCount(21L, 31L)).thenReturn(0L);
|
|
return container;
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("首次关联遇到有效装柜证据时拒绝")
|
|
void rejectsLinkWhenEvidenceExists() {
|
|
ContainerBookingEntity container = booking("EDITING", 2);
|
|
container.setContainerNo(null);
|
|
when(repository.find(11L)).thenReturn(container);
|
|
when(repository.hasEvidence(11L)).thenReturn(true);
|
|
|
|
assertThatThrownBy(() -> service.linkOceanBooking(11L, 9L, "admin",
|
|
new OceanBookingLinkRequest(21L, 31L, "MSCU6639870", 2)))
|
|
.isInstanceOf(BizException.class).hasMessageContaining("证据");
|
|
verifyNoInteractions(oceanBookings);
|
|
}
|
|
|
|
private static ApiResponse<WarehouseMasterDataClient.Detail> activeWarehouse(long warehouseId,
|
|
String type,
|
|
String name) {
|
|
return ApiResponse.success(new WarehouseMasterDataClient.Detail(
|
|
new WarehouseMasterDataClient.Warehouse(warehouseId, 0L, "CN-SHA-01", null, name, type, "ACTIVE")));
|
|
}
|
|
|
|
private static ContainerBookingEntity booking(String status, int version) {
|
|
ContainerBookingEntity value = new ContainerBookingEntity();
|
|
value.setId(11L);
|
|
value.setContainerNo("MSCU1234567");
|
|
value.setCabinetNo("柜-001");
|
|
value.setPlanStatus(status);
|
|
value.setVersion(version);
|
|
return value;
|
|
}
|
|
|
|
private static SaveRequest request() {
|
|
return new SaveRequest(101L, "上海仓", "BOL-001", "柏悦轮",
|
|
"V001", "40HQ", 201L, "船司A", "CNSHA", "上海港", "USLAX", "洛杉矶港",
|
|
"ONT8", "ONT8", new BigDecimal("68.0000"), new BigDecimal("19500.000"), 202L, "拖车公司A",
|
|
LocalDateTime.of(2026, 8, 10, 8, 0), LocalDateTime.of(2026, 8, 12, 8, 0), null,
|
|
LocalDateTime.of(2026, 8, 25, 8, 0), null,
|
|
LocalDateTime.of(2026, 8, 27, 8, 0), null);
|
|
}
|
|
|
|
private static SimulationCreateRequest simulation() {
|
|
return new SimulationCreateRequest("MATSON_CLX", 101L, "40HQ", new BigDecimal("68.0000"), new BigDecimal("19500.000"));
|
|
}
|
|
|
|
private static ChannelRow channel() {
|
|
ChannelRow channel = new ChannelRow(); channel.setChannelCode("MATSON_CLX_TRUCK"); channel.setChannelName("美西海运"); return channel;
|
|
}
|
|
}
|