Compare commits
59 Commits
main
...
feat/ku-selforg
| Author | SHA1 | Date | |
|---|---|---|---|
| 8a59d3e83e | |||
| de10e16bae | |||
| 987600dd38 | |||
| ff1965376d | |||
| 07fb4599e6 | |||
| 686cd0cff2 | |||
| ef96b4c81c | |||
| f8b190bf53 | |||
| 7f5f9867d9 | |||
| c4fa1fecc2 | |||
| fbf6d98abc | |||
| 6d970bc0fe | |||
| 58d7bc52bf | |||
| 72bd5d96d0 | |||
| b22fbe1845 | |||
| 93239d71f0 | |||
| 3b85a2b783 | |||
| e55622636d | |||
| 1ccb72c75f | |||
| 74a19d7e0e | |||
| 78822f3d6d | |||
| 6d89d96ad3 | |||
| 8b4a8063bb | |||
| f1a0555056 | |||
| 37069af6ca | |||
| 11f1270eb0 | |||
| 359643f4ce | |||
| 90b2caf6cb | |||
| b30cf4cdcc | |||
| 248375a1bf | |||
| 1fcaa242bc | |||
| 93dec39cd0 | |||
| c0e818e44c | |||
| ba9d192622 | |||
| 577975e47c | |||
| a11c9135ed | |||
| ffa71e4b80 | |||
| 67eeac5563 | |||
| 57f2ef43e0 | |||
| e35e74cd11 | |||
| 2e54f7a9ac | |||
| 68db00e8d4 | |||
| b2286167f0 | |||
| 051a56c3d3 | |||
| 6cc408fd52 | |||
| cabe4a8ed8 | |||
| f8ea5eed91 | |||
| 64006e89c8 | |||
| afcdea2770 | |||
| c43f501d00 | |||
| c1da60bb37 | |||
| dd76c5587d | |||
| 769d1db5a0 | |||
| 2fc99acde2 | |||
| b3e308b0c4 | |||
| ec582d7d1a | |||
| 28f28ae9d4 | |||
| 36dcd59428 | |||
| 5335ff3049 |
Vendored
+1
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"cSpell.enabled": false,
|
||||
"editor.bracketPairColorization.enabled": true,
|
||||
"editor.guides.bracketPairs": true,
|
||||
"editor.tabCompletion": "onlySnippets",
|
||||
|
||||
@@ -5,6 +5,25 @@
|
||||
#include "src/deletebranch.cpp"
|
||||
#include "src/deltrusted.cpp"
|
||||
#include "src/editbranch.cpp"
|
||||
#include "src/setprivate.cpp"
|
||||
#include "src/addwhite.cpp"
|
||||
#include "src/delwhite.cpp"
|
||||
#include "src/createdec.cpp"
|
||||
#include "src/joindec.cpp"
|
||||
#include "src/startdec.cpp"
|
||||
#include "src/votedec.cpp"
|
||||
#include "src/closedec.cpp"
|
||||
#include "src/exec.cpp"
|
||||
#include "src/confirmdec.cpp"
|
||||
#include "src/declinedec.cpp"
|
||||
#include "src/canceldec.cpp"
|
||||
#include "src/apprliab.cpp"
|
||||
#include "src/declliab.cpp"
|
||||
#include "src/apprauth.cpp"
|
||||
#include "src/declauth.cpp"
|
||||
#include "src/reqtrusted.cpp"
|
||||
#include "src/apprtrusted.cpp"
|
||||
#include "src/decltrusted.cpp"
|
||||
|
||||
using namespace eosio;
|
||||
|
||||
|
||||
@@ -63,6 +63,43 @@ public:
|
||||
[[eosio::action]] void deletebranch(eosio::name coopname, eosio::name braname);
|
||||
[[eosio::action]] void addtrusted(eosio::name coopname, eosio::name braname, eosio::name trusted);
|
||||
[[eosio::action]] void deltrusted(eosio::name coopname, eosio::name braname, eosio::name trusted);
|
||||
|
||||
|
||||
|
||||
// Приватность кооперативного участка и управление белым списком (председатель совета).
|
||||
// Приватный участок нельзя выбрать (selectbranch), если аккаунт не в белом списке участка.
|
||||
[[eosio::action]] void setprivate(eosio::name coopname, eosio::name braname, bool is_private);
|
||||
[[eosio::action]] void addwhite(eosio::name coopname, eosio::name braname, eosio::name account);
|
||||
[[eosio::action]] void delwhite(eosio::name coopname, eosio::name braname, eosio::name account);
|
||||
|
||||
// Универсальный механизм «собрание → решение» (L1)
|
||||
[[eosio::action]] void createdec(eosio::name coopname, eosio::checksum256 hash, eosio::name type, eosio::name initiator, document2 proposal, eosio::name braname, std::vector<decision_point> agenda);
|
||||
[[eosio::action]] void joindec(eosio::name coopname, eosio::checksum256 hash, eosio::name username);
|
||||
[[eosio::action]] void startdec(eosio::name coopname, eosio::checksum256 hash, eosio::name chairman, std::string address, std::vector<decision_point> agenda);
|
||||
[[eosio::action]] void votedec(eosio::name coopname, eosio::checksum256 hash, eosio::name username, document2 ballot, std::vector<decision_vote_point> votes);
|
||||
[[eosio::action]] void closedec(eosio::name coopname, eosio::checksum256 hash, document2 protocol);
|
||||
|
||||
// Расширение автоматизируемого решения (L2): создание кооперативного участка через совет.
|
||||
// Председатель участка подписывает пакетом заявление, договор о полной материальной
|
||||
// ответственности (liability) и доверенность председателю участка (authority).
|
||||
[[eosio::action]] void exec(eosio::name coopname, eosio::checksum256 hash, document2 petition, document2 liability, document2 authority);
|
||||
[[eosio::action]] void confirmdec(eosio::name coopname, eosio::checksum256 hash, document2 authorization);
|
||||
[[eosio::action]] void declinedec(eosio::name coopname, eosio::checksum256 hash, std::string reason);
|
||||
[[eosio::action]] void canceldec(eosio::name coopname, eosio::checksum256 hash, std::string reason);
|
||||
|
||||
// Договор о полной материальной ответственности председателя КУ: встречная подпись председателя совета
|
||||
// (callback'и одобрения совета; отклонение заблокировано — решение об учреждении участка уже принято)
|
||||
[[eosio::action]] void apprliab(eosio::name coopname, eosio::name username, eosio::checksum256 approval_hash, document2 approved_document);
|
||||
[[eosio::action]] void declliab(eosio::name coopname, eosio::name username, eosio::checksum256 approval_hash, std::string reason);
|
||||
|
||||
// Доверенность председателя КУ: встречная подпись председателя совета
|
||||
// (отдельное одобрение совета рядом с договором матответственности; отклонение заблокировано)
|
||||
[[eosio::action]] void apprauth(eosio::name coopname, eosio::name username, eosio::checksum256 approval_hash, document2 approved_document);
|
||||
[[eosio::action]] void declauth(eosio::name coopname, eosio::name username, eosio::checksum256 approval_hash, std::string reason);
|
||||
|
||||
// Доверенные лица по заявлению: договор о полной материальной ответственности (application)
|
||||
// и доверенность доверенному лицу (authority) — оба со встречной подписью председателя участка
|
||||
[[eosio::action]] void reqtrusted(eosio::name coopname, eosio::name braname, eosio::name username, eosio::checksum256 hash, document2 application, document2 authority);
|
||||
[[eosio::action]] void apprtrusted(eosio::name coopname, eosio::checksum256 hash, document2 countersigned, document2 countersigned_authority);
|
||||
[[eosio::action]] void decltrusted(eosio::name coopname, eosio::checksum256 hash, std::string reason);
|
||||
|
||||
|
||||
};
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Стандарт «Собрание пайщиков и решение участка».
|
||||
#
|
||||
# Контракт branch ведёт универсальный механизм собраний пайщиков: от объявления
|
||||
# собрания и присоединения участников до голосования бюллетенями и утверждения
|
||||
# протокола председателем собрания. Свободное решение фиксируется протоколом и
|
||||
# на этом завершается. Решение об учреждении кооперативного участка дополнительно
|
||||
# выносится на рассмотрение совета и по его утверждению создаёт участок.
|
||||
#
|
||||
# Источники в коде:
|
||||
# • cpp/branch/src/createdec.cpp — объявление собрания и повестки
|
||||
# • cpp/branch/src/joindec.cpp — присоединение участника
|
||||
# • cpp/branch/src/startdec.cpp — открытие голосования (с назначением председателя)
|
||||
# • cpp/branch/src/votedec.cpp — бюллетень участника
|
||||
# • cpp/branch/src/closedec.cpp — утверждение протокола председателем
|
||||
# • cpp/branch/src/exec.cpp — заявление в совет (для учреждения участка)
|
||||
# • cpp/branch/src/confirmdec.cpp — учреждение участка по решению совета
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
process_type: brn.decide
|
||||
id: public_branch_decide_process
|
||||
title: Собрание пайщиков и решение участка
|
||||
slug: decide
|
||||
status: proposed
|
||||
contract: branch
|
||||
summary: >
|
||||
От объявления собрания пайщиков и присоединения участников до голосования
|
||||
бюллетенями и утверждения протокола председателем. Свободное решение
|
||||
фиксируется протоколом; решение об учреждении кооперативного участка
|
||||
выносится на совет и по его утверждению создаёт участок.
|
||||
purpose: >
|
||||
«Собрание пайщиков и решение участка» — способ для пайщиков собраться в
|
||||
любой момент и принять решение по своей повестке. Инициатор объявляет
|
||||
собрание, желающие присоединяются, председатель собрания открывает
|
||||
голосование, участники голосуют бюллетенями (за / против / воздержался по
|
||||
каждому вопросу), председатель утверждает протокол. Свободное решение на
|
||||
этом завершается как документ. Решение об учреждении кооперативного участка
|
||||
председатель направляет на рассмотрение совета; по утверждению советом
|
||||
участок создаётся с указанными реквизитами.
|
||||
|
||||
roles:
|
||||
- participant # Пайщик-инициатор / участник / голосующий
|
||||
- chairman # Председатель собрания (для учреждения участка — будущий председатель участка)
|
||||
- soviet # Совет — утверждение учреждения участка
|
||||
|
||||
# ── Действия контракта ───────────────────────────────────────────────────────
|
||||
actions:
|
||||
- name: branch::createdec
|
||||
human: Объявить собрание
|
||||
actor: Пайщик
|
||||
role: opener
|
||||
purpose: >
|
||||
Инициатор формирует повестку дня и подписывает предложение о проведении
|
||||
собрания. Для учреждения кооперативного участка повестка включает вопросы
|
||||
об организации участка, адресе привязки и избрании председателя. Собрание
|
||||
открывается для присоединения участников.
|
||||
|
||||
- name: branch::joindec
|
||||
human: Присоединиться к собранию
|
||||
actor: Пайщик
|
||||
role: progress
|
||||
purpose: >
|
||||
Пайщик входит в состав участников собрания.
|
||||
|
||||
- name: branch::startdec
|
||||
human: Открыть голосование
|
||||
actor: Пайщик
|
||||
role: progress
|
||||
purpose: >
|
||||
Организатор собрания — он же председатель собрания по праву его создания —
|
||||
открывает голосование, указывая избираемого собранием председателя
|
||||
кооперативного участка из числа присоединившихся участников, а также
|
||||
адрес привязки участка, определённый собранием. Повестку в этот момент
|
||||
можно дополнить вопросами, внесёнными участниками на собрании. Открыть
|
||||
голосование можно только при наличии не менее трёх участников собрания —
|
||||
иначе кворум недостижим и собрание можно лишь отменить. Окно голосования
|
||||
отмеряется автоматически и составляет 15 минут — голосование проходит прямо
|
||||
на собрании.
|
||||
|
||||
- name: branch::votedec
|
||||
human: Подать бюллетень
|
||||
actor: Пайщик
|
||||
role: progress
|
||||
purpose: >
|
||||
Участник подаёт подписанный бюллетень с волеизъявлением (за / против /
|
||||
воздержался) по каждому вопросу повестки. Подсчёт голосов ведёт система.
|
||||
Бюллетень публикуется в реестре документов и привязывается к собранию.
|
||||
|
||||
- name: branch::closedec
|
||||
human: Утвердить протокол
|
||||
actor: Пайщик
|
||||
role: closer
|
||||
purpose: >
|
||||
Председатель собрания закрывает голосование и утверждает протокол решения
|
||||
своей подписью. Протокол публикуется в реестре документов и завершает
|
||||
пакет документов собрания (бюллетени и протокол собираются вместе).
|
||||
Свободное решение на этом завершается. Решение об учреждении участка
|
||||
переходит к исполнению.
|
||||
|
||||
- name: branch::exec
|
||||
human: Направить заявление в совет
|
||||
actor: Пайщик
|
||||
role: progress
|
||||
purpose: >
|
||||
Председатель собрания формирует заявление в совет об учреждении
|
||||
кооперативного участка; к нему прилагаются протокол собрания и бюллетени.
|
||||
Вопрос выносится на рассмотрение совета.
|
||||
links:
|
||||
- process_type: sov.authpkg
|
||||
label: Типовой процесс решения совета
|
||||
|
||||
- name: branch::confirmdec
|
||||
human: Учредить участок по решению совета
|
||||
actor: Совет
|
||||
role: closer
|
||||
purpose: >
|
||||
По утверждению советом кооперативный участок создаётся с указанными
|
||||
реквизитами (наименование, адрес привязки, избранный председатель).
|
||||
|
||||
- name: branch::declinedec
|
||||
human: Отклонить решением совета
|
||||
actor: Совет
|
||||
role: rollback
|
||||
purpose: >
|
||||
При отказе совета вопрос об учреждении участка снимается; причина
|
||||
фиксируется в журнале.
|
||||
|
||||
- name: branch::canceldec
|
||||
human: Отменить собрание
|
||||
actor: Пайщик
|
||||
role: rollback
|
||||
purpose: >
|
||||
Инициатор сворачивает собрание до вынесения вопроса на совет.
|
||||
|
||||
operations: []
|
||||
@@ -0,0 +1,61 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Стандарт «Приём доверенного лица кооперативного участка».
|
||||
#
|
||||
# Контракт branch ведёт приём доверенных лиц участка по заявлению: пайщик
|
||||
# подаёт заявление и договор о полной материальной ответственности, председатель
|
||||
# участка одобряет встречной подписью, и пайщик становится доверенным лицом.
|
||||
#
|
||||
# Источники в коде:
|
||||
# • cpp/branch/src/reqtrusted.cpp — подача заявки
|
||||
# • cpp/branch/src/apprtrusted.cpp — одобрение председателем участка
|
||||
# • cpp/branch/src/decltrusted.cpp — отклонение председателем участка
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
process_type: brn.trust
|
||||
id: public_branch_trust_process
|
||||
title: Приём доверенного лица кооперативного участка
|
||||
slug: trust
|
||||
status: proposed
|
||||
contract: branch
|
||||
summary: >
|
||||
Пайщик подаёт заявление и договор о полной материальной ответственности;
|
||||
председатель кооперативного участка одобряет встречной подписью, и пайщик
|
||||
входит в состав доверенных лиц участка (не более трёх).
|
||||
purpose: >
|
||||
«Приём доверенного лица кооперативного участка» позволяет пайщику стать
|
||||
доверенным лицом участка по заявлению. Заявитель подписывает заявление и
|
||||
договор о полной материальной ответственности; председатель участка
|
||||
рассматривает заявку и одобряет её встречной подписью на договоре, после
|
||||
чего пайщик получает права доверенного лица.
|
||||
|
||||
roles:
|
||||
- participant # Пайщик-заявитель
|
||||
- chairman # Председатель кооперативного участка
|
||||
|
||||
# ── Действия контракта ───────────────────────────────────────────────────────
|
||||
actions:
|
||||
- name: branch::reqtrusted
|
||||
human: Подать заявку доверенного
|
||||
actor: Пайщик
|
||||
role: opener
|
||||
purpose: >
|
||||
Пайщик подаёт заявку на приём доверенным лицом выбранного участка, прилагая
|
||||
подписанные заявление и договор о полной материальной ответственности.
|
||||
|
||||
- name: branch::apprtrusted
|
||||
human: Одобрить доверенного
|
||||
actor: Председатель участка
|
||||
role: closer
|
||||
purpose: >
|
||||
Председатель участка одобряет заявку встречной подписью на договоре
|
||||
материальной ответственности; пайщик входит в состав доверенных лиц
|
||||
участка.
|
||||
|
||||
- name: branch::decltrusted
|
||||
human: Отклонить заявку доверенного
|
||||
actor: Председатель участка
|
||||
role: rollback
|
||||
purpose: >
|
||||
Председатель участка отклоняет заявку; причина фиксируется в журнале.
|
||||
|
||||
operations: []
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* @brief Добавление пайщика в белый список кооперативного участка.
|
||||
* Пайщики из белого списка могут выбрать приватный кооперативный участок при вступлении
|
||||
* или смене участка. В белый список можно добавить любого пайщика кооператива.
|
||||
* @param coopname Наименование кооператива
|
||||
* @param braname Наименование кооперативного участка
|
||||
* @param account Аккаунт пайщика для добавления
|
||||
* @ingroup public_actions
|
||||
* @ingroup public_branch_actions
|
||||
|
||||
* @note Авторизация требуется от аккаунта: @p coopname
|
||||
*/
|
||||
[[eosio::action]] void branch::addwhite(eosio::name coopname, eosio::name braname, eosio::name account) {
|
||||
check_auth_or_fail(_branch, coopname, coopname, "addwhite"_n);
|
||||
|
||||
branch_index branches(_branch, coopname.value);
|
||||
auto branch = branches.find(braname.value);
|
||||
eosio::check(branch != branches.end(), "Кооперативный участок не найден");
|
||||
|
||||
get_account_or_fail(account);
|
||||
|
||||
branches.modify(branch, coopname, [&](auto &b) {
|
||||
b.add_account_to_whitelist(account);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* @brief Встречная подпись председателя совета на доверенности председателя
|
||||
* кооперативного участка.
|
||||
* Callback одобрения совета: председатель совета подписал доверенность второй
|
||||
* подписью (механизм soviet::confirmapprv проверил его подпись). Фиксируем
|
||||
* полностью подписанную доверенность в реестре документов по якорю одобрения.
|
||||
* @param coopname Наименование кооператива
|
||||
* @param username Председатель кооперативного участка — уполномоченный по доверенности
|
||||
* @param approval_hash Якорь одобрения (хэш документа доверенности)
|
||||
* @param approved_document Доверенность с встречной подписью председателя совета
|
||||
* @ingroup public_actions
|
||||
* @ingroup public_branch_actions
|
||||
|
||||
* @note Авторизация требуется от аккаунта: @p soviet
|
||||
*/
|
||||
[[eosio::action]] void branch::apprauth(eosio::name coopname, eosio::name username, eosio::checksum256 approval_hash, document2 approved_document) {
|
||||
require_auth(_soviet);
|
||||
|
||||
// Доверенность подписана обеими сторонами (председатель участка + председатель совета) —
|
||||
// фиксируем её в реестре документов как принятую.
|
||||
::Soviet::make_complete_document(_branch, coopname, username, "branchauth"_n, approval_hash, approved_document);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* @brief Встречная подпись председателя совета на договоре о полной материальной
|
||||
* ответственности председателя кооперативного участка.
|
||||
* Callback одобрения совета: председатель совета подписал договор второй подписью
|
||||
* (механизм soviet::confirmapprv проверил его подпись). Фиксируем полностью
|
||||
* подписанный договор в реестре документов по якорю процесса.
|
||||
* @param coopname Наименование кооператива
|
||||
* @param username Председатель кооперативного участка — сторона «Исполнитель»
|
||||
* @param approval_hash Якорь процесса (хэш одобрения)
|
||||
* @param approved_document Договор с встречной подписью председателя совета
|
||||
* @ingroup public_actions
|
||||
* @ingroup public_branch_actions
|
||||
|
||||
* @note Авторизация требуется от аккаунта: @p soviet
|
||||
*/
|
||||
[[eosio::action]] void branch::apprliab(eosio::name coopname, eosio::name username, eosio::checksum256 approval_hash, document2 approved_document) {
|
||||
require_auth(_soviet);
|
||||
|
||||
// Договор подписан обеими сторонами (председатель участка + председатель совета) —
|
||||
// фиксируем его в реестре документов как принятый.
|
||||
::Soviet::make_complete_document(_branch, coopname, username, "branchliab"_n, approval_hash, approved_document);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* @brief Одобрение заявки доверенного лица председателем участка.
|
||||
* Председатель кооперативного участка накладывает встречную подпись на договор
|
||||
* материальной ответственности и доверенность заявителя; оба полностью подписанных
|
||||
* документа фиксируются в реестре документов, пайщик добавляется в список доверенных
|
||||
* лиц участка, заявка стирается.
|
||||
* @param coopname Наименование кооператива
|
||||
* @param hash Внешний идентификатор заявки
|
||||
* @param countersigned Договор материальной ответственности со встречной подписью председателя участка
|
||||
* @param countersigned_authority Доверенность доверенному лицу со встречной подписью председателя участка
|
||||
* @ingroup public_actions
|
||||
* @ingroup public_branch_actions
|
||||
|
||||
* @note Авторизация требуется от аккаунта: @p coopname
|
||||
*/
|
||||
[[eosio::action]] void branch::apprtrusted(eosio::name coopname, eosio::checksum256 hash, document2 countersigned, document2 countersigned_authority) {
|
||||
check_auth_or_fail(_branch, coopname, coopname, "apprtrusted"_n);
|
||||
|
||||
verify_document_or_fail(countersigned);
|
||||
verify_document_or_fail(countersigned_authority);
|
||||
|
||||
auto req = get_trustreq_or_fail(coopname, hash);
|
||||
|
||||
auto br = get_branch_or_fail(coopname, req.braname);
|
||||
eosio::check(!br.is_account_in_trusted(req.username), "Пайщик уже является доверенным лицом участка");
|
||||
eosio::check(br.trusted.size() < 3, "Достигнут предел доверенных лиц участка (не более трёх)");
|
||||
|
||||
branch_index branches(_branch, coopname.value);
|
||||
auto bitr = branches.find(req.braname.value);
|
||||
branches.modify(bitr, coopname, [&](auto &row) {
|
||||
row.add_account_to_trusted(req.username);
|
||||
});
|
||||
|
||||
// Оба документа подписаны обеими сторонами (доверенное лицо + председатель участка) —
|
||||
// фиксируем их в реестре документов как принятые.
|
||||
::Soviet::make_complete_document(_branch, coopname, req.username, "trustliab"_n, hash, countersigned);
|
||||
::Soviet::make_complete_document(_branch, coopname, req.username, "trustauth"_n, hash, countersigned_authority);
|
||||
|
||||
trustreq_index trustreqs(_branch, coopname.value);
|
||||
auto ritr = trustreqs.find(req.id);
|
||||
trustreqs.erase(ritr);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* @brief Отмена собрания инициатором до вывода на совет.
|
||||
* Инициатор сворачивает собрание, пока решение не ушло в совет; запись и вопросы
|
||||
* повестки стираются, причина передаётся аргументом (история — в журнале действий).
|
||||
* @param coopname Наименование кооператива
|
||||
* @param hash Якорь процесса
|
||||
* @param reason Причина отмены
|
||||
* @ingroup public_actions
|
||||
* @ingroup public_branch_actions
|
||||
|
||||
* @note Авторизация требуется от аккаунта: @p coopname
|
||||
*/
|
||||
[[eosio::action]] void branch::canceldec(eosio::name coopname, eosio::checksum256 hash, std::string reason) {
|
||||
check_auth_or_fail(_branch, coopname, coopname, "canceldec"_n);
|
||||
|
||||
auto dec = get_decision_or_fail(coopname, hash);
|
||||
eosio::check(dec.status != "onapproval"_n, "Решение уже на рассмотрении совета — отмена недоступна");
|
||||
|
||||
decision_index decisions(_branch, coopname.value);
|
||||
auto itr = decisions.find(dec.id);
|
||||
erase_coodecquests(coopname, dec.id);
|
||||
decisions.erase(itr);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* @brief Закрытие собрания и утверждение протокола председателем.
|
||||
* Председатель закрывает голосование и утверждает протокол одной своей подписью
|
||||
* (секретарь не нужен — подсчёт ведёт контракт). Проверяется кворум и итог
|
||||
* голосования. Для типа "free" — терминал: запись стирается (история — в журнале).
|
||||
* Для "createbranch" — решение переходит в статус "approved" в ожидании исполнения
|
||||
* действием exec (вывод вопроса на совет).
|
||||
* @param coopname Наименование кооператива
|
||||
* @param hash Якорь процесса
|
||||
* @param protocol Подписанный председателем протокол решения
|
||||
* @ingroup public_actions
|
||||
* @ingroup public_branch_actions
|
||||
|
||||
* @note Авторизация требуется от аккаунта: @p coopname
|
||||
*/
|
||||
[[eosio::action]] void branch::closedec(eosio::name coopname, eosio::checksum256 hash, document2 protocol) {
|
||||
check_auth_or_fail(_branch, coopname, coopname, "closedec"_n);
|
||||
|
||||
verify_document_or_fail(protocol);
|
||||
|
||||
auto dec = get_decision_or_fail(coopname, hash);
|
||||
eosio::check(dec.status == "voting"_n, "Решение не находится в стадии голосования");
|
||||
|
||||
// Кворум: число поданных бюллетеней не меньше минимального
|
||||
eosio::check(dec.signed_ballots >= MIN_DECISION_QUORUM, "Не набран минимальный кворум собрания");
|
||||
|
||||
// Закрыть может только по истечении окна голосования либо когда проголосовали все участники
|
||||
auto now = current_time_point();
|
||||
bool window_passed = now.sec_since_epoch() > dec.close_at.sec_since_epoch();
|
||||
bool all_voted = dec.signed_ballots >= dec.participants.size();
|
||||
eosio::check(window_passed || all_voted, "Голосование ещё идёт");
|
||||
|
||||
// Протокол собрания пайщиков завершает ПАКЕТ СОБРАНИЯ в реестре — его якорь = хэш
|
||||
// предложения/повестки (proposal.hash), к нему же привязаны бюллетени (см. votedec).
|
||||
// Пакет появляется в реестре сразу при закрытии собрания. Отправка в совет (exec) —
|
||||
// ОТДЕЛЬНЫЙ пакет на хэше процесса (hash), чтобы документы собрания и документы совета
|
||||
// не сваливались в одну кашу под общим якорем.
|
||||
Soviet::make_complete_document(_branch, coopname, dec.initiator, get_valid_soviet_action("branchdec"_n), dec.proposal.hash, protocol);
|
||||
|
||||
if (dec.type == "free"_n) {
|
||||
// Свободное решение зафиксировано протоколом и завершается (история — в журнале действий)
|
||||
decision_index decisions(_branch, coopname.value);
|
||||
auto itr = decisions.find(dec.id);
|
||||
erase_coodecquests(coopname, dec.id);
|
||||
decisions.erase(itr);
|
||||
return;
|
||||
}
|
||||
|
||||
// Автоматизируемое решение — переходит в ожидание исполнения (exec → совет)
|
||||
decision_index decisions(_branch, coopname.value);
|
||||
auto itr = decisions.find(dec.id);
|
||||
decisions.modify(itr, coopname, [&](auto &d) {
|
||||
d.status = "approved"_n;
|
||||
d.protocol = protocol;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* @brief Подтверждение решения советом — создание кооперативного участка.
|
||||
* Callback совета: по утверждению учреждается кооперативный участок с реквизитами
|
||||
* из решения (аккаунт участка, избранный председатель), после чего запись решения
|
||||
* стирается (история — в журнале действий).
|
||||
* @param coopname Наименование кооператива
|
||||
* @param hash Якорь процесса
|
||||
* @param authorization Документ решения совета
|
||||
* @ingroup public_actions
|
||||
* @ingroup public_branch_actions
|
||||
|
||||
* @note Авторизация требуется от аккаунта: @p soviet
|
||||
*/
|
||||
[[eosio::action]] void branch::confirmdec(eosio::name coopname, eosio::checksum256 hash, document2 authorization) {
|
||||
require_auth(_soviet);
|
||||
|
||||
auto dec = get_decision_or_fail(coopname, hash);
|
||||
eosio::check(dec.type == "createbranch"_n, "Решение не относится к созданию участка");
|
||||
eosio::check(dec.status == "onapproval"_n, "Решение не находится на утверждении совета");
|
||||
|
||||
auto coop = get_cooperative_or_fail(coopname);
|
||||
|
||||
auto authorizer_account = get_account_or_fail(dec.chairman);
|
||||
eosio::check(authorizer_account.type == "individual"_n, "Только физическое лицо может быть назначено председателем кооперативного участка");
|
||||
|
||||
// Создаём кооперативный участок с избранным председателем
|
||||
branch_index branches(_branch, coopname.value);
|
||||
|
||||
// председатель может возглавлять только один кооперативный участок
|
||||
auto branches_by_trustee = branches.get_index<"bytrustee"_n>();
|
||||
eosio::check(branches_by_trustee.find(dec.chairman.value) == branches_by_trustee.end(),
|
||||
"Избранный председатель уже является председателем другого кооперативного участка");
|
||||
|
||||
branches.emplace(coopname, [&](auto &row) {
|
||||
row.braname = dec.braname;
|
||||
row.trustee = dec.chairman;
|
||||
});
|
||||
|
||||
action(
|
||||
permission_level{_branch, "active"_n},
|
||||
_registrator,
|
||||
"createbranch"_n,
|
||||
std::make_tuple(coopname, dec.braname))
|
||||
.send();
|
||||
|
||||
// председатель привязывается к собственному участку и не выбирает его заявлением
|
||||
action(
|
||||
permission_level{_branch, "active"_n},
|
||||
_soviet,
|
||||
"setbranch"_n,
|
||||
std::make_tuple(coopname, dec.chairman, dec.braname))
|
||||
.send();
|
||||
|
||||
uint64_t branch_count = add_branch_count(coopname);
|
||||
|
||||
if (!coop.is_branched && branch_count >= 3) { // переход на режим кооперативных участков
|
||||
action(
|
||||
permission_level{_branch, "active"_n},
|
||||
_registrator,
|
||||
"enabranches"_n,
|
||||
std::make_tuple(coopname))
|
||||
.send();
|
||||
}
|
||||
|
||||
// Участок учреждён. Договор о полной материальной ответственности председателя участка
|
||||
// (подписан председателем участка при подаче заявления) уходит на встречную подпись
|
||||
// председателю совета: единоличное одобрение без повторного голосования. Отклонить нельзя —
|
||||
// решение об учреждении уже принято (см. branch::declliab).
|
||||
::Soviet::create_approval(
|
||||
_branch,
|
||||
coopname,
|
||||
dec.chairman,
|
||||
dec.liability,
|
||||
"branchliab"_n,
|
||||
hash,
|
||||
_branch,
|
||||
"apprliab"_n,
|
||||
"declliab"_n,
|
||||
std::string(""));
|
||||
|
||||
// Доверенность председателю участка уходит на встречную подпись председателю совета
|
||||
// отдельным одобрением рядом с договором. Якорь одобрения — хэш самого документа
|
||||
// доверенности (hash процесса уже занят одобрением договора). Отклонить нельзя.
|
||||
::Soviet::create_approval(
|
||||
_branch,
|
||||
coopname,
|
||||
dec.chairman,
|
||||
dec.authority,
|
||||
"branchauth"_n,
|
||||
dec.authority.hash,
|
||||
_branch,
|
||||
"apprauth"_n,
|
||||
"declauth"_n,
|
||||
std::string(""));
|
||||
|
||||
// Решение исполнено — стираем запись и вопросы повестки (история в журнале действий)
|
||||
decision_index decisions(_branch, coopname.value);
|
||||
auto itr = decisions.find(dec.id);
|
||||
erase_coodecquests(coopname, dec.id);
|
||||
decisions.erase(itr);
|
||||
}
|
||||
@@ -20,18 +20,31 @@
|
||||
auto authorizer_account = get_account_or_fail(trustee);
|
||||
eosio::check(authorizer_account.type == "individual"_n, "Только физическое лицо может быть назначено председателем кооперативного участка");
|
||||
|
||||
// председатель может возглавлять только один кооперативный участок
|
||||
auto branches_by_trustee = branches.get_index<"bytrustee"_n>();
|
||||
eosio::check(branches_by_trustee.find(trustee.value) == branches_by_trustee.end(),
|
||||
"Пайщик уже является председателем другого кооперативного участка");
|
||||
|
||||
branches.emplace(coopname, [&](auto &row) {
|
||||
row.braname = braname;
|
||||
row.trustee = trustee;
|
||||
});
|
||||
|
||||
|
||||
action(
|
||||
permission_level{ _branch, "active"_n},
|
||||
_registrator,
|
||||
"createbranch"_n,
|
||||
std::make_tuple(coopname, braname)
|
||||
).send();
|
||||
|
||||
|
||||
// председатель привязывается к собственному участку и не выбирает его заявлением
|
||||
action(
|
||||
permission_level{ _branch, "active"_n},
|
||||
_soviet,
|
||||
"setbranch"_n,
|
||||
std::make_tuple(coopname, trustee, braname)
|
||||
).send();
|
||||
|
||||
uint64_t branch_count = add_branch_count(coopname);
|
||||
|
||||
if (!coop.is_branched && branch_count >= 3) { //регистрируем переход на кооперативные участки
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* @brief Объявление собрания пайщиков с повесткой (создание решения).
|
||||
* Универсальный механизм: тип "free" — свободное решение (фиксируется протоколом),
|
||||
* тип "createbranch" — автоматизируемое (после утверждения уходит в совет и создаёт КУ).
|
||||
* @param coopname Наименование кооператива
|
||||
* @param hash Якорь процесса (внешний идентификатор решения)
|
||||
* @param type Тип решения: "free" | "createbranch"
|
||||
* @param initiator Организатор собрания
|
||||
* @param proposal Подписанное предложение/повестка
|
||||
* @param braname Заранее сгенерированный аккаунт будущего КУ (для "createbranch", иначе пустое)
|
||||
* @param agenda Вопросы повестки дня
|
||||
* @ingroup public_actions
|
||||
* @ingroup public_branch_actions
|
||||
|
||||
* @note Авторизация требуется от аккаунта: @p coopname
|
||||
*/
|
||||
[[eosio::action]] void branch::createdec(eosio::name coopname, eosio::checksum256 hash, eosio::name type, eosio::name initiator, document2 proposal, eosio::name braname, std::vector<decision_point> agenda) {
|
||||
check_auth_or_fail(_branch, coopname, coopname, "createdec"_n);
|
||||
|
||||
verify_document_or_fail(proposal);
|
||||
|
||||
eosio::check(type == "free"_n || type == "createbranch"_n, "Недопустимый тип решения");
|
||||
|
||||
get_cooperative_or_fail(coopname);
|
||||
get_participant_or_fail(coopname, initiator);
|
||||
|
||||
decision_index decisions(_branch, coopname.value);
|
||||
auto idx = decisions.get_index<"byhash"_n>();
|
||||
eosio::check(idx.find(hash) == idx.end(), "Решение с указанным идентификатором уже существует");
|
||||
|
||||
eosio::check(!agenda.empty(), "Повестка дня не может быть пустой");
|
||||
|
||||
if (type == "createbranch"_n) {
|
||||
eosio::check(braname != ""_n, "Для создания кооперативного участка требуется аккаунт участка");
|
||||
}
|
||||
|
||||
uint64_t decision_id = get_global_id_in_scope(_branch, coopname, "decisions"_n);
|
||||
|
||||
decisions.emplace(coopname, [&](auto &d) {
|
||||
d.id = decision_id;
|
||||
d.hash = hash;
|
||||
d.coopname = coopname;
|
||||
d.type = type;
|
||||
d.initiator = initiator;
|
||||
d.chairman = ""_n; // председатель выбирается организатором из участников при открытии голосования
|
||||
d.status = "opened"_n;
|
||||
d.proposal = proposal;
|
||||
d.signed_ballots = 0;
|
||||
d.braname = braname;
|
||||
d.participants = {initiator}; // организатор сразу участник
|
||||
d.created_at = current_time_point();
|
||||
});
|
||||
|
||||
coodecquest_index questions(_branch, coopname.value);
|
||||
uint64_t number = 0;
|
||||
|
||||
for (const auto &point : agenda) {
|
||||
eosio::check(!point.title.empty(), "Вопрос должен содержать заголовок");
|
||||
eosio::check(!point.decision.empty(), "Вопрос должен содержать проект решения");
|
||||
|
||||
number++;
|
||||
eosio::check(number <= 10, "Не больше 10 вопросов на повестке собрания");
|
||||
|
||||
questions.emplace(coopname, [&](auto &q) {
|
||||
q.id = get_global_id_in_scope(_branch, coopname, "decisionq"_n);
|
||||
q.decision_id = decision_id;
|
||||
q.number = number;
|
||||
q.coopname = coopname;
|
||||
q.title = point.title;
|
||||
q.decision = point.decision;
|
||||
q.context = point.context;
|
||||
q.counter_votes_for = 0;
|
||||
q.counter_votes_against = 0;
|
||||
q.counter_votes_abstained = 0;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* @brief Запрет отклонения доверенности председателя кооперативного участка.
|
||||
* Callback отклонения совета: к этому моменту решение об учреждении кооперативного
|
||||
* участка уже принято советом и участок создан, поэтому доверенность председателю
|
||||
* участка отклонить нельзя — председателю совета остаётся только подписать её
|
||||
* встречной подписью (см. branch::apprauth). Действие всегда завершается ошибкой.
|
||||
* @param coopname Наименование кооператива
|
||||
* @param username Председатель кооперативного участка — уполномоченный по доверенности
|
||||
* @param approval_hash Якорь одобрения (хэш документа доверенности)
|
||||
* @param reason Причина отклонения (не используется)
|
||||
* @ingroup public_actions
|
||||
* @ingroup public_branch_actions
|
||||
|
||||
* @note Авторизация требуется от аккаунта: @p soviet
|
||||
*/
|
||||
[[eosio::action]] void branch::declauth(eosio::name coopname, eosio::name username, eosio::checksum256 approval_hash, std::string reason) {
|
||||
require_auth(_soviet);
|
||||
|
||||
eosio::check(false, "Доверенность председателя участка нельзя отклонить: решение совета об учреждении участка уже принято — требуется только встречная подпись председателя совета");
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* @brief Отклонение решения советом.
|
||||
* Callback совета: при отказе совета запись решения и вопросы повестки стираются,
|
||||
* причина передаётся аргументом (история — в журнале действий).
|
||||
* @param coopname Наименование кооператива
|
||||
* @param hash Якорь процесса
|
||||
* @param reason Причина отклонения
|
||||
* @ingroup public_actions
|
||||
* @ingroup public_branch_actions
|
||||
|
||||
* @note Авторизация требуется от аккаунта: @p soviet
|
||||
*/
|
||||
[[eosio::action]] void branch::declinedec(eosio::name coopname, eosio::checksum256 hash, std::string reason) {
|
||||
require_auth(_soviet);
|
||||
|
||||
decision_index decisions(_branch, coopname.value);
|
||||
auto idx = decisions.get_index<"byhash"_n>();
|
||||
auto itr = idx.find(hash);
|
||||
|
||||
if (itr != idx.end()) {
|
||||
erase_coodecquests(coopname, itr->id);
|
||||
idx.erase(itr);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* @brief Запрет отклонения договора о материальной ответственности председателя участка.
|
||||
* Callback отклонения совета: к этому моменту решение об учреждении кооперативного
|
||||
* участка уже принято советом и участок создан, поэтому договор о материальной
|
||||
* ответственности отклонить нельзя — председателю совета остаётся только подписать его
|
||||
* встречной подписью (см. branch::apprliab). Действие всегда завершается ошибкой, тем
|
||||
* самым гарантируя, что процесс не уйдёт в неопределённое состояние.
|
||||
* @param coopname Наименование кооператива
|
||||
* @param username Председатель кооперативного участка — сторона «Исполнитель»
|
||||
* @param approval_hash Якорь процесса (хэш одобрения)
|
||||
* @param reason Причина отклонения (не используется)
|
||||
* @ingroup public_actions
|
||||
* @ingroup public_branch_actions
|
||||
|
||||
* @note Авторизация требуется от аккаунта: @p soviet
|
||||
*/
|
||||
[[eosio::action]] void branch::declliab(eosio::name coopname, eosio::name username, eosio::checksum256 approval_hash, std::string reason) {
|
||||
require_auth(_soviet);
|
||||
|
||||
eosio::check(false, "Договор о материальной ответственности председателя участка нельзя отклонить: решение совета об учреждении участка уже принято — требуется только встречная подпись председателя совета");
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* @brief Отклонение заявки доверенного лица председателем участка.
|
||||
* Заявка стирается, причина передаётся аргументом (история — в журнале действий).
|
||||
* @param coopname Наименование кооператива
|
||||
* @param hash Внешний идентификатор заявки
|
||||
* @param reason Причина отклонения
|
||||
* @ingroup public_actions
|
||||
* @ingroup public_branch_actions
|
||||
|
||||
* @note Авторизация требуется от аккаунта: @p coopname
|
||||
*/
|
||||
[[eosio::action]] void branch::decltrusted(eosio::name coopname, eosio::checksum256 hash, std::string reason) {
|
||||
check_auth_or_fail(_branch, coopname, coopname, "decltrusted"_n);
|
||||
|
||||
auto req = get_trustreq_or_fail(coopname, hash);
|
||||
|
||||
trustreq_index trustreqs(_branch, coopname.value);
|
||||
auto ritr = trustreqs.find(req.id);
|
||||
trustreqs.erase(ritr);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* @brief Удаление аккаунта из белого списка кооперативного участка.
|
||||
* Удаляет аккаунт пайщика из белого списка приватного кооперативного участка.
|
||||
* @param coopname Наименование кооператива
|
||||
* @param braname Наименование кооперативного участка
|
||||
* @param account Аккаунт пайщика для удаления
|
||||
* @ingroup public_actions
|
||||
* @ingroup public_branch_actions
|
||||
|
||||
* @note Авторизация требуется от аккаунта: @p coopname
|
||||
*/
|
||||
[[eosio::action]] void branch::delwhite(eosio::name coopname, eosio::name braname, eosio::name account) {
|
||||
check_auth_or_fail(_branch, coopname, coopname, "delwhite"_n);
|
||||
|
||||
branch_index branches(_branch, coopname.value);
|
||||
auto branch = branches.find(braname.value);
|
||||
eosio::check(branch != branches.end(), "Кооперативный участок не найден");
|
||||
|
||||
branches.modify(branch, coopname, [&](auto &b) {
|
||||
b.remove_account_from_whitelist(account);
|
||||
});
|
||||
}
|
||||
@@ -20,8 +20,34 @@
|
||||
auto authorizer_account = get_account_or_fail(trustee);
|
||||
eosio::check(authorizer_account.type == "individual"_n, "Только физическое лицо может быть назначено председателем кооперативного участка");
|
||||
|
||||
eosio::name previous_trustee = branch->trustee;
|
||||
|
||||
if (previous_trustee != trustee) {
|
||||
// председатель может возглавлять только один кооперативный участок
|
||||
auto branches_by_trustee = branches.get_index<"bytrustee"_n>();
|
||||
eosio::check(branches_by_trustee.find(trustee.value) == branches_by_trustee.end(),
|
||||
"Пайщик уже является председателем другого кооперативного участка");
|
||||
}
|
||||
|
||||
branches.modify(branch, coopname, [&](auto &b) {
|
||||
b.trustee = trustee;
|
||||
});
|
||||
|
||||
|
||||
if (previous_trustee != trustee) {
|
||||
// прежний председатель освобождается от привязки и выбирает участок заново заявлением
|
||||
action(
|
||||
permission_level{ _branch, "active"_n},
|
||||
_soviet,
|
||||
"setbranch"_n,
|
||||
std::make_tuple(coopname, previous_trustee, ""_n)
|
||||
).send();
|
||||
|
||||
// новый председатель привязывается к собственному участку
|
||||
action(
|
||||
permission_level{ _branch, "active"_n},
|
||||
_soviet,
|
||||
"setbranch"_n,
|
||||
std::make_tuple(coopname, trustee, braname)
|
||||
).send();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* @brief Исполнение утверждённого решения о создании кооперативного участка.
|
||||
* Председатель собрания формирует заявление в совет и выводит вопрос об учреждении
|
||||
* кооперативного участка на рассмотрение совета. К повестке совета прилагаются
|
||||
* протокол собрания и бюллетени (по тому же якорю процесса).
|
||||
* @param coopname Наименование кооператива
|
||||
* @param hash Якорь процесса
|
||||
* @param petition Подписанное заявление председателя в совет
|
||||
* @param liability Подписанный председателем участка договор о полной материальной ответственности
|
||||
* @param authority Подписанная председателем участка доверенность председателю кооперативного участка
|
||||
* @ingroup public_actions
|
||||
* @ingroup public_branch_actions
|
||||
|
||||
* @note Авторизация требуется от аккаунта: @p coopname
|
||||
*/
|
||||
[[eosio::action]] void branch::exec(eosio::name coopname, eosio::checksum256 hash, document2 petition, document2 liability, document2 authority) {
|
||||
check_auth_or_fail(_branch, coopname, coopname, "exec"_n);
|
||||
|
||||
verify_document_or_fail(petition);
|
||||
verify_document_or_fail(liability);
|
||||
verify_document_or_fail(authority);
|
||||
|
||||
auto dec = get_decision_or_fail(coopname, hash);
|
||||
eosio::check(dec.type == "createbranch"_n, "Исполнение доступно только для решения о создании участка");
|
||||
eosio::check(dec.status == "approved"_n, "Решение не утверждено председателем");
|
||||
|
||||
decision_index decisions(_branch, coopname.value);
|
||||
auto itr = decisions.find(dec.id);
|
||||
decisions.modify(itr, coopname, [&](auto &d) {
|
||||
d.status = "onapproval"_n;
|
||||
d.petition = petition;
|
||||
d.liability = liability;
|
||||
d.authority = authority;
|
||||
});
|
||||
|
||||
::Soviet::create_agenda(
|
||||
_branch,
|
||||
coopname,
|
||||
dec.chairman,
|
||||
get_valid_soviet_action("branchdec"_n),
|
||||
hash,
|
||||
_branch,
|
||||
"confirmdec"_n,
|
||||
"declinedec"_n,
|
||||
petition,
|
||||
std::string(""));
|
||||
|
||||
// К пакету повестки совета линкуются договор о материальной ответственности и доверенность
|
||||
// председателя участка — совет читает их вместе с заявлением до утверждения (по якорю процесса).
|
||||
Action::send<newlink_interface>(
|
||||
_soviet,
|
||||
"newlink"_n,
|
||||
_branch,
|
||||
coopname,
|
||||
dec.chairman,
|
||||
get_valid_soviet_action("branchliab"_n),
|
||||
hash,
|
||||
liability
|
||||
);
|
||||
|
||||
Action::send<newlink_interface>(
|
||||
_soviet,
|
||||
"newlink"_n,
|
||||
_branch,
|
||||
coopname,
|
||||
dec.chairman,
|
||||
get_valid_soviet_action("branchauth"_n),
|
||||
hash,
|
||||
authority
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* @brief Присоединение пайщика к собранию.
|
||||
* Пайщик фиксируется в списке участников собрания.
|
||||
* @param coopname Наименование кооператива
|
||||
* @param hash Якорь процесса
|
||||
* @param username Имя присоединяющегося пайщика
|
||||
* @ingroup public_actions
|
||||
* @ingroup public_branch_actions
|
||||
|
||||
* @note Авторизация требуется от аккаунта: @p coopname
|
||||
*/
|
||||
[[eosio::action]] void branch::joindec(eosio::name coopname, eosio::checksum256 hash, eosio::name username) {
|
||||
check_auth_or_fail(_branch, coopname, coopname, "joindec"_n);
|
||||
|
||||
get_participant_or_fail(coopname, username);
|
||||
|
||||
auto dec = get_decision_or_fail(coopname, hash);
|
||||
eosio::check(dec.status == "opened"_n, "Присоединиться можно только до начала голосования");
|
||||
eosio::check(!dec.is_participant(username), "Пайщик уже присоединился к собранию");
|
||||
|
||||
decision_index decisions(_branch, coopname.value);
|
||||
auto itr = decisions.find(dec.id);
|
||||
decisions.modify(itr, coopname, [&](auto &d) {
|
||||
d.participants.push_back(username);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* @brief Подача заявки на приём доверенным лицом кооперативного участка.
|
||||
* Пайщик прикладывает подписанные заявление и договор о полной материальной
|
||||
* ответственности. Заявка ожидает одобрения председателя участка.
|
||||
* @param coopname Наименование кооператива
|
||||
* @param braname Кооперативный участок
|
||||
* @param username Заявитель
|
||||
* @param hash Внешний идентификатор заявки
|
||||
* @param application Подписанный заявителем договор о полной материальной ответственности
|
||||
* @param authority Подписанная заявителем доверенность доверенному лицу/оператору участка
|
||||
* @ingroup public_actions
|
||||
* @ingroup public_branch_actions
|
||||
|
||||
* @note Авторизация требуется от аккаунта: @p coopname
|
||||
*/
|
||||
[[eosio::action]] void branch::reqtrusted(eosio::name coopname, eosio::name braname, eosio::name username, eosio::checksum256 hash, document2 application, document2 authority) {
|
||||
check_auth_or_fail(_branch, coopname, coopname, "reqtrusted"_n);
|
||||
|
||||
verify_document_or_fail(application);
|
||||
verify_document_or_fail(authority);
|
||||
|
||||
get_participant_or_fail(coopname, username);
|
||||
|
||||
auto br = get_branch_or_fail(coopname, braname);
|
||||
eosio::check(br.trustee != username, "Председатель участка не может быть доверенным лицом");
|
||||
eosio::check(!br.is_account_in_trusted(username), "Пайщик уже является доверенным лицом участка");
|
||||
eosio::check(br.trusted.size() < 3, "Достигнут предел доверенных лиц участка (не более трёх)");
|
||||
|
||||
trustreq_index trustreqs(_branch, coopname.value);
|
||||
auto idx = trustreqs.get_index<"byhash"_n>();
|
||||
eosio::check(idx.find(hash) == idx.end(), "Заявка с указанным идентификатором уже существует");
|
||||
|
||||
trustreqs.emplace(coopname, [&](auto &r) {
|
||||
r.id = get_global_id_in_scope(_branch, coopname, "trustreqs"_n);
|
||||
r.hash = hash;
|
||||
r.coopname = coopname;
|
||||
r.braname = braname;
|
||||
r.username = username;
|
||||
r.application = application;
|
||||
r.authority = authority;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* @brief Установка признака приватности кооперативного участка.
|
||||
* Приватный кооперативный участок нельзя выбрать при вступлении или смене участка,
|
||||
* если аккаунт пайщика не добавлен в белый список этого участка.
|
||||
* @param coopname Наименование кооператива
|
||||
* @param braname Наименование кооперативного участка
|
||||
* @param is_private Признак приватности (true — приватный, false — публичный)
|
||||
* @ingroup public_actions
|
||||
* @ingroup public_branch_actions
|
||||
|
||||
* @note Авторизация требуется от аккаунта: @p coopname
|
||||
*/
|
||||
[[eosio::action]] void branch::setprivate(eosio::name coopname, eosio::name braname, bool is_private) {
|
||||
check_auth_or_fail(_branch, coopname, coopname, "setprivate"_n);
|
||||
|
||||
branch_index branches(_branch, coopname.value);
|
||||
auto branch = branches.find(braname.value);
|
||||
eosio::check(branch != branches.end(), "Кооперативный участок не найден");
|
||||
|
||||
branches.modify(branch, coopname, [&](auto &b) {
|
||||
b.set_private(is_private);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* @brief Открытие голосования по решению.
|
||||
* Организатор собрания (он же председатель собрания) открывает голосование,
|
||||
* указывая избираемого председателя кооперативного участка (из числа
|
||||
* присоединившихся участников) и — для решения "createbranch" —
|
||||
* адрес привязки кооперативного участка, определённый собранием.
|
||||
* Окно голосования отмеряется автоматически (DECISION_VOTING_WINDOW_SECONDS).
|
||||
* @param coopname Наименование кооператива
|
||||
* @param hash Якорь процесса
|
||||
* @param chairman Избираемый председатель кооперативного участка (из участников)
|
||||
* @param address Адрес привязки кооперативного участка (для "createbranch")
|
||||
* @param agenda Итоговая повестка собрания: если непуста — заменяет предварительную
|
||||
* (уточнённые на собрании формулировки и дополнительные вопросы); пустая — повестка без изменений
|
||||
* @ingroup public_actions
|
||||
* @ingroup public_branch_actions
|
||||
|
||||
* @note Авторизация требуется от аккаунта: @p coopname
|
||||
*/
|
||||
[[eosio::action]] void branch::startdec(eosio::name coopname, eosio::checksum256 hash, eosio::name chairman, std::string address, std::vector<decision_point> agenda) {
|
||||
check_auth_or_fail(_branch, coopname, coopname, "startdec"_n);
|
||||
|
||||
auto dec = get_decision_or_fail(coopname, hash);
|
||||
eosio::check(dec.status == "opened"_n, "Голосование уже начато или собрание закрыто");
|
||||
eosio::check(dec.participants.size() >= MIN_DECISION_QUORUM,
|
||||
"Для открытия голосования требуется не менее 3 участников собрания");
|
||||
eosio::check(dec.is_participant(chairman), "Председатель участка должен быть участником собрания");
|
||||
|
||||
if (dec.type == "createbranch"_n) {
|
||||
eosio::check(!address.empty(), "Для создания кооперативного участка требуется адрес привязки");
|
||||
}
|
||||
|
||||
// Итоговая повестка собрания: непустой список заменяет предварительную
|
||||
// (формулировки уточняются на собрании — наименование участка, председатель, новые вопросы)
|
||||
coodecquest_index questions(_branch, coopname.value);
|
||||
uint64_t number = 0;
|
||||
if (!agenda.empty()) {
|
||||
erase_coodecquests(coopname, dec.id);
|
||||
}
|
||||
|
||||
for (const auto &point : agenda) {
|
||||
eosio::check(!point.title.empty(), "Вопрос должен содержать заголовок");
|
||||
eosio::check(!point.decision.empty(), "Вопрос должен содержать проект решения");
|
||||
|
||||
number++;
|
||||
eosio::check(number <= 10, "Не больше 10 вопросов на повестке собрания");
|
||||
|
||||
questions.emplace(coopname, [&](auto &q) {
|
||||
q.id = get_global_id_in_scope(_branch, coopname, "decisionq"_n);
|
||||
q.decision_id = dec.id;
|
||||
q.number = number;
|
||||
q.coopname = coopname;
|
||||
q.title = point.title;
|
||||
q.decision = point.decision;
|
||||
q.context = point.context;
|
||||
q.counter_votes_for = 0;
|
||||
q.counter_votes_against = 0;
|
||||
q.counter_votes_abstained = 0;
|
||||
});
|
||||
}
|
||||
|
||||
auto now = current_time_point();
|
||||
|
||||
decision_index decisions(_branch, coopname.value);
|
||||
auto itr = decisions.find(dec.id);
|
||||
decisions.modify(itr, coopname, [&](auto &d) {
|
||||
d.status = "voting"_n;
|
||||
d.chairman = chairman;
|
||||
d.address = address;
|
||||
d.open_at = eosio::time_point_sec(now.sec_since_epoch());
|
||||
d.close_at = eosio::time_point_sec(now.sec_since_epoch() + DECISION_VOTING_WINDOW_SECONDS);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* @brief Голосование по вопросам повестки бюллетенем.
|
||||
* Участник подаёт бюллетень-заявление с волеизъявлением (за/против/воздержался)
|
||||
* по каждому вопросу повестки. Подсчёт ведёт контракт. Адаптация meet::vote.
|
||||
* @param coopname Наименование кооператива
|
||||
* @param hash Якорь процесса
|
||||
* @param username Имя голосующего участника
|
||||
* @param ballot Подписанный бюллетень
|
||||
* @param votes Массив голосов по вопросам повестки
|
||||
* @ingroup public_actions
|
||||
* @ingroup public_branch_actions
|
||||
|
||||
* @note Авторизация требуется от аккаунта: @p coopname
|
||||
*/
|
||||
[[eosio::action]] void branch::votedec(eosio::name coopname, eosio::checksum256 hash, eosio::name username, document2 ballot, std::vector<decision_vote_point> votes) {
|
||||
check_auth_or_fail(_branch, coopname, coopname, "votedec"_n);
|
||||
|
||||
verify_document_or_fail(ballot);
|
||||
|
||||
auto dec = get_decision_or_fail(coopname, hash);
|
||||
eosio::check(dec.status == "voting"_n, "Голосование не открыто");
|
||||
eosio::check(dec.is_participant(username), "Голосовать может только участник собрания");
|
||||
|
||||
auto now = current_time_point();
|
||||
eosio::check(now.sec_since_epoch() >= dec.open_at.sec_since_epoch(), "Голосование ещё не началось");
|
||||
eosio::check(now.sec_since_epoch() <= dec.close_at.sec_since_epoch(), "Голосование завершено");
|
||||
|
||||
// Собираем вопросы данного решения
|
||||
coodecquest_index questions(_branch, coopname.value);
|
||||
auto by_dec = questions.get_index<"bydecision"_n>();
|
||||
std::vector<coodecquest> decision_questions;
|
||||
for (auto itr = by_dec.lower_bound(dec.id); itr != by_dec.end() && itr->decision_id == dec.id; ++itr) {
|
||||
decision_questions.push_back(*itr);
|
||||
}
|
||||
eosio::check(votes.size() == decision_questions.size(), "Бюллетень должен содержать голоса по всем вопросам");
|
||||
|
||||
// Бюллетень покрывает все вопросы без дублирования
|
||||
std::set<uint64_t> question_ids;
|
||||
for (const auto &q : decision_questions) {
|
||||
question_ids.insert(q.id);
|
||||
}
|
||||
std::set<uint64_t> voted_ids;
|
||||
for (const auto &v : votes) {
|
||||
eosio::check(question_ids.find(v.question_id) != question_ids.end(), "В бюллетене указан неизвестный вопрос");
|
||||
eosio::check(voted_ids.insert(v.question_id).second, "Дублирование голосов по одному вопросу недопустимо");
|
||||
}
|
||||
|
||||
// Участник не голосовал ранее
|
||||
for (const auto &q : decision_questions) {
|
||||
auto already = [&](const std::vector<eosio::name> &voters) {
|
||||
return std::find(voters.begin(), voters.end(), username) != voters.end();
|
||||
};
|
||||
eosio::check(!already(q.voters_for) && !already(q.voters_against) && !already(q.voters_abstained), "Ваш голос уже учтён");
|
||||
}
|
||||
|
||||
// Учитываем голоса
|
||||
for (const auto &v : votes) {
|
||||
auto qitr = questions.find(v.question_id);
|
||||
eosio::check(qitr != questions.end(), "Вопрос не найден");
|
||||
questions.modify(qitr, coopname, [&](auto &q) {
|
||||
if (v.vote == "for"_n) {
|
||||
q.counter_votes_for++;
|
||||
q.voters_for.push_back(username);
|
||||
} else if (v.vote == "against"_n) {
|
||||
q.counter_votes_against++;
|
||||
q.voters_against.push_back(username);
|
||||
} else if (v.vote == "abstained"_n) {
|
||||
q.counter_votes_abstained++;
|
||||
q.voters_abstained.push_back(username);
|
||||
} else {
|
||||
eosio::check(false, "Недопустимое значение голоса");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
decision_index decisions(_branch, coopname.value);
|
||||
auto ditr = decisions.find(dec.id);
|
||||
decisions.modify(ditr, coopname, [&](auto &d) {
|
||||
d.signed_ballots++;
|
||||
});
|
||||
|
||||
// Бюллетень линкуется к ПАКЕТУ СОБРАНИЯ (якорь = хэш предложения proposal.hash),
|
||||
// туда же закрывающим документом ляжет протокол (см. closedec). Не на хэш процесса,
|
||||
// иначе бюллетени попадут в пакет заявления в совет.
|
||||
Action::send<newlink_interface>(
|
||||
_soviet,
|
||||
"newlink"_n,
|
||||
_branch,
|
||||
coopname,
|
||||
username,
|
||||
get_valid_soviet_action("ballot"_n),
|
||||
dec.proposal.hash,
|
||||
ballot
|
||||
);
|
||||
}
|
||||
@@ -92,6 +92,10 @@ static const std::set<eosio::name> soviet_actions = {
|
||||
"completegm"_n, //решение общего собрания пайщиков
|
||||
"ballot"_n, //бюллетень участника общего собрания пайщиков
|
||||
"gmnotify"_n, //уведомление участника общего собрания пайщиков
|
||||
//BRANCH
|
||||
"branchdec"_n, //решение собрания пайщиков об учреждении кооперативного участка
|
||||
"branchliab"_n, //договор о полной материальной ответственности председателя участка (линк в пакет совета)
|
||||
"branchauth"_n, //доверенность председателю кооперативного участка (линк в пакет совета)
|
||||
//CAPITAL
|
||||
"capitalinvst"_n, //заявление на инвестиции по договору УХД
|
||||
"createresult"_n, //клайм прироста благороста из задания
|
||||
|
||||
@@ -48,6 +48,8 @@
|
||||
|
||||
#include "table_branch_branches.hpp"
|
||||
#include "table_branch_branchstat.hpp"
|
||||
#include "table_branch_decisions.hpp"
|
||||
#include "table_branch_trustreqs.hpp"
|
||||
|
||||
// wallet / gateway / ledger / loan / marketplace
|
||||
#include "table_wallet_deposits.hpp"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <eosio/binary_extension.hpp>
|
||||
#include <eosio/eosio.hpp>
|
||||
#include <vector>
|
||||
|
||||
@@ -16,6 +17,12 @@ struct [[eosio::table, eosio::contract(BRANCH)]] coobranch {
|
||||
eosio::name trustee;
|
||||
std::vector<eosio::name> trusted;
|
||||
|
||||
// Поля приватности добавлены как binary_extension: у действующих кооперативов
|
||||
// в таблице уже могут быть строки, и расширение в конце struct сохраняет
|
||||
// обратную совместимость десериализации (старые записи читаются как публичные с пустым списком).
|
||||
eosio::binary_extension<bool> is_private; // приватный участок: выбрать его могут только из белого списка
|
||||
eosio::binary_extension<std::vector<eosio::name>> whitelist; // белый список аккаунтов, допущенных к выбору приватного участка
|
||||
|
||||
uint64_t primary_key() const { return braname.value; }
|
||||
uint64_t by_trustee() const { return trustee.value; }
|
||||
|
||||
@@ -37,6 +44,38 @@ struct [[eosio::table, eosio::contract(BRANCH)]] coobranch {
|
||||
}
|
||||
return is_account_in_trusted(username);
|
||||
}
|
||||
|
||||
// признак приватности с учётом отсутствующего расширения у старых записей
|
||||
// (const-перегрузка value_or() без аргумента возвращает bool() == false, если значения нет)
|
||||
bool is_branch_private() const { return is_private.value_or(); }
|
||||
|
||||
bool is_account_in_whitelist(const eosio::name &account) const {
|
||||
if (!whitelist.has_value()) {
|
||||
return false;
|
||||
}
|
||||
const auto &wl = whitelist.value();
|
||||
return std::find(wl.begin(), wl.end(), account) != wl.end();
|
||||
}
|
||||
|
||||
void set_private(bool value) { is_private.emplace(value); }
|
||||
|
||||
void add_account_to_whitelist(const eosio::name &account) {
|
||||
if (!whitelist.has_value()) {
|
||||
whitelist.emplace();
|
||||
}
|
||||
auto &wl = whitelist.value();
|
||||
eosio::check(std::find(wl.begin(), wl.end(), account) == wl.end(),
|
||||
"Аккаунт уже добавлен в белый список кооперативного участка");
|
||||
wl.push_back(account);
|
||||
}
|
||||
|
||||
void remove_account_from_whitelist(const eosio::name &account) {
|
||||
eosio::check(whitelist.has_value(), "Белый список кооперативного участка пуст");
|
||||
auto &wl = whitelist.value();
|
||||
auto it = std::find(wl.begin(), wl.end(), account);
|
||||
eosio::check(it != wl.end(), "Аккаунт не найден в белом списке кооперативного участка");
|
||||
wl.erase(it);
|
||||
}
|
||||
};
|
||||
|
||||
typedef eosio::multi_index<
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <eosio/eosio.hpp>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../consts.hpp"
|
||||
#include "document_core.hpp"
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Универсальный механизм «собрание пайщиков → решение» на контракте BRANCH.
|
||||
* Зеркало Совета: тип решения `free` (свободное — просто фиксируется протоколом)
|
||||
* либо автоматизируемый (`createbranch` — после утверждения председателем уходит
|
||||
* в совет действием exec и по решению совета создаёт кооперативный участок).
|
||||
* Якорь записи — hash (byhash). Голос фиксируется бюллетенем-заявлением;
|
||||
* протокол утверждает один председатель собрания (секретарь не нужен).
|
||||
* Жизненный цикл — Chain-RAM: терминал = erase, история — в журнале действий парсера.
|
||||
*/
|
||||
|
||||
/// Минимальный кворум учредительного собрания (число подписанных бюллетеней).
|
||||
static constexpr uint64_t MIN_DECISION_QUORUM = 3;
|
||||
|
||||
/// Длительность окна голосования собрания пайщиков участка (секунд).
|
||||
/// Голосование короткое — проходит прямо на собрании: организатор открывает
|
||||
/// его кнопкой, дедлайн отмеряется автоматически.
|
||||
static constexpr uint32_t DECISION_VOTING_WINDOW_SECONDS = 15 * 60;
|
||||
|
||||
/// Точка повестки дня собрания (вход действия createdec).
|
||||
struct decision_point {
|
||||
std::string title; ///< Текст вопроса
|
||||
std::string decision; ///< Проект решения по вопросу
|
||||
std::string context; ///< Контекст вопроса (может быть пустым)
|
||||
};
|
||||
|
||||
/// Голос участника по одному вопросу повестки (вход действия votedec).
|
||||
struct decision_vote_point {
|
||||
uint64_t question_id; ///< Идентификатор вопроса
|
||||
eosio::name vote; ///< Голос: "for"_n | "against"_n | "abstained"_n
|
||||
};
|
||||
|
||||
/**
|
||||
* @ingroup public_tables
|
||||
* @ingroup public_branch_tables
|
||||
* @par table: decisions
|
||||
*/
|
||||
struct [[eosio::table, eosio::contract(BRANCH)]] coodecision {
|
||||
uint64_t id; ///< Идентификатор решения
|
||||
eosio::checksum256 hash; ///< Якорь процесса (внешний идентификатор)
|
||||
eosio::name coopname; ///< Имя кооператива
|
||||
eosio::name type; ///< Тип решения: "free" | "createbranch"
|
||||
eosio::name initiator; ///< Организатор собрания
|
||||
eosio::name chairman; ///< Председатель собрания — выбирается организатором из участников при открытии голосования (для createbranch — он же кандидат в председатели КУ)
|
||||
eosio::name status; ///< opened | voting | approved | onapproval
|
||||
|
||||
document2 proposal; ///< Подписанное предложение/повестка инициатора
|
||||
document2 protocol; ///< Протокол решения (утверждает председатель)
|
||||
document2 petition; ///< Заявление председателя в совет (для createbranch)
|
||||
document2 liability; ///< Договор о полной индивидуальной материальной ответственности председателя КУ (подписан председателем участка, ждёт встречной подписи председателя совета)
|
||||
document2 authority; ///< Доверенность председателю КУ (подписана председателем участка, ждёт встречной подписи председателя совета)
|
||||
document2 authorization; ///< Решение совета (callback)
|
||||
|
||||
eosio::time_point_sec open_at; ///< Начало голосования
|
||||
eosio::time_point_sec close_at; ///< Плановое закрытие голосования
|
||||
uint64_t signed_ballots; ///< Число поданных бюллетеней
|
||||
|
||||
eosio::name braname; ///< Заранее сгенерированный аккаунт будущего КУ (createbranch)
|
||||
std::string address; ///< Адрес привязки КУ (createbranch)
|
||||
|
||||
std::vector<eosio::name> participants; ///< Присоединившиеся участники собрания
|
||||
eosio::time_point_sec created_at; ///< Дата создания
|
||||
// Место и время проведения собрания НЕ публикуются в блокчейн —
|
||||
// это приватные данные пайщиков, живут в БД платформы (composite db-слой).
|
||||
|
||||
uint64_t primary_key() const { return id; }
|
||||
eosio::checksum256 by_hash() const { return hash; }
|
||||
|
||||
bool is_participant(const eosio::name &username) const {
|
||||
return std::find(participants.begin(), participants.end(), username) != participants.end();
|
||||
}
|
||||
};
|
||||
|
||||
typedef eosio::multi_index<
|
||||
"decisions"_n, coodecision,
|
||||
eosio::indexed_by<"byhash"_n, eosio::const_mem_fun<coodecision, eosio::checksum256, &coodecision::by_hash>>>
|
||||
decision_index;
|
||||
|
||||
/**
|
||||
* @ingroup public_tables
|
||||
* @ingroup public_branch_tables
|
||||
* @par table: decisionq
|
||||
*/
|
||||
struct [[eosio::table, eosio::contract(BRANCH)]] coodecquest {
|
||||
uint64_t id; ///< Идентификатор вопроса
|
||||
uint64_t decision_id; ///< Идентификатор решения
|
||||
uint64_t number; ///< Номер вопроса в повестке
|
||||
eosio::name coopname; ///< Имя кооператива
|
||||
|
||||
std::string title; ///< Текст вопроса
|
||||
std::string decision; ///< Проект решения по вопросу
|
||||
std::string context; ///< Контекст вопроса
|
||||
|
||||
uint64_t counter_votes_for; ///< Счётчик голосов за
|
||||
uint64_t counter_votes_against; ///< Счётчик голосов против
|
||||
uint64_t counter_votes_abstained; ///< Счётчик воздержавшихся
|
||||
|
||||
std::vector<eosio::name> voters_for; ///< Проголосовавшие за
|
||||
std::vector<eosio::name> voters_against; ///< Проголосовавшие против
|
||||
std::vector<eosio::name> voters_abstained; ///< Воздержавшиеся
|
||||
|
||||
uint64_t primary_key() const { return id; }
|
||||
uint64_t by_decision() const { return decision_id; }
|
||||
};
|
||||
|
||||
typedef eosio::multi_index<
|
||||
"decisionq"_n, coodecquest,
|
||||
eosio::indexed_by<"bydecision"_n, eosio::const_mem_fun<coodecquest, uint64_t, &coodecquest::by_decision>>>
|
||||
coodecquest_index;
|
||||
|
||||
/// Получить решение по якорю или упасть.
|
||||
inline coodecision get_decision_or_fail(eosio::name coopname, eosio::checksum256 hash) {
|
||||
decision_index decisions(_branch, coopname.value);
|
||||
auto idx = decisions.get_index<"byhash"_n>();
|
||||
auto itr = idx.find(hash);
|
||||
eosio::check(itr != idx.end(), "Решение собрания не найдено");
|
||||
return *itr;
|
||||
}
|
||||
|
||||
/// Удалить все вопросы повестки данного решения (при erase решения).
|
||||
inline void erase_coodecquests(eosio::name coopname, uint64_t decision_id) {
|
||||
coodecquest_index questions(_branch, coopname.value);
|
||||
auto by_dec = questions.get_index<"bydecision"_n>();
|
||||
auto itr = by_dec.lower_bound(decision_id);
|
||||
while (itr != by_dec.end() && itr->decision_id == decision_id) {
|
||||
itr = by_dec.erase(itr);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
#include <eosio/eosio.hpp>
|
||||
|
||||
#include "../consts.hpp"
|
||||
#include "document_core.hpp"
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Заявки на приём доверенным лицом кооперативного участка.
|
||||
* Pending-таблица: заявитель подаёт подписанные заявление и договор о полной
|
||||
* материальной ответственности; председатель участка одобряет встречной подписью —
|
||||
* пайщик добавляется в trusted участка, заявка стирается (история — в журнале действий).
|
||||
*/
|
||||
|
||||
/**
|
||||
* @ingroup public_tables
|
||||
* @ingroup public_branch_tables
|
||||
* @par table: trustreqs
|
||||
*/
|
||||
struct [[eosio::table, eosio::contract(BRANCH)]] trustreq {
|
||||
uint64_t id; ///< Идентификатор заявки
|
||||
eosio::checksum256 hash; ///< Внешний идентификатор заявки
|
||||
eosio::name coopname; ///< Имя кооператива
|
||||
eosio::name braname; ///< Кооперативный участок
|
||||
eosio::name username; ///< Заявитель
|
||||
document2 application; ///< Договор о полной материальной ответственности доверенного лица (подписан заявителем)
|
||||
document2 authority; ///< Доверенность доверенному лицу/оператору участка (подписана заявителем, ждёт встречной подписи председателя участка)
|
||||
|
||||
uint64_t primary_key() const { return id; }
|
||||
eosio::checksum256 by_hash() const { return hash; }
|
||||
};
|
||||
|
||||
typedef eosio::multi_index<
|
||||
"trustreqs"_n, trustreq,
|
||||
eosio::indexed_by<"byhash"_n, eosio::const_mem_fun<trustreq, eosio::checksum256, &trustreq::by_hash>>>
|
||||
trustreq_index;
|
||||
|
||||
inline trustreq get_trustreq_or_fail(eosio::name coopname, eosio::checksum256 hash) {
|
||||
trustreq_index trustreqs(_branch, coopname.value);
|
||||
auto idx = trustreqs.get_index<"byhash"_n>();
|
||||
auto itr = idx.find(hash);
|
||||
eosio::check(itr != idx.end(), "Заявка доверенного не найдена");
|
||||
return *itr;
|
||||
}
|
||||
@@ -64,6 +64,7 @@ transitions:
|
||||
- Пайщик зарегистрирован в кооперативе (participants).
|
||||
- Указанный участок существует (branches).
|
||||
- Заявление подписано ЭЦП.
|
||||
- Если участок приватный — пайщик должен быть в белом списке участка.
|
||||
|
||||
# ── Секция 4. Сценарий ──────────────────────────────────────────────────────
|
||||
scenario:
|
||||
@@ -81,6 +82,7 @@ scenario:
|
||||
- Пайщик присутствует в participants кооператива.
|
||||
- Участок (branch) с указанным braname существует.
|
||||
- Заявление подписано ЭЦП.
|
||||
- Приватный участок доступен к выбору только пайщикам из его белого списка.
|
||||
post:
|
||||
- participants.braname = выбранный участок.
|
||||
- Документ заявления зафиксирован в реестрах документов.
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
#include "src/board/updateboard.cpp"
|
||||
|
||||
#include "src/branch/deletebranch.cpp"
|
||||
#include "src/branch/setbranch.cpp"
|
||||
|
||||
#include "src/decision/authorize.cpp"
|
||||
#include "src/decision/freedecision.cpp"
|
||||
|
||||
@@ -238,6 +238,7 @@ public:
|
||||
|
||||
//branch.cpp
|
||||
[[eosio::action]] void deletebranch(eosio::name coopname, eosio::name braname);
|
||||
[[eosio::action]] void setbranch(eosio::name coopname, eosio::name username, eosio::name braname);
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* @brief Системная привязка пайщика к кооперативному участку
|
||||
* Устанавливает или сбрасывает кооперативный участок пайщика без заявления —
|
||||
* вызывается контрактом участков при назначении или смене председателя участка:
|
||||
* председатель всегда привязан к собственному участку и не выбирает его заявлением.
|
||||
* Пустое наименование участка сбрасывает привязку (пайщик выберет участок заново).
|
||||
* @param coopname Наименование кооператива
|
||||
* @param username Наименование пайщика
|
||||
* @param braname Наименование кооперативного участка (пустое — сброс привязки)
|
||||
* @ingroup public_actions
|
||||
* @ingroup public_soviet_actions
|
||||
|
||||
* @note Авторизация требуется от аккаунта: @p _branch
|
||||
*/
|
||||
void soviet::setbranch(eosio::name coopname, eosio::name username, eosio::name braname) {
|
||||
require_auth(_branch);
|
||||
|
||||
participants_index participants(_soviet, coopname.value);
|
||||
auto participant = participants.find(username.value);
|
||||
|
||||
// председатель участка может не быть пайщиком на момент учреждения — не роняем создание участка
|
||||
if (participant == participants.end()) {
|
||||
return;
|
||||
}
|
||||
|
||||
participants.modify(participant, get_self(), [&](auto& row) {
|
||||
if (braname == ""_n) {
|
||||
row.braname.reset();
|
||||
} else {
|
||||
row.braname = braname;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -15,7 +15,22 @@
|
||||
require_auth(coopname);
|
||||
|
||||
verify_document_or_fail(document);
|
||||
get_branch_or_fail(coopname, braname);
|
||||
auto target_branch = get_branch_or_fail(coopname, braname);
|
||||
|
||||
// приватный кооперативный участок может выбрать только аккаунт из белого списка участка;
|
||||
// белым списком управляет председатель совета (действия addwhite/delwhite контракта branch)
|
||||
if (target_branch.is_branch_private()) {
|
||||
eosio::check(target_branch.is_account_in_whitelist(username),
|
||||
"Кооперативный участок приватный — выбрать его могут только пайщики из белого списка участка");
|
||||
}
|
||||
|
||||
// председатель кооперативного участка привязан к собственному участку
|
||||
// и не может сменить его, пока исполняет обязанности
|
||||
branch_index branches(_branch, coopname.value);
|
||||
auto branches_by_trustee = branches.get_index<"bytrustee"_n>();
|
||||
eosio::check(branches_by_trustee.find(username.value) == branches_by_trustee.end(),
|
||||
"Председатель кооперативного участка не может сменить участок, пока исполняет обязанности председателя");
|
||||
|
||||
participants_index participants(_soviet, coopname.value);
|
||||
auto participant = participants.find(username.value);
|
||||
eosio::check(participant != participants.end(), "Пайщик не найден");
|
||||
|
||||
-1
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"cSpell.words": ["subcode"],
|
||||
"editor.formatOnSave": false,
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"editor.defaultFormatter": null,
|
||||
|
||||
@@ -22,6 +22,7 @@ import { randomUUID } from 'crypto';
|
||||
import type { Cooperative } from 'cooptypes';
|
||||
import { ORGANIZATION_REPOSITORY, OrganizationRepository } from '~/domain/common/repositories/organization.repository';
|
||||
import { INDIVIDUAL_REPOSITORY, IndividualRepository } from '~/domain/common/repositories/individual.repository';
|
||||
import type { PassportDataDomainInterface } from '~/domain/common/interfaces/passport-data-domain.interface';
|
||||
import { ENTREPRENEUR_REPOSITORY, EntrepreneurRepository } from '~/domain/common/repositories/entrepreneur.repository';
|
||||
import {
|
||||
SEARCH_PRIVATE_ACCOUNTS_REPOSITORY,
|
||||
@@ -182,6 +183,35 @@ export class AccountInteractor {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Self-service: пайщик сохраняет СВОИ паспортные данные в реестр.
|
||||
*
|
||||
* Принимается только если паспорт ещё не был установлен ранее — существующие
|
||||
* данные не перезатираются (их изменение — отдельный процесс, не здесь).
|
||||
* Нужно для ролей, которым паспорт обязателен (председатель участка, доверенное
|
||||
* лицо) в кооперативах, где паспорт при регистрации не собирается.
|
||||
*/
|
||||
async saveOwnPassport(username: string, passport: PassportDataDomainInterface): Promise<AccountDomainEntity> {
|
||||
const existing = await this.individualRepository.findByUsername(username);
|
||||
if (!existing) {
|
||||
throw new HttpApiError(httpStatus.BAD_REQUEST, 'Паспортные данные можно сохранить только для физического лица');
|
||||
}
|
||||
if (existing.passport) {
|
||||
throw new HttpApiError(
|
||||
httpStatus.BAD_REQUEST,
|
||||
'Паспортные данные уже установлены ранее — их изменение производится отдельным процессом'
|
||||
);
|
||||
}
|
||||
|
||||
await this.individualRepository.create({ ...existing, passport, username });
|
||||
|
||||
const account = await this.getAccount(username);
|
||||
|
||||
this.eventsService.emit('account::updated', { username, account });
|
||||
|
||||
return new AccountDomainEntity(account);
|
||||
}
|
||||
|
||||
/**
|
||||
* Удаление аккаунта из системы учёта провайдера (off-chain).
|
||||
*
|
||||
|
||||
@@ -12,6 +12,7 @@ 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 { PassportInputDTO } from '../dto/passport-input.dto';
|
||||
import { DeleteAccountInputDTO } from '../dto/delete-account-input.dto';
|
||||
import { SearchPrivateAccountsInputDTO } from '../dto/search-private-accounts-input.dto';
|
||||
import { PrivateAccountSearchResultDTO } from '../dto/search-private-accounts-result.dto';
|
||||
@@ -111,4 +112,17 @@ export class AccountResolver {
|
||||
): Promise<AccountDTO> {
|
||||
return await this.accountService.updateAccount(data);
|
||||
}
|
||||
|
||||
@Mutation(() => AccountDTO, {
|
||||
name: 'saveMyPassport',
|
||||
description:
|
||||
'Сохранить собственные паспортные данные в реестре пайщиков. Применяется, когда паспорт ранее не был указан (например, при подписании договора материальной ответственности председателем кооперативного участка или доверенным лицом). Если паспортные данные уже установлены — они не перезаписываются.',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard)
|
||||
async saveMyPassport(
|
||||
@Args('passport', { type: () => PassportInputDTO }) passport: PassportInputDTO,
|
||||
@CurrentUser() currentUser: MonoAccountDomainInterface
|
||||
): Promise<AccountDTO> {
|
||||
return await this.accountService.saveOwnPassport(currentUser.username, passport);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ 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 { PassportInputDTO } from '../dto/passport-input.dto';
|
||||
import type { SearchPrivateAccountsInputDTO } from '../dto/search-private-accounts-input.dto';
|
||||
import { PrivateAccountSearchResultDTO } from '../dto/search-private-accounts-result.dto';
|
||||
|
||||
@@ -20,6 +21,11 @@ export class AccountService {
|
||||
return new AccountDTO(result);
|
||||
}
|
||||
|
||||
public async saveOwnPassport(username: string, passport: PassportInputDTO): Promise<AccountDTO> {
|
||||
const result = await this.accountInteractor.saveOwnPassport(username, passport);
|
||||
return new AccountDTO(result);
|
||||
}
|
||||
|
||||
public async deleteAccount(data: DeleteAccountInputDTO): Promise<void> {
|
||||
await this.accountInteractor.deleteAccount(data.username_for_delete);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
import type { AddBranchWhitelistDomainInterface } from '~/domain/branch/interfaces/add-branch-whitelist-domain-input.interface';
|
||||
|
||||
@InputType('AddBranchWhitelistInput')
|
||||
export class AddBranchWhitelistGraphQLInput implements AddBranchWhitelistDomainInterface {
|
||||
@Field(() => String, { description: 'Имя аккаунта кооператива' })
|
||||
coopname!: string;
|
||||
|
||||
@Field(() => String, { description: 'Имя аккаунта кооперативного участка' })
|
||||
braname!: string;
|
||||
|
||||
@Field(() => String, { description: 'Имя аккаунта пайщика, добавляемого в белый список участка' })
|
||||
account!: string;
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
import { ObjectType, Field } from '@nestjs/graphql';
|
||||
import { ObjectType, Field, Int } from '@nestjs/graphql';
|
||||
import { RepresentedByDTO } from '~/application/common/dto/represented-by.dto';
|
||||
import type { BranchDomainInterface } from '~/domain/branch/interfaces/branch-domain.interface';
|
||||
import type { BranchDomainEntity } from '~/domain/branch/entities/branch-domain.entity';
|
||||
import { OrganizationDetailsDTO } from '~/application/common/dto/organization-details.dto';
|
||||
import { IndividualDTO } from '~/application/common/dto/individual.dto';
|
||||
import { IndividualCertificateDTO } from '~/application/common/dto/individual-certificate.dto';
|
||||
import { AccountType } from '~/application/account/enum/account-type.enum';
|
||||
import { IsArray, IsJSON, IsString } from 'class-validator';
|
||||
import { BankPaymentMethodDTO } from '~/application/payment-method/dto/bank-payment-method.dto';
|
||||
import { AuthRoles } from '~/application/auth/decorators/auth.decorator';
|
||||
@@ -31,6 +33,14 @@ export class BranchDTO implements BranchDomainInterface {
|
||||
@IsArray()
|
||||
public readonly trusted: IndividualDTO[];
|
||||
|
||||
// публичная часть (сертификаты — только ФИО и имя аккаунта) доступна любому
|
||||
// пайщику; полные персональные данные выше остаются под ограничением ролей
|
||||
@Field(() => IndividualCertificateDTO, { description: 'Сертификат председателя кооперативного участка (ФИО)' })
|
||||
public readonly trustee_certificate: IndividualCertificateDTO;
|
||||
|
||||
@Field(() => [IndividualCertificateDTO], { description: 'Сертификаты доверенных лиц участка (ФИО)' })
|
||||
public readonly trusted_certificates: IndividualCertificateDTO[];
|
||||
|
||||
@Field(() => String, { description: 'Тип организации' })
|
||||
@IsString()
|
||||
public readonly type: string;
|
||||
@@ -79,11 +89,33 @@ export class BranchDTO implements BranchDomainInterface {
|
||||
@IsJSON()
|
||||
public readonly details: OrganizationDetailsDTO;
|
||||
|
||||
@Field(() => Int, { description: 'Количество пайщиков, состоящих в кооперативном участке' })
|
||||
public readonly participants_count: number;
|
||||
|
||||
@Field(() => Boolean, {
|
||||
description: 'Приватный кооперативный участок: выбрать его при вступлении или смене могут только пайщики из белого списка',
|
||||
})
|
||||
public readonly is_private: boolean;
|
||||
|
||||
@Field(() => Boolean, {
|
||||
description: 'Доступен ли участок текущему пайщику для выбора (публичный участок либо пайщик в белом списке)',
|
||||
})
|
||||
public readonly is_available: boolean;
|
||||
|
||||
// список ФИО пайщиков белого списка нужен председателю для управления приватным участком
|
||||
@Field(() => [IndividualCertificateDTO], { description: 'Пайщики в белом списке приватного участка (ФИО)' })
|
||||
@AuthRoles(['chairman', 'member'])
|
||||
public readonly whitelist_certificates: IndividualCertificateDTO[];
|
||||
|
||||
constructor(entity: BranchDomainEntity) {
|
||||
this.coopname = entity.coopname;
|
||||
this.braname = entity.braname;
|
||||
this.trustee = new IndividualDTO(entity.trustee);
|
||||
this.trusted = entity.trusted.map((trustedEntity) => new IndividualDTO(trustedEntity));
|
||||
this.trustee_certificate = new IndividualCertificateDTO({ ...entity.trustee, type: AccountType.individual });
|
||||
this.trusted_certificates = entity.trusted.map(
|
||||
(trustedEntity) => new IndividualCertificateDTO({ ...trustedEntity, type: AccountType.individual }),
|
||||
);
|
||||
this.type = entity.type;
|
||||
this.short_name = entity.short_name;
|
||||
this.full_name = entity.full_name;
|
||||
@@ -96,5 +128,11 @@ export class BranchDTO implements BranchDomainInterface {
|
||||
this.email = entity.email;
|
||||
this.details = new OrganizationDetailsDTO(entity.details);
|
||||
this.bank_account = new BankPaymentMethodDTO(entity.bank_account);
|
||||
this.participants_count = entity.participants_count;
|
||||
this.is_private = entity.is_private;
|
||||
this.is_available = entity.is_available;
|
||||
this.whitelist_certificates = entity.whitelist_members.map(
|
||||
(member) => new IndividualCertificateDTO({ ...member, type: AccountType.individual }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
import type { DeleteBranchWhitelistDomainInterface } from '~/domain/branch/interfaces/delete-branch-whitelist-domain-input.interface';
|
||||
|
||||
@InputType('DeleteBranchWhitelistInput')
|
||||
export class DeleteBranchWhitelistGraphQLInput implements DeleteBranchWhitelistDomainInterface {
|
||||
@Field(() => String, { description: 'Имя аккаунта кооператива' })
|
||||
coopname!: string;
|
||||
|
||||
@Field(() => String, { description: 'Имя аккаунта кооперативного участка' })
|
||||
braname!: string;
|
||||
|
||||
@Field(() => String, { description: 'Имя аккаунта пайщика, удаляемого из белого списка участка' })
|
||||
account!: string;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
import type { SetBranchPrivateDomainInterface } from '~/domain/branch/interfaces/set-branch-private-domain-input.interface';
|
||||
|
||||
@InputType('SetBranchPrivateInput')
|
||||
export class SetBranchPrivateGraphQLInput implements SetBranchPrivateDomainInterface {
|
||||
@Field(() => String, { description: 'Имя аккаунта кооператива' })
|
||||
coopname!: string;
|
||||
|
||||
@Field(() => String, { description: 'Имя аккаунта кооперативного участка' })
|
||||
braname!: string;
|
||||
|
||||
@Field(() => Boolean, {
|
||||
description: 'Признак приватности участка: при включении выбрать участок смогут только пайщики из белого списка',
|
||||
})
|
||||
is_private!: boolean;
|
||||
}
|
||||
@@ -11,7 +11,12 @@ import { EditBranchGraphQLInput } from '../dto/edit-branch-input.dto';
|
||||
import { DeleteBranchGraphQLInput } from '../dto/delete-branch-input.dto';
|
||||
import { AddTrustedAccountGraphQLInput } from '../dto/add-trusted-account-input.dto';
|
||||
import { DeleteTrustedAccountGraphQLInput } from '../dto/delete-trusted-account-input.dto';
|
||||
import { SetBranchPrivateGraphQLInput } from '../dto/set-branch-private-input.dto';
|
||||
import { AddBranchWhitelistGraphQLInput } from '../dto/add-branch-whitelist-input.dto';
|
||||
import { DeleteBranchWhitelistGraphQLInput } from '../dto/delete-branch-whitelist-input.dto';
|
||||
import { SelectBranchInputDTO } from '../dto/select-branch-input.dto';
|
||||
import { CurrentUser } from '~/application/auth/decorators/current-user.decorator';
|
||||
import type { MonoAccountDomainInterface } from '~/domain/account/interfaces/mono-account-domain.interface';
|
||||
import { SelectBranchGenerateDocumentInputDTO } from '../../document/documents-dto/select-branch-document.dto';
|
||||
import { GenerateDocumentOptionsInputDTO } from '~/application/document/dto/generate-document-options-input.dto';
|
||||
import { GeneratedDocumentDTO } from '~/application/document/dto/generated-document.dto';
|
||||
@@ -26,9 +31,11 @@ export class BranchResolver {
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
async getBranches(
|
||||
@Args('data', { type: () => GetBranchesGraphQLInput }) data: GetBranchesGraphQLInput
|
||||
@Args('data', { type: () => GetBranchesGraphQLInput }) data: GetBranchesGraphQLInput,
|
||||
@CurrentUser() currentUser: MonoAccountDomainInterface
|
||||
): Promise<BranchDTO[]> {
|
||||
return this.branchService.getBranches(data);
|
||||
// имя текущего пайщика нужно, чтобы вычислить доступность приватных участков (is_available)
|
||||
return this.branchService.getBranches(data, currentUser?.username);
|
||||
}
|
||||
|
||||
@Mutation(() => BranchDTO, { name: 'createBranch', description: 'Создать кооперативный участок' })
|
||||
@@ -80,6 +87,42 @@ export class BranchResolver {
|
||||
return this.branchService.deleteTrustedAccount(data);
|
||||
}
|
||||
|
||||
@Mutation(() => BranchDTO, {
|
||||
name: 'setBranchPrivate',
|
||||
description: 'Установить приватность кооперативного участка (выбор только из белого списка)',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['chairman'])
|
||||
async setBranchPrivate(
|
||||
@Args('data', { type: () => SetBranchPrivateGraphQLInput }) data: SetBranchPrivateGraphQLInput
|
||||
): Promise<BranchDTO> {
|
||||
return this.branchService.setBranchPrivate(data);
|
||||
}
|
||||
|
||||
@Mutation(() => BranchDTO, {
|
||||
name: 'addBranchWhitelist',
|
||||
description: 'Добавить пайщика в белый список приватного кооперативного участка',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['chairman'])
|
||||
async addBranchWhitelist(
|
||||
@Args('data', { type: () => AddBranchWhitelistGraphQLInput }) data: AddBranchWhitelistGraphQLInput
|
||||
): Promise<BranchDTO> {
|
||||
return this.branchService.addBranchWhitelist(data);
|
||||
}
|
||||
|
||||
@Mutation(() => BranchDTO, {
|
||||
name: 'deleteBranchWhitelist',
|
||||
description: 'Удалить пайщика из белого списка приватного кооперативного участка',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['chairman'])
|
||||
async deleteBranchWhitelist(
|
||||
@Args('data', { type: () => DeleteBranchWhitelistGraphQLInput }) data: DeleteBranchWhitelistGraphQLInput
|
||||
): Promise<BranchDTO> {
|
||||
return this.branchService.deleteBranchWhitelist(data);
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean, { name: 'selectBranch', description: 'Выбрать кооперативный участок' })
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['chairman', 'member', 'user'])
|
||||
|
||||
@@ -7,6 +7,9 @@ import type { CreateBranchGraphQLInput } from '../dto/create-branch-input.dto';
|
||||
import type { DeleteBranchGraphQLInput } from '../dto/delete-branch-input.dto';
|
||||
import type { AddTrustedAccountGraphQLInput } from '../dto/add-trusted-account-input.dto';
|
||||
import type { DeleteTrustedAccountGraphQLInput } from '../dto/delete-trusted-account-input.dto';
|
||||
import type { SetBranchPrivateGraphQLInput } from '../dto/set-branch-private-input.dto';
|
||||
import type { AddBranchWhitelistGraphQLInput } from '../dto/add-branch-whitelist-input.dto';
|
||||
import type { DeleteBranchWhitelistGraphQLInput } from '../dto/delete-branch-whitelist-input.dto';
|
||||
import type { SelectBranchInputDTO } from '../dto/select-branch-input.dto';
|
||||
import type { GenerateDocumentOptionsInputDTO } from '~/application/document/dto/generate-document-options-input.dto';
|
||||
import type { SelectBranchGenerateDocumentInputDTO } from '../../document/documents-dto/select-branch-document.dto';
|
||||
@@ -16,8 +19,8 @@ import type { GeneratedDocumentDTO } from '~/application/document/dto/generated-
|
||||
export class BranchService {
|
||||
constructor(private readonly branchInteractor: BranchInteractor) {}
|
||||
|
||||
public async getBranches(data: GetBranchesGraphQLInput): Promise<BranchDTO[]> {
|
||||
const branches = await this.branchInteractor.getBranches(data);
|
||||
public async getBranches(data: GetBranchesGraphQLInput, currentUsername?: string): Promise<BranchDTO[]> {
|
||||
const branches = await this.branchInteractor.getBranches(data, currentUsername);
|
||||
|
||||
const result: BranchDTO[] = [];
|
||||
|
||||
@@ -56,6 +59,21 @@ export class BranchService {
|
||||
return new BranchDTO(branch);
|
||||
}
|
||||
|
||||
public async setBranchPrivate(data: SetBranchPrivateGraphQLInput): Promise<BranchDTO> {
|
||||
const branch = await this.branchInteractor.setBranchPrivate(data);
|
||||
return new BranchDTO(branch);
|
||||
}
|
||||
|
||||
public async addBranchWhitelist(data: AddBranchWhitelistGraphQLInput): Promise<BranchDTO> {
|
||||
const branch = await this.branchInteractor.addBranchWhitelist(data);
|
||||
return new BranchDTO(branch);
|
||||
}
|
||||
|
||||
public async deleteBranchWhitelist(data: DeleteBranchWhitelistGraphQLInput): Promise<BranchDTO> {
|
||||
const branch = await this.branchInteractor.deleteBranchWhitelist(data);
|
||||
return new BranchDTO(branch);
|
||||
}
|
||||
|
||||
public async selectBranch(data: SelectBranchInputDTO): Promise<boolean> {
|
||||
const selected = await this.branchInteractor.selectBranch(data);
|
||||
return selected;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { BRANCH_BLOCKCHAIN_PORT, BranchBlockchainPort } from '~/domain/branch/in
|
||||
import type { GetBranchesDomainInput } from '~/domain/branch/interfaces/get-branches-domain-input.interface';
|
||||
import { BranchDomainEntity } from '~/domain/branch/entities/branch-domain.entity';
|
||||
import { ORGANIZATION_REPOSITORY, OrganizationRepository } from '~/domain/common/repositories/organization.repository';
|
||||
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
||||
import { BadRequestException, Inject, Injectable, Logger } from '@nestjs/common';
|
||||
import type { CreateBranchDomainInput } from '~/domain/branch/interfaces/create-branch-domain-input.interface';
|
||||
import { HttpApiError } from '~/utils/httpApiError';
|
||||
import httpStatus from 'http-status';
|
||||
@@ -10,6 +10,9 @@ import type { EditBranchDomainInput } from '~/domain/branch/interfaces/edit-bran
|
||||
import type { DeleteBranchDomainInput } from '~/domain/branch/interfaces/delete-branch-domain-input';
|
||||
import type { AddTrustedAccountDomainInterface } from '~/domain/branch/interfaces/add-trusted-account-domain-input.interface';
|
||||
import type { DeleteTrustedAccountDomainInterface } from '~/domain/branch/interfaces/delete-trusted-account-domain-input.interface';
|
||||
import type { SetBranchPrivateDomainInterface } from '~/domain/branch/interfaces/set-branch-private-domain-input.interface';
|
||||
import type { AddBranchWhitelistDomainInterface } from '~/domain/branch/interfaces/add-branch-whitelist-domain-input.interface';
|
||||
import type { DeleteBranchWhitelistDomainInterface } from '~/domain/branch/interfaces/delete-branch-whitelist-domain-input.interface';
|
||||
import { INDIVIDUAL_REPOSITORY, IndividualRepository } from '~/domain/common/repositories/individual.repository';
|
||||
import { IndividualDomainEntity } from '~/domain/branch/entities/individual-domain.entity';
|
||||
import { OrganizationDomainEntity } from '~/domain/branch/entities/organization-domain.entity';
|
||||
@@ -26,6 +29,8 @@ import { DocumentDomainEntity } from '~/domain/document/entity/document-domain.e
|
||||
|
||||
@Injectable()
|
||||
export class BranchInteractor {
|
||||
private readonly logger = new Logger(BranchInteractor.name);
|
||||
|
||||
constructor(
|
||||
@Inject(PAYMENT_METHOD_REPOSITORY) private readonly paymentMethodRepository: PaymentMethodRepository,
|
||||
@Inject(ORGANIZATION_REPOSITORY) private readonly organizationRepository: OrganizationRepository,
|
||||
@@ -35,7 +40,26 @@ export class BranchInteractor {
|
||||
private readonly documentInteractor: DocumentInteractor
|
||||
) {}
|
||||
|
||||
async getBranch(coopname: string, braname: string): Promise<BranchDomainEntity> {
|
||||
// Количество пайщиков на каждом участке: участники совета (status accepted),
|
||||
// у которых проставлен braname. Председатель и доверенные участка — тоже пайщики
|
||||
// и входят в это число. Возвращаем карту braname → количество за одно чтение таблицы.
|
||||
private async countParticipantsByBranch(coopname: string): Promise<Map<string, number>> {
|
||||
const participants = await this.branchBlockchainPort.getParticipants(coopname);
|
||||
const counts = new Map<string, number>();
|
||||
for (const participant of participants) {
|
||||
if (participant.status === 'accepted' && participant.braname) {
|
||||
counts.set(participant.braname, (counts.get(participant.braname) ?? 0) + 1);
|
||||
}
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
async getBranch(
|
||||
coopname: string,
|
||||
braname: string,
|
||||
participantsCount?: number,
|
||||
currentUsername?: string
|
||||
): Promise<BranchDomainEntity> {
|
||||
const branch = await this.branchBlockchainPort.getBranch(coopname, braname);
|
||||
|
||||
if (!branch) {
|
||||
@@ -51,6 +75,19 @@ export class BranchInteractor {
|
||||
trustedData.push(new IndividualDomainEntity(tr));
|
||||
}
|
||||
|
||||
// белый список приватного участка — резолвим ФИО для управления председателем
|
||||
// (поля is_private/whitelist приходят из binary_extension контракта и могут отсутствовать у старых записей)
|
||||
const whitelist = branch.whitelist ?? [];
|
||||
const whitelistMembers: IndividualDomainEntity[] = [];
|
||||
for (const account of whitelist) {
|
||||
const member = await this.individualRepository.findByUsername(account);
|
||||
if (member) whitelistMembers.push(new IndividualDomainEntity(member));
|
||||
}
|
||||
|
||||
// доступность участка для выбора текущим пайщиком: публичный либо пайщик в белом списке
|
||||
const isPrivate = branch.is_private ?? false;
|
||||
const isAvailable = !isPrivate || (!!currentUsername && whitelist.includes(currentUsername));
|
||||
|
||||
const bankAccount = new BankPaymentMethodDTO(
|
||||
await this.paymentMethodRepository.get({
|
||||
username: braname,
|
||||
@@ -59,19 +96,50 @@ export class BranchInteractor {
|
||||
})
|
||||
);
|
||||
|
||||
return new BranchDomainEntity(coopname, branch, databaseData, trusteeData, trustedData, bankAccount);
|
||||
// при одиночном запросе считаем число пайщиков сами; в списке оно приходит готовым
|
||||
const count = participantsCount ?? (await this.countParticipantsByBranch(coopname)).get(braname) ?? 0;
|
||||
|
||||
return new BranchDomainEntity(
|
||||
coopname,
|
||||
branch,
|
||||
databaseData,
|
||||
trusteeData,
|
||||
trustedData,
|
||||
bankAccount,
|
||||
count,
|
||||
whitelistMembers,
|
||||
isAvailable
|
||||
);
|
||||
}
|
||||
|
||||
async getBranches(data: GetBranchesDomainInput): Promise<BranchDomainEntity[]> {
|
||||
async getBranches(data: GetBranchesDomainInput, currentUsername?: string): Promise<BranchDomainEntity[]> {
|
||||
const branches = await this.branchBlockchainPort.getBranches(data.coopname);
|
||||
|
||||
// Фильтрация до сборки
|
||||
const filteredBranches = data.braname ? branches.filter((branch) => branch.braname === data.braname) : branches;
|
||||
|
||||
// одно чтение таблицы участников на весь список, далее раздаём по braname
|
||||
const participantCounts = await this.countParticipantsByBranch(data.coopname);
|
||||
|
||||
const result: BranchDomainEntity[] = [];
|
||||
for (const branch of filteredBranches) {
|
||||
const branchEntity = await this.getBranch(data.coopname, branch.braname);
|
||||
result.push(branchEntity);
|
||||
try {
|
||||
const branchEntity = await this.getBranch(
|
||||
data.coopname,
|
||||
branch.braname,
|
||||
participantCounts.get(branch.braname) ?? 0,
|
||||
currentUsername
|
||||
);
|
||||
result.push(branchEntity);
|
||||
} catch (error) {
|
||||
// Карточка участка (организация/председатель/реквизиты) после одобрения советом
|
||||
// создаётся обработчиком события confirmdec асинхронно. Пока проекция не догнала
|
||||
// блокчейн, getBranch бросает «Организация не найдена». Пропускаем не-готовый
|
||||
// участок, чтобы один такой не ронял весь список — он появится на следующей загрузке.
|
||||
this.logger.warn(
|
||||
`getBranches: участок ${branch.braname} ещё не материализован — пропущен (${(error as Error).message})`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -240,6 +308,54 @@ export class BranchInteractor {
|
||||
return await this.getBranch(data.coopname, data.braname);
|
||||
}
|
||||
|
||||
async setBranchPrivate(data: SetBranchPrivateDomainInterface): Promise<BranchDomainEntity> {
|
||||
const existingBranch = await this.branchBlockchainPort.getBranch(data.coopname, data.braname);
|
||||
|
||||
if (!existingBranch) {
|
||||
throw new HttpApiError(httpStatus.BAD_REQUEST, 'Кооперативный участок не найден');
|
||||
}
|
||||
|
||||
await this.branchBlockchainPort.setBranchPrivate({
|
||||
coopname: data.coopname,
|
||||
braname: data.braname,
|
||||
is_private: data.is_private,
|
||||
});
|
||||
|
||||
return await this.getBranch(data.coopname, data.braname);
|
||||
}
|
||||
|
||||
async addBranchWhitelist(data: AddBranchWhitelistDomainInterface): Promise<BranchDomainEntity> {
|
||||
const existingBranch = await this.branchBlockchainPort.getBranch(data.coopname, data.braname);
|
||||
|
||||
if (!existingBranch) {
|
||||
throw new HttpApiError(httpStatus.BAD_REQUEST, 'Кооперативный участок не найден');
|
||||
}
|
||||
|
||||
await this.branchBlockchainPort.addBranchWhitelist({
|
||||
coopname: data.coopname,
|
||||
braname: data.braname,
|
||||
account: data.account,
|
||||
});
|
||||
|
||||
return await this.getBranch(data.coopname, data.braname);
|
||||
}
|
||||
|
||||
async deleteBranchWhitelist(data: DeleteBranchWhitelistDomainInterface): Promise<BranchDomainEntity> {
|
||||
const existingBranch = await this.branchBlockchainPort.getBranch(data.coopname, data.braname);
|
||||
|
||||
if (!existingBranch) {
|
||||
throw new HttpApiError(httpStatus.BAD_REQUEST, 'Кооперативный участок не найден');
|
||||
}
|
||||
|
||||
await this.branchBlockchainPort.deleteBranchWhitelist({
|
||||
coopname: data.coopname,
|
||||
braname: data.braname,
|
||||
account: data.account,
|
||||
});
|
||||
|
||||
return await this.getBranch(data.coopname, data.braname);
|
||||
}
|
||||
|
||||
async selectBranch(data: SelectBranchInputDomainInterface): Promise<boolean> {
|
||||
// TODO move it to separate document domain service for validate
|
||||
const document = await this.documentRepository.findByHash(data.document.doc_hash);
|
||||
|
||||
@@ -39,7 +39,10 @@ export class SettingsDTO implements SettingsDomainInterface {
|
||||
@Field(() => Date, { description: 'Дата последнего обновления' })
|
||||
updated_at!: Date;
|
||||
|
||||
constructor(data: SettingsDomainInterface) {
|
||||
@Field(() => Boolean, { description: 'Открыта ли регистрация новых пайщиков' })
|
||||
is_registration_open!: boolean;
|
||||
|
||||
constructor(data: SettingsDomainInterface, isRegistrationOpen = false) {
|
||||
this.coopname = data.coopname;
|
||||
this.authorized_default_workspace = data.authorized_default_workspace;
|
||||
this.authorized_default_route = data.authorized_default_route;
|
||||
@@ -48,6 +51,7 @@ export class SettingsDTO implements SettingsDomainInterface {
|
||||
this.provider_name = data.provider_name;
|
||||
this.created_at = data.created_at;
|
||||
this.updated_at = data.updated_at;
|
||||
this.is_registration_open = isRegistrationOpen;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import { SymbolsDTO } from './symbols.dto';
|
||||
import { SettingsDTO } from './settings.dto';
|
||||
import { BoardMemberDTO } from './board-member.dto';
|
||||
import { SystemFeaturesDTO } from './system-features.dto';
|
||||
import { isRegistrationOpen } from '~/domain/system/utils/is-registration-open.util';
|
||||
|
||||
@ObjectType('SystemInfo')
|
||||
export class SystemInfoDTO {
|
||||
@@ -79,7 +80,7 @@ export class SystemInfoDTO {
|
||||
this.blockchain_info = new BlockchainInfoDTO(entity.blockchain_info);
|
||||
this.system_status = entity.system_status || 'install';
|
||||
this.symbols = entity.symbols;
|
||||
this.settings = new SettingsDTO(entity.settings);
|
||||
this.settings = new SettingsDTO(entity.settings, isRegistrationOpen(entity.vars));
|
||||
this.blockchain_account = entity.blockchain_account;
|
||||
this.cooperator_account = new CooperativeOperatorAccountDTO({
|
||||
...entity.cooperator_account,
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
PaymentMethodDomainPort,
|
||||
} from '~/domain/payment-method/ports/payment-method-domain.port';
|
||||
import { LoadContactsInteractor } from './load-contacts.interactor';
|
||||
import { isRegistrationOpen } from '~/domain/system/utils/is-registration-open.util';
|
||||
|
||||
@Injectable()
|
||||
export class SystemInteractor {
|
||||
@@ -307,4 +308,9 @@ export class SystemInteractor {
|
||||
async updateSettings(updates: UpdateSettingsInputDomainInterface): Promise<SettingsDomainEntity> {
|
||||
return this.settingsDomainPort.updateSettings(updates);
|
||||
}
|
||||
|
||||
async getRegistrationOpenStatus(): Promise<boolean> {
|
||||
const vars = await this.varsRepository.get();
|
||||
return isRegistrationOpen(vars);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,8 +99,11 @@ export class SystemService {
|
||||
* Обновляет настройки системы
|
||||
*/
|
||||
public async updateSettings(data: UpdateSettingsInputDTO): Promise<SettingsDTO> {
|
||||
const settings = await this.systemInteractor.updateSettings(data);
|
||||
return new SettingsDTO(settings);
|
||||
const [settings, isRegistrationOpenStatus] = await Promise.all([
|
||||
this.systemInteractor.updateSettings(data),
|
||||
this.systemInteractor.getRegistrationOpenStatus(),
|
||||
]);
|
||||
return new SettingsDTO(settings, isRegistrationOpenStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -33,6 +33,10 @@ export class BranchDomainEntity implements BranchDomainInterface {
|
||||
ogrn: string;
|
||||
kpp: string;
|
||||
};
|
||||
public readonly participants_count: number;
|
||||
public readonly is_private: boolean;
|
||||
public readonly is_available: boolean;
|
||||
public readonly whitelist_members: IndividualDomainInterface[];
|
||||
|
||||
constructor(
|
||||
coopname: string,
|
||||
@@ -40,7 +44,10 @@ export class BranchDomainEntity implements BranchDomainInterface {
|
||||
organizationDatabaseData: OrganizationDomainInterface,
|
||||
trusteeData: IndividualDomainInterface,
|
||||
trustedData: IndividualDomainInterface[],
|
||||
bankAccount: BankPaymentMethodDomainInterface
|
||||
bankAccount: BankPaymentMethodDomainInterface,
|
||||
participantsCount = 0,
|
||||
whitelistMembers: IndividualDomainInterface[] = [],
|
||||
isAvailable = true
|
||||
) {
|
||||
if (branchBlockchainData.braname != organizationDatabaseData.username)
|
||||
throw new Error(`Неверные данные для агрегата: username и braname кооперативного участка должны совпадать`);
|
||||
@@ -63,5 +70,11 @@ export class BranchDomainEntity implements BranchDomainInterface {
|
||||
this.phone = organizationDatabaseData.phone;
|
||||
this.email = organizationDatabaseData.email;
|
||||
this.details = organizationDatabaseData.details;
|
||||
this.participants_count = participantsCount;
|
||||
|
||||
// признак приватности приходит из binary_extension контракта: у старых записей отсутствует → публичный
|
||||
this.is_private = branchBlockchainData.is_private ?? false;
|
||||
this.whitelist_members = whitelistMembers;
|
||||
this.is_available = isAvailable;
|
||||
}
|
||||
}
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
import { BranchContract } from 'cooptypes';
|
||||
|
||||
export type AddBranchWhitelistDomainInterface = BranchContract.Actions.AddWhite.IAddWhite;
|
||||
@@ -6,9 +6,13 @@ export interface BranchBlockchainPort {
|
||||
editBranch(data: BranchContract.Actions.EditBranch.IEditBranch): Promise<TransactResult>;
|
||||
getBranches(coopname: string): Promise<BranchContract.Tables.Branches.IBranch[]>;
|
||||
getBranch(coopname: string, braname: string): Promise<BranchContract.Tables.Branches.IBranch | null>;
|
||||
getParticipants(coopname: string): Promise<SovietContract.Tables.Participants.IParticipants[]>;
|
||||
deleteBranch(data: BranchContract.Actions.DeleteBranch.IDeleteBranch): Promise<TransactResult>;
|
||||
addTrustedAccount(data: BranchContract.Actions.AddTrusted.IAddTrusted): Promise<TransactResult>;
|
||||
deleteTrustedAccount(data: BranchContract.Actions.DeleteTrusted.IDeleteTrusted): Promise<TransactResult>;
|
||||
setBranchPrivate(data: BranchContract.Actions.SetPrivate.ISetPrivate): Promise<TransactResult>;
|
||||
addBranchWhitelist(data: BranchContract.Actions.AddWhite.IAddWhite): Promise<TransactResult>;
|
||||
deleteBranchWhitelist(data: BranchContract.Actions.DelWhite.IDelWhite): Promise<TransactResult>;
|
||||
selectBranch(data: SovietContract.Actions.Branches.SelectBranch.ISelectBranch): Promise<TransactResult>;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,4 +5,7 @@ export type BranchDomainInterface = Omit<OrganizationDomainInterface, 'username'
|
||||
braname: string; ///< имя аккаунта
|
||||
trustee: IndividualDomainInterface;
|
||||
trusted: IndividualDomainInterface[];
|
||||
participants_count: number; ///< количество пайщиков, состоящих в участке
|
||||
is_private: boolean; ///< приватный участок: выбрать его можно только из белого списка
|
||||
is_available: boolean; ///< доступен ли участок текущему пайщику для выбора (публичный или он в белом списке)
|
||||
};
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
import { BranchContract } from 'cooptypes';
|
||||
|
||||
export type DeleteBranchWhitelistDomainInterface = BranchContract.Actions.DelWhite.IDelWhite;
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
import { BranchContract } from 'cooptypes';
|
||||
|
||||
export type SetBranchPrivateDomainInterface = BranchContract.Actions.SetPrivate.ISetPrivate;
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { AgreementNumberDomainInterface } from '~/domain/agreement/interfaces/agreement-number.interface';
|
||||
import type { VarsDomainInterface } from '~/domain/system/interfaces/vars-domain.interface';
|
||||
|
||||
const REGISTRATION_REQUIRED_AGREEMENT_VARS = [
|
||||
'wallet_agreement',
|
||||
'signature_agreement',
|
||||
'privacy_agreement',
|
||||
'user_agreement',
|
||||
'participant_application',
|
||||
] as const satisfies ReadonlyArray<keyof VarsDomainInterface>;
|
||||
|
||||
function isAgreementVarFilled(agreement?: AgreementNumberDomainInterface | null): boolean {
|
||||
if (!agreement) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const protocolNumber = agreement.protocol_number?.trim();
|
||||
const protocolDate = agreement.protocol_day_month_year?.trim();
|
||||
|
||||
return Boolean(protocolNumber) && Boolean(protocolDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Регистрация пайщиков доступна, когда заполнены реквизиты протоколов
|
||||
* базовых соглашений кооператива — без них factory не сгенерирует пакет документов.
|
||||
*/
|
||||
export function isRegistrationOpen(vars: VarsDomainInterface | null | undefined): boolean {
|
||||
if (!vars) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return REGISTRATION_REQUIRED_AGREEMENT_VARS.every((field) =>
|
||||
isAgreementVarFilled(vars[field] as AgreementNumberDomainInterface | null | undefined)
|
||||
);
|
||||
}
|
||||
+1
-1
@@ -873,7 +873,7 @@ export class CapitalBlockchainAdapter implements CapitalBlockchainPort {
|
||||
): Promise<CapitalContract.Tables.Segments.ISegment | null> {
|
||||
// Создаем составной ключ для поиска по индексу by_project_user (позиция 3)
|
||||
const compositeKey = this.domainToBlockchainUtils.combineChecksumAndUsername(projectHash, username);
|
||||
const keyUInt128 = UInt128.from(compositeKey);
|
||||
const keyUInt128 = UInt128.from(compositeKey.toString());
|
||||
|
||||
// Получаем сегмент из таблицы segments контракта capital
|
||||
const segment = await this.blockchainService.getSingleRow<CapitalContract.Tables.Segments.ISegment>(
|
||||
|
||||
@@ -11,6 +11,7 @@ import { ParticipantPluginModule } from './participant/participant-extension.mod
|
||||
import { ChatCoopPluginModule } from './chatcoop/chatcoop-extension.module';
|
||||
import { OneCoopPluginModule } from './1ccoop/oneccoop-extension.module';
|
||||
import { ReportsExtensionModule } from './reports/reports-extension.module';
|
||||
import { KuPluginModule } from './ku/ku-extension.module';
|
||||
import { ExpensesExtensionModule } from './expenses/expenses-extension.module';
|
||||
import { ExtensionDomainModule } from '~/domain/extension/extension-domain.module';
|
||||
import { GatewayDomainModule } from '~/domain/gateway/gateway-domain.module';
|
||||
@@ -37,6 +38,7 @@ export class ExtensionsModule {
|
||||
InterCommunicationBridgeModule,
|
||||
OneCoopPluginModule,
|
||||
ReportsExtensionModule,
|
||||
KuPluginModule,
|
||||
ExpensesExtensionModule,
|
||||
],
|
||||
providers: [],
|
||||
@@ -55,6 +57,7 @@ export class ExtensionsModule {
|
||||
InterCommunicationBridgeModule,
|
||||
OneCoopPluginModule,
|
||||
ReportsExtensionModule,
|
||||
KuPluginModule,
|
||||
ExpensesExtensionModule,
|
||||
],
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@ import { Schema as ParticipantSchema } from './participant/types';
|
||||
import { OneCoopPluginModule, OneCoopPlugin, Schema as OneCoopSchema } from './1ccoop/oneccoop-extension.module';
|
||||
import { CapitalPluginModule, CapitalPlugin, Schema as CapitalSchema } from './capital/capital-extension.module';
|
||||
import { ReportsExtensionModule } from './reports/reports-extension.module';
|
||||
import { KuPluginModule, KuPlugin, Schema as KuSchema } from './ku/ku-extension.module';
|
||||
|
||||
/**
|
||||
* Конфигурация рабочего стола (workspace), который предоставляет расширение
|
||||
@@ -141,23 +142,23 @@ export const AppRegistry: INamedExtension = {
|
||||
trustee: {
|
||||
is_builtin: true,
|
||||
is_internal: true,
|
||||
is_available: false,
|
||||
is_available: true,
|
||||
desktops: [
|
||||
{
|
||||
name: 'trustee',
|
||||
title: 'Стол Уполномоченного',
|
||||
title: 'Кооперативный участок',
|
||||
icon: 'fa-solid fa-users-cog',
|
||||
},
|
||||
],
|
||||
title: 'Стол Уполномоченного',
|
||||
description: 'Приложение для председателя кооперативного участка.',
|
||||
title: 'Кооперативный участок',
|
||||
description: 'Собрания пайщиков кооперативных участков: учреждение участков решением собрания с утверждением советом, свободные решения и приём доверенных лиц по заявлению.',
|
||||
image: 'https://i.ibb.co/MxbHCqqf/Chat-GPT-Image-11-2025-18-26-44.png',
|
||||
class: BuiltinPluginModule,
|
||||
pluginClass: BuiltinPlugin,
|
||||
schema: BuiltinSchema,
|
||||
class: KuPluginModule,
|
||||
pluginClass: KuPlugin,
|
||||
schema: KuSchema,
|
||||
tags: ['стол', 'управление'],
|
||||
readme: getReadmeContent('./yookassa'),
|
||||
instructions: getInstructionsContent('./yookassa'),
|
||||
readme: getReadmeContent('./ku'),
|
||||
instructions: getInstructionsContent('./ku'),
|
||||
get is_desktop() {
|
||||
return !!this.desktops && this.desktops.length > 0;
|
||||
},
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# Установка
|
||||
|
||||
Расширение встроенное и не требует конфигурации. После установки рабочий стол
|
||||
«Кооперативный участок» становится доступен пайщикам кооператива.
|
||||
@@ -0,0 +1,9 @@
|
||||
# Кооперативный участок
|
||||
|
||||
Расширение-стол для самоорганизации кооперативных участков:
|
||||
|
||||
- **Собрания пайщиков** — любой пайщик объявляет собрание со своей повесткой; участники присоединяются по заявлению, голосуют бюллетенями (за / против / воздержался по каждому вопросу), председатель собрания утверждает протокол одной подписью.
|
||||
- **Учреждение участка** — решение собрания типа «учреждение кооперативного участка» председатель направляет заявлением в совет; по утверждению советом участок создаётся с избранным председателем.
|
||||
- **Доверенные лица** — пайщик подаёт заявление и договор о полной материальной ответственности; председатель участка одобряет встречной подписью (не более трёх доверенных).
|
||||
|
||||
Данные читаются из PG-проекций (`ku_decisions`, `ku_decision_questions`, `ku_trust_requests`), история сохраняется после завершения процессов в блокчейне. Действия отправляются в контракт `branch` с подписью кооператива.
|
||||
@@ -0,0 +1,359 @@
|
||||
import { Field, InputType, Int } from '@nestjs/graphql';
|
||||
import { IsArray, IsNotEmpty, IsNumber, IsString, ValidateNested } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { KuDecisionType } from '../../domain/enums/ku-decision-type.enum';
|
||||
import type {
|
||||
ApproveKuTrustedInputDomainInterface,
|
||||
CancelKuDecisionInputDomainInterface,
|
||||
CloseKuDecisionInputDomainInterface,
|
||||
CreateKuDecisionInputDomainInterface,
|
||||
DeclineKuTrustedInputDomainInterface,
|
||||
ExecKuDecisionInputDomainInterface,
|
||||
JoinKuDecisionInputDomainInterface,
|
||||
KuAgendaPointInputDomainInterface,
|
||||
KuVoteItemInputDomainInterface,
|
||||
RequestKuTrustedInputDomainInterface,
|
||||
StartKuDecisionInputDomainInterface,
|
||||
VoteOnKuDecisionInputDomainInterface,
|
||||
} from '../../domain/interfaces/ku-action-inputs.interface';
|
||||
import {
|
||||
BranchTrusteeLiabilityAgreementSignedDocumentInputDTO,
|
||||
BranchEstablishmentPetitionSignedDocumentInputDTO,
|
||||
BranchTrustedLiabilityAgreementSignedDocumentInputDTO,
|
||||
BranchTrusteePowerOfAttorneySignedDocumentInputDTO,
|
||||
BranchTrustedPowerOfAttorneySignedDocumentInputDTO,
|
||||
BranchMeetingBallotSignedDocumentInputDTO,
|
||||
BranchMeetingDecisionSignedDocumentInputDTO,
|
||||
BranchMeetingProposalSignedDocumentInputDTO,
|
||||
} from './ku-documents.dto';
|
||||
|
||||
@InputType('KuAgendaPointInput', { description: 'Пункт повестки собрания пайщиков участка' })
|
||||
export class KuAgendaPointInputDTO implements KuAgendaPointInputDomainInterface {
|
||||
@Field(() => String, { description: 'Заголовок вопроса' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
title!: string;
|
||||
|
||||
@Field(() => String, { description: 'Проект решения по вопросу' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
decision!: string;
|
||||
|
||||
@Field(() => String, { description: 'Дополнительная информация по вопросу', defaultValue: '' })
|
||||
@IsString()
|
||||
context!: string;
|
||||
}
|
||||
|
||||
@InputType('CreateKuDecisionInput', { description: 'Объявление собрания пайщиков кооперативного участка' })
|
||||
export class CreateKuDecisionInputDTO implements CreateKuDecisionInputDomainInterface {
|
||||
@Field(() => String, { description: 'Имя аккаунта кооператива' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
coopname!: string;
|
||||
|
||||
@Field(() => String, { description: 'Хэш решения собрания (якорь процесса)' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
hash!: string;
|
||||
|
||||
@Field(() => KuDecisionType, { description: 'Тип решения собрания' })
|
||||
type!: KuDecisionType;
|
||||
|
||||
@Field(() => String, { description: 'Инициатор собрания' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
initiator!: string;
|
||||
|
||||
@Field(() => String, {
|
||||
description: 'Имя аккаунта будущего кооперативного участка (для учреждения)',
|
||||
defaultValue: '',
|
||||
})
|
||||
@IsString()
|
||||
braname!: string;
|
||||
|
||||
@Field(() => [KuAgendaPointInputDTO], { description: 'Повестка собрания' })
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => KuAgendaPointInputDTO)
|
||||
agenda!: KuAgendaPointInputDTO[];
|
||||
|
||||
@Field(() => BranchMeetingProposalSignedDocumentInputDTO, { description: 'Подписанное предложение повестки' })
|
||||
@ValidateNested()
|
||||
@Type(() => BranchMeetingProposalSignedDocumentInputDTO)
|
||||
proposal!: BranchMeetingProposalSignedDocumentInputDTO;
|
||||
|
||||
@Field(() => String, { description: 'Место проведения собрания (видно только пайщикам кооператива)' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
meet_place!: string;
|
||||
|
||||
@Field(() => String, { description: 'Дата и время проведения собрания (ISO)' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
meet_at!: string;
|
||||
}
|
||||
|
||||
@InputType('JoinKuDecisionInput', { description: 'Присоединение пайщика к собранию участка' })
|
||||
export class JoinKuDecisionInputDTO implements JoinKuDecisionInputDomainInterface {
|
||||
@Field(() => String, { description: 'Имя аккаунта кооператива' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
coopname!: string;
|
||||
|
||||
@Field(() => String, { description: 'Хэш решения собрания' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
hash!: string;
|
||||
|
||||
@Field(() => String, { description: 'Пайщик, присоединяющийся к собранию' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
username!: string;
|
||||
}
|
||||
|
||||
@InputType('StartKuDecisionInput', { description: 'Открытие голосования на собрании участка' })
|
||||
export class StartKuDecisionInputDTO implements StartKuDecisionInputDomainInterface {
|
||||
@Field(() => String, { description: 'Имя аккаунта кооператива' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
coopname!: string;
|
||||
|
||||
@Field(() => String, { description: 'Хэш решения собрания' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
hash!: string;
|
||||
|
||||
@Field(() => String, { description: 'Избираемый председатель кооперативного участка из числа присоединившихся участников' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
chairman!: string;
|
||||
|
||||
@Field(() => String, { description: 'Адрес привязки кооперативного участка (для учреждения)', defaultValue: '' })
|
||||
@IsString()
|
||||
address!: string;
|
||||
|
||||
@Field(() => String, {
|
||||
description: 'Наименование кооперативного участка (видно только пайщикам кооператива)',
|
||||
defaultValue: '',
|
||||
})
|
||||
@IsString()
|
||||
branch_name!: string;
|
||||
|
||||
@Field(() => String, {
|
||||
description: 'Email кооперативного участка (видно только пайщикам кооператива)',
|
||||
defaultValue: '',
|
||||
})
|
||||
@IsString()
|
||||
branch_email!: string;
|
||||
|
||||
@Field(() => String, {
|
||||
description: 'Телефон кооперативного участка (видно только пайщикам кооператива)',
|
||||
defaultValue: '',
|
||||
})
|
||||
@IsString()
|
||||
branch_phone!: string;
|
||||
|
||||
@Field(() => [KuAgendaPointInputDTO], {
|
||||
description: 'Дополнительные вопросы повестки, внесённые на собрании',
|
||||
defaultValue: [],
|
||||
})
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => KuAgendaPointInputDTO)
|
||||
agenda!: KuAgendaPointInputDTO[];
|
||||
}
|
||||
|
||||
@InputType('KuVoteItemInput', { description: 'Волеизъявление по вопросу повестки собрания участка' })
|
||||
export class KuVoteItemInputDTO implements KuVoteItemInputDomainInterface {
|
||||
@Field(() => Int, { description: 'Идентификатор вопроса повестки' })
|
||||
@IsNumber()
|
||||
question_id!: number;
|
||||
|
||||
@Field(() => String, { description: 'Голос по вопросу (for | against | abstained)' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
vote!: string;
|
||||
}
|
||||
|
||||
@InputType('VoteOnKuDecisionInput', { description: 'Подача бюллетеня на собрании участка' })
|
||||
export class VoteOnKuDecisionInputDTO implements VoteOnKuDecisionInputDomainInterface {
|
||||
@Field(() => String, { description: 'Имя аккаунта кооператива' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
coopname!: string;
|
||||
|
||||
@Field(() => String, { description: 'Хэш решения собрания' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
hash!: string;
|
||||
|
||||
@Field(() => String, { description: 'Голосующий участник собрания' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
username!: string;
|
||||
|
||||
@Field(() => BranchMeetingBallotSignedDocumentInputDTO, { description: 'Подписанный бюллетень' })
|
||||
@ValidateNested()
|
||||
@Type(() => BranchMeetingBallotSignedDocumentInputDTO)
|
||||
ballot!: BranchMeetingBallotSignedDocumentInputDTO;
|
||||
|
||||
@Field(() => [KuVoteItemInputDTO], { description: 'Волеизъявления по вопросам повестки' })
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => KuVoteItemInputDTO)
|
||||
votes!: KuVoteItemInputDTO[];
|
||||
}
|
||||
|
||||
@InputType('CloseKuDecisionInput', { description: 'Закрытие голосования и утверждение протокола собрания участка' })
|
||||
export class CloseKuDecisionInputDTO implements CloseKuDecisionInputDomainInterface {
|
||||
@Field(() => String, { description: 'Имя аккаунта кооператива' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
coopname!: string;
|
||||
|
||||
@Field(() => String, { description: 'Хэш решения собрания' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
hash!: string;
|
||||
|
||||
@Field(() => BranchMeetingDecisionSignedDocumentInputDTO, {
|
||||
description: 'Протокол собрания, утверждённый подписью председателя',
|
||||
})
|
||||
@ValidateNested()
|
||||
@Type(() => BranchMeetingDecisionSignedDocumentInputDTO)
|
||||
protocol!: BranchMeetingDecisionSignedDocumentInputDTO;
|
||||
}
|
||||
|
||||
@InputType('ExecKuDecisionInput', { description: 'Направление заявления председателя собрания в совет' })
|
||||
export class ExecKuDecisionInputDTO implements ExecKuDecisionInputDomainInterface {
|
||||
@Field(() => String, { description: 'Имя аккаунта кооператива' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
coopname!: string;
|
||||
|
||||
@Field(() => String, { description: 'Хэш решения собрания' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
hash!: string;
|
||||
|
||||
@Field(() => BranchEstablishmentPetitionSignedDocumentInputDTO, {
|
||||
description: 'Подписанное заявление председателя в совет',
|
||||
})
|
||||
@ValidateNested()
|
||||
@Type(() => BranchEstablishmentPetitionSignedDocumentInputDTO)
|
||||
petition!: BranchEstablishmentPetitionSignedDocumentInputDTO;
|
||||
|
||||
@Field(() => BranchTrusteeLiabilityAgreementSignedDocumentInputDTO, {
|
||||
description: 'Подписанный председателем участка договор о полной материальной ответственности (идёт в пакете в совет)',
|
||||
})
|
||||
@ValidateNested()
|
||||
@Type(() => BranchTrusteeLiabilityAgreementSignedDocumentInputDTO)
|
||||
liability!: BranchTrusteeLiabilityAgreementSignedDocumentInputDTO;
|
||||
|
||||
@Field(() => BranchTrusteePowerOfAttorneySignedDocumentInputDTO, {
|
||||
description: 'Подписанная председателем участка доверенность председателю участка (идёт в пакете в совет)',
|
||||
})
|
||||
@ValidateNested()
|
||||
@Type(() => BranchTrusteePowerOfAttorneySignedDocumentInputDTO)
|
||||
authority!: BranchTrusteePowerOfAttorneySignedDocumentInputDTO;
|
||||
}
|
||||
|
||||
@InputType('CancelKuDecisionInput', { description: 'Отмена собрания пайщиков участка' })
|
||||
export class CancelKuDecisionInputDTO implements CancelKuDecisionInputDomainInterface {
|
||||
@Field(() => String, { description: 'Имя аккаунта кооператива' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
coopname!: string;
|
||||
|
||||
@Field(() => String, { description: 'Хэш решения собрания' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
hash!: string;
|
||||
|
||||
@Field(() => String, { description: 'Причина отмены' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
reason!: string;
|
||||
}
|
||||
|
||||
@InputType('RequestKuTrustedInput', { description: 'Заявка на приём доверенным лицом кооперативного участка' })
|
||||
export class RequestKuTrustedInputDTO implements RequestKuTrustedInputDomainInterface {
|
||||
@Field(() => String, { description: 'Имя аккаунта кооператива' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
coopname!: string;
|
||||
|
||||
@Field(() => String, { description: 'Имя аккаунта кооперативного участка' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
braname!: string;
|
||||
|
||||
@Field(() => String, { description: 'Пайщик-заявитель' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
username!: string;
|
||||
|
||||
@Field(() => String, { description: 'Хэш заявки' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
hash!: string;
|
||||
|
||||
@Field(() => BranchTrustedLiabilityAgreementSignedDocumentInputDTO, {
|
||||
description: 'Подписанный договор о полной материальной ответственности доверенного лица',
|
||||
})
|
||||
@ValidateNested()
|
||||
@Type(() => BranchTrustedLiabilityAgreementSignedDocumentInputDTO)
|
||||
application!: BranchTrustedLiabilityAgreementSignedDocumentInputDTO;
|
||||
|
||||
@Field(() => BranchTrustedPowerOfAttorneySignedDocumentInputDTO, {
|
||||
description: 'Подписанная доверенным лицом доверенность доверенному лицу/оператору участка',
|
||||
})
|
||||
@ValidateNested()
|
||||
@Type(() => BranchTrustedPowerOfAttorneySignedDocumentInputDTO)
|
||||
authority!: BranchTrustedPowerOfAttorneySignedDocumentInputDTO;
|
||||
}
|
||||
|
||||
@InputType('ApproveKuTrustedInput', { description: 'Одобрение заявки доверенного встречной подписью председателя участка' })
|
||||
export class ApproveKuTrustedInputDTO implements ApproveKuTrustedInputDomainInterface {
|
||||
@Field(() => String, { description: 'Имя аккаунта кооператива' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
coopname!: string;
|
||||
|
||||
@Field(() => String, { description: 'Хэш заявки' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
hash!: string;
|
||||
|
||||
@Field(() => BranchTrustedLiabilityAgreementSignedDocumentInputDTO, {
|
||||
description: 'Договор материальной ответственности со встречной подписью председателя участка',
|
||||
})
|
||||
@ValidateNested()
|
||||
@Type(() => BranchTrustedLiabilityAgreementSignedDocumentInputDTO)
|
||||
countersigned!: BranchTrustedLiabilityAgreementSignedDocumentInputDTO;
|
||||
|
||||
@Field(() => BranchTrustedPowerOfAttorneySignedDocumentInputDTO, {
|
||||
description: 'Доверенность доверенному лицу со встречной подписью председателя участка',
|
||||
})
|
||||
@ValidateNested()
|
||||
@Type(() => BranchTrustedPowerOfAttorneySignedDocumentInputDTO)
|
||||
countersigned_authority!: BranchTrustedPowerOfAttorneySignedDocumentInputDTO;
|
||||
}
|
||||
|
||||
@InputType('DeclineKuTrustedInput', { description: 'Отклонение заявки доверенного лица' })
|
||||
export class DeclineKuTrustedInputDTO implements DeclineKuTrustedInputDomainInterface {
|
||||
@Field(() => String, { description: 'Имя аккаунта кооператива' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
coopname!: string;
|
||||
|
||||
@Field(() => String, { description: 'Хэш заявки' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
hash!: string;
|
||||
|
||||
@Field(() => String, { description: 'Причина отклонения' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
reason!: string;
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
import { Field, InputType, Int, ObjectType } from '@nestjs/graphql';
|
||||
import { GraphQLJSON } from 'graphql-type-json';
|
||||
import { IsBoolean, IsOptional, IsString } from 'class-validator';
|
||||
import { KuDecisionType } from '../../domain/enums/ku-decision-type.enum';
|
||||
import { KuDecisionStatus } from '../../domain/enums/ku-decision-status.enum';
|
||||
import { DocumentAggregateDTO } from '~/application/document/dto/document-aggregate.dto';
|
||||
import { AccountType } from '~/application/account/enum/account-type.enum';
|
||||
|
||||
@ObjectType('KuDecisionQuestion', { description: 'Вопрос повестки собрания пайщиков кооперативного участка' })
|
||||
export class KuDecisionQuestionDTO {
|
||||
@Field(() => Int, { nullable: true, description: 'Идентификатор вопроса в блокчейне' })
|
||||
id?: number;
|
||||
|
||||
@Field(() => Int, { nullable: true, description: 'Идентификатор решения собрания' })
|
||||
decision_id?: number;
|
||||
|
||||
@Field(() => Int, { nullable: true, description: 'Порядковый номер вопроса в повестке' })
|
||||
number?: number;
|
||||
|
||||
@Field(() => String, { nullable: true, description: 'Заголовок вопроса' })
|
||||
title?: string;
|
||||
|
||||
@Field(() => String, { nullable: true, description: 'Проект решения по вопросу' })
|
||||
decision?: string;
|
||||
|
||||
@Field(() => String, { nullable: true, description: 'Дополнительная информация по вопросу' })
|
||||
context?: string;
|
||||
|
||||
@Field(() => Int, { nullable: true, description: 'Количество голосов «за»' })
|
||||
counter_votes_for?: number;
|
||||
|
||||
@Field(() => Int, { nullable: true, description: 'Количество голосов «против»' })
|
||||
counter_votes_against?: number;
|
||||
|
||||
@Field(() => Int, { nullable: true, description: 'Количество голосов «воздержался»' })
|
||||
counter_votes_abstained?: number;
|
||||
|
||||
@Field(() => [String], { nullable: true, description: 'Проголосовавшие «за»' })
|
||||
voters_for?: string[];
|
||||
|
||||
@Field(() => [String], { nullable: true, description: 'Проголосовавшие «против»' })
|
||||
voters_against?: string[];
|
||||
|
||||
@Field(() => [String], { nullable: true, description: 'Проголосовавшие «воздержался»' })
|
||||
voters_abstained?: string[];
|
||||
}
|
||||
|
||||
@ObjectType('KuMeetingParticipant', { description: 'Участник собрания пайщиков кооперативного участка' })
|
||||
export class KuMeetingParticipantDTO {
|
||||
@Field(() => String, { description: 'Имя аккаунта участника' })
|
||||
username!: string;
|
||||
|
||||
@Field(() => String, { description: 'Отображаемое имя участника (ФИО)' })
|
||||
display_name!: string;
|
||||
|
||||
@Field(() => AccountType, { description: 'Тип аккаунта участника' })
|
||||
account_type!: AccountType;
|
||||
}
|
||||
|
||||
@ObjectType('KuDecision', { description: 'Решение собрания пайщиков кооперативного участка' })
|
||||
export class KuDecisionDTO {
|
||||
@Field(() => String, { description: 'Хэш решения (якорь процесса)' })
|
||||
hash!: string;
|
||||
|
||||
@Field(() => Int, { nullable: true, description: 'Идентификатор решения в блокчейне' })
|
||||
id?: number;
|
||||
|
||||
@Field(() => String, { nullable: true, description: 'Имя аккаунта кооператива' })
|
||||
coopname?: string;
|
||||
|
||||
@Field(() => KuDecisionType, { nullable: true, description: 'Тип решения' })
|
||||
type?: KuDecisionType;
|
||||
|
||||
@Field(() => String, { nullable: true, description: 'Инициатор собрания' })
|
||||
initiator?: string;
|
||||
|
||||
@Field(() => String, { nullable: true, description: 'Председатель собрания' })
|
||||
chairman?: string;
|
||||
|
||||
@Field(() => KuDecisionStatus, { nullable: true, description: 'Статус решения' })
|
||||
status?: KuDecisionStatus;
|
||||
|
||||
@Field(() => Boolean, { description: 'Существует ли запись в блокчейне (false — завершено и стёрто)' })
|
||||
present!: boolean;
|
||||
|
||||
@Field(() => GraphQLJSON, { nullable: true, description: 'Подписанное предложение повестки' })
|
||||
proposal?: object;
|
||||
|
||||
@Field(() => GraphQLJSON, { nullable: true, description: 'Утверждённый протокол собрания' })
|
||||
protocol?: object;
|
||||
|
||||
@Field(() => GraphQLJSON, { nullable: true, description: 'Заявление председателя в совет' })
|
||||
petition?: object;
|
||||
|
||||
@Field(() => GraphQLJSON, { nullable: true, description: 'Решение совета' })
|
||||
authorization?: object;
|
||||
|
||||
@Field(() => DocumentAggregateDTO, {
|
||||
nullable: true,
|
||||
description: 'Протокол собрания пайщиков с подписью и бюллетенями — для отображения на странице собрания',
|
||||
})
|
||||
protocol_document?: DocumentAggregateDTO;
|
||||
|
||||
@Field(() => DocumentAggregateDTO, {
|
||||
nullable: true,
|
||||
description: 'Решение совета об организации кооперативного участка — для отображения на странице собрания',
|
||||
})
|
||||
authorization_document?: DocumentAggregateDTO;
|
||||
|
||||
@Field(() => String, { nullable: true, description: 'Дата и время открытия голосования' })
|
||||
open_at?: string;
|
||||
|
||||
@Field(() => String, { nullable: true, description: 'Дата и время закрытия голосования' })
|
||||
close_at?: string;
|
||||
|
||||
@Field(() => Int, { nullable: true, description: 'Количество поданных бюллетеней' })
|
||||
signed_ballots?: number;
|
||||
|
||||
@Field(() => String, { nullable: true, description: 'Имя аккаунта кооперативного участка (служебное)' })
|
||||
braname?: string;
|
||||
|
||||
@Field(() => String, { nullable: true, description: 'Адрес привязки кооперативного участка' })
|
||||
address?: string;
|
||||
|
||||
@Field(() => [String], { nullable: true, description: 'Участники собрания' })
|
||||
participants?: string[];
|
||||
|
||||
@Field(() => [KuMeetingParticipantDTO], {
|
||||
nullable: true,
|
||||
description: 'Участники собрания с отображаемыми именами',
|
||||
})
|
||||
participants_info?: KuMeetingParticipantDTO[];
|
||||
|
||||
@Field(() => String, { nullable: true, description: 'Дата и время объявления собрания' })
|
||||
created_at?: string;
|
||||
|
||||
@Field(() => String, { nullable: true, description: 'Место проведения собрания (видно только пайщикам)' })
|
||||
meet_place?: string;
|
||||
|
||||
@Field(() => String, { nullable: true, description: 'Дата и время проведения собрания (видно только пайщикам)' })
|
||||
meet_at?: string;
|
||||
|
||||
@Field(() => String, { nullable: true, description: 'Наименование кооперативного участка (видно только пайщикам)' })
|
||||
branch_name?: string;
|
||||
|
||||
@Field(() => String, { nullable: true, description: 'Email кооперативного участка (видно только пайщикам)' })
|
||||
branch_email?: string;
|
||||
|
||||
@Field(() => String, { nullable: true, description: 'Телефон кооперативного участка (видно только пайщикам)' })
|
||||
branch_phone?: string;
|
||||
|
||||
@Field(() => [KuDecisionQuestionDTO], { nullable: true, description: 'Вопросы повестки собрания' })
|
||||
questions?: KuDecisionQuestionDTO[];
|
||||
|
||||
@Field(() => Int, { nullable: true, description: 'Номер блока последнего обновления' })
|
||||
block_num?: number;
|
||||
}
|
||||
|
||||
@InputType('KuDecisionFilterInput', { description: 'Фильтр решений собраний кооперативных участков' })
|
||||
export class KuDecisionFilterInputDTO {
|
||||
@Field(() => String, { nullable: true, description: 'Имя аккаунта кооператива' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
coopname?: string;
|
||||
|
||||
@Field(() => KuDecisionType, { nullable: true, description: 'Тип решения' })
|
||||
@IsOptional()
|
||||
type?: KuDecisionType;
|
||||
|
||||
@Field(() => KuDecisionStatus, { nullable: true, description: 'Статус решения' })
|
||||
@IsOptional()
|
||||
status?: KuDecisionStatus;
|
||||
|
||||
@Field(() => String, { nullable: true, description: 'Наименование кооперативного участка' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
braname?: string;
|
||||
|
||||
@Field(() => String, { nullable: true, description: 'Инициатор собрания' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
initiator?: string;
|
||||
|
||||
@Field(() => Boolean, { nullable: true, description: 'Только записи, существующие в блокчейне' })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
present?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,644 @@
|
||||
import { Field, InputType, Int, IntersectionType, OmitType } from '@nestjs/graphql';
|
||||
import { Cooperative } from 'cooptypes';
|
||||
import { IsArray, IsNotEmpty, IsNumber, IsOptional, IsString, ValidateNested } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { GenerateMetaDocumentInputDTO } from '~/application/document/dto/generate-meta-document-input.dto';
|
||||
import { MetaDocumentInputDTO } from '~/application/document/dto/meta-document-input.dto';
|
||||
import { SignedDigitalDocumentInputDTO } from '~/application/document/dto/signed-digital-document-input.dto';
|
||||
import type { ExcludeCommonProps } from '~/application/document/types';
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Общие вложенные DTO
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@InputType('KuAgendaQuestionInput', { description: 'Вопрос повестки собрания пайщиков участка' })
|
||||
export class KuAgendaQuestionInputDTO {
|
||||
@Field(() => String, { description: 'Номер вопроса' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
number!: string;
|
||||
|
||||
@Field(() => String, { description: 'Заголовок вопроса' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
title!: string;
|
||||
|
||||
@Field(() => String, { nullable: true, description: 'Дополнительная информация' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
context?: string;
|
||||
|
||||
@Field(() => String, { description: 'Проект решения по вопросу' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
decision!: string;
|
||||
}
|
||||
|
||||
@InputType('KuBallotAnswerInput', { description: 'Волеизъявление по вопросу повестки' })
|
||||
export class KuBallotAnswerInputDTO {
|
||||
@Field(() => String, { description: 'ID вопроса' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
id!: string;
|
||||
|
||||
@Field(() => String, { description: 'Номер вопроса' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
number!: string;
|
||||
|
||||
@Field(() => String, { description: 'Голос (за/против/воздержался)' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
vote!: 'for' | 'against' | 'abstained';
|
||||
}
|
||||
|
||||
@InputType('KuBallotQuestionInput', { description: 'Вопрос собрания участка для бюллетеня' })
|
||||
export class KuBallotQuestionInputDTO {
|
||||
@Field(() => String, { description: 'ID вопроса' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
id!: string;
|
||||
|
||||
@Field(() => String, { description: 'Номер вопроса' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
number!: string;
|
||||
|
||||
@Field(() => String, { description: 'Заголовок вопроса' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
title!: string;
|
||||
|
||||
@Field(() => String, { nullable: true, description: 'Дополнительная информация' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
context?: string;
|
||||
|
||||
@Field(() => String, { description: 'Проект решения по вопросу' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
decision!: string;
|
||||
}
|
||||
|
||||
@InputType('KuProtocolQuestionInput', { description: 'Вопрос протокола с результатами голосования' })
|
||||
export class KuProtocolQuestionInputDTO {
|
||||
@Field(() => String, { description: 'Номер вопроса' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
number!: string;
|
||||
|
||||
@Field(() => String, { description: 'Заголовок вопроса' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
title!: string;
|
||||
|
||||
@Field(() => String, { nullable: true, description: 'Дополнительная информация' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
context?: string;
|
||||
|
||||
@Field(() => String, { description: 'Текст решения по вопросу' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
decision!: string;
|
||||
|
||||
@Field(() => String, { description: 'Количество голосов «за»' })
|
||||
@IsString()
|
||||
counter_votes_for!: string;
|
||||
|
||||
@Field(() => String, { description: 'Количество голосов «против»' })
|
||||
@IsString()
|
||||
counter_votes_against!: string;
|
||||
|
||||
@Field(() => String, { description: 'Количество голосов «воздержался»' })
|
||||
@IsString()
|
||||
counter_votes_abstained!: string;
|
||||
|
||||
@Field(() => Number, { description: 'Процент голосов «за»' })
|
||||
@IsNumber()
|
||||
votes_for_percent!: number;
|
||||
|
||||
@Field(() => Number, { description: 'Процент голосов «против»' })
|
||||
@IsNumber()
|
||||
votes_against_percent!: number;
|
||||
|
||||
@Field(() => Number, { description: 'Процент голосов «воздержался»' })
|
||||
@IsNumber()
|
||||
votes_abstained_percent!: number;
|
||||
|
||||
@Field(() => Boolean, { description: 'Принято ли решение по вопросу' })
|
||||
is_accepted!: boolean;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 320 — Предложение повестки собрания пайщиков участка
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
type proposalAction = Cooperative.Registry.BranchMeetingProposal.Action;
|
||||
|
||||
@InputType('BaseBranchMeetingProposalMetaDocumentInput')
|
||||
class BaseBranchMeetingProposalMetaDocumentInputDTO implements ExcludeCommonProps<proposalAction> {
|
||||
@Field(() => String, { description: 'Тип решения собрания (createbranch | free)' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
type!: 'createbranch' | 'free';
|
||||
|
||||
@Field(() => String, { description: 'Хэш решения собрания' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
hash!: string;
|
||||
|
||||
@Field(() => String, { nullable: true, description: 'Наименование кооперативного участка' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
braname?: string;
|
||||
|
||||
@Field(() => String, { nullable: true, description: 'Адрес привязки кооперативного участка' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
address?: string;
|
||||
|
||||
@Field(() => String, { nullable: true, description: 'Кандидат в председатели кооперативного участка' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
chairman_candidate?: string;
|
||||
|
||||
@Field(() => [KuAgendaQuestionInputDTO], { description: 'Вопросы повестки' })
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => KuAgendaQuestionInputDTO)
|
||||
questions!: KuAgendaQuestionInputDTO[];
|
||||
}
|
||||
|
||||
@InputType('BranchMeetingProposalGenerateDocumentInput')
|
||||
export class BranchMeetingProposalGenerateDocumentInputDTO
|
||||
extends IntersectionType(
|
||||
BaseBranchMeetingProposalMetaDocumentInputDTO,
|
||||
OmitType(GenerateMetaDocumentInputDTO, ['registry_id'] as const)
|
||||
)
|
||||
implements proposalAction
|
||||
{
|
||||
registry_id!: number;
|
||||
}
|
||||
|
||||
@InputType('BranchMeetingProposalSignedMetaDocumentInput')
|
||||
export class BranchMeetingProposalSignedMetaDocumentInputDTO extends IntersectionType(
|
||||
BaseBranchMeetingProposalMetaDocumentInputDTO,
|
||||
MetaDocumentInputDTO
|
||||
) {}
|
||||
|
||||
@InputType('BranchMeetingProposalSignedDocumentInput')
|
||||
export class BranchMeetingProposalSignedDocumentInputDTO extends SignedDigitalDocumentInputDTO {
|
||||
@Field(() => BranchMeetingProposalSignedMetaDocumentInputDTO, {
|
||||
description: 'Метаинформация предложения повестки собрания',
|
||||
})
|
||||
public readonly meta!: BranchMeetingProposalSignedMetaDocumentInputDTO;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 322 — Бюллетень голосования на собрании участка
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
type ballotAction = Cooperative.Registry.BranchMeetingBallot.Action;
|
||||
|
||||
@InputType('BaseBranchMeetingBallotMetaDocumentInput')
|
||||
class BaseBranchMeetingBallotMetaDocumentInputDTO implements ExcludeCommonProps<ballotAction> {
|
||||
@Field(() => String, { description: 'Хэш решения собрания' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
hash!: string;
|
||||
|
||||
@Field(() => [KuBallotAnswerInputDTO], { description: 'Волеизъявления по вопросам повестки' })
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => KuBallotAnswerInputDTO)
|
||||
answers!: KuBallotAnswerInputDTO[];
|
||||
|
||||
@Field(() => [KuBallotQuestionInputDTO], { description: 'Вопросы повестки собрания' })
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => KuBallotQuestionInputDTO)
|
||||
questions!: KuBallotQuestionInputDTO[];
|
||||
}
|
||||
|
||||
@InputType('BranchMeetingBallotGenerateDocumentInput')
|
||||
export class BranchMeetingBallotGenerateDocumentInputDTO
|
||||
extends IntersectionType(
|
||||
BaseBranchMeetingBallotMetaDocumentInputDTO,
|
||||
OmitType(GenerateMetaDocumentInputDTO, ['registry_id'] as const)
|
||||
)
|
||||
implements ballotAction
|
||||
{
|
||||
registry_id!: number;
|
||||
}
|
||||
|
||||
@InputType('BranchMeetingBallotSignedMetaDocumentInput')
|
||||
export class BranchMeetingBallotSignedMetaDocumentInputDTO extends IntersectionType(
|
||||
BaseBranchMeetingBallotMetaDocumentInputDTO,
|
||||
MetaDocumentInputDTO
|
||||
) {}
|
||||
|
||||
@InputType('BranchMeetingBallotSignedDocumentInput')
|
||||
export class BranchMeetingBallotSignedDocumentInputDTO extends SignedDigitalDocumentInputDTO {
|
||||
@Field(() => BranchMeetingBallotSignedMetaDocumentInputDTO, {
|
||||
description: 'Метаинформация бюллетеня голосования',
|
||||
})
|
||||
public readonly meta!: BranchMeetingBallotSignedMetaDocumentInputDTO;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 323 — Протокол решения собрания пайщиков участка
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
type protocolAction = Cooperative.Registry.BranchMeetingDecision.Action;
|
||||
|
||||
@InputType('BaseBranchMeetingDecisionMetaDocumentInput')
|
||||
class BaseBranchMeetingDecisionMetaDocumentInputDTO implements ExcludeCommonProps<protocolAction> {
|
||||
@Field(() => String, { description: 'Хэш решения собрания' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
hash!: string;
|
||||
|
||||
@Field(() => String, { description: 'Номер протокола' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
protocol_number!: string;
|
||||
|
||||
@Field(() => String, { description: 'Имя аккаунта (username) председателя собрания' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
chairman!: string;
|
||||
|
||||
@Field(() => String, { description: 'Дата и время открытия собрания' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
open_at_datetime!: string;
|
||||
|
||||
@Field(() => String, { description: 'Дата и время закрытия собрания' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
close_at_datetime!: string;
|
||||
|
||||
@Field(() => Number, { description: 'Кворум собрания, %' })
|
||||
@IsNumber()
|
||||
current_quorum_percent!: number;
|
||||
|
||||
@Field(() => [KuProtocolQuestionInputDTO], { description: 'Вопросы повестки с результатами голосования' })
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => KuProtocolQuestionInputDTO)
|
||||
questions!: KuProtocolQuestionInputDTO[];
|
||||
}
|
||||
|
||||
@InputType('BranchMeetingDecisionGenerateDocumentInput')
|
||||
export class BranchMeetingDecisionGenerateDocumentInputDTO
|
||||
extends IntersectionType(
|
||||
BaseBranchMeetingDecisionMetaDocumentInputDTO,
|
||||
OmitType(GenerateMetaDocumentInputDTO, ['registry_id'] as const)
|
||||
)
|
||||
implements protocolAction
|
||||
{
|
||||
registry_id!: number;
|
||||
}
|
||||
|
||||
@InputType('BranchMeetingDecisionSignedMetaDocumentInput')
|
||||
export class BranchMeetingDecisionSignedMetaDocumentInputDTO extends IntersectionType(
|
||||
BaseBranchMeetingDecisionMetaDocumentInputDTO,
|
||||
MetaDocumentInputDTO
|
||||
) {}
|
||||
|
||||
@InputType('BranchMeetingDecisionSignedDocumentInput')
|
||||
export class BranchMeetingDecisionSignedDocumentInputDTO extends SignedDigitalDocumentInputDTO {
|
||||
@Field(() => BranchMeetingDecisionSignedMetaDocumentInputDTO, {
|
||||
description: 'Метаинформация протокола решения собрания',
|
||||
})
|
||||
public readonly meta!: BranchMeetingDecisionSignedMetaDocumentInputDTO;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 324 — Заявление председателя собрания в совет об учреждении участка
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
type petitionAction = Cooperative.Registry.BranchEstablishmentPetition.Action;
|
||||
|
||||
@InputType('BaseBranchEstablishmentPetitionMetaDocumentInput')
|
||||
class BaseBranchEstablishmentPetitionMetaDocumentInputDTO implements ExcludeCommonProps<petitionAction> {
|
||||
@Field(() => String, { description: 'Хэш решения собрания' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
hash!: string;
|
||||
|
||||
@Field(() => String, { description: 'Наименование кооперативного участка' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
branch_name!: string;
|
||||
|
||||
@Field(() => String, { description: 'Имя аккаунта (username) избранного председателя кооперативного участка' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
chairman!: string;
|
||||
|
||||
@Field(() => String, { description: 'Адрес привязки кооперативного участка' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
address!: string;
|
||||
}
|
||||
|
||||
@InputType('BranchEstablishmentPetitionGenerateDocumentInput')
|
||||
export class BranchEstablishmentPetitionGenerateDocumentInputDTO
|
||||
extends IntersectionType(
|
||||
BaseBranchEstablishmentPetitionMetaDocumentInputDTO,
|
||||
OmitType(GenerateMetaDocumentInputDTO, ['registry_id'] as const)
|
||||
)
|
||||
implements petitionAction
|
||||
{
|
||||
registry_id!: number;
|
||||
}
|
||||
|
||||
@InputType('BranchEstablishmentPetitionSignedMetaDocumentInput')
|
||||
export class BranchEstablishmentPetitionSignedMetaDocumentInputDTO extends IntersectionType(
|
||||
BaseBranchEstablishmentPetitionMetaDocumentInputDTO,
|
||||
MetaDocumentInputDTO
|
||||
) {}
|
||||
|
||||
@InputType('BranchEstablishmentPetitionSignedDocumentInput')
|
||||
export class BranchEstablishmentPetitionSignedDocumentInputDTO extends SignedDigitalDocumentInputDTO {
|
||||
@Field(() => BranchEstablishmentPetitionSignedMetaDocumentInputDTO, {
|
||||
description: 'Метаинформация заявления в совет об учреждении участка',
|
||||
})
|
||||
public readonly meta!: BranchEstablishmentPetitionSignedMetaDocumentInputDTO;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 325 — Решение совета об учреждении кооперативного участка
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
type establishmentDecisionAction = Cooperative.Registry.BranchEstablishmentSovietDecision.Action;
|
||||
|
||||
@InputType('BaseBranchEstablishmentDecisionMetaDocumentInput')
|
||||
class BaseBranchEstablishmentDecisionMetaDocumentInputDTO implements ExcludeCommonProps<establishmentDecisionAction> {
|
||||
@Field(() => Int, { description: 'Идентификатор решения совета' })
|
||||
@IsNumber()
|
||||
decision_id!: number;
|
||||
|
||||
@Field(() => String, { description: 'Наименование кооперативного участка' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
branch_name!: string;
|
||||
|
||||
@Field(() => String, { description: 'Адрес привязки кооперативного участка' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
address!: string;
|
||||
|
||||
@Field(() => String, { description: 'Имя аккаунта (username) избранного председателя кооперативного участка' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
chairman!: string;
|
||||
}
|
||||
|
||||
@InputType('BranchEstablishmentDecisionGenerateDocumentInput')
|
||||
export class BranchEstablishmentDecisionGenerateDocumentInputDTO
|
||||
extends IntersectionType(
|
||||
BaseBranchEstablishmentDecisionMetaDocumentInputDTO,
|
||||
OmitType(GenerateMetaDocumentInputDTO, ['registry_id'] as const)
|
||||
)
|
||||
implements establishmentDecisionAction
|
||||
{
|
||||
registry_id!: number;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 326 — Заявление о приёме доверенным лицом участка
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
type trustedStatementAction = Cooperative.Registry.BranchTrustedStatement.Action;
|
||||
|
||||
@InputType('BaseBranchTrustedStatementMetaDocumentInput')
|
||||
class BaseBranchTrustedStatementMetaDocumentInputDTO implements ExcludeCommonProps<trustedStatementAction> {
|
||||
@Field(() => String, { description: 'Хэш заявки доверенного' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
hash!: string;
|
||||
|
||||
@Field(() => String, { description: 'Наименование кооперативного участка' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
braname!: string;
|
||||
}
|
||||
|
||||
@InputType('BranchTrustedStatementGenerateDocumentInput')
|
||||
export class BranchTrustedStatementGenerateDocumentInputDTO
|
||||
extends IntersectionType(
|
||||
BaseBranchTrustedStatementMetaDocumentInputDTO,
|
||||
OmitType(GenerateMetaDocumentInputDTO, ['registry_id'] as const)
|
||||
)
|
||||
implements trustedStatementAction
|
||||
{
|
||||
registry_id!: number;
|
||||
}
|
||||
|
||||
@InputType('BranchTrustedStatementSignedMetaDocumentInput')
|
||||
export class BranchTrustedStatementSignedMetaDocumentInputDTO extends IntersectionType(
|
||||
BaseBranchTrustedStatementMetaDocumentInputDTO,
|
||||
MetaDocumentInputDTO
|
||||
) {}
|
||||
|
||||
@InputType('BranchTrustedStatementSignedDocumentInput')
|
||||
export class BranchTrustedStatementSignedDocumentInputDTO extends SignedDigitalDocumentInputDTO {
|
||||
@Field(() => BranchTrustedStatementSignedMetaDocumentInputDTO, {
|
||||
description: 'Метаинформация заявления доверенного лица',
|
||||
})
|
||||
public readonly meta!: BranchTrustedStatementSignedMetaDocumentInputDTO;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 327 — Договор о полной индивидуальной материальной ответственности доверенного лица
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
type trustedLiabilityAction = Cooperative.Registry.BranchTrustedLiabilityAgreement.Action;
|
||||
|
||||
@InputType('BaseBranchTrustedLiabilityAgreementMetaDocumentInput')
|
||||
class BaseBranchTrustedLiabilityAgreementMetaDocumentInputDTO implements ExcludeCommonProps<trustedLiabilityAction> {
|
||||
@Field(() => String, { description: 'Хэш заявки доверенного' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
hash!: string;
|
||||
|
||||
@Field(() => String, { description: 'Наименование кооперативного участка' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
branch_name!: string;
|
||||
|
||||
@Field(() => String, { description: 'Имя аккаунта (username) председателя кооперативного участка' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
trustee!: string;
|
||||
}
|
||||
|
||||
@InputType('BranchTrustedLiabilityAgreementGenerateDocumentInput')
|
||||
export class BranchTrustedLiabilityAgreementGenerateDocumentInputDTO
|
||||
extends IntersectionType(
|
||||
BaseBranchTrustedLiabilityAgreementMetaDocumentInputDTO,
|
||||
OmitType(GenerateMetaDocumentInputDTO, ['registry_id'] as const)
|
||||
)
|
||||
implements trustedLiabilityAction
|
||||
{
|
||||
registry_id!: number;
|
||||
}
|
||||
|
||||
@InputType('BranchTrustedLiabilityAgreementSignedMetaDocumentInput')
|
||||
export class BranchTrustedLiabilityAgreementSignedMetaDocumentInputDTO extends IntersectionType(
|
||||
BaseBranchTrustedLiabilityAgreementMetaDocumentInputDTO,
|
||||
MetaDocumentInputDTO
|
||||
) {}
|
||||
|
||||
@InputType('BranchTrustedLiabilityAgreementSignedDocumentInput')
|
||||
export class BranchTrustedLiabilityAgreementSignedDocumentInputDTO extends SignedDigitalDocumentInputDTO {
|
||||
@Field(() => BranchTrustedLiabilityAgreementSignedMetaDocumentInputDTO, {
|
||||
description: 'Метаинформация договора материальной ответственности доверенного лица',
|
||||
})
|
||||
public readonly meta!: BranchTrustedLiabilityAgreementSignedMetaDocumentInputDTO;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 328 — Договор о полной индивидуальной материальной ответственности председателя участка
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
type trusteeLiabilityAction = Cooperative.Registry.BranchTrusteeLiabilityAgreement.Action;
|
||||
|
||||
@InputType('BaseBranchTrusteeLiabilityAgreementMetaDocumentInput')
|
||||
class BaseBranchTrusteeLiabilityAgreementMetaDocumentInputDTO implements ExcludeCommonProps<trusteeLiabilityAction> {
|
||||
@Field(() => String, { description: 'Якорь процесса учреждения участка' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
hash!: string;
|
||||
|
||||
@Field(() => String, { description: 'Наименование кооперативного участка' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
branch_name!: string;
|
||||
}
|
||||
|
||||
@InputType('BranchTrusteeLiabilityAgreementGenerateDocumentInput')
|
||||
export class BranchTrusteeLiabilityAgreementGenerateDocumentInputDTO
|
||||
extends IntersectionType(
|
||||
BaseBranchTrusteeLiabilityAgreementMetaDocumentInputDTO,
|
||||
OmitType(GenerateMetaDocumentInputDTO, ['registry_id'] as const)
|
||||
)
|
||||
implements trusteeLiabilityAction
|
||||
{
|
||||
registry_id!: number;
|
||||
}
|
||||
|
||||
@InputType('BranchTrusteeLiabilityAgreementSignedMetaDocumentInput')
|
||||
export class BranchTrusteeLiabilityAgreementSignedMetaDocumentInputDTO extends IntersectionType(
|
||||
BaseBranchTrusteeLiabilityAgreementMetaDocumentInputDTO,
|
||||
MetaDocumentInputDTO
|
||||
) {}
|
||||
|
||||
@InputType('BranchTrusteeLiabilityAgreementSignedDocumentInput')
|
||||
export class BranchTrusteeLiabilityAgreementSignedDocumentInputDTO extends SignedDigitalDocumentInputDTO {
|
||||
@Field(() => BranchTrusteeLiabilityAgreementSignedMetaDocumentInputDTO, {
|
||||
description: 'Метаинформация договора материальной ответственности председателя участка',
|
||||
})
|
||||
public readonly meta!: BranchTrusteeLiabilityAgreementSignedMetaDocumentInputDTO;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 329 — Доверенность председателю кооперативного участка
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
type trusteePowerOfAttorneyAction = Cooperative.Registry.BranchTrusteePowerOfAttorney.Action;
|
||||
|
||||
@InputType('BaseBranchTrusteePowerOfAttorneyMetaDocumentInput')
|
||||
class BaseBranchTrusteePowerOfAttorneyMetaDocumentInputDTO implements ExcludeCommonProps<trusteePowerOfAttorneyAction> {
|
||||
@Field(() => String, { description: 'Якорь процесса учреждения участка' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
hash!: string;
|
||||
|
||||
@Field(() => String, { description: 'Наименование кооперативного участка' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
branch_name!: string;
|
||||
|
||||
@Field(() => String, { description: 'Адрес привязки кооперативного участка' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
branch_address!: string;
|
||||
}
|
||||
|
||||
@InputType('BranchTrusteePowerOfAttorneyGenerateDocumentInput')
|
||||
export class BranchTrusteePowerOfAttorneyGenerateDocumentInputDTO
|
||||
extends IntersectionType(
|
||||
BaseBranchTrusteePowerOfAttorneyMetaDocumentInputDTO,
|
||||
OmitType(GenerateMetaDocumentInputDTO, ['registry_id'] as const)
|
||||
)
|
||||
implements trusteePowerOfAttorneyAction
|
||||
{
|
||||
registry_id!: number;
|
||||
}
|
||||
|
||||
@InputType('BranchTrusteePowerOfAttorneySignedMetaDocumentInput')
|
||||
export class BranchTrusteePowerOfAttorneySignedMetaDocumentInputDTO extends IntersectionType(
|
||||
BaseBranchTrusteePowerOfAttorneyMetaDocumentInputDTO,
|
||||
MetaDocumentInputDTO
|
||||
) {}
|
||||
|
||||
@InputType('BranchTrusteePowerOfAttorneySignedDocumentInput')
|
||||
export class BranchTrusteePowerOfAttorneySignedDocumentInputDTO extends SignedDigitalDocumentInputDTO {
|
||||
@Field(() => BranchTrusteePowerOfAttorneySignedMetaDocumentInputDTO, {
|
||||
description: 'Метаинформация доверенности председателю участка',
|
||||
})
|
||||
public readonly meta!: BranchTrusteePowerOfAttorneySignedMetaDocumentInputDTO;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 330 — Доверенность доверенному лицу кооперативного участка
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
type trustedPowerOfAttorneyAction = Cooperative.Registry.BranchTrustedPowerOfAttorney.Action;
|
||||
|
||||
@InputType('BaseBranchTrustedPowerOfAttorneyMetaDocumentInput')
|
||||
class BaseBranchTrustedPowerOfAttorneyMetaDocumentInputDTO implements ExcludeCommonProps<trustedPowerOfAttorneyAction> {
|
||||
@Field(() => String, { description: 'Хэш заявки доверенного' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
hash!: string;
|
||||
|
||||
@Field(() => String, { description: 'Наименование кооперативного участка' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
branch_name!: string;
|
||||
|
||||
@Field(() => String, { description: 'Имя аккаунта (username) председателя кооперативного участка' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
trustee!: string;
|
||||
}
|
||||
|
||||
@InputType('BranchTrustedPowerOfAttorneyGenerateDocumentInput')
|
||||
export class BranchTrustedPowerOfAttorneyGenerateDocumentInputDTO
|
||||
extends IntersectionType(
|
||||
BaseBranchTrustedPowerOfAttorneyMetaDocumentInputDTO,
|
||||
OmitType(GenerateMetaDocumentInputDTO, ['registry_id'] as const)
|
||||
)
|
||||
implements trustedPowerOfAttorneyAction
|
||||
{
|
||||
registry_id!: number;
|
||||
}
|
||||
|
||||
@InputType('BranchTrustedPowerOfAttorneySignedMetaDocumentInput')
|
||||
export class BranchTrustedPowerOfAttorneySignedMetaDocumentInputDTO extends IntersectionType(
|
||||
BaseBranchTrustedPowerOfAttorneyMetaDocumentInputDTO,
|
||||
MetaDocumentInputDTO
|
||||
) {}
|
||||
|
||||
@InputType('BranchTrustedPowerOfAttorneySignedDocumentInput')
|
||||
export class BranchTrustedPowerOfAttorneySignedDocumentInputDTO extends SignedDigitalDocumentInputDTO {
|
||||
@Field(() => BranchTrustedPowerOfAttorneySignedMetaDocumentInputDTO, {
|
||||
description: 'Метаинформация доверенности доверенному лицу участка',
|
||||
})
|
||||
public readonly meta!: BranchTrustedPowerOfAttorneySignedMetaDocumentInputDTO;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { Field, InputType, Int, ObjectType } from '@nestjs/graphql';
|
||||
import { GraphQLJSON } from 'graphql-type-json';
|
||||
import { IsBoolean, IsOptional, IsString } from 'class-validator';
|
||||
import { DocumentAggregateDTO } from '~/application/document/dto/document-aggregate.dto';
|
||||
|
||||
@ObjectType('KuTrustRequest', { description: 'Заявка на приём доверенным лицом кооперативного участка' })
|
||||
export class KuTrustRequestDTO {
|
||||
@Field(() => String, { description: 'Хэш заявки' })
|
||||
hash!: string;
|
||||
|
||||
@Field(() => Int, { nullable: true, description: 'Идентификатор заявки в блокчейне' })
|
||||
id?: number;
|
||||
|
||||
@Field(() => String, { nullable: true, description: 'Имя аккаунта кооператива' })
|
||||
coopname?: string;
|
||||
|
||||
@Field(() => String, { nullable: true, description: 'Наименование кооперативного участка' })
|
||||
braname?: string;
|
||||
|
||||
@Field(() => String, { nullable: true, description: 'Пайщик-заявитель' })
|
||||
username?: string;
|
||||
|
||||
@Field(() => String, { nullable: true, description: 'ФИО пайщика-заявителя' })
|
||||
display_name?: string;
|
||||
|
||||
@Field(() => Boolean, { description: 'Существует ли запись в блокчейне (false — рассмотрена и стёрта)' })
|
||||
present!: boolean;
|
||||
|
||||
@Field(() => GraphQLJSON, {
|
||||
nullable: true,
|
||||
description: 'Заявление и договор о полной материальной ответственности',
|
||||
})
|
||||
application?: object;
|
||||
|
||||
@Field(() => GraphQLJSON, {
|
||||
nullable: true,
|
||||
description: 'Доверенность доверенному лицу/оператору участка с подписью заявителя',
|
||||
})
|
||||
authority?: object;
|
||||
|
||||
@Field(() => DocumentAggregateDTO, {
|
||||
nullable: true,
|
||||
description: 'Договор о полной материальной ответственности с подписью заявителя — для просмотра и встречной подписи председателя',
|
||||
})
|
||||
document?: DocumentAggregateDTO;
|
||||
|
||||
@Field(() => DocumentAggregateDTO, {
|
||||
nullable: true,
|
||||
description: 'Доверенность доверенному лицу с подписью заявителя — для просмотра и встречной подписи председателя',
|
||||
})
|
||||
authority_document?: DocumentAggregateDTO;
|
||||
|
||||
@Field(() => Int, { nullable: true, description: 'Номер блока последнего обновления' })
|
||||
block_num?: number;
|
||||
}
|
||||
|
||||
@InputType('KuTrustRequestFilterInput', { description: 'Фильтр заявок доверенных лиц кооперативных участков' })
|
||||
export class KuTrustRequestFilterInputDTO {
|
||||
@Field(() => String, { nullable: true, description: 'Имя аккаунта кооператива' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
coopname?: string;
|
||||
|
||||
@Field(() => String, { nullable: true, description: 'Наименование кооперативного участка' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
braname?: string;
|
||||
|
||||
@Field(() => String, { nullable: true, description: 'Пайщик-заявитель' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
username?: string;
|
||||
|
||||
@Field(() => Boolean, { nullable: true, description: 'Только записи, существующие в блокчейне' })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
present?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,372 @@
|
||||
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
|
||||
import { UseGuards } from '@nestjs/common';
|
||||
import { GqlJwtAuthGuard } from '~/application/auth/guards/graphql-jwt-auth.guard';
|
||||
import { RolesGuard } from '~/application/auth/guards/roles.guard';
|
||||
import { AuthRoles } from '~/application/auth/decorators/auth.decorator';
|
||||
import { CurrentUser } from '~/application/auth/decorators/current-user.decorator';
|
||||
import type { MonoAccountDomainInterface } from '~/domain/account/interfaces/mono-account-domain.interface';
|
||||
import { TransactionDTO } from '~/application/common/dto/transaction-result-response.dto';
|
||||
import { GeneratedDocumentDTO } from '~/application/document/dto/generated-document.dto';
|
||||
import { GenerateDocumentOptionsInputDTO } from '~/application/document/dto/generate-document-options-input.dto';
|
||||
import { createPaginationResult, PaginationInputDTO, PaginationResult } from '~/application/common/dto/pagination.dto';
|
||||
import { KuService } from '../services/ku.service';
|
||||
import { KuDecisionDTO, KuDecisionFilterInputDTO } from '../dto/ku-decision.dto';
|
||||
import { KuTrustRequestDTO, KuTrustRequestFilterInputDTO } from '../dto/ku-trust-request.dto';
|
||||
import {
|
||||
ApproveKuTrustedInputDTO,
|
||||
CancelKuDecisionInputDTO,
|
||||
CloseKuDecisionInputDTO,
|
||||
CreateKuDecisionInputDTO,
|
||||
DeclineKuTrustedInputDTO,
|
||||
ExecKuDecisionInputDTO,
|
||||
JoinKuDecisionInputDTO,
|
||||
RequestKuTrustedInputDTO,
|
||||
StartKuDecisionInputDTO,
|
||||
VoteOnKuDecisionInputDTO,
|
||||
} from '../dto/ku-action-inputs.dto';
|
||||
import {
|
||||
BranchEstablishmentPetitionGenerateDocumentInputDTO,
|
||||
BranchEstablishmentDecisionGenerateDocumentInputDTO,
|
||||
BranchTrustedLiabilityAgreementGenerateDocumentInputDTO,
|
||||
BranchTrusteeLiabilityAgreementGenerateDocumentInputDTO,
|
||||
BranchTrusteePowerOfAttorneyGenerateDocumentInputDTO,
|
||||
BranchTrustedPowerOfAttorneyGenerateDocumentInputDTO,
|
||||
BranchMeetingBallotGenerateDocumentInputDTO,
|
||||
BranchMeetingDecisionGenerateDocumentInputDTO,
|
||||
BranchMeetingProposalGenerateDocumentInputDTO,
|
||||
BranchTrustedStatementGenerateDocumentInputDTO,
|
||||
} from '../dto/ku-documents.dto';
|
||||
|
||||
// Пагинированные результаты
|
||||
const paginatedKuDecisionsResult = createPaginationResult(KuDecisionDTO, 'PaginatedKuDecisions');
|
||||
const paginatedKuTrustRequestsResult = createPaginationResult(KuTrustRequestDTO, 'PaginatedKuTrustRequests');
|
||||
|
||||
/**
|
||||
* GraphQL-резолвер собраний и решений кооперативных участков (контракт branch)
|
||||
*/
|
||||
@Resolver()
|
||||
export class KuResolver {
|
||||
constructor(private readonly kuService: KuService) {}
|
||||
|
||||
// ───────────────────────────────────────────────────────────────────────────
|
||||
// Мутации собрания
|
||||
// ───────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@Mutation(() => TransactionDTO, {
|
||||
name: 'kuCreateDecision',
|
||||
description: 'Объявить собрание пайщиков кооперативного участка',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['user', 'member', 'chairman'])
|
||||
async kuCreateDecision(
|
||||
@Args('data', { type: () => CreateKuDecisionInputDTO }) data: CreateKuDecisionInputDTO,
|
||||
@CurrentUser() currentUser: MonoAccountDomainInterface
|
||||
): Promise<TransactionDTO> {
|
||||
return this.kuService.createDecision(data, currentUser);
|
||||
}
|
||||
|
||||
@Mutation(() => TransactionDTO, {
|
||||
name: 'kuJoinDecision',
|
||||
description: 'Присоединиться к собранию пайщиков кооперативного участка',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['user', 'member', 'chairman'])
|
||||
async kuJoinDecision(
|
||||
@Args('data', { type: () => JoinKuDecisionInputDTO }) data: JoinKuDecisionInputDTO,
|
||||
@CurrentUser() currentUser: MonoAccountDomainInterface
|
||||
): Promise<TransactionDTO> {
|
||||
return this.kuService.joinDecision(data, currentUser);
|
||||
}
|
||||
|
||||
@Mutation(() => TransactionDTO, {
|
||||
name: 'kuStartDecision',
|
||||
description: 'Открыть голосование на собрании пайщиков участка',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['user', 'member', 'chairman'])
|
||||
async kuStartDecision(
|
||||
@Args('data', { type: () => StartKuDecisionInputDTO }) data: StartKuDecisionInputDTO,
|
||||
@CurrentUser() currentUser: MonoAccountDomainInterface
|
||||
): Promise<TransactionDTO> {
|
||||
return this.kuService.startDecision(data, currentUser);
|
||||
}
|
||||
|
||||
@Mutation(() => TransactionDTO, {
|
||||
name: 'kuVoteOnDecision',
|
||||
description: 'Подать бюллетень на собрании пайщиков участка',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['user', 'member', 'chairman'])
|
||||
async kuVoteOnDecision(
|
||||
@Args('data', { type: () => VoteOnKuDecisionInputDTO }) data: VoteOnKuDecisionInputDTO,
|
||||
@CurrentUser() currentUser: MonoAccountDomainInterface
|
||||
): Promise<TransactionDTO> {
|
||||
return this.kuService.voteOnDecision(data, currentUser);
|
||||
}
|
||||
|
||||
@Mutation(() => TransactionDTO, {
|
||||
name: 'kuCloseDecision',
|
||||
description: 'Закрыть голосование и утвердить протокол собрания',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['user', 'member', 'chairman'])
|
||||
async kuCloseDecision(
|
||||
@Args('data', { type: () => CloseKuDecisionInputDTO }) data: CloseKuDecisionInputDTO,
|
||||
@CurrentUser() currentUser: MonoAccountDomainInterface
|
||||
): Promise<TransactionDTO> {
|
||||
return this.kuService.closeDecision(data, currentUser);
|
||||
}
|
||||
|
||||
@Mutation(() => TransactionDTO, {
|
||||
name: 'kuExecDecision',
|
||||
description: 'Направить заявление председателя собрания в совет об учреждении участка',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['user', 'member', 'chairman'])
|
||||
async kuExecDecision(
|
||||
@Args('data', { type: () => ExecKuDecisionInputDTO }) data: ExecKuDecisionInputDTO,
|
||||
@CurrentUser() currentUser: MonoAccountDomainInterface
|
||||
): Promise<TransactionDTO> {
|
||||
return this.kuService.execDecision(data, currentUser);
|
||||
}
|
||||
|
||||
@Mutation(() => TransactionDTO, {
|
||||
name: 'kuCancelDecision',
|
||||
description: 'Отменить собрание пайщиков участка',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['user', 'member', 'chairman'])
|
||||
async kuCancelDecision(
|
||||
@Args('data', { type: () => CancelKuDecisionInputDTO }) data: CancelKuDecisionInputDTO,
|
||||
@CurrentUser() currentUser: MonoAccountDomainInterface
|
||||
): Promise<TransactionDTO> {
|
||||
return this.kuService.cancelDecision(data, currentUser);
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────────────────────────────────────────
|
||||
// Мутации доверенных лиц
|
||||
// ───────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@Mutation(() => TransactionDTO, {
|
||||
name: 'kuRequestTrusted',
|
||||
description: 'Подать заявку на приём доверенным лицом кооперативного участка',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['user', 'member', 'chairman'])
|
||||
async kuRequestTrusted(
|
||||
@Args('data', { type: () => RequestKuTrustedInputDTO }) data: RequestKuTrustedInputDTO,
|
||||
@CurrentUser() currentUser: MonoAccountDomainInterface
|
||||
): Promise<TransactionDTO> {
|
||||
return this.kuService.requestTrusted(data, currentUser);
|
||||
}
|
||||
|
||||
@Mutation(() => TransactionDTO, {
|
||||
name: 'kuApproveTrusted',
|
||||
description: 'Одобрить заявку доверенного встречной подписью председателя участка',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['user', 'member', 'chairman'])
|
||||
async kuApproveTrusted(
|
||||
@Args('data', { type: () => ApproveKuTrustedInputDTO }) data: ApproveKuTrustedInputDTO,
|
||||
@CurrentUser() currentUser: MonoAccountDomainInterface
|
||||
): Promise<TransactionDTO> {
|
||||
return this.kuService.approveTrusted(data, currentUser);
|
||||
}
|
||||
|
||||
@Mutation(() => TransactionDTO, {
|
||||
name: 'kuDeclineTrusted',
|
||||
description: 'Отклонить заявку доверенного лица',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['user', 'member', 'chairman'])
|
||||
async kuDeclineTrusted(
|
||||
@Args('data', { type: () => DeclineKuTrustedInputDTO }) data: DeclineKuTrustedInputDTO,
|
||||
@CurrentUser() currentUser: MonoAccountDomainInterface
|
||||
): Promise<TransactionDTO> {
|
||||
return this.kuService.declineTrusted(data, currentUser);
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────────────────────────────────────────
|
||||
// Генерация документов
|
||||
// ───────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@Mutation(() => GeneratedDocumentDTO, {
|
||||
name: 'kuGenerateMeetingProposal',
|
||||
description: 'Сгенерировать предложение повестки собрания пайщиков участка',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['user', 'member', 'chairman'])
|
||||
async kuGenerateMeetingProposal(
|
||||
@Args('data', { type: () => BranchMeetingProposalGenerateDocumentInputDTO })
|
||||
data: BranchMeetingProposalGenerateDocumentInputDTO,
|
||||
@Args('options', { nullable: true }) options?: GenerateDocumentOptionsInputDTO
|
||||
): Promise<GeneratedDocumentDTO> {
|
||||
return (await this.kuService.generateBranchMeetingProposal(data, options)) as GeneratedDocumentDTO;
|
||||
}
|
||||
|
||||
@Mutation(() => GeneratedDocumentDTO, {
|
||||
name: 'kuGenerateMeetingBallot',
|
||||
description: 'Сгенерировать бюллетень голосования на собрании участка',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['user', 'member', 'chairman'])
|
||||
async kuGenerateMeetingBallot(
|
||||
@Args('data', { type: () => BranchMeetingBallotGenerateDocumentInputDTO })
|
||||
data: BranchMeetingBallotGenerateDocumentInputDTO,
|
||||
@Args('options', { nullable: true }) options?: GenerateDocumentOptionsInputDTO
|
||||
): Promise<GeneratedDocumentDTO> {
|
||||
return (await this.kuService.generateBranchMeetingBallot(data, options)) as GeneratedDocumentDTO;
|
||||
}
|
||||
|
||||
@Mutation(() => GeneratedDocumentDTO, {
|
||||
name: 'kuGenerateMeetingDecision',
|
||||
description: 'Сгенерировать протокол решения собрания пайщиков участка',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['user', 'member', 'chairman'])
|
||||
async kuGenerateMeetingDecision(
|
||||
@Args('data', { type: () => BranchMeetingDecisionGenerateDocumentInputDTO })
|
||||
data: BranchMeetingDecisionGenerateDocumentInputDTO,
|
||||
@Args('options', { nullable: true }) options?: GenerateDocumentOptionsInputDTO
|
||||
): Promise<GeneratedDocumentDTO> {
|
||||
return (await this.kuService.generateBranchMeetingDecision(data, options)) as GeneratedDocumentDTO;
|
||||
}
|
||||
|
||||
@Mutation(() => GeneratedDocumentDTO, {
|
||||
name: 'kuGenerateEstablishmentPetition',
|
||||
description: 'Сгенерировать заявление председателя собрания в совет об учреждении участка',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['user', 'member', 'chairman'])
|
||||
async kuGenerateEstablishmentPetition(
|
||||
@Args('data', { type: () => BranchEstablishmentPetitionGenerateDocumentInputDTO })
|
||||
data: BranchEstablishmentPetitionGenerateDocumentInputDTO,
|
||||
@Args('options', { nullable: true }) options?: GenerateDocumentOptionsInputDTO
|
||||
): Promise<GeneratedDocumentDTO> {
|
||||
return (await this.kuService.generateBranchEstablishmentPetition(data, options)) as GeneratedDocumentDTO;
|
||||
}
|
||||
|
||||
@Mutation(() => GeneratedDocumentDTO, {
|
||||
name: 'kuGenerateEstablishmentDecision',
|
||||
description: 'Сгенерировать решение совета об учреждении кооперативного участка',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['member', 'chairman'])
|
||||
async kuGenerateEstablishmentDecision(
|
||||
@Args('data', { type: () => BranchEstablishmentDecisionGenerateDocumentInputDTO })
|
||||
data: BranchEstablishmentDecisionGenerateDocumentInputDTO,
|
||||
@Args('options', { nullable: true }) options?: GenerateDocumentOptionsInputDTO
|
||||
): Promise<GeneratedDocumentDTO> {
|
||||
return (await this.kuService.generateBranchEstablishmentDecision(data, options)) as GeneratedDocumentDTO;
|
||||
}
|
||||
|
||||
@Mutation(() => GeneratedDocumentDTO, {
|
||||
name: 'kuGenerateTrustedStatement',
|
||||
description: 'Сгенерировать заявление о приёме доверенным лицом участка',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['user', 'member', 'chairman'])
|
||||
async kuGenerateTrustedStatement(
|
||||
@Args('data', { type: () => BranchTrustedStatementGenerateDocumentInputDTO })
|
||||
data: BranchTrustedStatementGenerateDocumentInputDTO,
|
||||
@Args('options', { nullable: true }) options?: GenerateDocumentOptionsInputDTO
|
||||
): Promise<GeneratedDocumentDTO> {
|
||||
return (await this.kuService.generateBranchTrustedStatement(data, options)) as GeneratedDocumentDTO;
|
||||
}
|
||||
|
||||
@Mutation(() => GeneratedDocumentDTO, {
|
||||
name: 'kuGenerateTrustedLiabilityAgreement',
|
||||
description: 'Сгенерировать договор о полной индивидуальной материальной ответственности доверенного лица кооперативного участка',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['user', 'member', 'chairman'])
|
||||
async kuGenerateTrustedLiabilityAgreement(
|
||||
@Args('data', { type: () => BranchTrustedLiabilityAgreementGenerateDocumentInputDTO })
|
||||
data: BranchTrustedLiabilityAgreementGenerateDocumentInputDTO,
|
||||
@Args('options', { nullable: true }) options?: GenerateDocumentOptionsInputDTO
|
||||
): Promise<GeneratedDocumentDTO> {
|
||||
return (await this.kuService.generateBranchTrustedLiabilityAgreement(data, options)) as GeneratedDocumentDTO;
|
||||
}
|
||||
|
||||
@Mutation(() => GeneratedDocumentDTO, {
|
||||
name: 'kuGenerateTrusteeLiabilityAgreement',
|
||||
description: 'Сгенерировать договор о полной индивидуальной материальной ответственности председателя кооперативного участка',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['user', 'member', 'chairman'])
|
||||
async kuGenerateTrusteeLiabilityAgreement(
|
||||
@Args('data', { type: () => BranchTrusteeLiabilityAgreementGenerateDocumentInputDTO })
|
||||
data: BranchTrusteeLiabilityAgreementGenerateDocumentInputDTO,
|
||||
@Args('options', { nullable: true }) options?: GenerateDocumentOptionsInputDTO
|
||||
): Promise<GeneratedDocumentDTO> {
|
||||
return (await this.kuService.generateBranchTrusteeLiabilityAgreement(data, options)) as GeneratedDocumentDTO;
|
||||
}
|
||||
|
||||
@Mutation(() => GeneratedDocumentDTO, {
|
||||
name: 'kuGenerateTrusteePowerOfAttorney',
|
||||
description: 'Сгенерировать доверенность председателю кооперативного участка',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['user', 'member', 'chairman'])
|
||||
async kuGenerateTrusteePowerOfAttorney(
|
||||
@Args('data', { type: () => BranchTrusteePowerOfAttorneyGenerateDocumentInputDTO })
|
||||
data: BranchTrusteePowerOfAttorneyGenerateDocumentInputDTO,
|
||||
@Args('options', { nullable: true }) options?: GenerateDocumentOptionsInputDTO
|
||||
): Promise<GeneratedDocumentDTO> {
|
||||
return (await this.kuService.generateBranchTrusteePowerOfAttorney(data, options)) as GeneratedDocumentDTO;
|
||||
}
|
||||
|
||||
@Mutation(() => GeneratedDocumentDTO, {
|
||||
name: 'kuGenerateTrustedPowerOfAttorney',
|
||||
description: 'Сгенерировать доверенность доверенному лицу кооперативного участка',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['user', 'member', 'chairman'])
|
||||
async kuGenerateTrustedPowerOfAttorney(
|
||||
@Args('data', { type: () => BranchTrustedPowerOfAttorneyGenerateDocumentInputDTO })
|
||||
data: BranchTrustedPowerOfAttorneyGenerateDocumentInputDTO,
|
||||
@Args('options', { nullable: true }) options?: GenerateDocumentOptionsInputDTO
|
||||
): Promise<GeneratedDocumentDTO> {
|
||||
return (await this.kuService.generateBranchTrustedPowerOfAttorney(data, options)) as GeneratedDocumentDTO;
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────────────────────────────────────────
|
||||
// Запросы
|
||||
// ───────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@Query(() => paginatedKuDecisionsResult, {
|
||||
name: 'kuDecisions',
|
||||
description: 'Получить список решений собраний кооперативных участков',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['user', 'member', 'chairman'])
|
||||
async kuDecisions(
|
||||
@Args('filter', { nullable: true }) filter?: KuDecisionFilterInputDTO,
|
||||
@Args('options', { nullable: true }) options?: PaginationInputDTO
|
||||
): Promise<PaginationResult<KuDecisionDTO>> {
|
||||
return this.kuService.getDecisions(filter, options);
|
||||
}
|
||||
|
||||
@Query(() => KuDecisionDTO, {
|
||||
name: 'kuDecision',
|
||||
description: 'Получить решение собрания участка по хэшу (с вопросами повестки)',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['user', 'member', 'chairman'])
|
||||
async kuDecision(@Args('hash', { type: () => String }) hash: string): Promise<KuDecisionDTO> {
|
||||
return this.kuService.getDecision(hash);
|
||||
}
|
||||
|
||||
@Query(() => paginatedKuTrustRequestsResult, {
|
||||
name: 'kuTrustRequests',
|
||||
description: 'Получить список заявок доверенных лиц кооперативных участков',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['user', 'member', 'chairman'])
|
||||
async kuTrustRequests(
|
||||
@Args('filter', { nullable: true }) filter?: KuTrustRequestFilterInputDTO,
|
||||
@Args('options', { nullable: true }) options?: PaginationInputDTO
|
||||
): Promise<PaginationResult<KuTrustRequestDTO>> {
|
||||
return this.kuService.getTrustRequests(filter, options);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { Cron, CronExpression } from '@nestjs/schedule';
|
||||
import { BranchContract } from 'cooptypes';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { Workflows } from '@coopenomics/notifications';
|
||||
import { WinstonLoggerService } from '~/application/logger/logger-app.service';
|
||||
import { NOTIFICATION_PORT, type NotificationPort } from '~/domain/notification/interfaces/notify.port';
|
||||
import { ACCOUNT_DATA_PORT, type AccountDataPort } from '~/domain/account/ports/account-data.port';
|
||||
import { ORGANIZATION_REPOSITORY, type OrganizationRepository } from '~/domain/common/repositories/organization.repository';
|
||||
import { INDIVIDUAL_REPOSITORY, type IndividualRepository } from '~/domain/common/repositories/individual.repository';
|
||||
import {
|
||||
PAYMENT_METHOD_REPOSITORY,
|
||||
type PaymentMethodRepository,
|
||||
} from '~/domain/common/repositories/payment-method.repository';
|
||||
import { IndividualDomainEntity } from '~/domain/branch/entities/individual-domain.entity';
|
||||
import { OrganizationDomainEntity } from '~/domain/branch/entities/organization-domain.entity';
|
||||
import { PaymentMethodDomainEntity } from '~/domain/payment-method/entities/method-domain.entity';
|
||||
import type { ActionDomainInterface } from '~/domain/parser/interfaces/action-domain.interface';
|
||||
import config from '~/config/config';
|
||||
import { BRANCH_BLOCKCHAIN_PORT, type BranchBlockchainPort } from '~/domain/branch/interfaces/branch-blockchain.port';
|
||||
import { KU_DECISION_REPOSITORY, type KuDecisionRepository } from '../../domain/repositories/ku-decision.repository';
|
||||
import {
|
||||
KU_TRUST_REQUEST_REPOSITORY,
|
||||
type KuTrustRequestRepository,
|
||||
} from '../../domain/repositories/ku-trust-request.repository';
|
||||
|
||||
/**
|
||||
* Событийные реакции собраний кооперативных участков:
|
||||
* — старт голосования (branch::startdec) → уведомления всем участникам собрания;
|
||||
* — утверждение советом (branch::confirmdec) → создание карточки организации
|
||||
* участка в БД (человекочитаемое наименование и адрес — приватные данные,
|
||||
* которых нет в блокчейне), по образцу core-создания КУ со стола председателя.
|
||||
*/
|
||||
@Injectable()
|
||||
export class KuEventsService {
|
||||
constructor(
|
||||
@Inject(NOTIFICATION_PORT) private readonly notificationPort: NotificationPort,
|
||||
@Inject(ACCOUNT_DATA_PORT) private readonly accountPort: AccountDataPort,
|
||||
@Inject(KU_DECISION_REPOSITORY) private readonly decisionRepository: KuDecisionRepository,
|
||||
@Inject(KU_TRUST_REQUEST_REPOSITORY) private readonly trustRequestRepository: KuTrustRequestRepository,
|
||||
@Inject(BRANCH_BLOCKCHAIN_PORT) private readonly branchBlockchainPort: BranchBlockchainPort,
|
||||
@Inject(ORGANIZATION_REPOSITORY) private readonly organizationRepository: OrganizationRepository,
|
||||
@Inject(INDIVIDUAL_REPOSITORY) private readonly individualRepository: IndividualRepository,
|
||||
@Inject(PAYMENT_METHOD_REPOSITORY) private readonly paymentMethodRepository: PaymentMethodRepository,
|
||||
private readonly logger: WinstonLoggerService
|
||||
) {
|
||||
this.logger.setContext(KuEventsService.name);
|
||||
}
|
||||
|
||||
@OnEvent(`action::${BranchContract.contractName.production}::startdec`)
|
||||
async handleVotingStarted(actionData: ActionDomainInterface): Promise<void> {
|
||||
try {
|
||||
const action = actionData.data as { coopname: string; hash: string };
|
||||
if (action.coopname !== config.coopname) return;
|
||||
|
||||
const decision = await this.decisionRepository.findByHash(action.hash);
|
||||
if (!decision) {
|
||||
this.logger.warn(`startdec: решение ${action.hash} не найдено в проекции — уведомления не отправлены`);
|
||||
return;
|
||||
}
|
||||
|
||||
const coopShortName = await this.accountPort.getDisplayName(action.coopname).catch(() => action.coopname);
|
||||
const closeAtTime = decision.close_at
|
||||
? new Date(decision.close_at).toLocaleString('ru-RU', { timeZone: 'Europe/Moscow' })
|
||||
: '';
|
||||
|
||||
const payload: Workflows.BranchVotingStarted.IPayload = {
|
||||
coopShortName,
|
||||
meetPlace: decision.meet_place || '',
|
||||
closeAtTime,
|
||||
meetingUrl: `${config.frontend_url}/${action.coopname}/ku/meetings/${decision.hash}`,
|
||||
};
|
||||
|
||||
let sent = 0;
|
||||
for (const username of decision.participants ?? []) {
|
||||
try {
|
||||
const account = await this.accountPort.getAccount(username);
|
||||
const subscriberId = account.provider_account?.subscriber_id?.trim();
|
||||
const email = account.provider_account?.email;
|
||||
if (!subscriberId || !email) continue;
|
||||
|
||||
await this.notificationPort.notify({
|
||||
coopname: action.coopname,
|
||||
workflowId: Workflows.BranchVotingStarted.id,
|
||||
to: { subscriberId, email, username },
|
||||
payload,
|
||||
});
|
||||
sent++;
|
||||
} catch (error: any) {
|
||||
this.logger.warn(`startdec: не удалось уведомить участника ${username}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
this.logger.log(`startdec ${action.hash}: уведомлено ${sent}/${decision.participants?.length ?? 0} участников`);
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Ошибка обработки startdec: ${error.message}`, error.stack);
|
||||
}
|
||||
}
|
||||
|
||||
/** Уведомление одному пайщику; молча пропускает аккаунты без подписки на уведомления */
|
||||
private async notifyUser(username: string, workflowId: string, payload: Record<string, unknown>): Promise<boolean> {
|
||||
const account = await this.accountPort.getAccount(username);
|
||||
const subscriberId = account.provider_account?.subscriber_id?.trim();
|
||||
const email = account.provider_account?.email;
|
||||
if (!subscriberId || !email) return false;
|
||||
|
||||
await this.notificationPort.notify({
|
||||
coopname: config.coopname,
|
||||
workflowId,
|
||||
to: { subscriberId, email, username },
|
||||
payload,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Напоминание участникам за час до начала собрания (если время назначено) */
|
||||
@Cron(CronExpression.EVERY_5_MINUTES)
|
||||
async remindUpcomingMeetings(): Promise<void> {
|
||||
try {
|
||||
const now = Date.now();
|
||||
const from = new Date(now);
|
||||
const to = new Date(now + 65 * 60 * 1000);
|
||||
const meetings = await this.decisionRepository.findMeetingsForReminder(from, to);
|
||||
|
||||
for (const meeting of meetings) {
|
||||
// окно [now, now+65m): шлём, когда до начала остался час или меньше
|
||||
const coopShortName = await this.accountPort.getDisplayName(config.coopname).catch(() => config.coopname);
|
||||
const payload: Workflows.BranchMeetingReminder.IPayload = {
|
||||
coopShortName,
|
||||
meetPlace: meeting.meet_place || '',
|
||||
meetAtTime: meeting.meet_at
|
||||
? new Date(meeting.meet_at).toLocaleString('ru-RU', { timeZone: 'Europe/Moscow' })
|
||||
: '',
|
||||
meetingUrl: `${config.frontend_url}/${config.coopname}/ku/meetings/${meeting.hash}`,
|
||||
};
|
||||
|
||||
let sent = 0;
|
||||
for (const username of meeting.participants ?? []) {
|
||||
try {
|
||||
if (await this.notifyUser(username, Workflows.BranchMeetingReminder.id, payload)) sent++;
|
||||
} catch (error: any) {
|
||||
this.logger.warn(`напоминание о собрании ${meeting.hash}: не удалось уведомить ${username}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
await this.decisionRepository.markReminderSent(meeting.hash as string);
|
||||
this.logger.log(`напоминание о собрании ${meeting.hash}: уведомлено ${sent}/${meeting.participants?.length ?? 0}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Ошибка напоминаний о собраниях участков: ${error.message}`, error.stack);
|
||||
}
|
||||
}
|
||||
|
||||
/** Новая заявка доверенного → уведомление председателю участка */
|
||||
@OnEvent(`action::${BranchContract.contractName.production}::reqtrusted`)
|
||||
async handleTrustedRequested(actionData: ActionDomainInterface): Promise<void> {
|
||||
try {
|
||||
const action = actionData.data as { coopname: string; braname: string; username: string; hash: string };
|
||||
if (action.coopname !== config.coopname) return;
|
||||
|
||||
const branch = await this.branchBlockchainPort.getBranch(action.coopname, action.braname);
|
||||
if (!branch?.trustee) {
|
||||
this.logger.warn(`reqtrusted: участок ${action.braname} не найден — председатель не уведомлён`);
|
||||
return;
|
||||
}
|
||||
|
||||
const coopShortName = await this.accountPort.getDisplayName(action.coopname).catch(() => action.coopname);
|
||||
const applicantName = await this.accountPort.getDisplayName(action.username).catch(() => action.username);
|
||||
const payload: Workflows.BranchTrustedRequested.IPayload = {
|
||||
coopShortName,
|
||||
applicantName,
|
||||
branchUrl: `${config.frontend_url}/${action.coopname}/ku/branches/${action.braname}`,
|
||||
};
|
||||
|
||||
await this.notifyUser(branch.trustee, Workflows.BranchTrustedRequested.id, payload);
|
||||
this.logger.log(`reqtrusted ${action.hash}: председатель ${branch.trustee} уведомлён о заявке ${action.username}`);
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Ошибка обработки reqtrusted: ${error.message}`, error.stack);
|
||||
}
|
||||
}
|
||||
|
||||
@OnEvent(`action::${BranchContract.contractName.production}::apprtrusted`)
|
||||
async handleTrustedApproved(actionData: ActionDomainInterface): Promise<void> {
|
||||
await this.notifyTrustedResolved(actionData, 'одобрена');
|
||||
}
|
||||
|
||||
@OnEvent(`action::${BranchContract.contractName.production}::decltrusted`)
|
||||
async handleTrustedDeclined(actionData: ActionDomainInterface): Promise<void> {
|
||||
await this.notifyTrustedResolved(actionData, 'отклонена');
|
||||
}
|
||||
|
||||
/** Решение председателя по заявке доверенного → уведомление заявителю */
|
||||
private async notifyTrustedResolved(actionData: ActionDomainInterface, resolution: string): Promise<void> {
|
||||
try {
|
||||
const action = actionData.data as { coopname: string; hash: string };
|
||||
if (action.coopname !== config.coopname) return;
|
||||
|
||||
const request = await this.trustRequestRepository.findByHash(action.hash);
|
||||
if (!request?.username) {
|
||||
this.logger.warn(`заявка доверенного ${action.hash} не найдена в проекции — заявитель не уведомлён`);
|
||||
return;
|
||||
}
|
||||
|
||||
const coopShortName = await this.accountPort.getDisplayName(action.coopname).catch(() => action.coopname);
|
||||
const payload: Workflows.BranchTrustedResolved.IPayload = {
|
||||
coopShortName,
|
||||
resolution,
|
||||
branchUrl: `${config.frontend_url}/${action.coopname}/ku/branches/${request.braname}`,
|
||||
};
|
||||
|
||||
await this.notifyUser(request.username, Workflows.BranchTrustedResolved.id, payload);
|
||||
this.logger.log(`заявка доверенного ${action.hash} ${resolution}: заявитель ${request.username} уведомлён`);
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Ошибка уведомления о решении по заявке доверенного: ${error.message}`, error.stack);
|
||||
}
|
||||
}
|
||||
|
||||
@OnEvent(`action::${BranchContract.contractName.production}::confirmdec`)
|
||||
async handleBranchEstablished(actionData: ActionDomainInterface): Promise<void> {
|
||||
try {
|
||||
const action = actionData.data as { coopname: string; hash: string };
|
||||
if (action.coopname !== config.coopname) return;
|
||||
|
||||
const decision = await this.decisionRepository.findByHash(action.hash);
|
||||
if (!decision?.braname || !decision.chairman) {
|
||||
this.logger.warn(`confirmdec: решение ${action.hash} не найдено в проекции — карточка участка не создана`);
|
||||
return;
|
||||
}
|
||||
|
||||
const existing = await this.organizationRepository.findByUsername(decision.braname).catch(() => null);
|
||||
if (existing) return; // идемпотентность при повторной доставке события
|
||||
|
||||
const cooperative = await this.organizationRepository.findByUsername(action.coopname);
|
||||
if (!cooperative) {
|
||||
this.logger.error(`confirmdec: организация кооператива ${action.coopname} не найдена`);
|
||||
return;
|
||||
}
|
||||
|
||||
const trustee = new IndividualDomainEntity(await this.individualRepository.findByUsername(decision.chairman));
|
||||
const branchName = decision.branch_name || decision.braname;
|
||||
|
||||
const combinedData = new OrganizationDomainEntity({
|
||||
...cooperative,
|
||||
short_name: `КУ «${branchName}»`,
|
||||
full_name: `Кооперативный Участок «${branchName}»`,
|
||||
fact_address: decision.address || '',
|
||||
represented_by: {
|
||||
first_name: trustee.first_name,
|
||||
last_name: trustee.last_name,
|
||||
middle_name: trustee.middle_name,
|
||||
based_on: `Протокол собрания пайщиков кооперативного участка от ${new Date().toLocaleDateString('ru-RU')}`,
|
||||
position: 'председатель кооперативного участка',
|
||||
},
|
||||
username: decision.braname,
|
||||
});
|
||||
|
||||
await this.organizationRepository.create(combinedData);
|
||||
|
||||
const cooperativeBank = await this.paymentMethodRepository.get({
|
||||
username: action.coopname,
|
||||
method_type: 'bank_transfer',
|
||||
is_default: true,
|
||||
});
|
||||
|
||||
await this.paymentMethodRepository.save(
|
||||
new PaymentMethodDomainEntity({
|
||||
username: decision.braname,
|
||||
method_id: randomUUID().toString(),
|
||||
method_type: 'bank_transfer',
|
||||
data: cooperativeBank.data,
|
||||
is_default: true,
|
||||
})
|
||||
);
|
||||
|
||||
this.logger.log(`confirmdec ${action.hash}: создана карточка участка ${decision.braname} («${branchName}»)`);
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Ошибка обработки confirmdec: ${error.message}`, error.stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,515 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Cooperative } from 'cooptypes';
|
||||
import httpStatus from 'http-status';
|
||||
import { HttpApiError } from '~/utils/httpApiError';
|
||||
import { DocumentDomainService } from '~/domain/document/services/document-domain.service';
|
||||
import { DocumentAggregator } from '~/domain/document/aggregators/document.aggregator';
|
||||
import type { ISignedDocumentDomainInterface } from '~/domain/document/interfaces/signed-document-domain.interface';
|
||||
import type { DocumentDomainEntity } from '~/domain/document/entity/document-domain.entity';
|
||||
import type { GenerateDocumentOptionsInputDTO } from '~/application/document/dto/generate-document-options-input.dto';
|
||||
import type { TransactionDTO } from '~/application/common/dto/transaction-result-response.dto';
|
||||
import type { MonoAccountDomainInterface } from '~/domain/account/interfaces/mono-account-domain.interface';
|
||||
import { BRANCH_BLOCKCHAIN_PORT, type BranchBlockchainPort } from '~/domain/branch/interfaces/branch-blockchain.port';
|
||||
import { ACCOUNT_DATA_PORT, type AccountDataPort } from '~/domain/account/ports/account-data.port';
|
||||
import { AccountType } from '~/application/account/enum/account-type.enum';
|
||||
import type { PaginationInputDomainInterface } from '~/domain/common/interfaces/pagination.interface';
|
||||
import type { PaginationResult } from '~/application/common/dto/pagination.dto';
|
||||
import { KU_BLOCKCHAIN_PORT, type KuBlockchainPort } from '../../domain/interfaces/ku-blockchain.port';
|
||||
import { KU_DECISION_REPOSITORY, type KuDecisionRepository } from '../../domain/repositories/ku-decision.repository';
|
||||
import {
|
||||
KU_DECISION_QUESTION_REPOSITORY,
|
||||
type KuDecisionQuestionRepository,
|
||||
} from '../../domain/repositories/ku-decision-question.repository';
|
||||
import {
|
||||
KU_TRUST_REQUEST_REPOSITORY,
|
||||
type KuTrustRequestRepository,
|
||||
} from '../../domain/repositories/ku-trust-request.repository';
|
||||
import type { KuDecisionDomainEntity } from '../../domain/entities/ku-decision.entity';
|
||||
import type { KuDecisionQuestionDomainEntity } from '../../domain/entities/ku-decision-question.entity';
|
||||
import type { KuTrustRequestDomainEntity } from '../../domain/entities/ku-trust-request.entity';
|
||||
import type { KuDecisionType } from '../../domain/enums/ku-decision-type.enum';
|
||||
import type { KuDecisionStatus } from '../../domain/enums/ku-decision-status.enum';
|
||||
import type {
|
||||
ApproveKuTrustedInputDomainInterface,
|
||||
CancelKuDecisionInputDomainInterface,
|
||||
CloseKuDecisionInputDomainInterface,
|
||||
CreateKuDecisionInputDomainInterface,
|
||||
DeclineKuTrustedInputDomainInterface,
|
||||
ExecKuDecisionInputDomainInterface,
|
||||
JoinKuDecisionInputDomainInterface,
|
||||
RequestKuTrustedInputDomainInterface,
|
||||
StartKuDecisionInputDomainInterface,
|
||||
VoteOnKuDecisionInputDomainInterface,
|
||||
} from '../../domain/interfaces/ku-action-inputs.interface';
|
||||
import { KuDecisionDTO, KuDecisionFilterInputDTO, KuDecisionQuestionDTO } from '../dto/ku-decision.dto';
|
||||
import { KuTrustRequestDTO, KuTrustRequestFilterInputDTO } from '../dto/ku-trust-request.dto';
|
||||
import { DocumentAggregateDTO } from '~/application/document/dto/document-aggregate.dto';
|
||||
|
||||
/**
|
||||
* Сервис собраний и решений кооперативных участков.
|
||||
* Действия отправляются в контракт branch с подписью кооператива;
|
||||
* чтение — из PG-проекций (история сохраняется после erase в блокчейне).
|
||||
*/
|
||||
@Injectable()
|
||||
export class KuService {
|
||||
constructor(
|
||||
@Inject(KU_BLOCKCHAIN_PORT) private readonly kuBlockchainPort: KuBlockchainPort,
|
||||
@Inject(BRANCH_BLOCKCHAIN_PORT) private readonly branchBlockchainPort: BranchBlockchainPort,
|
||||
@Inject(KU_DECISION_REPOSITORY) private readonly decisionRepository: KuDecisionRepository,
|
||||
@Inject(KU_DECISION_QUESTION_REPOSITORY) private readonly questionRepository: KuDecisionQuestionRepository,
|
||||
@Inject(KU_TRUST_REQUEST_REPOSITORY) private readonly trustRequestRepository: KuTrustRequestRepository,
|
||||
@Inject(ACCOUNT_DATA_PORT) private readonly accountPort: AccountDataPort,
|
||||
private readonly documentDomainService: DocumentDomainService,
|
||||
private readonly documentAggregator: DocumentAggregator
|
||||
) {}
|
||||
|
||||
// ───────────────────────────────────────────────────────────────────────────
|
||||
// Проверки прав
|
||||
// ───────────────────────────────────────────────────────────────────────────
|
||||
|
||||
private assertSameUser(currentUser: MonoAccountDomainInterface, username: string): void {
|
||||
if (currentUser.username !== username) {
|
||||
throw new HttpApiError(httpStatus.FORBIDDEN, 'Действие доступно только от своего имени');
|
||||
}
|
||||
}
|
||||
|
||||
private async getDecisionOrFail(hash: string): Promise<KuDecisionDomainEntity> {
|
||||
const decision = await this.decisionRepository.findByHash(hash);
|
||||
if (!decision) {
|
||||
throw new HttpApiError(httpStatus.NOT_FOUND, 'Решение собрания участка не найдено');
|
||||
}
|
||||
return decision;
|
||||
}
|
||||
|
||||
private async assertIsDecisionChairman(currentUser: MonoAccountDomainInterface, hash: string): Promise<void> {
|
||||
const decision = await this.getDecisionOrFail(hash);
|
||||
if (decision.chairman !== currentUser.username) {
|
||||
throw new HttpApiError(httpStatus.FORBIDDEN, 'Действие доступно только председателю собрания');
|
||||
}
|
||||
}
|
||||
|
||||
private async assertIsDecisionInitiator(currentUser: MonoAccountDomainInterface, hash: string): Promise<void> {
|
||||
const decision = await this.getDecisionOrFail(hash);
|
||||
if (decision.initiator !== currentUser.username) {
|
||||
throw new HttpApiError(httpStatus.FORBIDDEN, 'Действие доступно только инициатору собрания');
|
||||
}
|
||||
}
|
||||
|
||||
private async assertIsBranchTrustee(currentUser: MonoAccountDomainInterface, requestHash: string): Promise<void> {
|
||||
const request = await this.trustRequestRepository.findByHash(requestHash);
|
||||
if (!request?.braname) {
|
||||
throw new HttpApiError(httpStatus.NOT_FOUND, 'Заявка доверенного не найдена');
|
||||
}
|
||||
|
||||
const branch = await this.branchBlockchainPort.getBranch(request.coopname as string, request.braname);
|
||||
if (!branch) {
|
||||
throw new HttpApiError(httpStatus.NOT_FOUND, 'Кооперативный участок не найден');
|
||||
}
|
||||
|
||||
if (branch.trustee !== currentUser.username) {
|
||||
throw new HttpApiError(httpStatus.FORBIDDEN, 'Действие доступно только председателю кооперативного участка');
|
||||
}
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────────────────────────────────────────
|
||||
// Действия собрания (контракт branch)
|
||||
// ───────────────────────────────────────────────────────────────────────────
|
||||
|
||||
async createDecision(
|
||||
data: CreateKuDecisionInputDomainInterface,
|
||||
currentUser: MonoAccountDomainInterface
|
||||
): Promise<TransactionDTO> {
|
||||
this.assertSameUser(currentUser, data.initiator);
|
||||
const result = await this.kuBlockchainPort.createDecision(data);
|
||||
|
||||
// Место и время проведения собрания — приватные данные пайщиков,
|
||||
// в блокчейн не публикуются и сохраняются только в БД платформы
|
||||
await this.decisionRepository.upsertPrivateData({
|
||||
hash: data.hash,
|
||||
coopname: data.coopname,
|
||||
type: data.type,
|
||||
initiator: data.initiator,
|
||||
meet_place: data.meet_place,
|
||||
meet_at: new Date(data.meet_at),
|
||||
});
|
||||
|
||||
return result as unknown as TransactionDTO;
|
||||
}
|
||||
|
||||
async joinDecision(
|
||||
data: JoinKuDecisionInputDomainInterface,
|
||||
currentUser: MonoAccountDomainInterface
|
||||
): Promise<TransactionDTO> {
|
||||
this.assertSameUser(currentUser, data.username);
|
||||
const result = await this.kuBlockchainPort.joinDecision(data);
|
||||
return result as unknown as TransactionDTO;
|
||||
}
|
||||
|
||||
async startDecision(
|
||||
data: StartKuDecisionInputDomainInterface,
|
||||
currentUser: MonoAccountDomainInterface
|
||||
): Promise<TransactionDTO> {
|
||||
// Голосование открывает организатор собрания, назначая председателя
|
||||
// из числа присоединившихся участников
|
||||
await this.assertIsDecisionInitiator(currentUser, data.hash);
|
||||
|
||||
const decision = await this.getDecisionOrFail(data.hash);
|
||||
if (!(decision.participants ?? []).includes(data.chairman)) {
|
||||
throw new HttpApiError(httpStatus.BAD_REQUEST, 'Председатель должен быть участником собрания');
|
||||
}
|
||||
if (decision.type === 'createbranch' && !data.branch_name) {
|
||||
throw new HttpApiError(httpStatus.BAD_REQUEST, 'Укажите наименование кооперативного участка');
|
||||
}
|
||||
// контакты нужны для добавления участка как подразделения после решения совета
|
||||
if (decision.type === 'createbranch' && (!data.branch_email || !data.branch_phone)) {
|
||||
throw new HttpApiError(httpStatus.BAD_REQUEST, 'Укажите email и телефон кооперативного участка');
|
||||
}
|
||||
if (decision.type === 'createbranch') {
|
||||
const chairmanAccount = await this.accountPort.getAccount(data.chairman);
|
||||
if (chairmanAccount.private_account?.type !== AccountType.individual) {
|
||||
throw new HttpApiError(
|
||||
httpStatus.BAD_REQUEST,
|
||||
'Председателем кооперативного участка может быть только физическое лицо'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Наименование и контакты участка — приватные данные, в блокчейн не публикуются
|
||||
await this.decisionRepository.upsertPrivateData({
|
||||
hash: data.hash,
|
||||
branch_name: data.branch_name,
|
||||
branch_email: data.branch_email,
|
||||
branch_phone: data.branch_phone,
|
||||
});
|
||||
|
||||
const result = await this.kuBlockchainPort.startDecision(data);
|
||||
return result as unknown as TransactionDTO;
|
||||
}
|
||||
|
||||
async voteOnDecision(
|
||||
data: VoteOnKuDecisionInputDomainInterface,
|
||||
currentUser: MonoAccountDomainInterface
|
||||
): Promise<TransactionDTO> {
|
||||
this.assertSameUser(currentUser, data.username);
|
||||
const result = await this.kuBlockchainPort.voteOnDecision(data);
|
||||
return result as unknown as TransactionDTO;
|
||||
}
|
||||
|
||||
async closeDecision(
|
||||
data: CloseKuDecisionInputDomainInterface,
|
||||
currentUser: MonoAccountDomainInterface
|
||||
): Promise<TransactionDTO> {
|
||||
// протокол утверждает председатель собрания — им автоматически является организатор
|
||||
await this.assertIsDecisionInitiator(currentUser, data.hash);
|
||||
const result = await this.kuBlockchainPort.closeDecision(data);
|
||||
return result as unknown as TransactionDTO;
|
||||
}
|
||||
|
||||
async execDecision(
|
||||
data: ExecKuDecisionInputDomainInterface,
|
||||
currentUser: MonoAccountDomainInterface
|
||||
): Promise<TransactionDTO> {
|
||||
// Заявление в совет и договор о материальной ответственности подписывает
|
||||
// избранный собранием председатель участка (он же сторона договора)
|
||||
await this.assertIsDecisionChairman(currentUser, data.hash);
|
||||
const result = await this.kuBlockchainPort.execDecision(data);
|
||||
return result as unknown as TransactionDTO;
|
||||
}
|
||||
|
||||
async cancelDecision(
|
||||
data: CancelKuDecisionInputDomainInterface,
|
||||
currentUser: MonoAccountDomainInterface
|
||||
): Promise<TransactionDTO> {
|
||||
await this.assertIsDecisionInitiator(currentUser, data.hash);
|
||||
const result = await this.kuBlockchainPort.cancelDecision(data);
|
||||
|
||||
// контракт стирает запись одинаково при любом исходе — факт отмены
|
||||
// фиксируем в БД, чтобы отличать «Отменено» от «Завершено»
|
||||
await this.decisionRepository.upsertPrivateData({
|
||||
hash: data.hash,
|
||||
cancelled: true,
|
||||
});
|
||||
|
||||
return result as unknown as TransactionDTO;
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────────────────────────────────────────
|
||||
// Доверенные лица
|
||||
// ───────────────────────────────────────────────────────────────────────────
|
||||
|
||||
async requestTrusted(
|
||||
data: RequestKuTrustedInputDomainInterface,
|
||||
currentUser: MonoAccountDomainInterface
|
||||
): Promise<TransactionDTO> {
|
||||
this.assertSameUser(currentUser, data.username);
|
||||
const result = await this.kuBlockchainPort.requestTrusted(data);
|
||||
return result as unknown as TransactionDTO;
|
||||
}
|
||||
|
||||
async approveTrusted(
|
||||
data: ApproveKuTrustedInputDomainInterface,
|
||||
currentUser: MonoAccountDomainInterface
|
||||
): Promise<TransactionDTO> {
|
||||
await this.assertIsBranchTrustee(currentUser, data.hash);
|
||||
const result = await this.kuBlockchainPort.approveTrusted(data);
|
||||
return result as unknown as TransactionDTO;
|
||||
}
|
||||
|
||||
async declineTrusted(
|
||||
data: DeclineKuTrustedInputDomainInterface,
|
||||
currentUser: MonoAccountDomainInterface
|
||||
): Promise<TransactionDTO> {
|
||||
await this.assertIsBranchTrustee(currentUser, data.hash);
|
||||
const result = await this.kuBlockchainPort.declineTrusted(data);
|
||||
return result as unknown as TransactionDTO;
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────────────────────────────────────────
|
||||
// Генерация документов 320–327
|
||||
// ───────────────────────────────────────────────────────────────────────────
|
||||
|
||||
private async generate(
|
||||
data: Cooperative.Document.IGenerate,
|
||||
registry_id: number,
|
||||
options?: GenerateDocumentOptionsInputDTO
|
||||
): Promise<DocumentDomainEntity> {
|
||||
data.registry_id = registry_id;
|
||||
return await this.documentDomainService.generateDocument({ data, options: options || {} });
|
||||
}
|
||||
|
||||
async generateBranchMeetingProposal(
|
||||
data: Cooperative.Registry.BranchMeetingProposal.Action,
|
||||
options?: GenerateDocumentOptionsInputDTO
|
||||
): Promise<DocumentDomainEntity> {
|
||||
return this.generate(data, Cooperative.Registry.BranchMeetingProposal.registry_id, options);
|
||||
}
|
||||
|
||||
async generateBranchMeetingBallot(
|
||||
data: Cooperative.Registry.BranchMeetingBallot.Action,
|
||||
options?: GenerateDocumentOptionsInputDTO
|
||||
): Promise<DocumentDomainEntity> {
|
||||
return this.generate(data, Cooperative.Registry.BranchMeetingBallot.registry_id, options);
|
||||
}
|
||||
|
||||
async generateBranchMeetingDecision(
|
||||
data: Cooperative.Registry.BranchMeetingDecision.Action,
|
||||
options?: GenerateDocumentOptionsInputDTO
|
||||
): Promise<DocumentDomainEntity> {
|
||||
return this.generate(data, Cooperative.Registry.BranchMeetingDecision.registry_id, options);
|
||||
}
|
||||
|
||||
async generateBranchEstablishmentPetition(
|
||||
data: Cooperative.Registry.BranchEstablishmentPetition.Action,
|
||||
options?: GenerateDocumentOptionsInputDTO
|
||||
): Promise<DocumentDomainEntity> {
|
||||
return this.generate(data, Cooperative.Registry.BranchEstablishmentPetition.registry_id, options);
|
||||
}
|
||||
|
||||
async generateBranchEstablishmentDecision(
|
||||
data: Cooperative.Registry.BranchEstablishmentSovietDecision.Action,
|
||||
options?: GenerateDocumentOptionsInputDTO
|
||||
): Promise<DocumentDomainEntity> {
|
||||
return this.generate(data, Cooperative.Registry.BranchEstablishmentSovietDecision.registry_id, options);
|
||||
}
|
||||
|
||||
async generateBranchTrustedStatement(
|
||||
data: Cooperative.Registry.BranchTrustedStatement.Action,
|
||||
options?: GenerateDocumentOptionsInputDTO
|
||||
): Promise<DocumentDomainEntity> {
|
||||
return this.generate(data, Cooperative.Registry.BranchTrustedStatement.registry_id, options);
|
||||
}
|
||||
|
||||
async generateBranchTrustedLiabilityAgreement(
|
||||
data: Cooperative.Registry.BranchTrustedLiabilityAgreement.Action,
|
||||
options?: GenerateDocumentOptionsInputDTO
|
||||
): Promise<DocumentDomainEntity> {
|
||||
return this.generate(data, Cooperative.Registry.BranchTrustedLiabilityAgreement.registry_id, options);
|
||||
}
|
||||
|
||||
async generateBranchTrusteeLiabilityAgreement(
|
||||
data: Cooperative.Registry.BranchTrusteeLiabilityAgreement.Action,
|
||||
options?: GenerateDocumentOptionsInputDTO
|
||||
): Promise<DocumentDomainEntity> {
|
||||
return this.generate(data, Cooperative.Registry.BranchTrusteeLiabilityAgreement.registry_id, options);
|
||||
}
|
||||
|
||||
async generateBranchTrusteePowerOfAttorney(
|
||||
data: Cooperative.Registry.BranchTrusteePowerOfAttorney.Action,
|
||||
options?: GenerateDocumentOptionsInputDTO
|
||||
): Promise<DocumentDomainEntity> {
|
||||
return this.generate(data, Cooperative.Registry.BranchTrusteePowerOfAttorney.registry_id, options);
|
||||
}
|
||||
|
||||
async generateBranchTrustedPowerOfAttorney(
|
||||
data: Cooperative.Registry.BranchTrustedPowerOfAttorney.Action,
|
||||
options?: GenerateDocumentOptionsInputDTO
|
||||
): Promise<DocumentDomainEntity> {
|
||||
return this.generate(data, Cooperative.Registry.BranchTrustedPowerOfAttorney.registry_id, options);
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────────────────────────────────────────
|
||||
// Запросы
|
||||
// ───────────────────────────────────────────────────────────────────────────
|
||||
|
||||
private toDecisionDTO(entity: KuDecisionDomainEntity, questions?: KuDecisionQuestionDomainEntity[]): KuDecisionDTO {
|
||||
return {
|
||||
hash: entity.hash as string,
|
||||
id: entity.id,
|
||||
coopname: entity.coopname,
|
||||
type: entity.type as KuDecisionType,
|
||||
initiator: entity.initiator,
|
||||
chairman: entity.chairman,
|
||||
status: (entity.present ? entity.status : entity.cancelled ? 'cancelled' : 'completed') as KuDecisionStatus,
|
||||
present: entity.present,
|
||||
proposal: entity.proposal,
|
||||
protocol: entity.protocol,
|
||||
petition: entity.petition,
|
||||
authorization: entity.authorization,
|
||||
open_at: entity.open_at,
|
||||
close_at: entity.close_at,
|
||||
signed_ballots: entity.signed_ballots,
|
||||
braname: entity.braname,
|
||||
address: entity.address,
|
||||
participants: entity.participants,
|
||||
created_at: entity.created_at,
|
||||
meet_place: entity.meet_place,
|
||||
meet_at: entity.meet_at?.toISOString(),
|
||||
branch_name: entity.branch_name,
|
||||
branch_email: entity.branch_email,
|
||||
branch_phone: entity.branch_phone,
|
||||
questions: questions?.map((question) => this.toQuestionDTO(question)),
|
||||
block_num: entity.block_num,
|
||||
};
|
||||
}
|
||||
|
||||
/** Отображаемые имена и типы аккаунтов участников собрания (для выбора председателя по ФИО) */
|
||||
private async resolveParticipantsInfo(
|
||||
participants: string[]
|
||||
): Promise<{ username: string; display_name: string; account_type: AccountType }[]> {
|
||||
return Promise.all(
|
||||
participants.map(async (username) => {
|
||||
try {
|
||||
const account = await this.accountPort.getAccount(username);
|
||||
const display_name = await this.accountPort.getDisplayName(username);
|
||||
const account_type = (account.private_account?.type as AccountType) ?? AccountType.individual;
|
||||
return { username, display_name: display_name || username, account_type };
|
||||
} catch {
|
||||
return { username, display_name: username, account_type: AccountType.individual };
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private toQuestionDTO(entity: KuDecisionQuestionDomainEntity): KuDecisionQuestionDTO {
|
||||
return {
|
||||
id: entity.id,
|
||||
decision_id: entity.decision_id,
|
||||
number: entity.number,
|
||||
title: entity.title,
|
||||
decision: entity.decision,
|
||||
context: entity.context,
|
||||
counter_votes_for: entity.counter_votes_for,
|
||||
counter_votes_against: entity.counter_votes_against,
|
||||
counter_votes_abstained: entity.counter_votes_abstained,
|
||||
voters_for: entity.voters_for,
|
||||
voters_against: entity.voters_against,
|
||||
voters_abstained: entity.voters_abstained,
|
||||
};
|
||||
}
|
||||
|
||||
private toTrustRequestDTO(entity: KuTrustRequestDomainEntity): KuTrustRequestDTO {
|
||||
return {
|
||||
hash: entity.hash as string,
|
||||
id: entity.id,
|
||||
coopname: entity.coopname,
|
||||
braname: entity.braname,
|
||||
username: entity.username,
|
||||
present: entity.present,
|
||||
application: entity.application,
|
||||
authority: entity.authority,
|
||||
block_num: entity.block_num,
|
||||
};
|
||||
}
|
||||
|
||||
async getDecisions(
|
||||
filter?: KuDecisionFilterInputDTO,
|
||||
options?: PaginationInputDomainInterface
|
||||
): Promise<PaginationResult<KuDecisionDTO>> {
|
||||
const result = await this.decisionRepository.findAllPaginated(filter, options);
|
||||
|
||||
return {
|
||||
...result,
|
||||
items: result.items.map((item) => this.toDecisionDTO(item)),
|
||||
};
|
||||
}
|
||||
|
||||
async getDecision(hash: string): Promise<KuDecisionDTO> {
|
||||
const decision = await this.getDecisionOrFail(hash);
|
||||
|
||||
let questions: KuDecisionQuestionDomainEntity[] = [];
|
||||
if (decision.coopname && decision.id !== undefined) {
|
||||
questions = await this.questionRepository.findByDecisionId(decision.coopname, decision.id);
|
||||
// контракт заменяет черновую повестку при открытии голосования (erase + emplace) —
|
||||
// стёртые вопросы остаются в БД с present=false и в повестку не попадают
|
||||
questions = questions.filter((question) => question.present);
|
||||
}
|
||||
|
||||
const dto = this.toDecisionDTO(decision, questions);
|
||||
dto.participants_info = await this.resolveParticipantsInfo(decision.participants ?? []);
|
||||
|
||||
// Протокол собрания пайщиков (323) и решение совета (325) — публикуемые документы
|
||||
// для страницы собрания. Договор матответственности (328) и доверенность (329)
|
||||
// содержат паспортные данные и сюда НЕ выносятся.
|
||||
if (decision.protocol) {
|
||||
const aggregate = await this.documentAggregator
|
||||
.buildDocumentAggregate(decision.protocol as unknown as ISignedDocumentDomainInterface)
|
||||
.catch(() => null);
|
||||
dto.protocol_document = aggregate ? new DocumentAggregateDTO(aggregate) : undefined;
|
||||
}
|
||||
if (decision.authorization) {
|
||||
const aggregate = await this.documentAggregator
|
||||
.buildDocumentAggregate(decision.authorization as unknown as ISignedDocumentDomainInterface)
|
||||
.catch(() => null);
|
||||
dto.authorization_document = aggregate ? new DocumentAggregateDTO(aggregate) : undefined;
|
||||
}
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
async getTrustRequests(
|
||||
filter?: KuTrustRequestFilterInputDTO,
|
||||
options?: PaginationInputDomainInterface
|
||||
): Promise<PaginationResult<KuTrustRequestDTO>> {
|
||||
const result = await this.trustRequestRepository.findAllPaginated(filter, options);
|
||||
|
||||
const items = await Promise.all(
|
||||
result.items.map(async (item) => {
|
||||
const dto = this.toTrustRequestDTO(item);
|
||||
if (dto.username) {
|
||||
dto.display_name = await this.accountPort.getDisplayName(dto.username).catch(() => dto.username);
|
||||
}
|
||||
// договор заявителя с сертификатами подписантов — председатель смотрит документ
|
||||
// и накладывает встречную подпись на него же, без регенерации
|
||||
if (item.application) {
|
||||
const aggregate = await this.documentAggregator
|
||||
.buildDocumentAggregate(item.application as unknown as ISignedDocumentDomainInterface)
|
||||
.catch(() => null);
|
||||
dto.document = aggregate ? new DocumentAggregateDTO(aggregate) : undefined;
|
||||
}
|
||||
// доверенность доверенному лицу — председатель так же накладывает встречную подпись
|
||||
if (item.authority) {
|
||||
const authorityAggregate = await this.documentAggregator
|
||||
.buildDocumentAggregate(item.authority as unknown as ISignedDocumentDomainInterface)
|
||||
.catch(() => null);
|
||||
dto.authority_document = authorityAggregate ? new DocumentAggregateDTO(authorityAggregate) : undefined;
|
||||
}
|
||||
return dto;
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
...result,
|
||||
items,
|
||||
};
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { Injectable, OnModuleInit, Inject } from '@nestjs/common';
|
||||
import { OnEvent, EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { WinstonLoggerService } from '~/application/logger/logger-app.service';
|
||||
import { AbstractEntitySyncService } from '~/shared/services/abstract-entity-sync.service';
|
||||
import { KuDecisionQuestionDomainEntity } from '../../domain/entities/ku-decision-question.entity';
|
||||
import { KuDecisionQuestionRepository, KU_DECISION_QUESTION_REPOSITORY } from '../../domain/repositories/ku-decision-question.repository';
|
||||
import { KuDecisionQuestionDeltaMapper } from '../../infrastructure/blockchain/mappers/ku-decision-question-delta.mapper';
|
||||
import type { IKuDecisionQuestionBlockchainData } from '../../domain/interfaces/ku-blockchain-data.interface';
|
||||
|
||||
/**
|
||||
* Сервис синхронизации вопросов повесток собраний участков с блокчейном (контракт branch).
|
||||
* История сохраняется в PG после erase записи в блокчейне (present=false).
|
||||
*/
|
||||
@Injectable()
|
||||
export class KuDecisionQuestionSyncService extends AbstractEntitySyncService<KuDecisionQuestionDomainEntity, IKuDecisionQuestionBlockchainData> implements OnModuleInit {
|
||||
protected readonly entityName = 'KuDecisionQuestionDomainEntity';
|
||||
|
||||
constructor(
|
||||
@Inject(KU_DECISION_QUESTION_REPOSITORY)
|
||||
repository: KuDecisionQuestionRepository,
|
||||
mapper: KuDecisionQuestionDeltaMapper,
|
||||
logger: WinstonLoggerService,
|
||||
private readonly eventEmitter: EventEmitter2
|
||||
) {
|
||||
super(repository, mapper, logger);
|
||||
}
|
||||
|
||||
async onModuleInit() {
|
||||
const allPatterns = this.getAllEventPatterns();
|
||||
this.logger.debug(`Подписка на ${allPatterns.length} паттернов событий: ${allPatterns.join(', ')}`);
|
||||
|
||||
allPatterns.forEach((pattern) => {
|
||||
this.eventEmitter.on(pattern, this.processDelta.bind(this));
|
||||
});
|
||||
}
|
||||
|
||||
@OnEvent('fork::*')
|
||||
async handleEntityFork(forkData: { block_num: number }): Promise<void> {
|
||||
await this.handleFork(forkData.block_num);
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { Injectable, OnModuleInit, Inject } from '@nestjs/common';
|
||||
import { OnEvent, EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { WinstonLoggerService } from '~/application/logger/logger-app.service';
|
||||
import { AbstractEntitySyncService } from '~/shared/services/abstract-entity-sync.service';
|
||||
import { KuDecisionDomainEntity } from '../../domain/entities/ku-decision.entity';
|
||||
import { KuDecisionRepository, KU_DECISION_REPOSITORY } from '../../domain/repositories/ku-decision.repository';
|
||||
import { KuDecisionDeltaMapper } from '../../infrastructure/blockchain/mappers/ku-decision-delta.mapper';
|
||||
import type { IKuDecisionBlockchainData } from '../../domain/interfaces/ku-blockchain-data.interface';
|
||||
|
||||
/**
|
||||
* Сервис синхронизации решений собраний участков с блокчейном (контракт branch).
|
||||
* История сохраняется в PG после erase записи в блокчейне (present=false).
|
||||
*/
|
||||
@Injectable()
|
||||
export class KuDecisionSyncService extends AbstractEntitySyncService<KuDecisionDomainEntity, IKuDecisionBlockchainData> implements OnModuleInit {
|
||||
protected readonly entityName = 'KuDecisionDomainEntity';
|
||||
|
||||
constructor(
|
||||
@Inject(KU_DECISION_REPOSITORY)
|
||||
repository: KuDecisionRepository,
|
||||
mapper: KuDecisionDeltaMapper,
|
||||
logger: WinstonLoggerService,
|
||||
private readonly eventEmitter: EventEmitter2
|
||||
) {
|
||||
super(repository, mapper, logger);
|
||||
}
|
||||
|
||||
async onModuleInit() {
|
||||
const allPatterns = this.getAllEventPatterns();
|
||||
this.logger.debug(`Подписка на ${allPatterns.length} паттернов событий: ${allPatterns.join(', ')}`);
|
||||
|
||||
allPatterns.forEach((pattern) => {
|
||||
this.eventEmitter.on(pattern, this.processDelta.bind(this));
|
||||
});
|
||||
}
|
||||
|
||||
@OnEvent('fork::*')
|
||||
async handleEntityFork(forkData: { block_num: number }): Promise<void> {
|
||||
await this.handleFork(forkData.block_num);
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { Injectable, OnModuleInit, Inject } from '@nestjs/common';
|
||||
import { OnEvent, EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { WinstonLoggerService } from '~/application/logger/logger-app.service';
|
||||
import { AbstractEntitySyncService } from '~/shared/services/abstract-entity-sync.service';
|
||||
import { KuTrustRequestDomainEntity } from '../../domain/entities/ku-trust-request.entity';
|
||||
import { KuTrustRequestRepository, KU_TRUST_REQUEST_REPOSITORY } from '../../domain/repositories/ku-trust-request.repository';
|
||||
import { KuTrustRequestDeltaMapper } from '../../infrastructure/blockchain/mappers/ku-trust-request-delta.mapper';
|
||||
import type { IKuTrustRequestBlockchainData } from '../../domain/interfaces/ku-blockchain-data.interface';
|
||||
|
||||
/**
|
||||
* Сервис синхронизации заявок доверенных участков с блокчейном (контракт branch).
|
||||
* История сохраняется в PG после erase записи в блокчейне (present=false).
|
||||
*/
|
||||
@Injectable()
|
||||
export class KuTrustRequestSyncService extends AbstractEntitySyncService<KuTrustRequestDomainEntity, IKuTrustRequestBlockchainData> implements OnModuleInit {
|
||||
protected readonly entityName = 'KuTrustRequestDomainEntity';
|
||||
|
||||
constructor(
|
||||
@Inject(KU_TRUST_REQUEST_REPOSITORY)
|
||||
repository: KuTrustRequestRepository,
|
||||
mapper: KuTrustRequestDeltaMapper,
|
||||
logger: WinstonLoggerService,
|
||||
private readonly eventEmitter: EventEmitter2
|
||||
) {
|
||||
super(repository, mapper, logger);
|
||||
}
|
||||
|
||||
async onModuleInit() {
|
||||
const allPatterns = this.getAllEventPatterns();
|
||||
this.logger.debug(`Подписка на ${allPatterns.length} паттернов событий: ${allPatterns.join(', ')}`);
|
||||
|
||||
allPatterns.forEach((pattern) => {
|
||||
this.eventEmitter.on(pattern, this.processDelta.bind(this));
|
||||
});
|
||||
}
|
||||
|
||||
@OnEvent('fork::*')
|
||||
async handleEntityFork(forkData: { block_num: number }): Promise<void> {
|
||||
await this.handleFork(forkData.block_num);
|
||||
}
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
import type { IBlockchainSynchronizable } from '~/shared/interfaces/blockchain-sync.interface';
|
||||
import { BaseDomainEntity } from '~/shared/sync/entities/base-domain.entity';
|
||||
import type {
|
||||
IKuDecisionQuestionBlockchainData,
|
||||
IKuDecisionQuestionDatabaseData,
|
||||
} from '../interfaces/ku-blockchain-data.interface';
|
||||
|
||||
/**
|
||||
* Доменная сущность вопроса повестки собрания пайщиков кооперативного участка
|
||||
* (таблица decisionq контракта branch).
|
||||
*/
|
||||
export class KuDecisionQuestionDomainEntity
|
||||
extends BaseDomainEntity<IKuDecisionQuestionDatabaseData>
|
||||
implements IBlockchainSynchronizable, Partial<IKuDecisionQuestionBlockchainData>
|
||||
{
|
||||
private static primary_key = 'id';
|
||||
private static sync_key = 'id';
|
||||
|
||||
public id?: number;
|
||||
public decision_id?: number;
|
||||
public number?: number;
|
||||
public coopname?: string;
|
||||
public title?: string;
|
||||
public decision?: string;
|
||||
public context?: string;
|
||||
public counter_votes_for?: number;
|
||||
public counter_votes_against?: number;
|
||||
public counter_votes_abstained?: number;
|
||||
public voters_for?: string[];
|
||||
public voters_against?: string[];
|
||||
public voters_abstained?: string[];
|
||||
|
||||
constructor(databaseData: IKuDecisionQuestionDatabaseData, blockchainData?: IKuDecisionQuestionBlockchainData) {
|
||||
super(databaseData, 'active');
|
||||
|
||||
if (blockchainData) {
|
||||
this.updateFromBlockchain(blockchainData, databaseData.block_num ?? 0, databaseData.present);
|
||||
}
|
||||
}
|
||||
|
||||
getBlockNum(): number | undefined {
|
||||
return this.block_num;
|
||||
}
|
||||
|
||||
public static getPrimaryKey(): string {
|
||||
return KuDecisionQuestionDomainEntity.primary_key;
|
||||
}
|
||||
|
||||
public static getSyncKey(): string {
|
||||
return KuDecisionQuestionDomainEntity.sync_key;
|
||||
}
|
||||
|
||||
getPrimaryKey(): string {
|
||||
return KuDecisionQuestionDomainEntity.primary_key;
|
||||
}
|
||||
|
||||
getSyncKey(): string {
|
||||
return KuDecisionQuestionDomainEntity.sync_key;
|
||||
}
|
||||
|
||||
updateFromBlockchain(blockchainData: IKuDecisionQuestionBlockchainData, blockNum: number, present = true): void {
|
||||
this.block_num = blockNum;
|
||||
this.present = present;
|
||||
|
||||
this.id = Number(blockchainData.id);
|
||||
this.decision_id = Number(blockchainData.decision_id);
|
||||
this.number = Number(blockchainData.number);
|
||||
this.coopname = blockchainData.coopname;
|
||||
this.title = blockchainData.title;
|
||||
this.decision = blockchainData.decision;
|
||||
this.context = blockchainData.context;
|
||||
this.counter_votes_for = Number(blockchainData.counter_votes_for);
|
||||
this.counter_votes_against = Number(blockchainData.counter_votes_against);
|
||||
this.counter_votes_abstained = Number(blockchainData.counter_votes_abstained);
|
||||
this.voters_for = blockchainData.voters_for;
|
||||
this.voters_against = blockchainData.voters_against;
|
||||
this.voters_abstained = blockchainData.voters_abstained;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import type { IBlockchainSynchronizable } from '~/shared/interfaces/blockchain-sync.interface';
|
||||
import { BaseDomainEntity } from '~/shared/sync/entities/base-domain.entity';
|
||||
import type { IKuDecisionBlockchainData, IKuDecisionDatabaseData } from '../interfaces/ku-blockchain-data.interface';
|
||||
|
||||
/**
|
||||
* Доменная сущность решения собрания пайщиков кооперативного участка.
|
||||
* Агрегирует данные базы данных (история сохраняется после erase в блокчейне)
|
||||
* и данные таблицы decisions контракта branch.
|
||||
*/
|
||||
export class KuDecisionDomainEntity
|
||||
extends BaseDomainEntity<IKuDecisionDatabaseData>
|
||||
implements IBlockchainSynchronizable, Partial<IKuDecisionBlockchainData>
|
||||
{
|
||||
// Статические поля ключей для поиска и синхронизации
|
||||
private static primary_key = 'hash';
|
||||
private static sync_key = 'hash';
|
||||
|
||||
public id?: number;
|
||||
public hash?: string;
|
||||
public coopname?: string;
|
||||
public type?: string;
|
||||
public initiator?: string;
|
||||
public chairman?: string;
|
||||
public proposal?: IKuDecisionBlockchainData['proposal'];
|
||||
public protocol?: IKuDecisionBlockchainData['protocol'];
|
||||
public petition?: IKuDecisionBlockchainData['petition'];
|
||||
public liability?: IKuDecisionBlockchainData['liability'];
|
||||
public authority?: IKuDecisionBlockchainData['authority'];
|
||||
public authorization?: IKuDecisionBlockchainData['authorization'];
|
||||
public open_at?: string;
|
||||
public close_at?: string;
|
||||
public signed_ballots?: number;
|
||||
public braname?: string;
|
||||
public address?: string;
|
||||
public participants?: string[];
|
||||
public created_at?: string;
|
||||
|
||||
// Приватные данные собрания — только БД, в блокчейн не публикуются
|
||||
public meet_place?: string;
|
||||
public meet_at?: Date;
|
||||
public branch_name?: string;
|
||||
public branch_email?: string;
|
||||
public branch_phone?: string;
|
||||
public cancelled?: boolean;
|
||||
public meet_reminder_sent?: boolean;
|
||||
|
||||
constructor(databaseData: IKuDecisionDatabaseData, blockchainData?: IKuDecisionBlockchainData) {
|
||||
super(databaseData);
|
||||
|
||||
this.meet_place = databaseData.meet_place;
|
||||
this.meet_at = databaseData.meet_at;
|
||||
this.branch_name = databaseData.branch_name;
|
||||
this.branch_email = databaseData.branch_email;
|
||||
this.branch_phone = databaseData.branch_phone;
|
||||
this.cancelled = databaseData.cancelled;
|
||||
this.meet_reminder_sent = databaseData.meet_reminder_sent;
|
||||
|
||||
if (blockchainData) {
|
||||
this.updateFromBlockchain(blockchainData, databaseData.block_num ?? 0, databaseData.present);
|
||||
}
|
||||
}
|
||||
|
||||
getBlockNum(): number | undefined {
|
||||
return this.block_num;
|
||||
}
|
||||
|
||||
public static getPrimaryKey(): string {
|
||||
return KuDecisionDomainEntity.primary_key;
|
||||
}
|
||||
|
||||
public static getSyncKey(): string {
|
||||
return KuDecisionDomainEntity.sync_key;
|
||||
}
|
||||
|
||||
getPrimaryKey(): string {
|
||||
return KuDecisionDomainEntity.primary_key;
|
||||
}
|
||||
|
||||
getSyncKey(): string {
|
||||
return KuDecisionDomainEntity.sync_key;
|
||||
}
|
||||
|
||||
updateFromBlockchain(blockchainData: IKuDecisionBlockchainData, blockNum: number, present = true): void {
|
||||
this.block_num = blockNum;
|
||||
this.present = present;
|
||||
|
||||
this.id = Number(blockchainData.id);
|
||||
this.hash = blockchainData.hash?.toLowerCase();
|
||||
this.coopname = blockchainData.coopname;
|
||||
this.type = blockchainData.type;
|
||||
this.initiator = blockchainData.initiator;
|
||||
this.chairman = blockchainData.chairman;
|
||||
// Статус контракта сохраняем в базовое поле status
|
||||
this.status = blockchainData.status;
|
||||
this.proposal = blockchainData.proposal;
|
||||
this.protocol = blockchainData.protocol;
|
||||
this.petition = blockchainData.petition;
|
||||
this.liability = blockchainData.liability;
|
||||
this.authority = blockchainData.authority;
|
||||
this.authorization = blockchainData.authorization;
|
||||
this.open_at = blockchainData.open_at;
|
||||
this.close_at = blockchainData.close_at;
|
||||
this.signed_ballots = Number(blockchainData.signed_ballots);
|
||||
this.braname = blockchainData.braname;
|
||||
this.address = blockchainData.address;
|
||||
this.participants = blockchainData.participants;
|
||||
this.created_at = blockchainData.created_at;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import type { IBlockchainSynchronizable } from '~/shared/interfaces/blockchain-sync.interface';
|
||||
import { BaseDomainEntity } from '~/shared/sync/entities/base-domain.entity';
|
||||
import type {
|
||||
IKuTrustRequestBlockchainData,
|
||||
IKuTrustRequestDatabaseData,
|
||||
} from '../interfaces/ku-blockchain-data.interface';
|
||||
|
||||
/**
|
||||
* Доменная сущность заявки на приём доверенным лицом кооперативного участка
|
||||
* (таблица trustreqs контракта branch).
|
||||
*/
|
||||
export class KuTrustRequestDomainEntity
|
||||
extends BaseDomainEntity<IKuTrustRequestDatabaseData>
|
||||
implements IBlockchainSynchronizable, Partial<IKuTrustRequestBlockchainData>
|
||||
{
|
||||
private static primary_key = 'hash';
|
||||
private static sync_key = 'hash';
|
||||
|
||||
public id?: number;
|
||||
public hash?: string;
|
||||
public coopname?: string;
|
||||
public braname?: string;
|
||||
public username?: string;
|
||||
public application?: IKuTrustRequestBlockchainData['application'];
|
||||
public authority?: IKuTrustRequestBlockchainData['authority'];
|
||||
|
||||
constructor(databaseData: IKuTrustRequestDatabaseData, blockchainData?: IKuTrustRequestBlockchainData) {
|
||||
super(databaseData, 'pending');
|
||||
|
||||
if (blockchainData) {
|
||||
this.updateFromBlockchain(blockchainData, databaseData.block_num ?? 0, databaseData.present);
|
||||
}
|
||||
}
|
||||
|
||||
getBlockNum(): number | undefined {
|
||||
return this.block_num;
|
||||
}
|
||||
|
||||
public static getPrimaryKey(): string {
|
||||
return KuTrustRequestDomainEntity.primary_key;
|
||||
}
|
||||
|
||||
public static getSyncKey(): string {
|
||||
return KuTrustRequestDomainEntity.sync_key;
|
||||
}
|
||||
|
||||
getPrimaryKey(): string {
|
||||
return KuTrustRequestDomainEntity.primary_key;
|
||||
}
|
||||
|
||||
getSyncKey(): string {
|
||||
return KuTrustRequestDomainEntity.sync_key;
|
||||
}
|
||||
|
||||
updateFromBlockchain(blockchainData: IKuTrustRequestBlockchainData, blockNum: number, present = true): void {
|
||||
this.block_num = blockNum;
|
||||
this.present = present;
|
||||
|
||||
this.id = Number(blockchainData.id);
|
||||
this.hash = blockchainData.hash?.toLowerCase();
|
||||
this.coopname = blockchainData.coopname;
|
||||
this.braname = blockchainData.braname;
|
||||
this.username = blockchainData.username;
|
||||
this.application = blockchainData.application;
|
||||
this.authority = blockchainData.authority;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { registerEnumType } from '@nestjs/graphql';
|
||||
|
||||
/**
|
||||
* Статус решения собрания пайщиков кооперативного участка.
|
||||
* Значения совпадают со статусами контракта branch.
|
||||
*/
|
||||
export enum KuDecisionStatus {
|
||||
/** Собрание объявлено, открыто присоединение участников */
|
||||
OPENED = 'opened',
|
||||
/** Голосование открыто */
|
||||
VOTING = 'voting',
|
||||
/** Протокол утверждён председателем собрания */
|
||||
APPROVED = 'approved',
|
||||
/** Заявление направлено на рассмотрение совета */
|
||||
ONAPPROVAL = 'onapproval',
|
||||
/** Завершено (запись стёрта в блокчейне: исполнено или отклонено) */
|
||||
COMPLETED = 'completed',
|
||||
/** Отменено организатором собрания */
|
||||
CANCELLED = 'cancelled',
|
||||
}
|
||||
|
||||
registerEnumType(KuDecisionStatus, {
|
||||
name: 'KuDecisionStatus',
|
||||
description: 'Статус решения собрания пайщиков кооперативного участка',
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import { registerEnumType } from '@nestjs/graphql';
|
||||
|
||||
/**
|
||||
* Тип решения собрания пайщиков кооперативного участка
|
||||
*/
|
||||
export enum KuDecisionType {
|
||||
/** Учреждение кооперативного участка */
|
||||
CREATEBRANCH = 'createbranch',
|
||||
/** Свободное решение */
|
||||
FREE = 'free',
|
||||
}
|
||||
|
||||
registerEnumType(KuDecisionType, {
|
||||
name: 'KuDecisionType',
|
||||
description: 'Тип решения собрания пайщиков кооперативного участка',
|
||||
});
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
import type { ISignedDocumentDomainInterface } from '~/domain/document/interfaces/signed-document-domain.interface';
|
||||
|
||||
/**
|
||||
* Доменные input-интерфейсы действий собрания пайщиков кооперативного участка
|
||||
* и приёма доверенных лиц (контракт branch).
|
||||
*/
|
||||
|
||||
export interface KuAgendaPointInputDomainInterface {
|
||||
title: string;
|
||||
decision: string;
|
||||
context: string;
|
||||
}
|
||||
|
||||
export interface CreateKuDecisionInputDomainInterface {
|
||||
coopname: string;
|
||||
hash: string;
|
||||
type: string;
|
||||
initiator: string;
|
||||
braname: string;
|
||||
agenda: KuAgendaPointInputDomainInterface[];
|
||||
proposal: ISignedDocumentDomainInterface;
|
||||
/** Место проведения собрания — приватные данные пайщиков, хранятся только в БД */
|
||||
meet_place: string;
|
||||
/** Время проведения собрания — приватные данные пайщиков, хранятся только в БД */
|
||||
meet_at: string;
|
||||
}
|
||||
|
||||
export interface JoinKuDecisionInputDomainInterface {
|
||||
coopname: string;
|
||||
hash: string;
|
||||
username: string;
|
||||
}
|
||||
|
||||
export interface StartKuDecisionInputDomainInterface {
|
||||
coopname: string;
|
||||
hash: string;
|
||||
/** Избираемый председатель кооперативного участка — из присоединившихся участников */
|
||||
chairman: string;
|
||||
/** Адрес привязки кооперативного участка, определённый собранием */
|
||||
address: string;
|
||||
/** Человекочитаемое наименование участка («РОМАШКА») — в блокчейн не публикуется */
|
||||
branch_name: string;
|
||||
/** Email участка — в блокчейн не публикуется */
|
||||
branch_email: string;
|
||||
/** Телефон участка — в блокчейн не публикуется */
|
||||
branch_phone: string;
|
||||
/** Дополнительные вопросы повестки, внесённые на собрании */
|
||||
agenda: KuAgendaPointInputDomainInterface[];
|
||||
}
|
||||
|
||||
export interface KuVoteItemInputDomainInterface {
|
||||
question_id: number;
|
||||
vote: string;
|
||||
}
|
||||
|
||||
export interface VoteOnKuDecisionInputDomainInterface {
|
||||
coopname: string;
|
||||
hash: string;
|
||||
username: string;
|
||||
ballot: ISignedDocumentDomainInterface;
|
||||
votes: KuVoteItemInputDomainInterface[];
|
||||
}
|
||||
|
||||
export interface CloseKuDecisionInputDomainInterface {
|
||||
coopname: string;
|
||||
hash: string;
|
||||
protocol: ISignedDocumentDomainInterface;
|
||||
}
|
||||
|
||||
export interface ExecKuDecisionInputDomainInterface {
|
||||
coopname: string;
|
||||
hash: string;
|
||||
petition: ISignedDocumentDomainInterface;
|
||||
liability: ISignedDocumentDomainInterface;
|
||||
authority: ISignedDocumentDomainInterface;
|
||||
}
|
||||
|
||||
export interface CancelKuDecisionInputDomainInterface {
|
||||
coopname: string;
|
||||
hash: string;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export interface RequestKuTrustedInputDomainInterface {
|
||||
coopname: string;
|
||||
braname: string;
|
||||
username: string;
|
||||
hash: string;
|
||||
application: ISignedDocumentDomainInterface;
|
||||
authority: ISignedDocumentDomainInterface;
|
||||
}
|
||||
|
||||
export interface ApproveKuTrustedInputDomainInterface {
|
||||
coopname: string;
|
||||
hash: string;
|
||||
countersigned: ISignedDocumentDomainInterface;
|
||||
countersigned_authority: ISignedDocumentDomainInterface;
|
||||
}
|
||||
|
||||
export interface DeclineKuTrustedInputDomainInterface {
|
||||
coopname: string;
|
||||
hash: string;
|
||||
reason: string;
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import type { BranchContract } from 'cooptypes';
|
||||
import type { IBaseDatabaseData } from '~/shared/sync/interfaces/base-database.interface';
|
||||
|
||||
/**
|
||||
* Интерфейс данных решения собрания участка из блокчейна (таблица decisions контракта branch)
|
||||
*/
|
||||
export type IKuDecisionBlockchainData = BranchContract.Tables.Decisions.IDecision;
|
||||
|
||||
/**
|
||||
* Интерфейс данных вопроса повестки из блокчейна (таблица decisionq контракта branch)
|
||||
*/
|
||||
export type IKuDecisionQuestionBlockchainData = BranchContract.Tables.DecisionQuestions.IDecisionQuestion;
|
||||
|
||||
/**
|
||||
* Интерфейс данных заявки доверенного из блокчейна (таблица trustreqs контракта branch)
|
||||
*/
|
||||
export type IKuTrustRequestBlockchainData = BranchContract.Tables.TrustReqs.ITrustRequest;
|
||||
|
||||
/**
|
||||
* Интерфейсы данных из базы данных.
|
||||
* Приватные данные собрания (место/время проведения, наименование участка)
|
||||
* в блокчейн не публикуются — доступны только пайщикам кооператива через БД.
|
||||
*/
|
||||
export type IKuDecisionDatabaseData = IBaseDatabaseData & {
|
||||
meet_place?: string;
|
||||
meet_at?: Date;
|
||||
branch_name?: string;
|
||||
branch_email?: string;
|
||||
branch_phone?: string;
|
||||
cancelled?: boolean;
|
||||
meet_reminder_sent?: boolean;
|
||||
};
|
||||
export type IKuDecisionQuestionDatabaseData = IBaseDatabaseData;
|
||||
export type IKuTrustRequestDatabaseData = IBaseDatabaseData;
|
||||
@@ -0,0 +1,53 @@
|
||||
import type { TransactResult } from '@wharfkit/session';
|
||||
import type {
|
||||
ApproveKuTrustedInputDomainInterface,
|
||||
CancelKuDecisionInputDomainInterface,
|
||||
CloseKuDecisionInputDomainInterface,
|
||||
CreateKuDecisionInputDomainInterface,
|
||||
DeclineKuTrustedInputDomainInterface,
|
||||
ExecKuDecisionInputDomainInterface,
|
||||
JoinKuDecisionInputDomainInterface,
|
||||
RequestKuTrustedInputDomainInterface,
|
||||
StartKuDecisionInputDomainInterface,
|
||||
VoteOnKuDecisionInputDomainInterface,
|
||||
} from './ku-action-inputs.interface';
|
||||
|
||||
/**
|
||||
* Блокчейн-порт собраний и решений кооперативных участков (контракт branch).
|
||||
* Все действия подписываются ключом кооператива.
|
||||
*/
|
||||
export interface KuBlockchainPort {
|
||||
/** Объявление собрания пайщиков участка */
|
||||
createDecision(data: CreateKuDecisionInputDomainInterface): Promise<TransactResult>;
|
||||
|
||||
/** Присоединение пайщика к собранию */
|
||||
joinDecision(data: JoinKuDecisionInputDomainInterface): Promise<TransactResult>;
|
||||
|
||||
/** Назначение председателя собрания */
|
||||
|
||||
/** Открытие голосования */
|
||||
startDecision(data: StartKuDecisionInputDomainInterface): Promise<TransactResult>;
|
||||
|
||||
/** Подача бюллетеня участником */
|
||||
voteOnDecision(data: VoteOnKuDecisionInputDomainInterface): Promise<TransactResult>;
|
||||
|
||||
/** Закрытие голосования и утверждение протокола председателем */
|
||||
closeDecision(data: CloseKuDecisionInputDomainInterface): Promise<TransactResult>;
|
||||
|
||||
/** Направление заявления председателя в совет (учреждение участка) */
|
||||
execDecision(data: ExecKuDecisionInputDomainInterface): Promise<TransactResult>;
|
||||
|
||||
/** Отмена собрания инициатором */
|
||||
cancelDecision(data: CancelKuDecisionInputDomainInterface): Promise<TransactResult>;
|
||||
|
||||
/** Подача заявки доверенного лица участка */
|
||||
requestTrusted(data: RequestKuTrustedInputDomainInterface): Promise<TransactResult>;
|
||||
|
||||
/** Одобрение заявки доверенного встречной подписью председателя участка */
|
||||
approveTrusted(data: ApproveKuTrustedInputDomainInterface): Promise<TransactResult>;
|
||||
|
||||
/** Отклонение заявки доверенного */
|
||||
declineTrusted(data: DeclineKuTrustedInputDomainInterface): Promise<TransactResult>;
|
||||
}
|
||||
|
||||
export const KU_BLOCKCHAIN_PORT = Symbol('KuBlockchainPort');
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import type { IBlockchainSyncRepository } from '~/shared/interfaces/blockchain-sync.interface';
|
||||
import type { KuDecisionQuestionDomainEntity } from '../entities/ku-decision-question.entity';
|
||||
|
||||
export interface KuDecisionQuestionRepository extends IBlockchainSyncRepository<KuDecisionQuestionDomainEntity> {
|
||||
findByDecisionId(coopname: string, decisionId: number): Promise<KuDecisionQuestionDomainEntity[]>;
|
||||
}
|
||||
|
||||
export const KU_DECISION_QUESTION_REPOSITORY = Symbol('KuDecisionQuestionRepository');
|
||||
@@ -0,0 +1,51 @@
|
||||
import type { IBlockchainSyncRepository } from '~/shared/interfaces/blockchain-sync.interface';
|
||||
import type {
|
||||
PaginationInputDomainInterface,
|
||||
PaginationResultDomainInterface,
|
||||
} from '~/domain/common/interfaces/pagination.interface';
|
||||
import type { KuDecisionDomainEntity } from '../entities/ku-decision.entity';
|
||||
|
||||
export interface KuDecisionFilterDomainInterface {
|
||||
coopname?: string;
|
||||
type?: string;
|
||||
status?: string;
|
||||
braname?: string;
|
||||
initiator?: string;
|
||||
present?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Приватные данные собрания — хранятся только в БД платформы,
|
||||
* в блокчейн не публикуются (видны только пайщикам кооператива).
|
||||
*/
|
||||
export interface KuDecisionPrivateDataDomainInterface {
|
||||
hash: string;
|
||||
coopname?: string;
|
||||
type?: string;
|
||||
initiator?: string;
|
||||
meet_place?: string;
|
||||
meet_at?: Date;
|
||||
branch_name?: string;
|
||||
branch_email?: string;
|
||||
branch_phone?: string;
|
||||
/** Контракт стирает запись одинаково при любом исходе — факт отмены фиксируем в БД */
|
||||
cancelled?: boolean;
|
||||
}
|
||||
|
||||
export interface KuDecisionRepository extends IBlockchainSyncRepository<KuDecisionDomainEntity> {
|
||||
findByHash(hash: string): Promise<KuDecisionDomainEntity | null>;
|
||||
findAllPaginated(
|
||||
filter?: KuDecisionFilterDomainInterface,
|
||||
options?: PaginationInputDomainInterface
|
||||
): Promise<PaginationResultDomainInterface<KuDecisionDomainEntity>>;
|
||||
/**
|
||||
* Записать приватные данные собрания (upsert по hash): запись могла ещё
|
||||
* не появиться из синка — тогда создаётся placeholder, который синк дополнит.
|
||||
*/
|
||||
upsertPrivateData(data: KuDecisionPrivateDataDomainInterface): Promise<void>;
|
||||
/** Живые собрания с назначенным временем в окне [from, to), по которым напоминание ещё не отправлено */
|
||||
findMeetingsForReminder(from: Date, to: Date): Promise<KuDecisionDomainEntity[]>;
|
||||
markReminderSent(hash: string): Promise<void>;
|
||||
}
|
||||
|
||||
export const KU_DECISION_REPOSITORY = Symbol('KuDecisionRepository');
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import type { IBlockchainSyncRepository } from '~/shared/interfaces/blockchain-sync.interface';
|
||||
import type {
|
||||
PaginationInputDomainInterface,
|
||||
PaginationResultDomainInterface,
|
||||
} from '~/domain/common/interfaces/pagination.interface';
|
||||
import type { KuTrustRequestDomainEntity } from '../entities/ku-trust-request.entity';
|
||||
|
||||
export interface KuTrustRequestFilterDomainInterface {
|
||||
coopname?: string;
|
||||
braname?: string;
|
||||
username?: string;
|
||||
present?: boolean;
|
||||
}
|
||||
|
||||
export interface KuTrustRequestRepository extends IBlockchainSyncRepository<KuTrustRequestDomainEntity> {
|
||||
findByHash(hash: string): Promise<KuTrustRequestDomainEntity | null>;
|
||||
findAllPaginated(
|
||||
filter?: KuTrustRequestFilterDomainInterface,
|
||||
options?: PaginationInputDomainInterface
|
||||
): Promise<PaginationResultDomainInterface<KuTrustRequestDomainEntity>>;
|
||||
}
|
||||
|
||||
export const KU_TRUST_REQUEST_REPOSITORY = Symbol('KuTrustRequestRepository');
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { BranchContract } from 'cooptypes';
|
||||
import type { TransactResult } from '@wharfkit/session';
|
||||
import httpStatus from 'http-status';
|
||||
import { BlockchainService } from '~/infrastructure/blockchain/blockchain.service';
|
||||
import { VaultDomainService, VAULT_DOMAIN_SERVICE } from '~/domain/vault/services/vault-domain.service';
|
||||
import { HttpApiError } from '~/utils/httpApiError';
|
||||
import { DomainToBlockchainUtils } from '~/shared/utils/domain-to-blockchain.utils';
|
||||
import type { KuBlockchainPort } from '../../../domain/interfaces/ku-blockchain.port';
|
||||
import type {
|
||||
ApproveKuTrustedInputDomainInterface,
|
||||
CancelKuDecisionInputDomainInterface,
|
||||
CloseKuDecisionInputDomainInterface,
|
||||
CreateKuDecisionInputDomainInterface,
|
||||
DeclineKuTrustedInputDomainInterface,
|
||||
ExecKuDecisionInputDomainInterface,
|
||||
JoinKuDecisionInputDomainInterface,
|
||||
RequestKuTrustedInputDomainInterface,
|
||||
StartKuDecisionInputDomainInterface,
|
||||
VoteOnKuDecisionInputDomainInterface,
|
||||
} from '../../../domain/interfaces/ku-action-inputs.interface';
|
||||
|
||||
/**
|
||||
* Адаптер блокчейн-порта собраний и решений кооперативных участков.
|
||||
* Все действия контракта branch подписываются ключом кооператива из vault.
|
||||
*/
|
||||
@Injectable()
|
||||
export class KuBlockchainAdapter implements KuBlockchainPort {
|
||||
constructor(
|
||||
private readonly blockchainService: BlockchainService,
|
||||
private readonly domainToBlockchainUtils: DomainToBlockchainUtils,
|
||||
@Inject(VAULT_DOMAIN_SERVICE) private readonly vaultDomainService: VaultDomainService
|
||||
) {}
|
||||
|
||||
private async transactAs(coopname: string, name: string, data: Record<string, unknown>): Promise<TransactResult> {
|
||||
const wif = await this.vaultDomainService.getWif(coopname);
|
||||
if (!wif) throw new HttpApiError(httpStatus.BAD_GATEWAY, 'Не найден приватный ключ для совершения операции');
|
||||
|
||||
this.blockchainService.initialize(coopname, wif);
|
||||
|
||||
return (await this.blockchainService.transact({
|
||||
account: BranchContract.contractName.production,
|
||||
name,
|
||||
authorization: [{ actor: coopname, permission: 'active' }],
|
||||
data,
|
||||
})) as TransactResult;
|
||||
}
|
||||
|
||||
async createDecision(data: CreateKuDecisionInputDomainInterface): Promise<TransactResult> {
|
||||
const blockchainData: BranchContract.Actions.CreateDec.ICreateDec = {
|
||||
coopname: data.coopname,
|
||||
hash: data.hash,
|
||||
type: data.type,
|
||||
initiator: data.initiator,
|
||||
proposal: this.domainToBlockchainUtils.convertSignedDocumentToBlockchainFormat(data.proposal),
|
||||
braname: data.braname,
|
||||
agenda: data.agenda,
|
||||
};
|
||||
return this.transactAs(data.coopname, BranchContract.Actions.CreateDec.actionName, blockchainData as any);
|
||||
}
|
||||
|
||||
async joinDecision(data: JoinKuDecisionInputDomainInterface): Promise<TransactResult> {
|
||||
const blockchainData: BranchContract.Actions.JoinDec.IJoinDec = {
|
||||
coopname: data.coopname,
|
||||
hash: data.hash,
|
||||
username: data.username,
|
||||
};
|
||||
return this.transactAs(data.coopname, BranchContract.Actions.JoinDec.actionName, blockchainData as any);
|
||||
}
|
||||
|
||||
async startDecision(data: StartKuDecisionInputDomainInterface): Promise<TransactResult> {
|
||||
// branch_name в блокчейн не уходит — приватное наименование хранится в БД
|
||||
const blockchainData: BranchContract.Actions.StartDec.IStartDec = {
|
||||
coopname: data.coopname,
|
||||
hash: data.hash,
|
||||
chairman: data.chairman,
|
||||
address: data.address,
|
||||
agenda: (data.agenda ?? []).map((point) => ({
|
||||
title: point.title,
|
||||
decision: point.decision,
|
||||
context: point.context ?? '',
|
||||
})),
|
||||
};
|
||||
return this.transactAs(data.coopname, BranchContract.Actions.StartDec.actionName, blockchainData as any);
|
||||
}
|
||||
|
||||
async voteOnDecision(data: VoteOnKuDecisionInputDomainInterface): Promise<TransactResult> {
|
||||
const blockchainData: BranchContract.Actions.VoteDec.IVoteDec = {
|
||||
coopname: data.coopname,
|
||||
hash: data.hash,
|
||||
username: data.username,
|
||||
ballot: this.domainToBlockchainUtils.convertSignedDocumentToBlockchainFormat(data.ballot),
|
||||
votes: data.votes,
|
||||
};
|
||||
return this.transactAs(data.coopname, BranchContract.Actions.VoteDec.actionName, blockchainData as any);
|
||||
}
|
||||
|
||||
async closeDecision(data: CloseKuDecisionInputDomainInterface): Promise<TransactResult> {
|
||||
const blockchainData: BranchContract.Actions.CloseDec.ICloseDec = {
|
||||
coopname: data.coopname,
|
||||
hash: data.hash,
|
||||
protocol: this.domainToBlockchainUtils.convertSignedDocumentToBlockchainFormat(data.protocol),
|
||||
};
|
||||
return this.transactAs(data.coopname, BranchContract.Actions.CloseDec.actionName, blockchainData as any);
|
||||
}
|
||||
|
||||
async execDecision(data: ExecKuDecisionInputDomainInterface): Promise<TransactResult> {
|
||||
const blockchainData: BranchContract.Actions.Exec.IExec = {
|
||||
coopname: data.coopname,
|
||||
hash: data.hash,
|
||||
petition: this.domainToBlockchainUtils.convertSignedDocumentToBlockchainFormat(data.petition),
|
||||
liability: this.domainToBlockchainUtils.convertSignedDocumentToBlockchainFormat(data.liability),
|
||||
authority: this.domainToBlockchainUtils.convertSignedDocumentToBlockchainFormat(data.authority),
|
||||
};
|
||||
return this.transactAs(data.coopname, BranchContract.Actions.Exec.actionName, blockchainData as any);
|
||||
}
|
||||
|
||||
async cancelDecision(data: CancelKuDecisionInputDomainInterface): Promise<TransactResult> {
|
||||
const blockchainData: BranchContract.Actions.CancelDec.ICancelDec = {
|
||||
coopname: data.coopname,
|
||||
hash: data.hash,
|
||||
reason: data.reason,
|
||||
};
|
||||
return this.transactAs(data.coopname, BranchContract.Actions.CancelDec.actionName, blockchainData as any);
|
||||
}
|
||||
|
||||
async requestTrusted(data: RequestKuTrustedInputDomainInterface): Promise<TransactResult> {
|
||||
const blockchainData: BranchContract.Actions.ReqTrusted.IReqTrusted = {
|
||||
coopname: data.coopname,
|
||||
braname: data.braname,
|
||||
username: data.username,
|
||||
hash: data.hash,
|
||||
application: this.domainToBlockchainUtils.convertSignedDocumentToBlockchainFormat(data.application),
|
||||
authority: this.domainToBlockchainUtils.convertSignedDocumentToBlockchainFormat(data.authority),
|
||||
};
|
||||
return this.transactAs(data.coopname, BranchContract.Actions.ReqTrusted.actionName, blockchainData as any);
|
||||
}
|
||||
|
||||
async approveTrusted(data: ApproveKuTrustedInputDomainInterface): Promise<TransactResult> {
|
||||
const blockchainData: BranchContract.Actions.ApprTrusted.IApprTrusted = {
|
||||
coopname: data.coopname,
|
||||
hash: data.hash,
|
||||
countersigned: this.domainToBlockchainUtils.convertSignedDocumentToBlockchainFormat(data.countersigned),
|
||||
countersigned_authority: this.domainToBlockchainUtils.convertSignedDocumentToBlockchainFormat(
|
||||
data.countersigned_authority
|
||||
),
|
||||
};
|
||||
return this.transactAs(data.coopname, BranchContract.Actions.ApprTrusted.actionName, blockchainData as any);
|
||||
}
|
||||
|
||||
async declineTrusted(data: DeclineKuTrustedInputDomainInterface): Promise<TransactResult> {
|
||||
const blockchainData: BranchContract.Actions.DeclTrusted.IDeclTrusted = {
|
||||
coopname: data.coopname,
|
||||
hash: data.hash,
|
||||
reason: data.reason,
|
||||
};
|
||||
return this.transactAs(data.coopname, BranchContract.Actions.DeclTrusted.actionName, blockchainData as any);
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import type { IDelta } from '~/types/common';
|
||||
import { WinstonLoggerService } from '~/application/logger/logger-app.service';
|
||||
import { AbstractBlockchainDeltaMapper } from '~/shared/abstract-blockchain-delta.mapper';
|
||||
import { KuDecisionDomainEntity } from '../../../domain/entities/ku-decision.entity';
|
||||
import type { IKuDecisionBlockchainData } from '../../../domain/interfaces/ku-blockchain-data.interface';
|
||||
import { KuContractInfoService } from '../../services/ku-contract-info.service';
|
||||
|
||||
/**
|
||||
* Маппер дельт блокчейна для таблицы decisions контракта branch
|
||||
*/
|
||||
@Injectable()
|
||||
export class KuDecisionDeltaMapper extends AbstractBlockchainDeltaMapper<IKuDecisionBlockchainData, KuDecisionDomainEntity> {
|
||||
constructor(private readonly logger: WinstonLoggerService, private readonly contractInfo: KuContractInfoService) {
|
||||
super();
|
||||
this.logger.setContext(KuDecisionDeltaMapper.name);
|
||||
}
|
||||
|
||||
mapDeltaToBlockchainData(delta: IDelta): IKuDecisionBlockchainData | null {
|
||||
const value = delta.value;
|
||||
if (!value) {
|
||||
this.logger.warn(`Delta has no value: table=${delta.table}, key=${delta.primary_key}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return value as IKuDecisionBlockchainData;
|
||||
}
|
||||
|
||||
extractSyncValue(delta: IDelta): string {
|
||||
const key = this.extractSyncKey();
|
||||
if (!delta.value || delta.value[key] === undefined || delta.value[key] === null) {
|
||||
throw new Error(`Delta has no value: table=${delta.table}, key=${key}`);
|
||||
}
|
||||
|
||||
return String(delta.value[key]);
|
||||
}
|
||||
|
||||
extractSyncKey(): string {
|
||||
return KuDecisionDomainEntity.getSyncKey();
|
||||
}
|
||||
|
||||
getSupportedContractNames(): string[] {
|
||||
return this.contractInfo.getSupportedContractNames();
|
||||
}
|
||||
|
||||
getSupportedTableNames(): string[] {
|
||||
return this.contractInfo.getTablePatterns('decisions');
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import type { IDelta } from '~/types/common';
|
||||
import { WinstonLoggerService } from '~/application/logger/logger-app.service';
|
||||
import { AbstractBlockchainDeltaMapper } from '~/shared/abstract-blockchain-delta.mapper';
|
||||
import { KuDecisionQuestionDomainEntity } from '../../../domain/entities/ku-decision-question.entity';
|
||||
import type { IKuDecisionQuestionBlockchainData } from '../../../domain/interfaces/ku-blockchain-data.interface';
|
||||
import { KuContractInfoService } from '../../services/ku-contract-info.service';
|
||||
|
||||
/**
|
||||
* Маппер дельт блокчейна для таблицы decisionq контракта branch
|
||||
*/
|
||||
@Injectable()
|
||||
export class KuDecisionQuestionDeltaMapper extends AbstractBlockchainDeltaMapper<IKuDecisionQuestionBlockchainData, KuDecisionQuestionDomainEntity> {
|
||||
constructor(private readonly logger: WinstonLoggerService, private readonly contractInfo: KuContractInfoService) {
|
||||
super();
|
||||
this.logger.setContext(KuDecisionQuestionDeltaMapper.name);
|
||||
}
|
||||
|
||||
mapDeltaToBlockchainData(delta: IDelta): IKuDecisionQuestionBlockchainData | null {
|
||||
const value = delta.value;
|
||||
if (!value) {
|
||||
this.logger.warn(`Delta has no value: table=${delta.table}, key=${delta.primary_key}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return value as IKuDecisionQuestionBlockchainData;
|
||||
}
|
||||
|
||||
extractSyncValue(delta: IDelta): string {
|
||||
const key = this.extractSyncKey();
|
||||
if (!delta.value || delta.value[key] === undefined || delta.value[key] === null) {
|
||||
throw new Error(`Delta has no value: table=${delta.table}, key=${key}`);
|
||||
}
|
||||
|
||||
return String(delta.value[key]);
|
||||
}
|
||||
|
||||
extractSyncKey(): string {
|
||||
return KuDecisionQuestionDomainEntity.getSyncKey();
|
||||
}
|
||||
|
||||
getSupportedContractNames(): string[] {
|
||||
return this.contractInfo.getSupportedContractNames();
|
||||
}
|
||||
|
||||
getSupportedTableNames(): string[] {
|
||||
return this.contractInfo.getTablePatterns('decisionq');
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import type { IDelta } from '~/types/common';
|
||||
import { WinstonLoggerService } from '~/application/logger/logger-app.service';
|
||||
import { AbstractBlockchainDeltaMapper } from '~/shared/abstract-blockchain-delta.mapper';
|
||||
import { KuTrustRequestDomainEntity } from '../../../domain/entities/ku-trust-request.entity';
|
||||
import type { IKuTrustRequestBlockchainData } from '../../../domain/interfaces/ku-blockchain-data.interface';
|
||||
import { KuContractInfoService } from '../../services/ku-contract-info.service';
|
||||
|
||||
/**
|
||||
* Маппер дельт блокчейна для таблицы trustreqs контракта branch
|
||||
*/
|
||||
@Injectable()
|
||||
export class KuTrustRequestDeltaMapper extends AbstractBlockchainDeltaMapper<IKuTrustRequestBlockchainData, KuTrustRequestDomainEntity> {
|
||||
constructor(private readonly logger: WinstonLoggerService, private readonly contractInfo: KuContractInfoService) {
|
||||
super();
|
||||
this.logger.setContext(KuTrustRequestDeltaMapper.name);
|
||||
}
|
||||
|
||||
mapDeltaToBlockchainData(delta: IDelta): IKuTrustRequestBlockchainData | null {
|
||||
const value = delta.value;
|
||||
if (!value) {
|
||||
this.logger.warn(`Delta has no value: table=${delta.table}, key=${delta.primary_key}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return value as IKuTrustRequestBlockchainData;
|
||||
}
|
||||
|
||||
extractSyncValue(delta: IDelta): string {
|
||||
const key = this.extractSyncKey();
|
||||
if (!delta.value || delta.value[key] === undefined || delta.value[key] === null) {
|
||||
throw new Error(`Delta has no value: table=${delta.table}, key=${key}`);
|
||||
}
|
||||
|
||||
return String(delta.value[key]);
|
||||
}
|
||||
|
||||
extractSyncKey(): string {
|
||||
return KuTrustRequestDomainEntity.getSyncKey();
|
||||
}
|
||||
|
||||
getSupportedContractNames(): string[] {
|
||||
return this.contractInfo.getSupportedContractNames();
|
||||
}
|
||||
|
||||
getSupportedTableNames(): string[] {
|
||||
return this.contractInfo.getTablePatterns('trustreqs');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { EntityVersionTypeormEntity } from '~/shared/sync/entities/entity-version.typeorm-entity';
|
||||
import { KuDecisionTypeormEntity } from '../entities/ku-decision.typeorm-entity';
|
||||
import { KuDecisionQuestionTypeormEntity } from '../entities/ku-decision-question.typeorm-entity';
|
||||
import { KuTrustRequestTypeormEntity } from '../entities/ku-trust-request.typeorm-entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
KuDecisionTypeormEntity,
|
||||
KuDecisionQuestionTypeormEntity,
|
||||
KuTrustRequestTypeormEntity,
|
||||
EntityVersionTypeormEntity,
|
||||
]),
|
||||
],
|
||||
exports: [TypeOrmModule],
|
||||
})
|
||||
export class KuDatabaseModule {}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import { Entity, Column, Index } from 'typeorm';
|
||||
import { BaseTypeormEntity } from '~/shared/sync/entities/base-typeorm.entity';
|
||||
|
||||
export const EntityName = 'ku_decision_questions';
|
||||
|
||||
@Entity(EntityName)
|
||||
@Index(`idx_${EntityName}_blockchain_id`, ['id'])
|
||||
@Index(`idx_${EntityName}_decision_id`, ['decision_id'])
|
||||
@Index(`idx_${EntityName}_coopname`, ['coopname'])
|
||||
export class KuDecisionQuestionTypeormEntity extends BaseTypeormEntity {
|
||||
static getTableName(): string {
|
||||
return EntityName;
|
||||
}
|
||||
|
||||
@Column({ type: 'integer', nullable: true })
|
||||
id!: number;
|
||||
|
||||
// Поля из блокчейна (table_branch_decisions.hpp, таблица decisionq)
|
||||
@Column({ type: 'integer' })
|
||||
decision_id!: number;
|
||||
|
||||
@Column({ type: 'integer' })
|
||||
number!: number;
|
||||
|
||||
@Column({ type: 'varchar', length: 12 })
|
||||
coopname!: string;
|
||||
|
||||
@Column({ type: 'text' })
|
||||
title!: string;
|
||||
|
||||
@Column({ type: 'text' })
|
||||
decision!: string;
|
||||
|
||||
@Column({ type: 'text', default: '' })
|
||||
context!: string;
|
||||
|
||||
@Column({ type: 'integer', default: 0 })
|
||||
counter_votes_for!: number;
|
||||
|
||||
@Column({ type: 'integer', default: 0 })
|
||||
counter_votes_against!: number;
|
||||
|
||||
@Column({ type: 'integer', default: 0 })
|
||||
counter_votes_abstained!: number;
|
||||
|
||||
@Column({ type: 'jsonb', default: () => `'[]'` })
|
||||
voters_for!: string[];
|
||||
|
||||
@Column({ type: 'jsonb', default: () => `'[]'` })
|
||||
voters_against!: string[];
|
||||
|
||||
@Column({ type: 'jsonb', default: () => `'[]'` })
|
||||
voters_abstained!: string[];
|
||||
}
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
import { Entity, Column, Index } from 'typeorm';
|
||||
import { BaseTypeormEntity } from '~/shared/sync/entities/base-typeorm.entity';
|
||||
|
||||
export const EntityName = 'ku_decisions';
|
||||
|
||||
@Entity(EntityName)
|
||||
@Index(`idx_${EntityName}_blockchain_id`, ['id'])
|
||||
@Index(`idx_${EntityName}_hash`, ['hash'], { unique: true })
|
||||
@Index(`idx_${EntityName}_coopname`, ['coopname'])
|
||||
@Index(`idx_${EntityName}_type`, ['type'])
|
||||
@Index(`idx_${EntityName}_braname`, ['braname'])
|
||||
@Index(`idx_${EntityName}_initiator`, ['initiator'])
|
||||
@Index(`idx_${EntityName}_created_at`, ['_created_at'])
|
||||
export class KuDecisionTypeormEntity extends BaseTypeormEntity {
|
||||
static getTableName(): string {
|
||||
return EntityName;
|
||||
}
|
||||
|
||||
@Column({ type: 'integer', nullable: true })
|
||||
id!: number;
|
||||
|
||||
// Поля из блокчейна (table_branch_decisions.hpp)
|
||||
@Column({ type: 'varchar' })
|
||||
hash!: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 12 })
|
||||
coopname!: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 12 })
|
||||
type!: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 12 })
|
||||
initiator!: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 12, default: '' })
|
||||
chairman!: string;
|
||||
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
proposal!: object;
|
||||
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
protocol!: object;
|
||||
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
petition!: object;
|
||||
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
liability!: object;
|
||||
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
authority!: object;
|
||||
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
authorization!: object;
|
||||
|
||||
@Column({ type: 'timestamp', nullable: true })
|
||||
open_at!: Date;
|
||||
|
||||
@Column({ type: 'timestamp', nullable: true })
|
||||
close_at!: Date;
|
||||
|
||||
@Column({ type: 'integer', default: 0 })
|
||||
signed_ballots!: number;
|
||||
|
||||
@Column({ type: 'varchar', length: 12, default: '' })
|
||||
braname!: string;
|
||||
|
||||
@Column({ type: 'varchar', default: '' })
|
||||
address!: string;
|
||||
|
||||
@Column({ type: 'jsonb', default: () => `'[]'` })
|
||||
participants!: string[];
|
||||
|
||||
@Column({ type: 'timestamp', nullable: true })
|
||||
created_at!: Date;
|
||||
|
||||
// Приватные данные собрания — только БД, в блокчейн не публикуются
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
meet_place!: string | null;
|
||||
|
||||
@Column({ type: 'timestamp', nullable: true })
|
||||
meet_at!: Date | null;
|
||||
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
branch_name!: string | null;
|
||||
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
branch_email!: string | null;
|
||||
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
branch_phone!: string | null;
|
||||
|
||||
@Column({ type: 'boolean', default: false })
|
||||
cancelled!: boolean;
|
||||
|
||||
// напоминание участникам за час до собрания уже отправлено
|
||||
@Column({ type: 'boolean', default: false })
|
||||
meet_reminder_sent!: boolean;
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import { Entity, Column, Index } from 'typeorm';
|
||||
import { BaseTypeormEntity } from '~/shared/sync/entities/base-typeorm.entity';
|
||||
|
||||
export const EntityName = 'ku_trust_requests';
|
||||
|
||||
@Entity(EntityName)
|
||||
@Index(`idx_${EntityName}_blockchain_id`, ['id'])
|
||||
@Index(`idx_${EntityName}_hash`, ['hash'], { unique: true })
|
||||
@Index(`idx_${EntityName}_coopname`, ['coopname'])
|
||||
@Index(`idx_${EntityName}_braname`, ['braname'])
|
||||
@Index(`idx_${EntityName}_username`, ['username'])
|
||||
export class KuTrustRequestTypeormEntity extends BaseTypeormEntity {
|
||||
static getTableName(): string {
|
||||
return EntityName;
|
||||
}
|
||||
|
||||
@Column({ type: 'integer', nullable: true })
|
||||
id!: number;
|
||||
|
||||
// Поля из блокчейна (table_branch_trustreqs.hpp)
|
||||
@Column({ type: 'varchar' })
|
||||
hash!: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 12 })
|
||||
coopname!: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 12 })
|
||||
braname!: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 12 })
|
||||
username!: string;
|
||||
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
application!: object;
|
||||
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
authority!: object;
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
import { KuDecisionQuestionDomainEntity } from '../../domain/entities/ku-decision-question.entity';
|
||||
import type { KuDecisionQuestionTypeormEntity } from '../entities/ku-decision-question.typeorm-entity';
|
||||
import type {
|
||||
IKuDecisionQuestionBlockchainData,
|
||||
IKuDecisionQuestionDatabaseData,
|
||||
} from '../../domain/interfaces/ku-blockchain-data.interface';
|
||||
|
||||
/**
|
||||
* Маппер между доменной сущностью вопроса повестки и TypeORM-сущностью
|
||||
*/
|
||||
export class KuDecisionQuestionMapper {
|
||||
static toDomain(entity: KuDecisionQuestionTypeormEntity): KuDecisionQuestionDomainEntity {
|
||||
const databaseData: IKuDecisionQuestionDatabaseData = {
|
||||
_id: entity._id,
|
||||
block_num: entity.block_num,
|
||||
present: entity.present,
|
||||
status: entity.status,
|
||||
_created_at: entity._created_at,
|
||||
_updated_at: entity._updated_at,
|
||||
};
|
||||
|
||||
let blockchainData: IKuDecisionQuestionBlockchainData | undefined;
|
||||
|
||||
if (entity.id !== null && entity.id !== undefined) {
|
||||
blockchainData = {
|
||||
id: entity.id,
|
||||
decision_id: entity.decision_id,
|
||||
number: entity.number,
|
||||
coopname: entity.coopname,
|
||||
title: entity.title,
|
||||
decision: entity.decision,
|
||||
context: entity.context,
|
||||
counter_votes_for: entity.counter_votes_for,
|
||||
counter_votes_against: entity.counter_votes_against,
|
||||
counter_votes_abstained: entity.counter_votes_abstained,
|
||||
voters_for: entity.voters_for ?? [],
|
||||
voters_against: entity.voters_against ?? [],
|
||||
voters_abstained: entity.voters_abstained ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
return new KuDecisionQuestionDomainEntity(databaseData, blockchainData);
|
||||
}
|
||||
|
||||
static toEntity(domain: KuDecisionQuestionDomainEntity): Partial<KuDecisionQuestionTypeormEntity> {
|
||||
return {
|
||||
_id: domain._id,
|
||||
block_num: domain.block_num ?? 0,
|
||||
present: domain.present,
|
||||
status: domain.status as string,
|
||||
_created_at: domain._created_at,
|
||||
_updated_at: domain._updated_at,
|
||||
id: domain.id as number,
|
||||
decision_id: domain.decision_id as number,
|
||||
number: domain.number as number,
|
||||
coopname: domain.coopname as string,
|
||||
title: domain.title as string,
|
||||
decision: domain.decision as string,
|
||||
context: domain.context ?? '',
|
||||
counter_votes_for: domain.counter_votes_for ?? 0,
|
||||
counter_votes_against: domain.counter_votes_against ?? 0,
|
||||
counter_votes_abstained: domain.counter_votes_abstained ?? 0,
|
||||
voters_for: domain.voters_for ?? [],
|
||||
voters_against: domain.voters_against ?? [],
|
||||
voters_abstained: domain.voters_abstained ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
static toUpdateEntity(domain: Partial<KuDecisionQuestionDomainEntity>): Partial<KuDecisionQuestionTypeormEntity> {
|
||||
const updateData: Partial<KuDecisionQuestionTypeormEntity> = {};
|
||||
|
||||
if (domain.block_num !== undefined) updateData.block_num = domain.block_num;
|
||||
if (domain.present !== undefined) updateData.present = domain.present;
|
||||
if (domain.counter_votes_for !== undefined) updateData.counter_votes_for = domain.counter_votes_for;
|
||||
if (domain.counter_votes_against !== undefined) updateData.counter_votes_against = domain.counter_votes_against;
|
||||
if (domain.counter_votes_abstained !== undefined) updateData.counter_votes_abstained = domain.counter_votes_abstained;
|
||||
if (domain.voters_for !== undefined) updateData.voters_for = domain.voters_for;
|
||||
if (domain.voters_against !== undefined) updateData.voters_against = domain.voters_against;
|
||||
if (domain.voters_abstained !== undefined) updateData.voters_abstained = domain.voters_abstained;
|
||||
|
||||
return updateData;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import { KuDecisionDomainEntity } from '../../domain/entities/ku-decision.entity';
|
||||
import type { KuDecisionTypeormEntity } from '../entities/ku-decision.typeorm-entity';
|
||||
import type {
|
||||
IKuDecisionBlockchainData,
|
||||
IKuDecisionDatabaseData,
|
||||
} from '../../domain/interfaces/ku-blockchain-data.interface';
|
||||
|
||||
/**
|
||||
* Маппер между доменной сущностью решения собрания участка и TypeORM-сущностью
|
||||
*/
|
||||
export class KuDecisionMapper {
|
||||
static toDomain(entity: KuDecisionTypeormEntity): KuDecisionDomainEntity {
|
||||
const databaseData: IKuDecisionDatabaseData = {
|
||||
_id: entity._id,
|
||||
block_num: entity.block_num,
|
||||
present: entity.present,
|
||||
status: entity.status,
|
||||
_created_at: entity._created_at,
|
||||
_updated_at: entity._updated_at,
|
||||
meet_place: entity.meet_place ?? undefined,
|
||||
meet_at: entity.meet_at ?? undefined,
|
||||
branch_name: entity.branch_name ?? undefined,
|
||||
branch_email: entity.branch_email ?? undefined,
|
||||
branch_phone: entity.branch_phone ?? undefined,
|
||||
cancelled: entity.cancelled ?? undefined,
|
||||
meet_reminder_sent: entity.meet_reminder_sent ?? undefined,
|
||||
};
|
||||
|
||||
let blockchainData: IKuDecisionBlockchainData | undefined;
|
||||
|
||||
if (entity.hash) {
|
||||
blockchainData = {
|
||||
id: entity.id,
|
||||
hash: entity.hash,
|
||||
coopname: entity.coopname,
|
||||
type: entity.type,
|
||||
initiator: entity.initiator,
|
||||
chairman: entity.chairman,
|
||||
status: entity.status,
|
||||
proposal: entity.proposal as IKuDecisionBlockchainData['proposal'],
|
||||
protocol: entity.protocol as IKuDecisionBlockchainData['protocol'],
|
||||
petition: entity.petition as IKuDecisionBlockchainData['petition'],
|
||||
liability: entity.liability as IKuDecisionBlockchainData['liability'],
|
||||
authority: entity.authority as IKuDecisionBlockchainData['authority'],
|
||||
authorization: entity.authorization as IKuDecisionBlockchainData['authorization'],
|
||||
open_at: entity.open_at?.toISOString() ?? '',
|
||||
close_at: entity.close_at?.toISOString() ?? '',
|
||||
signed_ballots: entity.signed_ballots,
|
||||
braname: entity.braname,
|
||||
address: entity.address,
|
||||
participants: entity.participants ?? [],
|
||||
created_at: entity.created_at?.toISOString() ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
return new KuDecisionDomainEntity(databaseData, blockchainData);
|
||||
}
|
||||
|
||||
static toEntity(domain: KuDecisionDomainEntity): Partial<KuDecisionTypeormEntity> {
|
||||
return {
|
||||
_id: domain._id,
|
||||
block_num: domain.block_num ?? 0,
|
||||
present: domain.present,
|
||||
status: domain.status as string,
|
||||
_created_at: domain._created_at,
|
||||
_updated_at: domain._updated_at,
|
||||
id: domain.id as number,
|
||||
hash: domain.hash as string,
|
||||
coopname: domain.coopname as string,
|
||||
type: domain.type as string,
|
||||
initiator: domain.initiator as string,
|
||||
chairman: domain.chairman as string,
|
||||
proposal: domain.proposal as object,
|
||||
protocol: domain.protocol as object,
|
||||
petition: domain.petition as object,
|
||||
liability: domain.liability as object,
|
||||
authority: domain.authority as object,
|
||||
authorization: domain.authorization as object,
|
||||
open_at: domain.open_at ? new Date(domain.open_at) : undefined,
|
||||
close_at: domain.close_at ? new Date(domain.close_at) : undefined,
|
||||
signed_ballots: domain.signed_ballots ?? 0,
|
||||
braname: domain.braname as string,
|
||||
address: domain.address as string,
|
||||
participants: domain.participants ?? [],
|
||||
created_at: domain.created_at ? new Date(domain.created_at) : undefined,
|
||||
meet_place: domain.meet_place ?? null,
|
||||
meet_at: domain.meet_at ?? null,
|
||||
branch_name: domain.branch_name ?? null,
|
||||
branch_email: domain.branch_email ?? null,
|
||||
branch_phone: domain.branch_phone ?? null,
|
||||
cancelled: domain.cancelled ?? false,
|
||||
meet_reminder_sent: domain.meet_reminder_sent ?? false,
|
||||
};
|
||||
}
|
||||
|
||||
static toUpdateEntity(domain: Partial<KuDecisionDomainEntity>): Partial<KuDecisionTypeormEntity> {
|
||||
const updateData: Partial<KuDecisionTypeormEntity> = {};
|
||||
|
||||
// запись могла быть создана placeholder'ом (upsertPrivateData) до прихода синка —
|
||||
// bc-поля первой дельты обязаны материализоваться при обновлении
|
||||
if (domain.id !== undefined) updateData.id = domain.id as number;
|
||||
if (domain.type !== undefined) updateData.type = domain.type as string;
|
||||
if (domain.initiator !== undefined) updateData.initiator = domain.initiator as string;
|
||||
if (domain.proposal !== undefined) updateData.proposal = domain.proposal as object;
|
||||
if (domain.braname !== undefined) updateData.braname = domain.braname as string;
|
||||
if (domain.created_at !== undefined && domain.created_at) updateData.created_at = new Date(domain.created_at);
|
||||
if (domain.block_num !== undefined) updateData.block_num = domain.block_num;
|
||||
if (domain.present !== undefined) updateData.present = domain.present;
|
||||
if (domain.status !== undefined) updateData.status = domain.status;
|
||||
if (domain.chairman !== undefined) updateData.chairman = domain.chairman;
|
||||
if (domain.protocol !== undefined) updateData.protocol = domain.protocol as object;
|
||||
if (domain.petition !== undefined) updateData.petition = domain.petition as object;
|
||||
if (domain.liability !== undefined) updateData.liability = domain.liability as object;
|
||||
if (domain.authority !== undefined) updateData.authority = domain.authority as object;
|
||||
if (domain.authorization !== undefined) updateData.authorization = domain.authorization as object;
|
||||
if (domain.open_at !== undefined) updateData.open_at = new Date(domain.open_at);
|
||||
if (domain.close_at !== undefined) updateData.close_at = new Date(domain.close_at);
|
||||
if (domain.signed_ballots !== undefined) updateData.signed_ballots = domain.signed_ballots;
|
||||
if (domain.address !== undefined) updateData.address = domain.address;
|
||||
if (domain.participants !== undefined) updateData.participants = domain.participants;
|
||||
if (domain.meet_place !== undefined) updateData.meet_place = domain.meet_place;
|
||||
if (domain.meet_at !== undefined) updateData.meet_at = domain.meet_at;
|
||||
if (domain.branch_name !== undefined) updateData.branch_name = domain.branch_name;
|
||||
if (domain.branch_email !== undefined) updateData.branch_email = domain.branch_email;
|
||||
if (domain.branch_phone !== undefined) updateData.branch_phone = domain.branch_phone;
|
||||
if (domain.cancelled !== undefined) updateData.cancelled = domain.cancelled;
|
||||
if (domain.meet_reminder_sent !== undefined) updateData.meet_reminder_sent = domain.meet_reminder_sent;
|
||||
|
||||
return updateData;
|
||||
}
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
import { KuTrustRequestDomainEntity } from '../../domain/entities/ku-trust-request.entity';
|
||||
import type { KuTrustRequestTypeormEntity } from '../entities/ku-trust-request.typeorm-entity';
|
||||
import type {
|
||||
IKuTrustRequestBlockchainData,
|
||||
IKuTrustRequestDatabaseData,
|
||||
} from '../../domain/interfaces/ku-blockchain-data.interface';
|
||||
|
||||
/**
|
||||
* Маппер между доменной сущностью заявки доверенного и TypeORM-сущностью
|
||||
*/
|
||||
export class KuTrustRequestMapper {
|
||||
static toDomain(entity: KuTrustRequestTypeormEntity): KuTrustRequestDomainEntity {
|
||||
const databaseData: IKuTrustRequestDatabaseData = {
|
||||
_id: entity._id,
|
||||
block_num: entity.block_num,
|
||||
present: entity.present,
|
||||
status: entity.status,
|
||||
_created_at: entity._created_at,
|
||||
_updated_at: entity._updated_at,
|
||||
};
|
||||
|
||||
let blockchainData: IKuTrustRequestBlockchainData | undefined;
|
||||
|
||||
if (entity.hash) {
|
||||
blockchainData = {
|
||||
id: entity.id,
|
||||
hash: entity.hash,
|
||||
coopname: entity.coopname,
|
||||
braname: entity.braname,
|
||||
username: entity.username,
|
||||
application: entity.application as IKuTrustRequestBlockchainData['application'],
|
||||
authority: entity.authority as IKuTrustRequestBlockchainData['authority'],
|
||||
};
|
||||
}
|
||||
|
||||
return new KuTrustRequestDomainEntity(databaseData, blockchainData);
|
||||
}
|
||||
|
||||
static toEntity(domain: KuTrustRequestDomainEntity): Partial<KuTrustRequestTypeormEntity> {
|
||||
return {
|
||||
_id: domain._id,
|
||||
block_num: domain.block_num ?? 0,
|
||||
present: domain.present,
|
||||
status: domain.status as string,
|
||||
_created_at: domain._created_at,
|
||||
_updated_at: domain._updated_at,
|
||||
id: domain.id as number,
|
||||
hash: domain.hash as string,
|
||||
coopname: domain.coopname as string,
|
||||
braname: domain.braname as string,
|
||||
username: domain.username as string,
|
||||
application: domain.application as object,
|
||||
authority: domain.authority as object,
|
||||
};
|
||||
}
|
||||
|
||||
static toUpdateEntity(domain: Partial<KuTrustRequestDomainEntity>): Partial<KuTrustRequestTypeormEntity> {
|
||||
const updateData: Partial<KuTrustRequestTypeormEntity> = {};
|
||||
|
||||
if (domain.block_num !== undefined) updateData.block_num = domain.block_num;
|
||||
if (domain.present !== undefined) updateData.present = domain.present;
|
||||
if (domain.application !== undefined) updateData.application = domain.application as object;
|
||||
if (domain.authority !== undefined) updateData.authority = domain.authority as object;
|
||||
|
||||
return updateData;
|
||||
}
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import type { IBlockchainSyncRepository } from '~/shared/interfaces/blockchain-sync.interface';
|
||||
import { BaseBlockchainRepository } from '~/shared/sync/repositories/base-blockchain.repository';
|
||||
import { EntityVersioningService } from '~/shared/sync/services/entity-versioning.service';
|
||||
import type { KuDecisionQuestionRepository } from '../../domain/repositories/ku-decision-question.repository';
|
||||
import { KuDecisionQuestionDomainEntity } from '../../domain/entities/ku-decision-question.entity';
|
||||
import { KuDecisionQuestionTypeormEntity } from '../entities/ku-decision-question.typeorm-entity';
|
||||
import { KuDecisionQuestionMapper } from '../mappers/ku-decision-question.mapper';
|
||||
import type {
|
||||
IKuDecisionQuestionBlockchainData,
|
||||
IKuDecisionQuestionDatabaseData,
|
||||
} from '../../domain/interfaces/ku-blockchain-data.interface';
|
||||
|
||||
@Injectable()
|
||||
export class KuDecisionQuestionTypeormRepository
|
||||
extends BaseBlockchainRepository<KuDecisionQuestionDomainEntity, KuDecisionQuestionTypeormEntity>
|
||||
implements KuDecisionQuestionRepository, IBlockchainSyncRepository<KuDecisionQuestionDomainEntity>
|
||||
{
|
||||
constructor(
|
||||
@InjectRepository(KuDecisionQuestionTypeormEntity)
|
||||
repository: Repository<KuDecisionQuestionTypeormEntity>,
|
||||
entityVersioningService: EntityVersioningService
|
||||
) {
|
||||
super(repository, entityVersioningService);
|
||||
}
|
||||
|
||||
protected getMapper() {
|
||||
return {
|
||||
toDomain: KuDecisionQuestionMapper.toDomain,
|
||||
toEntity: KuDecisionQuestionMapper.toEntity,
|
||||
};
|
||||
}
|
||||
|
||||
protected createDomainEntity(
|
||||
databaseData: IKuDecisionQuestionDatabaseData,
|
||||
blockchainData: IKuDecisionQuestionBlockchainData
|
||||
): KuDecisionQuestionDomainEntity {
|
||||
return new KuDecisionQuestionDomainEntity(databaseData, blockchainData);
|
||||
}
|
||||
|
||||
protected getSyncKey(): string {
|
||||
return KuDecisionQuestionDomainEntity.getSyncKey();
|
||||
}
|
||||
|
||||
async findByDecisionId(coopname: string, decisionId: number): Promise<KuDecisionQuestionDomainEntity[]> {
|
||||
const entities = await this.repository.find({
|
||||
where: { coopname, decision_id: decisionId },
|
||||
order: { number: 'ASC' },
|
||||
});
|
||||
return entities.map((entity) => KuDecisionQuestionMapper.toDomain(entity));
|
||||
}
|
||||
|
||||
async update(entity: KuDecisionQuestionDomainEntity): Promise<KuDecisionQuestionDomainEntity> {
|
||||
const updateData = KuDecisionQuestionMapper.toUpdateEntity(entity);
|
||||
await this.repository.update(entity._id, updateData);
|
||||
|
||||
const updatedEntity = await this.repository.findOne({ where: { _id: entity._id } });
|
||||
if (!updatedEntity) {
|
||||
throw new Error(`Вопрос повестки ${entity.id} не найден после обновления`);
|
||||
}
|
||||
|
||||
return KuDecisionQuestionMapper.toDomain(updatedEntity);
|
||||
}
|
||||
}
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import type { IBlockchainSyncRepository } from '~/shared/interfaces/blockchain-sync.interface';
|
||||
import { BaseBlockchainRepository } from '~/shared/sync/repositories/base-blockchain.repository';
|
||||
import { EntityVersioningService } from '~/shared/sync/services/entity-versioning.service';
|
||||
import type {
|
||||
PaginationInputDomainInterface,
|
||||
PaginationResultDomainInterface,
|
||||
} from '~/domain/common/interfaces/pagination.interface';
|
||||
import { PaginationUtils } from '~/shared/utils/pagination.utils';
|
||||
import type {
|
||||
KuDecisionFilterDomainInterface,
|
||||
KuDecisionPrivateDataDomainInterface,
|
||||
KuDecisionRepository,
|
||||
} from '../../domain/repositories/ku-decision.repository';
|
||||
import { KuDecisionDomainEntity } from '../../domain/entities/ku-decision.entity';
|
||||
import { KuDecisionTypeormEntity } from '../entities/ku-decision.typeorm-entity';
|
||||
import { KuDecisionMapper } from '../mappers/ku-decision.mapper';
|
||||
import type {
|
||||
IKuDecisionBlockchainData,
|
||||
IKuDecisionDatabaseData,
|
||||
} from '../../domain/interfaces/ku-blockchain-data.interface';
|
||||
|
||||
@Injectable()
|
||||
export class KuDecisionTypeormRepository
|
||||
extends BaseBlockchainRepository<KuDecisionDomainEntity, KuDecisionTypeormEntity>
|
||||
implements KuDecisionRepository, IBlockchainSyncRepository<KuDecisionDomainEntity>
|
||||
{
|
||||
constructor(
|
||||
@InjectRepository(KuDecisionTypeormEntity)
|
||||
repository: Repository<KuDecisionTypeormEntity>,
|
||||
entityVersioningService: EntityVersioningService
|
||||
) {
|
||||
super(repository, entityVersioningService);
|
||||
}
|
||||
|
||||
protected getMapper() {
|
||||
return {
|
||||
toDomain: KuDecisionMapper.toDomain,
|
||||
toEntity: KuDecisionMapper.toEntity,
|
||||
};
|
||||
}
|
||||
|
||||
protected createDomainEntity(
|
||||
databaseData: IKuDecisionDatabaseData,
|
||||
blockchainData: IKuDecisionBlockchainData
|
||||
): KuDecisionDomainEntity {
|
||||
return new KuDecisionDomainEntity(databaseData, blockchainData);
|
||||
}
|
||||
|
||||
protected getSyncKey(): string {
|
||||
return KuDecisionDomainEntity.getSyncKey();
|
||||
}
|
||||
|
||||
async findByHash(hash: string): Promise<KuDecisionDomainEntity | null> {
|
||||
const entity = await this.repository.findOne({ where: { hash: hash.toLowerCase() } });
|
||||
return entity ? KuDecisionMapper.toDomain(entity) : null;
|
||||
}
|
||||
|
||||
async upsertPrivateData(data: KuDecisionPrivateDataDomainInterface): Promise<void> {
|
||||
const hash = data.hash.toLowerCase();
|
||||
const existing = await this.repository.findOne({ where: { hash } });
|
||||
|
||||
const privateFields: Partial<KuDecisionTypeormEntity> = {};
|
||||
if (data.meet_place !== undefined) privateFields.meet_place = data.meet_place;
|
||||
if (data.meet_at !== undefined) privateFields.meet_at = data.meet_at;
|
||||
if (data.branch_name !== undefined) privateFields.branch_name = data.branch_name;
|
||||
if (data.branch_email !== undefined) privateFields.branch_email = data.branch_email;
|
||||
if (data.branch_phone !== undefined) privateFields.branch_phone = data.branch_phone;
|
||||
if (data.cancelled !== undefined) privateFields.cancelled = data.cancelled;
|
||||
|
||||
if (existing) {
|
||||
await this.repository.update(existing._id, privateFields);
|
||||
return;
|
||||
}
|
||||
|
||||
// Запись из синка ещё не пришла — создаём placeholder, который синк дополнит
|
||||
await this.repository.save(
|
||||
this.repository.create({
|
||||
hash,
|
||||
coopname: data.coopname ?? '',
|
||||
type: data.type ?? '',
|
||||
initiator: data.initiator ?? '',
|
||||
present: false,
|
||||
...privateFields,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async findMeetingsForReminder(from: Date, to: Date): Promise<KuDecisionDomainEntity[]> {
|
||||
const entities = await this.repository
|
||||
.createQueryBuilder('decision')
|
||||
.where('decision.present = true')
|
||||
.andWhere('decision.cancelled = false')
|
||||
.andWhere('decision.meet_reminder_sent = false')
|
||||
.andWhere('decision.meet_at >= :from AND decision.meet_at < :to', { from, to })
|
||||
.getMany();
|
||||
return entities.map((entity) => KuDecisionMapper.toDomain(entity));
|
||||
}
|
||||
|
||||
async markReminderSent(hash: string): Promise<void> {
|
||||
await this.repository.update({ hash: hash.toLowerCase() }, { meet_reminder_sent: true });
|
||||
}
|
||||
|
||||
async update(entity: KuDecisionDomainEntity): Promise<KuDecisionDomainEntity> {
|
||||
const updateData = KuDecisionMapper.toUpdateEntity(entity);
|
||||
await this.repository.update(entity._id, updateData);
|
||||
|
||||
const updatedEntity = await this.repository.findOne({ where: { _id: entity._id } });
|
||||
if (!updatedEntity) {
|
||||
throw new Error(`Решение собрания участка ${entity.hash} не найдено после обновления`);
|
||||
}
|
||||
|
||||
return KuDecisionMapper.toDomain(updatedEntity);
|
||||
}
|
||||
|
||||
async findAllPaginated(
|
||||
filter?: KuDecisionFilterDomainInterface,
|
||||
options?: PaginationInputDomainInterface
|
||||
): Promise<PaginationResultDomainInterface<KuDecisionDomainEntity>> {
|
||||
const validatedOptions: PaginationInputDomainInterface = options
|
||||
? PaginationUtils.validatePaginationOptions(options)
|
||||
: { page: 1, limit: 10, sortBy: undefined, sortOrder: 'ASC' as const };
|
||||
|
||||
const { limit, offset } = PaginationUtils.getSqlPaginationParams(validatedOptions);
|
||||
|
||||
const where: any = {};
|
||||
if (filter?.coopname) where.coopname = filter.coopname;
|
||||
if (filter?.type) where.type = filter.type;
|
||||
if (filter?.status) where.status = filter.status;
|
||||
if (filter?.braname) where.braname = filter.braname;
|
||||
if (filter?.initiator) where.initiator = filter.initiator;
|
||||
if (filter?.present !== undefined) where.present = filter.present;
|
||||
|
||||
const totalCount = await this.repository.count({ where });
|
||||
|
||||
const orderBy: any = {};
|
||||
if (validatedOptions.sortBy) {
|
||||
orderBy[validatedOptions.sortBy] = validatedOptions.sortOrder;
|
||||
} else {
|
||||
orderBy._created_at = 'DESC';
|
||||
}
|
||||
|
||||
const entities = await this.repository.find({
|
||||
where,
|
||||
skip: offset,
|
||||
take: limit,
|
||||
order: orderBy,
|
||||
});
|
||||
|
||||
const items = entities.map((entity) => KuDecisionMapper.toDomain(entity));
|
||||
|
||||
return PaginationUtils.createPaginationResult(items, totalCount, validatedOptions);
|
||||
}
|
||||
}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import type { IBlockchainSyncRepository } from '~/shared/interfaces/blockchain-sync.interface';
|
||||
import { BaseBlockchainRepository } from '~/shared/sync/repositories/base-blockchain.repository';
|
||||
import { EntityVersioningService } from '~/shared/sync/services/entity-versioning.service';
|
||||
import type {
|
||||
PaginationInputDomainInterface,
|
||||
PaginationResultDomainInterface,
|
||||
} from '~/domain/common/interfaces/pagination.interface';
|
||||
import { PaginationUtils } from '~/shared/utils/pagination.utils';
|
||||
import type {
|
||||
KuTrustRequestFilterDomainInterface,
|
||||
KuTrustRequestRepository,
|
||||
} from '../../domain/repositories/ku-trust-request.repository';
|
||||
import { KuTrustRequestDomainEntity } from '../../domain/entities/ku-trust-request.entity';
|
||||
import { KuTrustRequestTypeormEntity } from '../entities/ku-trust-request.typeorm-entity';
|
||||
import { KuTrustRequestMapper } from '../mappers/ku-trust-request.mapper';
|
||||
import type {
|
||||
IKuTrustRequestBlockchainData,
|
||||
IKuTrustRequestDatabaseData,
|
||||
} from '../../domain/interfaces/ku-blockchain-data.interface';
|
||||
|
||||
@Injectable()
|
||||
export class KuTrustRequestTypeormRepository
|
||||
extends BaseBlockchainRepository<KuTrustRequestDomainEntity, KuTrustRequestTypeormEntity>
|
||||
implements KuTrustRequestRepository, IBlockchainSyncRepository<KuTrustRequestDomainEntity>
|
||||
{
|
||||
constructor(
|
||||
@InjectRepository(KuTrustRequestTypeormEntity)
|
||||
repository: Repository<KuTrustRequestTypeormEntity>,
|
||||
entityVersioningService: EntityVersioningService
|
||||
) {
|
||||
super(repository, entityVersioningService);
|
||||
}
|
||||
|
||||
protected getMapper() {
|
||||
return {
|
||||
toDomain: KuTrustRequestMapper.toDomain,
|
||||
toEntity: KuTrustRequestMapper.toEntity,
|
||||
};
|
||||
}
|
||||
|
||||
protected createDomainEntity(
|
||||
databaseData: IKuTrustRequestDatabaseData,
|
||||
blockchainData: IKuTrustRequestBlockchainData
|
||||
): KuTrustRequestDomainEntity {
|
||||
return new KuTrustRequestDomainEntity(databaseData, blockchainData);
|
||||
}
|
||||
|
||||
protected getSyncKey(): string {
|
||||
return KuTrustRequestDomainEntity.getSyncKey();
|
||||
}
|
||||
|
||||
async findByHash(hash: string): Promise<KuTrustRequestDomainEntity | null> {
|
||||
const entity = await this.repository.findOne({ where: { hash: hash.toLowerCase() } });
|
||||
return entity ? KuTrustRequestMapper.toDomain(entity) : null;
|
||||
}
|
||||
|
||||
async update(entity: KuTrustRequestDomainEntity): Promise<KuTrustRequestDomainEntity> {
|
||||
const updateData = KuTrustRequestMapper.toUpdateEntity(entity);
|
||||
await this.repository.update(entity._id, updateData);
|
||||
|
||||
const updatedEntity = await this.repository.findOne({ where: { _id: entity._id } });
|
||||
if (!updatedEntity) {
|
||||
throw new Error(`Заявка доверенного ${entity.hash} не найдена после обновления`);
|
||||
}
|
||||
|
||||
return KuTrustRequestMapper.toDomain(updatedEntity);
|
||||
}
|
||||
|
||||
async findAllPaginated(
|
||||
filter?: KuTrustRequestFilterDomainInterface,
|
||||
options?: PaginationInputDomainInterface
|
||||
): Promise<PaginationResultDomainInterface<KuTrustRequestDomainEntity>> {
|
||||
const validatedOptions: PaginationInputDomainInterface = options
|
||||
? PaginationUtils.validatePaginationOptions(options)
|
||||
: { page: 1, limit: 10, sortBy: undefined, sortOrder: 'ASC' as const };
|
||||
|
||||
const { limit, offset } = PaginationUtils.getSqlPaginationParams(validatedOptions);
|
||||
|
||||
const where: any = {};
|
||||
if (filter?.coopname) where.coopname = filter.coopname;
|
||||
if (filter?.braname) where.braname = filter.braname;
|
||||
if (filter?.username) where.username = filter.username;
|
||||
if (filter?.present !== undefined) where.present = filter.present;
|
||||
|
||||
const totalCount = await this.repository.count({ where });
|
||||
|
||||
const orderBy: any = {};
|
||||
if (validatedOptions.sortBy) {
|
||||
orderBy[validatedOptions.sortBy] = validatedOptions.sortOrder;
|
||||
} else {
|
||||
orderBy._created_at = 'DESC';
|
||||
}
|
||||
|
||||
const entities = await this.repository.find({
|
||||
where,
|
||||
skip: offset,
|
||||
take: limit,
|
||||
order: orderBy,
|
||||
});
|
||||
|
||||
const items = entities.map((entity) => KuTrustRequestMapper.toDomain(entity));
|
||||
|
||||
return PaginationUtils.createPaginationResult(items, totalCount, validatedOptions);
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
/**
|
||||
* Сервис информации о поддерживаемых контрактах и таблицах для модуля КУ.
|
||||
* Источник данных — контракт branch (собрания/решения участков и заявки доверенных).
|
||||
*/
|
||||
@Injectable()
|
||||
export class KuContractInfoService {
|
||||
private readonly supportedContractNames: string[] = ['branch'];
|
||||
|
||||
/**
|
||||
* Паттерны таблиц для каждой сущности (базовое имя + маска)
|
||||
*/
|
||||
private readonly tablePatterns: Record<string, string[]> = {
|
||||
decisions: ['decisions', 'decisions*'],
|
||||
decisionq: ['decisionq', 'decisionq*'],
|
||||
trustreqs: ['trustreqs', 'trustreqs*'],
|
||||
};
|
||||
|
||||
getSupportedContractNames(): string[] {
|
||||
return [...this.supportedContractNames];
|
||||
}
|
||||
|
||||
getTablePatterns(entityName: string): string[] {
|
||||
const patterns = this.tablePatterns[entityName];
|
||||
if (!patterns) {
|
||||
throw new Error(
|
||||
`Unknown entity name: ${entityName}. Supported entities: ${Object.keys(this.tablePatterns).join(', ')}`
|
||||
);
|
||||
}
|
||||
return [...patterns];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import { Inject, Module } from '@nestjs/common';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
EXTENSION_REPOSITORY,
|
||||
type ExtensionDomainRepository,
|
||||
} from '~/domain/extension/repositories/extension-domain.repository';
|
||||
import { WinstonLoggerService } from '~/application/logger/logger-app.service';
|
||||
import type { ExtensionDomainEntity } from '~/domain/extension/entities/extension-domain.entity';
|
||||
import { BaseExtModule } from '../base.extension.module';
|
||||
import { DocumentDomainModule } from '~/domain/document/document.module';
|
||||
import { VaultDomainModule } from '~/domain/vault/vault-domain.module';
|
||||
import { DomainToBlockchainUtils } from '~/shared/utils/domain-to-blockchain.utils';
|
||||
|
||||
// База данных
|
||||
import { KuDatabaseModule } from './infrastructure/database/ku-database.module';
|
||||
|
||||
// Репозитории
|
||||
import { KU_DECISION_REPOSITORY } from './domain/repositories/ku-decision.repository';
|
||||
import { KU_DECISION_QUESTION_REPOSITORY } from './domain/repositories/ku-decision-question.repository';
|
||||
import { KU_TRUST_REQUEST_REPOSITORY } from './domain/repositories/ku-trust-request.repository';
|
||||
import { KuDecisionTypeormRepository } from './infrastructure/repositories/ku-decision.typeorm-repository';
|
||||
import { KuDecisionQuestionTypeormRepository } from './infrastructure/repositories/ku-decision-question.typeorm-repository';
|
||||
import { KuTrustRequestTypeormRepository } from './infrastructure/repositories/ku-trust-request.typeorm-repository';
|
||||
|
||||
// Blockchain
|
||||
import { KU_BLOCKCHAIN_PORT } from './domain/interfaces/ku-blockchain.port';
|
||||
import { KuBlockchainAdapter } from './infrastructure/blockchain/adapters/ku-blockchain.adapter';
|
||||
import { KuContractInfoService } from './infrastructure/services/ku-contract-info.service';
|
||||
import { KuDecisionDeltaMapper } from './infrastructure/blockchain/mappers/ku-decision-delta.mapper';
|
||||
import { KuDecisionQuestionDeltaMapper } from './infrastructure/blockchain/mappers/ku-decision-question-delta.mapper';
|
||||
import { KuTrustRequestDeltaMapper } from './infrastructure/blockchain/mappers/ku-trust-request-delta.mapper';
|
||||
|
||||
// Синхронизация
|
||||
import { KuDecisionSyncService } from './application/syncers/ku-decision-sync.service';
|
||||
import { KuDecisionQuestionSyncService } from './application/syncers/ku-decision-question-sync.service';
|
||||
import { KuTrustRequestSyncService } from './application/syncers/ku-trust-request-sync.service';
|
||||
|
||||
// Application
|
||||
import { AccountInfrastructureModule } from '~/infrastructure/account/account-infrastructure.module';
|
||||
import { KuService } from './application/services/ku.service';
|
||||
import { KuEventsService } from './application/services/ku-events.service';
|
||||
import { KuResolver } from './application/resolvers/ku.resolver';
|
||||
|
||||
// Дефолтные параметры конфигурации
|
||||
export const defaultConfig = {};
|
||||
|
||||
export const Schema = z.object({});
|
||||
|
||||
// Интерфейс для параметров конфигурации плагина
|
||||
export type IConfig = z.infer<typeof Schema>;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface ILog {}
|
||||
|
||||
export class KuPlugin extends BaseExtModule {
|
||||
constructor(
|
||||
@Inject(EXTENSION_REPOSITORY) private readonly extensionRepository: ExtensionDomainRepository,
|
||||
private readonly logger: WinstonLoggerService
|
||||
) {
|
||||
super();
|
||||
this.logger.setContext(KuPlugin.name);
|
||||
}
|
||||
|
||||
name = 'trustee';
|
||||
plugin!: ExtensionDomainEntity<IConfig>;
|
||||
|
||||
public configSchemas = Schema;
|
||||
public defaultConfig = defaultConfig;
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
this.logger.info(`Инициализация расширения «Кооперативный участок» (${this.name})`);
|
||||
}
|
||||
}
|
||||
|
||||
@Module({
|
||||
imports: [KuDatabaseModule, DocumentDomainModule, VaultDomainModule, AccountInfrastructureModule],
|
||||
providers: [
|
||||
// Plugin
|
||||
KuPlugin,
|
||||
|
||||
// Репозитории
|
||||
{
|
||||
provide: KU_DECISION_REPOSITORY,
|
||||
useClass: KuDecisionTypeormRepository,
|
||||
},
|
||||
{
|
||||
provide: KU_DECISION_QUESTION_REPOSITORY,
|
||||
useClass: KuDecisionQuestionTypeormRepository,
|
||||
},
|
||||
{
|
||||
provide: KU_TRUST_REQUEST_REPOSITORY,
|
||||
useClass: KuTrustRequestTypeormRepository,
|
||||
},
|
||||
KuDecisionTypeormRepository,
|
||||
KuDecisionQuestionTypeormRepository,
|
||||
KuTrustRequestTypeormRepository,
|
||||
|
||||
// Blockchain
|
||||
{
|
||||
provide: KU_BLOCKCHAIN_PORT,
|
||||
useClass: KuBlockchainAdapter,
|
||||
},
|
||||
KuBlockchainAdapter,
|
||||
KuContractInfoService,
|
||||
KuDecisionDeltaMapper,
|
||||
KuDecisionQuestionDeltaMapper,
|
||||
KuTrustRequestDeltaMapper,
|
||||
|
||||
// Синхронизация
|
||||
KuDecisionSyncService,
|
||||
KuDecisionQuestionSyncService,
|
||||
KuTrustRequestSyncService,
|
||||
|
||||
// Utils
|
||||
DomainToBlockchainUtils,
|
||||
|
||||
// Application
|
||||
KuService,
|
||||
KuEventsService,
|
||||
KuResolver,
|
||||
],
|
||||
exports: [KuPlugin, KuDecisionSyncService, KuDecisionQuestionSyncService, KuTrustRequestSyncService],
|
||||
})
|
||||
export class KuPluginModule {
|
||||
constructor(private readonly kuPlugin: KuPlugin) {}
|
||||
|
||||
async initialize(config?: IConfig) {
|
||||
await this.kuPlugin.initialize();
|
||||
void config;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user