fix: align ocean booking link eligibility
This commit is contained in:
@@ -171,8 +171,6 @@ public interface ContainerBookingMapper extends BaseMapper<ContainerBookingEntit
|
|||||||
@Select("SELECT COUNT(*) FROM fl_loading_pick_task t JOIN fl_loading_plan p ON p.id=t.loading_plan_id WHERE p.container_booking_id=#{id}") int hasPickTask(@Param("id") long id);
|
@Select("SELECT COUNT(*) FROM fl_loading_pick_task t JOIN fl_loading_plan p ON p.id=t.loading_plan_id WHERE p.container_booking_id=#{id}") int hasPickTask(@Param("id") long id);
|
||||||
@Select("SELECT COUNT(*) FROM fl_loading_evidence e JOIN fl_loading_plan_item i ON i.inbound_id=e.inbound_id AND i.account_id=e.tenant_id JOIN fl_loading_plan p ON p.id=i.loading_plan_id WHERE p.container_booking_id=#{id} AND i.deleted=0 AND e.status='ACTIVE'") int hasEvidence(@Param("id") long id);
|
@Select("SELECT COUNT(*) FROM fl_loading_evidence e JOIN fl_loading_plan_item i ON i.inbound_id=e.inbound_id AND i.account_id=e.tenant_id JOIN fl_loading_plan p ON p.id=i.loading_plan_id WHERE p.container_booking_id=#{id} AND i.deleted=0 AND e.status='ACTIVE'") int hasEvidence(@Param("id") long id);
|
||||||
@Select("SELECT COUNT(*) FROM fl_container_document WHERE container_booking_id=#{id}") int hasDocuments(@Param("id") long id);
|
@Select("SELECT COUNT(*) FROM fl_container_document WHERE container_booking_id=#{id}") int hasDocuments(@Param("id") long id);
|
||||||
@Select("SELECT COUNT(*) FROM fl_loading_plan_item i JOIN fl_loading_plan p ON p.id=i.loading_plan_id WHERE p.container_booking_id=#{id} AND i.deleted=0 AND i.account_id<>#{customerId}") int hasMismatchedCustomerItems(@Param("id") long id, @Param("customerId") long customerId);
|
|
||||||
|
|
||||||
@Update("""
|
@Update("""
|
||||||
UPDATE fl_container_booking SET container_no=#{containerNo}, voyage_no=#{voyageNo},
|
UPDATE fl_container_booking SET container_no=#{containerNo}, voyage_no=#{voyageNo},
|
||||||
updated_by=#{operatorId}, updated_by_name=#{operatorName},
|
updated_by=#{operatorId}, updated_by_name=#{operatorName},
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import jakarta.validation.constraints.NotEmpty;
|
|||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
import jakarta.validation.constraints.Positive;
|
import jakarta.validation.constraints.Positive;
|
||||||
import jakarta.validation.constraints.Size;
|
import jakarta.validation.constraints.Size;
|
||||||
import jakarta.validation.constraints.DecimalMin;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
@@ -30,8 +29,6 @@ public final class OceanBookingDtos {
|
|||||||
public record LinkCandidateQuery(@NotNull @Positive Long warehouseId,
|
public record LinkCandidateQuery(@NotNull @Positive Long warehouseId,
|
||||||
@NotBlank @Size(max = 64) String channelCode,
|
@NotBlank @Size(max = 64) String channelCode,
|
||||||
@NotBlank @Size(max = 32) String containerType,
|
@NotBlank @Size(max = 32) String containerType,
|
||||||
@NotNull @DecimalMin(value = "0", inclusive = false) BigDecimal capacityCbm,
|
|
||||||
@NotNull @DecimalMin(value = "0", inclusive = false) BigDecimal maxWeightKg,
|
|
||||||
@Min(1) Long page,
|
@Min(1) Long page,
|
||||||
@Min(1) @Max(200) Long size) {
|
@Min(1) @Max(200) Long size) {
|
||||||
public LinkCandidateQuery {
|
public LinkCandidateQuery {
|
||||||
|
|||||||
+13
@@ -3,8 +3,21 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
|||||||
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingContainerAllocationEntity;
|
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingContainerAllocationEntity;
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
import org.apache.ibatis.annotations.Param;
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import org.apache.ibatis.annotations.Select;
|
||||||
import org.apache.ibatis.annotations.Update;
|
import org.apache.ibatis.annotations.Update;
|
||||||
@Mapper public interface OceanBookingContainerAllocationMapper extends BaseMapper<OceanBookingContainerAllocationEntity> {
|
@Mapper public interface OceanBookingContainerAllocationMapper extends BaseMapper<OceanBookingContainerAllocationEntity> {
|
||||||
@Update("UPDATE fl_ocean_booking_container_allocation SET container_booking_id=#{targetId} WHERE container_booking_id=#{sourceId}")
|
@Update("UPDATE fl_ocean_booking_container_allocation SET container_booking_id=#{targetId} WHERE container_booking_id=#{sourceId}")
|
||||||
int reassign(@Param("sourceId") long sourceId, @Param("targetId") long targetId);
|
int reassign(@Param("sourceId") long sourceId, @Param("targetId") long targetId);
|
||||||
|
|
||||||
|
@Select("""
|
||||||
|
SELECT COUNT(*) FROM fl_ocean_booking_container_allocation allocation
|
||||||
|
JOIN fl_container_booking container ON container.id=allocation.container_booking_id
|
||||||
|
AND container.record_status='ACTIVE'
|
||||||
|
AND container.ocean_booking_id=allocation.ocean_booking_id
|
||||||
|
AND container.ocean_booking_requirement_id=allocation.requirement_id
|
||||||
|
WHERE allocation.ocean_booking_id=#{oceanBookingId}
|
||||||
|
AND allocation.requirement_id=#{requirementId}
|
||||||
|
""")
|
||||||
|
long countActiveLinked(@Param("oceanBookingId") long oceanBookingId,
|
||||||
|
@Param("requirementId") long requirementId);
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-8
@@ -26,9 +26,9 @@ public interface OceanBookingMapper extends BaseMapper<OceanBookingEntity> {
|
|||||||
WHERE party.ocean_booking_id=booking.id AND party.party_type='SHIPPER' LIMIT 1) shipper_name,
|
WHERE party.ocean_booking_id=booking.id AND party.party_type='SHIPPER' LIMIT 1) shipper_name,
|
||||||
(SELECT party.party_name FROM fl_ocean_booking_party party
|
(SELECT party.party_name FROM fl_ocean_booking_party party
|
||||||
WHERE party.ocean_booking_id=booking.id AND party.party_type='CONSIGNEE' LIMIT 1) consignee_name,
|
WHERE party.ocean_booking_id=booking.id AND party.party_type='CONSIGNEE' LIMIT 1) consignee_name,
|
||||||
(SELECT COUNT(*) FROM fl_ocean_booking_container_allocation allocation
|
COALESCE((SELECT SUM(requirement.quantity)
|
||||||
JOIN fl_container_booking container ON container.id=allocation.container_booking_id
|
FROM fl_ocean_booking_container_requirement requirement
|
||||||
WHERE allocation.ocean_booking_id=booking.id AND container.record_status='ACTIVE') container_count
|
WHERE requirement.ocean_booking_id=booking.id),0) container_count
|
||||||
FROM fl_ocean_booking booking
|
FROM fl_ocean_booking booking
|
||||||
WHERE (#{query.customerId} IS NULL OR booking.customer_id=#{query.customerId})
|
WHERE (#{query.customerId} IS NULL OR booking.customer_id=#{query.customerId})
|
||||||
AND booking.status IN ('DRAFT','RELEASED')
|
AND booking.status IN ('DRAFT','RELEASED')
|
||||||
@@ -54,17 +54,19 @@ public interface OceanBookingMapper extends BaseMapper<OceanBookingEntity> {
|
|||||||
JOIN fl_ocean_booking_container_requirement requirement
|
JOIN fl_ocean_booking_container_requirement requirement
|
||||||
ON requirement.ocean_booking_id=booking.id
|
ON requirement.ocean_booking_id=booking.id
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
SELECT ocean_booking_id,requirement_id,COUNT(*) allocated_quantity
|
SELECT allocation.ocean_booking_id,allocation.requirement_id,COUNT(*) allocated_quantity
|
||||||
FROM fl_ocean_booking_container_allocation
|
FROM fl_ocean_booking_container_allocation allocation
|
||||||
GROUP BY ocean_booking_id,requirement_id
|
JOIN fl_container_booking container ON container.id=allocation.container_booking_id
|
||||||
|
AND container.record_status='ACTIVE'
|
||||||
|
AND container.ocean_booking_id=allocation.ocean_booking_id
|
||||||
|
AND container.ocean_booking_requirement_id=allocation.requirement_id
|
||||||
|
GROUP BY allocation.ocean_booking_id,allocation.requirement_id
|
||||||
) allocated ON allocated.ocean_booking_id=booking.id
|
) allocated ON allocated.ocean_booking_id=booking.id
|
||||||
AND allocated.requirement_id=requirement.id
|
AND allocated.requirement_id=requirement.id
|
||||||
WHERE booking.status='RELEASED'
|
WHERE booking.status='RELEASED'
|
||||||
AND booking.warehouse_id=#{query.warehouseId}
|
AND booking.warehouse_id=#{query.warehouseId}
|
||||||
AND booking.channel_code=#{query.channelCode}
|
AND booking.channel_code=#{query.channelCode}
|
||||||
AND requirement.container_type=#{query.containerType}
|
AND requirement.container_type=#{query.containerType}
|
||||||
AND requirement.capacity_cbm=#{query.capacityCbm}
|
|
||||||
AND requirement.max_weight_kg=#{query.maxWeightKg}
|
|
||||||
AND requirement.quantity>COALESCE(allocated.allocated_quantity,0)
|
AND requirement.quantity>COALESCE(allocated.allocated_quantity,0)
|
||||||
ORDER BY booking.updated_at DESC,booking.id DESC,requirement.sort_no ASC,requirement.id ASC
|
ORDER BY booking.updated_at DESC,booking.id DESC,requirement.sort_no ASC,requirement.id ASC
|
||||||
""")
|
""")
|
||||||
|
|||||||
+3
@@ -49,6 +49,9 @@ public class OceanBookingRepository {
|
|||||||
public List<OceanBookingPartyEntity> parties(long id) { return parties.selectList(new LambdaQueryWrapper<OceanBookingPartyEntity>().eq(OceanBookingPartyEntity::getOceanBookingId, id).orderByAsc(OceanBookingPartyEntity::getId)); }
|
public List<OceanBookingPartyEntity> parties(long id) { return parties.selectList(new LambdaQueryWrapper<OceanBookingPartyEntity>().eq(OceanBookingPartyEntity::getOceanBookingId, id).orderByAsc(OceanBookingPartyEntity::getId)); }
|
||||||
public List<OceanBookingContainerRequirementEntity> requirements(long id) { return requirements.selectList(new LambdaQueryWrapper<OceanBookingContainerRequirementEntity>().eq(OceanBookingContainerRequirementEntity::getOceanBookingId, id).orderByAsc(OceanBookingContainerRequirementEntity::getSortNo)); }
|
public List<OceanBookingContainerRequirementEntity> requirements(long id) { return requirements.selectList(new LambdaQueryWrapper<OceanBookingContainerRequirementEntity>().eq(OceanBookingContainerRequirementEntity::getOceanBookingId, id).orderByAsc(OceanBookingContainerRequirementEntity::getSortNo)); }
|
||||||
public List<OceanBookingContainerAllocationEntity> allocations(long id) { return allocations.selectList(new LambdaQueryWrapper<OceanBookingContainerAllocationEntity>().eq(OceanBookingContainerAllocationEntity::getOceanBookingId, id).orderByAsc(OceanBookingContainerAllocationEntity::getAllocatedAt)); }
|
public List<OceanBookingContainerAllocationEntity> allocations(long id) { return allocations.selectList(new LambdaQueryWrapper<OceanBookingContainerAllocationEntity>().eq(OceanBookingContainerAllocationEntity::getOceanBookingId, id).orderByAsc(OceanBookingContainerAllocationEntity::getAllocatedAt)); }
|
||||||
|
public long activeLinkedCount(long oceanBookingId, long requirementId) {
|
||||||
|
return allocations.countActiveLinked(oceanBookingId, requirementId);
|
||||||
|
}
|
||||||
public List<RealContainer> containers(long id) {
|
public List<RealContainer> containers(long id) {
|
||||||
Map<Long, OceanBookingContainerRequirementEntity> requirementById = requirements(id).stream()
|
Map<Long, OceanBookingContainerRequirementEntity> requirementById = requirements(id).stream()
|
||||||
.collect(Collectors.toMap(OceanBookingContainerRequirementEntity::getId, Function.identity()));
|
.collect(Collectors.toMap(OceanBookingContainerRequirementEntity::getId, Function.identity()));
|
||||||
|
|||||||
+1
-2
@@ -57,8 +57,7 @@ public class OceanBookingService {
|
|||||||
|
|
||||||
public PageResult<LinkCandidate> linkCandidates(LinkCandidateQuery query) {
|
public PageResult<LinkCandidate> linkCandidates(LinkCandidateQuery query) {
|
||||||
return repository.linkCandidates(new LinkCandidateQuery(query.warehouseId(),
|
return repository.linkCandidates(new LinkCandidateQuery(query.warehouseId(),
|
||||||
query.channelCode().trim(), query.containerType().trim(), query.capacityCbm(),
|
query.channelCode().trim(), query.containerType().trim(), query.page(), query.size()));
|
||||||
query.maxWeightKg(), query.page(), query.size()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<ChannelOption> channels(long originWarehouseId) {
|
public List<ChannelOption> channels(long originWarehouseId) {
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ public class ContainerRepository {
|
|||||||
public boolean hasPickTask(long id) { return mapper.hasPickTask(id) > 0; }
|
public boolean hasPickTask(long id) { return mapper.hasPickTask(id) > 0; }
|
||||||
public boolean hasEvidence(long id) { return mapper.hasEvidence(id) > 0; }
|
public boolean hasEvidence(long id) { return mapper.hasEvidence(id) > 0; }
|
||||||
public boolean hasDocuments(long id) { return mapper.hasDocuments(id) > 0; }
|
public boolean hasDocuments(long id) { return mapper.hasDocuments(id) > 0; }
|
||||||
public boolean hasMismatchedCustomerItems(long id, long customerId) { return mapper.hasMismatchedCustomerItems(id, customerId) > 0; }
|
|
||||||
public int updateTransport(long id, String containerNo, String voyageNo, int version,
|
public int updateTransport(long id, String containerNo, String voyageNo, int version,
|
||||||
long operatorId, String operatorName) {
|
long operatorId, String operatorName) {
|
||||||
return mapper.updateTransport(id, containerNo, voyageNo, version, operatorId, operatorName);
|
return mapper.updateTransport(id, containerNo, voyageNo, version, operatorId, operatorName);
|
||||||
|
|||||||
@@ -85,17 +85,14 @@ public class ContainerService {
|
|||||||
if (booking == null || !"RELEASED".equals(booking.getStatus())) throw conflict("只能关联已发布订舱");
|
if (booking == null || !"RELEASED".equals(booking.getStatus())) throw conflict("只能关联已发布订舱");
|
||||||
OceanBookingContainerRequirementEntity requirement = oceanBookings.requirement(request.oceanBookingId(), request.requirementId());
|
OceanBookingContainerRequirementEntity requirement = oceanBookings.requirement(request.oceanBookingId(), request.requirementId());
|
||||||
if (requirement == null) throw new BizException(ResultCode.NOT_FOUND, "订舱箱型需求不存在");
|
if (requirement == null) throw new BizException(ResultCode.NOT_FOUND, "订舱箱型需求不存在");
|
||||||
long allocated = oceanBookings.allocations(request.oceanBookingId()).stream().filter(a -> request.requirementId().equals(a.getRequirementId())).count();
|
long allocated = oceanBookings.activeLinkedCount(request.oceanBookingId(), request.requirementId());
|
||||||
if (allocated >= requirement.getQuantity()) throw conflict("该订舱箱型需求已达到数量上限");
|
if (allocated >= requirement.getQuantity()) throw conflict("该订舱箱型需求已达到数量上限");
|
||||||
if (repository.containerNoExists(request.containerNo().trim(), id)) throw conflict("集装箱号已存在");
|
if (repository.containerNoExists(request.containerNo().trim(), id)) throw conflict("集装箱号已存在");
|
||||||
if (!java.util.Objects.equals(container.getWarehouseId(), booking.getWarehouseId())
|
if (!java.util.Objects.equals(container.getWarehouseId(), booking.getWarehouseId())
|
||||||
|| !java.util.Objects.equals(container.getChannelCode(), booking.getChannelCode())
|
|| !java.util.Objects.equals(container.getChannelCode(), booking.getChannelCode())
|
||||||
|| !java.util.Objects.equals(container.getContainerType(), requirement.getContainerType())
|
|| !java.util.Objects.equals(container.getContainerType(), requirement.getContainerType())) {
|
||||||
|| container.getCapacityCbm() == null || requirement.getCapacityCbm() == null
|
throw conflict("模拟柜与真实订舱柜仓库、渠道或柜型不一致");
|
||||||
|| container.getMaxWeightKg() == null || requirement.getMaxWeightKg() == null
|
}
|
||||||
|| container.getCapacityCbm().compareTo(requirement.getCapacityCbm()) != 0
|
|
||||||
|| container.getMaxWeightKg().compareTo(requirement.getMaxWeightKg()) != 0) throw conflict("模拟柜与真实订舱柜仓库、渠道、箱型或容量不一致");
|
|
||||||
if (booking.getCustomerId() != null && repository.hasMismatchedCustomerItems(id, booking.getCustomerId())) throw conflict("模拟柜已排货客户与真实订舱客户范围不一致");
|
|
||||||
if (repository.linkReleasedBooking(id, request.oceanBookingId(), request.requirementId(), request.containerNo().trim(), booking,
|
if (repository.linkReleasedBooking(id, request.oceanBookingId(), request.requirementId(), request.containerNo().trim(), booking,
|
||||||
request.expectedVersion(), operatorId, operatorName) != 1) {
|
request.expectedVersion(), operatorId, operatorName) != 1) {
|
||||||
throw conflict("真实订舱柜已被消费或货柜版本发生变化,请刷新后重试");
|
throw conflict("真实订舱柜已被消费或货柜版本发生变化,请刷新后重试");
|
||||||
|
|||||||
@@ -2,7 +2,12 @@ openapi: 3.0.3
|
|||||||
info: {title: Ocean Booking API, version: 2.0.0}
|
info: {title: Ocean Booking API, version: 2.0.0}
|
||||||
paths:
|
paths:
|
||||||
/api/platform/v1/ocean-bookings:
|
/api/platform/v1/ocean-bookings:
|
||||||
get: {summary: List DRAFT and RELEASED bookings, responses: {'200': {description: Booking page}}}
|
get:
|
||||||
|
summary: List DRAFT and RELEASED bookings
|
||||||
|
description: containerCount is the sum of quantity across all container requirements for the booking. It is zero when no requirement exists and never represents linked or ACTIVE containers.
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Booking page; each row uses the frozen requirement-total containerCount semantic
|
||||||
post:
|
post:
|
||||||
summary: Create an editable DRAFT
|
summary: Create an editable DRAFT
|
||||||
description: Creates only the booking aggregate and container requirements; no containerNo, container row, or allocation is created.
|
description: Creates only the booking aggregate and container requirements; no containerNo, container row, or allocation is created.
|
||||||
@@ -31,14 +36,12 @@ paths:
|
|||||||
/api/platform/v1/ocean-bookings/link-candidates:
|
/api/platform/v1/ocean-bookings/link-candidates:
|
||||||
get:
|
get:
|
||||||
summary: Page released booking requirement candidates for one container
|
summary: Page released booking requirement candidates for one container
|
||||||
description: Requires platform:firstleg:ocean-booking:list. Returns only exact warehouse, channel and fixed container specification matches with remainingQuantity greater than zero. Allocation counts are calculated by one bounded set query; booking detail calls are not used.
|
description: Requires platform:firstleg:ocean-booking:list. Eligibility matches only warehouseId, channelCode and containerType, with remainingQuantity greater than zero. Capacity and max weight are derived display snapshots and never eligibility fields. Allocated quantity counts only ACTIVE containers still pointing to the same booking requirement and is calculated by one bounded set query; booking detail calls are not used.
|
||||||
x-permission: platform:firstleg:ocean-booking:list
|
x-permission: platform:firstleg:ocean-booking:list
|
||||||
parameters:
|
parameters:
|
||||||
- {name: warehouseId, in: query, required: true, schema: {type: integer, format: int64, minimum: 1}}
|
- {name: warehouseId, in: query, required: true, schema: {type: integer, format: int64, minimum: 1}}
|
||||||
- {name: channelCode, in: query, required: true, schema: {type: string, maxLength: 64}}
|
- {name: channelCode, in: query, required: true, schema: {type: string, maxLength: 64}}
|
||||||
- {name: containerType, in: query, required: true, schema: {type: string, maxLength: 32}}
|
- {name: containerType, in: query, required: true, schema: {type: string, maxLength: 32}}
|
||||||
- {name: capacityCbm, in: query, required: true, schema: {type: number, exclusiveMinimum: true, minimum: 0}}
|
|
||||||
- {name: maxWeightKg, in: query, required: true, schema: {type: number, exclusiveMinimum: true, minimum: 0}}
|
|
||||||
- {name: page, in: query, required: false, schema: {type: integer, format: int64, minimum: 1, default: 1}}
|
- {name: page, in: query, required: false, schema: {type: integer, format: int64, minimum: 1, default: 1}}
|
||||||
- {name: size, in: query, required: false, schema: {type: integer, format: int64, minimum: 1, maximum: 200, default: 20}}
|
- {name: size, in: query, required: false, schema: {type: integer, format: int64, minimum: 1, maximum: 200, default: 20}}
|
||||||
responses:
|
responses:
|
||||||
@@ -145,6 +148,14 @@ components:
|
|||||||
code: {type: string, example: '200'}
|
code: {type: string, example: '200'}
|
||||||
message: {type: string, nullable: true}
|
message: {type: string, nullable: true}
|
||||||
data: {$ref: '#/components/schemas/LinkCandidatePage'}
|
data: {$ref: '#/components/schemas/LinkCandidatePage'}
|
||||||
|
OceanBookingListRow:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
containerCount:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
minimum: 0
|
||||||
|
description: Sum of all container requirement quantities; independent of allocation, linking or unlinking.
|
||||||
Detail:
|
Detail:
|
||||||
type: object
|
type: object
|
||||||
required: [oceanBooking, parties, containerRequirements, containers, histories]
|
required: [oceanBooking, parties, containerRequirements, containers, histories]
|
||||||
|
|||||||
+11
-1
@@ -68,11 +68,21 @@ class PlatformOceanBookingControllerContractTest {
|
|||||||
assertThat(yaml)
|
assertThat(yaml)
|
||||||
.contains("/api/platform/v1/ocean-bookings/link-candidates:")
|
.contains("/api/platform/v1/ocean-bookings/link-candidates:")
|
||||||
.contains("x-permission: platform:firstleg:ocean-booking:list")
|
.contains("x-permission: platform:firstleg:ocean-booking:list")
|
||||||
.contains("warehouseId", "channelCode", "containerType", "capacityCbm", "maxWeightKg")
|
.contains("warehouseId", "channelCode", "containerType")
|
||||||
.contains("LinkCandidateResponse", "requiredQuantity", "allocatedQuantity", "remainingQuantity")
|
.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");
|
.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) {
|
private static Method method(String name) {
|
||||||
return Arrays.stream(PlatformOceanBookingController.class.getDeclaredMethods())
|
return Arrays.stream(PlatformOceanBookingController.class.getDeclaredMethods())
|
||||||
.filter(method -> method.getName().equals(name)).findFirst().orElseThrow();
|
.filter(method -> method.getName().equals(name)).findFirst().orElseThrow();
|
||||||
|
|||||||
+25
-5
@@ -1,6 +1,7 @@
|
|||||||
package com.baiyue.firstleg.oceanbooking.repository;
|
package com.baiyue.firstleg.oceanbooking.repository;
|
||||||
|
|
||||||
import com.baiyue.firstleg.oceanbooking.mapper.OceanBookingMapper;
|
import com.baiyue.firstleg.oceanbooking.mapper.OceanBookingMapper;
|
||||||
|
import com.baiyue.firstleg.oceanbooking.mapper.OceanBookingContainerAllocationMapper;
|
||||||
import com.baiyue.firstleg.oceanbooking.dto.OceanBookingDtos.LinkCandidateQuery;
|
import com.baiyue.firstleg.oceanbooking.dto.OceanBookingDtos.LinkCandidateQuery;
|
||||||
import org.apache.ibatis.annotations.Select;
|
import org.apache.ibatis.annotations.Select;
|
||||||
import org.apache.ibatis.annotations.Update;
|
import org.apache.ibatis.annotations.Update;
|
||||||
@@ -21,7 +22,10 @@ class OceanBookingMapperContractTest {
|
|||||||
.contains("booking.status=#{query.status}")
|
.contains("booking.status=#{query.status}")
|
||||||
.contains("booking.booking_no LIKE")
|
.contains("booking.booking_no LIKE")
|
||||||
.contains("party.party_type='SHIPPER'", "party.party_type='CONSIGNEE'")
|
.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");
|
.contains("ORDER BY booking.updated_at DESC,booking.id DESC");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,12 +52,28 @@ class OceanBookingMapperContractTest {
|
|||||||
.contains("booking.warehouse_id=#{query.warehouseId}")
|
.contains("booking.warehouse_id=#{query.warehouseId}")
|
||||||
.contains("booking.channel_code=#{query.channelCode}")
|
.contains("booking.channel_code=#{query.channelCode}")
|
||||||
.contains("requirement.container_type=#{query.containerType}")
|
.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("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) remaining_quantity")
|
||||||
.contains("requirement.quantity>COALESCE(allocated.allocated_quantity,0)")
|
.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());
|
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
|
@Test
|
||||||
void linkCandidatesDefaultsPaginationAndDelegatesOneNormalizedSetQuery() {
|
void linkCandidatesDefaultsPaginationAndDelegatesOneNormalizedSetQuery() {
|
||||||
LinkCandidate candidate = new LinkCandidate(101L, "OB-260818-0001", 201L, "40HC",
|
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));
|
when(repository.linkCandidates(any())).thenReturn(new PageResult<>(List.of(candidate), 1, 20, 1, 1));
|
||||||
|
|
||||||
PageResult<LinkCandidate> result = service.linkCandidates(new LinkCandidateQuery(701L,
|
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()).containsExactly(candidate);
|
||||||
assertThat(result.records().get(0).remainingQuantity()).isEqualTo(2);
|
assertThat(result.records().get(0).remainingQuantity()).isEqualTo(2);
|
||||||
|
|||||||
-9
@@ -53,15 +53,6 @@ class ContainerBookingMapperContractTest {
|
|||||||
.contains("customer_item.account_id = #{query.customerId}");
|
.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
|
@Test
|
||||||
@DisplayName("关联订舱 SQL 的预计到港参数与 Mapper 参数名一致")
|
@DisplayName("关联订舱 SQL 的预计到港参数与 Mapper 参数名一致")
|
||||||
void linkReleasedBookingUsesDeclaredEstimatedArrivalParameter() {
|
void linkReleasedBookingUsesDeclaredEstimatedArrivalParameter() {
|
||||||
|
|||||||
@@ -288,7 +288,7 @@ class ContainerServiceTest {
|
|||||||
void linksReleasedBookingAfterPreloading() {
|
void linksReleasedBookingAfterPreloading() {
|
||||||
ContainerBookingEntity container = booking("EDITING", 2);
|
ContainerBookingEntity container = booking("EDITING", 2);
|
||||||
container.setWarehouseId(101L); container.setChannelCode("SEA-US"); container.setContainerType("40HC");
|
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();
|
OceanBookingEntity booking = new OceanBookingEntity();
|
||||||
booking.setId(21L); booking.setStatus("RELEASED"); booking.setWarehouseId(101L); booking.setChannelCode("SEA-US");
|
booking.setId(21L); booking.setStatus("RELEASED"); booking.setWarehouseId(101L); booking.setChannelCode("SEA-US");
|
||||||
OceanBookingContainerRequirementEntity requirement = new OceanBookingContainerRequirementEntity();
|
OceanBookingContainerRequirementEntity requirement = new OceanBookingContainerRequirementEntity();
|
||||||
@@ -296,7 +296,7 @@ class ContainerServiceTest {
|
|||||||
when(repository.find(11L)).thenReturn(container, container);
|
when(repository.find(11L)).thenReturn(container, container);
|
||||||
when(oceanBookings.find(21L)).thenReturn(booking);
|
when(oceanBookings.find(21L)).thenReturn(booking);
|
||||||
when(oceanBookings.requirement(21L, 31L)).thenReturn(requirement);
|
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"),
|
when(repository.linkReleasedBooking(eq(11L), eq(21L), eq(31L), eq("MSCU7654321"),
|
||||||
same(booking), eq(2), eq(9L), eq("admin"))).thenReturn(1);
|
same(booking), eq(2), eq(9L), eq("admin"))).thenReturn(1);
|
||||||
|
|
||||||
@@ -311,6 +311,24 @@ class ContainerServiceTest {
|
|||||||
verify(oceanBookings).insert(any(OceanBookingContainerAllocationEntity.class));
|
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
|
@Test
|
||||||
@DisplayName("未发布订舱不可关联")
|
@DisplayName("未发布订舱不可关联")
|
||||||
void rejectsDraftBooking() {
|
void rejectsDraftBooking() {
|
||||||
@@ -333,12 +351,10 @@ class ContainerServiceTest {
|
|||||||
booking.setId(21L); booking.setStatus("RELEASED");
|
booking.setId(21L); booking.setStatus("RELEASED");
|
||||||
OceanBookingContainerRequirementEntity requirement = new OceanBookingContainerRequirementEntity();
|
OceanBookingContainerRequirementEntity requirement = new OceanBookingContainerRequirementEntity();
|
||||||
requirement.setId(31L); requirement.setQuantity(1);
|
requirement.setId(31L); requirement.setQuantity(1);
|
||||||
OceanBookingContainerAllocationEntity allocation = new OceanBookingContainerAllocationEntity();
|
|
||||||
allocation.setRequirementId(31L);
|
|
||||||
when(repository.find(11L)).thenReturn(container);
|
when(repository.find(11L)).thenReturn(container);
|
||||||
when(oceanBookings.find(21L)).thenReturn(booking);
|
when(oceanBookings.find(21L)).thenReturn(booking);
|
||||||
when(oceanBookings.requirement(21L, 31L)).thenReturn(requirement);
|
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",
|
assertThatThrownBy(() -> service.linkOceanBooking(11L, 9L, "admin",
|
||||||
new OceanBookingLinkRequest(21L, 31L, "MSCU7654321", 2)))
|
new OceanBookingLinkRequest(21L, 31L, "MSCU7654321", 2)))
|
||||||
@@ -347,6 +363,28 @@ class ContainerServiceTest {
|
|||||||
any(), anyInt(), anyLong(), anyString());
|
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
|
@Test
|
||||||
@DisplayName("完全空的模拟柜直接解绑并删除当前allocation,不依赖MERGED来源柜")
|
@DisplayName("完全空的模拟柜直接解绑并删除当前allocation,不依赖MERGED来源柜")
|
||||||
void unlinksEmptySimulationDirectly() {
|
void unlinksEmptySimulationDirectly() {
|
||||||
|
|||||||
Reference in New Issue
Block a user