http://jira.mfd.msk:8088/browse/CLS-769 вывод средств OM*T checks

This commit is contained in:
ialbert 2024-10-04 15:22:07 +03:00
parent 62f0f3246f
commit ef0bb6cf7d
8 changed files with 377 additions and 10 deletions

View file

@ -15,8 +15,17 @@ public class ClearingServiceSettings {
private KafkaConsumerSettings kafkaConsumer;
private KafkaProducerSettings kafkaProducer;
private SessionStageSettings sessionStage;
private boolean balanceCheckOmt = false;
private TradeSettings trade = new TradeSettings();
public boolean isBalanceCheckOmt() {
return balanceCheckOmt;
}
public void setBalanceCheckOmt(boolean balanceCheckOmt) {
this.balanceCheckOmt = balanceCheckOmt;
}
public HazelcastClientParams getHazelcast() {
return hazelcast;
}

View file

@ -1,8 +1,5 @@
package ru.spcex.clearing.service.payment;
import static ru.spcex.clearing.session.stage.impl.GatewayRequester.mapError;
import static ru.spcex.platform.utils.number.BigDecimalUtil.safeBD;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.Locale;
@ -38,11 +35,13 @@ import ru.spcex.clearing.service.Sdf54Creator;
import ru.spcex.clearing.service.builder.PaymentInstructionBuilderV2;
import ru.spcex.clearing.service.integration.GatewayRequestCreator;
import ru.spcex.clearing.service.registry.AssetTBFProcessing;
import ru.spcex.clearing.service.registry.BalanceChecker;
import ru.spcex.clearing.service.registry.DmiService;
import ru.spcex.clearing.service.registry.RegistryManager;
import ru.spcex.clearing.service.schedule.TradingTimeService;
import ru.spcex.clearing.service.validation.ValidationStored;
import ru.spcex.clearing.session.stage.impl.GatewayRequester;
import static ru.spcex.clearing.session.stage.impl.GatewayRequester.mapError;
import ru.spcex.clearing.util.LocaleUtil;
import ru.spcex.clearing.util.security.UserRoleVerification;
import ru.spcex.platform.enumeration.CurrencyCode;
@ -57,6 +56,7 @@ import ru.spcex.platform.utils.enumeration.EnumMessage;
import ru.spcex.platform.utils.enumeration.IEnumId;
import ru.spcex.platform.utils.enumeration.IMessageResolver;
import ru.spcex.platform.utils.localization.SpringPropertiesLocalizer;
import static ru.spcex.platform.utils.number.BigDecimalUtil.safeBD;
import ru.spcex.platform.utils.text.TextUtil;
import ru.spcex.platform.utils.validation.IValidator;
@ -83,6 +83,7 @@ public class PaymentInstructionOutboundService {
private final TradingTimeService time;
private final GatewayRequester gateway;
private final NotificationSender notification;
private final BalanceChecker balanceChecker;
private final SpringPropertiesLocalizer resolver;
@Autowired
@ -90,7 +91,7 @@ public class PaymentInstructionOutboundService {
IMessageResolver msgs,
ImdgProvider imdgProvider,
Function<PIClearingOutbondActionNewRequest, IValidator> validation, RegistryManager rgsMng, KafkaSender kafkaSender, AssetTBFProcessing assetsMng, DmiService dmiService, TradingTimeService time, GatewayRequester gateway, NotificationSender notification,
SpringPropertiesLocalizer resolver) {
SpringPropertiesLocalizer resolver, BalanceChecker balanceChecker) {
this.imdgProvider = imdgProvider;
this.rights = rights;
this.msgs = msgs;
@ -102,6 +103,7 @@ public class PaymentInstructionOutboundService {
this.rgsImdg = imdgProvider.getImdg(IMDGDistributedNames.Map_Registry, Registry.class);
this.currImdg = imdgProvider.getImdg(IMDGDistributedNames.Map_Currency, Currency.class);
this.sdf54Imdg = imdgProvider.getImdg(IMDGDistributedNames.Map_SDf54, SDf54.class);
this.balanceChecker = balanceChecker;
this.sdf54Creator = new Sdf54Creator(imdgProvider, resolver);
this.idGenerator = imdgProvider.getImdgIdGenerator();
this.rgsMng = rgsMng;
@ -173,14 +175,22 @@ public class PaymentInstructionOutboundService {
//fixme pmt.setDocumentNumber(registry.getSecurityId());
pmtImdg.insert(pmt);
log.debug("new PaymentInstruction.id: {}", pmt.getId());
log.debug("PIClearingOutbondActionNewRequest.id={} new PaymentInstruction.id: {}",
req.getId(),
pmt.getId());
Optional<AssetTrio> assets = assetsMng.searchMoneyByAccAndCompany(addressee.getId(), accCred.getId());
if (assets.isEmpty() || assetsMng.insufficientBalance(assets.get().a__f(), amount)) {
log.debug("PIClearingOutbondActionNewRequest.id={} a__f asset id={}",
req.getId(),
assets.map(AssetTrio::a__f).map(Registry::getId).orElse(null));
if (assets.isEmpty() || balanceChecker.insufficientBalance(assets.get().a__f(),
tcr.getId(), //never null because validation
accCred.getCurrency(),
amount)) {
pmt.setTransactionStatus(TransactionStatus.cher.getKey());
pmt.setUpdated(now);
pmtImdg.update(pmt);
log.debug("insufficient balance, pmt.id: {}, am*f.balance={}",
pmt.getId(), assets.map(AssetTrio::a__f).map(Registry::getBalance).orElse(null));
pmt.getId(), assets.map(AssetTrio::a__f).map(Registry::getBalance).orElse(null));
return error(req.getId(), ClearingError.BalanceInsufficient);
}
log.debug("assets found and balance is ok. am*b.id={}, am*t.id={}, am*f.id={}",

View file

@ -0,0 +1,247 @@
package ru.spcex.clearing.service.registry;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Optional;
import java.util.function.BiFunction;
import java.util.function.Function;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import ru.clearing.classes.statics.data.registry.Registry;
import ru.clearing.classes.statics.data.security.CurrencyPairSecurity;
import ru.clearing.classes.statics.data.security.Rates;
import ru.clearing.classes.statics.data.security.RiskParameter;
import ru.spcex.clearing.config.element.ClearingServiceSettings;
import ru.spcex.clearing.imdg.IMDGDistributedNames;
import static ru.spcex.clearing.util.NumberUtils.safeCast;
import ru.spcex.platform.classes.base.SpcexObjectBase;
import ru.spcex.platform.enumeration.RegistryStatus;
import static ru.spcex.platform.enumeration.RegistryTradingParams.OM_T;
import ru.spcex.platform.enumeration.WorkflowStatus;
import ru.spcex.platform.imdg.api.Imdg;
import ru.spcex.platform.imdg.api.ImdgProvider;
import ru.spcex.platform.imdg.api.predicate.ImdgPredicate;
import ru.spcex.platform.imdg.api.predicate.ImdgPredicateBuilder;
import ru.spcex.platform.imdg.api.predicate.specific.RegistryCodeSqlBuilder;
import ru.spcex.platform.imdg.iml.hazelcast.adapter.CashCloser;
import ru.spcex.platform.imdg.iml.hazelcast.adapter.CashV2;
import ru.spcex.platform.imdg.iml.hazelcast.adapter.key.GenericCashingKey1;
import ru.spcex.platform.imdg.iml.hazelcast.adapter.key.GenericCashingKey2;
import static ru.spcex.platform.utils.number.BigDecimalUtil.safeBD;
import ru.spcex.platform.utils.text.TextUtil;
@Component
public class BalanceChecker {
private final Logger log = LoggerFactory.getLogger(getClass());
private final Imdg<Registry> rgsImdg;
private final Imdg<Rates> rateImdg;
private final Imdg<RiskParameter> riskParamImdg;
private final Imdg<CurrencyPairSecurity> currPairImdg;
private final boolean balanceCheckOmts;
@Autowired
public BalanceChecker(ImdgProvider imdgProvider, ClearingServiceSettings settings) {
this.rgsImdg = imdgProvider.getImdg(IMDGDistributedNames.Map_Registry, Registry.class);
this.rateImdg = imdgProvider.getCashingImdg(IMDGDistributedNames.Map_Rates, Rates.class, null);
this.riskParamImdg = imdgProvider.getCashingImdg(IMDGDistributedNames.Map_RiskParameter, RiskParameter.class, null);
this.currPairImdg = imdgProvider.getCashingImdg(IMDGDistributedNames.Map_CurrencyPairSecurity, CurrencyPairSecurity.class, null);
this.balanceCheckOmts = settings.isBalanceCheckOmt();
}
public boolean insufficientBalance(Registry a__f,
Long tcrId,
String accCurrency,
BigDecimal amount) {
if (!balanceCheckOmts) {
log.info("{}#id={}#balance={} check for amount {}; OM*T checks disabled",
a__f.getRegistryCode(), a__f.getId(), a__f.getBalance(), amount);
return safeBD(a__f.getBalance()).compareTo(safeBD(amount)) < 0;
}
log.info("{}#id={}#balance={} check for amount {}; OM*T checks enabled",
a__f.getRegistryCode(), a__f.getId(), a__f.getBalance(), amount);
LocalDate today = LocalDate.now();
ImdgPredicateBuilder rgsPb = rgsImdg.predicateBuilder();
ImdgPredicateBuilder ratePb = rateImdg.predicateBuilder();
Function<ImdgPredicate, ImdgPredicate> rgsAnd = p -> rgsPb.and(
rgsPb.sql(RegistryCodeSqlBuilder.getInstance(OM_T).build()),
rgsPb.equals("tradingClearingRegistryId", tcrId),
rgsPb.or(
rgsPb.equals("registryStatus", RegistryStatus.PROC.getKey()),
rgsPb.equals("registryStatus", RegistryStatus.MNG.getKey())
),
p
);
try (BalanceCheckerCash cash = new BalanceCheckerCash()) {
Collection<Registry> omtsToday = rgsImdg.getCollectionObjectsByPredicate(
rgsAnd.apply(
rgsPb.equals("settlementDate", today)
)
);
Collection<Registry> omtsNotToday = rgsImdg.getCollectionObjectsByPredicate(
rgsAnd.apply(
rgsPb.not(rgsPb.equals("settlementDate", today))
)
);
log.debug("a__f.id={} balance checking; omt today: {}, omt !today: {}",
a__f.getId(), omtsToday.size(), omtsNotToday.size());
BiFunction<Collection<Registry>, String, BigDecimal> omtSum = (rgss, settlType)
-> rgss
.stream()
.map(r -> {
Optional<Rates> crossRate = loadRate(r, today, cash);
Optional<RiskParameter> riskParam = loadRisk(r, settlType, cash);
BigDecimal oblAmount = r.getBalance();
BigDecimal rate = crossRate.map(Rates::getValue).orElse(BigDecimal.ONE);
BigDecimal risk = riskParam.map(RiskParameter::getValue).orElse(BigDecimal.ONE);
BigDecimal singleOblSum = safeBD(oblAmount)
.multiply(rate)
.multiply(risk);
log.debug("for {}.id={}: Rate.id={}; RiskParameter.id={}",
r.getRegistryCode(), r.getId(),
crossRate.map(SpcexObjectBase::getId).orElse(null),
riskParam.map(SpcexObjectBase::getId).orElse(null)
);
return singleOblSum;
})
.reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal todaySum = omtSum.apply(omtsToday, "T0");
BigDecimal notTodaySum = omtSum.apply(omtsNotToday, "T1");
Rates rate = rateImdg.getFirstObjectByPredicate(
ratePb.and(
ratePb.equals("currency", accCurrency),
ratePb.equals("valueDate", today)
)
);
BigDecimal rateValue = (rate != null && rate.getValue() != null) ?
rate.getValue() : BigDecimal.ONE;
BigDecimal sumForObligations = todaySum
.add(notTodaySum)
.divide(rateValue, RoundingMode.HALF_UP);
log.debug("a__f.id={}, today OM*T sum {}; !today OM*T sum {}; rate for final formula: id={}, value={}; overall OM*T sum: {}",
a__f.getId(), todaySum, notTodaySum,
rate != null ? rate.getId() : null,
rateValue,
sumForObligations
);
return safeBD(a__f.getBalance())
.subtract(sumForObligations)
.compareTo(safeBD(amount)) < 0;
}
}
private Optional<Rates> loadRate(Registry rgs,
LocalDate valueDate,
BalanceCheckerCash cash) {
if (TextUtil.isEmpty(rgs.getSecuritySymbol())) {
return Optional.empty();
}
ImdgPredicateBuilder ratePb = rateImdg.predicateBuilder();
ImdgPredicate ratePredicate = ratePb.and(
ratePb.equals("valueDate", valueDate),
ratePb.equals("currency", rgs.getSecuritySymbol())
);
return Optional.ofNullable(rateImdg.getFirstObjectByPredicate(
ratePb.cashed(
ratePredicate,
cash.ratesCash,
new GenericCashingKey2<>(valueDate, rgs.getSecuritySymbol())
)
));
}
private CurrencyPairSecurity loadCurrPairSec(String contract, BalanceCheckerCash cash) {
ImdgPredicateBuilder currPb = currPairImdg.predicateBuilder();
Long contractCasted = safeCast(contract);
if (contractCasted == null) {
return null;
}
ImdgPredicate prdct = currPb.cashed(
currPb.equals("currencyPairId", contractCasted),
cash.currPairCash,
new GenericCashingKey1<>(contractCasted));
return currPairImdg.getFirstObjectByPredicate(
prdct
);
}
private Optional<RiskParameter> loadRisk(
Registry rgs, String settlementType, BalanceCheckerCash cash
) {
CurrencyPairSecurity pair = loadCurrPairSec(
rgs.getContract(), cash
);
if (pair == null) {
log.warn("cannot find currencyPairSecurity for {}#id={}#contract={}",
rgs.getRegistryCode(),
rgs.getId(),
rgs.getContract());
return Optional.empty();
}
log.debug("found currencyPairSecurity.id={} for {}.id={}",
rgs.getRegistryCode(),
rgs.getId(),
pair.getId());
ImdgPredicateBuilder riskPb = riskParamImdg.predicateBuilder();
ImdgPredicate prdct = riskPb.and(
riskPb.equals("currencyPairId", pair.getId()),
riskPb.equals("settlementType", settlementType),
riskPb.equals("workflowStatus", WorkflowStatus.Active.getKey())
);
ImdgPredicate prdctF = riskPb.cashed(
prdct,
cash.riskCash,
new GenericCashingKey2<>(pair.getId(), settlementType)
);
return Optional.ofNullable(riskParamImdg.getFirstObjectByPredicate(prdctF));
}
protected static class BalanceCheckerCash extends CashCloser {
protected final CashV2<GenericCashingKey2<LocalDate, String>, Rates> ratesCash;
protected final CashV2<GenericCashingKey2<Long, String>, RiskParameter> riskCash;
protected final CashV2<GenericCashingKey1<Long>, CurrencyPairSecurity> currPairCash;
public BalanceCheckerCash() {
cashes = new ArrayList<>();
ratesCash = add(new CashV2<GenericCashingKey2<LocalDate, String>, Rates>() {
@Override
protected GenericCashingKey2<LocalDate, String> extractKey(Rates obj) {
return new GenericCashingKey2<>(obj.getValueDate(), obj.getCurrency());
}
});
riskCash = add(new CashV2<GenericCashingKey2<Long, String>, RiskParameter>() {
@Override
protected GenericCashingKey2<Long, String> extractKey(RiskParameter obj) {
return new GenericCashingKey2<>(obj.getCurrencyPairId(), obj.getSettlementType());
}
});
currPairCash = add(new CashV2<GenericCashingKey1<Long>, CurrencyPairSecurity>() {
@Override
protected GenericCashingKey1<Long> extractKey(CurrencyPairSecurity obj) {
return new GenericCashingKey1<>(obj.getCurrencyPairId());
}
});
}
}
}

View file

@ -0,0 +1,14 @@
package ru.spcex.clearing.util;
import ru.spcex.platform.utils.text.TextUtil;
public class NumberUtils {
public static Long safeCast(String s) {
if (TextUtil.isEmpty(s)) return null;
try {
return Long.parseLong(s);
} catch (NumberFormatException e) {
return null;
}
}
}

View file

@ -0,0 +1,24 @@
package ru.spcex.platform.imdg.iml.hazelcast.adapter.key;
import java.util.Objects;
public class GenericCashingKey1<T1> {
private final T1 first;
public GenericCashingKey1(T1 first) {
this.first = first;
}
@Override
public final boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof GenericCashingKey1<?> that)) return false;
return Objects.equals(first, that.first);
}
@Override
public int hashCode() {
return Objects.hashCode(first);
}
}

View file

@ -0,0 +1,28 @@
package ru.spcex.platform.imdg.iml.hazelcast.adapter.key;
import java.util.Objects;
public class GenericCashingKey2<T1, T2> {
private final T1 first;
private final T2 second;
public GenericCashingKey2(T1 first, T2 second) {
this.first = first;
this.second = second;
}
@Override
public final boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof GenericCashingKey2<?, ?> that)) return false;
return Objects.equals(first, that.first) && Objects.equals(second, that.second);
}
@Override
public int hashCode() {
int result = Objects.hashCode(first);
result = 31 * result + Objects.hashCode(second);
return result;
}
}

