优化
This commit is contained in:
@@ -60,4 +60,5 @@ public interface PrintPdfClient {
|
||||
|
||||
record DropshipPalletSkuRow(String inboundCode, String skuCode, String cargoName,
|
||||
int cartonCount, int skuQuantity) {}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
package com.baiyue.firstleg.domain;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
/** 首版固定渠道目录;上传名称只能映射到这些稳定编码。 */
|
||||
public final class PriceBookChannelCatalog {
|
||||
private PriceBookChannelCatalog() {}
|
||||
|
||||
private static final Set<String> SEA_RAIL_CHANNEL_CODES = Set.of(
|
||||
"EXX_RAIL_NY", "NYEXX", "OA_CHICAGO_RAIL", "CPHT");
|
||||
|
||||
public static final List<ChannelDefinition> CHANNELS = List.of(
|
||||
fba("MATSON_CLX_TRUCK", "美森正班海卡", "美森正班海卡", "ZBYHK", "ZBYHWK"),
|
||||
fba("MATSON_MAX_TRUCK", "美森加班海卡", "美森加班海卡", "BYHK", "BYHWK"),
|
||||
@@ -37,6 +41,11 @@ public final class PriceBookChannelCatalog {
|
||||
inboundCodePrefix, overseasInboundCodePrefix);
|
||||
}
|
||||
|
||||
public static boolean isSeaRail(String channelCode) {
|
||||
return channelCode != null
|
||||
&& SEA_RAIL_CHANNEL_CODES.contains(channelCode.trim().toUpperCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
public record ChannelDefinition(
|
||||
String channelCode,
|
||||
String displayName,
|
||||
|
||||
@@ -23,6 +23,7 @@ public interface LoadingListQueryMapper {
|
||||
p.id AS plan_id, p.plan_no, p.container_booking_id, p.plan_status, p.remark,
|
||||
p.created_by, p.created_by_name, p.created_at, p.version,
|
||||
b.container_no, b.cabinet_no, b.warehouse_id, b.warehouse_name,
|
||||
b.channel_code, b.outbound_confirmed_at, b.actual_empty_reported_at,
|
||||
b.vessel_name, b.voyage_no, b.container_type,
|
||||
(SELECT COUNT(*) FROM fl_preload_loading_plan_link preload_link
|
||||
WHERE preload_link.loading_plan_id = p.id) AS preload_link_count,
|
||||
@@ -51,6 +52,7 @@ public interface LoadingListQueryMapper {
|
||||
p.id, p.plan_no, p.container_booking_id, p.plan_status, p.remark,
|
||||
p.created_by, p.created_by_name, p.created_at, p.version,
|
||||
b.container_no, b.cabinet_no, b.warehouse_id, b.warehouse_name,
|
||||
b.channel_code, b.outbound_confirmed_at, b.actual_empty_reported_at,
|
||||
b.vessel_name, b.voyage_no, b.container_type
|
||||
""";
|
||||
|
||||
@@ -394,6 +396,9 @@ public interface LoadingListQueryMapper {
|
||||
private String cabinetNo;
|
||||
private Long warehouseId;
|
||||
private String warehouseName;
|
||||
private String channelCode;
|
||||
private LocalDateTime outboundConfirmedAt;
|
||||
private LocalDateTime actualEmptyReportedAt;
|
||||
private String vesselName;
|
||||
private String voyageNo;
|
||||
private String containerType;
|
||||
|
||||
+5
-19
@@ -15,7 +15,6 @@ import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
@@ -47,7 +46,7 @@ public class OceanBookingPrintService {
|
||||
String fileName = booking.getBookingNo() + "-" + suffix + ".pdf";
|
||||
PrintPdfClient.FreightA4PdfRequest request = new PrintPdfClient.FreightA4PdfRequest(
|
||||
template, A4, fileName, List.of(new PrintPdfClient.FreightDocument(
|
||||
fields(template, detail), List.of(cargo(detail.containerRequirements())))));
|
||||
fields(template, detail), List.of(emptyCargo()))));
|
||||
try {
|
||||
ResponseEntity<byte[]> response = print.generateFreightA4Documents(internalToken, requestId, request);
|
||||
MediaType contentType = response.getHeaders().getContentType();
|
||||
@@ -78,12 +77,12 @@ public class OceanBookingPrintService {
|
||||
fields.put("placeOfDelivery", text(booking.getPlaceOfDelivery()));
|
||||
fields.put("serviceTerm", text(booking.getServiceTerm()));
|
||||
fields.put("freightTerm", text(booking.getFreightTerm()));
|
||||
fields.put("totalPackagesInWords", Integer.toString(totalContainers(detail.containerRequirements())));
|
||||
fields.put("totalPackagesInWords", "");
|
||||
if (SHIPPING_INSTRUCTION.equals(template)) {
|
||||
fields.put("toCompany", text(booking.getBookingAgentName()));
|
||||
fields.put("jobNumber", text(booking.getBookingNo()));
|
||||
fields.put("etdDate", date(booking.getEstimatedSailingAt()));
|
||||
fields.put("cargoQuantity", Integer.toString(totalContainers(detail.containerRequirements())));
|
||||
fields.put("cargoQuantity", "");
|
||||
fields.put("carrier", text(booking.getShippingCompanyName()));
|
||||
fields.put("bookingAgent", text(booking.getBookingAgentName()));
|
||||
fields.put("remark", text(booking.getRemark()));
|
||||
@@ -99,18 +98,8 @@ public class OceanBookingPrintService {
|
||||
return fields;
|
||||
}
|
||||
|
||||
private static PrintPdfClient.CargoRow cargo(List<OceanBookingContainerRequirementEntity> requirements) {
|
||||
BigDecimal gross = requirements.stream().map(value -> value.getMaxWeightKg()
|
||||
.multiply(BigDecimal.valueOf(value.getQuantity()))).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
BigDecimal volume = requirements.stream().map(value -> value.getCapacityCbm()
|
||||
.multiply(BigDecimal.valueOf(value.getQuantity()))).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
return new PrintPdfClient.CargoRow("N/M", Integer.toString(totalContainers(requirements)),
|
||||
"CONTAINERS", requirementSummary(requirements), decimal(gross, 3) + " KGS",
|
||||
decimal(volume, 4) + " CBM");
|
||||
}
|
||||
|
||||
private static int totalContainers(List<OceanBookingContainerRequirementEntity> values) {
|
||||
return values.stream().mapToInt(OceanBookingContainerRequirementEntity::getQuantity).sum();
|
||||
private static PrintPdfClient.CargoRow emptyCargo() {
|
||||
return new PrintPdfClient.CargoRow("—", "—", "—", "—", "—", "—");
|
||||
}
|
||||
|
||||
private static String requirementSummary(List<OceanBookingContainerRequirementEntity> values) {
|
||||
@@ -128,9 +117,6 @@ public class OceanBookingPrintService {
|
||||
private static String date(java.time.LocalDateTime value) {
|
||||
return value == null ? "" : value.format(DateTimeFormatter.ISO_LOCAL_DATE);
|
||||
}
|
||||
private static String decimal(BigDecimal value, int scale) {
|
||||
return value.setScale(scale, RoundingMode.HALF_UP).toPlainString();
|
||||
}
|
||||
private static String first(String first, String fallback) { return !text(first).isEmpty() ? first.trim() : text(fallback); }
|
||||
private static String join(String first, String second) { return List.of(text(first), text(second)).stream().filter(value -> !value.isEmpty()).collect(Collectors.joining(" / ")); }
|
||||
private static String text(String value) { return value == null ? "" : value.trim(); }
|
||||
|
||||
@@ -261,7 +261,7 @@ public class InboundMeasurementService {
|
||||
ConfirmedWeightTotals confirmedTotals = confirmedWeightTotals(weights);
|
||||
if (confirmedTotals.palletCount() == 0) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED,
|
||||
"请至少上传并确认一张称重图片后再提交测量审核");
|
||||
"即使只有一箱,也必须上传并确认至少一张称重图片后再提交测量审核");
|
||||
}
|
||||
|
||||
requireConfirmedPackageTotal(inbound, items);
|
||||
|
||||
@@ -26,6 +26,7 @@ import com.baiyue.firstleg.repository.LoadingListRepository;
|
||||
import com.baiyue.firstleg.repository.LoadingPickRepository;
|
||||
import com.baiyue.firstleg.repository.ContainerRepository;
|
||||
import com.baiyue.firstleg.domain.Iso6346ContainerNumber;
|
||||
import com.baiyue.firstleg.domain.PriceBookChannelCatalog;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
@@ -138,7 +139,9 @@ public class LoadingListService {
|
||||
}
|
||||
public BusinessDocument containerLogisticsDocument(long bookingId,String type){ requireLogisticsType(type); requireBookingPlan(bookingId); return document(repository.document(bookingId,type)); }
|
||||
public BusinessDocument uploadContainerLogisticsDocument(long bookingId,String type,long operatorId,String operatorName,String key,int expectedVersion,String reason,MultipartFile file){
|
||||
requireLogisticsType(type); String businessType=fileType(type); return uploadDocument(requireBookingPlan(bookingId),type,businessType,bookingId,Long.toString(bookingId),operatorId,operatorName,key,expectedVersion,reason,file);
|
||||
requireLogisticsType(type); String businessType=fileType(type); AggregateRow plan=requireBookingPlan(bookingId);
|
||||
requireDeliveryOrderUploadAllowed(plan);
|
||||
return uploadDocument(plan,type,businessType,bookingId,Long.toString(bookingId),operatorId,operatorName,key,expectedVersion,reason,file);
|
||||
}
|
||||
public DocumentDownload downloadContainerDocument(long bookingId,String type,long fileId){
|
||||
requireLogisticsType(type); String businessType=fileType(type); requireBookingPlan(bookingId); ContainerDocumentEntity d=requireDocumentEntity(bookingId,type); if(!Long.valueOf(fileId).equals(d.getFileId())) throw notFound("货柜资料不存在");
|
||||
@@ -190,6 +193,17 @@ public class LoadingListService {
|
||||
public record DocumentDownload(String fileName,MediaType contentType,byte[] content) {}
|
||||
private static void requireLogisticsType(String type){ if(!DELIVERY_ORDER.equals(type)) throw badRequest("documentType只能是DELIVERY_ORDER"); }
|
||||
private static String fileType(String type){ if (DELIVERY_ORDER.equals(type)) return DO_FILE_TYPE; throw badRequest("documentType只能是DELIVERY_ORDER"); }
|
||||
private static void requireDeliveryOrderUploadAllowed(AggregateRow plan) {
|
||||
if (!"COMPLETED".equals(plan.getPlanStatus()) || plan.getOutboundConfirmedAt() == null) {
|
||||
throw conflict("货柜进入海外履约后才可上传DO资料");
|
||||
}
|
||||
if (plan.getActualEmptyReportedAt() != null) {
|
||||
throw conflict("货柜已还柜,不可再上传DO资料");
|
||||
}
|
||||
if (PriceBookChannelCatalog.isSeaRail(plan.getChannelCode())) {
|
||||
throw conflict("海铁渠道不适用DO资料");
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@BusinessAudit(event = "CONTAINER_CLEARANCE_DOCUMENT_ATTACH", resourceType = "CONTAINER",
|
||||
|
||||
@@ -92,14 +92,18 @@ public class MeasurementReviewService {
|
||||
"人工确认箱数合计与入库单总箱数不一致,不能审核通过");
|
||||
}
|
||||
var weights = measurements.listWeights(tenantId, inboundId);
|
||||
if (weights.isEmpty() || weights.stream().anyMatch(weight ->
|
||||
if (weights.isEmpty()) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED,
|
||||
"该票测量已提交但没有称重图片;请先退回测量,上传并确认至少一张称重图片后重新提交(即使只有一箱也必须称重)");
|
||||
}
|
||||
if (weights.stream().anyMatch(weight ->
|
||||
!"CONFIRMED".equals(weight.getRecognitionStatus())
|
||||
|| weight.getGrossWeightKg() == null
|
||||
|| weight.getGrossWeightKg().signum() <= 0
|
||||
|| weight.getNetWeightKg() == null
|
||||
|| weight.getNetWeightKg().signum() <= 0)) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED,
|
||||
"所有称重结果必须已确认且包含大于零的毛重和货物净重,才能审核通过");
|
||||
"该票存在未确认或重量无效的称重图片;请先在审核弹框逐张修正并确认后再审核");
|
||||
}
|
||||
BigDecimal grossWeight = weights.stream()
|
||||
.map(com.baiyue.firstleg.entity.InboundWeightResultEntity::getGrossWeightKg)
|
||||
|
||||
@@ -1224,7 +1224,7 @@ paths:
|
||||
/api/platform/v1/inbounds/{inboundId}/measurements/approve:
|
||||
post:
|
||||
summary: 审核通过测量
|
||||
description: 需要 firstleg:inbound:measurement-review 权限,且 Order 权威订单必须已审核通过。订单待审核期间可保留并提交到仓测量数据,但不能审核测量。标准头程审核通过后进入 WAIT_SPLIT;一件代发进入 WAIT_PALLETIZE,完成逐箱清点和封托后再进入 WAIT_SPLIT。审核时按该次测量数据尽力生成 CALCULATED 初算费用;价格未覆盖或初算异常只返回非阻断提示,不影响审核结果和后续履约状态。
|
||||
description: 需要 firstleg:inbound:measurement-review 权限,且 Order 权威订单必须已审核通过。订单待审核期间可保留并提交到仓测量数据,但不能审核测量。审核通过前必须存在至少一张已确认的称重图片;历史异常数据若已提交但没有称重图片,必须先退回测量后补录。标准头程审核通过后进入 WAIT_SPLIT;一件代发进入 WAIT_PALLETIZE,完成逐箱清点和封托后再进入 WAIT_SPLIT。审核时按该次测量数据尽力生成 CALCULATED 初算费用;价格未覆盖或初算异常只返回非阻断提示,不影响审核结果和后续履约状态。
|
||||
parameters:
|
||||
- {name: inboundId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
- {name: tenantId, in: query, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
@@ -1245,7 +1245,7 @@ paths:
|
||||
/api/platform/v1/inbounds/{inboundId}/measurements/submit:
|
||||
post:
|
||||
summary: 提交测量完成
|
||||
description: 提交只校验并复用称重图片确认阶段已经 OCCUPIED 且绑定完整的权威整票库位块,然后进入客服待审核;不会首次分配、重建或更换已显示槽位。Order 仍待审核时允许提交并冻结到仓测量证据,但不得继续审核测量或进入计费、分柜和装柜。
|
||||
description: 即使只有一箱,也必须先上传并确认至少一张称重图片。提交只校验并复用称重图片确认阶段已经 OCCUPIED 且绑定完整的权威整票库位块,然后进入客服待审核;不会首次分配、重建或更换已显示槽位。Order 仍待审核时允许提交并冻结到仓测量证据,但不得继续审核测量或进入计费、分柜和装柜。
|
||||
parameters:
|
||||
- {name: tenantId, in: query, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
- {name: inboundId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
|
||||
@@ -89,7 +89,7 @@ paths:
|
||||
/api/platform/v1/ocean-bookings/{oceanBookingId}/prints/shipping-instruction:
|
||||
get:
|
||||
summary: Render the current saved booking as an export shipping instruction PDF
|
||||
description: Firstleg maps the authoritative saved aggregate (DRAFT or RELEASED, with missing fields displayed as —) to SEA_FREIGHT_SHIPPING_INSTRUCTION_A4_V1 and synchronously proxies the generated PDF.
|
||||
description: Firstleg maps the authoritative saved aggregate (DRAFT or RELEASED, with missing fields displayed as —) to SEA_FREIGHT_SHIPPING_INSTRUCTION_A4_V1 and synchronously proxies the generated PDF. Container requirements are not treated as actual cargo quantity, weight, volume, marks, packaging, or goods descriptions.
|
||||
x-permission: platform:firstleg:ocean-booking:list
|
||||
parameters: [{$ref: '#/components/parameters/OceanBookingId'}]
|
||||
responses:
|
||||
@@ -97,7 +97,7 @@ paths:
|
||||
/api/platform/v1/ocean-bookings/{oceanBookingId}/prints/bill-of-lading-confirmation:
|
||||
get:
|
||||
summary: Render the current saved booking as a bill-of-lading confirmation PDF
|
||||
description: Firstleg maps the authoritative saved aggregate (DRAFT or RELEASED, with missing fields displayed as —) to BILL_OF_LADING_CONFIRMATION_A4_V1 and synchronously proxies the generated PDF.
|
||||
description: Firstleg maps the authoritative saved aggregate (DRAFT or RELEASED, with missing fields displayed as —) to BILL_OF_LADING_CONFIRMATION_A4_V1 and synchronously proxies the generated PDF. Container requirements remain a container summary and are not treated as actual cargo facts.
|
||||
x-permission: platform:firstleg:ocean-booking:list
|
||||
parameters: [{$ref: '#/components/parameters/OceanBookingId'}]
|
||||
responses:
|
||||
|
||||
@@ -39,10 +39,10 @@ paths:
|
||||
/api/platform/v1/containers/{id}/documents/{documentType}/upload:
|
||||
post:
|
||||
operationId: uploadContainerDocument
|
||||
description: '权限:platform:firstleg:terminal-drayage:document:edit;仅国外工作台DO,无完成动作'
|
||||
description: '权限:platform:firstleg:terminal-drayage:document:edit;仅已进入海外履约且未还柜的非海铁渠道可上传DO,无完成动作'
|
||||
parameters: [{$ref: '#/components/parameters/ContainerId'}, {$ref: '#/components/parameters/DocumentType'}, {name: Idempotency-Key, in: header, required: true, schema: {type: string}}]
|
||||
requestBody: {required: true, content: {multipart/form-data: {schema: {type: object, required: [file, expectedVersion], properties: {file: {type: string, format: binary}, expectedVersion: {type: integer, minimum: 0}, operationReason: {type: string, maxLength: 500}}}}}}
|
||||
responses: {'200': {description: OK, content: {application/json: {schema: {$ref: '#/components/schemas/DocumentEnvelope'}}}}}
|
||||
responses: {'200': {description: OK, content: {application/json: {schema: {$ref: '#/components/schemas/DocumentEnvelope'}}}}, '409': {description: '尚未进入海外履约、海铁渠道不适用DO或货柜已还柜'}}
|
||||
/api/platform/v1/containers/{id}/documents/{documentType}/{fileId}/download:
|
||||
get:
|
||||
operationId: downloadContainerDocument
|
||||
|
||||
+5
-1
@@ -27,7 +27,11 @@ class InboundMeasurementEvidenceOpenApiContractTest {
|
||||
Map<String, Object> submitOperation = stringMap(stringMap(paths.get(
|
||||
"/api/platform/v1/inbounds/{inboundId}/measurements/submit")).get("post"));
|
||||
assertThat(submitOperation.get("description").toString())
|
||||
.contains("只校验并复用", "不会首次分配");
|
||||
.contains("即使只有一箱", "上传并确认至少一张称重图片", "只校验并复用", "不会首次分配");
|
||||
Map<String, Object> approveOperation = stringMap(stringMap(paths.get(
|
||||
"/api/platform/v1/inbounds/{inboundId}/measurements/approve")).get("post"));
|
||||
assertThat(approveOperation.get("description").toString())
|
||||
.contains("已确认的称重图片", "历史异常数据", "先退回测量");
|
||||
|
||||
Map<String, Object> schemas = stringMap(stringMap(document.get("components")).get("schemas"));
|
||||
Map<String, Object> detailProperties = properties(schemas, "InboundMeasurementDetail");
|
||||
|
||||
+14
-5
@@ -31,7 +31,7 @@ class OceanBookingPrintServiceTest {
|
||||
|
||||
@BeforeEach void setUp() { MockitoAnnotations.openMocks(this); ReflectionTestUtils.setField(service, "internalToken", "token"); }
|
||||
|
||||
@Test void shippingInstructionUsesOnlyAuthoritativeReleasedAggregate() {
|
||||
@Test void shippingInstructionDoesNotTreatContainerCapacityAsActualCargo() {
|
||||
when(bookings.detail(10L)).thenReturn(detail("RELEASED"));
|
||||
HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_PDF);
|
||||
headers.set("X-Print-Page-Count", "1");
|
||||
@@ -49,12 +49,17 @@ class OceanBookingPrintServiceTest {
|
||||
assertThat(document.fields()).containsEntry("jobNumber", "OB-1")
|
||||
.containsEntry("shipper", "Shipper\nAddress")
|
||||
.containsEntry("oceanVesselVoyage", "Vessel / V001")
|
||||
.containsEntry("cargoQuantity", "—")
|
||||
.containsEntry("totalPackagesInWords", "—")
|
||||
.containsKeys("toCompany", "consignee", "notifyParty", "cargoQuantity",
|
||||
"portOfLoading", "portOfDischarge", "placeOfDelivery", "totalPackagesInWords");
|
||||
assertThat(document.cargoRows()).singleElement().satisfies(row -> {
|
||||
assertThat(row.quantity()).isEqualTo("2");
|
||||
assertThat(row.descriptionOfGoods()).isEqualTo("40HQ DRY x 2");
|
||||
assertThat(row.grossWeight()).isEqualTo("56000.000 KGS");
|
||||
assertThat(row.marksAndNos()).isEqualTo("—");
|
||||
assertThat(row.quantity()).isEqualTo("—");
|
||||
assertThat(row.packaging()).isEqualTo("—");
|
||||
assertThat(row.descriptionOfGoods()).isEqualTo("—");
|
||||
assertThat(row.grossWeight()).isEqualTo("—");
|
||||
assertThat(row.measurement()).isEqualTo("—");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -85,7 +90,11 @@ class OceanBookingPrintServiceTest {
|
||||
.containsEntry("serviceTerm", "—")
|
||||
.containsEntry("freightTerm", "—");
|
||||
}
|
||||
assertThat(request.getValue().documents().get(0).fields()).containsEntry("bookingNumber", "—");
|
||||
assertThat(request.getValue().documents().get(0).fields())
|
||||
.containsEntry("bookingNumber", "—")
|
||||
.containsEntry("containerSpec", "40HQ DRY x 2")
|
||||
.containsEntry("containerSummary", "40HQ DRY x 2")
|
||||
.containsEntry("totalPackagesInWords", "—");
|
||||
}
|
||||
|
||||
private static Detail detail(String status) {
|
||||
|
||||
+4
-2
@@ -1188,7 +1188,8 @@ class InboundMeasurementServiceTest {
|
||||
assertThatThrownBy(() -> service.submit(1L, 10L, 9L,
|
||||
new InboundMeasurementRequests.SubmitRequest(30L, 0)))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("至少上传并确认一张称重图片");
|
||||
.hasMessageContaining("即使只有一箱")
|
||||
.hasMessageContaining("上传并确认至少一张称重图片");
|
||||
|
||||
verify(repository, never()).updateMeasurement(any());
|
||||
verify(repository, never()).updateInbound(any());
|
||||
@@ -1212,7 +1213,8 @@ class InboundMeasurementServiceTest {
|
||||
assertThatThrownBy(() -> service.submit(1L, 10L, 9L,
|
||||
new InboundMeasurementRequests.SubmitRequest(30L, 0)))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("至少上传并确认一张称重图片");
|
||||
.hasMessageContaining("即使只有一箱")
|
||||
.hasMessageContaining("上传并确认至少一张称重图片");
|
||||
|
||||
verify(repository, never()).updateMeasurement(any());
|
||||
verify(repository, never()).updateInbound(any());
|
||||
|
||||
@@ -681,6 +681,48 @@ class LoadingListServiceTest {
|
||||
eq("COMPLETED"), eq("COMPLETED"), anyString(), anyString(), eq(9L), eq("operator"), isNull());
|
||||
}
|
||||
|
||||
@Test
|
||||
void seaRailContainerCannotUploadDeliveryOrder() {
|
||||
AggregateRow plan = row("COMPLETED", 4);
|
||||
plan.setChannelCode("OA_CHICAGO_RAIL");
|
||||
var file = new MockMultipartFile("file", "do.pdf", "application/pdf", "do".getBytes());
|
||||
when(repository.aggregateByBooking(30L)).thenReturn(plan);
|
||||
|
||||
assertThatThrownBy(() -> service.uploadContainerLogisticsDocument(30L,
|
||||
LoadingListService.DELIVERY_ORDER, 9L, "operator", "do-rail", 0, null, file))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("海铁渠道不适用DO资料");
|
||||
verifyNoInteractions(documentFiles);
|
||||
}
|
||||
|
||||
@Test
|
||||
void domesticStageContainerCannotUploadDeliveryOrder() {
|
||||
AggregateRow plan = row("COMPLETED", 4);
|
||||
plan.setOutboundConfirmedAt(null);
|
||||
var file = new MockMultipartFile("file", "do.pdf", "application/pdf", "do".getBytes());
|
||||
when(repository.aggregateByBooking(30L)).thenReturn(plan);
|
||||
|
||||
assertThatThrownBy(() -> service.uploadContainerLogisticsDocument(30L,
|
||||
LoadingListService.DELIVERY_ORDER, 9L, "operator", "do-domestic", 0, null, file))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("进入海外履约后");
|
||||
verifyNoInteractions(documentFiles);
|
||||
}
|
||||
|
||||
@Test
|
||||
void returnedContainerCannotUploadDeliveryOrder() {
|
||||
AggregateRow plan = row("COMPLETED", 4);
|
||||
plan.setActualEmptyReportedAt(java.time.LocalDateTime.now());
|
||||
var file = new MockMultipartFile("file", "do.pdf", "application/pdf", "do".getBytes());
|
||||
when(repository.aggregateByBooking(30L)).thenReturn(plan);
|
||||
|
||||
assertThatThrownBy(() -> service.uploadContainerLogisticsDocument(30L,
|
||||
LoadingListService.DELIVERY_ORDER, 9L, "operator", "do-returned", 0, null, file))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("已还柜");
|
||||
verifyNoInteractions(documentFiles);
|
||||
}
|
||||
|
||||
@Test
|
||||
void staleDeliveryOrderVersionReturnsConflictBeforeUploadingAnotherFile() {
|
||||
AggregateRow plan = row("COMPLETED", 4);
|
||||
@@ -918,6 +960,8 @@ class LoadingListServiceTest {
|
||||
row.setContainerBookingId(30L);
|
||||
row.setPlanStatus(status);
|
||||
row.setWarehouseId(100L);
|
||||
row.setChannelCode("OA_LA_TRUCK");
|
||||
row.setOutboundConfirmedAt(java.time.LocalDateTime.now());
|
||||
row.setVersion(version);
|
||||
return row;
|
||||
}
|
||||
|
||||
@@ -135,6 +135,41 @@ class MeasurementReviewServiceTest {
|
||||
verify(fixture.repository, never()).transitionReview(anyLong(), anyLong(), anyLong(), anyInt(), anyBoolean(), anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("历史异常测量已提交但没有称重图片时提示退回补录")
|
||||
void shouldExplainHowToRecoverSubmittedMeasurementWithoutWeightImage() {
|
||||
Fixture fixture = fixture(false);
|
||||
when(measurements.listWeights(10001L, 10L)).thenReturn(List.of());
|
||||
|
||||
assertThatThrownBy(() -> fixture.service.approve(10001L, 9L, "审核员", 10L,
|
||||
"approve-without-weight", new MeasurementReviewRequest(20L, 3, null)))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("已提交但没有称重图片")
|
||||
.hasMessageContaining("先退回测量")
|
||||
.hasMessageContaining("即使只有一箱也必须称重");
|
||||
|
||||
verify(fixture.repository, never()).transitionReview(
|
||||
anyLong(), anyLong(), anyLong(), anyInt(), anyBoolean(), anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("已上传但未确认的称重图片提示在审核弹框修正")
|
||||
void shouldExplainHowToConfirmInvalidWeightImageDuringReview() {
|
||||
Fixture fixture = fixture(false);
|
||||
InboundWeightResultEntity pending = confirmedWeight("357.500", "349.500");
|
||||
pending.setRecognitionStatus("PENDING_CONFIRM");
|
||||
when(measurements.listWeights(10001L, 10L)).thenReturn(List.of(pending));
|
||||
|
||||
assertThatThrownBy(() -> fixture.service.approve(10001L, 9L, "审核员", 10L,
|
||||
"approve-pending-weight", new MeasurementReviewRequest(20L, 3, null)))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("未确认或重量无效")
|
||||
.hasMessageContaining("在审核弹框逐张修正并确认");
|
||||
|
||||
verify(fixture.repository, never()).transitionReview(
|
||||
anyLong(), anyLong(), anyLong(), anyInt(), anyBoolean(), anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("多SKU测量行的箱数、重量、体积和体积重守恒后才能进入WAIT_SPLIT")
|
||||
void shouldApproveMultipleCargoLinesOnlyWhenAllFrozenTotalsMatch() {
|
||||
|
||||
Reference in New Issue
Block a user