feat(firstleg): expose authoritative measurement identifiers

This commit is contained in:
2026-09-08 17:24:29 +08:00
parent e597bebafd
commit ba81dece5e
9 changed files with 190 additions and 46 deletions
@@ -32,8 +32,10 @@ class InboundMeasurementEvidenceOpenApiContractTest {
Map<String, Object> schemas = stringMap(stringMap(document.get("components")).get("schemas"));
Map<String, Object> detailProperties = properties(schemas, "InboundMeasurementDetail");
assertThat(detailProperties).containsKeys(
"actualGrossWeightKg", "actualNetWeightKg", "latestDeclaration", "items", "weightResults",
"inboundCode", "actualGrossWeightKg", "actualNetWeightKg", "latestDeclaration", "items", "weightResults",
"palletLocationGuide");
assertThat(stringMap(detailProperties.get("inboundCode")).get("description").toString())
.contains("唯一入仓码", "UI业务主展示字段");
assertThat(stringMap(detailProperties.get("latestDeclaration")))
.containsEntry("$ref", "#/components/schemas/InboundMeasurementLatestDeclaration");
assertThat(properties(schemas, "InboundMeasurementLatestDeclaration"))
@@ -49,8 +51,11 @@ class InboundMeasurementEvidenceOpenApiContractTest {
Map<String, Object> draftProperties = properties(schemas, "InboundMeasurementSaveDraftRequest");
assertThat(draftProperties).containsKeys("measurementId", "version", "actualGoodsName", "items")
.doesNotContainKeys("originalDocumentNo", "originalGrossWeightKg", "originalVolumeCbm");
assertThat(paths).containsKey(
"/api/platform/v1/first-leg/inbounds/{inboundId}/measurement/draft");
String draftPath = "/api/platform/v1/first-leg/inbounds/{inboundId}/measurement/draft";
assertThat(paths).containsKey(draftPath);
Map<String, Object> draftOperation = stringMap(stringMap(paths.get(draftPath)).get("put"));
assertThat(draftOperation.get("description").toString())
.contains("MEASURE_PENDING", "冻结", "package-count");
assertThat(stringMap(stringMap(draftProperties.get("items")).get("items")))
.containsEntry("$ref", "#/components/schemas/InboundMeasurementItemRequest");
assertThat(properties(schemas, "InboundMeasurementItemRequest")).containsKeys(
@@ -58,7 +63,14 @@ class InboundMeasurementEvidenceOpenApiContractTest {
"widthCm", "heightCm", "intelligentImageFileId", "remark");
Map<String, Object> itemProperties = properties(schemas, "InboundMeasurementItem");
assertThat(itemProperties).containsKeys("id", "intelligentImageFileId");
assertThat(itemProperties).containsKeys(
"id", "inboundCode", "inboundDetailId", "intelligentImageFileId", "fbaShipmentNo", "scannedShipmentId");
assertThat(stringMap(itemProperties.get("inboundCode")).get("description").toString())
.contains("唯一权威入仓码", "根对象inboundCode一致");
assertThat(stringMap(itemProperties.get("fbaShipmentNo")).get("description").toString())
.contains("Order货物明细", "inboundDetailId", "匹配失败为空");
assertThat(stringMap(itemProperties.get("scannedShipmentId")).get("description").toString())
.contains("设备实扫", "不覆盖或回退fbaShipmentNo");
Map<String, Object> weightProperties = properties(schemas, "InboundWeightResult");
assertThat(weightProperties).containsKeys(
"id", "fileId", "recognitionStatus", "grossWeightKg", "netWeightKg", "confirmedAt",
@@ -75,8 +75,8 @@ class InboundMeasurementServiceTest {
"REF-50", new BigDecimal("2000"), new BigDecimal("20"), "123123")));
when(orderCargoClient.listForCartonLabels("internal-token", 1L, 80L)).thenReturn(
ApiResponse.success(List.of(new OrderCargoClient.CargoLabelBinding(
80001L, 80L, 1, "123123", 3, new BigDecimal("40"),
new BigDecimal("30"), new BigDecimal("20")))));
80001L, 80L, 1, "123123", null, 3, new BigDecimal("40"),
new BigDecimal("30"), new BigDecimal("20"), "FBA-DECLARED-001"))));
}
@Test
@@ -196,7 +196,9 @@ class InboundMeasurementServiceTest {
item.setId(40L);
item.setTenantId(1L);
item.setMeasurementId(30L);
item.setInboundDetailId(80001L);
item.setSourceImageFileId(140L);
item.setScannedShipmentId("FBA-SCANNED-001");
item.setOuterLengthCm(new BigDecimal("64.000"));
item.setOuterWidthCm(new BigDecimal("54.000"));
item.setOuterHeightCm(new BigDecimal("54.000"));
@@ -217,11 +219,15 @@ class InboundMeasurementServiceTest {
InboundMeasurementResponses.Detail result = service.detail(1L, 10L);
assertThat(result.inboundCode()).isEqualTo("RK-001");
assertThat(result.actualGrossWeightKg()).isEqualByComparingTo("30.000");
assertThat(result.actualNetWeightKg()).isEqualByComparingTo("11.500");
assertThat(result.items()).singleElement().satisfies(dimension -> {
assertThat(dimension.id()).isEqualTo(40L);
assertThat(dimension.inboundCode()).isEqualTo("RK-001");
assertThat(dimension.intelligentImageFileId()).isEqualTo(140L);
assertThat(dimension.fbaShipmentNo()).isEqualTo("FBA-DECLARED-001");
assertThat(dimension.scannedShipmentId()).isEqualTo("FBA-SCANNED-001");
assertThat(dimension.outerLengthCm()).isNull();
assertThat(dimension.outerWidthCm()).isNull();
assertThat(dimension.outerHeightCm()).isNull();
@@ -246,6 +252,63 @@ class InboundMeasurementServiceTest {
});
}
@Test
void detailMapsMultipleDeclaredShipmentIdsByInboundDetailWithoutFallingBackToScannedValue() {
InboundOrderEntity inbound = inbound("MEASURE_PENDING");
InboundMeasurementEntity measurement = new InboundMeasurementEntity();
measurement.setId(30L);
InboundMeasurementItemEntity first = new InboundMeasurementItemEntity();
first.setId(41L);
first.setInboundDetailId(80001L);
first.setScannedShipmentId("SCAN-DIFFERENT-001");
InboundMeasurementItemEntity second = new InboundMeasurementItemEntity();
second.setId(42L);
second.setInboundDetailId(80002L);
InboundMeasurementItemEntity unmatched = new InboundMeasurementItemEntity();
unmatched.setId(43L);
unmatched.setInboundDetailId(89999L);
unmatched.setScannedShipmentId("SCAN-ONLY-999");
when(repository.findInbound(1L, 10L)).thenReturn(inbound);
when(repository.findLatestMeasurement(1L, 10L)).thenReturn(measurement);
when(repository.listItems(1L, 30L)).thenReturn(List.of(first, second, unmatched));
when(repository.listWeights(1L, 10L)).thenReturn(List.of());
when(orderCargoClient.listForCartonLabels("internal-token", 1L, 80L)).thenReturn(
ApiResponse.success(List.of(
new OrderCargoClient.CargoLabelBinding(80001L, 80L, 1, "货物1", null,
1, null, null, null, "FBA-DECLARED-001"),
new OrderCargoClient.CargoLabelBinding(80002L, 80L, 2, "货物2", null,
1, null, null, null, "FBA-DECLARED-002"))));
InboundMeasurementResponses.Detail result = service.detail(1L, 10L);
assertThat(result.inboundCode()).isEqualTo("RK-001");
assertThat(result.items())
.extracting(InboundMeasurementResponses.Item::inboundDetailId,
InboundMeasurementResponses.Item::inboundCode,
InboundMeasurementResponses.Item::fbaShipmentNo,
InboundMeasurementResponses.Item::scannedShipmentId)
.containsExactly(
tuple(80001L, "RK-001", "FBA-DECLARED-001", "SCAN-DIFFERENT-001"),
tuple(80002L, "RK-001", "FBA-DECLARED-002", null),
tuple(89999L, "RK-001", null, "SCAN-ONLY-999"));
verify(orderCargoClient, times(1)).listForCartonLabels("internal-token", 1L, 80L);
}
@Test
void detailRejectsCargoBatchWhoseOrderOwnershipDoesNotMatchInbound() {
InboundOrderEntity inbound = inbound("MEASURE_PENDING");
when(repository.findInbound(1L, 10L)).thenReturn(inbound);
when(repository.listWeights(1L, 10L)).thenReturn(List.of());
when(orderCargoClient.listForCartonLabels("internal-token", 1L, 80L)).thenReturn(
ApiResponse.success(List.of(new OrderCargoClient.CargoLabelBinding(
80001L, 81L, 1, "其他订单货物", null, 1, null, null, null,
"FBA-OTHER-001"))));
assertThatThrownBy(() -> service.detail(1L, 10L))
.isInstanceOf(BizException.class)
.hasMessageContaining("货物明细归属不一致");
}
@Test
void detailReturnsWarehouseOuterDimensionsOnlyAfterManualOverrideWasSaved() {
InboundOrderEntity inbound = inbound("MEASURING");
@@ -333,8 +396,8 @@ class InboundMeasurementServiceTest {
"NEW-REF", new BigDecimal("40.000"), new BigDecimal("0.8000"), "最新申报品名")));
when(orderCargoClient.listForCartonLabels("internal-token", 1L, 80L)).thenReturn(
ApiResponse.success(List.of(new OrderCargoClient.CargoLabelBinding(
80001L, 80L, 1, "最新申报品名", 4, new BigDecimal("50"),
new BigDecimal("40"), new BigDecimal("30")))));
80001L, 80L, 1, "最新申报品名", null, 4, new BigDecimal("50"),
new BigDecimal("40"), new BigDecimal("30"), "FBA-LATEST-001"))));
InboundMeasurementResponses.Detail result = service.detail(1L, 10L);
@@ -356,6 +419,7 @@ class InboundMeasurementServiceTest {
assertThat(result.items()).singleElement().satisfies(measured -> {
assertThat(measured.id()).isEqualTo(40L);
assertThat(measured.boxCode()).isEqualTo("RK-001#2-1");
assertThat(measured.fbaShipmentNo()).isEqualTo("FBA-LATEST-001");
assertThat(measured.packageCount()).isEqualTo(2);
assertThat(measured.lengthCm()).isEqualByComparingTo("45");
});
@@ -1116,6 +1180,21 @@ class InboundMeasurementServiceTest {
verify(orderCargoClient).detail("internal-token", 1L, 80L, 80001L);
}
@Test
void savingDraftCannotReopenSubmittedMeasurementWhileReviewIsPending() {
InboundOrderEntity inbound = inbound("MEASURE_PENDING");
inbound.setMeasureReviewStatus("PENDING");
when(repository.findInbound(1L, 10L)).thenReturn(inbound);
assertThatThrownBy(() -> service.saveDraft(1L, 10L, 9L,
new InboundMeasurementRequests.SaveDraftRequest(30L, 10, null, List.of())))
.isInstanceOf(BizException.class)
.hasMessageContaining("已提交待审核");
verifyNoInteractions(batchService);
verify(orderCargoClient, never()).detail(any(), anyLong(), anyLong(), anyLong());
}
@Test
void savingDraftRejectsOverlongDefaultGoodsNameBeforeDatabaseWrite() {
InboundOrderEntity inbound = inbound("WAIT_MEASURE");
@@ -1157,6 +1236,7 @@ class InboundMeasurementServiceTest {
inbound.setTenantId(1L);
inbound.setOrderId(80L);
inbound.setWarehouseId(100L);
inbound.setInboundCode("RK-001");
inbound.setProcessStatus(status);
inbound.setTotalPackageCount(3);
inbound.setVersion(0);
@@ -392,28 +392,31 @@ class MeasurementBatchServiceTest {
}
@Test
void approvedManualMeasurementCanBeEditedAsDraftBeforeReviewCompletes() {
void submittedMeasurementCannotBeResetToDraftWhileReviewIsPending() {
InboundMeasurementEntity existing = new InboundMeasurementEntity();
existing.setId(100L);
existing.setTenantId(1L);
existing.setInboundId(10L);
existing.setSourceType("MANUAL");
existing.setStatus("APPROVED");
existing.setStatus("SUBMITTED");
existing.setVersion(3);
InboundOrderEntity inbound = inbound();
inbound.setProcessStatus("MEASURE_PENDING");
when(repository.findInbound(1L, 10L)).thenReturn(inbound);
when(repository.findMeasurement(1L, 10L, 100L)).thenReturn(existing);
when(repository.updateMeasurement(existing)).thenReturn(true);
MeasurementBatchCommand command = new MeasurementBatchCommand(
1L, 10L, 100L, 3, "MANUAL", "PLATFORM", null, null, null,
LocalDateTime.now(), null, "CKW-2", new BigDecimal("21"),
new BigDecimal("0.2"), "服装", 9L, List.of(item(BigDecimal.ONE)));
InboundMeasurementEntity saved = service.saveBatch(command);
assertThat(saved.getStatus()).isEqualTo("DRAFT");
verify(repository).updateMeasurement(existing);
verify(repository).replaceItems(org.mockito.ArgumentMatchers.eq(1L),
org.mockito.ArgumentMatchers.eq(100L), any());
assertThatThrownBy(() -> service.saveBatch(command))
.isInstanceOf(BizException.class)
.hasMessageContaining("已提交批次必须保持冻结");
assertThat(existing.getStatus()).isEqualTo("SUBMITTED");
verify(repository, never()).updateMeasurement(any());
verify(repository, never()).replaceItems(
org.mockito.ArgumentMatchers.anyLong(), org.mockito.ArgumentMatchers.anyLong(), any());
}
@Test