feat(procurement): implement sample fulfillment and settled fee workflow
This commit is contained in:
+2
@@ -43,6 +43,8 @@ public class OverseasFeeSource {
|
||||
private String paymentMethod;
|
||||
private String paymentReferenceNo;
|
||||
@TableField(typeHandler = JacksonTypeHandler.class) private List<String> paymentVoucherFileIds;
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
private com.baiyue.common.event.WalletSettlementReference walletSettlement;
|
||||
private LocalDate businessDate;
|
||||
private String currency;
|
||||
private BigDecimal amount;
|
||||
|
||||
+1
@@ -9,5 +9,6 @@ import org.apache.ibatis.annotations.Select;
|
||||
@Mapper
|
||||
public interface OverseasFeeSourceMapper extends BaseMapper<OverseasFeeSource> {
|
||||
@Select("SELECT * FROM ll_finance_fee_source WHERE source_fee_id=#{sourceFeeId} ORDER BY source_version DESC LIMIT 1 FOR UPDATE")
|
||||
@org.apache.ibatis.annotations.ResultMap("mybatis-plus_OverseasFeeSource")
|
||||
OverseasFeeSource lockCurrent(@Param("sourceFeeId") long sourceFeeId);
|
||||
}
|
||||
|
||||
+1
-1
@@ -69,7 +69,7 @@ public class OverseasFeeOutboxService {
|
||||
value.getOriginalCurrency(), value.getOriginalAmount(), value.getExchangeRate(), value.getExchangeRateMonth(),
|
||||
value.getChargeableQuantity(), value.getQuantityUnit(), value.getUnitPrice(), value.getPeriodStart(), value.getPeriodEnd(),
|
||||
value.getReviewStatus(),
|
||||
"PUBLISHED", action == null && hash == null ? "ACTIVE" : value.getSourceStatus(), hash);
|
||||
"PUBLISHED", action == null && hash == null ? "ACTIVE" : value.getSourceStatus(), hash, value.getWalletSettlement());
|
||||
}
|
||||
|
||||
private static String text(Long value) { return value == null ? null : String.valueOf(value); }
|
||||
|
||||
+51
-4
@@ -79,14 +79,20 @@ public class OverseasFeeSourceService {
|
||||
if (existing > 0) throw conflict("外接货已有派送应收尚未确认作废,请先完成原费用处理再重新转派送");
|
||||
}
|
||||
|
||||
private static void requireNonProcurement(String sourceType,String businessType) {
|
||||
if("OVERSEAS_PROCUREMENT_FEE".equals(sourceType) || "OVERSEAS_PROCUREMENT".equals(businessType) || "PROCUREMENT_FEE".equals(businessType))
|
||||
throw conflict("采购费用必须通过采购履约或财务节点确认,实扣成功后自动关联来源");
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public OverseasFeeSource create(String operatorId, String key, SaveRequest request) {
|
||||
requireNonProcurement(request.sourceType(),request.businessType());
|
||||
OverseasFeeCommand replay = replay(operatorId, "CREATE", key, request);
|
||||
if (replay != null) return current(replay.getSourceFeeId());
|
||||
validate(request);
|
||||
businessGuard.requireOwned(request);
|
||||
long sourceFeeId = IdWorker.getId();
|
||||
OverseasFeeSource value = build(IdWorker.getId(), sourceFeeId, 1, operatorId, request);
|
||||
OverseasFeeSource value = build(IdWorker.getId(), sourceFeeId, 1, operatorId, request, null);
|
||||
mapper.insert(value);
|
||||
command(operatorId, "CREATE", key, request, sourceFeeId, 1);
|
||||
return value;
|
||||
@@ -108,11 +114,45 @@ public class OverseasFeeSourceService {
|
||||
return value;
|
||||
}
|
||||
|
||||
private record SettledRequest(SaveRequest fee, OverseasFeeCurrencyConverter.Conversion conversion,
|
||||
com.baiyue.common.event.WalletSettlementReference settlement) { }
|
||||
|
||||
/** Internal business entry: money has already succeeded and its confirmed FX must not be recalculated. */
|
||||
@Transactional
|
||||
public OverseasFeeSource publishSettled(String operatorId, String key, SaveRequest request,
|
||||
OverseasFeeCurrencyConverter.Conversion conversion,
|
||||
com.baiyue.common.event.WalletSettlementReference settlement) {
|
||||
if (!"RECEIVABLE".equals(request.direction()) || conversion == null || settlement == null
|
||||
|| text(settlement.commandEventId()) == null || text(settlement.holdId()) == null || text(settlement.ledgerId()) == null
|
||||
|| !request.currency().equals(conversion.originalCurrency())
|
||||
|| request.amount().compareTo(conversion.originalAmount()) != 0
|
||||
|| conversion.settlementAmount().signum() <= 0 || conversion.exchangeRate().signum() <= 0
|
||||
|| !request.businessDate().toString().substring(0, 7).equals(conversion.exchangeRateMonth())) {
|
||||
throw bad("已扣费用的资金引用或确认汇率快照不完整");
|
||||
}
|
||||
var confirmed = new SettledRequest(request, conversion, settlement);
|
||||
OverseasFeeCommand replay = replay(operatorId, "PUBLISH_SETTLED", key, confirmed);
|
||||
if (replay != null) return current(replay.getSourceFeeId());
|
||||
validate(request);
|
||||
businessGuard.requireOwned(request);
|
||||
long sourceFeeId = IdWorker.getId();
|
||||
OverseasFeeSource value = build(IdWorker.getId(), sourceFeeId, 1, operatorId, request, conversion);
|
||||
value.setWalletSettlement(settlement);
|
||||
value.setReviewStatus("APPROVED"); value.setPublicationStatus("PUBLISHED");
|
||||
value.setReviewerId(operatorId); value.setReviewedAt(now());
|
||||
value.setReviewReason("已核定业务费用扣款成功后发布");
|
||||
mapper.insert(value);
|
||||
outbox.publish(value, "PUBLISHED");
|
||||
command(operatorId, "PUBLISH_SETTLED", key, confirmed, sourceFeeId, 1);
|
||||
return value;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public OverseasFeeSource submit(long sourceFeeId, String operatorId, String key) {
|
||||
OverseasFeeCommand replay = replay(operatorId, "SUBMIT", key, sourceFeeId);
|
||||
if (replay != null) return current(replay.getSourceFeeId());
|
||||
OverseasFeeSource value = lock(sourceFeeId);
|
||||
requireNonProcurement(value.getSourceType(),value.getBusinessType());
|
||||
if (!"DRAFT".equals(value.getReviewStatus()) && !"REJECTED".equals(value.getReviewStatus()))
|
||||
throw conflict("只有草稿或已驳回费用可以提交");
|
||||
value.setReviewStatus("PENDING"); value.setReviewReason(null);
|
||||
@@ -126,6 +166,7 @@ public class OverseasFeeSourceService {
|
||||
OverseasFeeCommand replay = replay(operatorId, "REVIEW", request.idempotencyKey(), request);
|
||||
if (replay != null) return current(replay.getSourceFeeId());
|
||||
OverseasFeeSource value = lock(sourceFeeId);
|
||||
requireNonProcurement(value.getSourceType(),value.getBusinessType());
|
||||
if (!"PENDING".equals(value.getReviewStatus()) || !"ACTIVE".equals(value.getSourceStatus()))
|
||||
throw conflict("只有待审核的有效费用可以审核");
|
||||
boolean approved = "APPROVE".equals(request.decision());
|
||||
@@ -143,6 +184,8 @@ public class OverseasFeeSourceService {
|
||||
OverseasFeeCommand replay = replay(operatorId, "ADJUST", request.idempotencyKey(), request);
|
||||
if (replay != null) return current(replay.getSourceFeeId());
|
||||
OverseasFeeSource old = lock(sourceFeeId);
|
||||
if (old.getWalletSettlement() != null) throw conflict("已扣费用只能通过关联的财务差额或退款调整");
|
||||
requireNonProcurement(old.getSourceType(),old.getBusinessType());
|
||||
if (cancellation.requested(old)) throw conflict("该费用已申请作废,请先处理财务结果");
|
||||
if (!"APPROVED".equals(old.getReviewStatus()) || !"ACTIVE".equals(old.getSourceStatus())
|
||||
|| old.getSourceVersion() != request.expectedVersion()) throw conflict("只有当前已发布版本可以调整");
|
||||
@@ -157,7 +200,7 @@ public class OverseasFeeSourceService {
|
||||
|| !Objects.equals(old.getContainerNo(), text(request.fee().containerNo())))
|
||||
throw conflict("调整不得改变费用来源或业务归属");
|
||||
old.setSourceStatus("SUPERSEDED"); old.setUpdatedBy(operatorId); old.setUpdatedAt(now()); mapper.updateById(old);
|
||||
OverseasFeeSource next = build(IdWorker.getId(), sourceFeeId, old.getSourceVersion() + 1, operatorId, request.fee());
|
||||
OverseasFeeSource next = build(IdWorker.getId(), sourceFeeId, old.getSourceVersion() + 1, operatorId, request.fee(), null);
|
||||
next.setReviewStatus("PENDING");
|
||||
mapper.insert(next);
|
||||
command(operatorId, "ADJUST", request.idempotencyKey(), request, sourceFeeId, next.getSourceVersion());
|
||||
@@ -169,6 +212,8 @@ public class OverseasFeeSourceService {
|
||||
OverseasFeeCommand replay = replay(operatorId, "VOID", request.idempotencyKey(), request);
|
||||
if (replay != null) return current(replay.getSourceFeeId());
|
||||
OverseasFeeSource value = lock(sourceFeeId);
|
||||
requireNonProcurement(value.getSourceType(),value.getBusinessType());
|
||||
if (value.getWalletSettlement() != null) throw conflict("已扣费用不能作废,请使用关联退款");
|
||||
if (!"APPROVED".equals(value.getReviewStatus()) || !"ACTIVE".equals(value.getSourceStatus())
|
||||
|| value.getSourceVersion() != request.expectedVersion()) throw conflict("只有当前已发布版本可以作废");
|
||||
if (cancellation.requested(value)) return cancellation.describe(List.of(value)).get(0);
|
||||
@@ -183,7 +228,8 @@ public class OverseasFeeSourceService {
|
||||
return value;
|
||||
}
|
||||
|
||||
private OverseasFeeSource build(long id, long sourceFeeId, int version, String operatorId, SaveRequest request) {
|
||||
private OverseasFeeSource build(long id, long sourceFeeId, int version, String operatorId, SaveRequest request,
|
||||
OverseasFeeCurrencyConverter.Conversion confirmedConversion) {
|
||||
OverseasFeeSource value = new OverseasFeeSource();
|
||||
value.setId(id); value.setSourceFeeId(sourceFeeId); value.setSourceVersion(version);
|
||||
value.setCustomerId(request.customerId().trim()); value.setCustomerName(text(request.customerName()));
|
||||
@@ -196,7 +242,8 @@ public class OverseasFeeSourceService {
|
||||
value.setSupplierCategory(text(request.supplierCategory())); value.setSettlementMode(text(request.settlementMode()));
|
||||
value.setPaidAt(request.paidAt()); value.setPaymentMethod(text(request.paymentMethod())); value.setPaymentReferenceNo(text(request.paymentReferenceNo()));
|
||||
value.setPaymentVoucherFileIds(request.paymentVoucherFileIds()); value.setBusinessDate(request.businessDate());
|
||||
OverseasFeeCurrencyConverter.Conversion conversion = currencyConverter.convert(request.currency(), request.amount(), request.businessDate());
|
||||
OverseasFeeCurrencyConverter.Conversion conversion = confirmedConversion != null ? confirmedConversion
|
||||
: currencyConverter.convert(request.currency(), request.amount(), request.businessDate());
|
||||
value.setCurrency("CNY"); value.setAmount(conversion.settlementAmount());
|
||||
value.setOriginalCurrency(conversion.originalCurrency()); value.setOriginalAmount(conversion.originalAmount());
|
||||
value.setExchangeRate(conversion.exchangeRate()); value.setExchangeRateMonth(conversion.exchangeRateMonth());
|
||||
|
||||
Reference in New Issue
Block a user