修正外箱快照清空并提供当前计费量只读预览

This commit is contained in:
2026-10-28 11:03:44 +08:00
parent b2e4f0d246
commit ca61f79b6d
10 changed files with 376 additions and 35 deletions
@@ -6,6 +6,7 @@ import com.baiyue.firstleg.domain.PriceModel;
import com.baiyue.firstleg.domain.PricingTaxMode;
import com.baiyue.firstleg.dto.FbaPricingDtos.FeeSnapshotResponse;
import com.baiyue.firstleg.dto.FbaPricingDtos.ManualQuoteRequest;
import com.baiyue.firstleg.dto.FbaPricingDtos.FeePreviewRequest;
import com.baiyue.firstleg.entity.CarrierFbaRateEntity;
import com.baiyue.firstleg.entity.InboundFeeEntity;
import com.baiyue.firstleg.entity.InboundFeeItemEntity;
@@ -70,6 +71,87 @@ class FbaPricingServiceTest {
@MockBean private ContainerTierAmountAllocator containerTierAllocator;
@Resource private FbaPricingService service;
@Test
/** 预览只按当前选择值估价,不复用旧费用,也不产生任何计费写入。 */
void previewsSelectedOuterVolumeWithoutWritingFeeState() {
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound("570.500", "3.6894"));
InboundMeasurementEntity measurement = new InboundMeasurementEntity();
measurement.setId(901L);
InboundMeasurementItemEntity item = new InboundMeasurementItemEntity();
item.setOuterLengthCm(new BigDecimal("63"));
item.setOuterWidthCm(new BigDecimal("53"));
item.setOuterHeightCm(new BigDecimal("53"));
item.setCalculationRuleVersion("WAREHOUSE_OUTER_OVERRIDE_V1");
when(measurementRepository.findLatestMeasurement(TENANT, INBOUND)).thenReturn(measurement);
when(measurementRepository.listItems(TENANT, 901L)).thenReturn(List.of(item));
when(priceBookEstimator.estimate(anyLong(), any())).thenReturn(
estimate("KG", "650.298000", "13.6", "8844.05", "0"));
var preview = service.preview(TENANT, INBOUND, new FeePreviewRequest(NOW,
new BigDecimal("570.500"), new BigDecimal("3.8940"), "ACTUAL", "OUTER", null));
assertThat(preview.actualVolumeCbm()).isEqualByComparingTo("3.8940");
assertThat(preview.chargeableQuantity()).isEqualByComparingTo("650.298000");
assertThat(preview.totalAmount()).isEqualByComparingTo("8844.05");
assertThat(preview.priceMatched()).isTrue();
verify(priceBookEstimator).estimate(eq(TENANT), argThat(request ->
request.actualWeightKg().compareTo(new BigDecimal("570.5")) == 0
&& request.actualVolumeCbm().compareTo(new BigDecimal("3.894")) == 0));
verify(repository, never()).insertFee(any());
verify(repository, never()).insertIdempotency(any());
verify(repository, never()).nextFeeVersion(anyLong(), anyLong());
verify(repository, never()).markInboundCalculated(anyLong(), anyLong(), any(), any(), any(), any(), any());
org.mockito.Mockito.verifyNoInteractions(financeOutbox, feeDetails);
}
@Test
/** FBA 未命中价格时仍返回服务端计费量,供人工单价预览使用。 */
void previewsManualQuantityWhenPriceIsMissing() {
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound("570.500", "3.6894"));
when(priceBookEstimator.estimate(anyLong(), any()))
.thenThrow(new BizException(ResultCode.NOT_FOUND, "价格未命中"));
var preview = service.preview(TENANT, INBOUND, new FeePreviewRequest(NOW,
new BigDecimal("570.5"), new BigDecimal("3.894"), null, null, null));
assertThat(preview.priceMatched()).isFalse();
assertThat(preview.unitPrice()).isNull();
assertThat(preview.chargeableQuantity()).isEqualByComparingTo("650.298000");
assertThat(preview.additionalAmount()).isZero();
assertThat(preview.totalAmount()).isNull();
verify(repository, never()).insertFee(any());
}
@Test
/** 外箱快照缺失时拒绝预览,不把当前规则的前端推导结果视作已保存事实。 */
void rejectsOuterPreviewWithoutSavedMeasurementSnapshot() {
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound("100", "1"));
assertThatThrownBy(() -> service.preview(TENANT, INBOUND,
new FeePreviewRequest(NOW, new BigDecimal("100"), BigDecimal.ONE, "ACTUAL", "OUTER", null)))
.isInstanceOf(BizException.class).hasMessageContaining("测量数据不存在");
org.mockito.Mockito.verifyNoInteractions(priceBookEstimator, financeOutbox, feeDetails);
}
@Test
/** 预览不能读取其他客户的入库单。 */
void rejectsCrossCustomerPreview() {
assertThatThrownBy(() -> service.preview(10002L, INBOUND, new FeePreviewRequest(NOW, null, null, null, null, null)))
.isInstanceOf(BizException.class).hasMessageContaining("入库单不存在");
org.mockito.Mockito.verifyNoInteractions(priceBookEstimator, financeOutbox, feeDetails);
}
@Test
/** 人工单价的金额也由服务端精确取整,不在页面重复实现舍入。 */
void previewsManualUnitPriceUsingExactMoneyRounding() {
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound("1", "0.001"));
when(priceBookEstimator.estimate(anyLong(), any())).thenReturn(estimate("KG", "1", "2", "2", "0"));
var preview = service.preview(TENANT, INBOUND, new FeePreviewRequest(NOW,
BigDecimal.ONE, new BigDecimal("0.001"), null, null, new BigDecimal("1.005")));
assertThat(preview.unitPrice()).isEqualByComparingTo("2");
assertThat(preview.totalAmount()).isEqualByComparingTo("1.01");
verify(repository, never()).insertFee(any());
}
@Test
@DisplayName("客户B不能读取客户A的费用快照")
void rejectsCrossCustomerFeeLookup() {