feat(firstleg): enforce pricing adjustment bounds
This commit is contained in:
@@ -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; })
|
||||
|
||||
Reference in New Issue
Block a user