优化
This commit is contained in:
@@ -41,6 +41,27 @@ public interface ContainerTierPricingMapper {
|
||||
List<MemberRow> members(@Param("customerId") long customerId,
|
||||
@Param("inboundId") long inboundId);
|
||||
|
||||
/** 锁住当前柜次内某客户全部有效明细,供阶梯费用组重算使用。 */
|
||||
@Select("""
|
||||
SELECT plan.container_booking_id AS containerBookingId,
|
||||
inbound.id AS inboundId,
|
||||
inbound.actual_net_weight_kg AS actualWeightKg,
|
||||
inbound.actual_volume_cbm AS actualVolumeCbm
|
||||
FROM fl_loading_plan plan
|
||||
JOIN fl_loading_plan_item item ON item.loading_plan_id=plan.id
|
||||
AND item.deleted=0
|
||||
AND item.account_id=#{customerId}
|
||||
JOIN fl_inbound_order inbound ON inbound.id=item.inbound_id
|
||||
AND inbound.tenant_id=#{customerId}
|
||||
AND inbound.deleted=0
|
||||
WHERE plan.container_booking_id=#{containerBookingId}
|
||||
AND plan.plan_status IN ('EDITING','LOADING','COMPLETED')
|
||||
ORDER BY item.inbound_id,item.id
|
||||
FOR UPDATE
|
||||
""")
|
||||
List<MemberRow> membersForUpdate(@Param("containerBookingId") long containerBookingId,
|
||||
@Param("customerId") long customerId);
|
||||
|
||||
record MemberRow(long containerBookingId, long inboundId, BigDecimal actualWeightKg,
|
||||
BigDecimal actualVolumeCbm) {}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,11 @@ public class FbaPricingRepository {
|
||||
return containerTierPricing.members(customerId, inboundId);
|
||||
}
|
||||
|
||||
public List<ContainerTierPricingMapper.MemberRow> containerTierMembersForUpdate(
|
||||
long containerBookingId, long customerId) {
|
||||
return containerTierPricing.membersForUpdate(containerBookingId, customerId);
|
||||
}
|
||||
|
||||
public void insertRule(CustomerChargeRuleEntity rule) { rules.insert(rule); }
|
||||
|
||||
public int clearDefaultRule(long tenantId, long customerId, long operatorId) {
|
||||
@@ -311,6 +316,19 @@ public class FbaPricingRepository {
|
||||
return inbounds.update(null, update);
|
||||
}
|
||||
|
||||
public int markInboundContainerFeeRecalculated(long tenantId, long inboundId, String processStatus,
|
||||
String chargeMode, java.math.BigDecimal chargeable) {
|
||||
LambdaUpdateWrapper<InboundPricingEntity> update = new LambdaUpdateWrapper<InboundPricingEntity>()
|
||||
.eq(InboundPricingEntity::getTenantId, tenantId)
|
||||
.eq(InboundPricingEntity::getId, inboundId)
|
||||
.eq(InboundPricingEntity::getProcessStatus, processStatus)
|
||||
.in(InboundPricingEntity::getFeeStatus, List.of("CALCULATED", "CONFIRMED"))
|
||||
.set(InboundPricingEntity::getFeeStatus, "CALCULATED");
|
||||
if ("KG".equals(chargeMode)) update.set(InboundPricingEntity::getChargeableWeightKg, chargeable);
|
||||
else update.set(InboundPricingEntity::getChargeableVolumeCbm, chargeable);
|
||||
return inbounds.update(null, update);
|
||||
}
|
||||
|
||||
public int confirmFee(long tenantId, long feeId, long operatorId, LocalDateTime now) {
|
||||
return fees.update(null, new LambdaUpdateWrapper<InboundFeeEntity>()
|
||||
.eq(InboundFeeEntity::getTenantId, tenantId).eq(InboundFeeEntity::getId, feeId)
|
||||
|
||||
@@ -13,6 +13,7 @@ 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.mapper.ContainerTierPricingMapper;
|
||||
import com.baiyue.firstleg.pricing.ActivePriceBookEstimator;
|
||||
import com.baiyue.firstleg.pricing.ContainerTierAmountAllocator;
|
||||
import com.baiyue.firstleg.domain.PriceModel;
|
||||
@@ -30,6 +31,7 @@ import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.Clock;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HexFormat;
|
||||
import java.util.List;
|
||||
import java.util.LinkedHashMap;
|
||||
@@ -43,6 +45,7 @@ public class FbaPricingService {
|
||||
private static final BigDecimal ONE_HUNDRED = new BigDecimal("100");
|
||||
private static final long AUTOMATIC_CONFIRM_OPERATOR_ID = 0L;
|
||||
private static final String AUTOMATIC_CONFIRM_OPERATOR_NAME = "装柜完成自动确认";
|
||||
private static final String CONTAINER_RECALCULATE_OPERATION = "CONTAINER_FEE_RECALCULATE";
|
||||
@Resource
|
||||
private FbaPricingRepository repository;
|
||||
@Resource
|
||||
@@ -213,6 +216,171 @@ public class FbaPricingService {
|
||||
.setScale(6, RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
/**
|
||||
* 柜内明细变更后,按最新有效柜内成员重新命中区域重量阶梯。
|
||||
* 仅使用当前费用快照冻结的选定重量、体积和测量来源,绝不回退到入库单实测值。
|
||||
*/
|
||||
@Transactional
|
||||
public ContainerFeeRecalculationResult recalculateUnlockedContainerCustomerFees(
|
||||
long containerBookingId, long tenantId, String trigger) {
|
||||
if (containerBookingId <= 0 || tenantId <= 0 || !StringUtils.hasText(trigger)) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "柜次、客户和重算触发标识必填");
|
||||
}
|
||||
String normalizedTrigger = trigger.trim();
|
||||
String key = "CONTAINER:" + containerBookingId + ":" + normalizedTrigger;
|
||||
if (key.length() > 128) throw new BizException(ResultCode.BAD_REQUEST, "重算触发标识不能超过96字符");
|
||||
String requestHash = hash(CONTAINER_RECALCULATE_OPERATION, containerBookingId, normalizedTrigger);
|
||||
InboundPricingIdempotencyEntity existing = repository.findIdempotency(tenantId,
|
||||
AUTOMATIC_CONFIRM_OPERATOR_ID, CONTAINER_RECALCULATE_OPERATION, key);
|
||||
if (existing != null) {
|
||||
if (!requestHash.equals(existing.getRequestHash())) throw conflict("重算触发标识已用于不同柜次");
|
||||
if (!"COMPLETED".equals(existing.getStatus())) throw conflict("相同柜次费用重算正在处理中");
|
||||
return new ContainerFeeRecalculationResult(ContainerFeeRecalculationStatus.REPLAYED, 0);
|
||||
}
|
||||
|
||||
Map<Long, ContainerTierPricingMapper.MemberRow> rows = new LinkedHashMap<>();
|
||||
for (var row : repository.containerTierMembersForUpdate(containerBookingId, tenantId)) {
|
||||
rows.putIfAbsent(row.inboundId(), row);
|
||||
}
|
||||
LocalDateTime now = LocalDateTime.now(clock);
|
||||
if (rows.isEmpty()) {
|
||||
return new ContainerFeeRecalculationResult(ContainerFeeRecalculationStatus.NOT_READY, 0);
|
||||
}
|
||||
|
||||
List<ContainerFeeMember> members = new ArrayList<>();
|
||||
boolean notReady = false;
|
||||
for (var row : rows.values()) {
|
||||
InboundPricingEntity inbound = repository.findInbound(tenantId, row.inboundId());
|
||||
if (inbound == null) throw conflict("柜内入库单已变化,请刷新后重试");
|
||||
InboundFeeEntity current = repository.findLatestFeeForUpdate(tenantId, row.inboundId());
|
||||
if (current == null) {
|
||||
notReady = true;
|
||||
continue;
|
||||
}
|
||||
if ("SENT".equals(current.getFeeStatus()) || current.getCustomerConfirmedAt() != null) {
|
||||
throw conflict("柜内存在已发送或客户已确认费用,不能重算阶梯费用");
|
||||
}
|
||||
if (!List.of("CALCULATED", "CONFIRMED").contains(current.getFeeStatus())) {
|
||||
throw conflict("柜内费用状态已变化,不能重算阶梯费用");
|
||||
}
|
||||
BigDecimal weight = requiredFrozenQuantity(current.getActualWeightKg(), "重量");
|
||||
BigDecimal volume = requiredFrozenQuantity(current.getActualVolumeCbm(), "体积");
|
||||
members.add(new ContainerFeeMember(inbound, current,
|
||||
weight.setScale(3, RoundingMode.HALF_UP), volume.setScale(4, RoundingMode.HALF_UP),
|
||||
chargeableWeight(weight, volume)));
|
||||
}
|
||||
if (notReady) return new ContainerFeeRecalculationResult(ContainerFeeRecalculationStatus.NOT_READY, 0);
|
||||
InboundPricingIdempotencyEntity token = beginContainerRecalculation(
|
||||
tenantId, containerBookingId, key, normalizedTrigger);
|
||||
if ("COMPLETED".equals(token.getStatus())) {
|
||||
return new ContainerFeeRecalculationResult(ContainerFeeRecalculationStatus.REPLAYED, 0);
|
||||
}
|
||||
BigDecimal aggregate = members.stream().map(ContainerFeeMember::chargeableQuantity)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
List<ContainerTierAmountAllocator.Member> allocationMembers = members.stream()
|
||||
.map(member -> new ContainerTierAmountAllocator.Member(member.inbound().getId(), member.chargeableQuantity()))
|
||||
.toList();
|
||||
List<ContainerFeeQuote> quotes = new ArrayList<>();
|
||||
for (ContainerFeeMember member : members) {
|
||||
PricingPolicy policy = pricingPolicy(member.inbound(), now);
|
||||
ActivePriceBookEstimator.Estimate estimate = priceBookEstimator.estimate(tenantId, new EstimateRequest(
|
||||
member.inbound().getWarehouseId(), member.inbound().getChannelCode(), member.inbound().getDestinationCode(),
|
||||
destinationType(member.inbound()), member.inbound().getDestinationWarehouseId(),
|
||||
member.inbound().getDestinationName(), null, pricingTaxMode(member.inbound()), policy.chargeMode(),
|
||||
aggregate, BigDecimal.ZERO));
|
||||
quotes.add(new ContainerFeeQuote(member, policy, estimate));
|
||||
}
|
||||
boolean regionalTier = quotes.stream().anyMatch(quote -> quote.estimate().priceModel() == PriceModel.REGION_WEIGHT_TIER);
|
||||
if (!regionalTier) {
|
||||
complete(token, containerBookingId, now);
|
||||
return new ContainerFeeRecalculationResult(ContainerFeeRecalculationStatus.NOT_APPLICABLE, 0);
|
||||
}
|
||||
if (quotes.stream().anyMatch(quote -> quote.estimate().priceModel() != PriceModel.REGION_WEIGHT_TIER)) {
|
||||
throw conflict("柜内客户费用报价模型不一致,不能重算阶梯费用");
|
||||
}
|
||||
for (ContainerFeeQuote quote : quotes) {
|
||||
ContainerTierAmountAllocator.Allocation allocation = containerTierAllocator.allocate(
|
||||
allocationMembers, quote.estimate().unitPrice());
|
||||
BigDecimal subtotal = allocation.memberAmounts().get(quote.member().inbound().getId());
|
||||
if (subtotal == null) throw conflict("柜级阶梯分摊结果不完整");
|
||||
persistContainerRecalculation(tenantId, quote, subtotal, now);
|
||||
}
|
||||
complete(token, containerBookingId, now);
|
||||
return new ContainerFeeRecalculationResult(ContainerFeeRecalculationStatus.RECALCULATED, quotes.size());
|
||||
}
|
||||
|
||||
private void persistContainerRecalculation(long tenantId, ContainerFeeQuote quote,
|
||||
BigDecimal subtotal, LocalDateTime now) {
|
||||
ContainerFeeMember member = quote.member();
|
||||
ActivePriceBookEstimator.Estimate estimate = quote.estimate();
|
||||
RateRow rate = estimate.rate();
|
||||
int version = repository.nextFeeVersion(tenantId, member.inbound().getId());
|
||||
BigDecimal additional = rate.getAdditionalFee() == null ? BigDecimal.ZERO
|
||||
: rate.getAdditionalFee().setScale(MONEY_SCALE, RoundingMode.HALF_UP);
|
||||
InboundFeeEntity recalculated = snapshot(tenantId, AUTOMATIC_CONFIRM_OPERATOR_ID, member.inbound(), estimate,
|
||||
rate, version, member.weight(), member.volume(), member.chargeableQuantity(),
|
||||
member.chargeableQuantity(), subtotal.setScale(MONEY_SCALE, RoundingMode.HALF_UP), additional,
|
||||
subtotal.add(additional).setScale(MONEY_SCALE, RoundingMode.HALF_UP), now);
|
||||
PricingPolicy policy = quote.policy();
|
||||
if (policy.rule() != null) {
|
||||
recalculated.setChargeRuleId(policy.rule().getId());
|
||||
recalculated.setChargeRuleVersion(policy.rule().getRuleVersion());
|
||||
recalculated.setRuleCode(policy.rule().getRuleCode());
|
||||
recalculated.setRuleEffectiveFrom(policy.rule().getEffectiveFrom());
|
||||
recalculated.setRuleEffectiveTo(policy.rule().getEffectiveTo());
|
||||
}
|
||||
recalculated.setMeasurementId(member.current().getMeasurementId());
|
||||
recalculated.setMeasurementSource(member.current().getMeasurementSource());
|
||||
if (repository.supersedeCurrent(tenantId, member.current().getId(), member.current().getFeeStatus()) != 1) {
|
||||
throw conflict("柜内费用已变化,不能重算阶梯费用");
|
||||
}
|
||||
repository.insertFee(recalculated);
|
||||
repository.insertItem(baseItem(tenantId, recalculated, rate, member.chargeableQuantity(),
|
||||
recalculated.getSubtotalAmount(), now));
|
||||
if (additional.signum() > 0) repository.insertItem(additionalItem(tenantId, recalculated, additional, now));
|
||||
if (repository.markInboundContainerFeeRecalculated(tenantId, member.inbound().getId(),
|
||||
member.inbound().getProcessStatus(), estimate.chargeUnit(), member.chargeableQuantity()) != 1) {
|
||||
throw conflict("柜内入库单状态已变化,不能重算阶梯费用");
|
||||
}
|
||||
feeDetails.materializeCalculatedFreight(tenantId, member.inbound().getId(), recalculated.getId(),
|
||||
AUTOMATIC_CONFIRM_OPERATOR_ID, now);
|
||||
}
|
||||
|
||||
private InboundPricingIdempotencyEntity beginContainerRecalculation(long tenantId, long containerBookingId,
|
||||
String key, String trigger) {
|
||||
String requestHash = hash(CONTAINER_RECALCULATE_OPERATION, containerBookingId, trigger);
|
||||
InboundPricingIdempotencyEntity existing = repository.findIdempotency(tenantId,
|
||||
AUTOMATIC_CONFIRM_OPERATOR_ID, CONTAINER_RECALCULATE_OPERATION, key);
|
||||
if (existing != null) {
|
||||
if (!requestHash.equals(existing.getRequestHash())) throw conflict("重算触发标识已用于不同柜次");
|
||||
if (!"COMPLETED".equals(existing.getStatus())) throw conflict("相同柜次费用重算正在处理中");
|
||||
return existing;
|
||||
}
|
||||
InboundPricingIdempotencyEntity token = new InboundPricingIdempotencyEntity();
|
||||
token.setTenantId(tenantId); token.setOperation(CONTAINER_RECALCULATE_OPERATION);
|
||||
token.setOperatorId(AUTOMATIC_CONFIRM_OPERATOR_ID); token.setInboundId(0L);
|
||||
token.setIdempotencyKey(key); token.setRequestHash(requestHash); token.setStatus("PROCESSING");
|
||||
token.setCreatedAt(LocalDateTime.now(clock)); token.setUpdatedAt(LocalDateTime.now(clock));
|
||||
repository.insertIdempotency(token);
|
||||
return token;
|
||||
}
|
||||
|
||||
private static BigDecimal requiredFrozenQuantity(BigDecimal value, String field) {
|
||||
if (value == null || value.signum() < 0) throw conflict("费用快照缺少冻结" + field + ",不能重算阶梯费用");
|
||||
return value;
|
||||
}
|
||||
|
||||
private record ContainerFeeMember(InboundPricingEntity inbound, InboundFeeEntity current,
|
||||
BigDecimal weight, BigDecimal volume, BigDecimal chargeableQuantity) { }
|
||||
|
||||
private record ContainerFeeQuote(ContainerFeeMember member, PricingPolicy policy,
|
||||
ActivePriceBookEstimator.Estimate estimate) { }
|
||||
|
||||
public enum ContainerFeeRecalculationStatus { RECALCULATED, NOT_READY, NOT_APPLICABLE, REPLAYED }
|
||||
|
||||
public record ContainerFeeRecalculationResult(ContainerFeeRecalculationStatus status,
|
||||
int recalculatedCount) { }
|
||||
|
||||
@Transactional
|
||||
public FeeSnapshotResponse confirm(long tenantId, long operatorId, long inboundId,
|
||||
String idempotencyKey) {
|
||||
|
||||
@@ -748,12 +748,129 @@ class FbaPricingServiceTest {
|
||||
void scopesContainerTierMembersByCustomerAndContainer() throws NoSuchMethodException {
|
||||
String sql = ContainerTierPricingMapper.class.getMethod("members", long.class, long.class)
|
||||
.getAnnotation(Select.class).value()[0];
|
||||
String lockedSql = ContainerTierPricingMapper.class.getMethod("membersForUpdate", long.class, long.class)
|
||||
.getAnnotation(Select.class).value()[0];
|
||||
|
||||
assertThat(sql).contains("plan.container_booking_id=current_plan.container_booking_id")
|
||||
.contains("grouped_item.account_id=#{customerId}")
|
||||
.contains("grouped_inbound.tenant_id=#{customerId}")
|
||||
.doesNotContain("grouped_inbound.destination_code")
|
||||
.doesNotContain("grouped_inbound.destination_warehouse_id");
|
||||
assertThat(lockedSql).contains("plan.container_booking_id=#{containerBookingId}")
|
||||
.contains("item.account_id=#{customerId}").contains("FOR UPDATE");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("柜内客户费用重算复用冻结选量和来源,并按统一阶梯分摊")
|
||||
void recalculatesUnlockedContainerFeesFromFrozenSelections() {
|
||||
readyRepository();
|
||||
InboundPricingEntity firstInbound = inbound("999", "9");
|
||||
InboundPricingEntity secondInbound = inbound("888", "8");
|
||||
secondInbound.setId(30002L); secondInbound.setInboundCode("RK-002");
|
||||
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");
|
||||
second.setId(502L); second.setInboundId(30002L); second.setActualWeightKg(new BigDecimal("110.000"));
|
||||
second.setActualVolumeCbm(new BigDecimal("0.1000")); second.setMeasurementId(302L);
|
||||
second.setMeasurementSource("ACTUAL");
|
||||
when(repository.containerTierMembersForUpdate(901L, TENANT)).thenReturn(List.of(
|
||||
memberRow(901L, INBOUND, "999", "9"), memberRow(901L, 30002L, "888", "8")
|
||||
));
|
||||
when(repository.findInbound(TENANT, INBOUND)).thenReturn(firstInbound);
|
||||
when(repository.findInbound(TENANT, 30002L)).thenReturn(secondInbound);
|
||||
when(repository.findLatestFeeForUpdate(TENANT, INBOUND)).thenReturn(first);
|
||||
when(repository.findLatestFeeForUpdate(TENANT, 30002L)).thenReturn(second);
|
||||
when(repository.nextFeeVersion(TENANT, INBOUND)).thenReturn(2);
|
||||
when(repository.nextFeeVersion(TENANT, 30002L)).thenReturn(2);
|
||||
when(repository.completeIdempotency(any(), eq(901L), eq(NOW))).thenReturn(1);
|
||||
when(repository.supersedeCurrent(eq(TENANT), anyLong(), anyString())).thenReturn(1);
|
||||
when(repository.markInboundContainerFeeRecalculated(eq(TENANT), anyLong(), anyString(), anyString(), any()))
|
||||
.thenReturn(1);
|
||||
when(priceBookEstimator.estimate(eq(TENANT), any())).thenReturn(
|
||||
regionEstimate("200", "8", "1600"), regionEstimate("200", "8", "1600"));
|
||||
when(containerTierAllocator.allocate(any(), eq(new BigDecimal("8")))).thenReturn(
|
||||
new ContainerTierAmountAllocator.Allocation(new BigDecimal("200"), new BigDecimal("1600"),
|
||||
Map.of(INBOUND, new BigDecimal("720.00"), 30002L, new BigDecimal("880.00"))));
|
||||
|
||||
service.recalculateUnlockedContainerCustomerFees(901L, TENANT, "loading-change-1");
|
||||
|
||||
ArgumentCaptor<InboundFeeEntity> fees = ArgumentCaptor.forClass(InboundFeeEntity.class);
|
||||
verify(repository, times(2)).insertFee(fees.capture());
|
||||
assertThat(fees.getAllValues().get(0).getActualWeightKg()).isEqualByComparingTo("90.000");
|
||||
assertThat(fees.getAllValues().get(0).getActualVolumeCbm()).isEqualByComparingTo("0.1000");
|
||||
assertThat(fees.getAllValues().get(0).getMeasurementId()).isEqualTo(301L);
|
||||
assertThat(fees.getAllValues().get(0).getMeasurementSource()).isEqualTo("OUTER");
|
||||
assertThat(fees.getAllValues().get(0).getSubtotalAmount()).isEqualByComparingTo("720.00");
|
||||
assertThat(fees.getAllValues().get(1).getActualWeightKg()).isEqualByComparingTo("110.000");
|
||||
assertThat(fees.getAllValues().get(1).getMeasurementId()).isEqualTo(302L);
|
||||
assertThat(fees.getAllValues().get(1).getMeasurementSource()).isEqualTo("ACTUAL");
|
||||
ArgumentCaptor<com.baiyue.firstleg.dto.PriceBookDtos.EstimateRequest> requests =
|
||||
ArgumentCaptor.forClass(com.baiyue.firstleg.dto.PriceBookDtos.EstimateRequest.class);
|
||||
verify(priceBookEstimator, times(2)).estimate(eq(TENANT), requests.capture());
|
||||
assertThat(requests.getAllValues()).allSatisfy(request -> {
|
||||
assertThat(request.actualWeightKg()).isEqualByComparingTo("200");
|
||||
assertThat(request.actualVolumeCbm()).isEqualByComparingTo(BigDecimal.ZERO);
|
||||
});
|
||||
verify(financeOutbox, never()).publishInbound(any(), anyLong(), anyLong(), anyLong(), anyInt(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("柜内任一已发送或客户确认费用阻断整组重算")
|
||||
void rejectsContainerRecalculationWhenAnyFeeIsImmutable() {
|
||||
readyRepository();
|
||||
InboundPricingEntity inbound = inbound("100", "0.1");
|
||||
InboundFeeEntity sent = savedFee("SENT");
|
||||
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);
|
||||
|
||||
assertThatThrownBy(() -> service.recalculateUnlockedContainerCustomerFees(901L, TENANT, "loading-change-2"))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("已发送或客户已确认");
|
||||
|
||||
verify(repository, never()).supersedeCurrent(anyLong(), anyLong(), anyString());
|
||||
verify(repository, never()).insertFee(any());
|
||||
verify(priceBookEstimator, never()).estimate(anyLong(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("待审核或待测量成员无初算费用时保持占位并返回未就绪")
|
||||
void keepsContainerRelationshipWhenAnyMemberFeeIsNotReady() {
|
||||
readyRepository();
|
||||
InboundPricingEntity pricedInbound = inbound("100", "0.1");
|
||||
InboundPricingEntity pendingInbound = inbound("80", "0.1");
|
||||
pendingInbound.setId(30002L); pendingInbound.setInboundCode("RK-PENDING");
|
||||
InboundFeeEntity priced = savedFee("CALCULATED");
|
||||
priced.setActualWeightKg(new BigDecimal("100")); priced.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(pricedInbound);
|
||||
when(repository.findInbound(TENANT, 30002L)).thenReturn(pendingInbound);
|
||||
when(repository.findLatestFeeForUpdate(TENANT, INBOUND)).thenReturn(priced);
|
||||
when(repository.findLatestFeeForUpdate(TENANT, 30002L)).thenReturn(null);
|
||||
|
||||
var result = service.recalculateUnlockedContainerCustomerFees(901L, TENANT, "pending-member");
|
||||
|
||||
assertThat(result.status()).isEqualTo(FbaPricingService.ContainerFeeRecalculationStatus.NOT_READY);
|
||||
assertThat(result.recalculatedCount()).isZero();
|
||||
verify(repository, never()).insertFee(any());
|
||||
verify(priceBookEstimator, never()).estimate(anyLong(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("相同柜次重算触发标识重放不重复创建费用版本")
|
||||
void replaysCompletedContainerRecalculationTrigger() {
|
||||
InboundPricingIdempotencyEntity completed = completedContainerRecalculation(901L, "same-trigger");
|
||||
when(repository.findIdempotency(TENANT, 0L, "CONTAINER_FEE_RECALCULATE", "CONTAINER:901:same-trigger"))
|
||||
.thenReturn(completed);
|
||||
|
||||
service.recalculateUnlockedContainerCustomerFees(901L, TENANT, "same-trigger");
|
||||
|
||||
verify(repository, never()).containerTierMembersForUpdate(anyLong(), anyLong());
|
||||
verify(repository, never()).insertFee(any());
|
||||
}
|
||||
|
||||
private void readyRepository() {
|
||||
@@ -834,6 +951,20 @@ class FbaPricingServiceTest {
|
||||
return value;
|
||||
}
|
||||
|
||||
private InboundPricingIdempotencyEntity completedContainerRecalculation(long containerBookingId, String trigger) {
|
||||
InboundPricingIdempotencyEntity value = new InboundPricingIdempotencyEntity();
|
||||
value.setId(900L); value.setOperation("CONTAINER_FEE_RECALCULATE"); value.setStatus("COMPLETED");
|
||||
value.setResultResourceId(containerBookingId);
|
||||
try {
|
||||
value.setRequestHash(java.util.HexFormat.of().formatHex(java.security.MessageDigest.getInstance("SHA-256")
|
||||
.digest(("CONTAINER_FEE_RECALCULATE:" + containerBookingId + ':' + trigger)
|
||||
.getBytes(java.nio.charset.StandardCharsets.UTF_8))));
|
||||
} catch (java.security.NoSuchAlgorithmException exception) {
|
||||
throw new AssertionError(exception);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private InboundFeeEntity savedFee(String status) {
|
||||
InboundFeeEntity value = new InboundFeeEntity();
|
||||
value.setId(501L); value.setTenantId(TENANT); value.setInboundId(INBOUND); value.setFeeNo("FBAF-30001-1");
|
||||
|
||||
Reference in New Issue
Block a user