This commit is contained in:
2026-09-11 16:17:16 +08:00
parent 2d928d667b
commit 51ce609ab1
57 changed files with 942 additions and 139 deletions
@@ -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");
@@ -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) {
@@ -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() {