feat(fleet): enforce reviewed trips and original fee coverage for dispatch
This commit is contained in:
+80
@@ -0,0 +1,80 @@
|
||||
package com.baiyue.lastleg.financefee.state;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baiyue.common.event.BusinessFeeSourceChangedV1;
|
||||
import com.baiyue.common.event.EventEnvelope;
|
||||
import com.baiyue.common.event.EventTypes;
|
||||
import com.baiyue.lastleg.entity.LastlegEventOutbox;
|
||||
import com.baiyue.lastleg.financefee.entity.OverseasFeeSource;
|
||||
import com.baiyue.lastleg.financefee.mapper.OverseasFeeSourceMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegEventOutboxMapper;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/** A request is not a financial void. Only the matched durable consumer ACK confirms it. */
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class OverseasFeeCancellation {
|
||||
private final LastlegEventOutboxMapper events;
|
||||
private final OverseasFeeSourceMapper fees;
|
||||
private final ObjectMapper json;
|
||||
|
||||
public static String eventKey(OverseasFeeSource fee, String action) {
|
||||
return EventTypes.BUSINESS_FEE_SOURCE_CHANGED + ":LASTLEG:" + fee.getSourceType() + ":"
|
||||
+ fee.getSourceFeeId() + ":" + fee.getSourceVersion() + ":" + action;
|
||||
}
|
||||
public boolean requested(OverseasFeeSource fee) { return events.findByEventKey(eventKey(fee, "VOIDED")) != null; }
|
||||
|
||||
public List<OverseasFeeSource> describe(List<OverseasFeeSource> values) {
|
||||
if (values.isEmpty()) return values;
|
||||
var keys = values.stream().flatMap(value -> java.util.stream.Stream.of(eventKey(value, "VOIDED"),
|
||||
eventKey(value, value.getSourceVersion() == 1 ? "PUBLISHED" : "ADJUSTED"))).toList();
|
||||
var byKey = events.selectList(new LambdaQueryWrapper<LastlegEventOutbox>().in(LastlegEventOutbox::getEventKey, keys))
|
||||
.stream().collect(Collectors.toMap(LastlegEventOutbox::getEventKey, row -> row));
|
||||
for (var value : values) {
|
||||
var request = byKey.get(eventKey(value, "VOIDED"));
|
||||
var publish = byKey.get(eventKey(value, value.getSourceVersion() == 1 ? "PUBLISHED" : "ADJUSTED"));
|
||||
value.setVoidStatus("VOIDED".equals(value.getSourceStatus()) ? "CONFIRMED" : request == null ? "NOT_REQUESTED"
|
||||
: "DEAD".equals(request.getStatus()) || publish == null || "DEAD".equals(publish.getStatus())
|
||||
? "FINANCE_ATTENTION" : "PENDING_FINANCE");
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
public boolean prerequisitePublished(LastlegEventOutbox request) {
|
||||
if (request.getEventKey() == null || !request.getEventKey().endsWith(":VOIDED")) return true;
|
||||
String prefix = request.getEventKey().substring(0, request.getEventKey().length() - "VOIDED".length());
|
||||
return events.selectCount(new LambdaQueryWrapper<LastlegEventOutbox>()
|
||||
.in(LastlegEventOutbox::getEventKey, List.of(prefix + "PUBLISHED", prefix + "ADJUSTED"))
|
||||
.eq(LastlegEventOutbox::getStatus, "PUBLISHED")) > 0;
|
||||
}
|
||||
|
||||
/** Invoked inside the same transaction as the lease-owner CAS and delivery journal completion. */
|
||||
@Transactional(propagation = Propagation.MANDATORY)
|
||||
public void acknowledge(LastlegEventOutbox event) {
|
||||
if (event.getEventKey() == null || !event.getEventKey().endsWith(":VOIDED")) return;
|
||||
try {
|
||||
var envelope = json.readValue(event.getPayloadJson(), EventEnvelope.class).requireValid();
|
||||
var payload = json.treeToValue(envelope.payload(), BusinessFeeSourceChangedV1.class);
|
||||
if (!"VOIDED".equals(payload.action()) || !"LASTLEG".equals(payload.sourceSystem()))
|
||||
throw new IllegalStateException("费用作废事件与来源不一致");
|
||||
var source = fees.lockCurrent(Long.parseLong(payload.sourceFeeId()));
|
||||
if (source == null || !Objects.equals(source.getSourceVersion(), payload.sourceVersion())
|
||||
|| !Objects.equals(source.getCustomerId(), payload.customerId())
|
||||
|| !Objects.equals(source.getSourceType(), payload.sourceType())
|
||||
|| !eventKey(source, "VOIDED").equals(event.getEventKey()) || !"ACTIVE".equals(source.getSourceStatus()))
|
||||
throw new IllegalStateException("财务已确认的作废来源与当前业务版本不一致");
|
||||
source.setSourceStatus("VOIDED"); source.setUpdatedAt(LocalDateTime.now(ZoneOffset.UTC));
|
||||
if (fees.updateById(source) != 1) throw new IllegalStateException("费用作废确认未能落账");
|
||||
} catch (com.fasterxml.jackson.core.JsonProcessingException error) {
|
||||
throw new IllegalStateException("费用作废事件无法解析", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user