Merge remote-tracking branch 'origin/test' into dev

# Conflicts:
#	firstleg/src/main/java/com/baiyue/firstleg/client/OrderCargoClient.java
#	order/src/main/java/com/baiyue/order/controller/InternalOrderCargoController.java
#	order/src/test/java/com/baiyue/order/controller/InternalOrderCargoControllerTest.java
This commit is contained in:
2026-09-08 17:34:12 +08:00
35 changed files with 621 additions and 79 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",
@@ -16,6 +16,7 @@ import org.springframework.security.access.prepost.PreAuthorize;
import java.lang.reflect.Method;
import java.lang.reflect.RecordComponent;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import jakarta.validation.constraints.NotNull;
@@ -145,9 +146,15 @@ class LoadingListControllerContractTest {
.doesNotContainKey("internalContainerNo");
assertThat(properties(schemas, "ConfirmedSummary"))
.containsKeys("planId", "containerBookingId", "loadingListNo");
assertThat(properties(schemas, "LoadingItem")).containsKeys(
"itemId", "accountId", "inboundId", "inboundDetailId", "skuCode",
Map<String, Object> loadingItem = properties(schemas, "LoadingItem");
assertThat(loadingItem).containsKeys(
"itemId", "accountId", "customsTaxMode", "taxRefundFlag", "inboundId", "inboundDetailId", "skuCode",
"destinationWarehouse", "productQuantity", "frozenAt");
assertThat(stringMap(loadingItem.get("customsTaxMode")))
.containsEntry("nullable", true)
.containsEntry("enum", List.of("TAX_REFUND", "BUY_DECLARATION"));
assertThat(stringMap(loadingItem.get("taxRefundFlag")))
.containsEntry("nullable", true);
assertThat(properties(schemas, "OperationHistory")).containsKeys(
"historyId", "itemId", "operationId", "actionType", "fromStatus", "toStatus",
"beforeSnapshot", "afterSnapshot", "operatorId", "occurredAt");
@@ -24,6 +24,8 @@ class FeeItemMigrationSqlContractTest {
.contains("matched_seed.trade_scope = f.trade_scope")
.contains("193000000001000111, 0, 'FBA_HEAD_FREIGHT'")
.contains("fee_code = 'FBA_HEAD_FREIGHT'")
.contains("193000000001000112, 0, 'PRIVATE_LAST_MILE_DELIVERY'")
.contains("fee_code = 'PRIVATE_LAST_MILE_DELIVERY'")
.contains("warehouse_receivable = 1")
.doesNotContain("LEFT JOIN tmp_fee_item_seed by_code")
.doesNotContain("LEFT JOIN tmp_fee_item_seed by_semantic")
@@ -59,6 +59,11 @@ class LoadingListQueryMapperContractTest {
String items = sql("selectItems", Select.class);
assertThat(items)
.contains("i.order_id", "i.order_no", "b.container_no")
.contains("inbound.tax_mode AS customs_tax_mode")
.contains("WHEN 'TAX_REFUND' THEN TRUE")
.contains("WHEN 'BUY_DECLARATION' THEN FALSE")
.contains("ELSE NULL")
.doesNotContain("fl_customs_container_document", "fl_customs_container_document_link")
.contains("b.record_status='ACTIVE'")
.contains("NULLIF(i.pallet_count,0)")
.contains("fl_inbound_weight_result weight")
@@ -5,6 +5,7 @@ import com.baiyue.firstleg.dto.LoadingListDtos.ConfirmedQuery;
import com.baiyue.firstleg.dto.LoadingListDtos.PreloadQuery;
import com.baiyue.firstleg.mapper.LoadingListQueryMapper;
import com.baiyue.firstleg.mapper.LoadingListQueryMapper.AggregateRow;
import com.baiyue.firstleg.mapper.LoadingListQueryMapper.ItemRow;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -96,4 +97,47 @@ class LoadingListRepositoryTest {
var confirmed = repository.preloadDetail(row).summary().readiness();
assertThat(confirmed.readyForPda()).isTrue();
}
@Test
void mapsFrozenCustomsTaxFactsPerInboundAcrossCustomers() {
AggregateRow aggregate = new AggregateRow();
aggregate.setPlanId(41L);
aggregate.setContainerBookingId(61L);
aggregate.setPlanNo("LP-41");
aggregate.setPlanStatus("COMPLETED");
aggregate.setVersion(3);
ItemRow refund = item(101L, 11L, 201L, "IN-REFUND", "TAX_REFUND", true);
ItemRow declaration = item(102L, 12L, 202L, "IN-BUY", "BUY_DECLARATION", false);
ItemRow unknown = item(103L, 12L, 203L, "IN-UNKNOWN", null, null);
when(mapper.selectItems(41L)).thenReturn(List.of(refund, declaration, unknown));
when(mapper.selectHistories(41L)).thenReturn(List.of());
var items = repository.confirmedDetail(aggregate).items();
assertThat(items).extracting(value -> value.inboundCode(), value -> value.accountId(),
value -> value.customsTaxMode(), value -> value.taxRefundFlag())
.containsExactly(
org.assertj.core.groups.Tuple.tuple("IN-REFUND", 11L, "TAX_REFUND", true),
org.assertj.core.groups.Tuple.tuple("IN-BUY", 12L, "BUY_DECLARATION", false),
org.assertj.core.groups.Tuple.tuple("IN-UNKNOWN", 12L, null, null));
}
private static ItemRow item(long itemId, long accountId, long inboundId, String inboundCode,
String customsTaxMode, Boolean taxRefundFlag) {
ItemRow row = new ItemRow();
row.setItemId(itemId);
row.setAccountId(accountId);
row.setCustomerName("customer-" + accountId);
row.setCustomsTaxMode(customsTaxMode);
row.setTaxRefundFlag(taxRefundFlag);
row.setOrderId(300L + inboundId);
row.setOrderNo("ORDER-" + inboundId);
row.setInboundId(inboundId);
row.setInboundDetailId(400L + inboundId);
row.setInboundCode(inboundCode);
row.setPackageCount(1);
row.setLoadSequence(1);
row.setItemVersion(0);
return row;
}
}
@@ -11,6 +11,7 @@ import com.baiyue.firstleg.entity.InboundPricingEntity;
import com.baiyue.firstleg.entity.InboundPricingIdempotencyEntity;
import com.baiyue.firstleg.entity.InboundMeasurementEntity;
import com.baiyue.firstleg.entity.InboundMeasurementItemEntity;
import com.baiyue.firstleg.entity.CustomerChargeRuleEntity;
import com.baiyue.firstleg.pricing.ActivePriceBookEstimator;
import com.baiyue.firstleg.repository.FbaPricingRepository;
import com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows.RateRow;
@@ -37,6 +38,7 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.never;
@@ -217,6 +219,10 @@ class FbaPricingServiceTest {
InboundPricingEntity inbound = inbound("363.001", "0.5000");
inbound.setPricingTaxMode("TAX_EXCLUDED");
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
CustomerChargeRuleEntity rule = new CustomerChargeRuleEntity();
rule.setId(601L); rule.setRuleCode("CBM-RULE"); rule.setRuleVersion(1);
rule.setChargeMode("CBM"); rule.setDefaultRule(true);
when(repository.matchRules(eq(inbound), any())).thenReturn(List.of(rule));
when(priceBookEstimator.estimate(any())).thenReturn(
estimate("CBM", "1.0001", "100.000000", "100.01", "0.00"));
@@ -228,6 +234,8 @@ class FbaPricingServiceTest {
assertThat(response.conversionFactor()).isEqualByComparingTo("363");
assertThat(response.roundingScale()).isEqualTo(6);
assertThat(response.roundingMode()).isEqualTo("HALF_UP");
assertThat(response.chargeRuleId()).isEqualTo(601L);
assertThat(response.ruleCode()).isEqualTo("CBM-RULE");
}
@Test
@@ -483,15 +491,19 @@ class FbaPricingServiceTest {
FeeSnapshotResponse response = service.adjustPublished(TENANT, OPERATOR, INBOUND,
new com.baiyue.firstleg.dto.FbaPricingDtos.AdjustFeeRequest(
1, new BigDecimal("12.5"), new BigDecimal("8.00"),
new BigDecimal("20.00"), "客服修正"), "adjust-1");
1, new BigDecimal("160"), new BigDecimal("8.00"),
new BigDecimal("20.00"), "客服修正", true), "adjust-1");
assertThat(response.feeVersion()).isEqualTo(2);
assertThat(response.feeStatus()).isEqualTo("SENT");
assertThat(response.totalAmount()).isEqualByComparingTo("120.00");
assertThat(response.totalAmount()).isEqualByComparingTo("1300.00");
assertThat(response.baselineChargeableQuantity()).isEqualByComparingTo("167.000000");
assertThat(response.lowerChargeableQuantity()).isEqualByComparingTo("150.300000");
assertThat(response.upperChargeableQuantity()).isEqualByComparingTo("183.700000");
assertThat(response.tolerancePercent()).isEqualByComparingTo("10.00");
assertThat(response.nextProcessStatus()).isEqualTo("WAIT_SPLIT");
assertThat(inbound.getProcessStatus()).isEqualTo("WAIT_SPLIT");
assertThat(inserted.get().getChargeableQuantity()).isEqualByComparingTo("12.5000");
assertThat(inserted.get().getChargeableQuantity()).isEqualByComparingTo("160.0000");
verify(repository).supersedeCurrent(TENANT, 501L, "SENT");
verify(financeOutbox).publishInbound(eq(FinanceFeeSourceOutboxService.ADJUSTED),
eq(TENANT), eq(INBOUND), eq(502L), eq(2), eq(NOW));
@@ -538,7 +550,7 @@ class FbaPricingServiceTest {
assertThatThrownBy(() -> service.adjustPublished(TENANT, OPERATOR, INBOUND,
new com.baiyue.firstleg.dto.FbaPricingDtos.AdjustFeeRequest(
1, new BigDecimal("12.5"), new BigDecimal("8.00"),
BigDecimal.ZERO, "客服修正"), "locked-adjust"))
BigDecimal.ZERO, "客服修正", true), "locked-adjust"))
.isInstanceOf(BizException.class)
.hasMessageContaining("客户已确认");
@@ -574,11 +586,11 @@ class FbaPricingServiceTest {
FeeSnapshotResponse response = service.adjustPublished(TENANT, OPERATOR, INBOUND,
new com.baiyue.firstleg.dto.FbaPricingDtos.AdjustFeeRequest(
1, new BigDecimal("12.5"), new BigDecimal("8.00"),
new BigDecimal("20.00"), "客服修正"), "adjust-calculated");
1, new BigDecimal("160"), new BigDecimal("8.00"),
new BigDecimal("20.00"), "客服修正", true), "adjust-calculated");
assertThat(response.feeStatus()).isEqualTo("CALCULATED");
assertThat(response.totalAmount()).isEqualByComparingTo("120.00");
assertThat(response.totalAmount()).isEqualByComparingTo("1300.00");
assertThat(response.nextProcessStatus()).isEqualTo("WAIT_SPLIT");
verify(repository).supersedeCurrent(TENANT, 501L, "CALCULATED");
verify(feeDetails).materializeCalculatedFreight(TENANT, INBOUND, 503L, OPERATOR, NOW);
@@ -598,7 +610,7 @@ class FbaPricingServiceTest {
assertThatThrownBy(() -> service.adjustPublished(TENANT, OPERATOR, INBOUND,
new com.baiyue.firstleg.dto.FbaPricingDtos.AdjustFeeRequest(
1, new BigDecimal("12.5"), new BigDecimal("8.00"),
BigDecimal.ZERO, "客服修正"), "stale-adjust"))
BigDecimal.ZERO, "客服修正", true), "stale-adjust"))
.isInstanceOf(BizException.class)
.extracting(error -> ((BizException) error).getResultCode())
.isEqualTo(ResultCode.CONFLICT);
@@ -607,6 +619,64 @@ class FbaPricingServiceTest {
verify(repository, never()).supersedeCurrent(anyLong(), anyLong(), any());
}
@Test
@DisplayName("人工计费量允许上下10%含边界")
void acceptsInclusiveToleranceBoundaries() {
InboundPricingEntity inbound = inbound("10", "1");
inbound.setProcessStatus("WAIT_SPLIT");
InboundFeeEntity current = savedFee("CALCULATED");
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
when(repository.findLatestFee(TENANT, INBOUND)).thenReturn(current);
when(repository.findIdempotency(TENANT, OPERATOR, "FEE_ADJUST", "lower-bound")).thenReturn(null);
doAnswer(invocation -> { ((InboundPricingIdempotencyEntity) invocation.getArgument(0)).setId(903L); return null; })
.when(repository).insertIdempotency(any());
when(repository.nextFeeVersion(TENANT, INBOUND)).thenReturn(2);
when(repository.supersedeCurrent(TENANT, 501L, "CALCULATED")).thenReturn(1);
when(repository.markInboundAdjusted(eq(TENANT), eq(INBOUND), eq("CALCULATED"), eq("KG"), any()))
.thenReturn(1);
doAnswer(invocation -> { ((InboundFeeEntity) invocation.getArgument(0)).setId(504L); return null; })
.when(repository).insertFee(any());
when(repository.completeIdempotency(any(), eq(504L), eq(NOW))).thenReturn(1);
when(repository.findFee(TENANT, 504L)).thenAnswer(invocation -> current);
service.adjustPublished(TENANT, OPERATOR, INBOUND,
new com.baiyue.firstleg.dto.FbaPricingDtos.AdjustFeeRequest(
1, new BigDecimal("150.3"), new BigDecimal("2"), BigDecimal.ZERO,
"下界确认", true), "lower-bound");
verify(repository).insertFee(argThat(fee ->
fee.getChargeableQuantity().compareTo(new BigDecimal("150.3000")) == 0));
}
@Test
@DisplayName("人工计费量越过10%或未二次确认返回冲突")
void rejectsOutOfToleranceOrUnconfirmedAdjustment() {
InboundPricingEntity inbound = inbound("10", "1");
inbound.setProcessStatus("WAIT_SPLIT");
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
when(repository.findLatestFee(TENANT, INBOUND)).thenReturn(savedFee("CALCULATED"));
when(repository.findIdempotency(anyLong(), anyLong(), any(), any())).thenReturn(null);
doAnswer(invocation -> { ((InboundPricingIdempotencyEntity) invocation.getArgument(0)).setId(904L); return null; })
.when(repository).insertIdempotency(any());
when(repository.nextFeeVersion(TENANT, INBOUND)).thenReturn(2);
assertThatThrownBy(() -> service.adjustPublished(TENANT, OPERATOR, INBOUND,
new com.baiyue.firstleg.dto.FbaPricingDtos.AdjustFeeRequest(
1, new BigDecimal("183.7001"), new BigDecimal("2"), BigDecimal.ZERO,
"越界", true), "outside"))
.isInstanceOf(BizException.class)
.extracting(error -> ((BizException) error).getResultCode())
.isEqualTo(ResultCode.CONFLICT);
assertThatThrownBy(() -> service.adjustPublished(TENANT, OPERATOR, INBOUND,
new com.baiyue.firstleg.dto.FbaPricingDtos.AdjustFeeRequest(
1, new BigDecimal("167"), new BigDecimal("2"), BigDecimal.ZERO,
"未确认", false), "unconfirmed"))
.isInstanceOf(BizException.class)
.extracting(error -> ((BizException) error).getResultCode())
.isEqualTo(ResultCode.CONFLICT);
verify(repository, never()).supersedeCurrent(anyLong(), anyLong(), any());
}
private void readyRepository() {
when(repository.findIdempotency(anyLong(), anyLong(), any(), any())).thenReturn(null);
doAnswer(invocation -> { ((InboundPricingIdempotencyEntity) invocation.getArgument(0)).setId(900L); return null; })
@@ -337,6 +337,29 @@ class FeeManagementServiceTest {
.hasMessageContaining("仓储应收/应付");
}
@Test
@DisplayName("私人仓尾程派送费只允许PRIVATE目的地")
void restrictsPrivateLastMileDeliveryFeeByDestinationType() {
InboundOrderEntity fbaInbound = inbound();
fbaInbound.setDestinationType("FBA");
FeeItemEntity privateDelivery = item(true, true);
privateDelivery.setFeeCode("PRIVATE_LAST_MILE_DELIVERY");
when(repository.findInbound(10001L, 21L)).thenReturn(fbaInbound);
when(repository.findItem(11L)).thenReturn(privateDelivery);
assertThatThrownBy(() -> details.create(10001L, 21L, 9L,
detailRequest("RECEIVABLE")))
.isInstanceOf(BizException.class)
.hasMessageContaining("目的地类型为PRIVATE");
verify(repository, never()).insertDetail(any());
fbaInbound.setDestinationType("PRIVATE");
doAnswer(invocation -> { ((InboundFeeDetailEntity) invocation.getArgument(0)).setId(32L); return null; })
.when(repository).insertDetail(any());
assertThat(details.create(10001L, 21L, 9L, detailRequest("RECEIVABLE")).getId())
.isEqualTo(32L);
}
@Test
@DisplayName("审核只更新待审核明细")
void approvesPendingDetail() {
@@ -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
@@ -292,6 +292,7 @@ class MerchantPriceBookServiceTest {
String weight, String volume) {
return new EstimateRequest(10L, channelCode, destinationCode, destinationType,
destinationWarehouseId, null, destinationRegion, pricingTaxMode,
pricingTaxMode == PricingTaxMode.TAX_INCLUDED ? "KG" : "CBM",
decimal(weight), decimal(volume));
}