merge: sync test and stabilize full acceptance

This commit is contained in:
2026-09-09 21:14:25 +08:00
40 changed files with 595 additions and 134 deletions
@@ -788,7 +788,7 @@ class FbaPricingServiceTest {
InboundFeeEntity first = savedFee("CALCULATED");
first.setActualWeightKg(new BigDecimal("90.000")); first.setActualVolumeCbm(new BigDecimal("0.1000"));
first.setMeasurementId(301L); first.setMeasurementSource("OUTER");
InboundFeeEntity second = savedFee("CONFIRMED");
InboundFeeEntity second = savedFee("CALCULATED");
second.setId(502L); second.setInboundId(30002L); second.setActualWeightKg(new BigDecimal("110.000"));
second.setActualVolumeCbm(new BigDecimal("0.1000")); second.setMeasurementId(302L);
second.setMeasurementSource("ACTUAL");
@@ -834,20 +834,61 @@ class FbaPricingServiceTest {
}
@Test
@DisplayName("柜内任一已发送或客户确认费用阻断整组重算")
void rejectsContainerRecalculationWhenAnyFeeIsImmutable() {
@DisplayName("柜内已发送费用保持锁定且不阻断其他未锁定费用重算")
void keepsImmutableContainerFeeAndRecalculatesOnlyUnlockedMembers() {
readyRepository();
InboundPricingEntity inbound = inbound("100", "0.1");
InboundPricingEntity lockedInbound = inbound("100", "0.1");
InboundPricingEntity unlockedInbound = inbound("80", "0.1");
unlockedInbound.setId(30002L); unlockedInbound.setInboundCode("RK-002");
InboundFeeEntity sent = savedFee("SENT");
sent.setActualWeightKg(new BigDecimal("100")); sent.setActualVolumeCbm(new BigDecimal("0.1"));
InboundFeeEntity calculated = savedFee("CALCULATED");
calculated.setId(502L); calculated.setInboundId(30002L);
calculated.setActualWeightKg(new BigDecimal("80")); calculated.setActualVolumeCbm(new BigDecimal("0.1"));
when(repository.containerTierMembersForUpdate(901L, TENANT)).thenReturn(List.of(
memberRow(901L, INBOUND, "100", "0.1"), memberRow(901L, 30002L, "80", "0.1")
));
when(repository.findInbound(TENANT, INBOUND)).thenReturn(lockedInbound);
when(repository.findInbound(TENANT, 30002L)).thenReturn(unlockedInbound);
when(repository.findLatestFeeForUpdate(TENANT, INBOUND)).thenReturn(sent);
when(repository.findLatestFeeForUpdate(TENANT, 30002L)).thenReturn(calculated);
when(repository.nextFeeVersion(TENANT, 30002L)).thenReturn(2);
when(repository.completeIdempotency(any(), eq(901L), eq(NOW))).thenReturn(1);
when(repository.supersedeCurrent(TENANT, 502L, "CALCULATED")).thenReturn(1);
when(repository.markInboundContainerFeeRecalculated(eq(TENANT), eq(30002L), anyString(), anyString(), any()))
.thenReturn(1);
when(priceBookEstimator.estimate(eq(TENANT), any())).thenReturn(regionEstimate("180", "8", "1440"));
when(containerTierAllocator.allocate(any(), eq(new BigDecimal("8")))).thenReturn(
new ContainerTierAmountAllocator.Allocation(new BigDecimal("180"), new BigDecimal("1440"),
Map.of(INBOUND, new BigDecimal("800.00"), 30002L, new BigDecimal("640.00"))));
var result = service.recalculateUnlockedContainerCustomerFees(901L, TENANT, "loading-change-2");
assertThat(result.status()).isEqualTo(FbaPricingService.ContainerFeeRecalculationStatus.RECALCULATED);
assertThat(result.recalculatedCount()).isEqualTo(1);
ArgumentCaptor<InboundFeeEntity> fee = ArgumentCaptor.forClass(InboundFeeEntity.class);
verify(repository).insertFee(fee.capture());
assertThat(fee.getValue().getInboundId()).isEqualTo(30002L);
assertThat(fee.getValue().getSubtotalAmount()).isEqualByComparingTo("640.00");
verify(repository, never()).supersedeCurrent(TENANT, 501L, "SENT");
verify(priceBookEstimator).estimate(eq(TENANT), any());
}
@Test
@DisplayName("柜内费用全部锁定时关系变更无需重算且不报错")
void skipsContainerRecalculationWhenEveryFeeIsImmutable() {
InboundPricingEntity inbound = inbound("100", "0.1");
InboundFeeEntity confirmed = savedFee("CONFIRMED");
when(repository.containerTierMembersForUpdate(901L, TENANT)).thenReturn(List.of(
memberRow(901L, INBOUND, "100", "0.1")
));
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
when(repository.findLatestFeeForUpdate(TENANT, INBOUND)).thenReturn(sent);
when(repository.findLatestFeeForUpdate(TENANT, INBOUND)).thenReturn(confirmed);
assertThatThrownBy(() -> service.recalculateUnlockedContainerCustomerFees(901L, TENANT, "loading-change-2"))
.isInstanceOf(BizException.class).hasMessageContaining("已发送或客户已确认");
var result = service.recalculateUnlockedContainerCustomerFees(901L, TENANT, "loading-change-locked");
assertThat(result.status()).isEqualTo(FbaPricingService.ContainerFeeRecalculationStatus.NOT_APPLICABLE);
assertThat(result.recalculatedCount()).isZero();
verify(repository, never()).supersedeCurrent(anyLong(), anyLong(), anyString());
verify(repository, never()).insertFee(any());
verify(priceBookEstimator, never()).estimate(anyLong(), any());
@@ -157,6 +157,26 @@ class InboundOrderCreationServiceTest {
verify(orders, times(2)).update(any(), any(Wrapper.class));
}
@Test
void approvalKeepsMeasurementAlreadyCapturedBeforeOrderReview() {
InboundOrderEntity prepared = inbound(900L, "PCHK2608140001", "UNPRICED", null, null);
prepared.setProcessStatus("MEASURING");
prepared.setOrderStatusSnapshot("SUBMITTED");
when(orders.selectOne(any(Wrapper.class))).thenReturn(prepared);
when(orders.update(any(), any(Wrapper.class))).thenReturn(1);
when(fulfillmentTasks.provision(any(), any())).thenReturn(task(700L));
assertThat(service.createFromApprovedOrder(approvedCommand("PCHK2608140001"))).isEqualTo(900L);
verify(orders, times(2)).update(any(), any(Wrapper.class));
ArgumentCaptor<InboundStatusHistoryEntity> historyCaptor =
ArgumentCaptor.forClass(InboundStatusHistoryEntity.class);
verify(histories).insert(historyCaptor.capture());
assertThat(historyCaptor.getValue().getFromStatus()).isEqualTo("MEASURING");
assertThat(historyCaptor.getValue().getToStatus()).isEqualTo("MEASURING");
assertThat(historyCaptor.getValue().getAction()).isEqualTo("ORDER_APPROVED");
}
private static InboundOrderEntity inbound(long id, String code, String feeStatus,
Long carrierId, String carrierName) {
InboundOrderEntity inbound = new InboundOrderEntity();
@@ -65,6 +65,21 @@ class MeasurementReviewServiceTest {
verify(repository, never()).transitionReview(anyLong(), anyLong(), anyLong(), anyInt(), anyBoolean(), anyLong());
}
@Test
@DisplayName("订单待审核时保留测量数据但不能审核测量")
void shouldBlockMeasurementReviewUntilOrderIsApproved() {
Fixture fixture = fixture(false);
fixture.order.setOrderStatusSnapshot("SUBMITTED");
assertThatThrownBy(() -> fixture.service.approve(10001L, 9L, "审核员", 10L,
"approve-before-order", new MeasurementReviewRequest(20L, 3, null)))
.isInstanceOf(BizException.class)
.hasMessageContaining("订单尚未审核通过");
verify(fixture.repository, never()).transitionReview(
anyLong(), anyLong(), anyLong(), anyInt(), anyBoolean(), anyLong());
}
@Test
@DisplayName("审核通过是测量到计费的唯一正常推进关口")
void shouldApproveOnlyPendingSubmittedMeasurement() {
@@ -249,6 +264,7 @@ class MeasurementReviewServiceTest {
when(properties.isAllowRejectedMeasurement()).thenReturn(allowRelease);
InboundOrderEntity order = new InboundOrderEntity();
order.setProcessStatus("MEASURE_PENDING");
order.setOrderStatusSnapshot("APPROVED");
order.setMeasureReviewStatus("PENDING");
order.setTotalPackageCount(5);
order.setActualGrossWeightKg(new BigDecimal("1787.500"));