fix: align ocean booking link eligibility
This commit is contained in:
+11
-1
@@ -68,11 +68,21 @@ class PlatformOceanBookingControllerContractTest {
|
||||
assertThat(yaml)
|
||||
.contains("/api/platform/v1/ocean-bookings/link-candidates:")
|
||||
.contains("x-permission: platform:firstleg:ocean-booking:list")
|
||||
.contains("warehouseId", "channelCode", "containerType", "capacityCbm", "maxWeightKg")
|
||||
.contains("warehouseId", "channelCode", "containerType")
|
||||
.contains("LinkCandidateResponse", "requiredQuantity", "allocatedQuantity", "remainingQuantity")
|
||||
.contains("Capacity and max weight are derived display snapshots and never eligibility fields")
|
||||
.contains("booking detail calls are not used");
|
||||
}
|
||||
|
||||
@Test
|
||||
void openApiDefinesContainerCountAsRequirementQuantityTotal() throws Exception {
|
||||
String yaml = Files.readString(Path.of("src/main/resources/openapi/ocean-bookings-v1.yaml"));
|
||||
assertThat(yaml)
|
||||
.contains("containerCount is the sum of quantity across all container requirements")
|
||||
.contains("never represents linked or ACTIVE containers")
|
||||
.contains("Sum of all container requirement quantities; independent of allocation, linking or unlinking.");
|
||||
}
|
||||
|
||||
private static Method method(String name) {
|
||||
return Arrays.stream(PlatformOceanBookingController.class.getDeclaredMethods())
|
||||
.filter(method -> method.getName().equals(name)).findFirst().orElseThrow();
|
||||
|
||||
+25
-5
@@ -1,6 +1,7 @@
|
||||
package com.baiyue.firstleg.oceanbooking.repository;
|
||||
|
||||
import com.baiyue.firstleg.oceanbooking.mapper.OceanBookingMapper;
|
||||
import com.baiyue.firstleg.oceanbooking.mapper.OceanBookingContainerAllocationMapper;
|
||||
import com.baiyue.firstleg.oceanbooking.dto.OceanBookingDtos.LinkCandidateQuery;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
@@ -21,7 +22,10 @@ class OceanBookingMapperContractTest {
|
||||
.contains("booking.status=#{query.status}")
|
||||
.contains("booking.booking_no LIKE")
|
||||
.contains("party.party_type='SHIPPER'", "party.party_type='CONSIGNEE'")
|
||||
.contains("fl_ocean_booking_container_allocation", "container.record_status='ACTIVE'", "container_count")
|
||||
.contains("COALESCE((SELECT SUM(requirement.quantity)")
|
||||
.contains("FROM fl_ocean_booking_container_requirement requirement")
|
||||
.contains("requirement.ocean_booking_id=booking.id),0) container_count")
|
||||
.doesNotContain("fl_ocean_booking_container_allocation allocation\n JOIN fl_container_booking")
|
||||
.contains("ORDER BY booking.updated_at DESC,booking.id DESC");
|
||||
}
|
||||
|
||||
@@ -48,12 +52,28 @@ class OceanBookingMapperContractTest {
|
||||
.contains("booking.warehouse_id=#{query.warehouseId}")
|
||||
.contains("booking.channel_code=#{query.channelCode}")
|
||||
.contains("requirement.container_type=#{query.containerType}")
|
||||
.contains("requirement.capacity_cbm=#{query.capacityCbm}")
|
||||
.contains("requirement.max_weight_kg=#{query.maxWeightKg}")
|
||||
.contains("FROM fl_ocean_booking_container_allocation")
|
||||
.contains("GROUP BY ocean_booking_id,requirement_id")
|
||||
.contains("container.record_status='ACTIVE'")
|
||||
.contains("container.ocean_booking_id=allocation.ocean_booking_id")
|
||||
.contains("container.ocean_booking_requirement_id=allocation.requirement_id")
|
||||
.contains("GROUP BY allocation.ocean_booking_id,allocation.requirement_id")
|
||||
.contains("requirement.quantity-COALESCE(allocated.allocated_quantity,0) remaining_quantity")
|
||||
.contains("requirement.quantity>COALESCE(allocated.allocated_quantity,0)")
|
||||
.doesNotContain("fl_container_booking", "SELECT booking.*");
|
||||
.doesNotContain("fl_container_booking", "SELECT booking.*",
|
||||
"requirement.capacity_cbm=#{query.capacityCbm}",
|
||||
"requirement.max_weight_kg=#{query.maxWeightKg}");
|
||||
}
|
||||
|
||||
@Test
|
||||
void finalLinkLimitCountsOnlyActiveContainersStillPointingAtTheSameRequirement() throws Exception {
|
||||
String sql = String.join("\n", OceanBookingContainerAllocationMapper.class
|
||||
.getDeclaredMethod("countActiveLinked", long.class, long.class)
|
||||
.getAnnotation(Select.class).value());
|
||||
assertThat(sql)
|
||||
.contains("container.record_status='ACTIVE'")
|
||||
.contains("container.ocean_booking_id=allocation.ocean_booking_id")
|
||||
.contains("container.ocean_booking_requirement_id=allocation.requirement_id")
|
||||
.contains("allocation.ocean_booking_id=#{oceanBookingId}")
|
||||
.contains("allocation.requirement_id=#{requirementId}");
|
||||
}
|
||||
}
|
||||
|
||||
+17
-1
@@ -171,6 +171,22 @@ class OceanBookingServiceTest {
|
||||
verify(repository, times(2)).page(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void pagePreservesRequirementTotalContainerCountsForSingleMultipleAndEmptyDrafts() {
|
||||
OceanBookingEntity one45Hc = booking("RELEASED", 1); one45Hc.setContainerCount(1L);
|
||||
OceanBookingEntity multipleTypes = booking("RELEASED", 1); multipleTypes.setId(2L);
|
||||
multipleTypes.setContainerCount(4L);
|
||||
OceanBookingEntity emptyDraft = booking("DRAFT", 0); emptyDraft.setId(3L);
|
||||
emptyDraft.setContainerCount(0L);
|
||||
when(repository.page(any())).thenReturn(new PageResult<>(
|
||||
List.of(one45Hc, multipleTypes, emptyDraft), 1, 20, 3, 1));
|
||||
|
||||
PageResult<OceanBookingEntity> result = service.page(new Query(null, null, null, 1, 20));
|
||||
|
||||
assertThat(result.records()).extracting(OceanBookingEntity::getContainerCount)
|
||||
.containsExactly(1L, 4L, 0L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void linkCandidatesDefaultsPaginationAndDelegatesOneNormalizedSetQuery() {
|
||||
LinkCandidate candidate = new LinkCandidate(101L, "OB-260818-0001", 201L, "40HC",
|
||||
@@ -179,7 +195,7 @@ class OceanBookingServiceTest {
|
||||
when(repository.linkCandidates(any())).thenReturn(new PageResult<>(List.of(candidate), 1, 20, 1, 1));
|
||||
|
||||
PageResult<LinkCandidate> result = service.linkCandidates(new LinkCandidateQuery(701L,
|
||||
" SEA-US ", " 40HC ", new BigDecimal("76.0"), new BigDecimal("28000.00"), null, null));
|
||||
" SEA-US ", " 40HC ", null, null));
|
||||
|
||||
assertThat(result.records()).containsExactly(candidate);
|
||||
assertThat(result.records().get(0).remainingQuantity()).isEqualTo(2);
|
||||
|
||||
-9
@@ -53,15 +53,6 @@ class ContainerBookingMapperContractTest {
|
||||
.contains("customer_item.account_id = #{query.customerId}");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("首次关联客户隔离查询使用原生不等号")
|
||||
void customerMismatchQueryUsesNativeSqlOperator() throws Exception {
|
||||
String sql = ContainerBookingMapper.class
|
||||
.getDeclaredMethod("hasMismatchedCustomerItems", long.class, long.class)
|
||||
.getAnnotation(Select.class).value()[0];
|
||||
assertThat(sql).contains("i.account_id<>#{customerId}").doesNotContain("<>");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("关联订舱 SQL 的预计到港参数与 Mapper 参数名一致")
|
||||
void linkReleasedBookingUsesDeclaredEstimatedArrivalParameter() {
|
||||
|
||||
@@ -288,7 +288,7 @@ class ContainerServiceTest {
|
||||
void linksReleasedBookingAfterPreloading() {
|
||||
ContainerBookingEntity container = booking("EDITING", 2);
|
||||
container.setWarehouseId(101L); container.setChannelCode("SEA-US"); container.setContainerType("40HC");
|
||||
container.setCapacityCbm(new BigDecimal("76.0000")); container.setMaxWeightKg(new BigDecimal("28000.000"));
|
||||
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("SEA-US");
|
||||
OceanBookingContainerRequirementEntity requirement = new OceanBookingContainerRequirementEntity();
|
||||
@@ -296,7 +296,7 @@ class ContainerServiceTest {
|
||||
when(repository.find(11L)).thenReturn(container, container);
|
||||
when(oceanBookings.find(21L)).thenReturn(booking);
|
||||
when(oceanBookings.requirement(21L, 31L)).thenReturn(requirement);
|
||||
when(oceanBookings.allocations(21L)).thenReturn(List.of());
|
||||
when(oceanBookings.activeLinkedCount(21L, 31L)).thenReturn(0L);
|
||||
when(repository.linkReleasedBooking(eq(11L), eq(21L), eq(31L), eq("MSCU7654321"),
|
||||
same(booking), eq(2), eq(9L), eq("admin"))).thenReturn(1);
|
||||
|
||||
@@ -311,6 +311,24 @@ class ContainerServiceTest {
|
||||
verify(oceanBookings).insert(any(OceanBookingContainerAllocationEntity.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("首次关联拒绝装柜仓库不一致")
|
||||
void rejectsLinkWhenWarehouseDiffers() {
|
||||
assertThreeFieldMismatch(102L, "SEA-US", "40HC");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("首次关联拒绝物流渠道不一致")
|
||||
void rejectsLinkWhenChannelDiffers() {
|
||||
assertThreeFieldMismatch(101L, "SEA-EU", "40HC");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("首次关联拒绝柜型不一致")
|
||||
void rejectsLinkWhenContainerTypeDiffers() {
|
||||
assertThreeFieldMismatch(101L, "SEA-US", "45HC");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("未发布订舱不可关联")
|
||||
void rejectsDraftBooking() {
|
||||
@@ -333,12 +351,10 @@ class ContainerServiceTest {
|
||||
booking.setId(21L); booking.setStatus("RELEASED");
|
||||
OceanBookingContainerRequirementEntity requirement = new OceanBookingContainerRequirementEntity();
|
||||
requirement.setId(31L); requirement.setQuantity(1);
|
||||
OceanBookingContainerAllocationEntity allocation = new OceanBookingContainerAllocationEntity();
|
||||
allocation.setRequirementId(31L);
|
||||
when(repository.find(11L)).thenReturn(container);
|
||||
when(oceanBookings.find(21L)).thenReturn(booking);
|
||||
when(oceanBookings.requirement(21L, 31L)).thenReturn(requirement);
|
||||
when(oceanBookings.allocations(21L)).thenReturn(List.of(allocation));
|
||||
when(oceanBookings.activeLinkedCount(21L, 31L)).thenReturn(1L);
|
||||
|
||||
assertThatThrownBy(() -> service.linkOceanBooking(11L, 9L, "admin",
|
||||
new OceanBookingLinkRequest(21L, 31L, "MSCU7654321", 2)))
|
||||
@@ -347,6 +363,28 @@ class ContainerServiceTest {
|
||||
any(), anyInt(), anyLong(), anyString());
|
||||
}
|
||||
|
||||
private void assertThreeFieldMismatch(long bookingWarehouseId, String bookingChannel,
|
||||
String requirementType) {
|
||||
ContainerBookingEntity container = booking("WAIT_LOADING", 2);
|
||||
container.setWarehouseId(101L); container.setChannelCode("SEA-US");
|
||||
container.setContainerType("40HC");
|
||||
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, "MSCU7654321", 2)))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("仓库、渠道或柜型不一致");
|
||||
verify(repository, never()).linkReleasedBooking(anyLong(), anyLong(), anyLong(), anyString(),
|
||||
any(), anyInt(), anyLong(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("完全空的模拟柜直接解绑并删除当前allocation,不依赖MERGED来源柜")
|
||||
void unlinksEmptySimulationDirectly() {
|
||||
|
||||
Reference in New Issue
Block a user