Merge remote-tracking branch 'origin/test' into dev

This commit is contained in:
2026-09-04 10:37:19 +08:00
4 changed files with 77 additions and 9 deletions
@@ -294,9 +294,9 @@ public class FbaPricingRepository {
.eq(InboundPricingEntity::getTenantId, tenantId)
.eq(InboundPricingEntity::getId, inboundId)
.eq(InboundPricingEntity::getProcessStatus, processStatus)
.in(InboundPricingEntity::getFeeStatus, List.of("UNPRICED", "CALCULATED"))
.in(InboundPricingEntity::getFeeStatus, List.of("UNPRICED", "CALCULATED", "SENT"))
.set(InboundPricingEntity::getFeeStatus, "CALCULATED");
if (!"WAIT_SPLIT".equals(processStatus)) {
if (!List.of("WAIT_PALLET", "WAIT_SPLIT", "LOADING").contains(processStatus)) {
update.set(InboundPricingEntity::getProcessStatus, "FEE_CONFIRMING");
}
if ("KG".equals(chargeMode)) update.set(InboundPricingEntity::getChargeableWeightKg, chargeable);
@@ -317,7 +317,7 @@ public class FbaPricingRepository {
return inbounds.update(null, new LambdaUpdateWrapper<InboundPricingEntity>()
.eq(InboundPricingEntity::getTenantId, tenantId).eq(InboundPricingEntity::getId, inboundId)
.in(InboundPricingEntity::getProcessStatus,
List.of("FEE_CONFIRMING", "WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED"))
List.of("FEE_CONFIRMING", "WAIT_PALLET", "WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED"))
.eq(InboundPricingEntity::getFeeStatus, "CALCULATED")
.set(InboundPricingEntity::getFeeStatus, "CONFIRMED"));
}
@@ -80,6 +80,16 @@ public class FbaPricingService {
if ("COMPLETED".equals(token.getStatus())) return response(tenantId, token.getResultResourceId(), null);
InboundPricingEntity inbound = requireEligibleInbound(tenantId, inboundId);
InboundFeeEntity published = null;
if ("SENT".equals(inbound.getFeeStatus())) {
published = repository.findLatestFeeForUpdate(tenantId, inboundId);
if (published == null || !"SENT".equals(published.getFeeStatus())) {
throw conflict("费用状态已变化,请刷新后重试");
}
if (published.getCustomerConfirmedAt() != null) {
throw conflict("客户已确认该费用,不允许再修改或重新发送");
}
}
var measurement = measurementRepository.findLatestMeasurement(tenantId, inboundId);
if ("OUTER".equalsIgnoreCase(volumeSource) && measurement == null) {
throw conflict("外箱规测量批次不存在,不能生成对客费用");
@@ -114,7 +124,13 @@ public class FbaPricingService {
weight, volume, converted, chargeable, subtotal, additional, total, now);
fee.setMeasurementId(measurement == null ? null : measurement.getId());
fee.setMeasurementSource("OUTER".equalsIgnoreCase(volumeSource) ? "OUTER" : volumeSource);
repository.supersedeCalculated(tenantId, inboundId);
if (published != null) {
if (repository.supersedeCurrent(tenantId, published.getId(), "SENT") != 1) {
throw conflict("费用已被客户确认或其他操作员修改,请刷新后重试");
}
} else {
repository.supersedeCalculated(tenantId, inboundId);
}
repository.insertFee(fee);
repository.insertItem(baseItem(tenantId, fee, rate, chargeable, subtotal, now));
if (additional.signum() > 0) repository.insertItem(additionalItem(tenantId, fee, additional, now));
@@ -124,7 +140,7 @@ public class FbaPricingService {
}
feeDetails.materializeCalculatedFreight(tenantId, inboundId, fee.getId(), operatorId, now);
complete(token, fee.getId(), now);
String nextStatus = isCompletedInbound(inbound.getProcessStatus())
String nextStatus = preserveProcessStatusDuringPricing(inbound.getProcessStatus())
? inbound.getProcessStatus() : "FEE_CONFIRMING";
return response(inbound, fee, rate.getUnitPrice(), nextStatus);
}
@@ -163,7 +179,7 @@ public class FbaPricingService {
String hash = hash("SEND", inboundId, current.getId().toString());
InboundPricingIdempotencyEntity token = beginIdempotency(
tenantId, operatorId, inboundId, "FEE_SEND", idempotencyKey, hash);
String nextProcessStatus = isCompletedInbound(inbound.getProcessStatus())
String nextProcessStatus = preserveProcessStatusDuringPricing(inbound.getProcessStatus())
? inbound.getProcessStatus() : "WAIT_PALLET";
if ("COMPLETED".equals(token.getStatus())) {
feeDetails.publishFreight(tenantId, inboundId, token.getResultResourceId(),
@@ -311,7 +327,7 @@ public class FbaPricingService {
private InboundPricingEntity requireEligibleInbound(long tenantId, long inboundId) {
InboundPricingEntity inbound = requireApprovedInbound(tenantId, inboundId);
if (!feeEditableBeforeLoading(inbound.getProcessStatus())
|| !List.of("UNPRICED", "CALCULATED").contains(inbound.getFeeStatus())) {
|| !List.of("UNPRICED", "CALCULATED", "SENT").contains(inbound.getFeeStatus())) {
throw conflict("当前入库单状态不允许费用试算");
}
return inbound;
@@ -336,9 +352,13 @@ public class FbaPricingService {
return "WAIT_SPLIT".equals(processStatus);
}
private static boolean preserveProcessStatusDuringPricing(String processStatus) {
return List.of("WAIT_PALLET", "WAIT_SPLIT", "LOADING").contains(processStatus);
}
private static boolean feeEditableBeforeLoading(String processStatus) {
return List.of("WAIT_MEASURE", "MEASURING", "MEASURE_PENDING", "MEASURE_REJECTED",
"WAIT_PRICING", "FEE_CONFIRMING", "WAIT_SPLIT").contains(processStatus);
"WAIT_PRICING", "FEE_CONFIRMING", "WAIT_PALLET", "WAIT_SPLIT", "LOADING").contains(processStatus);
}
private static PricingDestinationType destinationType(InboundPricingEntity inbound) {
@@ -198,7 +198,7 @@ class FbaPricingRepositoryRuleListTest {
throw new AssertionError(exception);
}
assertThat(source)
.contains("List.of(\"FEE_CONFIRMING\", \"WAIT_SPLIT\", \"LOADING\", \"LOADED\", \"DELIVERED\")");
.contains("List.of(\"FEE_CONFIRMING\", \"WAIT_PALLET\", \"WAIT_SPLIT\", \"LOADING\", \"LOADED\", \"DELIVERED\")");
}
private Collection<Object> queryValues(Long tenantId, String ruleCode) {
@@ -242,6 +242,54 @@ class FbaPricingServiceTest {
verify(priceBookEstimator).estimate(any());
}
@Test
@DisplayName("客户未确认时待装柜订单可重新计算并替换已发送费用")
void recalculatesSentFeeBeforeCustomerConfirmation() {
readyRepository();
InboundPricingEntity inbound = inbound("10", "1");
inbound.setProcessStatus("WAIT_PALLET");
inbound.setFeeStatus("SENT");
InboundFeeEntity sent = savedFee("SENT");
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
when(repository.findLatestFeeForUpdate(TENANT, INBOUND)).thenReturn(sent);
when(repository.nextFeeVersion(TENANT, INBOUND)).thenReturn(2);
when(repository.supersedeCurrent(TENANT, 501L, "SENT")).thenReturn(1);
when(priceBookEstimator.estimate(any())).thenReturn(
estimate("CBM", "1.0000", "10", "10", "0"));
FeeSnapshotResponse response = service.calculate(
TENANT, OPERATOR, INBOUND, NOW, "resend-before-confirm");
assertThat(response.feeVersion()).isEqualTo(2);
assertThat(response.feeStatus()).isEqualTo("CALCULATED");
assertThat(response.nextProcessStatus()).isEqualTo("WAIT_PALLET");
verify(repository).supersedeCurrent(TENANT, 501L, "SENT");
verify(repository).markInboundCalculated(TENANT, INBOUND, "WAIT_PALLET", "CBM",
new BigDecimal("1.0000"));
}
@Test
@DisplayName("客户已确认时拒绝重新计算已发送费用")
void rejectsRecalculationAfterCustomerConfirmation() {
readyRepository();
InboundPricingEntity inbound = inbound("10", "1");
inbound.setProcessStatus("WAIT_PALLET");
inbound.setFeeStatus("SENT");
InboundFeeEntity sent = savedFee("SENT");
sent.setCustomerConfirmedAt(NOW);
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
when(repository.findLatestFeeForUpdate(TENANT, INBOUND)).thenReturn(sent);
assertThatThrownBy(() -> service.calculate(
TENANT, OPERATOR, INBOUND, NOW, "resend-after-confirm"))
.isInstanceOf(BizException.class)
.hasMessageContaining("客户已确认");
verify(priceBookEstimator, never()).estimate(any());
verify(repository, never()).supersedeCurrent(anyLong(), anyLong(), any());
verify(repository, never()).insertFee(any());
}
@Test
@DisplayName("当前生效价格表无匹配价格时明确拒绝")
void rejectsMissingActivePriceBookRate() {