修改提交

This commit is contained in:
2026-09-03 21:09:22 +08:00
parent 171f450e59
commit baabfdabba
38 changed files with 646 additions and 206 deletions
@@ -9,6 +9,8 @@ import com.baiyue.firstleg.entity.InboundFeeEntity;
import com.baiyue.firstleg.entity.InboundFeeItemEntity;
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.pricing.ActivePriceBookEstimator;
import com.baiyue.firstleg.repository.FbaPricingRepository;
import com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows.RateRow;
@@ -27,6 +29,7 @@ import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.concurrent.atomic.AtomicReference;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -49,6 +52,7 @@ class FbaPricingServiceTest {
private static final long INBOUND = 30001L;
private static final LocalDateTime NOW = LocalDateTime.of(2026, 8, 3, 10, 0);
@MockBean private FbaPricingRepository repository;
@MockBean private com.baiyue.firstleg.repository.InboundMeasurementRepository measurementRepository;
@MockBean private InboundFeeDetailService feeDetails;
@MockBean private ActivePriceBookEstimator priceBookEstimator;
@Resource private FbaPricingService service;
@@ -100,6 +104,35 @@ class FbaPricingServiceTest {
assertThat(fee.getValue().getRoundingMode()).isEqualTo("HALF_UP");
}
@Test
@DisplayName("外箱规计费保存测量批次和尺寸来源快照")
void bindsSavedOuterMeasurementWhenCalculatingCustomerFee() {
readyRepository();
InboundPricingEntity inbound = inbound("62.500", "1.0019");
InboundMeasurementEntity measurement = new InboundMeasurementEntity();
measurement.setId(301L);
InboundMeasurementItemEntity item = new InboundMeasurementItemEntity();
item.setOuterLengthCm(new BigDecimal("34"));
item.setOuterWidthCm(new BigDecimal("24"));
item.setOuterHeightCm(new BigDecimal("64"));
item.setCalculationRuleVersion("CUSTOMER_DIMENSION_SMALL_V3");
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
when(measurementRepository.findLatestMeasurement(TENANT, INBOUND)).thenReturn(measurement);
when(measurementRepository.listItems(TENANT, 301L)).thenReturn(List.of(item));
when(priceBookEstimator.estimate(any())).thenReturn(
estimate("CBM", "1.0019", "100", "100.19", "0"));
service.calculate(TENANT, OPERATOR, INBOUND, NOW,
new BigDecimal("62.500"), new BigDecimal("1.0019"),
"OUTER", "OUTER", "calc-outer");
ArgumentCaptor<InboundFeeEntity> fee = ArgumentCaptor.forClass(InboundFeeEntity.class);
verify(repository).insertFee(fee.capture());
assertThat(fee.getValue().getMeasurementId()).isEqualTo(301L);
assertThat(fee.getValue().getMeasurementSource()).isEqualTo("OUTER");
assertThat(fee.getValue().getActualVolumeCbm()).isEqualByComparingTo("1.0019");
}
@Test
@DisplayName("缺少订单客户归属时拒绝生成对客费用")
void rejectsMissingOrderCustomerOwnership() {
@@ -182,44 +215,31 @@ class FbaPricingServiceTest {
}
@Test
@DisplayName("装柜完成后的自动费用仍可确认")
void confirmsCalculatedFeeAfterLoadingCompleted() {
@DisplayName("装柜完成后禁止确认费用")
void rejectsConfirmAfterLoadingCompleted() {
InboundPricingEntity inbound = inbound("10.000", "1.0000");
inbound.setProcessStatus("LOADED");
inbound.setFeeStatus("CALCULATED");
InboundFeeEntity fee = savedFee("CALCULATED");
when(repository.findLatestFee(TENANT, INBOUND)).thenReturn(fee);
when(repository.findIdempotency(TENANT, OPERATOR, "FEE_CONFIRM", "confirm-loaded"))
.thenReturn(null);
doAnswer(invocation -> {
((InboundPricingIdempotencyEntity) invocation.getArgument(0)).setId(900L);
return null;
}).when(repository).insertIdempotency(any());
when(repository.confirmFee(TENANT, 501L, OPERATOR, NOW)).thenReturn(1);
when(repository.markInboundConfirmed(TENANT, INBOUND)).thenReturn(1);
when(repository.completeIdempotency(any(), eq(501L), eq(NOW))).thenReturn(1);
when(repository.findFee(TENANT, 501L)).thenReturn(fee);
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
FeeSnapshotResponse response = service.confirm(
TENANT, OPERATOR, INBOUND, "confirm-loaded");
assertThat(response.nextProcessStatus()).isEqualTo("LOADED");
verify(repository).markInboundConfirmed(TENANT, INBOUND);
assertThatThrownBy(() -> service.confirm(TENANT, OPERATOR, INBOUND, "confirm-loaded"))
.isInstanceOf(BizException.class)
.hasMessageContaining("装柜流程");
verify(repository, never()).markInboundConfirmed(TENANT, INBOUND);
}
@Test
@DisplayName("审核未通过的测量快照禁止计费")
void rejectsUnapprovedMeasurementSnapshot() {
@DisplayName("审核未通过的测量快照也允许计费")
void allowsUnapprovedMeasurementSnapshot() {
readyRepository();
InboundPricingEntity inbound = inbound("10", "1");
inbound.setMeasureReviewStatus("REJECTED");
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
when(priceBookEstimator.estimate(any())).thenReturn(
estimate("CBM", "1.0000", "10", "10", "0"));
assertThatThrownBy(() -> service.calculate(TENANT, OPERATOR, INBOUND, NOW, "calc-3"))
.isInstanceOf(BizException.class)
.hasMessageContaining("审核通过");
verify(priceBookEstimator, never()).estimate(any());
assertThat(service.calculate(TENANT, OPERATOR, INBOUND, NOW, "calc-3")).isNotNull();
verify(priceBookEstimator).estimate(any());
}
@Test
@@ -368,6 +388,55 @@ class FbaPricingServiceTest {
eq(TENANT), eq(INBOUND), eq(502L), eq(2), eq(NOW));
}
@Test
@DisplayName("客户确认已发送费用后记录确认人并锁定版本")
void customerConfirmsSentFeeAndLocksVersion() {
InboundFeeEntity current = savedFee("SENT");
when(repository.findLatestFee(TENANT, INBOUND)).thenReturn(current);
when(repository.findIdempotency(TENANT, OPERATOR, "CUSTOMER_FEE_CONFIRM", "customer-confirm-1"))
.thenReturn(null);
doAnswer(invocation -> { ((InboundPricingIdempotencyEntity) invocation.getArgument(0)).setId(910L); return null; })
.when(repository).insertIdempotency(any());
when(repository.confirmFeeByCustomer(TENANT, 501L, 1, OPERATOR, "客户成员", NOW))
.thenAnswer(invocation -> {
current.setCustomerConfirmedBy(OPERATOR);
current.setCustomerConfirmedByName("客户成员");
current.setCustomerConfirmedAt(NOW);
return 1;
});
when(repository.completeIdempotency(any(), eq(501L), eq(NOW))).thenReturn(1);
when(repository.findFee(TENANT, 501L)).thenReturn(current);
FeeSnapshotResponse response = service.confirmByCustomer(
TENANT, OPERATOR, "客户成员", INBOUND, 1, "customer-confirm-1");
assertThat(response.customerConfirmationStatus()).isEqualTo("CONFIRMED");
assertThat(response.customerConfirmedBy()).isEqualTo(OPERATOR);
assertThat(response.customerConfirmedByName()).isEqualTo("客户成员");
assertThat(response.customerConfirmedAt()).isEqualTo(NOW);
}
@Test
@DisplayName("客户已确认费用时拒绝平台再次修改")
void rejectsAdjustmentAfterCustomerConfirmation() {
InboundPricingEntity inbound = inbound("10", "1");
inbound.setProcessStatus("WAIT_SPLIT");
InboundFeeEntity current = savedFee("SENT");
current.setCustomerConfirmedAt(NOW);
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
when(repository.findLatestFee(TENANT, INBOUND)).thenReturn(current);
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"))
.isInstanceOf(BizException.class)
.hasMessageContaining("客户已确认");
verify(repository, never()).insertIdempotency(any());
verify(repository, never()).supersedeCurrent(anyLong(), anyLong(), any());
}
@Test
@DisplayName("初算费用调整后保持初算状态且不发布财务来源")
void adjustsCalculatedFeeWithoutPublishingIt() {