View file

@ -0,0 +1,33 @@
package ru.spcex.platform.imdg.iml.hazelcast.adapter.key;
import java.util.Objects;
public class GenericCashingKey3<T1, T2, T3> {
private final T1 first;
private final T2 second;
private final T3 third;
public GenericCashingKey3(T1 first, T2 second, T3 third) {
this.first = first;
this.second = second;
this.third = third;
}
@Override
public final boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof GenericCashingKey3<?, ?, ?> that)) return false;
return Objects.equals(first, that.first)
&& Objects.equals(second, that.second)
&& Objects.equals(third, that.third);
}
@Override
public int hashCode() {
int result = Objects.hashCode(first);
result = 31 * result + Objects.hashCode(second);
result = 31 * result + Objects.hashCode(third);
return result;
}
}

View file

@ -1,9 +1,7 @@
package ru.spcex.platform.utils.number;
import org.springframework.util.StringUtils;
import ru.spcex.platform.utils.text.TextUtil;
import java.math.BigDecimal;
import ru.spcex.platform.utils.text.TextUtil;
public class BigDecimalUtil {
@ -77,6 +75,10 @@ public class BigDecimalUtil {
return bd == null ? BigDecimal.ZERO : bd;
}
public static BigDecimal safeBD(BigDecimal bd, BigDecimal defaultValue) {
return bd == null ? defaultValue : bd;
}
/**
* Null safe add:
*