accountImdg;
+
+ @PostConstruct
+ void init() {
+ companyImdg = hazelcast.getImdg(IMDGDistributedNames.Map_Company, Company.class);
+ accountImdg = hazelcast.getImdg(IMDGDistributedNames.Map_Account, Account.class);
+ }
+
+ @Test
+ void createAccountBalance() {
+ hazelcast.waitAvailable();
+ Company company = new Company();
+ company.setId(addresseeIdNew);
+ company.setTradingCode("code");
+ company.setShortName("ShortName");
+ company.setFullName("FullName");
+ companyImdg.insert(company);
+ Account account = new Account();
+ account.setId(accountIdNew);
+ account.setAccount("123456789");
+ account.setAccountType(AccountType.Clrn.getKey());
+ account.setAccountStatus(Status.Active.getKey());
+ accountImdg.insert(account);
+
+ AccountBalance accountBalanceNew = new AccountBalance();
+ accountBalanceNew.setId(id);
+ accountBalanceNew.setCompanyId(addresseeIdNew);
+ accountBalanceNew.setAccountId(accountIdNew);
+ accountBalanceNew.setAccountType(account.getAccountType());
+ accountBalanceNew.setAccount(account.getAccount());
+ accountBalanceNew.setOpenBalanceAmount(amountNew);
+ accountBalanceNew.setFreeBalanceAmount(amountNew);
+ accountBalanceNew.setBalanceAmount(amountNew);
+ accountBalanceNew.setBalanceAccountType(BalanceAccountType.Active.getKey());
+ accountBalanceNew.setCurrencyCode(cashMovementCurrencyCodeNew);
+ accountBalanceNew.setTradingCode(company.getTradingCode());
+ accountBalanceNew.setShortName(company.getShortName());
+ accountBalanceNew.setFullName(company.getFullName());
+ AccountResult predictableNewResult = new AccountResult(accountBalanceNew);
+ AccountResult resultNew = accountBalanceService.createAccountBalance(addresseeIdNew, accountIdNew, amountNew, cashMovementCurrencyCodeNew);
+ resultNew.getAccount().setId(id);
+
+ AccountBalance accountBalanceUpdate = new AccountBalance();
+ accountBalanceUpdate.setId(id);
+ accountBalanceUpdate.setCompanyId(addresseeIdUpdate);
+ accountBalanceUpdate.setAccountId(accountIdUpdate);
+ accountBalanceUpdate.setAccountType(account.getAccountType());
+ accountBalanceUpdate.setAccount(account.getAccount());
+ accountBalanceUpdate.setOpenBalanceAmount(amountUpdate);
+ accountBalanceUpdate.setFreeBalanceAmount(amountUpdate);
+ accountBalanceUpdate.setBalanceAmount(amountUpdate);
+ accountBalanceUpdate.setBalanceAccountType(BalanceAccountType.Active.getKey());
+ accountBalanceUpdate.setCurrencyCode(cashMovementCurrencyCodeUpdate);
+ accountBalanceUpdate.setTradingCode(company.getTradingCode());
+ accountBalanceUpdate.setShortName(company.getShortName());
+ accountBalanceUpdate.setFullName(company.getFullName());
+ AccountResult resultUpdate = accountBalanceService.createAccountBalance(addresseeIdUpdate, accountIdUpdate, amountUpdate, cashMovementCurrencyCodeUpdate);
+ resultUpdate.getAccount().setId(id);
+ AccountResult predictableUpdateResult = new AccountResult(accountBalanceUpdate);
+
+ ACCOUNT_BALANCE_MATCHER.assertMatch(resultNew, predictableNewResult);
+ ACCOUNT_BALANCE_MATCHER.assertMatch(resultUpdate, predictableUpdateResult);
+ }
+
+ @Test
+ void validatedCreateAccountBalance() {
+ AccountResult predictableResult;
+ hazelcast.waitAvailable();
+ predictableResult = new AccountResult(new EnumMessage(BalanceError.CompanyNotFound));
+
+ AccountResult result = accountBalanceService.createAccountBalance(addresseeIdNew, accountIdNew, amountNew, cashMovementCurrencyCodeNew);
+ ACCOUNT_BALANCE_MATCHER.assertMatch(result, predictableResult);
+
+ Company company = new Company();
+ company.setId(addresseeIdNew);
+ companyImdg.insert(company);
+
+ Account account = new Account();
+// account.setId(accountIdNew);
+ account.setAccountType(AccountType.Clrn.getKey());
+ account.setAccountStatus(Status.Active.getKey());
+ accountImdg.insert(account);
+ predictableResult = new AccountResult(new EnumMessage(BalanceError.AccountNotPresent));
+
+ result = accountBalanceService.createAccountBalance(addresseeIdNew, accountIdNew, amountNew, cashMovementCurrencyCodeNew);
+ ACCOUNT_BALANCE_MATCHER.assertMatch(result, predictableResult);
+
+ }
+}
\ No newline at end of file
diff --git a/clearing-parent/balance-service/src/test/java/ru/spcex/clearing/balance/utils/MatcherFactory.java b/clearing-parent/balance-service/src/test/java/ru/spcex/clearing/balance/utils/MatcherFactory.java
new file mode 100644
index 000000000..a18ccb4f0
--- /dev/null
+++ b/clearing-parent/balance-service/src/test/java/ru/spcex/clearing/balance/utils/MatcherFactory.java
@@ -0,0 +1,38 @@
+package ru.spcex.clearing.balance.utils;
+
+import java.util.Arrays;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Factory for creating test matchers.
+ *
+ * Comparing actual and expected objects via AssertJ
+ */
+public class MatcherFactory {
+
+ public static Matcher usingIgnoringFieldsComparator(String... fieldsToIgnore) {
+ return new Matcher<>(fieldsToIgnore);
+ }
+
+ public static class Matcher {
+ private final String[] fieldsToIgnore;
+
+ private Matcher(String... fieldsToIgnore) {
+ this.fieldsToIgnore = fieldsToIgnore;
+ }
+
+ public void assertMatch(T actual, T expected) {
+ assertThat(actual).usingRecursiveComparison().ignoringFields(fieldsToIgnore).isEqualTo(expected);
+ }
+
+ @SafeVarargs
+ public final void assertMatch(Iterable actual, T... expected) {
+ assertMatch(actual, Arrays.asList(expected));
+ }
+
+ public void assertMatch(Iterable actual, Iterable expected) {
+ assertThat(actual).usingRecursiveFieldByFieldElementComparatorIgnoringFields(fieldsToIgnore).isEqualTo(expected);
+ }
+ }
+}
diff --git a/clearing-parent/scheduler-service/src/main/java/ru/spcex/clearing/scheduler/enums/IEnumWithLongValue.java b/clearing-parent/scheduler-service/src/main/java/ru/spcex/clearing/scheduler/enums/IEnumWithLongValue.java
new file mode 100644
index 000000000..3e46ccc59
--- /dev/null
+++ b/clearing-parent/scheduler-service/src/main/java/ru/spcex/clearing/scheduler/enums/IEnumWithLongValue.java
@@ -0,0 +1,81 @@
+package ru.spcex.clearing.scheduler.enums;
+
+import java.io.Serializable;
+import java.util.Objects;
+
+/**
+ * Удобный интерфейс для enum при использовании TextErrorService.
+ */
+public interface IEnumWithLongValue extends Serializable {
+
+
+ /**
+ * Проверяет что среди данного набора Enum, присутствует элемент с данным id
+ * id может быть null
+ */
+ static & IEnumWithLongValue> boolean contains(Long id, T... enumSet) {
+ for (T e : enumSet) {
+ if (Objects.equals(id, e.getId()))
+ return true;
+ }
+ return false;
+ }
+
+ static & IEnumWithLongValue> boolean contains(T e, T... enumSet) {
+ for (T enumEl : enumSet) {
+ if (enumEl.equals(e))
+ return true;
+ }
+ return false;
+ }
+
+ static & IEnumWithLongValue> Long[] toLongArray(T... enumSet) {
+ Long[] enumId = new Long[enumSet.length];
+ for (int i = 0; i < enumSet.length; i++)
+ enumId[i] = enumSet[i].getId();
+ return enumId;
+ }
+
+ /**
+ * Проверяет что среди всех Enum данного класса, присутствует элемент с данным id
+ * id может быть null
+ */
+ static & IEnumWithLongValue> boolean contains(Class enumClass, Long id) {
+ for (T e : enumClass.getEnumConstants()) {
+ if (Objects.equals(id, e.getId()))
+ return true;
+ }
+ return false;
+ }
+
+
+ /**
+ * Возвращает Enum по id, если в заданном классе такой определен
+ * id может быть null
+ *
+ * @return Enum если нашел, иначе null
+ */
+ static & IEnumWithLongValue> T getEnumById(Class enumClass, Long id) {
+ for (T e : enumClass.getEnumConstants()) {
+ if (Objects.equals(id, e.getId()))
+ return e;
+ }
+ return null;
+ }
+
+ static Long getIdOrNull(IEnumWithLongValue enumVal) {
+ return enumVal == null ? null : enumVal.getId();
+ }
+
+
+ /**
+ * errorCode
+ **/
+ Long getId();
+
+ default boolean equalsById(Long id) {
+ return id != null && getId().equals(id);
+ }
+
+ String elementName();
+}
diff --git a/clearing-parent/scheduler-service/src/main/java/ru/spcex/clearing/scheduler/enums/TaskStatuses.java b/clearing-parent/scheduler-service/src/main/java/ru/spcex/clearing/scheduler/enums/TaskStatuses.java
new file mode 100644
index 000000000..b4f82ea38
--- /dev/null
+++ b/clearing-parent/scheduler-service/src/main/java/ru/spcex/clearing/scheduler/enums/TaskStatuses.java
@@ -0,0 +1,35 @@
+package ru.spcex.clearing.scheduler.enums;
+
+import java.util.Objects;
+
+public enum TaskStatuses {
+ ACTIVE("ACTV"),
+ BLOCKED("CNCL"),
+ CANCEL("BLKD");
+
+ private final String name;
+
+ TaskStatuses(String name) {
+ this.name = name;
+ }
+
+ public static TaskStatuses getEnumByName(String name) {
+ for (TaskStatuses e : TaskStatuses.values()) {
+ if (Objects.equals(name, e.name()))
+ return e;
+ }
+ return null;
+ }
+
+ public Boolean equalsByName(String name) {
+ return this.name.equalsIgnoreCase(name);
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
+ public String elementName() {
+ return this.name();
+ }
+}
diff --git a/clearing-parent/scheduler-service/src/main/java/ru/spcex/clearing/scheduler/enums/Tasks.java b/clearing-parent/scheduler-service/src/main/java/ru/spcex/clearing/scheduler/enums/Tasks.java
new file mode 100644
index 000000000..b92b42108
--- /dev/null
+++ b/clearing-parent/scheduler-service/src/main/java/ru/spcex/clearing/scheduler/enums/Tasks.java
@@ -0,0 +1,45 @@
+package ru.spcex.clearing.scheduler.enums;
+
+import java.util.Objects;
+
+public enum Tasks {
+ GBAL("GBAL"),//Зачисление остатков
+ ABLK("ABLK"),//Блокировка счета
+ GALB("GALB"),//Запрос остатков по всем счетам
+ ADBL("ADBL"),//Дозачисление/списание остатков
+ CORD("CORD"),//Формирование сводного платежного поручения
+ CORC("CORC"),//Получение подтверждения переводов
+ GTRD("GTRD"),//Получение сделок из Торговой системы
+ GVER("GVER"),// Запуск сверки
+ GBLD("GBLD"),// Поступление средств
+ SCLR("SCLR"),// Запуск клиринговой сессии
+ SPRC("SPRC"),// Запуск преклиринга
+ SPOC("SPOC");// Запуск постклиринга
+
+ private String name;
+
+ Tasks(String name) {
+ this.name = name;
+ }
+
+ public static Tasks getEnumByName(String name) {
+ for (Tasks e : Tasks.values()) {
+ if (Objects.equals(name, e.name()))
+ return e;
+ }
+ return null;
+ }
+
+ public Boolean equalsByName(String name) {
+ return this.name.equalsIgnoreCase(name);
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
+ public String elementName() {
+ return this.name();
+ }
+
+}
diff --git a/clearing-parent/scheduler-service/src/main/java/ru/spcex/clearing/scheduler/service/TaskManager.java b/clearing-parent/scheduler-service/src/main/java/ru/spcex/clearing/scheduler/service/TaskManager.java
new file mode 100644
index 000000000..4bfd70cdb
--- /dev/null
+++ b/clearing-parent/scheduler-service/src/main/java/ru/spcex/clearing/scheduler/service/TaskManager.java
@@ -0,0 +1,224 @@
+package ru.spcex.clearing.scheduler.service;
+
+import com.hazelcast.core.EntryEvent;
+import com.hazelcast.core.HazelcastInstance;
+import com.hazelcast.core.IMap;
+import com.hazelcast.map.listener.EntryAddedListener;
+import com.hazelcast.map.listener.EntryRemovedListener;
+import com.hazelcast.map.listener.EntryUpdatedListener;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.scheduling.TaskScheduler;
+import ru.clearing.classes.statics.data.scheduler.PlannerAllToday;
+import ru.spcex.clearing.scheduler.enums.TaskStatuses;
+import ru.spcex.clearing.scheduler.enums.Tasks;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.ZoneId;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ScheduledFuture;
+import java.util.stream.Collectors;
+
+import static ru.spcex.clearing.imdg.IMDGDistributedNames.Map_PlannerAllToday;
+import static ru.spcex.clearing.scheduler.enums.TaskStatuses.*;
+
+/**
+ * Планировщик задач, расписание берёт из Hazelcast map.
+ *
+ */
+public abstract class TaskManager implements EntryAddedListener,
+ EntryUpdatedListener, EntryRemovedListener,
+ InitializingBean {
+ private static final Logger log = LoggerFactory.getLogger(TaskManager.class);
+
+ protected HazelcastInstance hazelcastInstance;
+ protected IMap plannerAllTodayMapStore;
+ protected TaskScheduler taskScheduler;
+
+ protected ConcurrentHashMap scheduledJobs;
+
+ protected TaskManager(TaskScheduler taskScheduler, HazelcastInstance hazelcastInstance) {
+ this.taskScheduler = taskScheduler;
+ this.hazelcastInstance = hazelcastInstance;
+ }
+
+ private static LocalDateTime dateOldTypeConvert(Date oldDate) {
+ return LocalDateTime.ofInstant(oldDate.toInstant(), ZoneId.systemDefault());
+ }
+
+ private static LocalDate dateTypeConvert(Date oldDate) {
+ return dateOldTypeConvert(oldDate).toLocalDate();
+ }
+
+ private static LocalTime timeTypeConvert(Date oldDate) {
+ return dateOldTypeConvert(oldDate).toLocalTime();
+ }
+
+ @Override
+ public void afterPropertiesSet() {
+ plannerAllTodayMapStore = hazelcastInstance.getMap(Map_PlannerAllToday);
+ scheduledJobs = new ConcurrentHashMap<>();
+ updateScheduler();
+ }
+
+ // --- Слушатели Hazelcast Map ---
+ @Override
+ public void entryAdded(EntryEvent event) {
+ PlannerAllToday task = event.getValue();
+
+ Tasks taskType = Tasks.getEnumByName(task.getTask());
+ if (taskType == null) {
+ log.warn("Task skipped, {} task type not recognized", task.getTask());
+ return;
+ }
+ processTask(task);
+ }
+
+ @Override
+ public void entryUpdated(EntryEvent event) {
+ PlannerAllToday task = event.getValue();
+ PlannerAllToday oldTask = event.getOldValue();
+
+ LocalTime oldTime = oldTask.getTaskTime();
+// TaskStatuses oldStatus
+ //если таск относится к другому обработчику, пропускаем
+ if (!getTask().equalsByName(task.getTask()) && !getTask().equalsByName(oldTask.getTask())) {
+ return;
+ }
+ if (!Objects.equals(task.getTask(), oldTask.getTask())) {
+ throw new IllegalStateException("changed taskId for SchedulerAllToday in core");
+ }
+ if (ACTIVE.equalsByName(oldTask.getTaskStatus())) { //&& ACTIVE.equalsById(task.getTaskStatusId())
+ if (!removeTask(oldTime)) {
+ log.debug("cannot cancel task with type {}, time {}", getTask().name(), oldTime.toString());
+ } else {
+ log.debug("task with type {}, time {} execution cancelled, adding altered task...", getTask().name(), oldTime.toString());
+ }
+ processTask(task);
+ } else if (CANCEL.equalsByName(oldTask.getTaskStatus())) { //&& TaskStatuses.CANCEL.equalsById(task.getTaskStatusId())
+ restorePreviouslyRemovedTask(oldTime, oldTask);
+ processTask(task);
+ } else if (BLOCKED.equalsByName(oldTask.getTaskStatus())) {
+ processTask(task);
+ }
+ }
+
+ @Override
+ public void entryRemoved(EntryEvent event) {
+ PlannerAllToday taskToRemove = event.getOldValue();
+ if (taskToRemove == null) {
+ log.debug("no task in removed event");
+ return;
+ }
+ LocalTime removedTaskTime = taskToRemove.getTaskTime();
+
+ if (ACTIVE.equalsByName(taskToRemove.getTaskStatus())) {
+ if (removeTask(removedTaskTime))
+ log.debug("task successfully canceled");
+ } else if (CANCEL.equalsByName(taskToRemove.getTaskStatus())) {
+ restorePreviouslyRemovedTask(removedTaskTime, taskToRemove);
+ } else if (BLOCKED.equalsByName(taskToRemove.getTaskStatus())) {
+ log.debug("BLOCKED task removed; do nothing");
+ }
+ }
+
+ private void restorePreviouslyRemovedTask(LocalTime oldTime, PlannerAllToday oldTask) {
+ ScheduledFuture cancelledFuture = scheduledJobs.get(oldTime);
+ if (cancelledFuture != null && cancelledFuture.isCancelled()) {
+ PlannerAllToday schedulerAllToday = new PlannerAllToday();
+ schedulerAllToday.setTask(getTask().getName());
+ schedulerAllToday.setTaskStatus(ACTIVE.getName());
+ schedulerAllToday.setTaskTime(oldTask.getTaskTime());
+ log.debug("CANCEL task updated/removed; restoring previously cancelled task");
+ processTask(schedulerAllToday);
+ }
+ }
+
+ // --- Планирование задач ---
+ protected void updateScheduler() {
+ Collection schedulerAllTodays = plannerAllTodayMapStore.values();
+ Collection sortedSchedulers =
+ schedulerAllTodays.stream().sorted((o1, o2) -> (ACTIVE.equalsByName(o1.getTaskStatus()) && CANCEL.equalsByName(o2.getTaskStatus())) ? -1 : 0)
+ .collect(Collectors.toCollection(ArrayList::new));
+ for (PlannerAllToday schedulerAllToday : sortedSchedulers) {
+ processTask(schedulerAllToday);
+ }
+ }
+
+ // --- Работа с задачами ---
+ private void processTask(PlannerAllToday task) {
+ LocalTime taskTime = task.getTaskTime();
+ Tasks taskType = Tasks.getEnumByName(task.getTask());
+ TaskStatuses taskStatus = TaskStatuses.getEnumByName(task.getTaskStatus());
+ if (taskStatus == null) throw new IllegalStateException("task status from core can't be null");
+ if (!getTask().equals(taskType)) {
+ log.debug("Task skipped - taskTime {}, is not of acceptable type {}", taskTime.toString(), taskType != null ? taskType.name() : "");
+ return;
+ }
+ if (taskTime.isBefore(LocalTime.now())) {
+ log.debug("Task skipped - taskTime {} is before now, taskType {} ok", taskTime, getTask().toString());
+ return;
+ }
+ if (taskStatus.equals(BLOCKED)) {
+ log.debug("Task skipped - timeTime {} with status {}", taskTime.toString(), BLOCKED.toString());
+ return;
+ }
+ {
+ ScheduledFuture future = scheduledJobs.get(taskTime);
+ if (future != null) {
+ if (TaskStatuses.CANCEL.equals(taskStatus)) {
+ //случай когда пришел cancel, пытаемся отменить зарегистрированный ранее таск
+ future.cancel(false);
+ log.debug("cancelling task time {}", taskTime);
+ return;
+ } else if (TaskStatuses.ACTIVE.equals(taskStatus)) {
+ //случай когда пришел активный таск, и уже был на это время неотмененный
+ if (!future.isCancelled()) {
+ log.debug("such task time {} has already been registered", taskTime);
+ return;
+ }
+ }
+ }
+ }
+ if (TaskStatuses.CANCEL.equals(taskStatus)) {
+ log.debug("task type {} time {} with status CANCEL - no tasks to cancel found", taskType.name(), taskTime);
+ return;
+ }
+ //пришел активный таск
+ log.debug("adding task type {}, time {}", taskType.name(), taskTime);
+ ScheduledFuture future = taskScheduler.schedule(() -> doJob(task), LocalDateTime.of(LocalDate.now(), taskTime).atZone(ZoneId.systemDefault()).toInstant());
+ ScheduledFuture oldFuture = scheduledJobs.put(taskTime, future);
+ if (oldFuture != null && !oldFuture.isCancelled()) { //for synchronization, never
+ log.warn("tasks were added simultaneously, cancel former one");
+ boolean success = oldFuture.cancel(false);
+ log.warn("cancelling task " + (success ? "success" : "fail"));
+ }
+ }
+
+ private boolean removeTask(LocalTime taskTime) {
+ ScheduledFuture future = scheduledJobs.remove(taskTime);
+ if (future == null) {
+ log.debug("can't cancel task type {}, time {}, not found", getTask().name(), taskTime);
+ return false;
+ }
+ return future.cancel(false);
+ }
+
+ // --- Реализация выполнения задач ---
+
+ /**
+ * Рабочий тип запланированных задач.
+ *
+ * @return идентификатор для фильтра типов планировщика задачь. Планировать задачи только этого типа.
+ */
+ protected abstract Tasks getTask();
+
+ protected abstract void doJob(PlannerAllToday taskInfo);
+}