fix(acceptance): enforce immutable confirmed fees
This commit is contained in:
+11
@@ -222,6 +222,7 @@ public class CarrierScheduleQueryService {
|
||||
}
|
||||
CarrierScheduleQueryEntity value = requiredLockedVersion(id, request.expectedVersion());
|
||||
CarrierScheduleNode node = CarrierScheduleNode.require(request.standardNode());
|
||||
requireManualActualAdvance(value, node, request.actualAt());
|
||||
CarrierScheduleQueryEntity before = copy(value);
|
||||
String providerEventId = sha256("MANUAL|" + id + '|' + key);
|
||||
CarrierScheduleNodeFactEntity fact = addNode(value, "MANUAL", providerEventId, node.name(), node,
|
||||
@@ -306,6 +307,16 @@ public class CarrierScheduleQueryService {
|
||||
return false;
|
||||
}
|
||||
|
||||
private void requireManualActualAdvance(CarrierScheduleQueryEntity value, CarrierScheduleNode node,
|
||||
LocalDateTime actualAt) {
|
||||
if (actualAt == null) return;
|
||||
CarrierScheduleNode current = value.getLatestStandardNode() == null ? null
|
||||
: CarrierScheduleNode.require(value.getLatestStandardNode());
|
||||
if ("STOPPED".equals(value.getStatus()) || current != null && node.sequence() <= current.sequence()) {
|
||||
throw conflict("实际船节点必须按业务顺序向后推进");
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasOutboundDepartureFact(long customerId, long oceanBookingId, long containerBookingId) {
|
||||
CarrierScheduleQueryEntity query = repository.findByBusinessScope(customerId, oceanBookingId,
|
||||
containerBookingId);
|
||||
|
||||
@@ -98,8 +98,8 @@ public class FbaPricingService {
|
||||
if (published == null || !"SENT".equals(published.getFeeStatus())) {
|
||||
throw conflict("费用状态已变化,请刷新后重试");
|
||||
}
|
||||
if (feeRevisionLocked(inbound, published)) {
|
||||
throw conflict("客户已确认且该票已装柜完成,不允许再修改或重新发送费用");
|
||||
if (feeRevisionLocked(published)) {
|
||||
throw conflict("客户已确认,不允许再修改或重新发送费用");
|
||||
}
|
||||
}
|
||||
var measurement = measurementRepository.findLatestMeasurement(tenantId, inboundId);
|
||||
@@ -490,8 +490,8 @@ public class FbaPricingService {
|
||||
if (current == null || !List.of("CALCULATED", "CONFIRMED", "SENT").contains(current.getFeeStatus())) {
|
||||
throw conflict("入库单没有可修改的初算或已发布费用");
|
||||
}
|
||||
if (feeRevisionLocked(inbound, current)) {
|
||||
throw conflict("客户已确认且该票已装柜完成,不允许再修改费用");
|
||||
if (feeRevisionLocked(current)) {
|
||||
throw conflict("客户已确认,不允许再修改费用");
|
||||
}
|
||||
if (!request.expectedFeeVersion().equals(current.getFeeVersion())) {
|
||||
throw conflict("费用版本已变化,请刷新后重试");
|
||||
@@ -613,9 +613,8 @@ public class FbaPricingService {
|
||||
.contains(processStatus);
|
||||
}
|
||||
|
||||
private static boolean feeRevisionLocked(InboundPricingEntity inbound, InboundFeeEntity fee) {
|
||||
return fee != null && fee.getCustomerConfirmedAt() != null
|
||||
&& List.of("LOADED", "DELIVERED").contains(inbound.getProcessStatus());
|
||||
private static boolean feeRevisionLocked(InboundFeeEntity fee) {
|
||||
return fee != null && fee.getCustomerConfirmedAt() != null;
|
||||
}
|
||||
|
||||
private static PricingDestinationType destinationType(InboundPricingEntity inbound) {
|
||||
|
||||
+20
@@ -474,6 +474,26 @@ class CarrierScheduleQueryServiceTest {
|
||||
verify(repository).updateState(before, 3, 7L, "admin");
|
||||
}
|
||||
|
||||
@Test
|
||||
void manualActualFactRejectsCurrentOrEarlierBusinessNodeBeforeWritingFact() {
|
||||
CarrierScheduleQueryEntity task = task(0, 3);
|
||||
task.setStatus("READY"); task.setLatestStandardNode("AT_ORIGIN_PORT");
|
||||
task.setLatestNodeObservedAt(NOW.minusHours(1));
|
||||
when(repository.lock(1L)).thenReturn(task);
|
||||
|
||||
assertThatThrownBy(() -> service.addManualNode(1L, 7L, "admin", "manual-same-1",
|
||||
new ManualNodeRequest(3, "AT_ORIGIN_PORT", NOW, null, NOW,
|
||||
"CNSZX", "深圳", "重复当前节点")))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("按业务顺序向后推进");
|
||||
assertThatThrownBy(() -> service.addManualNode(1L, 7L, "admin", "manual-earlier-1",
|
||||
new ManualNodeRequest(3, "DEPARTED_WAREHOUSE", NOW, null, NOW,
|
||||
"CNSZX", "深圳", "补录更早节点")))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("按业务顺序向后推进");
|
||||
|
||||
verify(repository, never()).insertIfAbsent(any(CarrierScheduleNodeFactEntity.class));
|
||||
verify(repository, never()).updateState(any(), any(Integer.class), anyLong(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void holdIsPersistedAsSeparateImmutableExceptionFact() {
|
||||
CarrierScheduleQueryEntity task = task(0, 1);
|
||||
|
||||
@@ -461,7 +461,6 @@ class FbaPricingServiceTest {
|
||||
inbound.setProcessStatus("WAIT_SPLIT");
|
||||
inbound.setFeeStatus("SENT");
|
||||
InboundFeeEntity current = savedFee("SENT");
|
||||
current.setCustomerConfirmedAt(NOW.minusHours(1));
|
||||
AtomicReference<InboundFeeEntity> inserted = new AtomicReference<>();
|
||||
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
|
||||
when(repository.findLatestFee(TENANT, INBOUND)).thenReturn(current);
|
||||
@@ -531,10 +530,10 @@ class FbaPricingServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("客户已确认且装柜完成时拒绝平台再次修改")
|
||||
@DisplayName("客户确认后即拒绝平台再次修改")
|
||||
void rejectsAdjustmentAfterCustomerConfirmation() {
|
||||
InboundPricingEntity inbound = inbound("10", "1");
|
||||
inbound.setProcessStatus("LOADED");
|
||||
inbound.setProcessStatus("WAIT_SPLIT");
|
||||
InboundFeeEntity current = savedFee("SENT");
|
||||
current.setCustomerConfirmedAt(NOW);
|
||||
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
|
||||
|
||||
Reference in New Issue
Block a user