diff --git a/clearing-parent/clearing-service/src/main/java/ru/spcex/clearing/config/element/ClearingServiceSettings.java b/clearing-parent/clearing-service/src/main/java/ru/spcex/clearing/config/element/ClearingServiceSettings.java index bfa0bc150..3a56fd50f 100644 --- a/clearing-parent/clearing-service/src/main/java/ru/spcex/clearing/config/element/ClearingServiceSettings.java +++ b/clearing-parent/clearing-service/src/main/java/ru/spcex/clearing/config/element/ClearingServiceSettings.java @@ -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; } diff --git a/clearing-parent/clearing-service/src/main/java/ru/spcex/clearing/service/payment/PaymentInstructionOutboundService.java b/clearing-parent/clearing-service/src/main/java/ru/spcex/clearing/service/payment/PaymentInstructionOutboundService.java index 96e2c35fc..666230273 100644 --- a/clearing-parent/clearing-service/src/main/java/ru/spcex/clearing/service/payment/PaymentInstructionOutboundService.java +++ b/clearing-parent/clearing-service/src/main/java/ru/spcex/clearing/service/payment/PaymentInstructionOutboundService.java @@ -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 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 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={}", diff --git a/clearing-parent/clearing-service/src/main/java/ru/spcex/clearing/service/registry/BalanceChecker.java b/clearing-parent/clearing-service/src/main/java/ru/spcex/clearing/service/registry/BalanceChecker.java new file mode 100644 index 000000000..c1fdcdede --- /dev/null +++ b/clearing-parent/clearing-service/src/main/java/ru/spcex/clearing/service/registry/BalanceChecker.java @@ -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 rgsImdg; + private final Imdg rateImdg; + private final Imdg riskParamImdg; + private final Imdg 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 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 omtsToday = rgsImdg.getCollectionObjectsByPredicate( + rgsAnd.apply( + rgsPb.equals("settlementDate", today) + ) + ); + Collection 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, String, BigDecimal> omtSum = (rgss, settlType) + -> rgss + .stream() + .map(r -> { + Optional crossRate = loadRate(r, today, cash); + Optional 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 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 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, Rates> ratesCash; + protected final CashV2, RiskParameter> riskCash; + protected final CashV2, CurrencyPairSecurity> currPairCash; + + public BalanceCheckerCash() { + cashes = new ArrayList<>(); + ratesCash = add(new CashV2, Rates>() { + @Override + protected GenericCashingKey2 extractKey(Rates obj) { + return new GenericCashingKey2<>(obj.getValueDate(), obj.getCurrency()); + } + }); + riskCash = add(new CashV2, RiskParameter>() { + @Override + protected GenericCashingKey2 extractKey(RiskParameter obj) { + return new GenericCashingKey2<>(obj.getCurrencyPairId(), obj.getSettlementType()); + } + }); + currPairCash = add(new CashV2, CurrencyPairSecurity>() { + @Override + protected GenericCashingKey1 extractKey(CurrencyPairSecurity obj) { + return new GenericCashingKey1<>(obj.getCurrencyPairId()); + } + }); + } + } + + +} diff --git a/clearing-parent/clearing-service/src/main/java/ru/spcex/clearing/util/NumberUtils.java b/clearing-parent/clearing-service/src/main/java/ru/spcex/clearing/util/NumberUtils.java new file mode 100644 index 000000000..c5c002951 --- /dev/null +++ b/clearing-parent/clearing-service/src/main/java/ru/spcex/clearing/util/NumberUtils.java @@ -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; + } + } +} diff --git a/platform-parent/platform-imdg-api/src/main/java/ru/spcex/platform/imdg/iml/hazelcast/adapter/key/GenericCashingKey1.java b/platform-parent/platform-imdg-api/src/main/java/ru/spcex/platform/imdg/iml/hazelcast/adapter/key/GenericCashingKey1.java new file mode 100644 index 000000000..c285b273e --- /dev/null +++ b/platform-parent/platform-imdg-api/src/main/java/ru/spcex/platform/imdg/iml/hazelcast/adapter/key/GenericCashingKey1.java @@ -0,0 +1,24 @@ +package ru.spcex.platform.imdg.iml.hazelcast.adapter.key; + +import java.util.Objects; + +public class GenericCashingKey1 { + 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); + } +} \ No newline at end of file diff --git a/platform-parent/platform-imdg-api/src/main/java/ru/spcex/platform/imdg/iml/hazelcast/adapter/key/GenericCashingKey2.java b/platform-parent/platform-imdg-api/src/main/java/ru/spcex/platform/imdg/iml/hazelcast/adapter/key/GenericCashingKey2.java new file mode 100644 index 000000000..a0f6ab514 --- /dev/null +++ b/platform-parent/platform-imdg-api/src/main/java/ru/spcex/platform/imdg/iml/hazelcast/adapter/key/GenericCashingKey2.java @@ -0,0 +1,28 @@ +package ru.spcex.platform.imdg.iml.hazelcast.adapter.key; + +import java.util.Objects; + +public class GenericCashingKey2 { + 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; + } + } \ No newline at end of file diff --git a/platform-parent/platform-imdg-api/src/main/java/ru/spcex/platform/imdg/iml/hazelcast/adapter/key/GenericCashingKey3.java b/platform-parent/platform-imdg-api/src/main/java/ru/spcex/platform/imdg/iml/hazelcast/adapter/key/GenericCashingKey3.java new file mode 100644 index 000000000..0facf8707 --- /dev/null +++ b/platform-parent/platform-imdg-api/src/main/java/ru/spcex/platform/imdg/iml/hazelcast/adapter/key/GenericCashingKey3.java @@ -0,0 +1,33 @@ +package ru.spcex.platform.imdg.iml.hazelcast.adapter.key; + +import java.util.Objects; + +public class GenericCashingKey3 { + 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; + } +} \ No newline at end of file diff --git a/platform-parent/platform-utils/src/main/java/ru/spcex/platform/utils/number/BigDecimalUtil.java b/platform-parent/platform-utils/src/main/java/ru/spcex/platform/utils/number/BigDecimalUtil.java index 987b4456f..6e06c8800 100644 --- a/platform-parent/platform-utils/src/main/java/ru/spcex/platform/utils/number/BigDecimalUtil.java +++ b/platform-parent/platform-utils/src/main/java/ru/spcex/platform/utils/number/BigDecimalUtil.java @@ -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: *