Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6a450e2ac6 | |||
| 9fc87a705f | |||
| 305864c1ed | |||
| ad83d9cc77 | |||
| 226c7f9473 | |||
| 137d1ca8a4 | |||
| 59744e9d23 | |||
| ac8836628e | |||
| 1f649c8951 | |||
| 310468fb69 | |||
| 7f782e0a92 | |||
| d0c9f8bffc | |||
| 60a35f6f76 | |||
| a3a2923751 | |||
| 32e6a45f05 | |||
| 59665c6df4 | |||
| d23b4e3aac | |||
| 20897cf199 | |||
| 0b57232606 | |||
| 1bd8f003f7 | |||
| fcb614cd90 | |||
| 1c376c003a | |||
| a433ceaf1d | |||
| 850478b7f7 |
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"agentCanUpdateSnapshot": true
|
||||
}
|
||||
Vendored
+8
-5
@@ -1,16 +1,19 @@
|
||||
{
|
||||
"editor.bracketPairColorization.enabled": true,
|
||||
"editor.guides.bracketPairs": true,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.tabCompletion": "onlySnippets",
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint":"explicit"
|
||||
},
|
||||
// "editor.formatOnSave": true,
|
||||
// "editor.codeActionsOnSave": {
|
||||
// "source.addMissingImports.ts": "explicit",
|
||||
// "source.fixAll.ts": "explicit",
|
||||
// "source.fixAll.eslint": "explicit"
|
||||
// },
|
||||
"eslint.run": "onType",
|
||||
"eslint.validate": ["javascript", "typescript", "vue"],
|
||||
"i18n-ally.localesPaths": ["src/i18n"],
|
||||
"typescript.format.enable": true,
|
||||
"typescript.format.indentSwitchCase": false,
|
||||
"typescript.validate.enable": true
|
||||
"typescript.validate.enable": true,
|
||||
"notebook.defaultFormatter": "Vue.volar"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,63 @@
|
||||
# v2025.7.1-1
|
||||
|
||||
В этом релизе реализованы механизмы возврата паевого взноса пайщика и инструменты управления этим процессом для членов совета. Также внесены визуальные доработки интерфейсов для улучшения восприятия информации.
|
||||
|
||||
✨ Новые функции
|
||||
- [#276](https://github.com/coopenomics/mono/issues/276): Реализовать путь возврата паевого взноса из кошелька пайщика
|
||||
- [#281](https://github.com/coopenomics/mono/issues/281): Генерация заявления на возврат паевого взноса
|
||||
- [#282](https://github.com/coopenomics/mono/issues/282): Генерация решения совета на возврат паевого взноса
|
||||
- [#278](https://github.com/coopenomics/mono/issues/278): Введение методов управления возвратами паевых взносов в контроллере
|
||||
- [#279](https://github.com/coopenomics/mono/issues/279): Отобразить исходящие платежи с кнопками управления в реестре платежей для совета
|
||||
|
||||
🐛 Исправления ошибок
|
||||
- [#286](https://github.com/coopenomics/mono/issues/286): Поправить ошибки склонений времени
|
||||
- [#262](https://github.com/coopenomics/mono/issues/262): Баг: рабочий стол совета включается, однако, страница всегда открывается со стола пайщика
|
||||
- [#261](https://github.com/coopenomics/mono/issues/261): Баг: первая загрузка переадресует на главную страницу всегда - прямой переход на собрание становится недоступен.
|
||||
|
||||
🔧 Улучшения
|
||||
- [#285](https://github.com/coopenomics/mono/issues/285): Корректировка дизайна кошелька, профиля, повестки совета, реестра документов, реестра платежей
|
||||
- [#284](https://github.com/coopenomics/mono/issues/284): Разместить кошелек на главную вместо профиля
|
||||
- [#283](https://github.com/coopenomics/mono/issues/283): Ввести лоадер на переходе между рабочими столами
|
||||
- [#280](https://github.com/coopenomics/mono/issues/280): Мигрировать имеющиеся данные о входящих платежах в новую модель
|
||||
- [#277](https://github.com/coopenomics/mono/issues/277): Рефакторинг модуля платежей: переход на унифицированную модель входящего и исходящего платежа
|
||||
|
||||
#releases
|
||||
|
||||
---
|
||||
|
||||
# v2025.6.14
|
||||
|
||||
Разработан модуль для проведения очередных общих собраний пайщиков. Исправлены баги, внесены улучшения пользовательского интерфейса.
|
||||
|
||||
✨ Новые функции
|
||||
- [#264](https://github.com/coopenomics/mono/issues/264): Разработан смарт-контракт общего собрания пайщиков (meet)
|
||||
- [#263](https://github.com/coopenomics/mono/issues/263): Реализован модуль оповещений на электронные почты по жизненному циклу общего собрания пайщиков
|
||||
- [#273](https://github.com/coopenomics/mono/issues/273): Встроены реальные шаблоны документов общего собрания
|
||||
- [#268](https://github.com/coopenomics/mono/issues/268): Добавлена страница результатов общего собрания
|
||||
- [#267](https://github.com/coopenomics/mono/issues/267): Добавлена страница просмотра и скачивания бюллетеней и уведомлений по собранию
|
||||
- [#270](https://github.com/coopenomics/mono/issues/270): Ссылка в оповещении ведет на страницу собрания с документом-уведомлением для подписи
|
||||
|
||||
🐛 Исправления ошибок
|
||||
- [#262](https://github.com/coopenomics/mono/issues/262): Исправлено некорректное открытие рабочего стола совета
|
||||
- [#261](https://github.com/coopenomics/mono/issues/261): Исправлена ошибка с редиректом при первой загрузке и прямом переходе на собрание
|
||||
- [#259](https://github.com/coopenomics/mono/issues/259): Исправлены отступы в мобильной карточке пайщика
|
||||
- [#258](https://github.com/coopenomics/mono/issues/258): Убран hover-эффект на документе и пайщике в таблице
|
||||
|
||||
🔧 Улучшения
|
||||
- [#274](https://github.com/coopenomics/mono/issues/274): Настроена рассылка оповещений на почту при получении решения о проведении собрания
|
||||
- [#272](https://github.com/coopenomics/mono/issues/272): Подписанные уведомления сохраняются и извлекаются из реестра по Graph-QL
|
||||
- [#271](https://github.com/coopenomics/mono/issues/271): Ведется подсчет количества пайщиков в каждом кооперативе при добавлении и удалении
|
||||
- [#260](https://github.com/coopenomics/mono/issues/260): Введен счетчик количества пайщиков в кооперативе на контракте регистратора
|
||||
- [#256](https://github.com/coopenomics/mono/issues/256): Отображается статус членства каждого пайщика
|
||||
- [#255](https://github.com/coopenomics/mono/issues/255): В разделе Платежи отображается ФИО/Наименование плательщика
|
||||
- [#269](https://github.com/coopenomics/mono/issues/269): Реализована рассылка уведомлений перед началом собрания
|
||||
- [#265](https://github.com/coopenomics/mono/issues/265): Проведена отладка и тестирование процесса общего собрания пайщиков
|
||||
- [#266](https://github.com/coopenomics/mono/issues/266): Протестированы все процессы общего собрания
|
||||
|
||||
#releases
|
||||
|
||||
---
|
||||
|
||||
# v2025.5.14
|
||||
|
||||
В этом релизе реализован новый стандарт передачи и хранения документов по блокчейну с поддержкой неограниченного количества подписей и их валидацией. Также внесены улучшения в интерфейс и исправлены ошибки.
|
||||
|
||||
Vendored
BIN
Binary file not shown.
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@coopenomics/boot",
|
||||
"type": "module",
|
||||
"version": "2025.6.13",
|
||||
"version": "2025.7.2",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@9.0.6",
|
||||
"description": "",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@coopenomics/cleos",
|
||||
"version": "2025.6.13",
|
||||
"version": "2025.7.2",
|
||||
"private": true,
|
||||
"description": "",
|
||||
"scripts": {
|
||||
|
||||
@@ -14,6 +14,12 @@ void gateway::createoutpay(eosio::name coopname, eosio::name username, checksum2
|
||||
eosio::check(confirm_callback != ""_n, "Действие для коллбэка-успеха должено быть установлено");
|
||||
eosio::check(decline_callback != ""_n, "Действие для коллбэка-отлонения должно быть установлено");
|
||||
|
||||
// Валидируем callback действия если они от wallet контракта
|
||||
if (callback_contract == _wallet) {
|
||||
Wallet::get_valid_wallet_action(confirm_callback);
|
||||
Wallet::get_valid_wallet_action(decline_callback);
|
||||
}
|
||||
|
||||
auto outcome = Gateway::get_outcome(coopname, outcome_hash);
|
||||
|
||||
eosio::check(!outcome.has_value(), "Объект возврата уже существует с указанным хэшем");
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
void gateway::outcomplete(eosio::name coopname, checksum256 outcome_hash){
|
||||
require_auth(coopname);
|
||||
|
||||
auto exist_outcome = Gateway::get_outcome(coopname, outcome_hash);
|
||||
eosio::check(exist_outcome.has_value(), "Объект возврата не существует с указанным хэшем");
|
||||
|
||||
@@ -9,13 +8,15 @@ void gateway::outcomplete(eosio::name coopname, checksum256 outcome_hash){
|
||||
auto outcome = outcomes.find(exist_outcome -> id);
|
||||
|
||||
eosio::check(outcome -> status == "pending"_n, "Только принятые заявления на вывод могут быть обработаны");
|
||||
|
||||
action(
|
||||
permission_level{ _gateway, "active"_n},
|
||||
|
||||
// Используем интерфейс для типизированного вызова callback действия
|
||||
Action::send<completewthd_interface>(
|
||||
outcome -> callback_contract,
|
||||
outcome -> confirm_callback,
|
||||
std::make_tuple(coopname, outcome -> outcome_hash)
|
||||
).send();
|
||||
_gateway,
|
||||
coopname,
|
||||
outcome -> outcome_hash
|
||||
);
|
||||
|
||||
outcomes.erase(outcome);
|
||||
};
|
||||
|
||||
@@ -2,19 +2,22 @@ void gateway::outdecline(eosio::name coopname, checksum256 outcome_hash, std::st
|
||||
require_auth(coopname);
|
||||
|
||||
auto exist_outcome = Gateway::get_outcome(coopname, outcome_hash);
|
||||
eosio::check(!exist_outcome.has_value(), "Объект возврата уже существует с указанным хэшем");
|
||||
eosio::check(exist_outcome.has_value(), "Объект возврата не существует с указанным хэшем");
|
||||
Gateway::outcomes_index outcomes(_gateway, coopname.value);
|
||||
|
||||
auto outcome = outcomes.find(exist_outcome -> id);
|
||||
|
||||
eosio::check(outcome -> status == "pending"_n, "Только принятые заявления на вывод могут быть обработаны");
|
||||
|
||||
action(
|
||||
permission_level{ _gateway, "active"_n},
|
||||
outcome -> callback_contract,
|
||||
outcome -> decline_callback,
|
||||
std::make_tuple(coopname, outcome -> outcome_hash, reason)
|
||||
).send();
|
||||
// Используем интерфейс для типизированного вызова callback действия
|
||||
Action::send<declinewthd_interface>(
|
||||
outcome -> callback_contract,
|
||||
outcome -> decline_callback,
|
||||
_gateway,
|
||||
coopname,
|
||||
outcome -> outcome_hash,
|
||||
reason
|
||||
);
|
||||
|
||||
outcomes.erase(outcome);
|
||||
};
|
||||
|
||||
@@ -21,5 +21,6 @@
|
||||
#include "shared_approver.hpp"
|
||||
#include "shared_registrator.hpp"
|
||||
#include "shared_loan.hpp"
|
||||
#include "shared_branch.hpp"
|
||||
#include "approves.hpp"
|
||||
#include "debts.hpp"
|
||||
@@ -114,17 +114,11 @@ static const std::set<eosio::name> soviet_actions = {
|
||||
"capwthdrprog"_n, //произвести возврат накопленных членских взносов по программе на капиталиста
|
||||
"capwthdrproj"_n, //произвести возврат накопленных членских взносов по проекту на актора
|
||||
"capwthdrres"_n, //произвести возврат из задания
|
||||
};
|
||||
|
||||
static const std::set<eosio::name> gateway_income_actions = {
|
||||
"deposit"_n, //паевой взнос по ЦПП Кошелёк
|
||||
//WALLET
|
||||
"createwthd"_n, //создать заявление на возврат паевого взноса
|
||||
};
|
||||
|
||||
static const std::set<eosio::name> gateway_outcome_actions = {
|
||||
"withdraw"_n, //возврат паевого взноса по ЦПП Кошелёк
|
||||
};
|
||||
|
||||
|
||||
//program_ids
|
||||
static constexpr uint64_t _wallet_program_id = 1;
|
||||
static constexpr uint64_t _sosedi_program_id = 2;
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
#pragma once
|
||||
|
||||
namespace Branch {
|
||||
/**
|
||||
* @brief Получает количество кооперативных участков для данного кооператива
|
||||
* @param coopname Имя кооператива
|
||||
* @return Количество участков кооператива
|
||||
*/
|
||||
uint64_t get_branch_count(eosio::name coopname) {
|
||||
branchstat_index stat(_branch, _branch.value);
|
||||
auto st = stat.find(coopname.value);
|
||||
|
||||
if (st == stat.end()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return st->count;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Проверяет, является ли пользователь уполномоченным лицом (trustee) кооперативного участка
|
||||
* @param coopname Имя кооператива
|
||||
* @param username Имя пользователя для проверки
|
||||
* @return true если пользователь является уполномоченным, false в противном случае
|
||||
*/
|
||||
bool is_trustee(eosio::name coopname, eosio::name username) {
|
||||
branch_index branches(_branch, coopname.value);
|
||||
|
||||
auto by_trustee_index = branches.get_index<"bytrustee"_n>();
|
||||
auto trustee_itr = by_trustee_index.find(username.value);
|
||||
|
||||
return trustee_itr != by_trustee_index.end();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Проверяет, является ли пользователь доверенным лицом (trusted) в указанном участке кооператива
|
||||
* @param coopname Имя кооператива
|
||||
* @param braname Имя участка
|
||||
* @param username Имя пользователя для проверки
|
||||
* @return true если пользователь является доверенным, false в противном случае
|
||||
*/
|
||||
bool is_trusted(eosio::name coopname, eosio::name braname, eosio::name username) {
|
||||
branch_index branches(_branch, coopname.value);
|
||||
auto branch_itr = branches.find(braname.value);
|
||||
|
||||
if (branch_itr == branches.end()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return branch_itr->is_account_in_trusted(username);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,6 +3,16 @@
|
||||
using createoutpay_interface = void(CREATEOUTPAY_SIGNATURE);
|
||||
|
||||
namespace Gateway {
|
||||
|
||||
static const std::set<eosio::name> gateway_income_actions = {
|
||||
"deposit"_n, //паевой взнос по ЦПП Кошелёк
|
||||
};
|
||||
|
||||
static const std::set<eosio::name> gateway_outcome_actions = {
|
||||
"withdraw"_n, //возврат паевого взноса по ЦПП Кошелёк
|
||||
};
|
||||
|
||||
|
||||
inline eosio::name get_valid_income_action(const eosio::name& action) {
|
||||
eosio::check(gateway_income_actions.contains(action), "Недопустимое имя действия");
|
||||
return action;
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
#define COMPLETEWTHD_SIGNATURE name coopname, checksum256 withdraw_hash
|
||||
#define DECLINEWTHD_SIGNATURE name coopname, checksum256 withdraw_hash, std::string reason
|
||||
|
||||
using completewthd_interface = void(COMPLETEWTHD_SIGNATURE);
|
||||
using declinewthd_interface = void(DECLINEWTHD_SIGNATURE);
|
||||
|
||||
namespace Wallet {
|
||||
|
||||
static const std::set<eosio::name> wallet_callback_actions = {
|
||||
"authwthd"_n, // авторизация возврата паевого взноса
|
||||
"declinewthd"_n, // отклонение возврата паевого взноса
|
||||
"completewthd"_n, // завершение возврата паевого взноса
|
||||
};
|
||||
|
||||
inline eosio::name get_valid_wallet_action(const eosio::name& action) {
|
||||
eosio::check(wallet_callback_actions.contains(action), "Недопустимое имя действия wallet");
|
||||
return action;
|
||||
}
|
||||
|
||||
inline void validate_asset(const eosio::asset& amount) {
|
||||
check(amount.symbol == _root_govern_symbol, "Invalid token symbol");
|
||||
check(amount.is_valid(), "Invalid asset");
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
// Заглушка для получения числа пайщиков кооператива.
|
||||
uint64_t get_total_participants(eosio::name coopname) {
|
||||
// Используем метод из shared_registrator для получения актуального количества активных пайщиков
|
||||
// Получаем кооператив и проверяем режим участков
|
||||
auto cooperative = get_cooperative_or_fail(coopname);
|
||||
|
||||
// Если включен режим кооперативных участков, используем количество участков
|
||||
if (cooperative.is_branched) {
|
||||
return Branch::get_branch_count(coopname);
|
||||
}
|
||||
|
||||
// Иначе используем обычный подсчет активных пайщиков
|
||||
return Registrator::get_active_participants_count(coopname);
|
||||
}
|
||||
|
||||
@@ -14,6 +22,14 @@ void meet::vote(name coopname, checksum256 hash, name username, document2 ballot
|
||||
eosio::check(meet_opt.has_value(), "Собрание не найдено");
|
||||
auto meet_record = meet_opt.value();
|
||||
|
||||
// Получаем кооператив для проверки режима участков
|
||||
auto cooperative = get_cooperative_or_fail(coopname);
|
||||
|
||||
// Если включен режим кооперативных участков, проверяем что голосующий является уполномоченным
|
||||
if (cooperative.is_branched) {
|
||||
eosio::check(Branch::is_trustee(coopname, username), "Только уполномоченные кооперативных участков могут голосовать при режиме участков");
|
||||
}
|
||||
|
||||
// Проверяем статус собрания и временное окно голосования
|
||||
eosio::check(meet_record.status == "authorized"_n, "Собрание не авторизовано для голосования");
|
||||
auto now = current_time_point();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//TODO: нужно вырезать этот метод, заменив его на что-то другое.
|
||||
// сейчас мы в лоб проходим по всем пайщикам и отключаем их от кооперативного участка, в случае, если он удален.
|
||||
// так вообще не то. Годится только на малых масштабах.
|
||||
// так вообще не то. Годится только на малых масштабах (до 100 пайщиков на участке будет ок)
|
||||
/**
|
||||
* @brief Эффект удаления кооперативного участка
|
||||
* @param coopname Имя кооператива
|
||||
|
||||
@@ -25,6 +25,6 @@ void wallet::approvewthd(eosio::name coopname, checksum256 withdraw_hash, docume
|
||||
withdraw -> statement,
|
||||
std::string("")
|
||||
)
|
||||
).send();
|
||||
).send();
|
||||
|
||||
}
|
||||
@@ -14,16 +14,16 @@ void wallet::authwthd(eosio::name coopname, checksum256 withdraw_hash) {
|
||||
});
|
||||
|
||||
// создаём объект исходящего платежа в gateway с коллбэком после обработки
|
||||
action(permission_level{ _capital, "active"_n}, _gateway, "createoutpay"_n,
|
||||
std::make_tuple(
|
||||
coopname,
|
||||
withdraw -> username,
|
||||
withdraw_hash,
|
||||
withdraw -> quantity,
|
||||
_wallet,
|
||||
"completewthd"_n,
|
||||
"declinewthd"_n
|
||||
)
|
||||
).send();
|
||||
|
||||
Action::send<createoutpay_interface>(
|
||||
_gateway,
|
||||
"createoutpay"_n,
|
||||
_wallet,
|
||||
coopname,
|
||||
withdraw -> username,
|
||||
withdraw_hash,
|
||||
withdraw -> quantity,
|
||||
_wallet,
|
||||
Wallet::get_valid_wallet_action("completewthd"_n),
|
||||
Wallet::get_valid_wallet_action("declinewthd"_n)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
void wallet::completewthd(eosio::name coopname, checksum256 withdraw_hash, std::string memo) {
|
||||
void wallet::completewthd(COMPLETEWTHD_SIGNATURE) {
|
||||
require_auth(_gateway);
|
||||
|
||||
auto exist = Wallet::get_withdraw(coopname, withdraw_hash);
|
||||
@@ -16,7 +16,7 @@ void wallet::completewthd(eosio::name coopname, checksum256 withdraw_hash, std::
|
||||
std::make_tuple(coopname, withdraw -> quantity, false)
|
||||
).send();
|
||||
|
||||
std::string memo_in = "Возврат части целевого паевого взноса по ЦПП 'Цифровой Кошелёк'";
|
||||
std::string memo_in = "Возврат части паевого взноса по ЦПП 'Цифровой Кошелёк'";
|
||||
Wallet::sub_blocked_funds(_wallet, coopname, withdraw -> username, withdraw -> quantity, _wallet_program, memo_in);
|
||||
|
||||
withdraws.erase(withdraw);
|
||||
|
||||
@@ -28,22 +28,20 @@ void wallet::createwthd(eosio::name coopname, eosio::name username, checksum256
|
||||
d.created_at = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
|
||||
});
|
||||
|
||||
//TODO: перевести approve на hash-идентификатор???
|
||||
action(
|
||||
permission_level{_capital, "active"_n}, // кто вызывает
|
||||
Action::send<createagenda_interface>(
|
||||
_soviet,
|
||||
"createapprv"_n,
|
||||
std::make_tuple(
|
||||
coopname,
|
||||
username,
|
||||
statement,
|
||||
withdraw_hash,
|
||||
_wallet, // callback_contract (текущий контракт)
|
||||
"approvewthd"_n, // callback_action_approve
|
||||
"declinewthd"_n, // callback_action_decline
|
||||
std::string("")
|
||||
)
|
||||
).send();
|
||||
"createagenda"_n,
|
||||
_wallet,
|
||||
coopname,
|
||||
username,
|
||||
get_valid_soviet_action("createwthd"_n),
|
||||
withdraw_hash,
|
||||
_wallet, // callback_contract (текущий контракт)
|
||||
Wallet::get_valid_wallet_action("authwthd"_n), // callback_action_approve
|
||||
Wallet::get_valid_wallet_action("declinewthd"_n), // callback_action_decline
|
||||
statement,
|
||||
std::string("")
|
||||
);
|
||||
|
||||
std::string memo_in = "Возврат части целевого паевого взноса по ЦПП 'Цифровой Кошелёк'";
|
||||
Wallet::block_funds(_wallet, coopname, username, quantity, _wallet_program, memo_in);
|
||||
|
||||
@@ -25,8 +25,8 @@ public:
|
||||
|
||||
//возврат паевого взноса
|
||||
[[eosio::action]] void createwthd(eosio::name coopname, eosio::name username, checksum256 withdraw_hash, eosio::asset quantity, document2 statement);
|
||||
[[eosio::action]] void completewthd(eosio::name coopname, checksum256 withdraw_hash, std::string memo);
|
||||
[[eosio::action]] void declinewthd(eosio::name coopname, checksum256 withdraw_hash, std::string reason);
|
||||
[[eosio::action]] void completewthd(COMPLETEWTHD_SIGNATURE);
|
||||
[[eosio::action]] void declinewthd(DECLINEWTHD_SIGNATURE);
|
||||
[[eosio::action]] void authwthd(eosio::name coopname, checksum256 withdraw_hash);
|
||||
[[eosio::action]] void approvewthd(eosio::name coopname, checksum256 withdraw_hash, document2 approved_statement);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@coopenomics/contracts",
|
||||
"version": "2025.6.13",
|
||||
"version": "2025.7.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build:all": "./build-all.sh",
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
-- Исправление PRIMARY KEY в таблице payments
|
||||
|
||||
-- Сначала проверим текущую структуру
|
||||
\d payments
|
||||
|
||||
-- Если таблица существует и у неё есть PK на hash, то:
|
||||
|
||||
-- 1. Удаляем старый PRIMARY KEY (если есть)
|
||||
DO $$
|
||||
DECLARE
|
||||
pk_name TEXT;
|
||||
BEGIN
|
||||
SELECT conname INTO pk_name
|
||||
FROM pg_constraint
|
||||
WHERE conrelid = 'payments'::regclass AND contype = 'p';
|
||||
|
||||
IF pk_name IS NOT NULL THEN
|
||||
EXECUTE 'ALTER TABLE payments DROP CONSTRAINT ' || pk_name;
|
||||
RAISE NOTICE 'Удален PRIMARY KEY: %', pk_name;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- 2. Добавляем колонку id если её нет
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'payments' AND column_name = 'id') THEN
|
||||
ALTER TABLE payments ADD COLUMN id uuid DEFAULT gen_random_uuid();
|
||||
UPDATE payments SET id = gen_random_uuid() WHERE id IS NULL;
|
||||
ALTER TABLE payments ALTER COLUMN id SET NOT NULL;
|
||||
RAISE NOTICE 'Добавлена колонка id';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- 3. Создаем новый PRIMARY KEY по id
|
||||
ALTER TABLE payments ADD PRIMARY KEY (id);
|
||||
|
||||
-- 4. Делаем hash nullable
|
||||
ALTER TABLE payments ALTER COLUMN hash DROP NOT NULL;
|
||||
|
||||
-- Проверяем результат
|
||||
\d payments
|
||||
@@ -0,0 +1,32 @@
|
||||
# Миграции
|
||||
|
||||
## Правила именования
|
||||
|
||||
- Обычные миграции: `V{версия}__описание.ts` (например, `V1.0.0__initial.ts`)
|
||||
- Тестовые миграции: `V{версия}__описание__test.ts` (например, `V1.0.2__blockchain_data_sync__test.ts`)
|
||||
|
||||
## Поведение
|
||||
|
||||
- Обычные миграции применяются только один раз, если их версия ещё не применялась.
|
||||
- Тестовые миграции (файлы с `__test` в имени) запускаются всегда, даже если версия уже применялась.
|
||||
- Флаг `isTest` в коде миграции больше не используется — только по имени файла.
|
||||
- Если в миграции указан параметр `validUntil` (дата), то миграция не будет применена, если эта дата уже прошла (по UTC).
|
||||
|
||||
## Пример
|
||||
|
||||
- `V1.0.0__initial.ts` — обычная миграция
|
||||
- `V1.0.2__blockchain_data_sync__test.ts` — тестовая миграция
|
||||
|
||||
## validUntil
|
||||
|
||||
В объекте миграции можно указать поле `validUntil` типа `Date`. Если текущая дата превышает это значение, миграция не будет применена.
|
||||
|
||||
```ts
|
||||
export default {
|
||||
name: 'example',
|
||||
validUntil: new Date('2024-07-01T00:00:00Z'),
|
||||
up: async ({ blockchain }) => {
|
||||
/* ... */
|
||||
},
|
||||
};
|
||||
```
|
||||
@@ -0,0 +1,265 @@
|
||||
import { Order } from '../src/models';
|
||||
import { TypeOrmPaymentRepository } from '../src/infrastructure/database/typeorm/repositories/typeorm-payment.repository';
|
||||
import { PaymentEntity } from '../src/infrastructure/database/typeorm/entities/payment.entity';
|
||||
import { DataSource } from 'typeorm';
|
||||
import config from '../src/config/config';
|
||||
import { PaymentStatusEnum } from '../src/domain/gateway/enums/payment-status.enum';
|
||||
import { PaymentTypeEnum, PaymentDirectionEnum, getPaymentDirection } from '../src/domain/gateway/enums/payment-type.enum';
|
||||
import type { PaymentDomainInterface } from '../src/domain/gateway/interfaces/payment-domain.interface';
|
||||
import { sha256 } from '../src/utils/sha256';
|
||||
import { generator } from '../src/services/document.service';
|
||||
|
||||
export default {
|
||||
name: 'Миграция платежей из MongoDB в PostgreSQL (унифицированная модель)',
|
||||
validUntil: new Date('2025-07-03'), // Действует до
|
||||
|
||||
async up(): Promise<boolean> {
|
||||
console.log('Выполнение миграции: Перенос платежей из MongoDB в PostgreSQL (унифицированная модель)');
|
||||
|
||||
try {
|
||||
await generator.connect(config.mongoose.url);
|
||||
|
||||
// Инициализируем подключение к PostgreSQL
|
||||
const dataSource = new DataSource({
|
||||
type: 'postgres',
|
||||
host: config.postgres.host,
|
||||
port: Number(config.postgres.port),
|
||||
username: config.postgres.username,
|
||||
password: config.postgres.password,
|
||||
database: config.postgres.database,
|
||||
entities: [PaymentEntity],
|
||||
synchronize: true, // Создаем таблицу если она не существует
|
||||
});
|
||||
|
||||
await dataSource.initialize();
|
||||
console.log('Подключение к PostgreSQL установлено');
|
||||
|
||||
// Создаем репозиторий для работы с платежами
|
||||
const paymentRepository = new TypeOrmPaymentRepository(dataSource.getRepository(PaymentEntity));
|
||||
|
||||
// Получаем все заказы из MongoDB
|
||||
const orders = await Order.find();
|
||||
console.log(`Найдено ${orders.length} заказов в MongoDB`);
|
||||
|
||||
let migratedCount = 0;
|
||||
let skippedCount = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
for (const order of orders) {
|
||||
try {
|
||||
// Генерируем hash из order_num для поиска существующих платежей
|
||||
const hash = sha256(String(order.order_num));
|
||||
|
||||
// Проверяем, существует ли уже платеж с таким hash
|
||||
const existingPayment = await paymentRepository.findByHash(hash);
|
||||
|
||||
if (existingPayment) {
|
||||
console.log(`Платеж с hash ${hash} (order_num: ${order.order_num}) уже существует, пропускаем`);
|
||||
skippedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Мапим старые статусы на новые
|
||||
const statusMapping: Record<string, PaymentStatusEnum> = {
|
||||
pending: PaymentStatusEnum.PENDING,
|
||||
paid: PaymentStatusEnum.PAID,
|
||||
completed: PaymentStatusEnum.COMPLETED,
|
||||
failed: PaymentStatusEnum.FAILED,
|
||||
expired: PaymentStatusEnum.EXPIRED,
|
||||
refunded: PaymentStatusEnum.REFUNDED,
|
||||
};
|
||||
|
||||
// Мапим старые типы на новые
|
||||
const typeMapping: Record<string, PaymentTypeEnum> = {
|
||||
registration: PaymentTypeEnum.REGISTRATION,
|
||||
deposit: PaymentTypeEnum.DEPOSIT,
|
||||
};
|
||||
|
||||
const paymentType = typeMapping[order.type] || PaymentTypeEnum.REGISTRATION;
|
||||
const paymentDirection = getPaymentDirection(paymentType);
|
||||
const paymentStatus = statusMapping[order.status] || PaymentStatusEnum.PENDING;
|
||||
|
||||
// Проверяем, что все мигрируемые платежи действительно входящие
|
||||
if (paymentDirection !== PaymentDirectionEnum.INCOMING) {
|
||||
console.warn(`⚠️ Платеж ${order.order_num} имеет неожиданное направление ${paymentDirection}, пропускаем`);
|
||||
skippedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Создаем объект платежа согласно доменному интерфейсу
|
||||
const paymentData: PaymentDomainInterface = {
|
||||
hash, // используем сгенерированный hash вместо blockchain_num
|
||||
coopname: order.creator,
|
||||
username: order.username,
|
||||
quantity: parseFloat(order.quantity),
|
||||
symbol: order.symbol,
|
||||
type: paymentType,
|
||||
direction: paymentDirection,
|
||||
status: paymentStatus,
|
||||
provider: order.provider,
|
||||
secret: order.secret,
|
||||
message: order.message,
|
||||
expired_at: order.expired_at,
|
||||
created_at: (order as any).createdAt || new Date(), // Mongoose timestamps
|
||||
updated_at: (order as any).updatedAt || new Date(),
|
||||
payment_details: order.details
|
||||
? {
|
||||
data: order.details.data,
|
||||
amount_plus_fee: order.details.amount_plus_fee,
|
||||
amount_without_fee: order.details.amount_without_fee,
|
||||
fee_amount: order.details.fee_amount,
|
||||
fee_percent: order.details.fee_percent,
|
||||
fact_fee_percent: order.details.fact_fee_percent,
|
||||
tolerance_percent: order.details.tolerance_percent,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
|
||||
// Создаем платеж через репозиторий
|
||||
await paymentRepository.create(paymentData);
|
||||
|
||||
migratedCount++;
|
||||
console.log(
|
||||
`✅ Мигрирован платеж ${order.order_num} -> hash: ${hash} (${paymentType}/${paymentDirection}, ${paymentStatus})`
|
||||
);
|
||||
} catch (error) {
|
||||
errorCount++;
|
||||
console.error(`❌ Ошибка при миграции платежа ${order.order_num}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n=== Результаты миграции ===');
|
||||
console.log(`Мигрировано: ${migratedCount}`);
|
||||
console.log(`Пропущено: ${skippedCount}`);
|
||||
console.log(`Ошибок: ${errorCount}`);
|
||||
console.log(`Всего обработано: ${orders.length}`);
|
||||
|
||||
// Проверяем, что все мигрированные платежи действительно входящие
|
||||
console.log('\n=== Проверка миграции ===');
|
||||
const allPayments = await paymentRepository.getAllPayments({}, { page: 1, limit: 1000, sortOrder: 'ASC' });
|
||||
const incomingCount = allPayments.items.filter(
|
||||
(p: PaymentDomainInterface) => p.direction === PaymentDirectionEnum.INCOMING
|
||||
).length;
|
||||
const outgoingCount = allPayments.items.filter(
|
||||
(p: PaymentDomainInterface) => p.direction === PaymentDirectionEnum.OUTGOING
|
||||
).length;
|
||||
|
||||
console.log(`Входящих платежей: ${incomingCount}`);
|
||||
console.log(`Исходящих платежей: ${outgoingCount}`);
|
||||
|
||||
if (outgoingCount > 0) {
|
||||
console.warn(`⚠️ Обнаружены исходящие платежи после миграции! Это может указывать на проблему.`);
|
||||
}
|
||||
|
||||
// Дополнительная валидация данных
|
||||
console.log('\n=== Валидация данных ===');
|
||||
const registrationPayments = allPayments.items.filter(
|
||||
(p: PaymentDomainInterface) => p.type === PaymentTypeEnum.REGISTRATION
|
||||
).length;
|
||||
const depositPayments = allPayments.items.filter(
|
||||
(p: PaymentDomainInterface) => p.type === PaymentTypeEnum.DEPOSIT
|
||||
).length;
|
||||
|
||||
console.log(`Регистрационных платежей: ${registrationPayments}`);
|
||||
console.log(`Депозитных платежей: ${depositPayments}`);
|
||||
|
||||
// Проверяем обязательные поля
|
||||
const paymentsWithMissingData = allPayments.items.filter(
|
||||
(p: PaymentDomainInterface) => !p.coopname || !p.username || !p.quantity || !p.symbol || !p.provider || !p.secret
|
||||
);
|
||||
|
||||
if (paymentsWithMissingData.length > 0) {
|
||||
console.warn(`⚠️ Найдено ${paymentsWithMissingData.length} платежей с отсутствующими обязательными данными`);
|
||||
paymentsWithMissingData.forEach((p: PaymentDomainInterface) => {
|
||||
console.warn(` Платеж ${p.hash}: отсутствуют поля`, {
|
||||
coopname: !p.coopname,
|
||||
username: !p.username,
|
||||
quantity: !p.quantity,
|
||||
symbol: !p.symbol,
|
||||
provider: !p.provider,
|
||||
secret: !p.secret,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
console.log('✅ Все платежи имеют обязательные поля');
|
||||
}
|
||||
|
||||
// Проверяем корректность типов и направлений
|
||||
const invalidDirections = allPayments.items.filter((p: PaymentDomainInterface) => {
|
||||
const expectedDirection = getPaymentDirection(p.type);
|
||||
return p.direction !== expectedDirection;
|
||||
});
|
||||
|
||||
if (invalidDirections.length > 0) {
|
||||
console.warn(`⚠️ Найдено ${invalidDirections.length} платежей с некорректным направлением`);
|
||||
} else {
|
||||
console.log('✅ Все платежи имеют корректные направления');
|
||||
}
|
||||
|
||||
// Создаем базовые индексы для оптимизации запросов
|
||||
console.log('\n=== Создание индексов ===');
|
||||
|
||||
try {
|
||||
await dataSource.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_payments_status ON payments(status);
|
||||
`);
|
||||
|
||||
await dataSource.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_payments_type ON payments(type);
|
||||
`);
|
||||
|
||||
await dataSource.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_payments_direction ON payments(direction);
|
||||
`);
|
||||
|
||||
await dataSource.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_payments_username ON payments(username);
|
||||
`);
|
||||
|
||||
await dataSource.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_payments_coopname ON payments(coopname);
|
||||
`);
|
||||
|
||||
await dataSource.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_payments_created_at ON payments(created_at);
|
||||
`);
|
||||
|
||||
await dataSource.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_payments_secret ON payments(secret);
|
||||
`);
|
||||
|
||||
await dataSource.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_payments_hash ON payments(hash);
|
||||
`);
|
||||
|
||||
await dataSource.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_payments_quantity ON payments(quantity);
|
||||
`);
|
||||
await dataSource.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_payments_symbol ON payments(symbol);
|
||||
`);
|
||||
await dataSource.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_payments_expired_at ON payments(expired_at);
|
||||
`);
|
||||
|
||||
await dataSource.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_payments_composite ON payments(coopname, username, status, type);
|
||||
`);
|
||||
|
||||
console.log('Базовые индексы созданы');
|
||||
} catch (indexError) {
|
||||
console.warn('Ошибка при создании индексов:', indexError);
|
||||
}
|
||||
|
||||
// Закрываем подключение к PostgreSQL
|
||||
await dataSource.destroy();
|
||||
console.log('Подключение к PostgreSQL закрыто');
|
||||
|
||||
console.log('Миграция завершена: Перенос платежей в унифицированную модель выполнен успешно');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при выполнении миграции платежей:', error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
};
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "create-nodejs-express-app",
|
||||
"version": "2025.6.13",
|
||||
"version": "2025.7.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "create-nodejs-express-app",
|
||||
"version": "2025.6.13",
|
||||
"version": "2025.7.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@a2seven/yoo-checkout": "^1.1.4",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@coopenomics/controller",
|
||||
"version": "2025.6.13",
|
||||
"version": "2025.7.2",
|
||||
"description": "",
|
||||
"private": false,
|
||||
"bin": "bin/createNodejsApp.js",
|
||||
|
||||
@@ -244,6 +244,9 @@ input AnnualGeneralMeetingAgendaGenerateDocumentInput {
|
||||
"""Имя генератора, использованного для создания документа"""
|
||||
generator: String
|
||||
|
||||
"""Флаг повторного собрания"""
|
||||
is_repeated: Boolean!
|
||||
|
||||
"""Язык документа"""
|
||||
lang: String
|
||||
|
||||
@@ -298,6 +301,9 @@ input AnnualGeneralMeetingAgendaSignedMetaDocumentInput {
|
||||
"""Имя генератора, использованного для создания документа"""
|
||||
generator: String!
|
||||
|
||||
"""Флаг повторного собрания"""
|
||||
is_repeated: Boolean!
|
||||
|
||||
"""Язык документа"""
|
||||
lang: String!
|
||||
|
||||
@@ -524,6 +530,9 @@ input AnnualGeneralMeetingSovietDecisionGenerateDocumentInput {
|
||||
"""Имя генератора, использованного для создания документа"""
|
||||
generator: String
|
||||
|
||||
"""Флаг повторного собрания"""
|
||||
is_repeated: Boolean!
|
||||
|
||||
"""Язык документа"""
|
||||
lang: String
|
||||
|
||||
@@ -1491,7 +1500,10 @@ input CreateChildOrderInput {
|
||||
|
||||
input CreateDepositPaymentInput {
|
||||
"""Сумма взноса"""
|
||||
quantity: String!
|
||||
quantity: Float!
|
||||
|
||||
"""Символ валюты"""
|
||||
symbol: String!
|
||||
|
||||
"""Имя аккаунта пользователя"""
|
||||
username: String!
|
||||
@@ -1631,6 +1643,34 @@ input CreateProjectFreeDecisionInput {
|
||||
question: String!
|
||||
}
|
||||
|
||||
input CreateWithdrawInput {
|
||||
"""Имя аккаунта кооператива"""
|
||||
coopname: String!
|
||||
|
||||
"""ID метода платежа"""
|
||||
method_id: String!
|
||||
|
||||
"""Хеш платежа для связи с withdraw"""
|
||||
payment_hash: String!
|
||||
|
||||
"""Количество средств"""
|
||||
quantity: Float!
|
||||
|
||||
"""Подписанное заявление на возврат средств"""
|
||||
statement: ReturnByMoneySignedDocumentInput!
|
||||
|
||||
"""Символ валюты"""
|
||||
symbol: String!
|
||||
|
||||
"""Имя пользователя"""
|
||||
username: String!
|
||||
}
|
||||
|
||||
type CreateWithdrawResponse {
|
||||
"""Хеш созданной заявки на вывод"""
|
||||
withdraw_hash: String!
|
||||
}
|
||||
|
||||
type CreatedProjectFreeDecision {
|
||||
"""Проект решения, которое предлагается принять"""
|
||||
decision: String!
|
||||
@@ -2061,6 +2101,92 @@ input FreeDecisionGenerateDocumentInput {
|
||||
version: String
|
||||
}
|
||||
|
||||
type GatewayPayment {
|
||||
"""Данные из блокчейна"""
|
||||
blockchain_data: JSON
|
||||
|
||||
"""Можно ли изменить статус"""
|
||||
can_change_status: Boolean!
|
||||
|
||||
"""Название кооператива"""
|
||||
coopname: String!
|
||||
|
||||
"""Дата создания"""
|
||||
created_at: DateTime!
|
||||
|
||||
"""Направление платежа"""
|
||||
direction: PaymentDirection!
|
||||
|
||||
"""Человекочитаемое направление платежа"""
|
||||
direction_label: String!
|
||||
|
||||
"""Дата истечения"""
|
||||
expired_at: DateTime
|
||||
|
||||
"""Форматированная сумма"""
|
||||
formatted_amount: String!
|
||||
|
||||
"""Хеш платежа"""
|
||||
hash: String
|
||||
|
||||
"""Уникальный идентификатор платежа"""
|
||||
id: ID
|
||||
|
||||
"""Хеш входящего платежа (устарело)"""
|
||||
income_hash: String
|
||||
|
||||
"""Завершен ли платеж окончательно"""
|
||||
is_final: Boolean!
|
||||
|
||||
"""Дополнительная информация"""
|
||||
memo: String
|
||||
|
||||
"""Сообщение"""
|
||||
message: String
|
||||
|
||||
"""Хеш исходящего платежа (устарело)"""
|
||||
outcome_hash: String
|
||||
|
||||
"""Детали платежа"""
|
||||
payment_details: PaymentDetails
|
||||
|
||||
"""ID платежного метода"""
|
||||
payment_method_id: String
|
||||
|
||||
"""Провайдер платежа"""
|
||||
provider: String
|
||||
|
||||
"""Количество/сумма"""
|
||||
quantity: Float!
|
||||
|
||||
"""Подписанный документ заявления"""
|
||||
statement: JSON
|
||||
|
||||
"""Статус платежа"""
|
||||
status: PaymentStatus!
|
||||
|
||||
"""Человекочитаемый статус"""
|
||||
status_label: String!
|
||||
|
||||
"""Символ валюты"""
|
||||
symbol: String!
|
||||
|
||||
"""Тип платежа"""
|
||||
type: PaymentType!
|
||||
|
||||
"""Человекочитаемый тип платежа"""
|
||||
type_label: String!
|
||||
|
||||
"""Дата обновления"""
|
||||
updated_at: DateTime
|
||||
|
||||
"""Имя пользователя"""
|
||||
username: String!
|
||||
|
||||
"""Сертификат пользователя, создавшего платеж"""
|
||||
username_certificate: UserCertificateUnion
|
||||
}
|
||||
|
||||
input GenerateDocumentInput {
|
||||
"""Номер блока, на котором был создан документ"""
|
||||
block_num: Int
|
||||
@@ -2190,20 +2316,6 @@ input GetPaymentMethodsInput {
|
||||
username: String
|
||||
}
|
||||
|
||||
input GetPaymentsInput {
|
||||
"""Идентификатор платежа в блокчейне"""
|
||||
blockchain_id: String
|
||||
|
||||
"""Идентификатор платежа во внутренней системе"""
|
||||
id: String
|
||||
|
||||
"""Статус платежа"""
|
||||
status: PaymentStatus
|
||||
|
||||
"""Имя пользователя для фильтрации платежей"""
|
||||
username: String
|
||||
}
|
||||
|
||||
type Individual {
|
||||
"""Дата рождения"""
|
||||
birthdate: String!
|
||||
@@ -2342,6 +2454,9 @@ type Meet {
|
||||
"""Сертификат инициатора собрания"""
|
||||
initiator_certificate: UserCertificateUnion
|
||||
|
||||
"""Уровень собрания"""
|
||||
level: String!
|
||||
|
||||
"""Список пользователей, которые подписали уведомление"""
|
||||
notified_users: [String!]!
|
||||
|
||||
@@ -2652,14 +2767,14 @@ type Mutation {
|
||||
createChildOrder(data: CreateChildOrderInput!): Transaction!
|
||||
|
||||
"""
|
||||
Создание объекта паевого платежа производится мутацией CreateDeposit. Выполнение мутации возвращает идентификатор платежа и данные для его совершения в зависимости от выбранного платежного провайдера.
|
||||
Создание объекта паевого платежа производится мутацией createDepositPayment. Выполнение мутации возвращает идентификатор платежа и данные для его совершения в зависимости от выбранного платежного провайдера.
|
||||
"""
|
||||
createDepositPayment(data: CreateDepositPaymentInput!): Payment!
|
||||
createDepositPayment(data: CreateDepositPaymentInput!): GatewayPayment!
|
||||
|
||||
"""
|
||||
Создание объекта регистрационного платежа производится мутацией CreateInitial. Выполнение мутации возвращает идентификатор платежа и данные для его совершения в зависимости от выбранного платежного провайдера.
|
||||
Создание объекта регистрационного платежа производится мутацией createInitialPayment. Выполнение мутации возвращает идентификатор платежа и данные для его совершения в зависимости от выбранного платежного провайдера.
|
||||
"""
|
||||
createInitialPayment(data: CreateInitialPaymentInput!): Payment!
|
||||
createInitialPayment(data: CreateInitialPaymentInput!): GatewayPayment!
|
||||
|
||||
"""Создать предложение на поставку имущества"""
|
||||
createParentOffer(data: CreateParentOfferInput!): Transaction!
|
||||
@@ -2669,6 +2784,9 @@ type Mutation {
|
||||
"""
|
||||
createProjectOfFreeDecision(data: CreateProjectFreeDecisionInput!): CreatedProjectFreeDecision!
|
||||
|
||||
"""Создать заявку на вывод средств"""
|
||||
createWithdraw(input: CreateWithdrawInput!): CreateWithdrawResponse!
|
||||
|
||||
"""Отклонить заявку"""
|
||||
declineRequest(data: DeclineRequestInput!): Transaction!
|
||||
|
||||
@@ -2737,6 +2855,12 @@ type Mutation {
|
||||
"""Сгенерировать документ заявления о возврате имущества."""
|
||||
generateReturnByAssetStatement(data: ReturnByAssetStatementGenerateDocumentInput!, options: GenerateDocumentOptionsInput): GeneratedDocument!
|
||||
|
||||
"""Сгенерировать документ решения совета о возврате паевого взноса"""
|
||||
generateReturnByMoneyDecisionDocument(data: ReturnByMoneyDecisionGenerateDocumentInput!, options: GenerateDocumentOptionsInput): GeneratedDocument!
|
||||
|
||||
"""Сгенерировать документ заявления на возврат паевого взноса"""
|
||||
generateReturnByMoneyStatementDocument(data: ReturnByMoneyGenerateDocumentInput!, options: GenerateDocumentOptionsInput): GeneratedDocument!
|
||||
|
||||
"""Сгенерировать документ, подтверждающий выбор кооперативного участка"""
|
||||
generateSelectBranchDocument(data: SelectBranchGenerateDocumentInput!, options: GenerateDocumentOptionsInput): GeneratedDocument!
|
||||
|
||||
@@ -2822,7 +2946,7 @@ type Mutation {
|
||||
"""
|
||||
Управление статусом платежа осущствляется мутацией setPaymentStatus. При переходе платежа в статус PAID вызывается эффект в блокчейне, который завершает операцию автоматическим переводом платежа в статус COMPLETED. При установке статуса REFUNDED запускается процесс отмены платежа в блокчейне. Остальные статусы не приводят к эффектам в блокчейне.
|
||||
"""
|
||||
setPaymentStatus(data: SetPaymentStatusInput!): Payment!
|
||||
setPaymentStatus(data: SetPaymentStatusInput!): GatewayPayment!
|
||||
|
||||
"""Сохранить приватный ключ в зашифрованном серверном хранилище"""
|
||||
setWif(data: SetWifInput!): Boolean!
|
||||
@@ -2963,6 +3087,20 @@ enum OrganizationType {
|
||||
ZAO
|
||||
}
|
||||
|
||||
type PaginatedGatewayPaymentsPaginationResult {
|
||||
"""Текущая страница"""
|
||||
currentPage: Int!
|
||||
|
||||
"""Элементы текущей страницы"""
|
||||
items: [GatewayPayment!]!
|
||||
|
||||
"""Общее количество элементов"""
|
||||
totalCount: Int!
|
||||
|
||||
"""Общее количество страниц"""
|
||||
totalPages: Int!
|
||||
}
|
||||
|
||||
input PaginationInput {
|
||||
"""Количество элементов на странице"""
|
||||
limit: Int! = 10
|
||||
@@ -3182,54 +3320,6 @@ input PassportInput {
|
||||
series: Float!
|
||||
}
|
||||
|
||||
type Payment {
|
||||
"""Аккаунт пользователя, совершающего платеж"""
|
||||
account: Account!
|
||||
|
||||
"""Сумма платежа"""
|
||||
amount: Float!
|
||||
|
||||
"""Идентификационный номер платежа в блокчейне"""
|
||||
blockchain_id: Float!
|
||||
|
||||
"""Дата создания платежа"""
|
||||
created_at: DateTime!
|
||||
|
||||
"""Детали платежа"""
|
||||
details: PaymentDetails!
|
||||
|
||||
"""Дата истечения срока давности платежа"""
|
||||
expired_at: DateTime!
|
||||
|
||||
"""Идентификатор платежа во внутренней системе учёта"""
|
||||
id: String!
|
||||
|
||||
"""Содержит сервисное сообщение провайдера об ошибке обработки платежа"""
|
||||
message: String!
|
||||
|
||||
"""
|
||||
Идентификатор наименования провайдера платежа, ответственного за обработку
|
||||
"""
|
||||
provider: String!
|
||||
|
||||
"""
|
||||
Идентификатор номера платежа, который отображается пользователю в платежных документах
|
||||
"""
|
||||
status: PaymentStatus!
|
||||
|
||||
"""Символ тикера валюты платежа"""
|
||||
symbol: String!
|
||||
|
||||
"""Тип платежа (deposit или registration)"""
|
||||
type: String!
|
||||
|
||||
"""Дата обновления платежа"""
|
||||
updated_at: DateTime!
|
||||
|
||||
"""Имя аккаунта пользователя, совершающего платеж"""
|
||||
username: String!
|
||||
}
|
||||
|
||||
type PaymentDetails {
|
||||
"""Сумма платежа с учетом комиссии"""
|
||||
amount_plus_fee: String!
|
||||
@@ -3237,8 +3327,8 @@ type PaymentDetails {
|
||||
"""Сумма платежа без учета комиссии"""
|
||||
amount_without_fee: String!
|
||||
|
||||
"""Строка с данными платежа"""
|
||||
data: String!
|
||||
"""Данные платежа (QR-код, токен, реквизиты и т.д.)"""
|
||||
data: JSON!
|
||||
|
||||
"""Фактический процент комиссии"""
|
||||
fact_fee_percent: Float!
|
||||
@@ -3253,6 +3343,35 @@ type PaymentDetails {
|
||||
tolerance_percent: Float!
|
||||
}
|
||||
|
||||
"""Направление платежа"""
|
||||
enum PaymentDirection {
|
||||
INCOMING
|
||||
OUTGOING
|
||||
}
|
||||
|
||||
input PaymentFiltersInput {
|
||||
"""Название кооператива"""
|
||||
coopname: String
|
||||
|
||||
"""Направление платежа"""
|
||||
direction: PaymentDirection
|
||||
|
||||
"""Хэш платежа"""
|
||||
hash: String
|
||||
|
||||
"""Провайдер платежа"""
|
||||
provider: String
|
||||
|
||||
"""Статус платежа"""
|
||||
status: PaymentStatus
|
||||
|
||||
"""Тип платежа"""
|
||||
type: PaymentType
|
||||
|
||||
"""Имя пользователя"""
|
||||
username: String
|
||||
}
|
||||
|
||||
type PaymentMethod {
|
||||
"""Дата создания"""
|
||||
created_at: DateTime!
|
||||
@@ -3292,30 +3411,25 @@ type PaymentMethodPaginationResult {
|
||||
totalPages: Int!
|
||||
}
|
||||
|
||||
type PaymentPaginationResult {
|
||||
"""Текущая страница"""
|
||||
currentPage: Int!
|
||||
|
||||
"""Элементы текущей страницы"""
|
||||
items: [Payment!]!
|
||||
|
||||
"""Общее количество элементов"""
|
||||
totalCount: Int!
|
||||
|
||||
"""Общее количество страниц"""
|
||||
totalPages: Int!
|
||||
}
|
||||
|
||||
"""Статус платежа"""
|
||||
enum PaymentStatus {
|
||||
CANCELLED
|
||||
COMPLETED
|
||||
EXPIRED
|
||||
FAILED
|
||||
PAID
|
||||
PENDING
|
||||
PROCESSING
|
||||
REFUNDED
|
||||
}
|
||||
|
||||
"""Тип платежа по назначению"""
|
||||
enum PaymentType {
|
||||
DEPOSIT
|
||||
REGISTRATION
|
||||
WITHDRAWAL
|
||||
}
|
||||
|
||||
type Permission {
|
||||
"""Родительское разрешение"""
|
||||
parent: String!
|
||||
@@ -3352,6 +3466,22 @@ type PrivateAccount {
|
||||
type: AccountType!
|
||||
}
|
||||
|
||||
union PrivateAccountSearchData = Entrepreneur | Individual | Organization
|
||||
|
||||
type PrivateAccountSearchResult {
|
||||
"""Данные найденного аккаунта"""
|
||||
data: PrivateAccountSearchData!
|
||||
|
||||
"""Поля, в которых найдены совпадения"""
|
||||
highlightedFields: [String!]
|
||||
|
||||
"""Оценка релевантности результата"""
|
||||
score: Float
|
||||
|
||||
"""Тип аккаунта"""
|
||||
type: String!
|
||||
}
|
||||
|
||||
input ProhibitRequestInput {
|
||||
"""Имя аккаунта кооператива"""
|
||||
coopname: String!
|
||||
@@ -3519,11 +3649,18 @@ type Query {
|
||||
"""Получить список методов оплаты"""
|
||||
getPaymentMethods(data: GetPaymentMethodsInput): PaymentMethodPaginationResult!
|
||||
|
||||
"""Получить список платежей"""
|
||||
getPayments(data: GetPaymentsInput, options: PaginationInput): PaymentPaginationResult!
|
||||
"""
|
||||
Получить список платежей с возможностью фильтрации по типу, статусу и направлению.
|
||||
"""
|
||||
getPayments(data: PaymentFiltersInput, options: PaginationInput): PaginatedGatewayPaymentsPaginationResult!
|
||||
|
||||
"""Получить сводную публичную информацию о системе"""
|
||||
getSystemInfo: SystemInfo!
|
||||
|
||||
"""
|
||||
Поиск приватных данных аккаунтов по запросу. Поиск осуществляется по полям ФИО, ИНН, ОГРН, наименованию организации и другим приватным данным.
|
||||
"""
|
||||
searchPrivateAccounts(data: SearchPrivateAccountsInput!): [PrivateAccountSearchResult!]!
|
||||
}
|
||||
|
||||
"""Вопрос повестки собрания с результатами голосования"""
|
||||
@@ -4018,11 +4155,173 @@ input ReturnByAssetStatementSignedMetaDocumentInput {
|
||||
version: String!
|
||||
}
|
||||
|
||||
input ReturnByMoneyDecisionGenerateDocumentInput {
|
||||
"""Номер блока, на котором был создан документ"""
|
||||
block_num: Int
|
||||
|
||||
"""Название кооператива, связанное с документом"""
|
||||
coopname: String!
|
||||
|
||||
"""Дата и время создания документа"""
|
||||
created_at: String
|
||||
|
||||
"""Валюта"""
|
||||
currency: String!
|
||||
|
||||
"""ID решения совета"""
|
||||
decision_id: Float!
|
||||
|
||||
"""Имя генератора, использованного для создания документа"""
|
||||
generator: String
|
||||
|
||||
"""Язык документа"""
|
||||
lang: String
|
||||
|
||||
"""Ссылки, связанные с документом"""
|
||||
links: [String!]
|
||||
|
||||
"""Хэш платежа"""
|
||||
payment_hash: String!
|
||||
|
||||
"""Количество средств к возврату"""
|
||||
quantity: String!
|
||||
|
||||
"""Часовой пояс, в котором был создан документ"""
|
||||
timezone: String
|
||||
|
||||
"""Название документа"""
|
||||
title: String
|
||||
|
||||
"""Имя пользователя, создавшего документ"""
|
||||
username: String!
|
||||
|
||||
"""Версия генератора, использованного для создания документа"""
|
||||
version: String
|
||||
}
|
||||
|
||||
input ReturnByMoneyGenerateDocumentInput {
|
||||
"""Номер блока, на котором был создан документ"""
|
||||
block_num: Int
|
||||
|
||||
"""Название кооператива, связанное с документом"""
|
||||
coopname: String!
|
||||
|
||||
"""Дата и время создания документа"""
|
||||
created_at: String
|
||||
|
||||
"""Валюта"""
|
||||
currency: String!
|
||||
|
||||
"""Имя генератора, использованного для создания документа"""
|
||||
generator: String
|
||||
|
||||
"""Язык документа"""
|
||||
lang: String
|
||||
|
||||
"""Ссылки, связанные с документом"""
|
||||
links: [String!]
|
||||
|
||||
"""ID платежного метода"""
|
||||
method_id: String!
|
||||
|
||||
"""Хеш платежа для связи с withdraw"""
|
||||
payment_hash: String!
|
||||
|
||||
"""Количество средств к возврату"""
|
||||
quantity: String!
|
||||
|
||||
"""Часовой пояс, в котором был создан документ"""
|
||||
timezone: String
|
||||
|
||||
"""Название документа"""
|
||||
title: String
|
||||
|
||||
"""Имя пользователя, создавшего документ"""
|
||||
username: String!
|
||||
|
||||
"""Версия генератора, использованного для создания документа"""
|
||||
version: String
|
||||
}
|
||||
|
||||
input ReturnByMoneySignedDocumentInput {
|
||||
"""Хэш содержимого документа"""
|
||||
doc_hash: String!
|
||||
|
||||
"""Общий хэш (doc_hash + meta_hash)"""
|
||||
hash: String!
|
||||
|
||||
"""
|
||||
Метаинформация для документа заявления на возврат паевого взноса денежными средствами
|
||||
"""
|
||||
meta: ReturnByMoneySignedMetaDocumentInput!
|
||||
|
||||
"""Хэш мета-данных"""
|
||||
meta_hash: String!
|
||||
|
||||
"""Вектор подписей"""
|
||||
signatures: [SignatureInfoInput!]!
|
||||
|
||||
"""Версия стандарта документа"""
|
||||
version: String!
|
||||
}
|
||||
|
||||
input ReturnByMoneySignedMetaDocumentInput {
|
||||
"""Номер блока, на котором был создан документ"""
|
||||
block_num: Int!
|
||||
|
||||
"""Название кооператива, связанное с документом"""
|
||||
coopname: String!
|
||||
|
||||
"""Дата и время создания документа"""
|
||||
created_at: String!
|
||||
|
||||
"""Валюта"""
|
||||
currency: String!
|
||||
|
||||
"""Имя генератора, использованного для создания документа"""
|
||||
generator: String!
|
||||
|
||||
"""Язык документа"""
|
||||
lang: String!
|
||||
|
||||
"""Ссылки, связанные с документом"""
|
||||
links: [String!]!
|
||||
|
||||
"""ID платежного метода"""
|
||||
method_id: String!
|
||||
|
||||
"""Хеш платежа для связи с withdraw"""
|
||||
payment_hash: String!
|
||||
|
||||
"""Количество средств к возврату"""
|
||||
quantity: String!
|
||||
|
||||
"""ID документа в реестре"""
|
||||
registry_id: Int!
|
||||
|
||||
"""Часовой пояс, в котором был создан документ"""
|
||||
timezone: String!
|
||||
|
||||
"""Название документа"""
|
||||
title: String!
|
||||
|
||||
"""Имя пользователя, создавшего документ"""
|
||||
username: String!
|
||||
|
||||
"""Версия генератора, использованного для создания документа"""
|
||||
version: String!
|
||||
}
|
||||
|
||||
type SbpAccount {
|
||||
"""Мобильный телефон получателя"""
|
||||
phone: String!
|
||||
}
|
||||
|
||||
input SearchPrivateAccountsInput {
|
||||
"""Поисковый запрос для поиска приватных аккаунтов"""
|
||||
query: String!
|
||||
}
|
||||
|
||||
input SelectBranchGenerateDocumentInput {
|
||||
"""Номер блока, на котором был создан документ"""
|
||||
block_num: Int
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import swaggerJsdoc from 'swagger-jsdoc';
|
||||
import swaggerDefinition from '../src/docs/swaggerDef';
|
||||
import fs from 'fs'
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const specs = swaggerJsdoc({
|
||||
@@ -8,5 +8,4 @@ const specs = swaggerJsdoc({
|
||||
apis: ['src/docs/*.yml', 'src/**/*.ts'],
|
||||
});
|
||||
|
||||
|
||||
fs.writeFileSync(path.join(__dirname, '../src/docs/swagger.json'), JSON.stringify(specs, null, 2));
|
||||
|
||||
@@ -2,7 +2,7 @@ spectaql:
|
||||
targetDir: ./docs
|
||||
introspection:
|
||||
# schemaFile: ./schema.gql
|
||||
url: 'http://localhost:2998/v1/graphql'
|
||||
url: 'https://testnet.coopenomics.world/api/v1/graphql'
|
||||
|
||||
info:
|
||||
title: 'API-Документация'
|
||||
|
||||
@@ -68,6 +68,18 @@ const envVarsSchema = z.object({
|
||||
REDIS_PASSWORD: z.string(),
|
||||
BLOCKCHAIN_RPC: z.string().min(1, { message: 'Не должно быть пустым' }),
|
||||
CHAIN_ID: z.string().min(1, { message: 'Не должно быть пустым' }),
|
||||
|
||||
// Параметры блокчейна
|
||||
ROOT_SYMBOL: z.string().default('AXON'),
|
||||
ROOT_GOVERN_SYMBOL: z.string().default('RUB'),
|
||||
ROOT_PRECISION: z
|
||||
.string()
|
||||
.default('4')
|
||||
.transform((val) => parseInt(val, 10)),
|
||||
ROOT_GOVERN_PRECISION: z
|
||||
.string()
|
||||
.default('4')
|
||||
.transform((val) => parseInt(val, 10)),
|
||||
});
|
||||
|
||||
// Валидация переменных окружения
|
||||
@@ -95,6 +107,10 @@ export default {
|
||||
blockchain: {
|
||||
url: envVars.data.BLOCKCHAIN_RPC,
|
||||
id: envVars.data.CHAIN_ID,
|
||||
root_symbol: envVars.data.ROOT_SYMBOL,
|
||||
root_govern_symbol: envVars.data.ROOT_GOVERN_SYMBOL,
|
||||
root_precision: envVars.data.ROOT_PRECISION,
|
||||
root_govern_precision: envVars.data.ROOT_GOVERN_PRECISION,
|
||||
},
|
||||
mongoose: {
|
||||
url: envVars.data.MONGODB_URL + (envVars.data.NODE_ENV === 'test' ? '-test' : ''),
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
export * as authController from './auth.controller';
|
||||
export * as userController from './user.controller';
|
||||
export * as documentController from './document.controller';
|
||||
export * as orderController from './order.controller';
|
||||
export * as coopController from './coop.controller';
|
||||
export * as notifyController from './notify.controller';
|
||||
export * as systemController from './system.controller';
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import http from 'http-status';
|
||||
import catchAsync from '../utils/catchAsync';
|
||||
import { orderService } from '../services';
|
||||
import { ICreateDeposit } from '../types';
|
||||
import { Request, Response } from 'express';
|
||||
import httpStatus from 'http-status';
|
||||
import pick from '../utils/pick';
|
||||
import logger from '../config/logger';
|
||||
import { ProviderInteractor } from '~/domain/provider/provider.interactor';
|
||||
import { nestApp } from '..';
|
||||
|
||||
export const catchIPN = catchAsync(async (req: Request, res: Response) => {
|
||||
const providerName = req.params.provider;
|
||||
logger.info(`Recieve new IPN for provider ${providerName}`, { body: req.body, source: 'catchIPN' });
|
||||
|
||||
const providerInteractor = nestApp.get(ProviderInteractor);
|
||||
const provider = providerInteractor.getProvider(providerName);
|
||||
|
||||
// Обрабатываем IPN данные
|
||||
await provider.handleIPN(req.body);
|
||||
res.status(200).send();
|
||||
});
|
||||
|
||||
export const createDeposit = catchAsync(async (req: any, res: Response) => {
|
||||
const username = req.user.username;
|
||||
|
||||
const result = await orderService.createDeposit(username, req.body as ICreateDeposit);
|
||||
res.status(http.CREATED).send(result);
|
||||
});
|
||||
|
||||
export const createInitialPayment = catchAsync(async (req: any, res: Response) => {
|
||||
const username = req.user.username;
|
||||
const result = await orderService.createInitialOrder(username);
|
||||
res.status(http.CREATED).send(result);
|
||||
});
|
||||
|
||||
export const getOrders = catchAsync(async (req: Request, res: Response) => {
|
||||
const filter = pick(req.query, ['username', 'id', 'order_num', 'status']);
|
||||
const options = pick(req.query, ['sortBy', 'limit', 'page']);
|
||||
|
||||
const orders = await orderService.getOrders(filter, options);
|
||||
res.status(httpStatus.OK).send(orders);
|
||||
});
|
||||
|
||||
export const getMyOrders = catchAsync(async (req: Request, res: Response) => {
|
||||
const filter = pick(req.query, ['status', 'id']);
|
||||
const options = pick(req.query, ['sortBy', 'limit', 'page']);
|
||||
|
||||
filter.username = req.params.username;
|
||||
|
||||
const orders = await orderService.getOrders(filter, options);
|
||||
res.status(httpStatus.OK).send(orders);
|
||||
});
|
||||
|
||||
export const setStatus = catchAsync(async (req: Request, res: Response) => {
|
||||
const { status, id } = req.body;
|
||||
|
||||
await orderService.setStatus(id, status);
|
||||
|
||||
res.status(httpStatus.OK).send();
|
||||
});
|
||||
@@ -17,6 +17,10 @@ import { AccountType } from '~/modules/account/enum/account-type.enum';
|
||||
import { ORGANIZATION_REPOSITORY, OrganizationRepository } from '~/domain/common/repositories/organization.repository';
|
||||
import { INDIVIDUAL_REPOSITORY, IndividualRepository } from '~/domain/common/repositories/individual.repository';
|
||||
import { ENTREPRENEUR_REPOSITORY, EntrepreneurRepository } from '~/domain/common/repositories/entrepreneur.repository';
|
||||
import {
|
||||
SEARCH_PRIVATE_ACCOUNTS_REPOSITORY,
|
||||
SearchPrivateAccountsRepository,
|
||||
} from '~/domain/common/repositories/search-private-accounts.repository';
|
||||
import { IndividualDomainEntity } from '~/domain/branch/entities/individual-domain.entity';
|
||||
import { OrganizationDomainEntity } from '~/domain/branch/entities/organization-domain.entity';
|
||||
import { EntrepreneurDomainEntity } from '~/domain/branch/entities/entrepreneur-domain.entity';
|
||||
@@ -24,6 +28,10 @@ import { ACCOUNT_BLOCKCHAIN_PORT, AccountBlockchainPort } from '../interfaces/ac
|
||||
import { CANDIDATE_REPOSITORY, CandidateRepository } from '../repository/candidate.repository';
|
||||
import { userStatus } from '~/types/user.types';
|
||||
import { sha256 } from '~/utils/sha256';
|
||||
import type {
|
||||
SearchPrivateAccountsInputDomainInterface,
|
||||
PrivateAccountSearchResultDomainInterface,
|
||||
} from '~/domain/common/interfaces/search-private-accounts-domain.interface';
|
||||
|
||||
@Injectable()
|
||||
export class AccountDomainInteractor {
|
||||
@@ -32,6 +40,8 @@ export class AccountDomainInteractor {
|
||||
@Inject(ORGANIZATION_REPOSITORY) private readonly organizationRepository: OrganizationRepository,
|
||||
@Inject(INDIVIDUAL_REPOSITORY) private readonly individualRepository: IndividualRepository,
|
||||
@Inject(ENTREPRENEUR_REPOSITORY) private readonly entrepreneurRepository: EntrepreneurRepository,
|
||||
@Inject(SEARCH_PRIVATE_ACCOUNTS_REPOSITORY)
|
||||
private readonly searchPrivateAccountsRepository: SearchPrivateAccountsRepository,
|
||||
@Inject(ACCOUNT_BLOCKCHAIN_PORT) private readonly accountBlockchainPort: AccountBlockchainPort,
|
||||
@Inject(CANDIDATE_REPOSITORY) private readonly candidateRepository: CandidateRepository
|
||||
) {}
|
||||
@@ -176,4 +186,21 @@ export class AccountDomainInteractor {
|
||||
throw new HttpException(`Ошибка при регистрации в блокчейне: ${error.message}`, HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Поиск приватных аккаунтов по запросу
|
||||
* @param input Входные данные для поиска
|
||||
* @returns Массив результатов поиска
|
||||
*/
|
||||
async searchPrivateAccounts(
|
||||
input: SearchPrivateAccountsInputDomainInterface
|
||||
): Promise<PrivateAccountSearchResultDomainInterface[]> {
|
||||
this.logger.log(`Поиск приватных аккаунтов по запросу: "${input.query}"`);
|
||||
|
||||
const results = await this.searchPrivateAccountsRepository.searchPrivateAccounts(input);
|
||||
|
||||
this.logger.log(`Найдено ${results.length} приватных аккаунтов`);
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,7 +242,7 @@ export class BranchDomainInteractor {
|
||||
|
||||
async selectBranch(data: SelectBranchInputDomainInterface): Promise<boolean> {
|
||||
// TODO move it to separate document domain service for validate
|
||||
const document = await this.documentRepository.findByHash(data.document.hash);
|
||||
const document = await this.documentRepository.findByHash(data.document.doc_hash);
|
||||
if (!document) throw new BadRequestException('Документ не найден');
|
||||
|
||||
if (data.document.meta.registry_id != Cooperative.Registry.SelectBranchStatement.registry_id)
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import type { IndividualDomainInterface } from './individual-domain.interface';
|
||||
import type { EntrepreneurDomainInterface } from './entrepreneur-domain.interface';
|
||||
import type { OrganizationDomainInterface } from './organization-domain.interface';
|
||||
|
||||
/**
|
||||
* Интерфейс для входных данных поиска приватных аккаунтов
|
||||
*/
|
||||
export interface SearchPrivateAccountsInputDomainInterface {
|
||||
query: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Тип данных результата поиска - union type всех возможных типов приватных данных
|
||||
*/
|
||||
export type PrivateAccountSearchDataDomainInterface =
|
||||
| IndividualDomainInterface
|
||||
| EntrepreneurDomainInterface
|
||||
| OrganizationDomainInterface;
|
||||
|
||||
/**
|
||||
* Интерфейс результата поиска приватных аккаунтов
|
||||
*/
|
||||
export interface PrivateAccountSearchResultDomainInterface {
|
||||
type: 'individual' | 'entrepreneur' | 'organization';
|
||||
data: PrivateAccountSearchDataDomainInterface;
|
||||
score?: number;
|
||||
highlightedFields?: string[];
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import type {
|
||||
SearchPrivateAccountsInputDomainInterface,
|
||||
PrivateAccountSearchResultDomainInterface,
|
||||
} from '../interfaces/search-private-accounts-domain.interface';
|
||||
|
||||
/**
|
||||
* Доменный репозиторий для поиска приватных аккаунтов
|
||||
*/
|
||||
export interface SearchPrivateAccountsRepository {
|
||||
/**
|
||||
* Поиск приватных аккаунтов по запросу
|
||||
* @param input Входные данные для поиска
|
||||
* @returns Массив результатов поиска
|
||||
*/
|
||||
searchPrivateAccounts(
|
||||
input: SearchPrivateAccountsInputDomainInterface
|
||||
): Promise<PrivateAccountSearchResultDomainInterface[]>;
|
||||
}
|
||||
|
||||
export const SEARCH_PRIVATE_ACCOUNTS_REPOSITORY = Symbol('SearchPrivateAccountsRepository');
|
||||
@@ -5,9 +5,6 @@ import type { DocumentDomainEntity } from '../entity/document-domain.entity';
|
||||
import type { PaginationResultDomainInterface } from '~/domain/common/interfaces/pagination.interface';
|
||||
import type { DocumentPackageAggregateDomainInterface } from '../interfaces/document-package-aggregate-domain.interface';
|
||||
import type { GetDocumentsInputDomainInterface } from '../interfaces/get-documents-input-domain.interface';
|
||||
import { toDotNotation } from '~/utils/toDotNotation';
|
||||
import { getActions } from '~/utils/getFetch';
|
||||
import { SovietContract } from 'cooptypes';
|
||||
import { DocumentPackageAggregator } from '../aggregators/document-package.aggregator';
|
||||
import { DocumentAggregator } from '../aggregators/document.aggregator';
|
||||
import type { DocumentAggregateDomainInterface } from '../interfaces/document-domain-aggregate.interface';
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AccountDomainModule } from './account/account-domain.module';
|
||||
import { ExtensionDomainModule } from './extension/extension-domain.module';
|
||||
import { PaymentDomainModule } from './payment/payment.module';
|
||||
import { PaymentMethodDomainModule } from './payment-method/payment-method-domain.module';
|
||||
import { ProviderDomainModule } from './provider/provider.module';
|
||||
import { SystemDomainModule } from './system/system-domain.module';
|
||||
@@ -18,6 +17,8 @@ import { DesktopDomainModule } from './desktop/desktop-domain.module';
|
||||
import { MeetDomainModule } from './meet/meet-domain.module';
|
||||
import { ControllerWsMeetModule } from './meet/controllers-ws-meet.module';
|
||||
import { UserCertificateDomainModule } from './user-certificate/user-certificate.module';
|
||||
import { GatewayDomainModule } from './gateway/gateway-domain.module';
|
||||
import { WalletDomainModule } from './wallet/wallet-domain.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -27,7 +28,6 @@ import { UserCertificateDomainModule } from './user-certificate/user-certificate
|
||||
AgreementDomainModule,
|
||||
DesktopDomainModule,
|
||||
ExtensionDomainModule,
|
||||
PaymentDomainModule,
|
||||
PaymentMethodDomainModule,
|
||||
ProviderDomainModule,
|
||||
SystemDomainModule,
|
||||
@@ -39,6 +39,8 @@ import { UserCertificateDomainModule } from './user-certificate/user-certificate
|
||||
MeetDomainModule,
|
||||
ControllerWsMeetModule,
|
||||
UserCertificateDomainModule,
|
||||
GatewayDomainModule,
|
||||
WalletDomainModule,
|
||||
],
|
||||
exports: [
|
||||
AuthDomainModule,
|
||||
@@ -47,7 +49,6 @@ import { UserCertificateDomainModule } from './user-certificate/user-certificate
|
||||
AgreementDomainModule,
|
||||
DesktopDomainModule,
|
||||
ExtensionDomainModule,
|
||||
PaymentDomainModule,
|
||||
PaymentMethodDomainModule,
|
||||
ProviderDomainModule,
|
||||
SystemDomainModule,
|
||||
@@ -58,6 +59,8 @@ import { UserCertificateDomainModule } from './user-certificate/user-certificate
|
||||
CooplaceDomainModule,
|
||||
MeetDomainModule,
|
||||
UserCertificateDomainModule,
|
||||
GatewayDomainModule,
|
||||
WalletDomainModule,
|
||||
],
|
||||
})
|
||||
export class DomainModule {}
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
import type { PaymentDomainInterface, PaymentDetailsDomainInterface } from '../interfaces/payment-domain.interface';
|
||||
import { PaymentStatusEnum } from '../enums/payment-status.enum';
|
||||
import { PaymentTypeEnum, PaymentDirectionEnum } from '../enums/payment-type.enum';
|
||||
import type { UserCertificateDomainInterface } from '~/domain/user-certificate/interfaces/user-certificate-domain.interface';
|
||||
import { IndividualCertificateDTO } from '~/modules/common/dto/individual-certificate.dto';
|
||||
import { EntrepreneurCertificateDTO } from '~/modules/common/dto/entrepreneur-certificate.dto';
|
||||
import { OrganizationCertificateDTO } from '~/modules/common/dto/organization-certificate.dto';
|
||||
import { AccountType } from '~/modules/account/enum/account-type.enum';
|
||||
|
||||
/**
|
||||
* Универсальная доменная сущность платежа
|
||||
* Работает как для входящих, так и для исходящих платежей
|
||||
*/
|
||||
export class PaymentDomainEntity implements PaymentDomainInterface {
|
||||
id?: string;
|
||||
coopname: string;
|
||||
username: string;
|
||||
hash: string;
|
||||
quantity: number;
|
||||
symbol: string;
|
||||
type: PaymentTypeEnum;
|
||||
direction: PaymentDirectionEnum;
|
||||
status: PaymentStatusEnum;
|
||||
provider?: string;
|
||||
payment_method_id?: string;
|
||||
secret?: string;
|
||||
message?: string;
|
||||
memo?: string;
|
||||
expired_at?: Date;
|
||||
created_at: Date;
|
||||
updated_at?: Date;
|
||||
payment_details?: PaymentDetailsDomainInterface;
|
||||
blockchain_data?: any;
|
||||
statement?: any;
|
||||
|
||||
constructor(data: PaymentDomainInterface) {
|
||||
this.id = data.id;
|
||||
this.coopname = data.coopname;
|
||||
this.username = data.username;
|
||||
this.hash = data.hash;
|
||||
this.quantity = data.quantity;
|
||||
this.symbol = data.symbol;
|
||||
this.type = data.type;
|
||||
this.direction = data.direction;
|
||||
this.status = data.status;
|
||||
this.provider = data.provider;
|
||||
this.payment_method_id = data.payment_method_id;
|
||||
this.secret = data.secret;
|
||||
this.message = data.message;
|
||||
this.memo = data.memo;
|
||||
this.expired_at = data.expired_at;
|
||||
this.created_at = data.created_at;
|
||||
this.updated_at = data.updated_at;
|
||||
this.payment_details = data.payment_details;
|
||||
this.blockchain_data = data.blockchain_data;
|
||||
this.statement = data.statement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить количество и символ в формате строки
|
||||
*/
|
||||
getFormattedAmount(): string {
|
||||
return `${this.quantity} ${this.symbol}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверить, может ли статус быть изменен
|
||||
*/
|
||||
canChangeStatus(): boolean {
|
||||
return (
|
||||
this.status !== PaymentStatusEnum.COMPLETED &&
|
||||
this.status !== PaymentStatusEnum.CANCELLED &&
|
||||
this.status !== PaymentStatusEnum.REFUNDED
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить человекочитаемый статус
|
||||
*/
|
||||
getStatusLabel(): string {
|
||||
const statusLabels: Record<PaymentStatusEnum, string> = {
|
||||
[PaymentStatusEnum.PENDING]: 'Ожидает обработки',
|
||||
[PaymentStatusEnum.PROCESSING]: 'В процессе',
|
||||
[PaymentStatusEnum.PAID]: 'Оплачен',
|
||||
[PaymentStatusEnum.COMPLETED]: 'Завершен',
|
||||
[PaymentStatusEnum.FAILED]: 'Ошибка',
|
||||
[PaymentStatusEnum.EXPIRED]: 'Истек',
|
||||
[PaymentStatusEnum.CANCELLED]: 'Отменен',
|
||||
[PaymentStatusEnum.REFUNDED]: 'Возвращен',
|
||||
};
|
||||
return statusLabels[this.status] || this.status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить человекочитаемый тип платежа
|
||||
*/
|
||||
getTypeLabel(): string {
|
||||
const typeLabels: Record<PaymentTypeEnum, string> = {
|
||||
[PaymentTypeEnum.REGISTRATION]: 'Регистрационный взнос',
|
||||
[PaymentTypeEnum.DEPOSIT]: 'Паевой взнос',
|
||||
[PaymentTypeEnum.WITHDRAWAL]: 'Возврат взноса',
|
||||
};
|
||||
return typeLabels[this.type] || this.type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить человекочитаемое направление платежа
|
||||
*/
|
||||
getDirectionLabel(): string {
|
||||
const directionLabels: Record<PaymentDirectionEnum, string> = {
|
||||
[PaymentDirectionEnum.INCOMING]: 'Входящий',
|
||||
[PaymentDirectionEnum.OUTGOING]: 'Исходящий',
|
||||
};
|
||||
return directionLabels[this.direction] || this.direction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить соответствующий хеш для типа платежа
|
||||
*/
|
||||
getSpecificHash(): { [key: string]: string } {
|
||||
const hashValue = this.hash || '';
|
||||
if (this.direction === PaymentDirectionEnum.OUTGOING) {
|
||||
return { outcome_hash: hashValue };
|
||||
} else {
|
||||
return { income_hash: hashValue };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Преобразовать в DTO для GraphQL (без sensitive данных)
|
||||
*/
|
||||
toDTO(usernameCertificate?: UserCertificateDomainInterface | null): any {
|
||||
// Вспомогательная функция для создания сертификата DTO
|
||||
const createCertificateDTO = (
|
||||
certificate: UserCertificateDomainInterface | null
|
||||
): IndividualCertificateDTO | EntrepreneurCertificateDTO | OrganizationCertificateDTO | null => {
|
||||
if (!certificate) return null;
|
||||
|
||||
switch (certificate.type) {
|
||||
case AccountType.individual:
|
||||
return new IndividualCertificateDTO(certificate);
|
||||
case AccountType.entrepreneur:
|
||||
return new EntrepreneurCertificateDTO(certificate);
|
||||
case AccountType.organization:
|
||||
return new OrganizationCertificateDTO(certificate);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
id: this.id,
|
||||
coopname: this.coopname,
|
||||
username: this.username,
|
||||
username_certificate: createCertificateDTO(usernameCertificate || null),
|
||||
hash: this.hash,
|
||||
...this.getSpecificHash(), // Добавляем конкретный хеш
|
||||
quantity: this.quantity,
|
||||
symbol: this.symbol,
|
||||
type: this.type,
|
||||
direction: this.direction,
|
||||
status: this.status,
|
||||
provider: this.provider,
|
||||
payment_method_id: this.payment_method_id,
|
||||
message: this.message,
|
||||
memo: this.memo,
|
||||
expired_at: this.expired_at,
|
||||
created_at: this.created_at,
|
||||
updated_at: this.updated_at,
|
||||
payment_details: this.payment_details,
|
||||
blockchain_data: this.blockchain_data,
|
||||
statement: this.statement,
|
||||
formatted_amount: this.getFormattedAmount(),
|
||||
status_label: this.getStatusLabel(),
|
||||
type_label: this.getTypeLabel(),
|
||||
direction_label: this.getDirectionLabel(),
|
||||
can_change_status: this.canChangeStatus(),
|
||||
is_final: ![PaymentStatusEnum.PENDING, PaymentStatusEnum.PROCESSING].includes(this.status),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Единый enum для статусов платежей
|
||||
* Покрывает все состояния входящих и исходящих платежей
|
||||
*/
|
||||
export enum PaymentStatusEnum {
|
||||
// Начальные состояния
|
||||
PENDING = 'pending',
|
||||
|
||||
// Процесс обработки
|
||||
PROCESSING = 'processing',
|
||||
PAID = 'paid',
|
||||
|
||||
// Завершенные состояния
|
||||
COMPLETED = 'completed',
|
||||
|
||||
// Ошибочные состояния
|
||||
FAILED = 'failed',
|
||||
EXPIRED = 'expired',
|
||||
|
||||
// Отмененные состояния
|
||||
CANCELLED = 'cancelled',
|
||||
REFUNDED = 'refunded',
|
||||
}
|
||||
|
||||
/**
|
||||
* Человекочитаемые названия статусов
|
||||
*/
|
||||
export const PAYMENT_STATUS_LABELS: Record<PaymentStatusEnum, string> = {
|
||||
[PaymentStatusEnum.PENDING]: 'Ожидает оплаты',
|
||||
[PaymentStatusEnum.PROCESSING]: 'Обрабатывается',
|
||||
[PaymentStatusEnum.PAID]: 'Оплачен',
|
||||
[PaymentStatusEnum.COMPLETED]: 'Завершен',
|
||||
[PaymentStatusEnum.FAILED]: 'Не удался',
|
||||
[PaymentStatusEnum.EXPIRED]: 'Истек',
|
||||
[PaymentStatusEnum.CANCELLED]: 'Отменен',
|
||||
[PaymentStatusEnum.REFUNDED]: 'Возвращен',
|
||||
};
|
||||
|
||||
/**
|
||||
* Статусы, при которых платеж можно редактировать
|
||||
*/
|
||||
export const EDITABLE_PAYMENT_STATUSES = [PaymentStatusEnum.PENDING, PaymentStatusEnum.PROCESSING];
|
||||
|
||||
/**
|
||||
* Финальные статусы платежей
|
||||
*/
|
||||
export const FINAL_PAYMENT_STATUSES = [
|
||||
PaymentStatusEnum.COMPLETED,
|
||||
PaymentStatusEnum.FAILED,
|
||||
PaymentStatusEnum.EXPIRED,
|
||||
PaymentStatusEnum.CANCELLED,
|
||||
PaymentStatusEnum.REFUNDED,
|
||||
];
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Тип платежа по назначению
|
||||
*/
|
||||
export enum PaymentTypeEnum {
|
||||
// Входящие платежи (от пользователей к кооперативу)
|
||||
REGISTRATION = 'registration', // Регистрационный взнос
|
||||
DEPOSIT = 'deposit', // Паевой взнос
|
||||
WITHDRAWAL = 'withdrawal', // Возврат паевого взноса
|
||||
}
|
||||
|
||||
/**
|
||||
* Направление платежа
|
||||
*/
|
||||
export enum PaymentDirectionEnum {
|
||||
INCOMING = 'incoming', // Входящий платеж
|
||||
OUTGOING = 'outgoing', // Исходящий платеж
|
||||
}
|
||||
|
||||
/**
|
||||
* Человекочитаемые названия типов платежей
|
||||
*/
|
||||
export const PAYMENT_TYPE_LABELS: Record<PaymentTypeEnum, string> = {
|
||||
[PaymentTypeEnum.REGISTRATION]: 'Вступительный и мин. паевой взносы',
|
||||
[PaymentTypeEnum.DEPOSIT]: 'Паевой взнос',
|
||||
[PaymentTypeEnum.WITHDRAWAL]: 'Возврат паевого взноса',
|
||||
};
|
||||
|
||||
/**
|
||||
* Человекочитаемые названия направлений платежей
|
||||
*/
|
||||
export const PAYMENT_DIRECTION_LABELS: Record<PaymentDirectionEnum, string> = {
|
||||
[PaymentDirectionEnum.INCOMING]: 'Входящий',
|
||||
[PaymentDirectionEnum.OUTGOING]: 'Исходящий',
|
||||
};
|
||||
|
||||
/**
|
||||
* Определяет направление платежа по его типу
|
||||
*/
|
||||
export function getPaymentDirection(type: PaymentTypeEnum): PaymentDirectionEnum {
|
||||
const incomingTypes = [PaymentTypeEnum.REGISTRATION, PaymentTypeEnum.DEPOSIT];
|
||||
|
||||
return incomingTypes.includes(type) ? PaymentDirectionEnum.INCOMING : PaymentDirectionEnum.OUTGOING;
|
||||
}
|
||||
|
||||
/**
|
||||
* Входящие типы платежей
|
||||
*/
|
||||
export const INCOMING_PAYMENT_TYPES = [PaymentTypeEnum.REGISTRATION, PaymentTypeEnum.DEPOSIT];
|
||||
|
||||
/**
|
||||
* Исходящие типы платежей
|
||||
*/
|
||||
export const OUTGOING_PAYMENT_TYPES = [PaymentTypeEnum.WITHDRAWAL];
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { GatewayInteractor } from './interactors/gateway.interactor';
|
||||
import { GatewayExpiryCronService } from './gateway-expiry-cron.service';
|
||||
import { InfrastructureModule } from '~/infrastructure/infrastructure.module';
|
||||
import { AccountDomainModule } from '~/domain/account/account-domain.module';
|
||||
import { ProviderDomainModule } from '~/domain/provider/provider.module';
|
||||
import { SystemDomainModule } from '~/domain/system/system-domain.module';
|
||||
|
||||
@Module({
|
||||
imports: [InfrastructureModule, AccountDomainModule, ProviderDomainModule, SystemDomainModule],
|
||||
providers: [GatewayInteractor, GatewayExpiryCronService],
|
||||
exports: [GatewayInteractor],
|
||||
})
|
||||
export class GatewayDomainModule {}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Injectable, OnModuleInit, Logger } from '@nestjs/common';
|
||||
import cron from 'node-cron';
|
||||
import { GatewayInteractor } from './interactors/gateway.interactor';
|
||||
|
||||
@Injectable()
|
||||
export class GatewayExpiryCronService implements OnModuleInit {
|
||||
private readonly logger = new Logger(GatewayExpiryCronService.name);
|
||||
|
||||
constructor(private readonly gatewayInteractor: GatewayInteractor) {}
|
||||
|
||||
onModuleInit() {
|
||||
// Запуск задачи раз в 10 минут
|
||||
cron.schedule('*/10 * * * *', async () => {
|
||||
const count = await this.gatewayInteractor.expireOutdatedPayments();
|
||||
if (count > 0) {
|
||||
this.logger.log(`Истекло ${count} платежей (переведены в статус EXPIRED) через node-cron`);
|
||||
}
|
||||
});
|
||||
this.logger.log('node-cron для expireOutdatedPayments запущен (каждые 10 минут)');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,521 @@
|
||||
import { Injectable, Inject, Logger, NotFoundException, BadRequestException } from '@nestjs/common';
|
||||
import type { SetPaymentStatusInputDomainInterface } from '../interfaces/set-payment-status-domain-input.interface';
|
||||
import type { InternalPaymentFiltersDomainInterface } from '../interfaces/payment-filters-domain.interface';
|
||||
import type {
|
||||
PaginationInputDomainInterface,
|
||||
PaginationResultDomainInterface,
|
||||
} from '~/domain/common/interfaces/pagination.interface';
|
||||
import { PaymentDomainEntity } from '../entities/payment-domain.entity';
|
||||
import { GatewayBlockchainPort, GATEWAY_BLOCKCHAIN_PORT } from '../ports/gateway-blockchain.port';
|
||||
import { PaymentRepository, PAYMENT_REPOSITORY } from '../repositories/payment.repository';
|
||||
import { PaymentStatusEnum } from '../enums/payment-status.enum';
|
||||
import { PaymentDirectionEnum, PaymentTypeEnum } from '../enums/payment-type.enum';
|
||||
import type { PaymentDomainInterface } from '../interfaces/payment-domain.interface';
|
||||
import type { CreateInitialPaymentInputDomainInterface } from '../interfaces/create-initial-payment-input-domain.interface';
|
||||
import type { CreateDepositPaymentInputDomainInterface } from '../interfaces/create-deposit-payment-input-domain.interface';
|
||||
import type { CreateWithdrawPaymentInputDomainInterface } from '../interfaces/create-withdraw-payment-input-domain.interface';
|
||||
import config from '~/config/config';
|
||||
import { AccountDomainInteractor } from '~/domain/account/interactors/account.interactor';
|
||||
import type { CompleteIncomeDomainInterface } from '../interfaces/complete-income-domain.interface';
|
||||
import type { CompleteOutcomeDomainInterface } from '../interfaces/complete-outcome-domain.interface';
|
||||
import { generateUniqueHash } from '~/utils/generate-hash.util';
|
||||
import { ProviderInteractor } from '~/domain/provider/provider.interactor';
|
||||
import { QuantityUtils } from '~/shared/utils/quantity.utils';
|
||||
import { SystemDomainInteractor } from '~/domain/system/interactors/system.interactor';
|
||||
import { AccountDomainService, ACCOUNT_DOMAIN_SERVICE } from '~/domain/account/services/account-domain.service';
|
||||
import { AccountType } from '~/modules/account/enum/account-type.enum';
|
||||
import Settings from '~/models/settings.model';
|
||||
import { PaymentMethodRepository, PAYMENT_METHOD_REPOSITORY } from '~/domain/common/repositories/payment-method.repository';
|
||||
import type { PaymentDetailsDomainInterface } from '../interfaces/payment-domain.interface';
|
||||
|
||||
/**
|
||||
* Интерактор домена gateway для управления платежами (просмотр, изменение статуса и создание)
|
||||
*
|
||||
* Особенности работы с истечением платежей:
|
||||
* - Все новые платежи (регистрация и депозит) автоматически получают срок истечения +24 часа
|
||||
* - При создании нового платежа проверяется наличие активного платежа того же типа
|
||||
* - Истекшие платежи автоматически обновляются в статус EXPIRED при создании новых платежей
|
||||
* - Доступен отдельный метод expireOutdatedPayments() для периодической очистки
|
||||
*/
|
||||
@Injectable()
|
||||
export class GatewayInteractor {
|
||||
private readonly logger = new Logger(GatewayInteractor.name);
|
||||
|
||||
constructor(
|
||||
@Inject(GATEWAY_BLOCKCHAIN_PORT)
|
||||
private readonly gatewayBlockchainPort: GatewayBlockchainPort,
|
||||
@Inject(PAYMENT_REPOSITORY)
|
||||
private readonly paymentRepository: PaymentRepository,
|
||||
private readonly accountDomainInteractor: AccountDomainInteractor,
|
||||
private readonly providerInteractor: ProviderInteractor,
|
||||
private readonly systemDomainInteractor: SystemDomainInteractor,
|
||||
@Inject(ACCOUNT_DOMAIN_SERVICE)
|
||||
private readonly accountDomainService: AccountDomainService,
|
||||
@Inject(PAYMENT_METHOD_REPOSITORY)
|
||||
private readonly paymentMethodRepository: PaymentMethodRepository
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Создать дату истечения платежа (24 часа от текущего момента)
|
||||
* @returns дата истечения
|
||||
*/
|
||||
private createPaymentExpirationDate(): Date {
|
||||
return new Date(Date.now() + 24 * 60 * 60 * 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить платежи с фильтрацией (универсальный метод с поддержкой секрета)
|
||||
*/
|
||||
async getPayments(
|
||||
filters: InternalPaymentFiltersDomainInterface,
|
||||
options: PaginationInputDomainInterface
|
||||
): Promise<PaginationResultDomainInterface<PaymentDomainEntity>> {
|
||||
const result = await this.paymentRepository.getAllPayments(filters, options);
|
||||
|
||||
// Преобразуем результаты в доменные сущности
|
||||
const items = result.items.map((item) => new PaymentDomainEntity(item));
|
||||
|
||||
return {
|
||||
items,
|
||||
totalCount: result.totalCount,
|
||||
totalPages: result.totalPages,
|
||||
currentPage: result.currentPage,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Установить статус платежа (работает для входящих и исходящих платежей)
|
||||
*/
|
||||
async setPaymentStatus(data: SetPaymentStatusInputDomainInterface): Promise<PaymentDomainEntity> {
|
||||
try {
|
||||
const payment = await this.paymentRepository.findById(data.id);
|
||||
if (!payment) {
|
||||
throw new NotFoundException(`Не удалось найти платеж с ID ${data.id}`);
|
||||
}
|
||||
|
||||
const paymentEntity = new PaymentDomainEntity(payment);
|
||||
const statusEnum = data.status as unknown as PaymentStatusEnum;
|
||||
|
||||
// Проверяем можно ли изменить статус
|
||||
if (!paymentEntity.canChangeStatus()) {
|
||||
throw new Error(`Невозможно изменить статус платежа с текущим статусом: ${payment.status}`);
|
||||
}
|
||||
|
||||
const result = await this.paymentRepository.setPaymentStatus(data.id, statusEnum);
|
||||
|
||||
if (!result) {
|
||||
throw new NotFoundException(`Не удалось найти платеж с ID ${data.id}`);
|
||||
}
|
||||
|
||||
// Обрабатываем платеж при статусе PAID
|
||||
if (statusEnum === PaymentStatusEnum.PAID) {
|
||||
if (result.direction === PaymentDirectionEnum.INCOMING) {
|
||||
// Обработка входящих платежей
|
||||
await this.processIncomingPayment(result);
|
||||
} else if (result.direction === PaymentDirectionEnum.OUTGOING) {
|
||||
// Обработка исходящих платежей
|
||||
await this.processOutgoingPayment(result);
|
||||
}
|
||||
}
|
||||
|
||||
return new PaymentDomainEntity(result);
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Не удалось обновить статус платежа: ${error.message}`);
|
||||
throw new NotFoundException(`Не удалось найти платеж с ID ${data.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Выполнить процессинг платежа
|
||||
*/
|
||||
async executeIncomePayment(id: string, status: PaymentStatusEnum) {
|
||||
const payment = await this.paymentRepository.findById(id);
|
||||
if (!payment) {
|
||||
this.logger.error(`Платеж с ID ${id} не найден`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (status === PaymentStatusEnum.PAID && payment.status !== PaymentStatusEnum.COMPLETED) {
|
||||
await this.processIncomingPayment(payment);
|
||||
} else {
|
||||
this.logger.warn(`Платеж с ID ${id} имеет статус ${status}, пропускаем обработку`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обработка входящих платежей
|
||||
*/
|
||||
private async processIncomingPayment(payment: PaymentDomainInterface) {
|
||||
this.logger.log(`Обрабатываем входящий платеж ${payment.id}`);
|
||||
|
||||
try {
|
||||
if (payment.type === PaymentTypeEnum.REGISTRATION) {
|
||||
await this.accountDomainInteractor.registerBlockchainAccount(payment.username);
|
||||
this.logger.log(`Новый пользователь зарегистрирован: ${payment.username}`);
|
||||
if (payment.id) {
|
||||
await this.paymentRepository.update(payment.id, { status: PaymentStatusEnum.COMPLETED });
|
||||
}
|
||||
} else if (payment.type === PaymentTypeEnum.DEPOSIT) {
|
||||
// Парсим quantity и форматируем для блокчейна с правильным precision
|
||||
const blockchainFormattedQuantity = QuantityUtils.formatQuantityForBlockchain(payment.quantity, payment.symbol);
|
||||
|
||||
const completeIncomeData: CompleteIncomeDomainInterface = {
|
||||
coopname: payment.coopname,
|
||||
username: payment.username,
|
||||
quantity: blockchainFormattedQuantity,
|
||||
income_hash: payment.hash,
|
||||
};
|
||||
await this.gatewayBlockchainPort.completeIncome(completeIncomeData);
|
||||
if (payment.id) {
|
||||
await this.paymentRepository.update(payment.id, { status: PaymentStatusEnum.COMPLETED });
|
||||
}
|
||||
this.logger.log(`Пользователь ${payment.username} внес паевой взнос на сумму ${blockchainFormattedQuantity}`);
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (payment.id) {
|
||||
await this.paymentRepository.update(payment.id, {
|
||||
status: PaymentStatusEnum.FAILED,
|
||||
message: e.message,
|
||||
});
|
||||
}
|
||||
this.logger.error(`Ошибка обработки входящего платежа: ${payment.id}, сообщение: ${e.message}`, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обработка исходящих платежей
|
||||
*/
|
||||
private async processOutgoingPayment(payment: PaymentDomainInterface) {
|
||||
this.logger.log(`Обрабатываем исходящий платеж ${payment.id}`);
|
||||
|
||||
try {
|
||||
const completeOutcomeData: CompleteOutcomeDomainInterface = {
|
||||
coopname: payment.coopname,
|
||||
outcome_hash: payment.hash,
|
||||
};
|
||||
|
||||
await this.gatewayBlockchainPort.completeOutcome(completeOutcomeData);
|
||||
|
||||
if (payment.id) {
|
||||
await this.paymentRepository.update(payment.id, { status: PaymentStatusEnum.COMPLETED });
|
||||
}
|
||||
|
||||
this.logger.log(`Завершен исходящий платеж: ${payment.hash} для пользователя ${payment.username}`);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
if (payment.id) {
|
||||
await this.paymentRepository.update(payment.id, {
|
||||
status: PaymentStatusEnum.FAILED,
|
||||
message: e.message,
|
||||
});
|
||||
}
|
||||
this.logger.error(`Ошибка обработки исходящего платежа: ${payment.id}, сообщение: ${e.message}`, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Создать регистрационный платеж
|
||||
*/
|
||||
async createInitialPayment(data: CreateInitialPaymentInputDomainInterface): Promise<PaymentDomainEntity> {
|
||||
// Обновляем истекшие платежи перед созданием нового
|
||||
await this.paymentRepository.expireOutdatedPayments();
|
||||
|
||||
// Получаем информацию о системе для извлечения сумм платежей
|
||||
const systemInfo = await this.systemDomainInteractor.getInfo();
|
||||
const cooperatorAccount = systemInfo.cooperator_account;
|
||||
|
||||
// Получаем информацию об аккаунте пользователя для определения типа
|
||||
const userAccount = await this.accountDomainService.getAccount(data.username);
|
||||
|
||||
if (!userAccount.private_account) {
|
||||
throw new Error(`Не удалось определить тип аккаунта для пользователя ${data.username}`);
|
||||
}
|
||||
|
||||
// Определяем сумму платежа на основе типа аккаунта
|
||||
let registrationAmount: string;
|
||||
if (userAccount.private_account.type === AccountType.organization) {
|
||||
registrationAmount = cooperatorAccount.org_registration;
|
||||
} else {
|
||||
// Для individual и entrepreneur используем обычную сумму registration
|
||||
registrationAmount = cooperatorAccount.registration;
|
||||
}
|
||||
|
||||
// Парсим количество и символ из строки суммы
|
||||
const { amount, symbol } = QuantityUtils.parseQuantityString(registrationAmount);
|
||||
|
||||
// Валидируем символ
|
||||
QuantityUtils.validateSymbol(symbol);
|
||||
|
||||
// Проверяем, нет ли уже активного платежа этого типа для пользователя с такой же суммой
|
||||
const existingPayment = await this.paymentRepository.findActivePendingPayment(
|
||||
data.username,
|
||||
PaymentTypeEnum.REGISTRATION,
|
||||
amount,
|
||||
symbol
|
||||
);
|
||||
|
||||
if (existingPayment) {
|
||||
this.logger.log(
|
||||
`Найден существующий активный регистрационный платеж для пользователя ${data.username} на сумму ${amount} ${symbol}`
|
||||
);
|
||||
return new PaymentDomainEntity(existingPayment);
|
||||
}
|
||||
|
||||
// Получаем настройки для определения провайдера
|
||||
const settings = await Settings.getSettings();
|
||||
const provider = settings.provider.name;
|
||||
|
||||
const secret = generateUniqueHash();
|
||||
const hash = generateUniqueHash();
|
||||
const now = new Date();
|
||||
const expiredAt = this.createPaymentExpirationDate();
|
||||
|
||||
const paymentData: PaymentDomainInterface = {
|
||||
id: '', // будет установлен в репозитории
|
||||
coopname: config.coopname,
|
||||
username: data.username,
|
||||
quantity: amount,
|
||||
symbol: symbol,
|
||||
secret,
|
||||
type: PaymentTypeEnum.REGISTRATION,
|
||||
direction: PaymentDirectionEnum.INCOMING,
|
||||
provider,
|
||||
status: PaymentStatusEnum.PENDING,
|
||||
memo: `Вступительный и минимальный паевой взносы №${hash.slice(0, 8)}`,
|
||||
payment_method_id: undefined,
|
||||
expired_at: expiredAt,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
hash,
|
||||
};
|
||||
|
||||
// Создаем платеж в базе данных
|
||||
const createdPayment = await this.paymentRepository.create(paymentData);
|
||||
|
||||
if (!createdPayment.id) {
|
||||
throw new Error('Не удалось создать платеж - отсутствует ID');
|
||||
}
|
||||
|
||||
// Получаем провайдер и создаем платежные детали
|
||||
const paymentProvider = this.providerInteractor.getProvider(provider);
|
||||
if (!paymentProvider) {
|
||||
throw new Error(`Провайдер ${provider} не найден`);
|
||||
}
|
||||
|
||||
try {
|
||||
const paymentDetails = await paymentProvider.createPayment(hash);
|
||||
|
||||
// Обновляем платеж с деталями
|
||||
await this.paymentRepository.update(createdPayment.id, {
|
||||
payment_details: paymentDetails,
|
||||
});
|
||||
|
||||
// Возвращаем обновленный платеж
|
||||
const updatedPayment = await this.paymentRepository.findById(createdPayment.id);
|
||||
if (!updatedPayment) {
|
||||
throw new Error(`Не удалось найти обновленный платеж с ID ${createdPayment.id}`);
|
||||
}
|
||||
return new PaymentDomainEntity(updatedPayment);
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Ошибка при создании платежных деталей: ${error.message}`);
|
||||
// Если не удалось создать детали платежа, помечаем платеж как неудачный
|
||||
await this.paymentRepository.update(createdPayment.id, {
|
||||
status: PaymentStatusEnum.FAILED,
|
||||
message: `Ошибка создания платежа: ${error.message}`,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Создать депозитный платеж
|
||||
*/
|
||||
async createDeposit(data: CreateDepositPaymentInputDomainInterface): Promise<PaymentDomainEntity> {
|
||||
// Обновляем истекшие платежи перед созданием нового
|
||||
await this.paymentRepository.expireOutdatedPayments();
|
||||
|
||||
// Валидируем символ перед созданием платежа
|
||||
QuantityUtils.validateSymbol(data.symbol);
|
||||
|
||||
// Проверяем, нет ли уже активного платежа этого типа для пользователя с такой же суммой
|
||||
const existingPayment = await this.paymentRepository.findActivePendingPayment(
|
||||
data.username,
|
||||
PaymentTypeEnum.DEPOSIT,
|
||||
data.quantity,
|
||||
data.symbol
|
||||
);
|
||||
|
||||
if (existingPayment) {
|
||||
this.logger.log(
|
||||
`Найден существующий активный депозитный платеж для пользователя ${data.username} на сумму ${data.quantity} ${data.symbol}`
|
||||
);
|
||||
return new PaymentDomainEntity(existingPayment);
|
||||
}
|
||||
|
||||
// Получаем настройки для определения провайдера
|
||||
const settings = await Settings.getSettings();
|
||||
const provider = settings.provider.name;
|
||||
|
||||
const secret = generateUniqueHash();
|
||||
const hash = generateUniqueHash();
|
||||
const now = new Date();
|
||||
const expiredAt = this.createPaymentExpirationDate();
|
||||
|
||||
const paymentData: PaymentDomainInterface = {
|
||||
id: '', // будет установлен в репозитории
|
||||
coopname: config.coopname,
|
||||
username: data.username,
|
||||
quantity: data.quantity,
|
||||
symbol: data.symbol,
|
||||
type: PaymentTypeEnum.DEPOSIT,
|
||||
direction: PaymentDirectionEnum.INCOMING,
|
||||
provider: provider,
|
||||
status: PaymentStatusEnum.PENDING,
|
||||
memo: `Паевой взнос по соглашению о ЦПП "Цифровой Кошелёк" №${hash.slice(0, 8)}`,
|
||||
secret,
|
||||
payment_method_id: undefined,
|
||||
expired_at: expiredAt,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
hash,
|
||||
};
|
||||
|
||||
// Создаем платеж в базе данных
|
||||
const createdPayment = await this.paymentRepository.create(paymentData);
|
||||
|
||||
if (!createdPayment.id) {
|
||||
throw new Error('Не удалось создать платеж - отсутствует ID');
|
||||
}
|
||||
|
||||
// Получаем провайдер и создаем платежные детали
|
||||
const paymentProvider = this.providerInteractor.getProvider(provider);
|
||||
if (!paymentProvider) {
|
||||
throw new Error(`Провайдер ${provider} не найден`);
|
||||
}
|
||||
|
||||
try {
|
||||
const paymentDetails = await paymentProvider.createPayment(hash);
|
||||
|
||||
// Обновляем платеж с деталями
|
||||
await this.paymentRepository.update(createdPayment.id, {
|
||||
payment_details: paymentDetails,
|
||||
});
|
||||
|
||||
// Возвращаем обновленный платеж
|
||||
const updatedPayment = await this.paymentRepository.findById(createdPayment.id);
|
||||
if (!updatedPayment) {
|
||||
throw new Error(`Не удалось найти обновленный платеж с ID ${createdPayment.id}`);
|
||||
}
|
||||
return new PaymentDomainEntity(updatedPayment);
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Ошибка при создании платежных деталей: ${error.message}`);
|
||||
// Если не удалось создать детали платежа, помечаем платеж как неудачный
|
||||
await this.paymentRepository.update(createdPayment.id, {
|
||||
status: PaymentStatusEnum.FAILED,
|
||||
message: `Ошибка создания платежа: ${error.message}`,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Создать исходящий платеж (withdraw)
|
||||
*/
|
||||
async createWithdraw(data: CreateWithdrawPaymentInputDomainInterface): Promise<PaymentDomainEntity> {
|
||||
// Обновляем истекшие платежи перед созданием нового
|
||||
await this.paymentRepository.expireOutdatedPayments();
|
||||
|
||||
// Валидируем символ перед созданием платежа
|
||||
QuantityUtils.validateSymbol(data.symbol);
|
||||
|
||||
// Проверяем, нет ли уже платежа с таким же хешем - это ошибка
|
||||
const existingPayment = await this.paymentRepository.findByHash(data.payment_hash);
|
||||
|
||||
if (existingPayment) {
|
||||
throw new Error(`Платеж с хешем ${data.payment_hash} уже существует. Возможно, заявка была создана ранее.`);
|
||||
}
|
||||
|
||||
// Получаем настройки для определения провайдера
|
||||
const settings = await Settings.getSettings();
|
||||
const provider = settings.provider.name;
|
||||
|
||||
const now = new Date();
|
||||
const expiredAt = this.createPaymentExpirationDate();
|
||||
|
||||
// Получаем данные платежного метода из generator repository
|
||||
let paymentDetails: PaymentDetailsDomainInterface;
|
||||
try {
|
||||
const paymentMethod = await this.paymentMethodRepository.get({
|
||||
username: data.username,
|
||||
method_id: data.method_id,
|
||||
});
|
||||
|
||||
// Формируем payment_details на основе данных платежного метода
|
||||
paymentDetails = {
|
||||
data: paymentMethod.data,
|
||||
// Для исходящих платежей комиссия обычно не применяется
|
||||
amount_plus_fee: data.quantity.toString(),
|
||||
amount_without_fee: data.quantity.toString(),
|
||||
fee_amount: '0',
|
||||
fee_percent: 0,
|
||||
fact_fee_percent: 0,
|
||||
tolerance_percent: 0,
|
||||
};
|
||||
} catch (error: any) {
|
||||
throw new BadRequestException(
|
||||
`Платежный метод ${data.method_id} для пользователя ${data.username} не найден. Невозможно создать исходящий платеж.`
|
||||
);
|
||||
}
|
||||
|
||||
const paymentData: PaymentDomainInterface = {
|
||||
id: '', // будет установлен в репозитории
|
||||
coopname: data.coopname,
|
||||
username: data.username,
|
||||
quantity: data.quantity,
|
||||
symbol: data.symbol,
|
||||
type: PaymentTypeEnum.WITHDRAWAL,
|
||||
direction: PaymentDirectionEnum.OUTGOING,
|
||||
provider: provider,
|
||||
status: PaymentStatusEnum.PENDING,
|
||||
memo: `Возврат паевого взноса №${data.payment_hash.slice(0, 8)}`,
|
||||
secret: generateUniqueHash(),
|
||||
payment_method_id: data.method_id,
|
||||
payment_details: paymentDetails,
|
||||
expired_at: expiredAt,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
hash: data.payment_hash, // Используем переданный payment_hash
|
||||
};
|
||||
|
||||
// Создаем платеж в базе данных
|
||||
const createdPayment = await this.paymentRepository.create(paymentData);
|
||||
|
||||
if (!createdPayment.id) {
|
||||
throw new Error('Не удалось создать платеж - отсутствует ID');
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Создан исходящий платеж ${data.payment_hash} для пользователя ${data.username} на сумму ${data.quantity} ${data.symbol} с платежным методом ${data.method_id}`
|
||||
);
|
||||
|
||||
return new PaymentDomainEntity(createdPayment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновить все истекшие платежи в статус EXPIRED
|
||||
* Может вызываться периодически или по требованию
|
||||
* @returns количество обновленных платежей
|
||||
*/
|
||||
async expireOutdatedPayments(): Promise<number> {
|
||||
try {
|
||||
const expiredCount = await this.paymentRepository.expireOutdatedPayments();
|
||||
if (expiredCount > 0) {
|
||||
this.logger.log(`Истекло ${expiredCount} платежей (переведены в статус EXPIRED)`);
|
||||
}
|
||||
return expiredCount;
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Ошибка при истечении платежей: ${error.message}`, error);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
export interface CompleteIncomeDomainInterface {
|
||||
coopname: string;
|
||||
username: string;
|
||||
quantity: string;
|
||||
income_hash: string;
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
export interface CompleteOutcomeDomainInterface {
|
||||
coopname: string;
|
||||
outcome_hash: string;
|
||||
}
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
export interface CreateDepositPaymentInputDomainInterface {
|
||||
quantity: string;
|
||||
quantity: number;
|
||||
username: string;
|
||||
symbol: string;
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import type { ISignedDocumentDomainInterface } from '~/domain/document/interfaces/signed-document-domain.interface';
|
||||
|
||||
/**
|
||||
* Доменный интерфейс для создания исходящего платежа
|
||||
*/
|
||||
export interface CreateOutgoingPaymentInputDomainInterface {
|
||||
coopname: string;
|
||||
username: string;
|
||||
quantity: string;
|
||||
symbol: string;
|
||||
method_id: string;
|
||||
memo?: string;
|
||||
statement: ISignedDocumentDomainInterface;
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import type { ISignedDocumentDomainInterface } from '~/domain/document/interfaces/signed-document-domain.interface';
|
||||
|
||||
/**
|
||||
* Доменный интерфейс для создания исходящего платежа (withdraw)
|
||||
*/
|
||||
export interface CreateWithdrawPaymentInputDomainInterface {
|
||||
coopname: string;
|
||||
username: string;
|
||||
quantity: number;
|
||||
symbol: string;
|
||||
method_id: string;
|
||||
statement: ISignedDocumentDomainInterface;
|
||||
payment_hash: string; // Хеш платежа для связи с withdraw
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
export interface DeclineOutcomeDomainInterface {
|
||||
coopname: string;
|
||||
outcome_hash: string;
|
||||
reason: string;
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
export enum PaymentTypeDomainInterface {
|
||||
export enum IncomingPaymentTypeDomainInterface {
|
||||
REGISTRATION = 'registration',
|
||||
DEPOSIT = 'deposit',
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import type { PaymentStatusEnum } from '../enums/payment-status.enum';
|
||||
import type { PaymentTypeEnum, PaymentDirectionEnum } from '../enums/payment-type.enum';
|
||||
import type { ISignedDocumentDomainInterface } from '~/domain/document/interfaces/signed-document-domain.interface';
|
||||
|
||||
/**
|
||||
* Детали платежа
|
||||
*/
|
||||
export interface PaymentDetailsDomainInterface {
|
||||
/** Строка с данными платежа (QR-код, токен, реквизиты и т.д.) */
|
||||
data: any;
|
||||
/** Сумма платежа с учетом комиссии */
|
||||
amount_plus_fee: string;
|
||||
/** Сумма платежа без учета комиссии */
|
||||
amount_without_fee: string;
|
||||
/** Размер комиссии в абсолютных значениях */
|
||||
fee_amount: string;
|
||||
/** Процент комиссии */
|
||||
fee_percent: number;
|
||||
/** Фактический процент комиссии */
|
||||
fact_fee_percent: number;
|
||||
/** Допустимый процент отклонения */
|
||||
tolerance_percent: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Унифицированный доменный интерфейс платежа
|
||||
* Подходит для всех типов платежей (входящих и исходящих)
|
||||
*/
|
||||
export interface PaymentDomainInterface {
|
||||
/** Уникальный идентификатор платежа */
|
||||
id?: string;
|
||||
|
||||
/** Хеш платежа для идентификации в блокчейне */
|
||||
hash: string;
|
||||
|
||||
/** Название кооператива */
|
||||
coopname: string;
|
||||
|
||||
/** Имя пользователя */
|
||||
username: string;
|
||||
|
||||
/** Количество/сумма платежа */
|
||||
quantity: number;
|
||||
|
||||
/** Символ валюты */
|
||||
symbol: string;
|
||||
|
||||
/** Тип платежа (назначение) */
|
||||
type: PaymentTypeEnum;
|
||||
|
||||
/** Направление платежа (входящий/исходящий) */
|
||||
direction: PaymentDirectionEnum;
|
||||
|
||||
/** Статус платежа */
|
||||
status: PaymentStatusEnum;
|
||||
|
||||
/** Провайдер платежа */
|
||||
provider?: string;
|
||||
|
||||
/** ID платежного метода */
|
||||
payment_method_id?: string;
|
||||
|
||||
/** Секрет для авторизации платежа */
|
||||
secret?: string;
|
||||
|
||||
/** Сообщение об ошибке или дополнительная информация */
|
||||
message?: string;
|
||||
|
||||
/** Дополнительная информация */
|
||||
memo?: string;
|
||||
|
||||
/** Дата истечения платежа */
|
||||
expired_at?: Date;
|
||||
|
||||
/** Дата создания */
|
||||
created_at: Date;
|
||||
|
||||
/** Дата последнего обновления */
|
||||
updated_at?: Date;
|
||||
|
||||
/** Детали платежа */
|
||||
payment_details?: PaymentDetailsDomainInterface;
|
||||
|
||||
/** Данные из блокчейна */
|
||||
blockchain_data?: any;
|
||||
|
||||
/** Подписанный документ заявления (для исходящих платежей) */
|
||||
statement?: ISignedDocumentDomainInterface;
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
import type { PaymentStatusEnum } from '../enums/payment-status.enum';
|
||||
import type { PaymentTypeEnum, PaymentDirectionEnum } from '../enums/payment-type.enum';
|
||||
|
||||
/**
|
||||
* Доменный интерфейс для фильтрации платежей (внешний API)
|
||||
* Не содержит sensitive данные как secret
|
||||
*/
|
||||
export interface PaymentFiltersDomainInterface {
|
||||
coopname?: string;
|
||||
username?: string;
|
||||
status?: PaymentStatusEnum;
|
||||
type?: PaymentTypeEnum;
|
||||
direction?: PaymentDirectionEnum;
|
||||
provider?: string;
|
||||
hash?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Расширенный доменный интерфейс для внутренней фильтрации платежей
|
||||
* Содержит все поля включая sensitive данные
|
||||
*/
|
||||
export interface InternalPaymentFiltersDomainInterface extends PaymentFiltersDomainInterface {
|
||||
secret?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Доменный интерфейс для создания платежа
|
||||
*/
|
||||
export interface CreatePaymentDomainInterface {
|
||||
coopname: string;
|
||||
username: string;
|
||||
quantity: string;
|
||||
symbol: string;
|
||||
type: PaymentTypeEnum;
|
||||
provider?: string;
|
||||
payment_method_id?: string;
|
||||
memo?: string;
|
||||
expired_at?: Date;
|
||||
statement?: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Доменный интерфейс для обновления статуса платежа
|
||||
*/
|
||||
export interface UpdatePaymentStatusDomainInterface {
|
||||
id: string;
|
||||
status: PaymentStatusEnum;
|
||||
message?: string;
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
import type { PaymentStatusEnum } from '../enums/payment-status.enum';
|
||||
|
||||
export interface SetPaymentStatusInputDomainInterface {
|
||||
id: string;
|
||||
status: PaymentStatusEnum;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { TransactionResult } from '~/domain/blockchain/types/transaction-result.type';
|
||||
import type { GatewayContract } from 'cooptypes';
|
||||
import type { CompleteIncomeDomainInterface } from '../interfaces/complete-income-domain.interface';
|
||||
import type { CompleteOutcomeDomainInterface } from '../interfaces/complete-outcome-domain.interface';
|
||||
import type { DeclineOutcomeDomainInterface } from '../interfaces/decline-outcome-domain.interface';
|
||||
|
||||
/**
|
||||
* Порт для взаимодействия с gateway блокчейном
|
||||
* Только управление уже созданными outcomes - создание происходит автоматически другими контрактами
|
||||
*/
|
||||
export interface GatewayBlockchainPort {
|
||||
// Завершение исходящего платежа
|
||||
completeOutcome(data: CompleteOutcomeDomainInterface): Promise<TransactionResult>;
|
||||
|
||||
// Отклонение исходящего платежа
|
||||
declineOutcome(data: DeclineOutcomeDomainInterface): Promise<TransactionResult>;
|
||||
|
||||
// Получение исходящих платежей из блокчейна
|
||||
getOutcomes(coopname: string): Promise<GatewayContract.Tables.Outcomes.IOutcome[]>;
|
||||
|
||||
// Получение конкретного исходящего платежа
|
||||
getOutcome(coopname: string, outcome_hash: string): Promise<GatewayContract.Tables.Outcomes.IOutcome | null>;
|
||||
|
||||
// Завершение входящего платежа (депозита)
|
||||
completeIncome(data: CompleteIncomeDomainInterface): Promise<TransactionResult>;
|
||||
}
|
||||
|
||||
export const GATEWAY_BLOCKCHAIN_PORT = Symbol('GATEWAY_BLOCKCHAIN_PORT');
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { PaymentStatusEnum } from '../enums/payment-status.enum';
|
||||
import type { PaymentDomainInterface } from '../interfaces/payment-domain.interface';
|
||||
import type { InternalPaymentFiltersDomainInterface } from '../interfaces/payment-filters-domain.interface';
|
||||
import type {
|
||||
PaginationInputDomainInterface,
|
||||
PaginationResultDomainInterface,
|
||||
} from '~/domain/common/interfaces/pagination.interface';
|
||||
import { PaymentTypeEnum } from '../enums/payment-type.enum';
|
||||
|
||||
export interface PaymentRepository {
|
||||
// Общие методы
|
||||
findById(id: string): Promise<PaymentDomainInterface | null>;
|
||||
findByHash(hash: string): Promise<PaymentDomainInterface | null>;
|
||||
create(data: PaymentDomainInterface): Promise<PaymentDomainInterface>;
|
||||
update(id: string, data: Partial<PaymentDomainInterface>): Promise<PaymentDomainInterface | null>;
|
||||
|
||||
// Методы для работы с платежами
|
||||
updatePaymentWithBlockchainData(hash: string, blockchain_data: any): Promise<PaymentDomainInterface | null>;
|
||||
setPaymentStatus(id: string, status: PaymentStatusEnum): Promise<PaymentDomainInterface | null>;
|
||||
|
||||
// Универсальные методы
|
||||
getAllPayments(
|
||||
filters: InternalPaymentFiltersDomainInterface,
|
||||
options: PaginationInputDomainInterface
|
||||
): Promise<PaginationResultDomainInterface<PaymentDomainInterface>>;
|
||||
|
||||
// Новые методы для работы с истекшими платежами
|
||||
/**
|
||||
* Обновить все истекшие платежи в статус EXPIRED
|
||||
* @returns количество обновленных платежей
|
||||
*/
|
||||
expireOutdatedPayments(): Promise<number>;
|
||||
|
||||
/**
|
||||
* Найти активный платеж того же типа для пользователя
|
||||
* @param username имя пользователя
|
||||
* @param type тип платежа
|
||||
* @param quantity количество
|
||||
* @param symbol символ валюты
|
||||
* @returns активный платеж или null
|
||||
*/
|
||||
findActivePendingPayment(
|
||||
username: string,
|
||||
type: PaymentTypeEnum,
|
||||
quantity: number,
|
||||
symbol: string
|
||||
): Promise<PaymentDomainInterface | null>;
|
||||
}
|
||||
|
||||
export const PAYMENT_REPOSITORY = Symbol('PaymentRepository');
|
||||
@@ -146,7 +146,6 @@ export class MeetDomainInteractor {
|
||||
|
||||
// Получаем данные processed из репозитория
|
||||
const processedMeetEntity = await this.meetProcessedRepository.findByHash(processingData.hash);
|
||||
|
||||
// Создаем агрегат с данными из репозитория и блокчейна
|
||||
return new MeetAggregate(preMeet, processingData, processedMeetEntity);
|
||||
})
|
||||
|
||||
+12
-1
@@ -16,6 +16,9 @@ import http from 'http-status';
|
||||
import { PublicKey, Signature } from '@wharfkit/antelope';
|
||||
import { ISignedDocumentDomainInterface } from '~/domain/document/interfaces/signed-document-domain.interface';
|
||||
import { Classes } from '@coopenomics/sdk';
|
||||
import { GatewayInteractor } from '~/domain/gateway/interactors/gateway.interactor';
|
||||
import type { CreateInitialPaymentInputDomainInterface } from '~/domain/gateway/interfaces/create-initial-payment-input-domain.interface';
|
||||
import { PaymentDomainEntity } from '~/domain/gateway/entities/payment-domain.entity';
|
||||
|
||||
@Injectable()
|
||||
export class ParticipantDomainInteractor {
|
||||
@@ -24,7 +27,8 @@ export class ParticipantDomainInteractor {
|
||||
constructor(
|
||||
private readonly documentDomainService: DocumentDomainService,
|
||||
private readonly accountDomainService: AccountDomainService,
|
||||
@Inject(CANDIDATE_REPOSITORY) private readonly candidateRepository: CandidateRepository
|
||||
@Inject(CANDIDATE_REPOSITORY) private readonly candidateRepository: CandidateRepository,
|
||||
private readonly gatewayInteractor: GatewayInteractor
|
||||
) {}
|
||||
|
||||
async generateParticipantApplication(
|
||||
@@ -167,4 +171,11 @@ export class ParticipantDomainInteractor {
|
||||
|
||||
return this.accountDomainService.getAccount(data.username);
|
||||
}
|
||||
|
||||
/**
|
||||
* Создать регистрационный платеж
|
||||
*/
|
||||
async createInitialPayment(data: CreateInitialPaymentInputDomainInterface): Promise<PaymentDomainEntity> {
|
||||
return await this.gatewayInteractor.createInitialPayment(data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,10 @@ import { Module } from '@nestjs/common';
|
||||
import { DocumentDomainModule } from '../document/document.module';
|
||||
import { ParticipantDomainInteractor } from './interactors/participant-domain.interactor';
|
||||
import { AccountDomainModule } from '../account/account-domain.module';
|
||||
import { GatewayDomainModule } from '../gateway/gateway-domain.module';
|
||||
|
||||
@Module({
|
||||
imports: [AccountDomainModule, DocumentDomainModule],
|
||||
imports: [AccountDomainModule, DocumentDomainModule, GatewayDomainModule],
|
||||
providers: [ParticipantDomainInteractor],
|
||||
exports: [ParticipantDomainInteractor],
|
||||
})
|
||||
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
export interface GetPaymentMethodDomainInterface {
|
||||
username: string;
|
||||
method_type: 'bank_transfer' | 'spb';
|
||||
method_type?: 'bank_transfer' | 'spb';
|
||||
method_id?: string;
|
||||
is_default?: boolean;
|
||||
block_num?: number;
|
||||
}
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
import type { PaymentDetailsDTO } from '~/modules/payment/dto/payment-details.dto';
|
||||
import { PaymentStatus } from '../interfaces/payment-status-domain.interface';
|
||||
import { type IOrder } from '~/types';
|
||||
import type { PaymentDTO } from '~/modules/payment/dto/payment.dto';
|
||||
import type { PaymentTypeDomainInterface } from '../interfaces/payment-type-domain.interface';
|
||||
import type { AccountDomainEntity } from '~/domain/account/entities/account-domain.entity';
|
||||
import { AccountDTO } from '~/modules/account/dto/account.dto';
|
||||
|
||||
export class PaymentDomainEntity {
|
||||
id?: string;
|
||||
orderNum?: number;
|
||||
secret!: string;
|
||||
creator!: string;
|
||||
status: PaymentStatus = PaymentStatus.PENDING;
|
||||
type!: PaymentTypeDomainInterface;
|
||||
provider!: string;
|
||||
message?: string;
|
||||
username!: string;
|
||||
quantity!: string;
|
||||
symbol!: string;
|
||||
account!: AccountDomainEntity;
|
||||
details?: {
|
||||
data: any;
|
||||
amount_plus_fee: string;
|
||||
amount_without_fee: string;
|
||||
fee_amount: string;
|
||||
fee_percent: number;
|
||||
fact_fee_percent: number;
|
||||
tolerance_percent: number;
|
||||
};
|
||||
expiredAt!: Date;
|
||||
userId!: string;
|
||||
createdAt!: Date;
|
||||
updatedAt!: Date;
|
||||
|
||||
constructor(orderData: IOrder, account?: AccountDomainEntity);
|
||||
constructor(data: Partial<IOrder & { created_at?: Date; updated_at?: Date; account?: AccountDomainEntity }>);
|
||||
constructor(
|
||||
orderOrData: IOrder | Partial<IOrder & { created_at?: Date; updated_at?: Date; account?: AccountDomainEntity }>,
|
||||
accountParam?: AccountDomainEntity
|
||||
) {
|
||||
// Распознаем форму вызова конструктора
|
||||
const data = orderOrData as Partial<IOrder & { created_at?: Date; updated_at?: Date; account?: AccountDomainEntity }>;
|
||||
const account = accountParam || data.account;
|
||||
|
||||
Object.assign(this, {
|
||||
id: data.id,
|
||||
orderNum: data.order_num,
|
||||
secret: data.secret,
|
||||
creator: data.creator,
|
||||
status: data.status as unknown as PaymentStatus,
|
||||
type: data.type,
|
||||
provider: data.provider,
|
||||
message: data.message,
|
||||
username: data.username,
|
||||
quantity: data.quantity,
|
||||
symbol: data.symbol,
|
||||
details: data.details,
|
||||
account: account,
|
||||
expiredAt:
|
||||
data.expired_at ||
|
||||
(() => {
|
||||
const now = new Date();
|
||||
now.setDate(now.getDate() + 1);
|
||||
return now;
|
||||
})(),
|
||||
user: data.user,
|
||||
createdAt: data.created_at ?? new Date(),
|
||||
updatedAt: data.updated_at ?? new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
toDTO(): PaymentDTO {
|
||||
if (!this.id) {
|
||||
throw new Error('ID is required for PaymentDTO');
|
||||
}
|
||||
if (this.orderNum === undefined || this.orderNum === null) {
|
||||
throw new Error('Order number is required for PaymentDTO');
|
||||
}
|
||||
if (!this.account) {
|
||||
throw new Error('Account is required for PaymentDTO');
|
||||
}
|
||||
|
||||
const details: PaymentDetailsDTO | undefined = this.details
|
||||
? {
|
||||
data: this.details.data,
|
||||
amount_plus_fee: this.details.amount_plus_fee,
|
||||
amount_without_fee: this.details.amount_without_fee,
|
||||
fee_amount: this.details.fee_amount,
|
||||
fee_percent: this.details.fee_percent,
|
||||
fact_fee_percent: this.details.fact_fee_percent,
|
||||
tolerance_percent: this.details.tolerance_percent,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
id: this.id,
|
||||
blockchain_id: this.orderNum,
|
||||
provider: this.provider,
|
||||
type: this.type,
|
||||
details: details || ({} as PaymentDetailsDTO),
|
||||
status: this.status,
|
||||
message: this.message || '',
|
||||
amount: parseFloat(this.quantity),
|
||||
symbol: this.symbol,
|
||||
username: this.username,
|
||||
account: new AccountDTO(this.account),
|
||||
expired_at: this.expiredAt,
|
||||
created_at: this.createdAt,
|
||||
updated_at: this.updatedAt,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,160 +0,0 @@
|
||||
// domain/payment/interactors/PaymentInteractor.ts
|
||||
|
||||
import { Inject, Injectable, Logger, NotFoundException } from '@nestjs/common';
|
||||
import { Order } from '~/models';
|
||||
import { userService, blockchainService } from '~/services';
|
||||
import { userStatus } from '~/types/user.types';
|
||||
import { type IOrder } from '~/types/order.types';
|
||||
import { PaymentTypeDomainInterface } from '../interfaces/payment-type-domain.interface';
|
||||
import { PaymentStatus } from '../interfaces/payment-status-domain.interface';
|
||||
import type {
|
||||
PaginationInputDomainInterface,
|
||||
PaginationResultDomainInterface,
|
||||
} from '~/domain/common/interfaces/pagination.interface';
|
||||
import type { GetPaymentsInputDomainInterface } from '../interfaces/get-payments-input-domain.interface';
|
||||
import type { CreateInitialPaymentInputDomainInterface } from '../interfaces/create-initial-input-domain.interface';
|
||||
import type { CreateDepositPaymentInputDomainInterface } from '../interfaces/create-deposit-input-domain.interface';
|
||||
import type { SetPaymentStatusInputDomainInterface } from '../interfaces/set-payment-status-domain-input.interface';
|
||||
import { PaymentDomainService } from '../services/payment-domain.service';
|
||||
import type { PaginationResultLegacy } from '~/types/pagination.types';
|
||||
import { PaymentDomainEntity } from '../entity/payment.entity';
|
||||
import { AccountDomainInteractor } from '~/domain/account/interactors/account.interactor';
|
||||
|
||||
@Injectable()
|
||||
export class PaymentInteractor {
|
||||
constructor(
|
||||
private readonly paymentDomainService: PaymentDomainService,
|
||||
private readonly accountDomainInteractor: AccountDomainInteractor
|
||||
) {}
|
||||
|
||||
private readonly logger = new Logger(PaymentInteractor.name);
|
||||
|
||||
async getPayments(
|
||||
data: GetPaymentsInputDomainInterface,
|
||||
options: PaginationInputDomainInterface
|
||||
): Promise<PaginationResultDomainInterface<PaymentDomainEntity>> {
|
||||
const pre_result: PaginationResultLegacy<IOrder> = await this.paymentDomainService.getOrders(data, options);
|
||||
|
||||
const items = await Promise.all(
|
||||
pre_result.results.map(async (el) => {
|
||||
try {
|
||||
// Получаем аккаунт для каждого платежа
|
||||
const account = await this.accountDomainInteractor.getAccount(el.username);
|
||||
|
||||
// Создаем объект платежа, просто передавая IOrder и account
|
||||
return new PaymentDomainEntity(el, account);
|
||||
} catch (error: any) {
|
||||
this.logger.warn(
|
||||
`Не удалось получить данные аккаунта для платежа ${el.id}, username: ${el.username}: ${error.message}`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Удаляем null из массива
|
||||
const validItems = items.filter(Boolean) as PaymentDomainEntity[];
|
||||
|
||||
const result: PaginationResultDomainInterface<PaymentDomainEntity> = {
|
||||
items: validItems,
|
||||
totalCount: pre_result.totalResults,
|
||||
totalPages: pre_result.totalPages,
|
||||
currentPage: pre_result.page,
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async createInitialPayment(data: CreateInitialPaymentInputDomainInterface): Promise<PaymentDomainEntity> {
|
||||
try {
|
||||
// Сначала получаем аккаунт
|
||||
const account = await this.accountDomainInteractor.getAccount(data.username);
|
||||
|
||||
// Затем создаем платеж
|
||||
const result = await this.paymentDomainService.createInitialOrder(data);
|
||||
|
||||
// Создаем и возвращаем сущность платежа, просто передавая IOrder и account
|
||||
return new PaymentDomainEntity(result, account);
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Не удалось создать регистрационный платеж: ${error.message}`);
|
||||
throw new NotFoundException(`Не удалось найти аккаунт пользователя ${data.username} или создать платеж`);
|
||||
}
|
||||
}
|
||||
|
||||
async createDeposit(data: CreateDepositPaymentInputDomainInterface): Promise<PaymentDomainEntity> {
|
||||
try {
|
||||
// Сначала получаем аккаунт
|
||||
const account = await this.accountDomainInteractor.getAccount(data.username);
|
||||
|
||||
// Затем создаем платеж
|
||||
const result = await this.paymentDomainService.createDeposit(data);
|
||||
|
||||
// Создаем и возвращаем сущность платежа, просто передавая IOrder и account
|
||||
return new PaymentDomainEntity(result, account);
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Не удалось создать паевой платеж: ${error.message}`);
|
||||
throw new NotFoundException(`Не удалось найти аккаунт пользователя ${data.username} или создать платеж`);
|
||||
}
|
||||
}
|
||||
|
||||
async setPaymentStatus(data: SetPaymentStatusInputDomainInterface): Promise<PaymentDomainEntity> {
|
||||
try {
|
||||
// Получаем обновленный платеж
|
||||
const result = await this.paymentDomainService.setStatus(data);
|
||||
|
||||
// Получаем аккаунт
|
||||
const account = await this.accountDomainInteractor.getAccount(result.username);
|
||||
|
||||
// Создаем и возвращаем сущность платежа, просто передавая IOrder и account
|
||||
return new PaymentDomainEntity(result, account);
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Не удалось обновить статус платежа: ${error.message}`);
|
||||
throw new NotFoundException(`Не удалось найти платеж с ID ${data.id} или аккаунт пользователя`);
|
||||
}
|
||||
}
|
||||
|
||||
async execute(id: string, status: string) {
|
||||
const order = await Order.findById(id);
|
||||
if (!order) {
|
||||
this.logger.error(`Order with id ${id} not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (status) {
|
||||
case PaymentStatus.PAID:
|
||||
if (order.status !== 'completed') await this.processOrder(order);
|
||||
break;
|
||||
|
||||
case PaymentStatus.FAILED:
|
||||
this.logger.warn(`Payment for order ${id} failed`);
|
||||
break;
|
||||
|
||||
default:
|
||||
this.logger.log(`Status ${status} for order ${id} does not require processing`);
|
||||
}
|
||||
}
|
||||
|
||||
private async processOrder(order: IOrder) {
|
||||
this.logger.log(`Processing blockchain data for order ${order.id}`);
|
||||
|
||||
try {
|
||||
if (order.type === PaymentTypeDomainInterface.REGISTRATION) {
|
||||
// Используем новый интерактор для регистрации в блокчейне
|
||||
await this.accountDomainInteractor.registerBlockchainAccount(order.username);
|
||||
this.logger.log('New user registered:', { username: order.username });
|
||||
|
||||
// Обновляем статус заказа
|
||||
await Order.updateOne({ _id: order.id }, { status: PaymentStatus.COMPLETED });
|
||||
} else if (order.type === PaymentTypeDomainInterface.DEPOSIT) {
|
||||
await blockchainService.completeDeposit(order);
|
||||
|
||||
// Обновляем статус заказа
|
||||
await Order.updateOne({ _id: order.id }, { status: PaymentStatus.COMPLETED });
|
||||
this.logger.log(`User ${order.username} made a share contribution of ${order.quantity}`);
|
||||
}
|
||||
} catch (e: any) {
|
||||
await Order.updateOne({ _id: order.id }, { status: PaymentStatus.FAILED, message: e.message });
|
||||
this.logger.error(`Error processing blockchain transaction for order: ${order.id} with message: ${e.message}`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
export interface GetPaymentsInputDomainInterface {
|
||||
username?: string;
|
||||
id?: string;
|
||||
blockchain_id?: string;
|
||||
status?: string;
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
export enum PaymentStatus {
|
||||
PENDING = 'pending',
|
||||
PAID = 'paid',
|
||||
COMPLETED = 'completed',
|
||||
FAILED = 'failed',
|
||||
EXPIRED = 'expired',
|
||||
REFUNDED = 'refunded',
|
||||
}
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
import type { PaymentStatus } from './payment-status-domain.interface';
|
||||
|
||||
export interface SetPaymentStatusInputDomainInterface {
|
||||
id: string;
|
||||
status: PaymentStatus;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
// domain/payment/payment.module.ts
|
||||
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PaymentInteractor } from './interactors/payment.interactor';
|
||||
import { PaymentDomainService } from './services/payment-domain.service';
|
||||
import { AccountDomainModule } from '../account/account-domain.module';
|
||||
|
||||
@Module({
|
||||
imports: [AccountDomainModule],
|
||||
providers: [PaymentInteractor, PaymentDomainService],
|
||||
exports: [PaymentInteractor, PaymentDomainService],
|
||||
})
|
||||
export class PaymentDomainModule {}
|
||||
@@ -1,205 +0,0 @@
|
||||
import { Order } from '../../../models/index';
|
||||
import mongoose, { type ObjectId } from 'mongoose';
|
||||
import { getUserByUsername } from '../../../services/user.service';
|
||||
import logger from '../../../config/logger';
|
||||
import { generator } from '../../../services/document.service';
|
||||
import { orderStatus, type IOrder } from '../../../types/order.types';
|
||||
import ApiError from '../../../utils/ApiError';
|
||||
import httpStatus from 'http-status';
|
||||
import { redisPublisher } from '../../../services/redis.service';
|
||||
import crypto from 'crypto';
|
||||
import Settings from '../../../models/settings.model';
|
||||
import { blockchainService, userService } from '../../../services';
|
||||
import config from '../../../config/config';
|
||||
import { userStatus } from '../../../types/user.types';
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PaymentTypeDomainInterface } from '../interfaces/payment-type-domain.interface';
|
||||
import { ProviderDomainService } from '~/domain/provider/provider-domain.service';
|
||||
import type { PaginationResultLegacy } from '~/types/pagination.types';
|
||||
import type { CreateDepositPaymentInputDomainInterface } from '../interfaces/create-deposit-input-domain.interface';
|
||||
import type { CreateInitialPaymentInputDomainInterface } from '../interfaces/create-initial-input-domain.interface';
|
||||
import type { SetPaymentStatusInputDomainInterface } from '../interfaces/set-payment-status-domain-input.interface';
|
||||
import { sha256 } from '~/utils/sha256';
|
||||
|
||||
@Injectable()
|
||||
export class PaymentDomainService {
|
||||
constructor(private readonly providerDomainService: ProviderDomainService) {}
|
||||
|
||||
async createOrder(
|
||||
username: string,
|
||||
providerName: string,
|
||||
type: PaymentTypeDomainInterface,
|
||||
amount: string
|
||||
): Promise<IOrder> {
|
||||
const cooperative = await generator.constructCooperative(config.coopname);
|
||||
|
||||
if (!cooperative) throw new Error('Кооператив не найден');
|
||||
|
||||
const session = await mongoose.startSession();
|
||||
|
||||
const [, symbol] = amount.split(' ');
|
||||
|
||||
let result;
|
||||
try {
|
||||
await session.withTransaction(async () => {
|
||||
const user = await getUserByUsername(username);
|
||||
let db_order;
|
||||
|
||||
if (type === PaymentTypeDomainInterface.REGISTRATION && user.initial_order) {
|
||||
db_order = await Order.findOne({ _id: user.initial_order }); //add expiration
|
||||
}
|
||||
|
||||
if (!db_order) {
|
||||
db_order = new Order({
|
||||
creator: config.coopname,
|
||||
secret: this.generateOrderSecret(),
|
||||
status: orderStatus.pending,
|
||||
type: type,
|
||||
provider: providerName,
|
||||
username,
|
||||
quantity: amount,
|
||||
symbol: symbol,
|
||||
user: user._id as unknown as ObjectId,
|
||||
});
|
||||
user.initial_order = db_order._id;
|
||||
|
||||
await db_order.save({ session });
|
||||
await user.save({ session });
|
||||
}
|
||||
|
||||
const secret = db_order.secret;
|
||||
|
||||
const provider = this.providerDomainService.getProvider(providerName);
|
||||
|
||||
const paymentDetails = await provider.createPayment(
|
||||
amount,
|
||||
symbol,
|
||||
type === 'deposit'
|
||||
? `Паевой взнос по соглашению о ЦПП "Цифровой Кошелёк" №${db_order._id.toString().slice(0, 8)}`
|
||||
: `Вступительный и минимальный паевый взносы №${db_order._id.toString().slice(0, 8)}`,
|
||||
db_order.order_num as number,
|
||||
secret
|
||||
);
|
||||
|
||||
db_order.details = paymentDetails;
|
||||
|
||||
await db_order.save({ session });
|
||||
|
||||
logger.info('Order created', { providerName, type, username, amount, source: 'createDeposit' });
|
||||
|
||||
result = db_order;
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (e: any) {
|
||||
logger.error(`Error happen on createOrder process: ${e.message}`);
|
||||
throw e;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async createDeposit(data: CreateDepositPaymentInputDomainInterface): Promise<IOrder> {
|
||||
const cooperative = await generator.constructCooperative(config.coopname);
|
||||
if (!cooperative) throw new Error('Кооператив не найден');
|
||||
|
||||
const [, symbol_initial] = cooperative.initial.split(' ');
|
||||
|
||||
const [amount, symbol] = data.quantity.split(' ');
|
||||
const quantity = `${parseFloat(amount).toFixed(4)} ${symbol}`;
|
||||
if (symbol_initial != symbol) throw new ApiError(httpStatus.BAD_REQUEST, 'Неверный символ для платежа');
|
||||
|
||||
const provider = (await Settings.getSettings()).provider.name;
|
||||
|
||||
return this.createOrder(data.username, provider, PaymentTypeDomainInterface.DEPOSIT, quantity);
|
||||
}
|
||||
|
||||
async createInitialOrder(data: CreateInitialPaymentInputDomainInterface): Promise<IOrder> {
|
||||
const cooperative = await generator.constructCooperative(config.coopname);
|
||||
if (!cooperative) throw new Error('Кооператив не найден');
|
||||
|
||||
const user = await getUserByUsername(data.username);
|
||||
|
||||
let amount = '';
|
||||
|
||||
if (user.type === 'individual' || user.type === 'entrepreneur') {
|
||||
amount = cooperative.registration;
|
||||
} else {
|
||||
amount = cooperative.org_registration;
|
||||
}
|
||||
|
||||
const provider = (await Settings.getSettings()).provider.name;
|
||||
|
||||
return this.createOrder(data.username, provider, PaymentTypeDomainInterface.REGISTRATION, amount);
|
||||
}
|
||||
|
||||
checkPaymentSymbol(incomeSymbol: string, extectedSymbol: string) {
|
||||
if (incomeSymbol != extectedSymbol) return { status: 'error', message: `${incomeSymbol} != expectedSymbol` };
|
||||
else return { status: 'success', message: '' };
|
||||
}
|
||||
|
||||
checkPaymentAmount(incomeAmount: string, expectedAmount: string, tolerancePercentage: number) {
|
||||
//погрешность возникает при округлении суммы платежа на стороне провайдера при расчете им процентов комиссии
|
||||
const tolerance = parseFloat(expectedAmount) * (tolerancePercentage / 100); // Абсолютное значение погрешности
|
||||
|
||||
if (parseFloat(incomeAmount) < parseFloat(expectedAmount) - tolerance) {
|
||||
return {
|
||||
status: 'error',
|
||||
message: `Недостаточно средств, поступило: ${incomeAmount}, ожидается: ${expectedAmount}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
message: '',
|
||||
};
|
||||
}
|
||||
|
||||
generateOrderSecret(length = 48): string {
|
||||
return crypto.randomBytes(length).toString('hex'); // Генерирует случайный секрет в виде hex-строки
|
||||
}
|
||||
|
||||
getAmountPlusFee(amount: number, fee: number): number {
|
||||
if (fee < 0 || fee >= 100) {
|
||||
throw new Error('Fee must be between 0 and 100.');
|
||||
}
|
||||
return amount / ((100 - fee) / 100);
|
||||
}
|
||||
|
||||
async setStatus(data: SetPaymentStatusInputDomainInterface): Promise<IOrder> {
|
||||
const order = await Order.findById(data.id);
|
||||
|
||||
if (!order) {
|
||||
throw new ApiError(httpStatus.BAD_REQUEST, 'Ордер не найден');
|
||||
}
|
||||
|
||||
if (data.status == 'refunded') {
|
||||
await blockchainService.cancelOrder({
|
||||
coopname: config.coopname,
|
||||
income_hash: sha256(order.order_num as number),
|
||||
reason: '',
|
||||
});
|
||||
|
||||
if (order.type === 'registration') {
|
||||
const user = await userService.getUserByUsername(order.username);
|
||||
user.status = userStatus['100_Refunded'];
|
||||
user.save();
|
||||
}
|
||||
}
|
||||
|
||||
//обновляем статус
|
||||
order.status = orderStatus[data.status];
|
||||
await order.save();
|
||||
|
||||
// Отправляем обновление через Redis, чтобы обработчик мог выполнить действия
|
||||
redisPublisher.publish(`${config.coopname}:orderStatusUpdate`, JSON.stringify({ id: data.id, status: data.status }));
|
||||
|
||||
logger.info(`Статус ордера ${data.id} обновлен до ${data.status}`, { source: 'setStatus' });
|
||||
|
||||
return order;
|
||||
}
|
||||
|
||||
async getOrders(filter: any = {}, options: any = {}): Promise<PaginationResultLegacy<IOrder>> {
|
||||
const orders = await Order.paginate(filter, options);
|
||||
return orders;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import { Injectable, Inject, Logger } from '@nestjs/common';
|
||||
import { Cooperative } from 'cooptypes';
|
||||
import { DocumentDomainService } from '~/domain/document/services/document-domain.service';
|
||||
import { DocumentDomainEntity } from '~/domain/document/entity/document-domain.entity';
|
||||
import { WalletBlockchainPort, WALLET_BLOCKCHAIN_PORT } from '../ports/wallet-blockchain.port';
|
||||
import type { CreateWithdrawInputDomainInterface } from '../interfaces/create-withdraw-input-domain.interface';
|
||||
import { GatewayInteractor } from '~/domain/gateway/interactors/gateway.interactor';
|
||||
import type { CreateDepositPaymentInputDomainInterface } from '~/domain/gateway/interfaces/create-deposit-payment-input-domain.interface';
|
||||
import { PaymentDomainEntity } from '~/domain/gateway/entities/payment-domain.entity';
|
||||
import { PaymentStatusEnum } from '~/domain/gateway/enums/payment-status.enum';
|
||||
|
||||
/**
|
||||
* Интерактор домена wallet для управления паевыми взносами, их возвратами, и генерацией документов
|
||||
*/
|
||||
@Injectable()
|
||||
export class WalletDomainInteractor {
|
||||
private readonly logger = new Logger(WalletDomainInteractor.name);
|
||||
|
||||
constructor(
|
||||
private readonly documentDomainService: DocumentDomainService,
|
||||
@Inject(WALLET_BLOCKCHAIN_PORT)
|
||||
private readonly walletBlockchainPort: WalletBlockchainPort,
|
||||
private readonly gatewayInteractor: GatewayInteractor
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Создать депозитный платеж
|
||||
*/
|
||||
async createDepositPayment(data: CreateDepositPaymentInputDomainInterface): Promise<PaymentDomainEntity> {
|
||||
return await this.gatewayInteractor.createDeposit(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Генерация документа заявления на возврат паевого взноса (900)
|
||||
*/
|
||||
async generateReturnByMoneyStatementDocument(
|
||||
data: Cooperative.Registry.ReturnByMoney.Action,
|
||||
options: Cooperative.Document.IGenerationOptions
|
||||
): Promise<DocumentDomainEntity> {
|
||||
// Устанавливаем registry_id для документа заявления
|
||||
data.registry_id = Cooperative.Registry.ReturnByMoney.registry_id;
|
||||
return await this.documentDomainService.generateDocument({ data, options });
|
||||
}
|
||||
|
||||
/**
|
||||
* Генерация документа решения совета о возврате паевого взноса (901)
|
||||
*/
|
||||
async generateReturnByMoneyDecisionDocument(
|
||||
data: Cooperative.Registry.ReturnByMoneyDecision.Action,
|
||||
options: Cooperative.Document.IGenerationOptions
|
||||
): Promise<DocumentDomainEntity> {
|
||||
// Устанавливаем registry_id для документа решения
|
||||
data.registry_id = Cooperative.Registry.ReturnByMoneyDecision.registry_id;
|
||||
return await this.documentDomainService.generateDocument({ data, options });
|
||||
}
|
||||
|
||||
/**
|
||||
* Создание заявки на вывод средств (createWithdraw)
|
||||
* Данный метод создает платеж в gateway и withdraw в blockchain
|
||||
*/
|
||||
async createWithdraw(data: CreateWithdrawInputDomainInterface): Promise<{ withdraw_hash: string }> {
|
||||
// Используем payment_hash из параметров вместо генерации нового
|
||||
const withdraw_hash = data.payment_hash;
|
||||
|
||||
let createdPayment: PaymentDomainEntity | null = null;
|
||||
|
||||
try {
|
||||
// 1. Создаем исходящий платеж в gateway для отслеживания
|
||||
createdPayment = await this.gatewayInteractor.createWithdraw({
|
||||
coopname: data.coopname,
|
||||
username: data.username,
|
||||
quantity: data.quantity,
|
||||
symbol: data.symbol,
|
||||
method_id: data.method_id,
|
||||
statement: data.statement,
|
||||
payment_hash: data.payment_hash,
|
||||
});
|
||||
|
||||
// 2. Создаем withdraw в wallet контракте
|
||||
// wallet контракт автоматически создаст outcome в gateway контракте
|
||||
await this.walletBlockchainPort.createWithdraw({
|
||||
coopname: data.coopname,
|
||||
username: data.username,
|
||||
withdraw_hash,
|
||||
quantity: `${data.quantity} ${data.symbol}`,
|
||||
statement: data.statement,
|
||||
});
|
||||
|
||||
this.logger.log(`Создан withdraw в wallet: ${withdraw_hash}, outcome будет создан автоматически в gateway`);
|
||||
|
||||
return { withdraw_hash };
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Ошибка при создании withdraw: ${error.message}`, error);
|
||||
|
||||
// Если платеж был создан, но произошла ошибка при создании withdraw в блокчейне
|
||||
if (createdPayment?.id) {
|
||||
try {
|
||||
// Обновляем статус платежа на FAILED с сообщением об ошибке
|
||||
await this.gatewayInteractor.setPaymentStatus({
|
||||
id: createdPayment.id,
|
||||
status: PaymentStatusEnum.FAILED,
|
||||
});
|
||||
|
||||
this.logger.log(`Платеж ${createdPayment.id} помечен как FAILED из-за ошибки создания withdraw`);
|
||||
} catch (updateError: any) {
|
||||
this.logger.error(`Ошибка при обновлении статуса платежа: ${updateError.message}`, updateError);
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import type { ISignedDocumentDomainInterface } from '~/domain/document/interfaces/signed-document-domain.interface';
|
||||
|
||||
/**
|
||||
* Доменный интерфейс для создания вывода средств (withdraw)
|
||||
*/
|
||||
export interface CreateWithdrawInputDomainInterface {
|
||||
coopname: string;
|
||||
username: string;
|
||||
quantity: number;
|
||||
symbol: string;
|
||||
method_id: string;
|
||||
statement: ISignedDocumentDomainInterface;
|
||||
payment_hash: string; // Хеш платежа, генерируется на фронтенде
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import type { TransactionResult } from '~/domain/blockchain/types/transaction-result.type';
|
||||
import type { WalletContract } from 'cooptypes';
|
||||
import type { ISignedDocumentDomainInterface } from '~/domain/document/interfaces/signed-document-domain.interface';
|
||||
|
||||
/**
|
||||
* Доменные интерфейсы для входных данных wallet блокчейн адаптера
|
||||
*/
|
||||
export interface CreateWithdrawDomainInterface {
|
||||
coopname: string;
|
||||
username: string;
|
||||
withdraw_hash: string;
|
||||
quantity: string;
|
||||
statement: ISignedDocumentDomainInterface;
|
||||
}
|
||||
|
||||
export interface GenerateReturnStatementDomainInterface {
|
||||
coopname: string;
|
||||
username: string;
|
||||
quantity: string;
|
||||
symbol: string;
|
||||
method_id: string;
|
||||
memo?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Порт для взаимодействия с wallet блокчейном
|
||||
*/
|
||||
export interface WalletBlockchainPort {
|
||||
// Создание заявки на вывод средств в wallet контракте
|
||||
createWithdraw(data: CreateWithdrawDomainInterface): Promise<TransactionResult>;
|
||||
|
||||
// Генерация заявления на возврат паевого взноса
|
||||
generateReturnStatement(data: GenerateReturnStatementDomainInterface): Promise<ISignedDocumentDomainInterface>;
|
||||
|
||||
// Получение данных из wallet контракта
|
||||
getWithdraw(coopname: string, withdraw_hash: string): Promise<WalletContract.Tables.Withdraws.IWithdraws | null>;
|
||||
|
||||
// Получение всех withdrawals для кооператива
|
||||
getWithdraws(coopname: string): Promise<WalletContract.Tables.Withdraws.IWithdraws[]>;
|
||||
}
|
||||
|
||||
export const WALLET_BLOCKCHAIN_PORT = Symbol('WALLET_BLOCKCHAIN_PORT');
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { WalletDomainInteractor } from './interactors/wallet.interactor';
|
||||
import { DocumentDomainModule } from '~/domain/document/document.module';
|
||||
import { InfrastructureModule } from '~/infrastructure/infrastructure.module';
|
||||
import { GatewayDomainModule } from '../gateway/gateway-domain.module';
|
||||
|
||||
/**
|
||||
* Доменный модуль Wallet
|
||||
*/
|
||||
@Module({
|
||||
imports: [DocumentDomainModule, InfrastructureModule, GatewayDomainModule],
|
||||
providers: [WalletDomainInteractor],
|
||||
exports: [WalletDomainInteractor],
|
||||
})
|
||||
export class WalletDomainModule {}
|
||||
@@ -50,7 +50,11 @@ export class MeetTrackerService {
|
||||
// Основная функция проверки собраний
|
||||
async checkMeets(): Promise<void> {
|
||||
try {
|
||||
this.logger.info('Проверка собраний');
|
||||
// Подтягиваем актуальный конфиг из базы
|
||||
const repo = await this.extensionRepository.findByName(this.extensionName);
|
||||
if (repo) {
|
||||
this.pluginConfig = repo;
|
||||
}
|
||||
|
||||
// Получаем все собрания из блокчейна через порт
|
||||
const meets = await this.meetPort.getMeets({ coopname: config.coopname }, undefined);
|
||||
@@ -141,6 +145,8 @@ export class MeetTrackerService {
|
||||
if (closedMeetIds.includes(meetID)) {
|
||||
continue;
|
||||
}
|
||||
// Удаляем все старые собрания с тем же id перед добавлением нового
|
||||
this.pluginConfig.config.trackedMeets = this.pluginConfig.config.trackedMeets.filter((tm) => tm.id !== meetID);
|
||||
// Создаем новое отслеживаемое собрание
|
||||
const newTrackedMeet: TrackedMeet = {
|
||||
id: meetID,
|
||||
@@ -209,6 +215,15 @@ export class MeetTrackerService {
|
||||
if (statusChanged) {
|
||||
this.logger.info(`Изменение статуса собрания ${meetHash} (№${meetID}): ${oldStatus} -> ${extendedStatus}`);
|
||||
|
||||
if (
|
||||
oldStatus === ExtendedMeetStatus.ONRESTART &&
|
||||
extendedStatus === ExtendedMeetStatus.WAITING_FOR_OPENING &&
|
||||
!trackedMeet.notifications.restartNotification
|
||||
) {
|
||||
await this.notificationSender.sendRestartNotification(trackedMeet);
|
||||
trackedMeet.notifications.restartNotification = true;
|
||||
}
|
||||
|
||||
// При переходе с created на waitingForOpening отправляем начальное уведомление
|
||||
if (
|
||||
oldStatus === ExtendedMeetStatus.CREATED &&
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { PaymentDetails } from '../../types';
|
||||
import { generator } from '../../services/document.service';
|
||||
import { getAmountPlusFee } from '../../services/order.service';
|
||||
import Joi from 'joi';
|
||||
import { getAmountPlusFee } from '~/shared/utils/payments';
|
||||
|
||||
import { PaymentProvider } from '../../services/payment/paymentProvider';
|
||||
import { nestApp } from '~/index';
|
||||
import { ProviderInteractor } from '~/domain/provider/provider.interactor';
|
||||
@@ -16,6 +16,9 @@ import type { ExtensionDomainEntity } from '~/domain/extension/entities/extensio
|
||||
import { z } from 'zod';
|
||||
import config from '~/config/config';
|
||||
import type { Cooperative } from 'cooptypes';
|
||||
import { PAYMENT_REPOSITORY, PaymentRepository } from '~/domain/gateway/repositories/payment.repository';
|
||||
import { TypeOrmPaymentRepository } from '~/infrastructure/database/typeorm/repositories/typeorm-payment.repository';
|
||||
import { QuantityUtils } from '~/shared/utils/quantity.utils';
|
||||
|
||||
// Дефолтные параметры конфигурации
|
||||
export const defaultConfig = {};
|
||||
@@ -32,6 +35,7 @@ export interface ILog {}
|
||||
export class QrPayPlugin extends PaymentProvider {
|
||||
constructor(
|
||||
@Inject(EXTENSION_REPOSITORY) private readonly extensionRepository: ExtensionDomainRepository,
|
||||
@Inject(PAYMENT_REPOSITORY) private readonly paymentRepository: PaymentRepository,
|
||||
private readonly logger: WinstonLoggerService
|
||||
) {
|
||||
super();
|
||||
@@ -60,18 +64,22 @@ export class QrPayPlugin extends PaymentProvider {
|
||||
this.logger.log(`Платежный провайдер ${this.name} успешно зарегистрирован.`);
|
||||
}
|
||||
|
||||
public async createPayment(
|
||||
amount: string,
|
||||
symbol: string,
|
||||
description: string,
|
||||
order_num: number,
|
||||
secret: string
|
||||
): Promise<PaymentDetails> {
|
||||
public async createPayment(hash: string): Promise<PaymentDetails> {
|
||||
// Получаем данные платежа по hash
|
||||
const payment = await this.paymentRepository.findByHash(hash);
|
||||
|
||||
if (!payment) {
|
||||
throw new Error(`Платеж с hash ${hash} не найден`);
|
||||
}
|
||||
|
||||
const amount = payment.quantity;
|
||||
const symbol = payment.symbol;
|
||||
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
const cooperative = await generator.constructCooperative(config.coopname);
|
||||
const amount_plus_fee = getAmountPlusFee(parseFloat(amount), this.fee_percent).toFixed(2);
|
||||
const fee_amount = (parseFloat(amount_plus_fee) - parseFloat(amount)).toFixed(2);
|
||||
const fact_fee_percent = Math.round((parseFloat(fee_amount) / parseFloat(amount)) * 100 * 100) / 100;
|
||||
const amount_plus_fee = getAmountPlusFee(amount, this.fee_percent).toFixed(2);
|
||||
const fee_amount = (parseFloat(amount_plus_fee) - amount).toFixed(2);
|
||||
const fact_fee_percent = Math.round((parseFloat(fee_amount) / amount) * 100 * 100) / 100;
|
||||
|
||||
const paymentMethod = (await generator.get('paymentMethod', {
|
||||
username: config.coopname,
|
||||
@@ -81,16 +89,18 @@ export class QrPayPlugin extends PaymentProvider {
|
||||
|
||||
const bankAccount = paymentMethod.data as Cooperative.Payments.IBankAccount;
|
||||
|
||||
const description = payment.memo || `Платеж для ${payment.username}`;
|
||||
|
||||
const invoice = `ST00012|Name=${cooperative?.full_name}|PersonalAcc=${bankAccount.account_number}|BankName=${
|
||||
bankAccount.bank_name
|
||||
}|BIC=${bankAccount.details.bik}|CorrespAcc=${bankAccount.details.corr}|Sum=${parseInt(
|
||||
amount
|
||||
amount_plus_fee
|
||||
)}00|Purpose=${description}. Без НДС.|PayeeINN=${cooperative?.details.inn}|KPP=${cooperative?.details.kpp}`;
|
||||
|
||||
const result: PaymentDetails = {
|
||||
data: invoice,
|
||||
amount_plus_fee: `${amount_plus_fee} ${symbol}`,
|
||||
amount_without_fee: amount,
|
||||
amount_without_fee: `${amount.toFixed(2)} ${symbol}`,
|
||||
fee_amount: `${fee_amount} ${symbol}`,
|
||||
fee_percent: this.fee_percent,
|
||||
fact_fee_percent,
|
||||
@@ -109,6 +119,10 @@ export class QrPayPlugin extends PaymentProvider {
|
||||
provide: EXTENSION_REPOSITORY, // токен для инъекции
|
||||
useClass: TypeOrmExtensionDomainRepository, // Реализация для интерфейса
|
||||
},
|
||||
{
|
||||
provide: PAYMENT_REPOSITORY,
|
||||
useClass: TypeOrmPaymentRepository,
|
||||
},
|
||||
], // Регистрируем PowerupPlugin как провайдер
|
||||
exports: [QrPayPlugin], // Экспортируем его для доступа в других модулях
|
||||
})
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
import { PollingProvider } from '../../services/payment/polling/pollingProvider';
|
||||
import { Order } from '../../models';
|
||||
import type { PaymentDetails } from '../../types';
|
||||
import { generator } from '../../services/document.service';
|
||||
import { redisPublisher } from '../../services/redis.service';
|
||||
import { PaymentState } from '../../models/paymentState.model';
|
||||
import axios from 'axios';
|
||||
import { checkPaymentAmount, checkPaymentSymbol, getAmountPlusFee } from '../../services/order.service';
|
||||
import { orderStatus } from '../../types/order.types';
|
||||
import { checkPaymentAmount, checkPaymentSymbol, getAmountPlusFee } from '~/shared/utils/payments';
|
||||
import config from '../../config/config';
|
||||
import { Inject, Module } from '@nestjs/common';
|
||||
import {
|
||||
EXTENSION_REPOSITORY,
|
||||
type ExtensionDomainRepository,
|
||||
} from '~/domain/extension/repositories/extension-domain.repository';
|
||||
import { TypeOrmExtensionDomainRepository } from '~/infrastructure/database/typeorm/repositories/typeorm-extension.repository';
|
||||
import { WinstonLoggerService } from '~/modules/logger/logger-app.service';
|
||||
import type { ExtensionDomainEntity } from '~/domain/extension/entities/extension-domain.entity';
|
||||
import { z } from 'zod';
|
||||
import type { Cooperative } from 'cooptypes';
|
||||
import { PaymentStatusEnum } from '~/domain/gateway/enums/payment-status.enum';
|
||||
import { PAYMENT_REPOSITORY, PaymentRepository } from '~/domain/gateway/repositories/payment.repository';
|
||||
import { TypeOrmPaymentRepository } from '~/infrastructure/database/typeorm/repositories/typeorm-payment.repository';
|
||||
import { PaymentDirectionEnum } from '~/domain/gateway/enums/payment-type.enum';
|
||||
|
||||
// Дефолтные параметры конфигурации
|
||||
export const defaultConfig = {};
|
||||
@@ -75,6 +78,7 @@ export interface ILog {}
|
||||
export class SberpollPlugin extends PollingProvider {
|
||||
constructor(
|
||||
@Inject(EXTENSION_REPOSITORY) private readonly extensionRepository: ExtensionDomainRepository,
|
||||
@Inject(PAYMENT_REPOSITORY) private readonly paymentRepository: PaymentRepository,
|
||||
private readonly logger: WinstonLoggerService
|
||||
) {
|
||||
super();
|
||||
@@ -100,18 +104,22 @@ export class SberpollPlugin extends PollingProvider {
|
||||
this.logger.info(`Инициализация ${this.name} с конфигурацией`, this.plugin);
|
||||
}
|
||||
|
||||
public async createPayment(
|
||||
amount: string,
|
||||
symbol: string,
|
||||
description: string,
|
||||
order_num: number,
|
||||
secret: string
|
||||
): Promise<PaymentDetails> {
|
||||
public async createPayment(hash: string): Promise<PaymentDetails> {
|
||||
// Получаем данные платежа по hash
|
||||
const payment = await this.paymentRepository.findByHash(hash);
|
||||
|
||||
if (!payment) {
|
||||
throw new Error(`Платеж с hash ${hash} не найден`);
|
||||
}
|
||||
|
||||
const amount = payment.quantity;
|
||||
const symbol = payment.symbol;
|
||||
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
const cooperative = await generator.constructCooperative(config.coopname);
|
||||
const amount_plus_fee = getAmountPlusFee(parseFloat(amount), this.fee_percent).toFixed(2);
|
||||
const fee_amount = (parseFloat(amount_plus_fee) - parseFloat(amount)).toFixed(2);
|
||||
const fact_fee_percent = Math.round((parseFloat(fee_amount) / parseFloat(amount)) * 100 * 100) / 100;
|
||||
const amount_plus_fee = getAmountPlusFee(amount, this.fee_percent).toFixed(2);
|
||||
const fee_amount = (parseFloat(amount_plus_fee) - amount).toFixed(2);
|
||||
const fact_fee_percent = Math.round((parseFloat(fee_amount) / amount) * 100 * 100) / 100;
|
||||
|
||||
const paymentMethod = (await generator.get('paymentMethod', {
|
||||
username: config.coopname,
|
||||
@@ -121,16 +129,18 @@ export class SberpollPlugin extends PollingProvider {
|
||||
|
||||
const bankAccount = paymentMethod.data as Cooperative.Payments.IBankAccount;
|
||||
|
||||
const description = payment.memo || `Платеж для ${payment.username}`;
|
||||
|
||||
const invoice = `ST00012|Name=${cooperative?.full_name}|PersonalAcc=${bankAccount.account_number}|BankName=${
|
||||
bankAccount.bank_name
|
||||
}|BIC=${bankAccount.details.bik}|CorrespAcc=${bankAccount.details.corr}|Sum=${parseInt(
|
||||
amount
|
||||
amount_plus_fee
|
||||
)}00|Purpose=${description}. Без НДС.|PayeeINN=${cooperative?.details.inn}|KPP=${cooperative?.details.kpp}`;
|
||||
|
||||
const result: PaymentDetails = {
|
||||
data: invoice,
|
||||
amount_plus_fee: `${amount_plus_fee} ${symbol}`,
|
||||
amount_without_fee: amount,
|
||||
amount_without_fee: `${amount} ${symbol}`,
|
||||
fee_amount: `${fee_amount} ${symbol}`,
|
||||
fee_percent: this.fee_percent,
|
||||
fact_fee_percent,
|
||||
@@ -213,61 +223,88 @@ export class SberpollPlugin extends PollingProvider {
|
||||
const transactions = data.transactions || [];
|
||||
|
||||
for (const transaction of transactions) {
|
||||
// Извлечение номера заказа
|
||||
// Извлечение номера заказа из назначения платежа
|
||||
const orderNumber = this.extractOrderNumber(transaction.paymentPurpose);
|
||||
|
||||
if (!orderNumber) {
|
||||
this.logger.warn(
|
||||
`Не найден номер заказа в строке назначения платежа: ${transaction.paymentPurpose} транзакции ${transaction.id}`
|
||||
);
|
||||
break; //прерываем цикл - в платеже не указан номер заказа
|
||||
continue; // Переходим к следующей транзакции
|
||||
}
|
||||
|
||||
this.logger.info(`Обработка транзакции ${transaction.id} с номером заказа ${orderNumber}`);
|
||||
|
||||
//TODO здесь нужно искать по ЧАСТИ _id а не по всему. Т.к. полного соответствия не будет.
|
||||
const order = await Order.findOne({ _id: orderNumber });
|
||||
// Ищем платеж по memo, который содержит номер заказа
|
||||
// или по другому подходящему полю
|
||||
const payments = await this.paymentRepository.getAllPayments(
|
||||
{
|
||||
direction: PaymentDirectionEnum.INCOMING,
|
||||
status: PaymentStatusEnum.PENDING,
|
||||
},
|
||||
{ limit: 100, page: 1, sortOrder: 'ASC' }
|
||||
);
|
||||
|
||||
if (!order) {
|
||||
this.logger.warn(`Не найден заказ с номером ${orderNumber} по транзакции ${transaction.id}`);
|
||||
break;
|
||||
// Ищем платеж, у которого в memo или id содержится номер заказа
|
||||
const payment = payments.items.find(
|
||||
(p) => p.memo?.includes(orderNumber) || p.id?.includes(orderNumber) || p.hash?.includes(orderNumber)
|
||||
);
|
||||
|
||||
if (!payment) {
|
||||
this.logger.warn(`Не найден платеж с номером заказа ${orderNumber} по транзакции ${transaction.id}`);
|
||||
continue; // Переходим к следующей транзакции
|
||||
}
|
||||
|
||||
const [, symbol] = order.quantity.split(' ');
|
||||
const symbol = payment.symbol;
|
||||
const symbol_check = checkPaymentSymbol(transaction.amountRub.currencyName, symbol);
|
||||
|
||||
if (symbol_check.status === 'error') {
|
||||
this.logger.warn(symbol_check.message);
|
||||
order.status = orderStatus.failed;
|
||||
order.message = symbol_check.message;
|
||||
await order.save();
|
||||
redisPublisher.publish(
|
||||
`${config.coopname}:orderStatusUpdate`,
|
||||
JSON.stringify({ id: order.id, status: 'failed' })
|
||||
);
|
||||
break;
|
||||
if (payment.id) {
|
||||
await this.paymentRepository.update(payment.id, {
|
||||
status: PaymentStatusEnum.FAILED,
|
||||
message: symbol_check.message,
|
||||
});
|
||||
redisPublisher.publish(
|
||||
`${config.coopname}:orderStatusUpdate`,
|
||||
JSON.stringify({ id: payment.id, status: PaymentStatusEnum.FAILED })
|
||||
);
|
||||
}
|
||||
continue; // Переходим к следующей транзакции
|
||||
}
|
||||
|
||||
//ордер найден, проверяем сумму платежа
|
||||
const amount_check = checkPaymentAmount(transaction.amountRub.amount, order.quantity, this.tolerance_percent);
|
||||
// Платеж найден, проверяем сумму платежа
|
||||
const amount_check = checkPaymentAmount(
|
||||
parseFloat(transaction.amountRub.amount),
|
||||
payment.quantity,
|
||||
this.tolerance_percent
|
||||
);
|
||||
|
||||
if (amount_check.status === 'error') {
|
||||
this.logger.warn(amount_check.message);
|
||||
order.status = orderStatus.failed;
|
||||
order.message = amount_check.message;
|
||||
await order.save();
|
||||
redisPublisher.publish(
|
||||
`${config.coopname}:orderStatusUpdate`,
|
||||
JSON.stringify({ id: order.id, status: 'failed' })
|
||||
);
|
||||
break;
|
||||
if (payment.id) {
|
||||
await this.paymentRepository.update(payment.id, {
|
||||
status: PaymentStatusEnum.FAILED,
|
||||
message: amount_check.message,
|
||||
});
|
||||
redisPublisher.publish(
|
||||
`${config.coopname}:orderStatusUpdate`,
|
||||
JSON.stringify({ id: payment.id, status: PaymentStatusEnum.FAILED })
|
||||
);
|
||||
}
|
||||
continue; // Переходим к следующей транзакции
|
||||
}
|
||||
|
||||
//отмечаем ордер оплаченным если все проверки пройдены
|
||||
order.status = orderStatus.paid;
|
||||
await order.save();
|
||||
// Отмечаем платеж оплаченным если все проверки пройдены
|
||||
if (payment.id) {
|
||||
await this.paymentRepository.update(payment.id, { status: PaymentStatusEnum.PAID });
|
||||
redisPublisher.publish(
|
||||
`${config.coopname}:orderStatusUpdate`,
|
||||
JSON.stringify({ id: payment.id, status: PaymentStatusEnum.PAID })
|
||||
);
|
||||
}
|
||||
|
||||
redisPublisher.publish(`${config.coopname}:orderStatusUpdate`, JSON.stringify({ id: order.id, status: 'paid' }));
|
||||
this.logger.info(`Платеж ${payment.id} успешно обработан и отмечен как оплаченный`);
|
||||
}
|
||||
|
||||
// Обновление состояния после обработки текущей страницы
|
||||
@@ -294,7 +331,17 @@ export class SberpollPlugin extends PollingProvider {
|
||||
}
|
||||
|
||||
@Module({
|
||||
providers: [SberpollPlugin], // Регистрируем SberpollPlugin как провайдер
|
||||
providers: [
|
||||
SberpollPlugin,
|
||||
{
|
||||
provide: EXTENSION_REPOSITORY,
|
||||
useClass: TypeOrmExtensionDomainRepository,
|
||||
},
|
||||
{
|
||||
provide: PAYMENT_REPOSITORY,
|
||||
useClass: TypeOrmPaymentRepository,
|
||||
},
|
||||
], // Регистрируем SberpollPlugin как провайдер
|
||||
exports: [SberpollPlugin], // Экспортируем его для доступа в других модулях
|
||||
})
|
||||
export class SberpollPluginModule {
|
||||
|
||||
@@ -1,54 +1,53 @@
|
||||
import { Module, Inject } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { YooCheckout } from '@a2seven/yoo-checkout';
|
||||
import { Order } from '../../models';
|
||||
import { IPNProvider } from '../../services/payment/ipn/ipnProvider';
|
||||
|
||||
import type { PaymentDetails } from '../../types';
|
||||
import IPN from '../../models/ipn.model';
|
||||
import { checkPaymentAmount, checkPaymentSymbol, getAmountPlusFee } from '../../services/order.service';
|
||||
import Settings from '../../models/settings.model';
|
||||
import config from '../../config/config';
|
||||
import { redisPublisher } from '../../services/redis.service';
|
||||
import Joi from 'joi';
|
||||
import { z } from 'zod';
|
||||
import { nestApp } from '~/index';
|
||||
import config from '~/config/config';
|
||||
import { ExtensionDomainEntity } from '~/domain/extension/entities/extension-domain.entity';
|
||||
import { ProviderInteractor } from '~/domain/provider/provider.interactor';
|
||||
import { Inject, Module } from '@nestjs/common';
|
||||
import { IPNProvider } from '~/services/payment/ipn/ipnProvider';
|
||||
import { WinstonLoggerService } from '~/modules/logger/logger-app.service';
|
||||
import { TypeOrmPaymentRepository } from '~/infrastructure/database/typeorm/repositories/typeorm-payment.repository';
|
||||
import { PaymentEntity } from '~/infrastructure/database/typeorm/entities/payment.entity';
|
||||
import { PaymentStatusEnum } from '~/domain/gateway/enums/payment-status.enum';
|
||||
import { PaymentDirectionEnum } from '~/domain/gateway/enums/payment-type.enum';
|
||||
import IPN from '~/models/ipn.model';
|
||||
import Settings from '~/models/settings.model';
|
||||
import type { PaymentDetails } from '~/types/order.types';
|
||||
import { PAYMENT_REPOSITORY } from '~/domain/gateway/repositories/payment.repository';
|
||||
import { redisPublisher } from '~/services/redis.service';
|
||||
import {
|
||||
EXTENSION_REPOSITORY,
|
||||
type ExtensionDomainRepository,
|
||||
} from '~/domain/extension/repositories/extension-domain.repository';
|
||||
import { WinstonLoggerService } from '~/modules/logger/logger-app.service';
|
||||
import type { ExtensionDomainEntity } from '~/domain/extension/entities/extension-domain.entity';
|
||||
import { z } from 'zod';
|
||||
import { TypeOrmExtensionDomainRepository } from '~/infrastructure/database/typeorm/repositories/typeorm-extension.repository';
|
||||
import { checkPaymentAmount, checkPaymentSymbol, getAmountPlusFee } from '~/shared/utils/payments';
|
||||
|
||||
// Дефолтные параметры конфигурации
|
||||
export const defaultConfig = {};
|
||||
export const Schema = z.object({
|
||||
client: z.string(),
|
||||
secret: z.string(),
|
||||
});
|
||||
|
||||
export const defaultConfig = {
|
||||
client: '',
|
||||
secret: '',
|
||||
};
|
||||
|
||||
interface IIpnRequest {
|
||||
event: string;
|
||||
object: {
|
||||
/**
|
||||
* Unknown Property
|
||||
*/
|
||||
[x: string]: unknown;
|
||||
amount: {
|
||||
/**
|
||||
* Unknown Property
|
||||
*/
|
||||
[x: string]: unknown;
|
||||
currency: string;
|
||||
value: string;
|
||||
};
|
||||
authorization_details?: {
|
||||
/**
|
||||
* Unknown Property
|
||||
*/
|
||||
[x: string]: unknown;
|
||||
auth_code?: string;
|
||||
rrn?: string;
|
||||
three_d_secure?: {
|
||||
/**
|
||||
* Unknown Property
|
||||
*/
|
||||
[x: string]: unknown;
|
||||
applied?: boolean;
|
||||
};
|
||||
@@ -58,30 +57,18 @@ interface IIpnRequest {
|
||||
expires_at?: string;
|
||||
id: string;
|
||||
income_amount: {
|
||||
/**
|
||||
* Unknown Property
|
||||
*/
|
||||
[x: string]: unknown;
|
||||
currency: string;
|
||||
value: string;
|
||||
};
|
||||
metadata: {
|
||||
secret: string;
|
||||
/**
|
||||
* Unknown Property
|
||||
*/
|
||||
[x: string]: unknown;
|
||||
};
|
||||
paid: boolean;
|
||||
payment_method: {
|
||||
/**
|
||||
* Unknown Property
|
||||
*/
|
||||
[x: string]: unknown;
|
||||
card?: {
|
||||
/**
|
||||
* Unknown Property
|
||||
*/
|
||||
[x: string]: unknown;
|
||||
card_type?: string;
|
||||
expiry_month?: string;
|
||||
@@ -98,9 +85,6 @@ interface IIpnRequest {
|
||||
};
|
||||
refundable?: boolean;
|
||||
refunded_amount: {
|
||||
/**
|
||||
* Unknown Property
|
||||
*/
|
||||
[x: string]: unknown;
|
||||
currency: string;
|
||||
value: string;
|
||||
@@ -111,9 +95,6 @@ interface IIpnRequest {
|
||||
type: string;
|
||||
}
|
||||
|
||||
export const Schema = z.object({});
|
||||
|
||||
// Интерфейс для параметров конфигурации плагина
|
||||
export type IConfig = z.infer<typeof Schema>;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
@@ -122,6 +103,7 @@ export interface ILog {}
|
||||
export class YookassaPlugin extends IPNProvider {
|
||||
constructor(
|
||||
@Inject(EXTENSION_REPOSITORY) private readonly extensionRepository: ExtensionDomainRepository,
|
||||
@Inject(PAYMENT_REPOSITORY) private readonly paymentRepository: TypeOrmPaymentRepository,
|
||||
private readonly logger: WinstonLoggerService
|
||||
) {
|
||||
super();
|
||||
@@ -151,7 +133,7 @@ export class YookassaPlugin extends IPNProvider {
|
||||
public fee_percent = 3.5; ///%
|
||||
|
||||
public async handleIPN(request: IIpnRequest): Promise<void> {
|
||||
const { event, object } = request;
|
||||
const { event } = request;
|
||||
|
||||
const exist = (await IPN.findOne({ 'data.object.id': request.object.id }))?.data as IIpnRequest;
|
||||
|
||||
@@ -159,62 +141,93 @@ export class YookassaPlugin extends IPNProvider {
|
||||
await IPN.create({ provider: 'yookassa', data: request });
|
||||
|
||||
const { secret } = request.object.metadata;
|
||||
const order = await Order.findOne({ secret });
|
||||
const payments = await this.paymentRepository.getAllPayments(
|
||||
{
|
||||
secret,
|
||||
direction: PaymentDirectionEnum.INCOMING, // Ищем только входящие платежи
|
||||
},
|
||||
{ limit: 1, page: 1, sortOrder: 'DESC' }
|
||||
);
|
||||
const payment = payments.items[0];
|
||||
|
||||
if (order) {
|
||||
this.logger.info('Order found', { source: 'handleIPN', id: order.id });
|
||||
if (payment) {
|
||||
this.logger.info('Payment found', { source: 'handleIPN', id: payment.id });
|
||||
|
||||
if (event === 'payment.succeeded') {
|
||||
const [, symbol] = order.quantity.split(' ');
|
||||
const symbol = payment.symbol;
|
||||
|
||||
const symbol_result = checkPaymentSymbol(request.object.income_amount.currency, symbol);
|
||||
|
||||
if (symbol_result.status == 'error') {
|
||||
this.logger.warn('Payment symbol verification failed', {
|
||||
source: 'handleIPN',
|
||||
id: order.id,
|
||||
id: payment.id,
|
||||
message: symbol_result.message,
|
||||
});
|
||||
|
||||
await Order.updateOne({ _id: order.id }, { status: 'failed', message: symbol_result.message });
|
||||
redisPublisher.publish(
|
||||
`${config.coopname}:orderStatusUpdate`,
|
||||
JSON.stringify({ id: order.id, status: 'failed' })
|
||||
);
|
||||
if (payment.id) {
|
||||
await this.paymentRepository.update(payment.id, {
|
||||
status: PaymentStatusEnum.FAILED,
|
||||
message: symbol_result.message,
|
||||
});
|
||||
redisPublisher.publish(
|
||||
`${config.coopname}:orderStatusUpdate`,
|
||||
JSON.stringify({ id: payment.id, status: PaymentStatusEnum.FAILED })
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const result = checkPaymentAmount(request.object.income_amount.value, order.quantity, this.tolerance_percent);
|
||||
const result = checkPaymentAmount(
|
||||
parseFloat(request.object.income_amount.value),
|
||||
payment.quantity,
|
||||
this.tolerance_percent
|
||||
);
|
||||
|
||||
if (result.status === 'success') {
|
||||
// Обработка успешного платежа
|
||||
this.logger.info('Payment amount verified, updating order status to paid', {
|
||||
this.logger.info('Payment amount verified, updating payment status to paid', {
|
||||
source: 'handleIPN',
|
||||
id: order.id,
|
||||
id: payment.id,
|
||||
});
|
||||
|
||||
await Order.updateOne({ _id: order.id }, { status: 'paid' });
|
||||
redisPublisher.publish(`${config.coopname}:orderStatusUpdate`, JSON.stringify({ id: order.id, status: 'paid' }));
|
||||
if (payment.id) {
|
||||
await this.paymentRepository.update(payment.id, { status: PaymentStatusEnum.PAID });
|
||||
redisPublisher.publish(
|
||||
`${config.coopname}:orderStatusUpdate`,
|
||||
JSON.stringify({ id: payment.id, status: PaymentStatusEnum.PAID })
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Обработка неудачного платежа
|
||||
this.logger.warn('Payment amount verification failed', {
|
||||
source: 'handleIPN',
|
||||
id: order.id,
|
||||
id: payment.id,
|
||||
message: result.message,
|
||||
});
|
||||
|
||||
await Order.updateOne({ _id: order.id }, { status: 'failed', message: result.message });
|
||||
redisPublisher.publish(
|
||||
`${config.coopname}:orderStatusUpdate`,
|
||||
JSON.stringify({ id: order.id, status: 'failed' })
|
||||
);
|
||||
if (payment.id) {
|
||||
await this.paymentRepository.update(payment.id, {
|
||||
status: PaymentStatusEnum.FAILED,
|
||||
message: result.message,
|
||||
});
|
||||
redisPublisher.publish(
|
||||
`${config.coopname}:orderStatusUpdate`,
|
||||
JSON.stringify({ id: payment.id, status: PaymentStatusEnum.FAILED })
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (event === 'payment.failed') {
|
||||
// Обработка неудачного платежа
|
||||
this.logger.warn('Payment failed event received', { source: 'handleIPN', id: order.id });
|
||||
this.logger.warn('Payment failed event received', { source: 'handleIPN', id: payment.id });
|
||||
|
||||
await Order.updateOne({ _id: order.id }, { status: 'failed' });
|
||||
redisPublisher.publish(`${config.coopname}:orderStatusUpdate`, JSON.stringify({ id: order.id, status: 'failed' }));
|
||||
if (payment.id) {
|
||||
await this.paymentRepository.update(payment.id, { status: PaymentStatusEnum.FAILED });
|
||||
redisPublisher.publish(
|
||||
`${config.coopname}:orderStatusUpdate`,
|
||||
JSON.stringify({ id: payment.id, status: PaymentStatusEnum.FAILED })
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
//TODO платеж есть, а ордера на него нет. Что делаем?
|
||||
@@ -228,13 +241,22 @@ export class YookassaPlugin extends IPNProvider {
|
||||
}
|
||||
}
|
||||
|
||||
public async createPayment(
|
||||
amount: string,
|
||||
symbol: string,
|
||||
description: string,
|
||||
order_num: number,
|
||||
secret: string
|
||||
): Promise<PaymentDetails> {
|
||||
public async createPayment(hash: string): Promise<PaymentDetails> {
|
||||
// Получаем данные платежа по hash
|
||||
const payment = await this.paymentRepository.findByHash(hash);
|
||||
|
||||
if (!payment) {
|
||||
throw new Error(`Платеж с hash ${hash} не найден`);
|
||||
}
|
||||
|
||||
// Используем QuantityUtils для парсинга quantity
|
||||
const symbol = payment.symbol;
|
||||
const amount = payment.quantity;
|
||||
|
||||
if (!payment.secret) {
|
||||
throw new Error(`У платежа ${hash} отсутствует secret`);
|
||||
}
|
||||
|
||||
const settings = await Settings.getSettings();
|
||||
|
||||
const checkout = new YooCheckout({
|
||||
@@ -242,33 +264,36 @@ export class YookassaPlugin extends IPNProvider {
|
||||
secretKey: settings.provider.secret,
|
||||
});
|
||||
|
||||
const amount_plus_fee = getAmountPlusFee(parseFloat(amount), this.fee_percent).toFixed(2);
|
||||
const fee_amount = (parseFloat(amount_plus_fee) - parseFloat(amount)).toFixed(2);
|
||||
const amount_plus_fee = getAmountPlusFee(amount, this.fee_percent).toFixed(2);
|
||||
const fee_amount = (parseFloat(amount_plus_fee) - amount).toFixed(2);
|
||||
|
||||
// Фактический процент комиссии
|
||||
const fact_fee_percent = Math.round((parseFloat(fee_amount) / parseFloat(amount)) * 100 * 100) / 100;
|
||||
const fact_fee_percent = Math.round((parseFloat(fee_amount) / amount) * 100 * 100) / 100;
|
||||
|
||||
const payment = await checkout.createPayment(
|
||||
const description = payment.memo || `Платеж для ${payment.username}`;
|
||||
|
||||
const payment_result = await checkout.createPayment(
|
||||
{
|
||||
description,
|
||||
amount: {
|
||||
value: amount_plus_fee,
|
||||
currency: 'RUB',
|
||||
currency: symbol,
|
||||
},
|
||||
confirmation: {
|
||||
type: 'embedded',
|
||||
},
|
||||
metadata: {
|
||||
secret,
|
||||
secret: payment.secret,
|
||||
},
|
||||
capture: true,
|
||||
},
|
||||
secret
|
||||
payment.secret
|
||||
);
|
||||
|
||||
return {
|
||||
data: payment?.confirmation?.confirmation_token || '',
|
||||
data: payment_result?.confirmation?.confirmation_token || '',
|
||||
amount_plus_fee: `${amount_plus_fee} ${symbol}`,
|
||||
amount_without_fee: amount,
|
||||
amount_without_fee: `${amount} ${symbol}`,
|
||||
fee_amount: `${fee_amount} ${symbol}`,
|
||||
fee_percent: this.fee_percent,
|
||||
fact_fee_percent,
|
||||
@@ -278,8 +303,19 @@ export class YookassaPlugin extends IPNProvider {
|
||||
}
|
||||
|
||||
@Module({
|
||||
providers: [YookassaPlugin], // Регистрируем SberpollPlugin как провайдер
|
||||
exports: [YookassaPlugin], // Экспортируем его для доступа в других модулях
|
||||
imports: [TypeOrmModule.forFeature([PaymentEntity])],
|
||||
providers: [
|
||||
YookassaPlugin,
|
||||
{
|
||||
provide: PAYMENT_REPOSITORY,
|
||||
useClass: TypeOrmPaymentRepository,
|
||||
},
|
||||
{
|
||||
provide: EXTENSION_REPOSITORY,
|
||||
useClass: TypeOrmExtensionDomainRepository,
|
||||
},
|
||||
],
|
||||
exports: [YookassaPlugin],
|
||||
})
|
||||
export class YookassaPluginModule {
|
||||
constructor(private readonly yookassaPlugin: YookassaPlugin) {}
|
||||
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { BlockchainService } from '../blockchain.service';
|
||||
import { GatewayContract, WalletContract } from 'cooptypes';
|
||||
import { TransactResult } from '@wharfkit/session';
|
||||
import Vault from '~/models/vault.model';
|
||||
import httpStatus from 'http-status';
|
||||
import { HttpApiError } from '~/errors/http-api-error';
|
||||
import type { TransactionResult } from '~/domain/blockchain/types/transaction-result.type';
|
||||
import type { GatewayBlockchainPort } from '~/domain/gateway/ports/gateway-blockchain.port';
|
||||
import type { CompleteIncomeDomainInterface } from '~/domain/gateway/interfaces/complete-income-domain.interface';
|
||||
import type { CompleteOutcomeDomainInterface } from '~/domain/gateway/interfaces/complete-outcome-domain.interface';
|
||||
import type { DeclineOutcomeDomainInterface } from '~/domain/gateway/interfaces/decline-outcome-domain.interface';
|
||||
|
||||
/**
|
||||
* Блокчейн адаптер для gateway
|
||||
* Реализует только управление уже созданными outcomes
|
||||
*/
|
||||
@Injectable()
|
||||
export class GatewayBlockchainAdapter implements GatewayBlockchainPort {
|
||||
private readonly logger = new Logger(GatewayBlockchainAdapter.name);
|
||||
|
||||
constructor(private readonly blockchainService: BlockchainService) {}
|
||||
|
||||
/**
|
||||
* Завершение исходящего платежа
|
||||
*/
|
||||
async completeOutcome(data: CompleteOutcomeDomainInterface): Promise<TransactionResult> {
|
||||
const wif = await Vault.getWif(data.coopname);
|
||||
if (!wif) throw new HttpApiError(httpStatus.BAD_GATEWAY, 'Не найден приватный ключ для совершения операции');
|
||||
|
||||
this.blockchainService.initialize(data.coopname, wif);
|
||||
|
||||
// Преобразуем доменные данные в формат cooptypes контракта
|
||||
const blockchainData: GatewayContract.Actions.CompleteOutcome.ICompleteOutcome = {
|
||||
coopname: data.coopname,
|
||||
outcome_hash: data.outcome_hash,
|
||||
};
|
||||
console.log('blockchainData', blockchainData);
|
||||
const result = (await this.blockchainService.transact({
|
||||
account: GatewayContract.contractName.production,
|
||||
name: GatewayContract.Actions.CompleteOutcome.actionName,
|
||||
authorization: [{ actor: data.coopname, permission: 'active' }],
|
||||
data: blockchainData,
|
||||
})) as TransactResult;
|
||||
|
||||
this.logger.log(`Завершен исходящий платеж: ${data.outcome_hash}`);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Отклонение исходящего платежа
|
||||
*/
|
||||
async declineOutcome(data: DeclineOutcomeDomainInterface): Promise<TransactionResult> {
|
||||
const wif = await Vault.getWif(data.coopname);
|
||||
if (!wif) throw new HttpApiError(httpStatus.BAD_GATEWAY, 'Не найден приватный ключ для совершения операции');
|
||||
|
||||
this.blockchainService.initialize(data.coopname, wif);
|
||||
|
||||
// Преобразуем доменные данные в формат cooptypes контракта
|
||||
const blockchainData: GatewayContract.Actions.DeclineOutcome.IOutDecline = {
|
||||
coopname: data.coopname,
|
||||
outcome_hash: data.outcome_hash,
|
||||
reason: data.reason,
|
||||
};
|
||||
|
||||
const result = (await this.blockchainService.transact({
|
||||
account: GatewayContract.contractName.production,
|
||||
name: GatewayContract.Actions.DeclineOutcome.actionName,
|
||||
authorization: [{ actor: data.coopname, permission: 'active' }],
|
||||
data: blockchainData,
|
||||
})) as TransactResult;
|
||||
|
||||
this.logger.log(`Отклонен исходящий платеж: ${data.outcome_hash}, причина: ${data.reason}`);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение исходящих платежей из блокчейна
|
||||
*/
|
||||
async getOutcomes(coopname: string): Promise<GatewayContract.Tables.Outcomes.IOutcome[]> {
|
||||
return this.blockchainService.getAllRows(
|
||||
GatewayContract.contractName.production,
|
||||
coopname,
|
||||
GatewayContract.Tables.Outcomes.tableName
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение конкретного исходящего платежа
|
||||
*/
|
||||
async getOutcome(coopname: string, outcome_hash: string): Promise<GatewayContract.Tables.Outcomes.IOutcome | null> {
|
||||
try {
|
||||
const outcomes = await this.blockchainService.query(
|
||||
GatewayContract.contractName.production,
|
||||
coopname,
|
||||
GatewayContract.Tables.Outcomes.tableName,
|
||||
{
|
||||
indexPosition: 'secondary',
|
||||
from: outcome_hash,
|
||||
to: outcome_hash,
|
||||
}
|
||||
);
|
||||
|
||||
return outcomes.length > 0 ? outcomes[0] : null;
|
||||
} catch (error: any) {
|
||||
this.logger.warn(`Не удалось получить outcome ${outcome_hash} для ${coopname}: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Завершение входящего платежа
|
||||
*/
|
||||
async completeIncome(data: CompleteIncomeDomainInterface): Promise<TransactionResult> {
|
||||
// Получаем приватный ключ кооператива
|
||||
const wif = await Vault.getWif(data.coopname);
|
||||
if (!wif) throw new HttpApiError(httpStatus.BAD_GATEWAY, 'Не найден приватный ключ для совершения операции');
|
||||
|
||||
this.blockchainService.initialize(data.coopname, wif);
|
||||
|
||||
// Формируем данные для создания депозита
|
||||
const createDeposit: WalletContract.Actions.CreateDeposit.ICreateDeposit = {
|
||||
coopname: data.coopname,
|
||||
username: data.username,
|
||||
quantity: data.quantity,
|
||||
deposit_hash: data.income_hash,
|
||||
};
|
||||
|
||||
// Формируем данные для завершения дохода
|
||||
const completeDeposit: GatewayContract.Actions.CompleteIncome.ICompleteIncome = {
|
||||
coopname: data.coopname,
|
||||
income_hash: data.income_hash,
|
||||
};
|
||||
|
||||
// Формируем массив действий
|
||||
const actions = [
|
||||
{
|
||||
account: WalletContract.contractName.production,
|
||||
name: WalletContract.Actions.CreateDeposit.actionName,
|
||||
authorization: [
|
||||
{
|
||||
actor: data.coopname,
|
||||
permission: 'active',
|
||||
},
|
||||
],
|
||||
data: createDeposit,
|
||||
},
|
||||
{
|
||||
account: GatewayContract.contractName.production,
|
||||
name: GatewayContract.Actions.CompleteIncome.actionName,
|
||||
authorization: [
|
||||
{
|
||||
actor: data.coopname,
|
||||
permission: 'active',
|
||||
},
|
||||
],
|
||||
data: completeDeposit,
|
||||
},
|
||||
];
|
||||
|
||||
// Выполняем транзакцию
|
||||
const result = await this.blockchainService.transact(actions);
|
||||
this.logger.log(
|
||||
`Входящий платеж завершён: income_hash=${data.income_hash}, username=${data.username}, quantity=${data.quantity}`
|
||||
);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { BlockchainService } from '../blockchain.service';
|
||||
import { WalletContract } from 'cooptypes';
|
||||
import { TransactResult } from '@wharfkit/session';
|
||||
import Vault from '~/models/vault.model';
|
||||
import httpStatus from 'http-status';
|
||||
import { HttpApiError } from '~/errors/http-api-error';
|
||||
import type { TransactionResult } from '~/domain/blockchain/types/transaction-result.type';
|
||||
import type {
|
||||
WalletBlockchainPort,
|
||||
CreateWithdrawDomainInterface,
|
||||
GenerateReturnStatementDomainInterface,
|
||||
} from '~/domain/wallet/ports/wallet-blockchain.port';
|
||||
import type { ISignedDocumentDomainInterface } from '~/domain/document/interfaces/signed-document-domain.interface';
|
||||
import { DomainToBlockchainUtils } from '../utils/domain-to-blockchain.utils';
|
||||
|
||||
/**
|
||||
* Блокчейн адаптер для wallet
|
||||
* Реализует взаимодействие с контрактом wallet
|
||||
*/
|
||||
@Injectable()
|
||||
export class WalletBlockchainAdapter implements WalletBlockchainPort {
|
||||
private readonly logger = new Logger(WalletBlockchainAdapter.name);
|
||||
|
||||
constructor(
|
||||
private readonly blockchainService: BlockchainService,
|
||||
private readonly domainToBlockchainUtils: DomainToBlockchainUtils
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Создание заявки на вывод средств в контракте wallet
|
||||
*/
|
||||
async createWithdraw(data: CreateWithdrawDomainInterface): Promise<TransactionResult> {
|
||||
const wif = await Vault.getWif(data.coopname);
|
||||
if (!wif) throw new HttpApiError(httpStatus.BAD_GATEWAY, 'Не найден приватный ключ для совершения операции');
|
||||
|
||||
this.blockchainService.initialize(data.coopname, wif);
|
||||
|
||||
// Форматируем quantity к необходимой точности для блокчейна
|
||||
const formattedQuantity = this.domainToBlockchainUtils.formatQuantityWithPrecision(data.quantity);
|
||||
|
||||
// Преобразуем доменные данные в формат cooptypes контракта
|
||||
const blockchainData: WalletContract.Actions.CreateWithdraw.ICreateWithdraw = {
|
||||
coopname: data.coopname,
|
||||
username: data.username,
|
||||
withdraw_hash: data.withdraw_hash,
|
||||
quantity: formattedQuantity,
|
||||
statement: this.domainToBlockchainUtils.convertSignedDocumentToBlockchainFormat(data.statement),
|
||||
};
|
||||
|
||||
const result = (await this.blockchainService.transact({
|
||||
account: WalletContract.contractName.production,
|
||||
name: WalletContract.Actions.CreateWithdraw.actionName,
|
||||
authorization: [{ actor: data.coopname, permission: 'active' }],
|
||||
data: blockchainData,
|
||||
})) as TransactResult;
|
||||
|
||||
this.logger.log(`Создана заявка на вывод средств: ${data.withdraw_hash}`);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Генерация заявления на возврат паевого взноса
|
||||
*/
|
||||
async generateReturnStatement(_data: GenerateReturnStatementDomainInterface): Promise<ISignedDocumentDomainInterface> {
|
||||
// TODO: Реализовать генерацию заявления через документный сервис
|
||||
// Пока возвращаем заглушку
|
||||
throw new Error('Метод generateReturnStatement еще не реализован');
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение данных из wallet контракта
|
||||
*/
|
||||
async getWithdraw(coopname: string, withdraw_hash: string): Promise<WalletContract.Tables.Withdraws.IWithdraws | null> {
|
||||
try {
|
||||
const withdraws = await this.blockchainService.query(
|
||||
WalletContract.contractName.production,
|
||||
coopname,
|
||||
WalletContract.Tables.Withdraws.tableName,
|
||||
{
|
||||
indexPosition: 'secondary',
|
||||
from: withdraw_hash,
|
||||
to: withdraw_hash,
|
||||
}
|
||||
);
|
||||
|
||||
return withdraws.length > 0 ? withdraws[0] : null;
|
||||
} catch (error: any) {
|
||||
this.logger.warn(`Не удалось получить withdraw ${withdraw_hash} для ${coopname}: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение всех withdrawals для кооператива
|
||||
*/
|
||||
async getWithdraws(coopname: string): Promise<WalletContract.Tables.Withdraws.IWithdraws[]> {
|
||||
return this.blockchainService.getAllRows(
|
||||
WalletContract.contractName.production,
|
||||
coopname,
|
||||
WalletContract.Tables.Withdraws.tableName
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,10 @@ import { MEET_BLOCKCHAIN_PORT } from '~/domain/meet/ports/meet-blockchain.port';
|
||||
import { MeetBlockchainAdapter } from './adapters/meet-blockchain.adapter';
|
||||
import { DomainToBlockchainUtils } from './utils/domain-to-blockchain.utils';
|
||||
import { DomainModule } from '~/domain/domain.module';
|
||||
import { GatewayBlockchainAdapter } from './adapters/gateway-blockchain.adapter';
|
||||
import { WALLET_BLOCKCHAIN_PORT } from '~/domain/wallet/ports/wallet-blockchain.port';
|
||||
import { GATEWAY_BLOCKCHAIN_PORT } from '~/domain/gateway/ports/gateway-blockchain.port';
|
||||
import { WalletBlockchainAdapter } from './adapters/wallet-blockchain.adapter';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
@@ -49,9 +53,20 @@ import { DomainModule } from '~/domain/domain.module';
|
||||
provide: MEET_BLOCKCHAIN_PORT,
|
||||
useClass: MeetBlockchainAdapter,
|
||||
},
|
||||
// Провайдеры для gateway
|
||||
{
|
||||
provide: GATEWAY_BLOCKCHAIN_PORT,
|
||||
useClass: GatewayBlockchainAdapter,
|
||||
},
|
||||
// Провайдеры для wallet
|
||||
{
|
||||
provide: WALLET_BLOCKCHAIN_PORT,
|
||||
useClass: WalletBlockchainAdapter,
|
||||
},
|
||||
DomainToBlockchainUtils,
|
||||
],
|
||||
exports: [
|
||||
BlockchainService,
|
||||
BLOCKCHAIN_PORT,
|
||||
BRANCH_BLOCKCHAIN_PORT,
|
||||
SYSTEM_BLOCKCHAIN_PORT,
|
||||
@@ -59,6 +74,8 @@ import { DomainModule } from '~/domain/domain.module';
|
||||
SOVIET_BLOCKCHAIN_PORT,
|
||||
COOPLACE_BLOCKCHAIN_PORT,
|
||||
MEET_BLOCKCHAIN_PORT,
|
||||
GATEWAY_BLOCKCHAIN_PORT,
|
||||
WALLET_BLOCKCHAIN_PORT,
|
||||
DomainToBlockchainUtils,
|
||||
],
|
||||
})
|
||||
|
||||
+57
-4
@@ -1,6 +1,8 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Cooperative } from 'cooptypes';
|
||||
import moment from 'moment';
|
||||
import config from '~/config/config';
|
||||
import type { ISignedDocumentDomainInterface } from '~/domain/document/interfaces/signed-document-domain.interface';
|
||||
|
||||
/**
|
||||
* Утилитарный класс для конвертации доменных объектов в инфраструктурные типы блокчейна
|
||||
@@ -12,9 +14,7 @@ export class DomainToBlockchainUtils {
|
||||
* @param document Доменный объект подписанного документа
|
||||
* @returns Объект документа в формате блокчейна
|
||||
*/
|
||||
convertSignedDocumentToBlockchainFormat(
|
||||
document: Cooperative.Document.ISignedDocument2
|
||||
): Cooperative.Document.IChainDocument2 {
|
||||
convertSignedDocumentToBlockchainFormat(document: ISignedDocumentDomainInterface): Cooperative.Document.IChainDocument2 {
|
||||
return {
|
||||
version: document.version,
|
||||
hash: document.hash,
|
||||
@@ -25,6 +25,22 @@ export class DomainToBlockchainUtils {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Преобразует документ из блокчейна в доменный интерфейс
|
||||
* @param chainDoc Документ из блокчейна
|
||||
* @returns Документ в доменном формате
|
||||
*/
|
||||
convertBlockchainDocumentToDomainFormat(chainDoc: Cooperative.Document.IChainDocument2): ISignedDocumentDomainInterface {
|
||||
return {
|
||||
version: chainDoc.version,
|
||||
hash: chainDoc.hash,
|
||||
doc_hash: chainDoc.doc_hash,
|
||||
meta_hash: chainDoc.meta_hash,
|
||||
meta: typeof chainDoc.meta === 'string' ? (chainDoc.meta === '' ? {} : JSON.parse(chainDoc.meta)) : chainDoc.meta,
|
||||
signatures: chainDoc.signatures,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Преобразует Date в формат EOSIO time_point_sec для блокчейна
|
||||
* @param date Дата или строка с датой
|
||||
@@ -35,7 +51,44 @@ export class DomainToBlockchainUtils {
|
||||
}
|
||||
|
||||
/**
|
||||
* Преобразует объект IChainDocument2 в ISignedDocument2
|
||||
* Форматирование quantity с проверкой символа и использованием precision из конфига
|
||||
* @param quantity Строка в формате "число символ" (например, "100.0 AXON")
|
||||
* @returns Отформатированная строка quantity для блокчейна
|
||||
*/
|
||||
formatQuantityWithPrecision(quantity: string): string {
|
||||
// Извлекаем число и символ
|
||||
const parts = quantity.split(' ');
|
||||
if (parts.length !== 2) {
|
||||
throw new Error(`Неверный формат quantity: ${quantity}. Ожидается "число символ"`);
|
||||
}
|
||||
|
||||
const [amount, symbol] = parts;
|
||||
const numericAmount = parseFloat(amount);
|
||||
|
||||
if (isNaN(numericAmount)) {
|
||||
throw new Error(`Некорректное числовое значение в quantity: ${amount}`);
|
||||
}
|
||||
|
||||
// Проверяем символ на соответствие с конфигом
|
||||
let precision: number;
|
||||
if (symbol === config.blockchain.root_symbol) {
|
||||
precision = config.blockchain.root_precision;
|
||||
} else if (symbol === config.blockchain.root_govern_symbol) {
|
||||
precision = config.blockchain.root_govern_precision;
|
||||
} else {
|
||||
throw new Error(
|
||||
`Неподдерживаемый символ: ${symbol}. Поддерживаются только: ${config.blockchain.root_symbol}, ${config.blockchain.root_govern_symbol}`
|
||||
);
|
||||
}
|
||||
|
||||
// Форматируем к необходимому количеству знаков после запятой
|
||||
const formattedAmount = numericAmount.toFixed(precision);
|
||||
|
||||
return `${formattedAmount} ${symbol}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Преобразует объект IChainDocument2 в ISignedDocument2 (для совместимости)
|
||||
* @param chainDoc Документ из блокчейна
|
||||
* @returns Документ в формате ISignedDocument2
|
||||
*/
|
||||
|
||||
+7
@@ -14,6 +14,8 @@ import { PROJECT_FREE_DECISION_REPOSITORY } from '~/domain/common/repositories/p
|
||||
import { ProjectFreeDecisionRepositoryImplementation } from './repositories/project-free-decision-generator.repository';
|
||||
import { VARS_REPOSITORY } from '~/domain/common/repositories/vars.repository';
|
||||
import { VarsRepositoryImplementation } from './repositories/vars-generator.repository';
|
||||
import { SEARCH_PRIVATE_ACCOUNTS_REPOSITORY } from '~/domain/common/repositories/search-private-accounts.repository';
|
||||
import { SearchPrivateAccountsRepositoryImplementation } from './repositories/search-private-accounts-generator.repository';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
@@ -46,6 +48,10 @@ import { VarsRepositoryImplementation } from './repositories/vars-generator.repo
|
||||
provide: VARS_REPOSITORY,
|
||||
useClass: VarsRepositoryImplementation,
|
||||
},
|
||||
{
|
||||
provide: SEARCH_PRIVATE_ACCOUNTS_REPOSITORY,
|
||||
useClass: SearchPrivateAccountsRepositoryImplementation,
|
||||
},
|
||||
],
|
||||
exports: [
|
||||
ORGANIZATION_REPOSITORY,
|
||||
@@ -55,6 +61,7 @@ import { VarsRepositoryImplementation } from './repositories/vars-generator.repo
|
||||
DOCUMENT_REPOSITORY,
|
||||
PROJECT_FREE_DECISION_REPOSITORY,
|
||||
VARS_REPOSITORY,
|
||||
SEARCH_PRIVATE_ACCOUNTS_REPOSITORY,
|
||||
],
|
||||
})
|
||||
export class GeneratorRepositoriesModule {}
|
||||
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { SearchPrivateAccountsRepository } from '~/domain/common/repositories/search-private-accounts.repository';
|
||||
import type {
|
||||
SearchPrivateAccountsInputDomainInterface,
|
||||
PrivateAccountSearchResultDomainInterface,
|
||||
PrivateAccountSearchDataDomainInterface,
|
||||
} from '~/domain/common/interfaces/search-private-accounts-domain.interface';
|
||||
import type { IndividualDomainInterface } from '~/domain/common/interfaces/individual-domain.interface';
|
||||
import type { EntrepreneurDomainInterface } from '~/domain/common/interfaces/entrepreneur-domain.interface';
|
||||
import type { OrganizationDomainInterface } from '~/domain/common/interfaces/organization-domain.interface';
|
||||
import { generator } from '~/services/document.service';
|
||||
import type { ISearchResult } from '@coopenomics/factory';
|
||||
|
||||
/**
|
||||
* Инфраструктурная реализация репозитория поиска приватных аккаунтов
|
||||
* Использует фабрику документов для выполнения поиска
|
||||
*/
|
||||
@Injectable()
|
||||
export class SearchPrivateAccountsRepositoryImplementation implements SearchPrivateAccountsRepository {
|
||||
/**
|
||||
* Выполняет поиск приватных аккаунтов через фабрику документов
|
||||
* @param input Входные данные для поиска
|
||||
* @returns Массив результатов поиска, преобразованных в доменные интерфейсы
|
||||
*/
|
||||
async searchPrivateAccounts(
|
||||
input: SearchPrivateAccountsInputDomainInterface
|
||||
): Promise<PrivateAccountSearchResultDomainInterface[]> {
|
||||
// Вызываем метод поиска из фабрики документов
|
||||
const factoryResults: ISearchResult[] = await generator.search(input.query);
|
||||
|
||||
// Преобразуем результаты фабрики в доменные интерфейсы
|
||||
const domainResults: PrivateAccountSearchResultDomainInterface[] = factoryResults.map((result) =>
|
||||
this.convertFactoryResultToDomain(result)
|
||||
);
|
||||
|
||||
return domainResults;
|
||||
}
|
||||
|
||||
/**
|
||||
* Преобразует результат из фабрики в доменный интерфейс
|
||||
* @param factoryResult Результат поиска из фабрики
|
||||
* @returns Результат поиска в доменном формате
|
||||
*/
|
||||
private convertFactoryResultToDomain(factoryResult: ISearchResult): PrivateAccountSearchResultDomainInterface {
|
||||
let domainData: PrivateAccountSearchDataDomainInterface;
|
||||
|
||||
switch (factoryResult.type) {
|
||||
case 'individual':
|
||||
domainData = this.convertIndividualData(factoryResult.data as any);
|
||||
break;
|
||||
case 'entrepreneur':
|
||||
domainData = this.convertEntrepreneurData(factoryResult.data as any);
|
||||
break;
|
||||
case 'organization':
|
||||
domainData = this.convertOrganizationData(factoryResult.data as any);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Неизвестный тип данных: ${factoryResult.type}`);
|
||||
}
|
||||
|
||||
return {
|
||||
type: factoryResult.type,
|
||||
data: domainData,
|
||||
score: factoryResult.score,
|
||||
highlightedFields: factoryResult.highlightedFields,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Преобразует данные физического лица из фабрики в доменный интерфейс
|
||||
* @param data Данные из фабрики
|
||||
* @returns Доменный интерфейс физического лица
|
||||
*/
|
||||
private convertIndividualData(data: any): IndividualDomainInterface {
|
||||
return {
|
||||
username: data.username,
|
||||
first_name: data.first_name,
|
||||
last_name: data.last_name,
|
||||
middle_name: data.middle_name,
|
||||
birthdate: data.birthdate,
|
||||
full_address: data.full_address,
|
||||
phone: data.phone,
|
||||
email: data.email,
|
||||
passport: data.passport
|
||||
? {
|
||||
series: data.passport.series,
|
||||
number: data.passport.number,
|
||||
issued_by: data.passport.issued_by,
|
||||
issued_at: data.passport.issued_at,
|
||||
code: data.passport.code,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Преобразует данные индивидуального предпринимателя из фабрики в доменный интерфейс
|
||||
* @param data Данные из фабрики
|
||||
* @returns Доменный интерфейс индивидуального предпринимателя
|
||||
*/
|
||||
private convertEntrepreneurData(data: any): EntrepreneurDomainInterface {
|
||||
return {
|
||||
username: data.username,
|
||||
first_name: data.first_name,
|
||||
last_name: data.last_name,
|
||||
middle_name: data.middle_name,
|
||||
birthdate: data.birthdate,
|
||||
phone: data.phone,
|
||||
email: data.email,
|
||||
country: data.country,
|
||||
city: data.city,
|
||||
full_address: data.full_address,
|
||||
details: {
|
||||
inn: data.details.inn,
|
||||
ogrn: data.details.ogrn,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Преобразует данные организации из фабрики в доменный интерфейс
|
||||
* @param data Данные из фабрики
|
||||
* @returns Доменный интерфейс организации
|
||||
*/
|
||||
private convertOrganizationData(data: any): OrganizationDomainInterface {
|
||||
return {
|
||||
username: data.username,
|
||||
type: data.type,
|
||||
short_name: data.short_name,
|
||||
full_name: data.full_name,
|
||||
represented_by: {
|
||||
first_name: data.represented_by.first_name,
|
||||
last_name: data.represented_by.last_name,
|
||||
middle_name: data.represented_by.middle_name,
|
||||
position: data.represented_by.position,
|
||||
based_on: data.represented_by.based_on,
|
||||
},
|
||||
country: data.country,
|
||||
city: data.city,
|
||||
full_address: data.full_address,
|
||||
fact_address: data.fact_address,
|
||||
phone: data.phone,
|
||||
email: data.email,
|
||||
details: {
|
||||
inn: data.details.inn,
|
||||
ogrn: data.details.ogrn,
|
||||
kpp: data.details.kpp,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
|
||||
import { PaymentStatusEnum } from '~/domain/gateway/enums/payment-status.enum';
|
||||
import { PaymentTypeEnum, PaymentDirectionEnum } from '~/domain/gateway/enums/payment-type.enum';
|
||||
import type {
|
||||
PaymentDomainInterface,
|
||||
PaymentDetailsDomainInterface,
|
||||
} from '~/domain/gateway/interfaces/payment-domain.interface';
|
||||
import type { ISignedDocumentDomainInterface } from '~/domain/document/interfaces/signed-document-domain.interface';
|
||||
|
||||
/**
|
||||
* Унифицированная сущность платежа для TypeORM
|
||||
* Поддерживает все типы платежей (входящие и исходящие)
|
||||
*/
|
||||
@Entity('payments')
|
||||
export class PaymentEntity implements PaymentDomainInterface {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column()
|
||||
hash!: string;
|
||||
|
||||
@Column()
|
||||
coopname!: string;
|
||||
|
||||
@Column()
|
||||
username!: string;
|
||||
|
||||
@Column()
|
||||
quantity!: number;
|
||||
|
||||
@Column()
|
||||
symbol!: string;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: PaymentTypeEnum,
|
||||
})
|
||||
type!: PaymentTypeEnum;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: PaymentDirectionEnum,
|
||||
})
|
||||
direction!: PaymentDirectionEnum;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: PaymentStatusEnum,
|
||||
default: PaymentStatusEnum.PENDING,
|
||||
})
|
||||
status!: PaymentStatusEnum;
|
||||
|
||||
@Column({ nullable: true })
|
||||
provider?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
payment_method_id?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
secret?: string;
|
||||
|
||||
@Column({ nullable: true, type: 'text' })
|
||||
message?: string;
|
||||
|
||||
@Column({ nullable: true, type: 'text' })
|
||||
memo?: string;
|
||||
|
||||
@Column({ nullable: true, type: 'timestamp' })
|
||||
expired_at?: Date;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamp' })
|
||||
created_at!: Date;
|
||||
|
||||
@UpdateDateColumn({ nullable: true, type: 'timestamp' })
|
||||
updated_at?: Date;
|
||||
|
||||
@Column('json', { nullable: true })
|
||||
payment_details?: PaymentDetailsDomainInterface;
|
||||
|
||||
@Column('json', { nullable: true })
|
||||
blockchain_data?: any;
|
||||
|
||||
@Column('json', { nullable: true })
|
||||
statement?: ISignedDocumentDomainInterface;
|
||||
}
|
||||
+229
@@ -0,0 +1,229 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { PaymentEntity } from '../entities/payment.entity';
|
||||
import type { PaymentRepository } from '~/domain/gateway/repositories/payment.repository';
|
||||
import type { PaymentDomainInterface } from '~/domain/gateway/interfaces/payment-domain.interface';
|
||||
import type { InternalPaymentFiltersDomainInterface } from '~/domain/gateway/interfaces/payment-filters-domain.interface';
|
||||
import type {
|
||||
PaginationInputDomainInterface,
|
||||
PaginationResultDomainInterface,
|
||||
} from '~/domain/common/interfaces/pagination.interface';
|
||||
import { PaymentStatusEnum } from '~/domain/gateway/enums/payment-status.enum';
|
||||
import { PaymentDirectionEnum, PaymentTypeEnum } from '~/domain/gateway/enums/payment-type.enum';
|
||||
|
||||
@Injectable()
|
||||
export class TypeOrmPaymentRepository implements PaymentRepository {
|
||||
constructor(
|
||||
@InjectRepository(PaymentEntity)
|
||||
private readonly paymentRepository: Repository<PaymentEntity>
|
||||
) {}
|
||||
|
||||
async findById(id: string): Promise<PaymentDomainInterface | null> {
|
||||
const payment = await this.paymentRepository.findOneBy({ id });
|
||||
if (!payment) return null;
|
||||
return this.mapToDomainEntity(payment);
|
||||
}
|
||||
|
||||
async findByHash(hash: string): Promise<PaymentDomainInterface | null> {
|
||||
const payment = await this.paymentRepository.findOneBy({ hash });
|
||||
if (!payment) return null;
|
||||
return this.mapToDomainEntity(payment);
|
||||
}
|
||||
|
||||
async create(data: PaymentDomainInterface): Promise<PaymentDomainInterface> {
|
||||
const payment = new PaymentEntity();
|
||||
payment.hash = data.hash;
|
||||
payment.coopname = data.coopname;
|
||||
payment.username = data.username;
|
||||
payment.quantity = data.quantity;
|
||||
payment.symbol = data.symbol;
|
||||
payment.payment_method_id = data.payment_method_id;
|
||||
payment.status = data.status;
|
||||
payment.type = data.type;
|
||||
payment.direction = data.direction;
|
||||
payment.provider = data.provider;
|
||||
payment.secret = data.secret;
|
||||
payment.message = data.message;
|
||||
payment.expired_at = data.expired_at;
|
||||
payment.created_at = data.created_at;
|
||||
payment.memo = data.memo;
|
||||
payment.payment_details = data.payment_details;
|
||||
payment.blockchain_data = data.blockchain_data;
|
||||
payment.statement = data.statement;
|
||||
|
||||
const createdEntity = await this.paymentRepository.save(payment);
|
||||
return this.mapToDomainEntity(createdEntity);
|
||||
}
|
||||
|
||||
async update(id: string, data: Partial<PaymentDomainInterface>): Promise<PaymentDomainInterface | null> {
|
||||
const payment = await this.paymentRepository.findOneBy({ id });
|
||||
if (!payment) return null;
|
||||
|
||||
Object.assign(payment, data);
|
||||
payment.updated_at = new Date();
|
||||
|
||||
const updatedEntity = await this.paymentRepository.save(payment);
|
||||
return this.mapToDomainEntity(updatedEntity);
|
||||
}
|
||||
|
||||
async updatePaymentWithBlockchainData(hash: string, blockchain_data: any): Promise<PaymentDomainInterface | null> {
|
||||
const payment = await this.paymentRepository.findOneBy({ hash });
|
||||
if (!payment) return null;
|
||||
|
||||
payment.blockchain_data = blockchain_data;
|
||||
payment.updated_at = new Date();
|
||||
|
||||
const updatedEntity = await this.paymentRepository.save(payment);
|
||||
return this.mapToDomainEntity(updatedEntity);
|
||||
}
|
||||
|
||||
async setPaymentStatus(id: string, status: PaymentStatusEnum): Promise<PaymentDomainInterface | null> {
|
||||
const payment = await this.paymentRepository.findOneBy({ id });
|
||||
if (!payment) return null;
|
||||
|
||||
payment.status = status;
|
||||
payment.updated_at = new Date();
|
||||
|
||||
const updatedEntity = await this.paymentRepository.save(payment);
|
||||
return this.mapToDomainEntity(updatedEntity);
|
||||
}
|
||||
|
||||
async getAllPayments(
|
||||
filters: InternalPaymentFiltersDomainInterface,
|
||||
options: PaginationInputDomainInterface
|
||||
): Promise<PaginationResultDomainInterface<PaymentDomainInterface>> {
|
||||
const queryBuilder = this.paymentRepository.createQueryBuilder('payment');
|
||||
|
||||
if (filters.direction) {
|
||||
queryBuilder.where('payment.direction = :direction', { direction: filters.direction });
|
||||
} else if (filters.type) {
|
||||
if (filters.type === PaymentTypeEnum.WITHDRAWAL) {
|
||||
queryBuilder.where('payment.direction = :direction', { direction: PaymentDirectionEnum.OUTGOING });
|
||||
} else {
|
||||
queryBuilder.where('payment.direction = :direction', { direction: PaymentDirectionEnum.INCOMING });
|
||||
}
|
||||
}
|
||||
|
||||
if (filters.username) {
|
||||
queryBuilder.andWhere('payment.username ILIKE :username', { username: `%${filters.username}%` });
|
||||
}
|
||||
|
||||
if (filters.status) {
|
||||
queryBuilder.andWhere('payment.status = :status', { status: filters.status });
|
||||
}
|
||||
|
||||
if (filters.coopname) {
|
||||
queryBuilder.andWhere('payment.coopname = :coopname', { coopname: filters.coopname });
|
||||
}
|
||||
|
||||
if (filters.provider) {
|
||||
queryBuilder.andWhere('payment.provider = :provider', { provider: filters.provider });
|
||||
}
|
||||
|
||||
if (filters.secret) {
|
||||
queryBuilder.andWhere('payment.secret = :secret', { secret: filters.secret });
|
||||
}
|
||||
|
||||
if (filters.hash) {
|
||||
queryBuilder.andWhere('payment.hash = :hash', { hash: filters.hash });
|
||||
}
|
||||
|
||||
queryBuilder.orderBy('payment.created_at', 'DESC');
|
||||
|
||||
const limit = options.limit || 10;
|
||||
const page = options.page || 1;
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
queryBuilder.take(limit).skip(skip);
|
||||
|
||||
const [payments, totalCount] = await queryBuilder.getManyAndCount();
|
||||
|
||||
const items = payments.map((payment) => this.mapToDomainEntity(payment));
|
||||
|
||||
return {
|
||||
items,
|
||||
totalCount,
|
||||
totalPages: Math.ceil(totalCount / limit),
|
||||
currentPage: page,
|
||||
};
|
||||
}
|
||||
|
||||
private mapToDomainEntity(entity: PaymentEntity): PaymentDomainInterface {
|
||||
return {
|
||||
id: entity.id,
|
||||
hash: entity.hash,
|
||||
coopname: entity.coopname,
|
||||
username: entity.username,
|
||||
quantity: entity.quantity,
|
||||
symbol: entity.symbol,
|
||||
payment_method_id: entity.payment_method_id,
|
||||
status: entity.status,
|
||||
type: entity.type,
|
||||
direction: entity.direction,
|
||||
provider: entity.provider,
|
||||
secret: entity.secret,
|
||||
message: entity.message,
|
||||
expired_at: entity.expired_at,
|
||||
created_at: entity.created_at,
|
||||
updated_at: entity.updated_at,
|
||||
memo: entity.memo,
|
||||
payment_details: entity.payment_details,
|
||||
blockchain_data: entity.blockchain_data,
|
||||
statement: entity.statement,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновить все истекшие платежи в статус EXPIRED
|
||||
* @returns количество обновленных платежей
|
||||
*/
|
||||
async expireOutdatedPayments(): Promise<number> {
|
||||
const now = new Date();
|
||||
|
||||
const result = await this.paymentRepository
|
||||
.createQueryBuilder()
|
||||
.update(PaymentEntity)
|
||||
.set({
|
||||
status: PaymentStatusEnum.EXPIRED,
|
||||
updated_at: now,
|
||||
})
|
||||
.where('expired_at IS NOT NULL')
|
||||
.andWhere('expired_at < :now', { now })
|
||||
.andWhere('status = :status', { status: PaymentStatusEnum.PENDING })
|
||||
.execute();
|
||||
|
||||
return result.affected || 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Найти активный платеж того же типа для пользователя
|
||||
* @param username имя пользователя
|
||||
* @param type тип платежа
|
||||
* @param quantity количество
|
||||
* @param symbol символ валюты
|
||||
* @returns активный платеж или null
|
||||
*/
|
||||
async findActivePendingPayment(
|
||||
username: string,
|
||||
type: PaymentTypeEnum,
|
||||
quantity: number,
|
||||
symbol: string
|
||||
): Promise<PaymentDomainInterface | null> {
|
||||
const now = new Date();
|
||||
|
||||
const payment = await this.paymentRepository
|
||||
.createQueryBuilder('payment')
|
||||
.where('payment.username = :username', { username })
|
||||
.andWhere('payment.type = :type', { type })
|
||||
.andWhere('payment.quantity = :quantity', { quantity })
|
||||
.andWhere('payment.symbol = :symbol', { symbol })
|
||||
.andWhere('payment.status = :status', { status: PaymentStatusEnum.PENDING })
|
||||
.andWhere('(payment.expired_at IS NULL OR payment.expired_at > :now)', { now })
|
||||
.orderBy('payment.created_at', 'DESC')
|
||||
.getOne();
|
||||
|
||||
if (!payment) return null;
|
||||
return this.mapToDomainEntity(payment);
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,9 @@ import { TypeOrmCandidateRepository } from './repositories/typeorm-candidate.rep
|
||||
import { MeetProcessedEntity } from './entities/meet-processed.entity';
|
||||
import { MEET_PROCESSED_REPOSITORY } from '~/domain/meet/repositories/meet-processed.repository';
|
||||
import { TypeOrmMeetProcessedRepository } from './repositories/typeorm-meet-processed.repository';
|
||||
import { PaymentEntity } from './entities/payment.entity';
|
||||
import { PAYMENT_REPOSITORY } from '~/domain/gateway/repositories/payment.repository';
|
||||
import { TypeOrmPaymentRepository } from './repositories/typeorm-payment.repository';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
@@ -43,6 +46,7 @@ import { TypeOrmMeetProcessedRepository } from './repositories/typeorm-meet-proc
|
||||
MeetProcessedEntity,
|
||||
MigrationEntity,
|
||||
CandidateEntity,
|
||||
PaymentEntity,
|
||||
]),
|
||||
],
|
||||
providers: [
|
||||
@@ -70,6 +74,10 @@ import { TypeOrmMeetProcessedRepository } from './repositories/typeorm-meet-proc
|
||||
provide: CANDIDATE_REPOSITORY,
|
||||
useClass: TypeOrmCandidateRepository,
|
||||
},
|
||||
{
|
||||
provide: PAYMENT_REPOSITORY,
|
||||
useClass: TypeOrmPaymentRepository,
|
||||
},
|
||||
],
|
||||
exports: [
|
||||
NestTypeOrmModule,
|
||||
@@ -79,6 +87,7 @@ import { TypeOrmMeetProcessedRepository } from './repositories/typeorm-meet-proc
|
||||
MEET_PROCESSED_REPOSITORY,
|
||||
MIGRATION_REPOSITORY,
|
||||
CANDIDATE_REPOSITORY,
|
||||
PAYMENT_REPOSITORY,
|
||||
],
|
||||
})
|
||||
export class TypeOrmModule {}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// app.module.ts
|
||||
import { Module } from '@nestjs/common';
|
||||
import { DatabaseModule } from './database/database.module';
|
||||
import { GraphqlModule } from './graphql/graphql.module';
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
import { Inject, Injectable, OnModuleDestroy } from '@nestjs/common';
|
||||
import Redis from 'ioredis';
|
||||
import { REDIS_PROVIDER } from './redis.provider';
|
||||
import { WinstonLoggerService } from '~/modules/logger/logger-app.service';
|
||||
|
||||
export interface StreamMessage {
|
||||
messageId: string;
|
||||
fields: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface StreamConsumerOptions {
|
||||
stream: string;
|
||||
group: string;
|
||||
consumer: string;
|
||||
count?: number;
|
||||
block?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Сервис для работы с Redis Streams
|
||||
* Обеспечивает надежное потребление событий от parser
|
||||
*/
|
||||
@Injectable()
|
||||
export class RedisStreamService implements OnModuleDestroy {
|
||||
private consumers: Map<string, boolean> = new Map();
|
||||
|
||||
constructor(
|
||||
@Inject(REDIS_PROVIDER) private readonly redisClient: { subscriber: Redis; publisher: Redis },
|
||||
private readonly logger: WinstonLoggerService
|
||||
) {
|
||||
this.logger.setContext(RedisStreamService.name);
|
||||
}
|
||||
|
||||
onModuleDestroy() {
|
||||
// Останавливаем всех потребителей
|
||||
this.consumers.forEach((_, key) => {
|
||||
this.consumers.set(key, false);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Создание группы потребителей
|
||||
*/
|
||||
async createConsumerGroup(stream: string, group: string, startId = '0'): Promise<void> {
|
||||
try {
|
||||
await this.redisClient.subscriber.xgroup('CREATE', stream, group, startId, 'MKSTREAM');
|
||||
this.logger.log(`Consumer group created: ${group} for stream: ${stream}`);
|
||||
} catch (error: any) {
|
||||
if (error.message.includes('BUSYGROUP')) {
|
||||
this.logger.log(`Consumer group ${group} already exists for stream ${stream}`);
|
||||
} else {
|
||||
this.logger.error(`Error creating consumer group: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Потребление сообщений из stream
|
||||
*/
|
||||
async startConsumer(
|
||||
options: StreamConsumerOptions,
|
||||
messageHandler: (message: StreamMessage) => Promise<void>
|
||||
): Promise<void> {
|
||||
const { stream, group, consumer, count = 1, block = 1000 } = options;
|
||||
const consumerKey = `${stream}:${group}:${consumer}`;
|
||||
|
||||
// Отмечаем потребителя как активного
|
||||
this.consumers.set(consumerKey, true);
|
||||
|
||||
this.logger.log(`Starting consumer: ${consumerKey}`);
|
||||
|
||||
while (this.consumers.get(consumerKey)) {
|
||||
try {
|
||||
const result: any = await this.redisClient.subscriber.xreadgroup(
|
||||
'GROUP',
|
||||
group,
|
||||
consumer,
|
||||
'COUNT',
|
||||
count.toString(),
|
||||
'BLOCK',
|
||||
block.toString(),
|
||||
'STREAMS',
|
||||
stream,
|
||||
'>'
|
||||
);
|
||||
|
||||
if (result && result.length > 0) {
|
||||
for (const [streamName, messages] of result) {
|
||||
if (streamName === stream) {
|
||||
for (const [messageId, messageData] of messages) {
|
||||
const fields: Record<string, string> = {};
|
||||
|
||||
// Преобразуем массив [key, value, key, value] в объект
|
||||
for (let i = 0; i < messageData.length; i += 2) {
|
||||
fields[messageData[i]] = messageData[i + 1];
|
||||
}
|
||||
|
||||
try {
|
||||
await messageHandler({ messageId, fields });
|
||||
await this.acknowledgeMessage(stream, group, messageId);
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Error processing message ${messageId}: ${error.message}`);
|
||||
// Не подтверждаем сообщение при ошибке - оно останется в pending
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (this.consumers.get(consumerKey)) {
|
||||
this.logger.error(`Consumer ${consumerKey} error: ${error.message}`);
|
||||
// Небольшая пауза перед повторной попыткой
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(`Consumer stopped: ${consumerKey}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Подтверждение обработки сообщения
|
||||
*/
|
||||
async acknowledgeMessage(stream: string, group: string, messageId: string): Promise<void> {
|
||||
try {
|
||||
await this.redisClient.subscriber.xack(stream, group, messageId);
|
||||
this.logger.debug(`Message acknowledged: ${messageId} in stream ${stream}`);
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Error acknowledging message ${messageId} in stream ${stream}, group ${group}: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Остановка потребителя
|
||||
*/
|
||||
stopConsumer(stream: string, group: string, consumer: string): void {
|
||||
const consumerKey = `${stream}:${group}:${consumer}`;
|
||||
this.consumers.set(consumerKey, false);
|
||||
this.logger.log(`Stopping consumer: ${consumerKey}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение информации о pending сообщениях
|
||||
*/
|
||||
async getPendingMessages(stream: string, group: string): Promise<any> {
|
||||
try {
|
||||
return await this.redisClient.subscriber.xpending(stream, group);
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Error getting pending messages: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение информации о группах потребителей
|
||||
*/
|
||||
async getConsumerGroups(stream: string): Promise<any> {
|
||||
try {
|
||||
return await this.redisClient.subscriber.xinfo('GROUPS', stream);
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Error getting consumer groups: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
import { Module } from '@nestjs/common';
|
||||
import { RedisService } from './redis.service';
|
||||
import { RedisStreamService } from './redis-stream.service';
|
||||
import { RedisProvider } from './redis.provider';
|
||||
|
||||
@Module({
|
||||
providers: [RedisProvider, RedisService],
|
||||
exports: [RedisService], // Экспортируем RedisService для использования в других модулях
|
||||
providers: [RedisProvider, RedisService, RedisStreamService],
|
||||
exports: [RedisService, RedisStreamService], // Экспортируем оба сервиса
|
||||
})
|
||||
export class RedisModule {}
|
||||
|
||||
@@ -41,11 +41,6 @@ export const RedisProvider: Provider = {
|
||||
logConnectionStatus(redisSubscriber, 'Subscriber');
|
||||
logConnectionStatus(redisPublisher, 'Publisher');
|
||||
|
||||
// await Promise.all([
|
||||
// redisSubscriber.connect().catch((error) => logger.error(`Subscriber connect error: ${error.message}`)),
|
||||
// redisPublisher.connect().catch((error) => logger.error(`Publisher connect error: ${error.message}`)),
|
||||
// ]);
|
||||
|
||||
return {
|
||||
subscriber: redisSubscriber,
|
||||
publisher: redisPublisher,
|
||||
|
||||
@@ -9,7 +9,6 @@ import { WinstonLoggerService } from '../modules/logger/logger-app.service';
|
||||
|
||||
export interface Migration {
|
||||
name: string;
|
||||
isTest?: boolean; // Если true, то миграция запускается в тестовом режиме (без сохранения в БД)
|
||||
validUntil?: Date; // Дата в UTC, до которой миграция должна применяться. Если не указана или null, миграция применяется всегда
|
||||
up: (services: { blockchain: BlockchainService }) => Promise<boolean>;
|
||||
}
|
||||
@@ -146,9 +145,7 @@ export class MigrationManager {
|
||||
});
|
||||
}
|
||||
|
||||
async runMigration(migration: Migration, version: string, description: string): Promise<boolean> {
|
||||
const isTest = migration.isTest === true;
|
||||
|
||||
async runMigration(migration: Migration, version: string, description: string, isTest: boolean): Promise<boolean> {
|
||||
try {
|
||||
if (isTest) {
|
||||
logger.info(`[ТЕСТОВАЯ МИГРАЦИЯ] Запуск миграции ${version} (${description}): ${migration.name}`);
|
||||
@@ -158,7 +155,11 @@ export class MigrationManager {
|
||||
|
||||
// Предоставляем блокчейн-сервис в миграцию
|
||||
const result = await migration.up({ blockchain: this.blockchainService });
|
||||
await this.markMigrationAsApplied(version, migration.name, result, isTest);
|
||||
|
||||
// Отмечаем как примененную только если миграция успешна И не является тестовой
|
||||
if (result && !isTest) {
|
||||
await this.markMigrationAsApplied(version, migration.name, true, false);
|
||||
}
|
||||
|
||||
if (isTest) {
|
||||
logger.info(
|
||||
@@ -171,7 +172,12 @@ export class MigrationManager {
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error(`Ошибка при выполнении миграции ${version} (${description}):`, error);
|
||||
await this.markMigrationAsApplied(version, migration.name, false, isTest);
|
||||
|
||||
// Отмечаем как неуспешную только если это НЕ тестовая миграция
|
||||
if (!isTest) {
|
||||
await this.markMigrationAsApplied(version, migration.name, false, false);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -187,8 +193,8 @@ export class MigrationManager {
|
||||
// Вывод списка найденных миграций
|
||||
if (migrationFiles.length > 0) {
|
||||
logger.info('Список найденных миграций:');
|
||||
migrationFiles.forEach(({ version, description }) => {
|
||||
logger.info(`- ${version}: ${description}`);
|
||||
migrationFiles.forEach(({ version, description, filename }) => {
|
||||
logger.info(`- ${version}: ${description} (${filename})`);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -200,6 +206,15 @@ export class MigrationManager {
|
||||
const currentDate = new Date();
|
||||
|
||||
for (const { filename, version, description } of migrationFiles) {
|
||||
const isTest = filename.includes('__test');
|
||||
|
||||
// Обычные миграции пропускаем, если уже применялись
|
||||
if (!isTest && appliedVersions.has(version)) {
|
||||
logger.info(`Миграция ${version} (${description}) уже применена, пропускаем`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Грузим файл миграции
|
||||
const result = await this.loadMigration(filename);
|
||||
if (!result) {
|
||||
logger.warn(`Не удалось загрузить миграцию из файла ${filename}`);
|
||||
@@ -207,13 +222,6 @@ export class MigrationManager {
|
||||
}
|
||||
|
||||
const { migration } = result;
|
||||
const isTest = migration.isTest === true;
|
||||
|
||||
// Проверяем, была ли миграция уже применена успешно (для не тестовых миграций)
|
||||
if (!isTest && appliedVersions.has(version)) {
|
||||
logger.info(`Миграция ${version} (${description}) уже применена, пропускаем`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Проверяем дату валидности миграции
|
||||
if (migration.validUntil && currentDate > migration.validUntil) {
|
||||
@@ -225,13 +233,14 @@ export class MigrationManager {
|
||||
|
||||
// Если это тестовая миграция, сообщаем об этом
|
||||
if (isTest) {
|
||||
logger.info(`Миграция ${version} (${description}) запускается в ТЕСТОВОМ режиме`);
|
||||
logger.info(`Миграция ${version} (${description}) запускается в ТЕСТОВОМ режиме (по имени файла)`);
|
||||
}
|
||||
|
||||
// Запускаем миграцию
|
||||
const success = await this.runMigration(migration, version, description);
|
||||
const success = await this.runMigration(migration, version, description, isTest);
|
||||
|
||||
if (!success) {
|
||||
// Останавливаем процесс только при ошибке в НЕ тестовых миграциях
|
||||
if (!success && !isTest) {
|
||||
logger.error(`Миграция ${version} (${description}) завершена с ошибкой, останавливаем процесс`);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import mongoose, { Schema, model, type Model } from 'mongoose';
|
||||
import { toJSON, paginate } from './plugins/index';
|
||||
import AutoIncrementFactory from 'mongoose-sequence';
|
||||
import { generateOrderSecret } from '../services/order.service';
|
||||
import { type IOrder, orderStatus } from '../types/order.types';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import crypto from 'crypto';
|
||||
|
||||
const AutoIncrement = AutoIncrementFactory(mongoose);
|
||||
|
||||
export function generateOrderSecret(length = 16): string {
|
||||
return crypto.randomBytes(length).toString('hex'); // Генерирует случайный секрет в виде hex-строки
|
||||
}
|
||||
|
||||
interface IOrderModel extends Model<IOrder> {
|
||||
isEmailTaken(email: string, excludeUsername?: mongoose.Types.ObjectId): Promise<boolean>;
|
||||
paginate(filter, options): any;
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { InputType, Field } from '@nestjs/graphql';
|
||||
import { IsString, IsNotEmpty } from 'class-validator';
|
||||
import type { SearchPrivateAccountsInputDomainInterface } from '~/domain/common/interfaces/search-private-accounts-domain.interface';
|
||||
|
||||
@InputType('SearchPrivateAccountsInput')
|
||||
export class SearchPrivateAccountsInputDTO implements SearchPrivateAccountsInputDomainInterface {
|
||||
@Field(() => String, { description: 'Поисковый запрос для поиска приватных аккаунтов' })
|
||||
@IsString({ message: 'Поисковый запрос должен быть строкой' })
|
||||
@IsNotEmpty({ message: 'Поисковый запрос не может быть пустым' })
|
||||
query!: string;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { ObjectType, Field, createUnionType } from '@nestjs/graphql';
|
||||
import { IsString, IsArray, IsOptional, IsNumber } from 'class-validator';
|
||||
import { IndividualDTO } from '~/modules/common/dto/individual.dto';
|
||||
import { EntrepreneurDTO } from '~/modules/common/dto/entrepreneur.dto';
|
||||
import { OrganizationDTO } from '~/modules/common/dto/organization.dto';
|
||||
import type { PrivateAccountSearchResultDomainInterface } from '~/domain/common/interfaces/search-private-accounts-domain.interface';
|
||||
|
||||
// Создаем Union Type для GraphQL
|
||||
export const PrivateAccountSearchDataUnion = createUnionType({
|
||||
name: 'PrivateAccountSearchData',
|
||||
types: () => [IndividualDTO, EntrepreneurDTO, OrganizationDTO] as const,
|
||||
resolveType: (value) => {
|
||||
if (IndividualDTO.isTypeOf(value)) {
|
||||
return IndividualDTO;
|
||||
}
|
||||
if (EntrepreneurDTO.isTypeOf(value)) {
|
||||
return EntrepreneurDTO;
|
||||
}
|
||||
if (OrganizationDTO.isTypeOf(value)) {
|
||||
return OrganizationDTO;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
|
||||
@ObjectType('PrivateAccountSearchResult')
|
||||
export class PrivateAccountSearchResultDTO {
|
||||
@Field(() => String, { description: 'Тип аккаунта' })
|
||||
@IsString()
|
||||
type!: 'individual' | 'entrepreneur' | 'organization';
|
||||
|
||||
@Field(() => PrivateAccountSearchDataUnion, { description: 'Данные найденного аккаунта' })
|
||||
data!: IndividualDTO | EntrepreneurDTO | OrganizationDTO;
|
||||
|
||||
@Field(() => Number, { nullable: true, description: 'Оценка релевантности результата' })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
score?: number;
|
||||
|
||||
@Field(() => [String], { nullable: true, description: 'Поля, в которых найдены совпадения' })
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
highlightedFields?: string[];
|
||||
|
||||
constructor(entity: PrivateAccountSearchResultDomainInterface) {
|
||||
this.type = entity.type;
|
||||
this.score = entity.score;
|
||||
this.highlightedFields = entity.highlightedFields;
|
||||
|
||||
// Преобразуем доменные данные в соответствующий DTO
|
||||
switch (entity.type) {
|
||||
case 'individual':
|
||||
this.data = new IndividualDTO(entity.data as any);
|
||||
break;
|
||||
case 'entrepreneur':
|
||||
this.data = new EntrepreneurDTO(entity.data as any);
|
||||
break;
|
||||
case 'organization':
|
||||
this.data = new OrganizationDTO(entity.data as any);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Неизвестный тип аккаунта: ${entity.type}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,8 @@ import type { PaginationResultDomainInterface } from '~/domain/common/interfaces
|
||||
import { RegisterAccountInputDTO } from '../dto/register-account-input.dto';
|
||||
import { RegisteredAccountDTO } from '../dto/registered-account.dto';
|
||||
import { UpdateAccountInputDTO } from '../dto/update-account-input.dto';
|
||||
import { SearchPrivateAccountsInputDTO } from '../dto/search-private-accounts-input.dto';
|
||||
import { PrivateAccountSearchResultDTO } from '../dto/search-private-accounts-result.dto';
|
||||
|
||||
export const AccountsPaginationResult = createPaginationResult(AccountDTO, 'Accounts');
|
||||
|
||||
@@ -40,6 +42,19 @@ export class AccountResolver {
|
||||
return await this.accountService.getAccounts(data, options);
|
||||
}
|
||||
|
||||
@Query(() => [PrivateAccountSearchResultDTO], {
|
||||
name: 'searchPrivateAccounts',
|
||||
description:
|
||||
'Поиск приватных данных аккаунтов по запросу. Поиск осуществляется по полям ФИО, ИНН, ОГРН, наименованию организации и другим приватным данным.',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['chairman', 'member'])
|
||||
async searchPrivateAccounts(
|
||||
@Args('data', { type: () => SearchPrivateAccountsInputDTO }) data: SearchPrivateAccountsInputDTO
|
||||
): Promise<PrivateAccountSearchResultDTO[]> {
|
||||
return this.accountService.searchPrivateAccounts(data);
|
||||
}
|
||||
|
||||
@Mutation(() => RegisteredAccountDTO, {
|
||||
name: 'registerAccount',
|
||||
description: 'Зарегистрировать аккаунт пользователя в системе',
|
||||
|
||||
@@ -8,6 +8,8 @@ import type { RegisterAccountInputDTO } from '../dto/register-account-input.dto'
|
||||
import { RegisteredAccountDTO } from '../dto/registered-account.dto';
|
||||
import type { DeleteAccountInputDTO } from '../dto/delete-account-input.dto';
|
||||
import type { UpdateAccountInputDTO } from '../dto/update-account-input.dto';
|
||||
import type { SearchPrivateAccountsInputDTO } from '../dto/search-private-accounts-input.dto';
|
||||
import { PrivateAccountSearchResultDTO } from '../dto/search-private-accounts-result.dto';
|
||||
|
||||
@Injectable()
|
||||
export class AccountService {
|
||||
@@ -44,4 +46,15 @@ export class AccountService {
|
||||
const result = await this.accountDomainInteractor.registerAccount(data);
|
||||
return new RegisteredAccountDTO(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Поиск приватных аккаунтов
|
||||
* @param data Входные данные для поиска
|
||||
* @returns Массив результатов поиска
|
||||
*/
|
||||
public async searchPrivateAccounts(data: SearchPrivateAccountsInputDTO): Promise<PrivateAccountSearchResultDTO[]> {
|
||||
const results = await this.accountDomainInteractor.searchPrivateAccounts(data);
|
||||
|
||||
return results.map((result) => new PrivateAccountSearchResultDTO(result));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,10 +14,11 @@ import { DecisionModule } from './free-decision/decision.module';
|
||||
import { AgreementModule } from './agreement/agreement.module';
|
||||
import { ParticipantModule } from './participant/participant.module';
|
||||
import { AgendaModule } from './agenda/agenda.module';
|
||||
import { PaymentModule } from './payment/payment.module';
|
||||
import { CooplaceModule } from './cooplace/cooplace.module';
|
||||
import { DesktopModule } from './desktop/desktop.module';
|
||||
import { MeetModule } from './meet/meet.module';
|
||||
import { GatewayModule } from './gateway/gateway.module';
|
||||
import { WalletModule } from './wallet/wallet.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -32,13 +33,14 @@ import { MeetModule } from './meet/meet.module';
|
||||
PaymentMethodModule,
|
||||
QueueModule,
|
||||
RedisAppModule,
|
||||
PaymentModule,
|
||||
SystemModule,
|
||||
DocumentModule,
|
||||
DecisionModule,
|
||||
ParticipantModule,
|
||||
CooplaceModule,
|
||||
MeetModule,
|
||||
GatewayModule,
|
||||
WalletModule,
|
||||
],
|
||||
exports: [
|
||||
AccountModule,
|
||||
@@ -50,7 +52,6 @@ import { MeetModule } from './meet/meet.module';
|
||||
DesktopModule,
|
||||
LoggerModule,
|
||||
PaymentMethodModule,
|
||||
PaymentModule,
|
||||
QueueModule,
|
||||
RedisAppModule,
|
||||
SystemModule,
|
||||
@@ -59,6 +60,8 @@ import { MeetModule } from './meet/meet.module';
|
||||
ParticipantModule,
|
||||
CooplaceModule,
|
||||
MeetModule,
|
||||
GatewayModule,
|
||||
WalletModule,
|
||||
],
|
||||
})
|
||||
export class ApplicationModule {}
|
||||
|
||||
+6
-1
@@ -5,7 +5,7 @@ import { MetaDocumentInputDTO } from '~/modules/document/dto/meta-document-input
|
||||
import { SignedDigitalDocumentInputDTO } from '~/modules/document/dto/signed-digital-document-input.dto';
|
||||
import type { ExcludeCommonProps } from '~/modules/document/types';
|
||||
import { Type } from 'class-transformer';
|
||||
import { IsArray, IsObject, ValidateNested } from 'class-validator';
|
||||
import { IsArray, IsBoolean, IsNotEmpty, IsObject, ValidateNested } from 'class-validator';
|
||||
import { AgendaMeetDTO } from '~/modules/meet/dto/agenda-meet.dto';
|
||||
import { AgendaGeneralMeetQuestionDTO } from '~/modules/meet/dto/agenda-general-meet-question.dto';
|
||||
|
||||
@@ -25,6 +25,11 @@ class BaseAnnualGeneralMeetingAgendaMetaDocumentInputDTO implements ExcludeCommo
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => AgendaGeneralMeetQuestionDTO)
|
||||
questions!: AgendaGeneralMeetQuestionDTO[];
|
||||
|
||||
@Field(() => Boolean, { description: 'Флаг повторного собрания' })
|
||||
@IsBoolean()
|
||||
@IsNotEmpty()
|
||||
is_repeated!: boolean;
|
||||
}
|
||||
|
||||
@InputType(`AnnualGeneralMeetingAgendaGenerateDocumentInput`)
|
||||
|
||||
+6
-11
@@ -4,7 +4,7 @@ import { GenerateMetaDocumentInputDTO } from '~/modules/document/dto/generate-me
|
||||
import { MetaDocumentInputDTO } from '~/modules/document/dto/meta-document-input.dto';
|
||||
import { SignedDigitalDocumentInputDTO } from '~/modules/document/dto/signed-digital-document-input.dto';
|
||||
import type { ExcludeCommonProps } from '~/modules/document/types';
|
||||
import { IsNotEmpty, IsNumber, IsString } from 'class-validator';
|
||||
import { IsBoolean, IsNotEmpty, IsNumber, IsString } from 'class-validator';
|
||||
|
||||
// интерфейс параметров для генерации
|
||||
type action = Cooperative.Registry.AnnualGeneralMeetingSovietDecision.Action;
|
||||
@@ -20,6 +20,11 @@ class BaseAnnualGeneralMeetingSovietDecisionDocumentInputDTO implements ExcludeC
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
meet_hash!: string;
|
||||
|
||||
@Field(() => Boolean, { description: 'Флаг повторного собрания' })
|
||||
@IsBoolean()
|
||||
@IsNotEmpty()
|
||||
is_repeated!: boolean;
|
||||
}
|
||||
|
||||
@InputType(`AnnualGeneralMeetingSovietDecisionGenerateDocumentInput`)
|
||||
@@ -31,16 +36,6 @@ export class AnnualGeneralMeetingSovietDecisionGenerateDocumentInputDTO
|
||||
implements action
|
||||
{
|
||||
registry_id!: number;
|
||||
|
||||
@Field(() => Number, { description: 'ID решения совета' })
|
||||
@IsNumber()
|
||||
@IsNotEmpty()
|
||||
decision_id!: number;
|
||||
|
||||
@Field(() => String, { description: 'Хеш собрания' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
meet_hash!: string;
|
||||
}
|
||||
|
||||
@InputType(`AnnualGeneralMeetingSovietDecisionSignedMetaDocumentInput`)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user