diff --git a/clearing-parent/clearing-service/src/main/java/ru/spcex/clearing/service/executors/Sdf21Executor.java b/clearing-parent/clearing-service/src/main/java/ru/spcex/clearing/service/executors/Sdf21Executor.java new file mode 100644 index 000000000..df42c4285 --- /dev/null +++ b/clearing-parent/clearing-service/src/main/java/ru/spcex/clearing/service/executors/Sdf21Executor.java @@ -0,0 +1,459 @@ +package ru.spcex.clearing.service.executors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Service; +import ru.clearing.classes.statics.data.account.Account; +import ru.clearing.classes.statics.data.account.AccountBalance; +import ru.clearing.classes.statics.data.account.ClearingAccount; +import ru.clearing.classes.statics.data.company.Company; +import ru.clearing.classes.statics.data.misc.Currency; +import ru.clearing.classes.statics.data.registry.Registry; +import ru.clearing.classes.statics.data.registry.TradingClearingRegistry; +import ru.clearing.classes.statics.data.sdf.SDf02; +import ru.clearing.classes.statics.data.sdf.SDf21; +import ru.clearing.classes.statics.data.security.Security; +import ru.clearing.classes.statics.data.statement.Statement; +import ru.spcex.clearing.error.ClearingError; +import ru.spcex.clearing.imdg.IMDGDistributedNames; +import ru.spcex.clearing.platform.messaging.domain.cud.balance.StatementRequest; +import ru.spcex.clearing.platform.messaging.service.sender.KafkaSender; +import ru.spcex.clearing.service.AnltSearcher; +import ru.spcex.clearing.service.LoggingService; +import ru.spcex.clearing.service.builder.RegistrySecurityBuilder; +import ru.spcex.clearing.service.model.Result; +import ru.spcex.clearing.service.validation.ValidationStored; +import ru.spcex.clearing.session.stage.util.RegistryUtil; +import ru.spcex.platform.enumeration.*; +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.api.predicate.specific.SecuritySelector; +import ru.spcex.platform.utils.enumeration.EnumMessage; +import ru.spcex.platform.utils.enumeration.IEnumKey; +import ru.spcex.platform.utils.enumeration.IMessageResolver; +import ru.spcex.platform.utils.text.TextUtil; +import ru.spcex.platform.utils.validation.IValidator; + +import java.math.BigDecimal; +import java.time.Instant; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.Collection; +import java.util.Optional; +import java.util.function.Function; +import java.util.regex.Pattern; + +import static ru.spcex.platform.utils.number.BigDecimalUtil.safeBD; + +@Service +public class Sdf21Executor extends AbstractExecutor { + private final Logger log = LoggerFactory.getLogger(getClass()); + private final Imdg statementImdg; + private final Imdg registryImdg; + private final Function sDf21Validator; + private final LoggingService errorLogger; + private final Imdg sdf02Imdg; + private final ImdgProvider imdgProvider; + private final IMessageResolver errorResolver; + private final Imdg accountBalanceImdg; + private final Imdg clearingAccountImdg; + private final Imdg tradingClearingRegistryImdg; + private final Imdg accountImdg; + private final Imdg companyImdg; + private final Imdg securityImdg; + private final Imdg currencyImdg; + private final SecuritySelector scrSlct; + private final AnltSearcher anltSearcher; + private final IMessageResolver messageResolver; + private final Pattern pattern = Pattern.compile("№.*"); + + public Sdf21Executor(@Qualifier("sdf21Validator") Function sDf21Validator, + LoggingService errorLogger, + ImdgProvider imdgProvider, + IMessageResolver errorResolver, AnltSearcher anltSearcher, IMessageResolver messageResolver) { + this.statementImdg = imdgProvider.getImdg(IMDGDistributedNames.Map_Statement, Statement.class); + this.registryImdg = imdgProvider.getImdg(IMDGDistributedNames.Map_Registry, Registry.class); + this.sdf02Imdg = imdgProvider.getImdg(IMDGDistributedNames.Map_SDf02, SDf02.class); + this.accountBalanceImdg = imdgProvider.getImdg(IMDGDistributedNames.Map_AccountBalance, AccountBalance.class); + this.clearingAccountImdg = imdgProvider.getImdg(IMDGDistributedNames.Map_ClearingAccount, ClearingAccount.class); + this.tradingClearingRegistryImdg = imdgProvider.getImdg(IMDGDistributedNames.Map_TradingClearingRegistry, TradingClearingRegistry.class); + this.currencyImdg = imdgProvider.getImdg(IMDGDistributedNames.Map_Currency, Currency.class); + this.accountImdg = imdgProvider.getImdg(IMDGDistributedNames.Map_Account, Account.class); + this.companyImdg = imdgProvider.getImdg(IMDGDistributedNames.Map_Company, Company.class); + this.sDf21Validator = sDf21Validator; + this.errorLogger = errorLogger; + this.imdgProvider = imdgProvider; + this.errorResolver = errorResolver; + this.scrSlct = new SecuritySelector<>(imdgProvider, Security.class); + this.anltSearcher = anltSearcher; + this.messageResolver = messageResolver; + this.securityImdg = imdgProvider.getImdg(IMDGDistributedNames.Map_Security, Security.class); + } + + //todo доделать контроль sdf01 и sdf57 + + @Override + public String exportTableName() { + return null; + } + + @Override + public boolean isNeedToSendCommand() { + return false; + } + + @Override + public void sendCommand(KafkaSender kafkaSender, Result result) {} + + //V - Изменение statement по sDf57 + // + //1. Перед изменением statement необходимо выполнить ряд проверок: + // 1.1. Найти в company запись, у которой company.tradingCode=sDf57.deal_deb. Если такой записи нет, записать в лог ошибку (5211) "Компания %s не найдена". + // 1.2. Найти в company запись, у которой company.tradingCode=sDf57.deal_cred. Если такой записи нет, записать в лог ошибку (5211) "Компания %s не найдена". + // 1.3. Проверить, есть ли в таблице account счет, у которого account.account=sDf57.c_acc_deb. Если такой записи нет, записать в лог ошибку (5217) "Счет %s не найден". + // 1.4. Проверить, есть ли в таблице account счет, у которого account.account=sDf57.c_acc_cred. Если такой записи нет, записать в лог ошибку (5217) "Счет %s не найден". + // 1.5. Проверить, что sDf57.pay_val=RUR. Иначе записать в лог ошибку (5213) "Валюта %s не найдена". + //2. В зависимости от результата прохождения проверок в п.1 раздела Изменение statement по sDf57: + // 2.1. Если все проверки пройдены, необходимо сформировать 2 новые записи согласно описанию с соответствующим тэгом, + // где первая запись будет содержать информацию о плательщике (по sDf57.deal_deb), + // а вторая о получателе (по sDf57.deal_cred) и связаны по одному inSDfId (= sDf57.id). + // 2.2. Иначе запись в statement не добавляется. + //3. По итогу добавления statement: + // Если в п.1 раздела Изменение statement по sDf57 ошибок не обнаружено, то инициировать изменение таблицы registry согласно описанию с соответствующим тэгом. + // По итогу изменения registry должны быть обновлены значения полей в statement: + // - operationStatus; + // - errorCode в случае выявления ошибки; + // - errorText в случае выявления ошибки. + public Result execute(Collection sdf, StatementRequest statementRequest) { + Result result = new Result(); + Long generationIdForGroup = imdgProvider.getImdgIdGenerator().nextId(); + result.setGenerationId(generationIdForGroup); + log.info("SDF21 execution: sdf21 number={}, groupId={}", sdf.size(), sdf.stream().findFirst().map(SDf21::getGenerationId).orElse(null)); + for (SDf21 sdf21 : sdf) { + IValidator validator = sDf21Validator.apply(sdf21); + Optional error = validator.tillFirstError(); + if (error.isPresent()) { + log.error("error while validating sdf21.id={} - {}", sdf21.getId(), messageResolver.resolve(error.get())); + continue; + } + Company company = validator.getStored(ValidationStored.Sdf21Company); + Account account = validator.getStored(ValidationStored.Sdf21Account); + log.debug("company.id={}, account.id={}", company.getId(), account.getId()); + //адресат CREDIT / владелец DEBIT + Statement stmt = create(sdf21, company, account); + statementImdg.insert(stmt); + log.debug("created statement.id={}", stmt.getId()); + + Optional err = validateActiveness(company, account); + if (err.isEmpty()) { + Optional astFound = RegistrySearch.builder(registryImdg) + .rgsCode(RegistryTradingParams.AS_T) + .companyId(company.getId()) //раньше было stmt.getAddresseeId() + .accountId(account.getId()) //раньше было stmt.getAccountId() + .find(); + astFound.ifPresentOrElse(rgs -> { + log.debug("stmt.id={}, found AS*T.id={}, updating...", stmt.getId(), rgs.getId()); + updateReg(stmt, rgs); + //добавил создание если не найдены + Registry registryUnitB = findRegByUnit(company.getId(), account.getId(), stmt.getContract(), rgs, RegistryUnit.B).orElse(copyRegB(rgs)); + Registry registryUnitF = findRegByUnit(company.getId(), account.getId(), stmt.getContract(), rgs, RegistryUnit.F).orElse(copyRegF(rgs, null)); //передаю null т.к. отнимется ниже по коду + registryUnitF.setBalance(rgs.getBalance().subtract(registryUnitB.getBalance())); + registryUnitF.setUpdated(Instant.now()); + //registry F всегда меняется + if (registryUnitF.getId() == null) { + registryImdg.insert(registryUnitF); + log.debug("stmt.id={}, new AM*F.id={}", stmt.getId(), registryUnitF.getId()); + } else { + registryImdg.update(registryUnitF); + log.debug("stmt.id={}, updated AM*F.id={}", stmt.getId(), registryUnitF.getId()); + } + //нужно сохранить registry B только если его не было + if (registryUnitB.getId() == null) { + registryImdg.insert(registryUnitB); + log.debug("stmt.id={}, new AM*B.id={}", stmt.getId(), registryUnitB.getId()); + } + registryImdg.update(rgs); + log.debug("stmt.id={}, updated AM*T.id={}: balance {}/debit {}/ credit {}", + stmt.getId(), + rgs.getId(), + rgs.getBalance(), + rgs.getDebit(), + rgs.getCredit()); + }, () -> { + Registry registry = RegistrySecurityBuilder.builder(imdgProvider) + .statement(stmt) + .company(company) + .account(account) + .designation(RegistryDesignation.A) + .build(); + calcBalance(registry, stmt); + Registry registryB = copyRegB(registry); + Registry registryF = copyRegF(registry, registryB.getBalance()); + + registryImdg.insert(registry); + registryImdg.insert(registryF); + registryImdg.insert(registryB); + log.debug("stmt.id={}, new AM*T.id={}, new AM*F.id={}, new AM*B.id={}", + stmt.getId(), + registry.getId(), + registryF.getId(), + registryB.getId()); + }); + stmt.setOperationStatus(OperationStatus.Executed.getKey()); + statementImdg.update(stmt); + log.debug("stmt.id={} status -> {}", stmt.getId(), OperationStatus.Executed.getKey()); + } else { + log.debug("statement.id={} activness validation failed {}", stmt.getId(), messageResolver.resolve(err.get())); + stmt.setErrorCodeId(err.get().getSubject().getId()); // fixme ErrorText insert + stmt.setOperationStatus(OperationStatus.Rejected.getKey()); + statementImdg.update(stmt); + } + } + return result; + } + + private Registry copyRegB(Registry rgs) { + Registry rgsB = rgs.clone(); + rgsB.setRegistryUnit(RegistryUnit.B.getKey()); + rgsB.setRegistryCode(RegistryUtil.clearingCode(rgsB)); + rgsB.setBalance(BigDecimal.ZERO); + rgsB.setDebit(BigDecimal.ZERO); + rgsB.setCredit(BigDecimal.ZERO); + rgsB.setDiffBalance(BigDecimal.ZERO); + rgsB.setCheckBalance(BigDecimal.ZERO); + return rgsB; + } + + private Registry copyRegF(Registry rgs, BigDecimal rgsBBalance) { + Registry rgsF = rgs.clone(); + rgsF.setRegistryUnit(RegistryUnit.F.getKey()); + rgsF.setRegistryCode(RegistryUtil.clearingCode(rgsF)); + rgsF.setBalance(safeBD(rgs.getBalance()).subtract(safeBD(rgsBBalance))); + rgsF.setCredit(BigDecimal.ZERO); + rgsF.setDiffBalance(BigDecimal.ZERO); + rgsF.setCheckBalance(BigDecimal.ZERO); + return rgsF; + } + + private Optional validateActiveness(Company company, Account account) { + if (!WorkflowStatus.Active.equalsByKey(company.getWorkflowStatus())) { + return Optional.of(new EnumMessage(ClearingError.CompanyNotActive, company.getId())); + } + if (!WorkflowStatus.Active.equalsByKey(account.getStatus())) { + return Optional.of(new EnumMessage(ClearingError.AccountNotActive, account.getId())); + } + return Optional.empty(); + } + + private Statement create(SDf21 sdf21, Company cmp, Account acc) { + Statement statement = new Statement(); + statement.setAddresseeId(cmp.getId()); + statement.setSenderId(Sender.Prc.getId()); + statement.setStatementType(StatementType.incr.getKey()); + //fixme statement.setComment(sdf21.()); + statement.setAccountId(acc.getId()); + + Security security = scrSlct.selectSecurityBySymbol(sdf21.getSecurityCode()); + if (security != null) { + statement.setSecurityId(security.getId()); + } + statement.setAccount(acc.getAccount()); + OperationCode operationCode = IEnumKey.getEnumByKey(OperationCode.class, sdf21.getOperationCode()); + if (operationCode != null) { + switch (operationCode) { + case RECFREE -> statement.setInOutDirection(InOutDirection.in.getKey()); + case DELFREE -> statement.setInOutDirection(InOutDirection.out.getKey()); + } + } + statement.setSettlementDate(payDate(sdf21.getOperationDate())); + statement.setAmount(TextUtil.isEmpty(sdf21.getQuantity()) ? null : new BigDecimal(sdf21.getQuantity())); + statement.setOperationStatus(OperationStatus.Pending.getKey()); + statement.setInSDfId(sdf21.getId()); + statement.setInOutSDfType(InOutSDfType.type21.getKey()); + + statement.setClearingDate(LocalDate.now()); + statement.setCreated(Instant.now()); + return statement; + } + + private void calcBalance(Registry rgs, Statement statement) { + InOutDirection inOutDirection = IEnumKey.getEnumByKey(InOutDirection.class, statement.getInOutDirection()); + //считаю balance при создании нулевым и исхожу из этого + //При добавлении (на базе изменения statement по sDf57): + //Если значение statement.inOutDirection=IN, то: + //= текущее значение registry.balance + statement.amount + // + //Если значение statement.inOutDirection=OUT, то: + //= текущее значение registry.balance - statement.amount + switch (inOutDirection) { + case in -> { + rgs.setBalance(statement.getAmount()); + rgs.setCredit(statement.getAmount()); + } + case out -> { + rgs.setBalance(statement.getAmount().negate()); + rgs.setDebit(statement.getAmount().negate()); + } + } + AccountType accType = IEnumKey.getEnumByKey(AccountType.class, rgs.getAccountType()); + if (!AccountType.Info.equals(accType)) {//fixme nuzhno li? + rgs.setDiffBalance(safeBD(rgs.getBalance()).subtract(safeBD(rgs.getCheckBalance()))); + } + rgs.setDiffBalance(safeBD(rgs.getBalance()).subtract(safeBD(rgs.getCheckBalance()))); + rgs.setBalanceDimension(BalanceDimension.PICS.getKey()); //fixme??? + } + + private void updateReg(Statement s, Registry r) { + InOutDirection direction = IEnumKey.getEnumByKey(InOutDirection.class, s.getInOutDirection()); + switch (direction) { + case in -> { + r.setBalance(safeBD(r.getBalance()).add(safeBD(s.getAmount()))); + r.setCredit(safeBD(r.getCredit()).add(safeBD(s.getAmount()))); + } + case out -> { + r.setBalance(safeBD(r.getBalance()).subtract(safeBD(s.getAmount()))); + r.setDebit(safeBD(r.getDebit()).add(safeBD(s.getAmount()))); + } + } + AccountType accType = IEnumKey.getEnumByKey(AccountType.class, r.getAccountType()); + if (!AccountType.Info.equals(accType)) { + r.setDiffBalance(safeBD(r.getBalance()).subtract(safeBD(r.getCheckBalance()))); + } + r.setUpdated(Instant.now()); + } + + private static class RegistrySearch { + private final ImdgPredicateBuilder pb; + private final Imdg registryImdg; + private ImdgPredicate predicate; + + private RegistrySearch(Imdg registryImdg) { + this.registryImdg = registryImdg; + this.pb = registryImdg.predicateBuilder(); + } + + private static RegistrySearch builder(Imdg registryImdg) { + RegistrySearch registrySearch = new RegistrySearch(registryImdg); + registrySearch.predicate = registrySearch.pb.alwaysTrue(); + return registrySearch; + } + + private RegistrySearch rgsCode(RegistryTradingParams rgsCode) { + String sql = RegistryCodeSqlBuilder.getInstance(rgsCode).build(); + predicate = pb.and(predicate, pb.sql(sql)); + return this; + } + + private RegistrySearch companyId(Long companyId) { + predicate = pb.and(predicate, pb.equals("companyId", companyId)); + return this; + } + + private RegistrySearch accountType(AccountType accountType) { + predicate = pb.and(predicate, pb.equals("accountType", accountType.getKey())); + return this; + } + + private RegistrySearch accountId(Long accountId) { + predicate = pb.and(predicate, pb.equals("accountId", accountId)); + return this; + } + + private RegistrySearch contract(String contract) { + predicate = pb.and(predicate, pb.equals("contract", contract)); + return this; + } + + private RegistrySearch status() { + predicate = pb.and(predicate, pb.or( + pb.equals("registryStatus", RegistryStatus.PROC.getKey()), + pb.equals("registryStatus", RegistryStatus.MNG.getKey()) + )); + return this; + } + + private Optional find() { + return Optional.ofNullable(registryImdg.getSingleObjectByPredicate(predicate)); + } + } + + private Optional findRegByUnit(Long companyId, Long accountId, String contract, Registry registry, RegistryUnit unit) { + RegistryTradingParams p = new RegistryTradingParams( + IEnumKey.getEnumByKey(RegistryDesignation.class, registry.getRegistryDesignation()), + IEnumKey.getEnumByKey(RegistryInstrumentType.class, registry.getRegistryInstrumentType()), + IEnumKey.getEnumByKey(RegistryCapacity.class, registry.getRegistryCapacity()), + unit + ); + String sql = RegistryCodeSqlBuilder.getInstance(p).build(); + ImdgPredicateBuilder pb = registryImdg.predicateBuilder(); + ImdgPredicate rgstrPredicate = pb.and(pb.sql(sql), + pb.sql(sql), + pb.equals("companyId", companyId), + pb.equals("accountId", accountId) + + ); + if (RegistryDesignation.D.equalsByKey(registry.getRegistryDesignation()) && !TextUtil.isEmpty(contract)) { + rgstrPredicate = pb.and(rgstrPredicate, pb.equals("contract", contract)); + } + return Optional.ofNullable(registryImdg.getSingleObjectByPredicate(rgstrPredicate)); + } + + DateTimeFormatter payDateFormatter = DateTimeFormatter.ofPattern("dd.MM.yy"); + + private LocalDate payDate(String payDate) { + if (TextUtil.isEmpty(payDate)) { + return null; + } + return LocalDate.parse(payDate, payDateFormatter); + } + + private static String getTkrCodeFromComment(String comment) { + if (comment == null) { + return null; + } + comment = comment.toUpperCase(); + int tcrIndex = comment.indexOf("ТКР"); + if (tcrIndex == -1) { + return null; + } + comment = comment.substring(tcrIndex + 3); + return comment.replaceAll("\\s+", ""); + } + + private static String getContractFromSpecif(String specif) { + if (specif == null) { + return null; + } + int index = specif.indexOf("№"); + if (index == -1) { + return null; + } + StringBuilder buffer = new StringBuilder(); + String afterN = specif.substring(index + 1); + int countSlashes = 0; + for(int i = 0; i < afterN.length(); i++) { + char c = afterN.charAt(i); + if (!Character.isWhitespace(c)) { + buffer.append(c); + if (c == '/') { + countSlashes++; + } + } else { + if (countSlashes == 3) { + break; + } + } + } + return buffer.toString(); + } + + @FunctionalInterface + private static interface TriFunction { + R apply(T1 arg1, T2 arg2, T3 arg3); + } + +}