diff --git a/firstleg/src/main/java/com/baiyue/firstleg/repository/FbaPricingRepository.java b/firstleg/src/main/java/com/baiyue/firstleg/repository/FbaPricingRepository.java index 73e311255..dbeba115d 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/repository/FbaPricingRepository.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/repository/FbaPricingRepository.java @@ -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() .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")); } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/FbaPricingService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/FbaPricingService.java index 50918fc23..96c152a84 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/FbaPricingService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/FbaPricingService.java @@ -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) { diff --git a/firstleg/src/test/java/com/baiyue/firstleg/repository/FbaPricingRepositoryRuleListTest.java b/firstleg/src/test/java/com/baiyue/firstleg/repository/FbaPricingRepositoryRuleListTest.java index 48b879b7d..48004d23f 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/repository/FbaPricingRepositoryRuleListTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/repository/FbaPricingRepositoryRuleListTest.java @@ -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 queryValues(Long tenantId, String ruleCode) { diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/FbaPricingServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/FbaPricingServiceTest.java index aa4034c48..d46350cfe 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/FbaPricingServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/FbaPricingServiceTest.java @@ -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() {