Compare commits

...

44 Commits

Author SHA1 Message Date
Alex Ant 11a891392e update workflow #2 2025-06-14 17:56:14 +05:00
Alex Ant 7930ef8ef6 workflow fix 2025-06-14 16:09:22 +05:00
Alex Ant 8484a14905 откатываем функционал 2025-06-14 13:40:01 +05:00
Alex Ant e7a0968b94 fix hash router 2025-06-14 11:07:35 +05:00
Alex Ant 87241b2de0 rollback 2025-06-14 09:07:57 +05:00
Alex Ant 4a5fb2af8b chore(release): publish 2025-06-13 20:45:38 +05:00
Alex Ant b9acc67818 update 2025-06-13 20:18:34 +05:00
Alex Ant 5be9fa6ae8 update 2025-06-13 20:15:39 +05:00
Alex Ant e0f8d6a64c back to hash routing 2025-06-13 19:48:36 +05:00
Alex Ant 2276ab396e update 2025-06-13 19:20:02 +05:00
Alex Ant 9de84afc29 looking for bug 2025-06-13 17:08:27 +05:00
Alex Ant 747e567666 enable test mode 2025-06-13 16:15:50 +05:00
Alex Ant 675376079e Обновлено использование индекса cooperatives в shared_registrator.hpp, добавлено новое действие delmeet в meet для удаления собраний. Исправлены проверки на наличие активных пайщиков и добавлено форматирование ссылок в компонентах голосования и повестки. Обновлены формы создания и перезапуска собраний для поддержки новых требований. 2025-06-13 13:26:32 +05:00
Alex Ant 1fa0cee816 Добавлено использование функции parseLinks для форматирования ссылок в контексте и решениях в компонентах повестки и голосования. Упрощен код за счет удаления дублирующей функции parseLinks из компонентов. 2025-06-12 20:26:43 +05:00
Alex Ant 6a7f71eb5a Добавлено форматирование ссылок в повестке собрания. Реализована функция для преобразования текста с ссылками в HTML-формат. Исправлено отображение контекста повестки. 2025-06-12 20:22:33 +05:00
Alex Ant 6bda6948bf Добавлено новое поле 'level' в интерфейсы и DTO для обработки собраний, обновлены адаптеры для поддержки этого поля. Пофиксена проблема с Float на проценте кворума. 2025-06-12 20:14:55 +05:00
Alex Ant abb43b4221 fix count 2025-06-12 19:17:45 +05:00
Alex Ant 4193c45205 подсчитываем количество активных пайщиков в кооперативах 2025-06-12 15:47:27 +05:00
Alex Ant 36eb50fef9 meet fix 2025-06-12 15:45:12 +05:00
Alex Ant ad0ebf82ee добавлено действие migrate в meet и созданы миграции на добавление новых шаблонов 2025-06-12 14:56:42 +05:00
Alex Ant 54d55e288f release 2025-06-12 11:13:51 +05:00
Alex Ant 9fb51b00bd Добавлены новые зависимости moment-timezone и @novu/js, обновлены версии существующих пакетов. Внесены изменения в модули и сервисы для поддержки новых функциональностей, включая адаптеры для расширений и обработку уведомлений. Реализована логика работы с временными зонами и обновлены интерфейсы для работы с аккаунтами и собраниями. 2025-06-11 21:39:13 +05:00
Alex Ant d56dcc153a Добавлены новые поля для ответов голосования в схемы и DTO, а также обновлены интерфейсы и типы для поддержки голосования на собраниях. Реализована логика обработки ответов и обновлены компоненты для работы с новыми данными. Добавлена поддержка часового пояса в форматирование дат. 2025-06-10 14:14:44 +05:00
Alex Ant d9f421acbd Обновлены компоненты рабочего стола: добавлены маршруты по умолчанию для рабочих столов "participant" и "soviet", реализована логика выбора рабочего стола на основе прав пользователя и сохраненного выбора. Также добавлены новые методы для навигации и сохранения состояния рабочего стола. Исправлены ошибки в компонентах и улучшена верстка. 2025-06-09 17:54:33 +05:00
Alex Ant 01ec687ba0 almost works 2025-06-09 15:27:03 +05:00
Alex Ant 0ee1efac4e add exampleData 2025-06-03 13:12:35 +05:00
Alex Ant fbf9d514a0 update 2025-06-03 10:54:14 +05:00
Alex Ant be7a9e9e3a update 2025-06-02 23:15:51 +05:00
Alex Ant bc3b3f701f update cooptypes and factory 2025-06-02 23:00:32 +05:00
Alex Ant 0d8be7a489 Добавлена поддержка часового пояса в компоненты создания и перезапуска собраний. Обновлены метки для отображения времени с учетом часового пояса. Добавлены зависимости moment-timezone и обновлены конфигурации окружения. 2025-05-30 22:54:22 +05:00
Alex Ant 2fdeaed3a0 Поправлена верстка. Обновлены компоненты для отображения новых данных о вопросах и результатах голосования. Изменен SDK для корректной мульти-подписи документа. 2025-05-30 21:42:07 +05:00
Alex Ant 13481d8f95 Добавлены поля для председателя и секретаря в интерфейсы и DTO собраний, а также обновлены соответствующие селекторы и агрегаты документов. Введены сертификаты для председателя и секретаря, обновлены зависимости и исправлены ссылки на информацию о пользователях в документах. 2025-05-23 17:48:52 +05:00
Alex Ant f4b87baa2b Добавлены новые типы сертификатов пользователей (EntrepreneurCertificate, IndividualCertificate, OrganizationCertificate, RepresentedByCertificate) и обновлены соответствующие DTO и интерфейсы. Обновлены зависимости в модулях и исправлены ссылки на информацию о подписантах в документах. 2025-05-23 14:14:56 +05:00
Alex Ant a337dbceb8 expandableDocument for meet 2025-05-23 10:49:03 +05:00
Alex Ant 04592a9908 meet aggregation DTO 2025-05-22 17:51:29 +05:00
Alex Ant 9d204ba496 chore(release): publish 2025-05-14 16:16:51 +05:00
Alex Ant 64dd48b408 повторная миграция соглашений 2025-05-14 15:41:34 +05:00
Alex Ant 515a374f86 chore(release): publish 2025-05-14 12:45:52 +05:00
Alex Ant ddae8d6a53 Обновлен changelog для версии v2025.5.14. 2025-05-14 12:22:46 +05:00
Alex Ant 6e87047e1d Добавлена поддержка смены темы (светлая/темная) с сохранением в localStorage, обновлены соответствующие компоненты и стили. Исправлено отображение данных участников в таблицах. 2025-05-14 12:12:00 +05:00
Alex Ant 62e13853c5 Добавлено поле аккаунта в тип Payment и обновлены соответствующие сущности и DTO, а также интерфейс для отображения ФИО плательщика. 2025-05-13 18:15:21 +05:00
Alex Ant a09959d751 chore(release): publish 2025-05-13 14:56:31 +05:00
Alex Ant e2678df443 factory bugs fix 2025-05-13 10:19:44 +05:00
Alex Ant fedec69e44 fix bug on dinamic import 2025-05-13 09:10:04 +05:00
403 changed files with 12396 additions and 2317 deletions
+1
View File
@@ -10,6 +10,7 @@ alwaysApply: false
2. Типы IName, IChecksum256, ITimePointSec - это просто строки.
3. Домен должен быть полностью изолирован от инфраструктурных деталей.
4. Направление зависимостей - внутрь (к ядру, домену).
5. Домен, инфраструктура и приложение связаны через App.module, НЕ НУЖНО импортировать их друг в друга, а достаточно просто импортировать домен на уровне приложения чтобы использовать все экспорты домена.
## Структура проекта
- `domain/` - доменный слой (бизнес-логика, независимая от инфраструктуры)
+2
View File
@@ -10,3 +10,5 @@ alwaysApply: true
- Стиль шаблонов PUG на composite API.
- Всегда создаём index.ts файлы для vue компонент.
- Создавай индексные файлы для vue через export {default as NAME} from './where', а на уровне выше, если это необходимо, делай export * from './ui'
- Всегда удаляем неиспользуемые импорты.
- Не используем emit избыточно. Вместо них используем композабл функции моделей фич, если необходимо. Emit только в крайнем случае.
+96
View File
@@ -0,0 +1,96 @@
---
description:
globs:
alwaysApply: false
---
# Структура файлов шаблонов документов и алгоритм правки
## Основные директории и файлы
### 1. Определение типов и интерфейсов (cooptypes)
**Путь:** `/cooptypes/src/cooperative/registry/[ID.DocumentName]/index.ts`
В этих файлах определяются:
- Интерфейсы данных (`IAgendaMeet`, etc.)
- Основная модель данных документа (`Model`)
- HTML шаблон документа (`context`)
- Переводы строк (`translations`)
- Пример данных для тестирования (`exampleData`)
**Для изменения шаблона нужно править:**
- Интерфейсы модели данных при изменении структуры
- HTML в переменной `context` для изменения внешнего вида
- Строки переводов в `translations.ru`
### 2. Схемы валидации (factory)
**Путь:** `/factory/src/Schema/[SchemaName].ts`
Содержат:
- JSON схемы для валидации данных
- Определение обязательных полей
**При изменении структуры данных:**
- Обновите соответствующую схему валидации
- Обновите список обязательных полей в `required`
### 3. Фабрики документов (factory)
**Путь:** `/factory/src/Actions/[ID.DocumentName].ts`
Обрабатывают:
- Получение данных из разных источников
- Сборку модели для шаблонизатора
- Валидацию данных по схеме
- Генерацию PDF
**При изменении логики сборки документа:**
- Обновите метод `generateDocument()`
- Добавьте новые источники данных
### 4. Шаблоны (factory)
**Путь:** `/factory/src/Templates/[ID.DocumentName].ts`
Связывают:
- Шаблон из cooptypes
- Схему валидации
- Метаданные документа
**После изменения в cooptypes:**
- Убедитесь, что схема в Templates соответствует новой модели
### 5. Тесты (factory)
**Путь:** `/factory/test/[category].test.ts`
Содержат:
- Тестовые данные для генерации документов
- Вызовы функции `testDocumentGeneration`
**После внесения изменений:**
- Обновите тестовые данные в соответствии с новой структурой
- Запустите тесты командой `pnpm test`
## Алгоритм внесения изменений
1. **Модификация типов**
- Изменить файл в `/cooptypes/src/cooperative/registry/[ID.DocumentName]/index.ts`
- Обновить интерфейсы, шаблон HTML и переводы
2. **Обновление схемы валидации**
- Изменить соответствующую схему в `/factory/src/Schema/`
- Убедиться, что обязательные поля совпадают с интерфейсом
3. **Компиляция библиотеки типов**
- Выполнить `cd /cooptypes && pnpm build`
4. **Обновление тестовых данных**
- Привести тестовые данные в соответствие с новой структурой
5. **Запуск тестов**
- Выполнить `cd /factory && pnpm test [filename].test.ts`
## Особенности шаблонизации
- Используется Nunjucks для шаблонов
- Поддерживаются конструкции: `{% if %}`, `{% for %}`, `{% trans %}`
- Переменные вставляются через `{{ variable }}`
- Переводы через `{% trans 'KEY' %}`
+261
View File
@@ -0,0 +1,261 @@
---
description:
globs: src/**/*.ts
alwaysApply: false
---
# Фабрика Документов Кооперативов
## Общая Архитектура
Фабрика документов — это система генерации PDF документов для кооперативов, построенная на TypeScript с использованием MongoDB для хранения данных. Система состоит из трех основных частей:
1. **registry/** — JSON-шаблоны документов (статические данные)
2. **factory/** — основная фабрика с логикой генерации
3. **cooptypes/** — типы данных и интерфейсы
## Структура Registry
В корневой папке `registry/` находятся JSON файлы с номерными названиями, представляющие шаблоны документов:
### Основные документы:
- `1.walletProgramAgreement.json` — соглашение о кошельке
- `2.regulationElectronicSignature.json` — регламент электронной подписи
- `3.privacyPolicy.json` — политика конфиденциальности
- `4.userAgreement.json` — пользовательское соглашение
- `50.CoopenomicsAgreement.json` — соглашение с партнерами
- `100.participantApplication.json` — заявление участника
- `101.selectBranchStatement.json` — заявление о выборе филиала
### Документы общих собраний (300-304):
- `300.annualGeneralMeetingAgenda.json` — предложение повестки дня
- `301.annualGeneralMeetingSovietDecision.json` — протокол заседания совета
- `302.annualGeneralMeetingNotification.json` — уведомление о собрании
- `303.annualGeneralMeetingVotingBallot.json` — бюллетень для голосования
- `304.annualGeneralMeetingDecision.json` — протокол общего собрания
### Инвестиционные документы (1000+):
- `1000.investAgreement.json` — инвестиционное соглашение
- `1001.investByResultStatement.json` — заявление о зачете по результатам
- `1002.investByResultAct.json` — акт зачета по результатам
- `1005.investByMoneyStatement.json` — заявление о зачете денежных средств
### Структура JSON-шаблона:
```json
{
"context": "<div>...HTML шаблон с переменными...</div>",
"model": {...данные для примера...},
"translation": {...переводы ключей...},
"object_model": {...схема объектной модели...}
}
```
## Фабрика (factory/)
### Основные компоненты:
#### src/index.ts — Главный класс Generator
```typescript
export class Generator implements IGenerator {
// Хранилище фабрик для каждого типа документа
factories: { [K in Numbers]: DocFactory<IGenerate> }
// MongoDB коннектор
public storage: MongoDBConnector
// Основной метод генерации
async generate(data: IGenerate, options?: IGenerationOptions): Promise<IGeneratedDocument>
}
```
#### Архитектура Factory Pattern:
- Базовый класс `DocFactory<T>` в `src/Factory/index.ts`
- Каждый документ имеет свою фабрику в `src/Actions/`
- Фабрики наследуются от `DocFactory` и реализуют метод `generateDocument()`
### Сервисы:
#### Services/Generator/ — PDF генерация
- `PDFService` — конвертирует HTML в PDF через WeasyPrint
- Использует шрифт Arial (base64)
- Добавляет метаданные в PDF
- Вычисляет SHA-256 хеш документа
#### Services/Templator/ — Шаблонизация
- Основан на Nunjucks
- Поддерживает кастомное расширение `{% trans %}` для переводов
- Рендерит HTML из шаблона с подстановкой переменных
#### Services/Validator/ — Валидация
- Использует AJV для JSON Schema валидации
- Поддерживает кастомные форматы (телефон)
- Локализация ошибок на русском языке
#### Services/Databazor/ — База данных
- `MongoDBConnector` — работа с MongoDB
- `DataService` — абстракция над данными
- Коллекции: `deltas`, `actions`, `documents`, и другие
### Модели данных (src/Models/):
#### Основные типы пользователей:
- `Individual` — физические лица (ФИО, паспорт, адрес)
- `Organization` — организации (ИНН, ОГРН, представитель)
- `Entrepreneur` — ИП (ФИО + ИНН/ОГРН)
#### Кооперативные данные:
- `Cooperative` — данные кооператива
- `PaymentMethod` — платежные методы
- `Vars` — переменные кооператива
- `Project` — проекты
### Система Action-ов:
Каждый документ имеет Action класс в `src/Actions/` с методом `generateDocument()`:
1. **Получение шаблона** — из локального Registry или MongoDB
2. **Сбор данных** — пользователь, кооператив, переменные, специфичные данные
3. **Валидация** — проверка по JSON схеме
4. **Рендеринг** — HTML из шаблона + данные
5. **PDF генерация** — HTML → PDF с метаданными
6. **Сохранение** — в MongoDB (если не skip_save)
## Система типов (cooptypes/)
### cooperative/registry/ — Типы документов
Каждый документ имеет папку с интерфейсами:
- `Action` — входные данные для генерации
- `Model` — модель данных для шаблона
- `Template` — структура шаблона
### contracts/ — Блокчейн контракты
- `registrator/` — регистрация кооперативов
- `soviet/` — управление советом
- `meet/` — общие собрания
- `wallet/`, `capital/`, `fund/` — финансовые операции
## База данных MongoDB
### Основные коллекции:
#### deltas — Состояние блокчейна
```javascript
{
block_num: number,
present: boolean,
code: string, // название контракта
scope: string, // область действия
table: string, // имя таблицы
primary_key: string,
value: {...} // данные записи
}
```
#### actions — Действия блокчейна
```javascript
{
block_num: number,
account: string,
name: string, // имя действия
receiver: string,
data: {...} // данные действия
}
```
#### documents — Сгенерированные документы
```javascript
{
hash: string, // SHA-256 хеш
binary: Uint8Array, // PDF данные
html: string, // HTML исходник
meta: { // метаданные
title: string,
created_at: string,
lang: string
}
}
```
### Таблицы данных:
- `coops` — кооперативы
- `meets` — собрания
- `questions` — вопросы собраний
- `decisions` — решения совета
- `individuals` — физические лица
- `organizations` — организации
- `entrepreneurs` — ИП
- `paymentMethods` — платежные методы
- `vars` — переменные кооперативов
## Генерация документов
### Процесс генерации:
1. **Вызов** `generator.generate(action, options)`
2. **Поиск фабрики** по `registry_id`
3. **Загрузка шаблона** (локально или из БД)
4. **Сбор данных** из MongoDB по `coopname`, `username`, `block_num`
5. **Создание модели** — объединение всех данных
6. **Валидация** модели по JSON схеме
7. **Рендеринг HTML** через Nunjucks
8. **Генерация PDF** через WeasyPrint
9. **Добавление метаданных** в PDF
10. **Вычисление хеша** SHA-256
11. **Сохранение** в MongoDB
### Пример использования:
```typescript
const generator = new Generator()
await generator.connect(mongoUri)
const document = await generator.generate({
registry_id: '300',
coopname: 'voskhod',
username: 'ant',
block_num: 0,
meet: {...},
questions: [...]
})
```
## Особенности реализации
### Шаблонизация:
- HTML шаблоны с CSS стилями
- Переменные в формате `{{variable.field}}`
- Условная логика `{% if condition %}`
- Циклы `{% for item in array %}`
- Переводы `{% trans 'KEY', var1, var2 %}`
### Подписи:
- Цифровые подписи вместо физических
- Текст "Подписано электронной подписью"
- Убраны подчеркивания для подписей
### Типы собраний:
- `regular` — очередное
- `extraordinary` — внеочередное
- Условная логика в шаблонах
### Филиалы:
- `coop.is_branched` — проверка на наличие филиалов
- "пайщиков" vs "уполномоченных" в зависимости от типа
### Форматирование дат:
- Формат: "г. Москва, 15 декабря 2024 г."
- Без кавычек вокруг дат
- Запятая после города
## Тестирование
### test/utils/index.ts — Тестовые утилиты:
- `preLoading()` — инициализация тестовых данных
- Создание кооператива, пользователей, платежных методов
- Настройка данных собраний и решений
- Очистка временных файлов
### Тестовые данные:
- Кооператив "ВОСХОД"
- Пользователи: ant, individual, entrepreneur
- Организации: voskhod, branch, exampleorg
- Собрания с вопросами и решениями
Фабрика поддерживает полный цикл создания документов кооператива от заявлений до протоколов собраний с возможностью кастомизации под разные типы кооперативов и требования.
+8 -3
View File
@@ -14,7 +14,7 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v3
with:
ref: testnet
ref: ${{ github.ref }}
fetch-depth: 0
- name: Debug info
@@ -81,13 +81,18 @@ jobs:
- name: Trigger deployment webhook
if: ${{ success() }}
run: |
if [[ $GITHUB_REF == refs/tags/* ]]; then
if [[ $GITHUB_REF == refs/tags/latest ]]; then
# Хук для тестнета
curl -X POST ${{ vars.TESTNET_WEBHOOK_URL }} \
-H 'Content-Type: application/json' \
-d '${{ env.DOCKER_TAG }}'
elif [[ $GITHUB_REF == refs/tags/* ]]; then
# Хук для продакшена
curl -X POST ${{ vars.PRODUCTION_WEBHOOK_URL }} \
-H 'Content-Type: application/json' \
-d '${{ env.DOCKER_TAG }}'
else
# Хук для тестнета
# Хук для тестнета (обычная ветка)
curl -X POST ${{ vars.TESTNET_WEBHOOK_URL }} \
-H 'Content-Type: application/json' \
-d '${{ env.DOCKER_TAG }}'
+24
View File
@@ -1,3 +1,27 @@
# v2025.5.14
В этом релизе реализован новый стандарт передачи и хранения документов по блокчейну с поддержкой неограниченного количества подписей и их валидацией. Также внесены улучшения в интерфейс и исправлены ошибки.
✨ Новые функции
- [#252](https://github.com/coopenomics/mono/issues/252): Внедрение обновленного стандарта хранения и передачи документов по блокчейну
- [#251](https://github.com/coopenomics/mono/issues/251): Реализация версионированного мигратора данных для контроллера кооператива
- [#244](https://github.com/coopenomics/mono/issues/244): Обновление стандарта сборки документов и переход на хэш-идентификаторы
🐛 Исправления ошибок
- [#249](https://github.com/coopenomics/mono/issues/249): Исправлена спутанная маршрутизация между рабочими столами кооперативов
- [#253](https://github.com/coopenomics/mono/issues/253): Исправлена избыточная точность суммы оплаты в заявлении на вступление
🔧 Улучшения
- [#259](https://github.com/coopenomics/mono/issues/259): Исправлены отступы в мобильной карточке пайщика на странице пайщиков
- [#258](https://github.com/coopenomics/mono/issues/258): Удалён hover-эффект для документов и пайщиков в таблице
- [#257](https://github.com/coopenomics/mono/issues/257): Добавлено сохранение светлой/тёмной темы в localStorage и восстановление при загрузке страницы
- [#256](https://github.com/coopenomics/mono/issues/256): Отображение статуса членства каждого пайщика в разделе "Пайщики"
- [#255](https://github.com/coopenomics/mono/issues/255): Отображение ФИО/Наименования плательщика в разделе "Платежи"
#releases
---
# v2025.5.2
В этом релизе рабочие столы переведены на серверный рендеринг, что улучшает стабильность развертывания и упрощает автоматизацию поставки ПО. Данный релиз является подготовительным к переходу на новый стандарт цифровых документов на платформе.
BIN
View File
Binary file not shown.
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@coopenomics/boot",
"type": "module",
"version": "2025.5.5-2",
"version": "2025.6.13",
"private": true,
"packageManager": "pnpm@9.0.6",
"description": "",
+8 -10
View File
@@ -607,10 +607,10 @@ export default class Blockchain {
}
async registerUser(
params: RegistratorContract.Actions.RegisterUser.IRegistrerUser,
params: RegistratorContract.Actions.RegisterUser.IRegisterUser,
) {
await this.update_pass_instance()
console.dir(params, { depth: null })
await this.api.transact(
{
actions: [
@@ -623,9 +623,7 @@ export default class Blockchain {
permission: 'active',
},
],
data: {
...params,
},
data: params,
},
],
},
@@ -640,7 +638,7 @@ export default class Blockchain {
async transfer(params: TokenContract.Actions.Transfer.ITransfer) {
await this.update_pass_instance()
console.dir(params, { depth: null })
const result = await this.api.transact(
{
actions: [
@@ -748,7 +746,7 @@ export default class Blockchain {
name: SovietContract.Actions.Decisions.VoteFor.actionName,
authorization: [
{
actor: params.member,
actor: params.username,
permission: 'active',
},
],
@@ -833,7 +831,7 @@ export default class Blockchain {
}
async ConfirmPayment(
params: GatewayContract.Actions.CompleteIncome.ICompeteIncome,
params: GatewayContract.Actions.CompleteIncome.ICompleteIncome,
) {
await this.update_pass_instance()
@@ -923,7 +921,7 @@ export default class Blockchain {
},
)
console.log('Шаблон создан: ', params)
console.log('Шаблон создан: ', params.registry_id)
}
async createTranslation(
@@ -955,7 +953,7 @@ export default class Blockchain {
},
)
console.log('Перевод создан: ', params)
console.log('Перевод создан: ', params.registry_id)
}
async createProgram(
+2 -2
View File
@@ -1,5 +1,5 @@
import { startInfra } from "./infra"
import { startCoop } from "./cooperative"
import { startInfra } from './infra'
import { startCoop } from './cooperative'
export async function boot() {
await startInfra()
+25 -17
View File
@@ -1,11 +1,7 @@
import axios from 'axios'
import { describe, expect, it } from 'vitest'
import { Registry } from '@coopenomics/factory'
import { Cooperative as TCooperative } from 'cooptypes'
import type { Account, Contract, Keys } from '../types'
import type { Cooperative } from 'cooptypes'
import type { Keys } from '../types'
import config, { GOVERN_SYMBOL, SYMBOL } from '../configs'
import Blockchain from '../blockchain'
import { sendPostToCoopbackWithSecret, sleep } from '../utils'
import { generateRandomSHA256 } from '../utils/randomHash'
const test_hash
@@ -15,11 +11,23 @@ const test_sign
const test_pkey = 'EOS5JhMfxbsNebajHcTEK8yC9uNN9Dit9hEmzE8ri8yMhhzxrLg3J'
const test_meta = JSON.stringify({})
const document = {
const document: Cooperative.Document.ISignedDocument2 = {
hash: test_hash,
signature: test_sign,
public_key: test_pkey,
signatures: [
{
id: 1,
signed_hash: test_hash,
public_key: test_pkey,
signed_at: '2025-05-14T12:22:26',
meta: test_meta,
signer: 'cooperative1',
signature: test_sign,
},
],
meta: test_meta,
version: '1.0.0',
doc_hash: test_hash,
meta_hash: test_hash,
}
export class CooperativeClass {
@@ -85,8 +93,13 @@ export class CooperativeClass {
console.log('Голосуем по решению в провайдере')
await this.blockchain.votefor({
version: '1.0.0',
signed_at: '2025-05-14T12:22:26',
signed_hash: test_hash,
signature: test_sign,
public_key: test_pkey,
coopname: config.provider,
member: config.provider_chairman,
username: config.provider_chairman,
decision_id: 1,
})
@@ -114,12 +127,7 @@ export class CooperativeClass {
administrator: config.provider,
username: username!,
agreement_type: 'wallet',
document: { // отправляем произвольный документ с валидной подписью
hash: '157192B276DA23CC84AB078FC8755C051C5F0430BF4802E55718221E6B76C777',
public_key: 'PUB_K1_5JhMfxbsNebajHcTEK8yC9uNN9Dit9hEmzE8ri8yMhhzzEtUA4',
signature: 'SIG_K1_KmKWPBC8dZGGDGhbKEoZEzPr3h5crRrR2uLdGRF5DJbeibY1MY1bZ9sPwHsgmPfiGFv9psfoCVsXFh9TekcLuvaeuxRKA8',
meta: '{}',
},
document,
})
console.log('Переводим аккаунт в кооператив')
@@ -141,7 +149,7 @@ export class CooperativeClass {
})
console.log('Переводим инициализационные токены')
await this.blockchain.transfer({ from: 'eosio', to: username, quantity: `100.0000 ${SYMBOL}`, memo: '' })
await this.blockchain.transfer({ from: 'eosio', to: account.username, quantity: `100.0000 ${SYMBOL}`, memo: '' })
console.log(`Арендуем ресурсы кооперативу`)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@coopenomics/cleos",
"version": "2025.5.5-2",
"version": "2025.6.13",
"private": true,
"description": "",
"scripts": {
@@ -312,6 +312,7 @@ struct [[eosio::table, eosio::contract(REGISTRATOR)]] cooperative2 {
eosio::binary_extension<eosio::name> status; ///< Статус процесса подключения
eosio::binary_extension<eosio::time_point_sec> created_at; ///< Дата поступления заявки на подключение (pending | ... | active | blocked)
eosio::binary_extension<document2> document; ///< Подписанный документ соглашения на подключение
eosio::binary_extension<uint64_t> active_participants_count; ///< Счетчик активных пайщиков в кооперативе
/**
+1 -1
View File
@@ -80,7 +80,7 @@ struct [[eosio::table, eosio::contract(SOVIET)]] agreement2 {
};
typedef eosio::multi_index<"agreements2"_n, agreement2,
typedef eosio::multi_index<"agreements3"_n, agreement2,
eosio::indexed_by<"byusername"_n, eosio::const_mem_fun<agreement2, uint64_t, &agreement2::by_username>>,
eosio::indexed_by<"bystatus"_n, eosio::const_mem_fun<agreement2, uint64_t, &agreement2::by_status>>,
eosio::indexed_by<"bydraft"_n, eosio::const_mem_fun<agreement2, uint64_t, &agreement2::by_draft>>,
+2 -1
View File
@@ -104,7 +104,8 @@ static const std::set<eosio::name> soviet_actions = {
//MEET
"creategm"_n,//предложение повестки планового общего собрание
"completegm"_n, //решение общего собрания пайщиков
"ballot"_n, //бюллетень участника общего собрания пайщиков
"gmnotify"_n, //уведомление участника общего собрания пайщиков
//CAPITAL
"capitalinvst"_n, //заявление на инвестиции по договору УХД
"createresult"_n, //клайм прироста капитализации из задания
+17 -3
View File
@@ -42,13 +42,27 @@ struct document2 {
std::vector<signature_info> signatures;
};
void verify_document_or_fail(const document2 &doc)
{
void verify_document_or_fail(
const document2 &doc,
const std::vector<eosio::name>& required_signers = {}
) {
for (const auto &sig : doc.signatures) {
// Проверка завершится прерыванием, если восстановление подписи провалится
assert_recover_key(sig.signed_hash, sig.signature, sig.public_key);
}
};
if (!required_signers.empty()) {
for (const auto& required : required_signers) {
bool found = false;
for (const auto& sig : doc.signatures) {
if (sig.signer == required) {
found = true;
break;
}
}
eosio::check(found, ("Не найдена подпись от обязательного подписанта: " + required.to_string()).c_str());
}
}
}
bool is_empty_document(const document2 &doc)
{
@@ -51,4 +51,23 @@ namespace Registrator {
return *itr;
}
/**
* @brief Получает количество активных пайщиков кооператива
* @param coopname Имя кооператива
* @return uint64_t Количество активных пайщиков
*/
inline uint64_t get_active_participants_count(eosio::name coopname) {
using namespace eosio;
// Получаем информацию о кооперативе
cooperatives2_index cooperatives(_registrator, _registrator.value);
auto coop_itr = cooperatives.find(coopname.value);
if (coop_itr == cooperatives.end() || !coop_itr->is_cooperative || !coop_itr->active_participants_count.has_value()) {
eosio::check(false, "Счетчик пайщиков кооператива не найден");
}
return coop_itr->active_participants_count.value();
}
}
@@ -24,6 +24,9 @@ using newdeclined_interface = void(NEWDECLINED_SIGNATURE);
#define NEWACT_SIGNATURE name coopname, name username, name action, checksum256 package, document2 document
using newact_interface = void(NEWACT_SIGNATURE);
#define NEWLINK_SIGNATURE name coopname, name username, name action, checksum256 package, document2 document
using newlink_interface = void(NEWLINK_SIGNATURE);
#define NEWAGREEMENT_SIGNATURE name coopname, name username, name type, document2 document
using newagreement_interface = void(NEWAGREEMENT_SIGNATURE);
@@ -1,9 +1,12 @@
#define NEWGDECISION_SIGNATURE \
name coopname, /* Имя кооператива */ \
name presider, /* Имя председателя */ \
name secretary, /* Имя секретаря */ \
checksum256 hash, /* Хэш собрания */ \
std::vector<question_result> results, /* Итог по вопросам */ \
uint64_t signed_ballots, /* Число подписанных бюллетеней */ \
uint64_t quorum_percent, /* Процент кворума */ \
bool quorum_passed /* Флаг достигнутого кворума */
double quorum_percent, /* Процент кворума */ \
bool quorum_passed, /* Флаг достигнутого кворума */ \
document2 decision /* Протокол общего собрания */
using newgdecision_interface = void(NEWGDECISION_SIGNATURE);
@@ -1,6 +1,7 @@
// Например, во внешем header можно объявить структуру:
struct question_result {
uint64_t question_id;
uint64_t number;
std::string title;
std::string decision;
std::string context;
@@ -8,7 +8,8 @@ struct [[eosio::table, eosio::contract(MEET)]] meet {
uint64_t id; /// Идентификатор собрания.
checksum256 hash; /// Внешний хэш-идентификатор собрания.
name coopname; ///< Имя кооператива.
name type; ///< Тип общего собрания (annual | special)
name type; ///< Тип общего собрания (regular | extra)
name level; ///< Уровень общего собрания (cooperative | branch)
name initiator; ///< Имя пользователя, который инициировал собрание.
name presider; ///< Имя аккаунта председателя собрания.
name secretary; ///< Имя аккаунта секретаря собрания
@@ -18,12 +19,13 @@ struct [[eosio::table, eosio::contract(MEET)]] meet {
time_point_sec created_at; ///< Дата создания предложения.
time_point_sec open_at; ///< Дата начала собрания.
time_point_sec close_at; ///< Дата завершения собрания.
std::vector<name> notified_users; ///< Пользователи, которые подписали уведомление
uint64_t quorum_percent = 75; /// Цель по кворуму, чтобы собрание считалось состоявшимся
double quorum_percent = 75.0; /// Цель по кворуму, чтобы собрание считалось состоявшимся
uint64_t signed_ballots; /// Общее количество подписанных бюллетеней
uint64_t current_quorum_percent; /// Текущий процент кворума
double current_quorum_percent = 0; /// Текущий процент кворума
uint64_t cycle = 1; /// Цикл общего собрания, который указывает на то, что были предыдущие собрания, которые не состоялись по кворуму
bool quorum_passed; /// Флаг пройденного кворума
bool quorum_passed = false; /// Флаг пройденного кворума
document2 proposal; ///< Предложение
document2 authorization; ///< Решение совета
+18
View File
@@ -8,9 +8,27 @@
#include "src/signbysecr.cpp"
#include "src/signbypresid.cpp"
#include "src/newgdecision.cpp"
#include "src/gmnotify.cpp"
#include <optional>
[[eosio::action]]
void meet::migrate(){
require_auth(_meet);
};
[[eosio::action]]
void meet::delmeet(eosio::name coopname, uint64_t meet_id) {
require_auth(_meet);
Meet::meets_index genmeets(_meet, coopname.value);
auto meet = genmeets.find(meet_id);
eosio::check(genmeets.end() != meet, "Собрание не найдено");
genmeets.erase(meet);
};
std::optional<Meet::meet> meet::get_meet(eosio::name coopname, const checksum256 &hash) {
Meet::meets_index genmeets(_meet, coopname.value);
auto hash_index = genmeets.get_index<"byhash"_n>();
+9 -2
View File
@@ -26,12 +26,15 @@ public:
void authmeet(eosio::name coopname, checksum256 hash, document2 authorization);
[[eosio::action]]
void declmeet(name coopname, checksum256 hash, std::string reason);
[[eosio::action]]
void gmnotify(name coopname, checksum256 hash, name username, document2 notification);
[[eosio::action]]
void vote(name coopname, checksum256 hash, name username, document2 ballot, std::vector<vote_point> votes);
[[eosio::action]]
void restartmeet(name coopname, checksum256 hash, document2 newproposal, time_point_sec new_open_at, time_point_sec new_close_at);
void restartmeet(name coopname, checksum256 hash, checksum256 new_hash, document2 newproposal, time_point_sec new_open_at, time_point_sec new_close_at);
[[eosio::action]]
void signbysecr(name coopname, name username, checksum256 hash, document2 secretary_decision);
@@ -39,6 +42,10 @@ public:
[[eosio::action]]
void signbypresid(name coopname, name username, checksum256 hash, document2 presider_decision);
[[eosio::action]] void migrate();
[[eosio::action]] void delmeet(eosio::name coopname, uint64_t meet_id);
// Сервисное действие:
[[eosio::action]] void newgdecision(NEWGDECISION_SIGNATURE);
private:
@@ -6,7 +6,8 @@ void meet::authmeet(eosio::name coopname, checksum256 hash, document2 authorizat
Meet::meets_index genmeets(_meet, coopname.value);
auto genmeet = genmeets.find(exist_meet -> id);
eosio::check(genmeet -> status == "onrestart"_n || genmeet -> status == "created"_n, "Собрание не находится в неподходящем статусе");
if (!TEST_MODE)
eosio::check(genmeet -> open_at.sec_since_epoch() >= current_time_point().sec_since_epoch() + MIN_OPEN_AGM_DELAY_SEC,
"Дата открытия должна быть по крайней мере через 15 дней от момента решения совета");
@@ -27,7 +27,8 @@ void meet::createmeet(name coopname, checksum256 hash, eosio::name initiator, na
g.id = meet_id;
g.hash = hash;
g.coopname = coopname;
g.type = "annual"_n;
g.type = "regular"_n;
g.level = "cooperative"_n;
g.initiator = initiator;
g.presider = presider;
g.secretary = secretary;
@@ -0,0 +1,32 @@
void meet::gmnotify(name coopname, checksum256 hash, name username, document2 notification) {
require_auth(coopname);
// Проверяем документ уведомления
verify_document_or_fail(notification);
// Получаем собрание по хэшу
Meet::meets_index genmeets(_meet, coopname.value);
auto meet_idx = genmeets.get_index<"byhash"_n>();
auto meet_itr = meet_idx.find(hash);
eosio::check(meet_itr != meet_idx.end(), "Собрание не найдено");
// Проверяем, что пользователь ещё не уведомлял
const auto& notified = meet_itr->notified_users;
eosio::check(std::find(notified.begin(), notified.end(), username) == notified.end(), "Пользователь уже подписал уведомление");
// Добавляем пользователя
meet_idx.modify(meet_itr, coopname, [&](auto& m) {
m.notified_users.push_back(username);
});
Action::send<newlink_interface>(
_soviet,
"newlink"_n,
_meet,
coopname,
username,
get_valid_soviet_action("gmnotify"_n),
hash,
notification
);
}
@@ -1,4 +1,4 @@
void meet::newgdecision(NEWGDECISION_SIGNATURE) {
require_auth(_meet);
// мы просто фиксируем результаты - действием для удобства извлечения.
require_recipient(coopname);
}
@@ -1,4 +1,4 @@
void meet::restartmeet(name coopname, checksum256 hash, document2 newproposal, time_point_sec new_open_at, time_point_sec new_close_at) {
void meet::restartmeet(name coopname, checksum256 hash, checksum256 new_hash, document2 newproposal, time_point_sec new_open_at, time_point_sec new_close_at) {
require_auth(coopname);
// Получаем объект собрания
@@ -6,6 +6,9 @@ void meet::restartmeet(name coopname, checksum256 hash, document2 newproposal, t
eosio::check(meet_opt.has_value(), "Собрание не найдено");
auto meet_record = meet_opt.value();
// Проверяем, что новый хэш отличается от старого
eosio::check(hash != new_hash, "Новый хэш должен отличаться от старого");
// Проверяем, что собрание закрыто и кворум не пройден
auto now = current_time_point();
eosio::check(now.sec_since_epoch() > meet_record.close_at.sec_since_epoch(), "Собрание ещё не закрыто");
@@ -26,18 +29,20 @@ void meet::restartmeet(name coopname, checksum256 hash, document2 newproposal, t
eosio::check(meet_itr != genmeets.end(), "Собрание не найдено при обновлении");
uint64_t old_cycle = meet_itr->cycle;
uint64_t old_quorum = meet_itr->quorum_percent;
double old_quorum = meet_itr->quorum_percent;
document2 empty_document;
genmeets.modify(meet_itr, coopname, [&](auto &m) {
m.proposal = newproposal;
m.authorization = empty_document;
m.open_at = new_open_at;
m.close_at = new_close_at;
m.signed_ballots = 0;
m.current_quorum_percent = 0;
m.quorum_passed = false;
m.hash = new_hash; // Заменяем старый хэш на новый
m.status = "onrestart"_n;
m.proposal = newproposal;
m.authorization = empty_document;
m.open_at = new_open_at;
m.close_at = new_close_at;
m.signed_ballots = 0;
m.current_quorum_percent = 0.0;
m.quorum_passed = false;
m.cycle++; // переходим к следующему циклу
@@ -46,14 +51,15 @@ void meet::restartmeet(name coopname, checksum256 hash, document2 newproposal, t
// 2) при cycle=3 -> 25%
// 3) при cycle >=4 -> делим текущее вдвое
if (old_cycle == 1) {
m.quorum_percent = 50;
m.quorum_percent = 50.0;
} else if (old_cycle == 2) {
m.quorum_percent = 25;
m.quorum_percent = 25.0;
} else {
// при cycle>=3 (т.е. после третьего запуска)
// каждый раз делим предыдущее значение на 2
m.quorum_percent = old_quorum / 2;
m.quorum_percent = old_quorum / 2.0;
}
m.notified_users.clear(); // Очищаем уведомления
});
// Обнуляем состояние голосования по всем вопросам данного собрания
@@ -85,10 +91,10 @@ void meet::restartmeet(name coopname, checksum256 hash, document2 newproposal, t
coopname,
meet_record.initiator,
get_valid_soviet_action("creategm"_n),
hash,
new_hash, // Используем новый хэш
_meet,
"authagm"_n,
"declagm"_n,
"authmeet"_n,
"declmeet"_n,
newproposal,
std::string("")
);
@@ -19,11 +19,11 @@ void meet::signbypresid(name coopname, name username, checksum256 hash, document
eosio::check(meet_record.quorum_passed == true,
"Собрание не достигло кворума, не может быть закрыто с успехом.");
// Проверяем, что секретарь уже подписал протокол
eosio::check(meet_record.status == "preclose"_n, "Протокол должен быть сначала подписан секретарем собрания");
// Проверяем, что протокол уже подписан секретарем собрания
eosio::check(meet_record.status == "preclosed"_n, "Протокол должен быть сначала подписан секретарем собрания");
// Проверяем документ
verify_document_or_fail(presider_decision);
verify_document_or_fail(presider_decision, {meet_record.secretary, meet_record.presider});
// Сформируем данные об итогах голосования по каждому вопросу
Meet::questions_index questions(_meet, coopname.value);
@@ -41,6 +41,7 @@ void meet::signbypresid(name coopname, name username, checksum256 hash, document
results.push_back(question_result{
qitr->id,
qitr->number,
qitr->title,
qitr->decision,
qitr->context,
@@ -69,11 +70,14 @@ void meet::signbypresid(name coopname, name username, checksum256 hash, document
"newgdecision"_n, // имя экшена
_meet, // актор (permission_level{get_self(),"active"})
coopname,
meet_record.presider,
meet_record.secretary,
hash,
results,
meet_record.signed_ballots,
meet_record.quorum_percent,
meet_record.quorum_passed
meet_record.quorum_passed,
presider_decision
);
// После записи решения – удаляем все вопросы
@@ -82,16 +86,15 @@ void meet::signbypresid(name coopname, name username, checksum256 hash, document
qitr = by_meet.erase(qitr);
}
// отправляем документ в принятый реестр
Action::send<newresolved_interface>(
_soviet,
"newresolved"_n,
_meet,
coopname,
username,
get_valid_soviet_action("completegm"_n),
hash,
presider_decision
);
Action::send<newlink_interface>(
_soviet,
"newlink"_n,
_meet,
coopname,
username,
get_valid_soviet_action("completegm"_n),
hash,
presider_decision
);
}
@@ -7,6 +7,8 @@ void meet::signbysecr(name coopname, name username, checksum256 hash, document2
auto meet_record = meet_opt.value();
eosio::check(username == meet_record.secretary, "Вы не являетесь секретарем собрания");
eosio::check(meet_record.status == "authorized"_n, "Собрание не находится в статусе 'authorized'");
// Проверяем, что текущее время > close_at
auto now = current_time_point();
@@ -18,7 +20,7 @@ void meet::signbysecr(name coopname, name username, checksum256 hash, document2
"Собрание не достигло кворума, не может быть закрыто с успехом.");
// Проверяем документ
verify_document_or_fail(secretary_decision);
verify_document_or_fail(secretary_decision, {meet_record.secretary});
// Обновляем запись в таблице, сохраняя решение секретаря
Meet::meets_index genmeets(_meet, coopname.value);
@@ -29,22 +31,9 @@ void meet::signbysecr(name coopname, name username, checksum256 hash, document2
hash_index.modify(meet_itr, same_payer, [&](auto& m) {
m.decision1 = secretary_decision;
m.status = "preclose"_n; // Новый статус после подписи секретарем
m.status = "preclosed"_n; // Новый статус после подписи секретарем
});
// отправляем документ во входящий реестр
Action::send<newsubmitted_interface>(
_soviet,
"newsubmitted"_n,
_meet,
coopname,
username,
get_valid_soviet_action("completegm"_n),
hash,
secretary_decision
);
}
+14 -3
View File
@@ -1,7 +1,7 @@
// Заглушка для получения числа пайщиков кооператива.
uint64_t get_total_participants(eosio::name coopname) {
// Здесь можно реализовать получение актуальных данных, например, через inline action или вызов другого контракта.
return 2; // Пример: 100 пайщиков
// Используем метод из shared_registrator для получения актуального количества активных пайщиков
return Registrator::get_active_participants_count(coopname);
}
void meet::vote(name coopname, checksum256 hash, name username, document2 ballot, std::vector<vote_point> votes) {
@@ -83,7 +83,7 @@ void meet::vote(name coopname, checksum256 hash, name username, document2 ballot
genmeets.modify(meet_itr, coopname, [&](auto &m) {
m.signed_ballots++; // регистрируем принятый бюллетень
uint64_t total_participants = get_total_participants(coopname);
m.current_quorum_percent = (m.signed_ballots * 100) / total_participants;
m.current_quorum_percent = (static_cast<double>(m.signed_ballots) * 100.0) / static_cast<double>(total_participants);
if (m.current_quorum_percent > m.quorum_percent) {
m.quorum_passed = true;
}
@@ -100,4 +100,15 @@ void meet::vote(name coopname, checksum256 hash, name username, document2 ballot
// Здесь можно при необходимости обновить поле решения, логирование или вызвать inline action.
}
}
Action::send<newlink_interface>(
_soviet,
"newlink"_n,
_meet,
coopname,
username,
get_valid_soviet_action("ballot"_n),
hash,
ballot
);
}
@@ -21,5 +21,6 @@
#include "src/coop/enabranches.cpp"
#include "src/coop/disbranches.cpp"
#include "src/coop/updatecoop.cpp"
#include "src/coop/decparticpnt.cpp"
#include "src/verification/verificate.cpp"
@@ -67,5 +67,8 @@ public:
[[eosio::action]] void enabranches(eosio::name coopname);
[[eosio::action]] void disbranches(eosio::name coopname);
// Уменьшение счетчика активных пайщиков при блокировке
[[eosio::action]] void decparticpnt(eosio::name coopname, eosio::name username);
};
@@ -0,0 +1,23 @@
/**
* @brief Уменьшает счетчик активных пайщиков кооператива
* @param coopname Имя кооператива
* @param username Имя пайщика
*/
void registrator::decparticpnt(eosio::name coopname, eosio::name username)
{
// Проверка авторизации - может вызывать только контракт soviet
require_auth(_soviet);
// Получаем информацию о кооперативе
cooperatives2_index cooperatives(_registrator, _registrator.value);
auto coop_itr = cooperatives.find(coopname.value);
if (coop_itr != cooperatives.end() && coop_itr->is_cooperative) {
if (coop_itr->active_participants_count.has_value() && coop_itr->active_participants_count.value() > 0) {
// Уменьшаем счетчик только если он больше нуля
cooperatives.modify(coop_itr, _registrator, [&](auto &coop) {
coop.active_participants_count = coop.active_participants_count.value() - 1;
});
}
}
}
@@ -1,80 +1,35 @@
[[eosio::action]] void registrator::migrate() {
require_auth(_registrator);
// Registrator::candidates_legacy_index candidates_legacy(_registrator, _provider.value);
// auto candidate_legacy_itr = candidates_legacy.begin();
// while (candidate_legacy_itr != candidates_legacy.end()) {
// candidate_legacy_itr = candidates_legacy.erase(candidate_legacy_itr);
// }
// Новый код: удаление всех записей из cooperatives2_index
// cooperatives2_index coops2(_registrator, _registrator.value);
// auto coop2_itr = coops2.begin();
// while (coop2_itr != coops2.end()) {
// coop2_itr = coops2.erase(coop2_itr);
// }
cooperatives_index coops(_registrator, _registrator.value);
// Миграция cooperative -> cooperative2
// Получаем таблицу кооперативов
cooperatives2_index coops2(_registrator, _registrator.value);
for (auto coop_itr = coops.begin(); coop_itr != coops.end(); ++coop_itr) {
// Проверяем, что запись еще не существует в cooperatives2
if (coops2.find(coop_itr->username.value) == coops2.end()) {
// Создаем новую запись в cooperative2
coops2.emplace(_registrator, [&](auto& new_coop) {
new_coop.username = coop_itr->username;
new_coop.parent_username = coop_itr->parent_username;
new_coop.announce = coop_itr->announce;
new_coop.description = coop_itr->description;
new_coop.is_cooperative = coop_itr->is_cooperative;
new_coop.is_branched = coop_itr->is_branched;
new_coop.is_enrolled = coop_itr->is_enrolled;
new_coop.coop_type = coop_itr->coop_type;
new_coop.registration = coop_itr->registration;
new_coop.initial = coop_itr->initial;
new_coop.minimum = coop_itr->minimum;
if (coop_itr->org_registration.has_value())
new_coop.org_registration = coop_itr->org_registration;
if (coop_itr->org_initial.has_value())
new_coop.org_initial = coop_itr->org_initial;
if (coop_itr->org_minimum.has_value())
new_coop.org_minimum = coop_itr->org_minimum;
if (coop_itr->status.has_value())
new_coop.status = coop_itr->status;
if (coop_itr->created_at.has_value())
new_coop.created_at = coop_itr->created_at;
// Если есть документ в кооперативе, преобразуем его в document2
if (coop_itr->document.has_value()) {
document2 new_doc;
new_doc.version = "0";
new_doc.hash = coop_itr->document.value().hash;
new_doc.doc_hash = coop_itr->document.value().hash;
new_doc.meta_hash = eosio::checksum256();
new_doc.meta = coop_itr->document.value().meta;
// Добавляем информацию о подписи
signature_info sig;
sig.id = 0;
sig.signer = coop_itr->username;
sig.public_key = coop_itr->document.value().public_key;
sig.signature = coop_itr->document.value().signature;
sig.signed_hash = coop_itr->document.value().hash;
// Используем created_at как время подписания, если доступно
if (coop_itr->created_at.has_value()) {
sig.signed_at = coop_itr->created_at.value();
} else {
// В противном случае используем текущее время
sig.signed_at = eosio::time_point_sec(eosio::current_time_point());
}
new_doc.signatures.push_back(sig);
new_coop.document = new_doc;
}
});
// Для каждого кооператива проверяем наличие счетчика активных пайщиков
for (auto coop_itr = coops2.begin(); coop_itr != coops2.end(); ++coop_itr) {
// Если счетчик активных пайщиков уже установлен, пропускаем кооператив
if (coop_itr->active_participants_count.has_value()) {
continue;
}
// Получаем имя кооператива для использования как scope
eosio::name coopname = coop_itr->username;
// Получаем таблицу участников из контракта soviet с scope кооператива
participants_index participants(_soviet, coopname.value);
// Счетчик активных пайщиков
uint64_t active_count = 0;
// Проходим по всем участникам и считаем активных (has_vote == true)
for (auto part_it = participants.begin(); part_it != participants.end(); ++part_it) {
if (part_it->has_vote) {
active_count++;
}
}
// Обновляем запись кооператива, устанавливая счетчик активных пайщиков
coops2.modify(coop_itr, _registrator, [&](auto& coop) {
coop.active_participants_count = active_count;
});
}
}
@@ -49,6 +49,20 @@ void registrator::confirmreg(eosio::name coopname, checksum256 registration_hash
std::make_tuple(coopname, candidate -> initial)
).send();
// Увеличиваем счетчик активных пайщиков
cooperatives2_index cooperatives(_registrator, _registrator.value);
auto coop_itr = cooperatives.find(coopname.value);
if (coop_itr != cooperatives.end() && coop_itr->is_cooperative) {
cooperatives.modify(coop_itr, _registrator, [&](auto &coop) {
if (coop.active_participants_count.has_value()) {
coop.active_participants_count = coop.active_participants_count.value() + 1;
} else {
coop.active_participants_count = 1;
}
});
}
Registrator::candidates_index candidates(_registrator, coopname.value);
auto it = candidates.find(candidate -> username.value);
candidates.erase(it);
+1 -1
View File
@@ -18,7 +18,6 @@
#include "src/agreement/declineagree.cpp"
#include "src/agreement/sndagreement.cpp"
#include "src/agreement/migrateagree.cpp"
#include "src/approve/confirmapprv.cpp"
#include "src/approve/createapprv.cpp"
#include "src/approve/declineapprv.cpp"
@@ -41,6 +40,7 @@
#include "src/doc/declinedoc.cpp"
#include "src/doc/newact.cpp"
#include "src/doc/newlink.cpp"
#include "src/doc/newdecision.cpp"
#include "src/doc/newdeclined.cpp"
#include "src/doc/newresolved.cpp"
@@ -50,6 +50,7 @@ public:
[[eosio::action]] void newpackage(NEWPACKAGE_SIGNATURE);
[[eosio::action]] void newact(NEWACT_SIGNATURE);
[[eosio::action]] void newlink(NEWLINK_SIGNATURE);
[[eosio::action]] void newdeclined(NEWDECLINED_SIGNATURE);
[[eosio::action]] void declinedoc(eosio::name coopname, eosio::name username, checksum256 hash, document2 document);
@@ -39,7 +39,7 @@ void soviet::migrateagree(eosio::name coopname, uint64_t agreement_id) {
new_doc.signatures.push_back(sig);
// Создаем новую запись в agreement2 с тем же ID
eosio::multi_index<"agreements2"_n, agreement2> agreements2(_self, coopname.value);
agreements2_index agreements2(_self, coopname.value);
agreements2.emplace(_self, [&](auto& new_agreement) {
new_agreement.id = agreement_itr->id;
@@ -0,0 +1,6 @@
[[eosio::action]] void soviet::newlink(NEWLINK_SIGNATURE) {
check_auth_and_get_payer_or_fail(contracts_whitelist);
require_recipient(coopname);
require_recipient(username);
};
@@ -10,10 +10,25 @@ void soviet::block(eosio::name coopname, eosio::name admin, eosio::name username
participants_index participants(_soviet, coopname.value);
auto participant = participants.find(username.value);
// Сохраняем текущие значения статуса и права голоса
bool had_vote = participant->has_vote;
bool was_active = participant->status == "accepted"_n;
participants.modify(participant, _soviet, [&](auto &row){
row.status = "blocked"_n;
row.is_initial = false;
row.is_minimum = false;
row.has_vote = false;
});
// Если участник был активен и имел право голоса, уменьшаем счетчик активных пайщиков
if (was_active && had_vote) {
// Отправляем уведомление в registrator для уменьшения счетчика
action(
permission_level{_soviet, "active"_n},
_registrator,
"decparticpnt"_n,
std::make_tuple(coopname, username)
).send();
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@coopenomics/contracts",
"version": "2025.5.5-2",
"version": "2025.6.13",
"private": true,
"scripts": {
"build:all": "./build-all.sh",
@@ -97,6 +97,7 @@ export interface IQuestion {
export interface IQuestionResult {
question_id: IUint64
number: IUint64
title: string
decision: string
context: string
+3
View File
@@ -8,6 +8,7 @@ PORT=2998
SERVER_SECRET=SECRET
SOCKET_SERVER=http://localhost:2222
# URL of the Mongo DB
MONGODB_URL=mongodb://127.0.0.1:27017/cooperative-x
COOPNAME=voskhod
@@ -47,3 +48,5 @@ POSTGRES_PORT=5432
POSTGRES_USERNAME=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_DATABASE=voskhod
TIMEZONE=Europe/Moscow
+6 -6
View File
@@ -144,7 +144,7 @@
<li><a href="#definition-AddParticipantInput">AddParticipantInput</a></li>
<li><a href="#definition-AddTrustedAccountInput">AddTrustedAccountInput</a></li>
<li><a href="#definition-AgendaMeetPoint">AgendaMeetPoint</a></li>
<li><a href="#definition-AgendaMeetPointInput">AgendaMeetPointInput</a></li>
<li><a href="#definition-AgendaGeneralMeetPointInput">AgendaGeneralMeetPointInput</a></li>
<li><a href="#definition-AgendaWithDocuments">AgendaWithDocuments</a></li>
<li><a href="#definition-AgreementInput">AgreementInput</a></li>
<li><a href="#definition-AgreementVar">AgreementVar</a></li>
@@ -9706,11 +9706,11 @@
</div>
</div>
</section>
<section id="definition-AgendaMeetPointInput" class="definition definition-input-object" data-traverse-target="definition-AgendaMeetPointInput">
<section id="definition-AgendaGeneralMeetPointInput" class="definition definition-input-object" data-traverse-target="definition-AgendaGeneralMeetPointInput">
<div class="definition-group-name">
<a href="#group-Types">Types</a>
</div>
<h2 class="definition-heading">AgendaMeetPointInput</h2>
<h2 class="definition-heading">AgendaGeneralMeetPointInput</h2>
<div class="doc-row">
<div class="doc-copy">
<div class="definition-description doc-copy-section">
@@ -13616,7 +13616,7 @@
<tbody>
<tr>
<td>
<span class="property-name"><code>agenda</code></span> - <span class="property-type"><a href="#definition-AgendaMeetPointInput"><code>[AgendaMeetPointInput!]!</code></a></span>
<span class="property-name"><code>agenda</code></span> - <span class="property-type"><a href="#definition-AgendaGeneralMeetPointInput"><code>[AgendaGeneralMeetPointInput!]!</code></a></span>
</td>
<td> Повестка собрания </td>
</tr>
@@ -13678,7 +13678,7 @@
<html>
<head></head>
<body><pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
<span class="hljs-attr">"agenda"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><span class="hljs-string">AgendaMeetPointInput</span><span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>
<span class="hljs-attr">"agenda"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><span class="hljs-string">AgendaGeneralMeetPointInput</span><span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>
<span class="hljs-attr">"close_at"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"2007-12-03T10:15:30Z"</span><span class="hljs-punctuation">,</span>
<span class="hljs-attr">"coopname"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"abc123"</span><span class="hljs-punctuation">,</span>
<span class="hljs-attr">"hash"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"xyz789"</span><span class="hljs-punctuation">,</span>
@@ -24721,4 +24721,4 @@
</div>
</div>
</body>
</html>
</html>
@@ -2,6 +2,7 @@ import mongoose from 'mongoose';
export default {
name: 'Добавление поля fact_address для организаций',
validUntil: new Date(), // Текущая дата, миграция больше не будет применяться
async up() {
console.log('Выполнение миграции: Добавление поля fact_address для организаций');
@@ -2,6 +2,7 @@ import mongoose from 'mongoose';
export default {
name: 'Преобразование числовых method_id в строковые',
validUntil: new Date(), // Текущая дата, миграция больше не будет применяться
async up() {
console.log('Выполнение миграции: Преобразование числовых method_id в строковые');
@@ -6,6 +6,7 @@ import type { SovietContract } from 'cooptypes';
export default {
name: 'Синхронизация данных из блокчейна',
isTest: false, // Включаем тестовый режим - миграция не будет сохранена в БД
validUntil: new Date(), // Текущая дата, миграция больше не будет применяться
async up({ blockchain }: { blockchain: BlockchainService }): Promise<boolean> {
console.log('Выполнение миграции: Синхронизация данных из блокчейна');
@@ -0,0 +1,108 @@
import { BlockchainService } from '../src/infrastructure/blockchain/blockchain.service';
import Vault from '../src/models/vault.model';
import config from '../src/config/config';
import type { SovietContract } from 'cooptypes';
export default {
name: 'Синхронизация данных из блокчейна',
isTest: false, // Включаем тестовый режим - миграция не будет сохранена в БД
validUntil: new Date(), // Текущая дата, миграция больше не будет применяться
async up({ blockchain }: { blockchain: BlockchainService }): Promise<boolean> {
console.log('Выполнение миграции: Синхронизация данных из блокчейна');
try {
// Проверяем статус подключения к блокчейну без авторизации
const info = await blockchain.getInfo();
console.log(`Подключение к блокчейну успешно. Chain ID: ${info.chain_id}`);
console.log(`Информация о блокчейне: Chain - ${info.chain_id}, Head Block: ${info.head_block_num}`);
// Имя кооператива, чьи данные будем синхронизировать
const coopname = config.coopname; // Укажите имя вашего кооператива
// Получаем приватный ключ из Vault модели
try {
const wif = await Vault.getWif(coopname);
if (wif) {
console.log(`Получен приватный ключ для аккаунта ${coopname}`);
// Инициализируем блокчейн-сервис с полученным ключом
blockchain.initialize(coopname, wif);
console.log(`Блокчейн-сервис инициализирован с аккаунтом ${coopname}`);
const oldRows = await blockchain.getAllRows<SovietContract.Tables.AgreementsLegacy.IAgreementLegacy>(
'soviet',
coopname,
'agreements'
);
console.log('Получено legacy соглашений:', oldRows.length);
// Получаем все строки из таблицы agreements2
const newRows = await blockchain.getAllRows<SovietContract.Tables.Agreements.IAgreement>(
'soviet',
coopname,
'agreements3'
);
console.log('Получено новых соглашений:', newRows.length);
// Находим соглашения, которые ещё не были перенесены
const newAgreementIds = new Set(newRows.map((row) => row.id));
const notMigratedAgreements = oldRows.filter((row) => !newAgreementIds.has(row.id));
console.log('Соглашения, которые НЕ были перенесены:', notMigratedAgreements.length);
console.log('Детали непереносимых соглашений:');
notMigratedAgreements.forEach((agreement) => {
console.log(
`ID: ${agreement.id}, Пользователь: ${agreement.username}, Тип: ${agreement.type}, Статус: ${agreement.status}`
);
});
// Подготовка массива транзакций для миграции
if (notMigratedAgreements.length > 0) {
console.log('Подготовлен массив транзакций для миграции');
console.log('COOPNAME: ', coopname);
// Последовательная миграция соглашений
for (let i = 0; i < notMigratedAgreements.length; i++) {
const agreement = notMigratedAgreements[i];
console.log(`Миграция соглашения ${i + 1}/${notMigratedAgreements.length}, ID: ${agreement.id}`);
try {
await blockchain.transact({
account: 'soviet',
name: 'migrateagree',
authorization: [{ actor: coopname, permission: 'active' }],
data: {
coopname,
agreement_id: agreement.id,
},
});
console.log(`✅ Соглашение ID: ${agreement.id} успешно перенесено`);
} catch (error) {
console.error(`❌ Ошибка при переносе соглашения ID: ${agreement.id}:`, error);
}
}
console.log('Миграция соглашений завершена');
}
// Теперь можно выполнять транзакции от имени аккаунта
// blockchain.transact(...);
} else {
console.log(`Приватный ключ для ${coopname} не найден в Vault`);
}
} catch (vaultError) {
console.error('Ошибка при получении ключа из Vault:', vaultError);
}
console.log('Миграция завершена: Синхронизация данных из блокчейна выполнена успешно');
return true;
} catch (error) {
console.error('Ошибка при выполнении миграции блокчейн-данных:', error);
return false;
}
},
};
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "create-nodejs-express-app",
"version": "2025.5.5-2",
"version": "2025.6.13",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "create-nodejs-express-app",
"version": "2025.5.5-2",
"version": "2025.6.13",
"license": "MIT",
"dependencies": {
"@a2seven/yoo-checkout": "^1.1.4",
+4 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@coopenomics/controller",
"version": "2025.5.5-2",
"version": "2025.6.13",
"description": "",
"private": false,
"bin": "bin/createNodejsApp.js",
@@ -64,6 +64,7 @@
"dependencies": {
"@a2seven/yoo-checkout": "^1.1.4",
"@coopenomics/factory": "workspace:*",
"@coopenomics/sdk": "workspace:*",
"@graphql-codegen/typescript-graphql-request": "^6.2.0",
"@graphql-codegen/typescript-resolvers": "^4.4.0",
"@graphql-tools/schema": "^10.0.7",
@@ -126,6 +127,7 @@
"jspdf": "^2.5.1",
"lodash": "^4.17.21",
"moment": "^2.24.0",
"moment-timezone": "^0.5.45",
"mongoose": "^8.5.2",
"mongoose-sequence": "^6.0.1",
"morgan": "^1.9.1",
@@ -155,8 +157,7 @@
"ws": "^8.18.0",
"xss-clean": "^0.1.1",
"zod": "^3.23.8",
"zod-to-json-schema": "^3.23.5",
"@coopenomics/sdk": "workspace:*"
"zod-to-json-schema": "^3.23.5"
},
"devDependencies": {
"@antfu/eslint-config": "^2.24.1",
+373 -21
View File
@@ -159,8 +159,8 @@ input AddTrustedAccountInput {
trusted: String!
}
"""Пункт повестки собрания"""
type AgendaMeetPoint {
"""Пункт повестки общего собрания (для ввода)"""
input AgendaGeneralMeetPointInput {
"""Контекст или дополнительная информация по пункту повестки"""
context: String!
@@ -171,8 +171,35 @@ type AgendaMeetPoint {
title: String!
}
"""Пункт повестки собрания (для ввода)"""
input AgendaMeetPointInput {
"""Вопрос повестки общего собрания"""
input AgendaGeneralMeetQuestion {
"""Контекст или дополнительная информация по вопросу"""
context: String
"""Предлагаемое решение по вопросу повестки"""
decision: String!
"""Номер вопроса в повестке"""
number: String!
"""Заголовок вопроса повестки"""
title: String!
}
"""Данные собрания для повестки"""
input AgendaMeet {
"""Дата и время окончания собрания"""
close_at_datetime: String!
"""Дата и время начала собрания"""
open_at_datetime: String!
"""Тип собрания (очередное или внеочередное)"""
type: String!
}
"""Пункт повестки собрания"""
type AgendaMeetPoint {
"""Контекст или дополнительная информация по пункту повестки"""
context: String!
@@ -222,6 +249,8 @@ input AnnualGeneralMeetingAgendaGenerateDocumentInput {
"""Ссылки, связанные с документом"""
links: [String!]
meet: AgendaMeet!
questions: [AgendaGeneralMeetQuestion!]!
"""Часовой пояс, в котором был создан документ"""
timezone: String
@@ -274,6 +303,8 @@ input AnnualGeneralMeetingAgendaSignedMetaDocumentInput {
"""Ссылки, связанные с документом"""
links: [String!]!
meet: AgendaMeet!
questions: [AgendaGeneralMeetQuestion!]!
"""ID документа в реестре"""
registry_id: Int!
@@ -310,6 +341,9 @@ input AnnualGeneralMeetingDecisionGenerateDocumentInput {
"""Ссылки, связанные с документом"""
links: [String!]
"""Хеш собрания"""
meet_hash: String!
"""Часовой пояс, в котором был создан документ"""
timezone: String
@@ -362,6 +396,9 @@ input AnnualGeneralMeetingDecisionSignedMetaDocumentInput {
"""Ссылки, связанные с документом"""
links: [String!]!
"""Хеш собрания"""
meet_hash: String!
"""ID документа в реестре"""
registry_id: Int!
@@ -397,6 +434,9 @@ input AnnualGeneralMeetingNotificationGenerateDocumentInput {
"""Ссылки, связанные с документом"""
links: [String!]
"""Хеш собрания"""
meet_hash: String!
"""Часовой пояс, в котором был создан документ"""
timezone: String
@@ -410,6 +450,64 @@ input AnnualGeneralMeetingNotificationGenerateDocumentInput {
version: String
}
input AnnualGeneralMeetingNotificationSignedDocumentInput {
"""Хэш содержимого документа"""
doc_hash: String!
"""Общий хэш (doc_hash + meta_hash)"""
hash: String!
"""Метаинформация"""
meta: AnnualGeneralMeetingNotificationSignedMetaDocumentInput!
"""Хэш мета-данных"""
meta_hash: String!
"""Вектор подписей"""
signatures: [SignatureInfoInput!]!
"""Версия стандарта документа"""
version: String!
}
input AnnualGeneralMeetingNotificationSignedMetaDocumentInput {
"""Номер блока, на котором был создан документ"""
block_num: Int!
"""Название кооператива, связанное с документом"""
coopname: String!
"""Дата и время создания документа"""
created_at: String!
"""Имя генератора, использованного для создания документа"""
generator: String!
"""Язык документа"""
lang: String!
"""Ссылки, связанные с документом"""
links: [String!]!
"""Хеш собрания"""
meet_hash: String!
"""ID документа в реестре"""
registry_id: Int!
"""Часовой пояс, в котором был создан документ"""
timezone: String!
"""Название документа"""
title: String!
"""Имя пользователя, создавшего документ"""
username: String!
"""Версия генератора, использованного для создания документа"""
version: String!
}
input AnnualGeneralMeetingSovietDecisionGenerateDocumentInput {
"""Номер блока, на котором был создан документ"""
block_num: Int
@@ -420,6 +518,9 @@ input AnnualGeneralMeetingSovietDecisionGenerateDocumentInput {
"""Дата и время создания документа"""
created_at: String
"""ID решения совета"""
decision_id: Float!
"""Имя генератора, использованного для создания документа"""
generator: String
@@ -429,6 +530,9 @@ input AnnualGeneralMeetingSovietDecisionGenerateDocumentInput {
"""Ссылки, связанные с документом"""
links: [String!]
"""Хеш собрания"""
meet_hash: String!
"""Часовой пояс, в котором был создан документ"""
timezone: String
@@ -443,6 +547,9 @@ input AnnualGeneralMeetingSovietDecisionGenerateDocumentInput {
}
input AnnualGeneralMeetingVotingBallotGenerateDocumentInput {
"""Ответы голосования"""
answers: [AnswerInput!]!
"""Номер блока, на котором был создан документ"""
block_num: Int
@@ -461,13 +568,16 @@ input AnnualGeneralMeetingVotingBallotGenerateDocumentInput {
"""Ссылки, связанные с документом"""
links: [String!]
"""Хеш собрания"""
meet_hash: String!
"""Часовой пояс, в котором был создан документ"""
timezone: String
"""Название документа"""
title: String
"""Имя пользователя, создавшего документ"""
"""Имя пользователя голосующего"""
username: String!
"""Версия генератора, использованного для создания документа"""
@@ -495,6 +605,9 @@ input AnnualGeneralMeetingVotingBallotSignedDocumentInput {
}
input AnnualGeneralMeetingVotingBallotSignedMetaDocumentInput {
"""Ответы голосования"""
answers: [AnswerInput!]!
"""Номер блока, на котором был создан документ"""
block_num: Int!
@@ -513,6 +626,9 @@ input AnnualGeneralMeetingVotingBallotSignedMetaDocumentInput {
"""Ссылки, связанные с документом"""
links: [String!]!
"""Хеш собрания"""
meet_hash: String!
"""ID документа в реестре"""
registry_id: Int!
@@ -522,13 +638,24 @@ input AnnualGeneralMeetingVotingBallotSignedMetaDocumentInput {
"""Название документа"""
title: String!
"""Имя пользователя, создавшего документ"""
"""Имя пользователя голосующего"""
username: String!
"""Версия генератора, использованного для создания документа"""
version: String!
}
input AnswerInput {
"""ID вопроса"""
id: String!
"""Номер вопроса"""
number: String!
"""Голос (за/против/воздержался)"""
vote: String!
}
input AssetContributionActGenerateDocumentInput {
"""Идентификатор акта"""
act_id: String!
@@ -978,9 +1105,22 @@ type BlockchainDecision {
statement: SignedBlockchainDocument!
type: String!
username: String!
"""Сертификат пользователя, создавшего решение"""
username_certificate: UserCertificateUnion
validated: Boolean!
votes_against: [String!]!
"""
Сертификаты пользователей, голосовавших "против"
"""
votes_against_certificates: [UserCertificateUnion!]!
votes_for: [String!]!
"""
Сертификаты пользователей, голосовавших "за"
"""
votes_for_certificates: [UserCertificateUnion!]!
}
"""Информация о состоянии блокчейна"""
@@ -1164,6 +1304,9 @@ type ContactsDTO {
}
type CooperativeOperatorAccount {
"""Количество активных участников"""
active_participants_count: Float!
"""Объявление кооператива"""
announce: String!
@@ -1244,7 +1387,7 @@ enum Country {
input CreateAnnualGeneralMeetInput {
"""Повестка собрания"""
agenda: [AgendaMeetPointInput!]!
agenda: [AgendaGeneralMeetPointInput!]!
"""Время закрытия собрания"""
close_at: DateTime!
@@ -1252,9 +1395,6 @@ input CreateAnnualGeneralMeetInput {
"""Имя аккаунта кооператива"""
coopname: String!
"""Хеш"""
hash: String!
"""Имя аккаунта инициатора"""
initiator: String!
@@ -1713,6 +1853,26 @@ type Entrepreneur {
username: String!
}
type EntrepreneurCertificate {
"""Имя"""
first_name: String!
"""ИНН"""
inn: String!
"""Фамилия"""
last_name: String!
"""Отчество"""
middle_name: String
"""Тип аккаунта"""
type: AccountType!
"""Имя аккаунта"""
username: String!
}
type EntrepreneurDetails {
"""ИНН"""
inn: String!
@@ -1730,12 +1890,15 @@ input EntrepreneurDetailsInput {
}
"""
Расширенное действие блокчейна с персональными данными пользователя, совершившего его.
Расширенное действие блокчейна с сертификатом пользователя, совершившего его.
"""
type ExtendedBlockchainAction {
account: String!
account_ram_deltas: [AccountRamDelta!]!
action_ordinal: Int!
"""Сертификат пользователя (сокращенная информация)"""
actor_certificate: UserCertificateUnion
authorization: [ActionAuthorization!]!
block_id: String!
block_num: Int!
@@ -1752,9 +1915,39 @@ type ExtendedBlockchainAction {
receipt: ActionReceipt!
receiver: String!
transaction_id: String!
}
"""Доп. данные о пользователе (физ/ИП/организация)"""
user: UserDataUnion
"""Расширенный статус собрания на основе дат и состояния"""
enum ExtendedMeetStatus {
"""Авторизовано"""
AUTHORIZED
"""Закрыто"""
CLOSED
"""Создано"""
CREATED
"""Истекло без кворума"""
EXPIRED_NO_QUORUM
"""Неопределенное состояние"""
NONE
"""Ожидаем утверждения новой даты собрания"""
ONRESTART
"""Предварительно закрыто"""
PRECLOSED
"""Голосование завершено, ожидает подписей"""
VOTING_COMPLETED
"""Голосование идет"""
VOTING_IN_PROGRESS
"""Ожидает открытия"""
WAITING_FOR_OPENING
}
type Extension {
@@ -2040,6 +2233,23 @@ type Individual {
username: String!
}
type IndividualCertificate {
"""Имя"""
first_name: String!
"""Фамилия"""
last_name: String!
"""Отчество"""
middle_name: String
"""Тип аккаунта"""
type: AccountType!
"""Имя аккаунта"""
username: String!
}
input Init {
"""
Объект организации кооператива, которая обслуживает данный экземпляр программного обеспечения MONO
@@ -2114,6 +2324,12 @@ type Meet {
"""Цикл собрания"""
cycle: Float!
"""Документ с решением секретаря"""
decision1: DocumentAggregate
"""Документ с решением председателя"""
decision2: DocumentAggregate
"""Хеш собрания"""
hash: String!
@@ -2123,14 +2339,23 @@ type Meet {
"""Инициатор собрания"""
initiator: String!
"""Сертификат инициатора собрания"""
initiator_certificate: UserCertificateUnion
"""Список пользователей, которые подписали уведомление"""
notified_users: [String!]!
"""Дата открытия собрания"""
open_at: DateTime!
"""Председатель собрания"""
presider: String!
"""Сертификат председателя собрания"""
presider_certificate: UserCertificateUnion
"""Документ с повесткой собрания"""
proposal: DocumentAggregate!
proposal: DocumentAggregate
"""Флаг достижения кворума"""
quorum_passed: Boolean!
@@ -2141,6 +2366,9 @@ type Meet {
"""Секретарь собрания"""
secretary: String!
"""Сертификат секретаря собрания"""
secretary_certificate: UserCertificateUnion
"""Количество подписанных бюллетеней"""
signed_ballots: Float!
@@ -2183,33 +2411,78 @@ type MeetPreProcessing {
"""Инициатор собрания"""
initiator: String!
"""Сертификат инициатора собрания"""
initiator_certificate: UserCertificateUnion
"""Дата открытия собрания"""
open_at: DateTime!
"""Председатель собрания"""
presider: String!
"""Сертификат председателя собрания"""
presider_certificate: UserCertificateUnion
"""Документ с предложением повестки собрания"""
proposal: DocumentAggregate
"""Секретарь собрания"""
secretary: String!
"""Сертификат секретаря собрания"""
secretary_certificate: UserCertificateUnion
}
"""Данные о собрании после обработки"""
type MeetProcessed {
"""Решение по собранию в формате блокчейн-действия"""
decision: BlockchainAction!
"""Имя кооператива"""
coopname: String!
"""Документ решения из блокчейна"""
decision: SignedDigitalDocument!
"""Агрегат документа решения"""
decisionAggregate: DocumentAggregate
"""Хеш собрания"""
hash: String!
"""Председатель собрания"""
presider: String!
"""Сертификат председателя собрания"""
presider_certificate: UserCertificateUnion
"""Пройден ли кворум"""
quorum_passed: Boolean!
"""Процент кворума"""
quorum_percent: Int!
"""Результаты голосования по вопросам"""
results: [MeetQuestionResult!]!
"""Секретарь собрания"""
secretary: String!
"""Сертификат секретаря собрания"""
secretary_certificate: UserCertificateUnion
"""Количество подписанных бюллетеней"""
signed_ballots: Int!
}
"""Данные о собрании в процессе обработки"""
type MeetProcessing {
"""Расширенный статус собрания на основе дат и состояния"""
extendedStatus: ExtendedMeetStatus!
"""Хеш собрания"""
hash: String!
"""Флаг указывающий, голосовал ли текущий пользователь"""
isVoted: Boolean!
"""Основная информация о собрании"""
meet: Meet!
@@ -2217,6 +2490,36 @@ type MeetProcessing {
questions: [Question!]!
}
"""Результат голосования по вопросу"""
type MeetQuestionResult {
"""Принят ли вопрос"""
accepted: Boolean!
"""Контекст вопроса"""
context: String!
"""Принятое решение"""
decision: String!
"""Порядковый номер вопроса"""
number: Int!
"""Идентификатор вопроса"""
question_id: Int!
"""Заголовок вопроса"""
title: String!
"""Количество воздержавшихся"""
votes_abstained: Int!
"""Количество голосов против"""
votes_against: Int!
"""Количество голосов за"""
votes_for: Int!
}
input MetaDocumentInput {
"""Номер блока, на котором был создан документ"""
block_num: Int!
@@ -2477,6 +2780,9 @@ type Mutation {
"""Модерировать заявку"""
moderateRequest(data: ModerateRequestInput!): Transaction!
"""Уведомление о проведении общего собрания пайщиков"""
notifyOnAnnualGeneralMeet(data: NotifyOnAnnualGeneralMeetInput!): MeetAggregate!
"""Отклонить модерацию по заявке"""
prohibitRequest(data: ProhibitRequestInput!): Transaction!
@@ -2564,6 +2870,13 @@ type Mutation {
voteOnAnnualGeneralMeet(data: VoteOnAnnualGeneralMeetInput!): MeetAggregate!
}
input NotifyOnAnnualGeneralMeetInput {
coopname: String!
meet_hash: String!
notification: AnnualGeneralMeetingNotificationSignedDocumentInput!
username: String!
}
type Organization {
"""Город"""
city: String!
@@ -2602,6 +2915,26 @@ type Organization {
username: String!
}
type OrganizationCertificate {
"""ИНН"""
inn: String!
"""ОГРН"""
ogrn: String!
"""Данные представителя"""
represented_by: RepresentedByCertificate!
"""Короткое название организации"""
short_name: String!
"""Тип аккаунта"""
type: AccountType!
"""Имя аккаунта"""
username: String!
}
type OrganizationDetails {
"""ИНН"""
inn: String!
@@ -2850,6 +3183,9 @@ input PassportInput {
}
type Payment {
"""Аккаунт пользователя, совершающего платеж"""
account: Account!
"""Сумма платежа"""
amount: Float!
@@ -3296,7 +3632,7 @@ input RegisterAccountInput {
organization_data: CreateOrganizationDataInput
"""Публичный ключ"""
public_key: String
public_key: String!
"""Имя аккаунта реферера"""
referer: String
@@ -3358,6 +3694,20 @@ type RepresentedBy {
position: String!
}
type RepresentedByCertificate {
"""Имя"""
first_name: String!
"""Фамилия"""
last_name: String!
"""Отчество"""
middle_name: String!
"""Должность"""
position: String!
}
input RepresentedByInput {
based_on: String!
first_name: String!
@@ -3838,7 +4188,9 @@ type SignatureInfo {
signed_at: String!
signed_hash: String!
signer: String!
signer_info: UserDataUnion
"""Сертификат подписанта (сокращенная информация)"""
signer_certificate: UserCertificateUnion
}
input SignatureInfoInput {
@@ -3846,7 +4198,7 @@ input SignatureInfoInput {
id: Float!
"""Мета-данные подписи"""
meta: JSON!
meta: String!
"""Публичный ключ"""
public_key: String!
@@ -4236,8 +4588,8 @@ type UserAccount {
verifications: [Verification!]!
}
"""Объединение информации о пользователях"""
union UserDataUnion = Entrepreneur | Individual | Organization
"""Объединение сертификатов пользователей (сокращенная информация)"""
union UserCertificateUnion = EntrepreneurCertificate | IndividualCertificate | OrganizationCertificate
"""Статус пользователя"""
enum UserStatus {
+15 -9
View File
@@ -42,27 +42,32 @@ const envVarsSchema = z.object({
SMTP_USERNAME: z.string().default(''), // Пустая строка для необязательного значения
SMTP_PASSWORD: z.string().default(''), // Пустая строка для необязательного значения
EMAIL_FROM: z.string().default(''), // Пустая строка для необязательного значения
COOPNAME: z.string().default('default_coopname'), // Задаём дефолтное значение
GRAPHQL_SERVICE: z.string().default('http://localhost:4090').describe('адрес сервиса GRAPHQL'),
COOPNAME: z.string().min(1, { message: 'Не должно быть пустым' }).default('voskhod'), // Задаём дефолтное значение, пустая строка невалидна
TIMEZONE: z.string().min(1, { message: 'Не должно быть пустым' }).default('Europe/Moscow'), // Пустая строка невалидна
GRAPHQL_SERVICE: z
.string()
.min(1, { message: 'Не должно быть пустым' })
.default('http://localhost:4090')
.describe('адрес сервиса GRAPHQL'),
// Новые переменные для PostgreSQL
POSTGRES_HOST: z.string().default('127.0.0.1'),
POSTGRES_HOST: z.string().min(1, { message: 'Не должно быть пустым' }).default('127.0.0.1'),
POSTGRES_PORT: z
.string()
.default('5432')
.transform((val) => parseInt(val, 10)),
POSTGRES_USERNAME: z.string(),
POSTGRES_PASSWORD: z.string(),
POSTGRES_DATABASE: z.string(),
POSTGRES_USERNAME: z.string().min(1, { message: 'Не должно быть пустым' }),
POSTGRES_PASSWORD: z.string().min(1, { message: 'Не должно быть пустым' }),
POSTGRES_DATABASE: z.string().min(1, { message: 'Не должно быть пустым' }),
REDIS_HOST: z.string(),
REDIS_HOST: z.string().min(1, { message: 'Не должно быть пустым' }),
REDIS_PORT: z
.string()
.default('6379')
.transform((val) => parseInt(val, 10)),
REDIS_PASSWORD: z.string(),
BLOCKCHAIN_RPC: z.string(),
CHAIN_ID: z.string(),
BLOCKCHAIN_RPC: z.string().min(1, { message: 'Не должно быть пустым' }),
CHAIN_ID: z.string().min(1, { message: 'Не должно быть пустым' }),
});
// Валидация переменных окружения
@@ -86,6 +91,7 @@ export default {
base_url: envVars.data.BASE_URL,
port: envVars.data.PORT,
server_secret: envVars.data.SERVER_SECRET,
timezone: envVars.data.TIMEZONE,
blockchain: {
url: envVars.data.BLOCKCHAIN_RPC,
id: envVars.data.CHAIN_ID,
@@ -1,11 +1,18 @@
import { io, Socket } from 'socket.io-client';
import { IAction } from '../types/common';
import { SovietContract } from 'cooptypes';
import { wsService } from '../services';
import logger from '../config/logger';
import config from '../config/config';
import { MeetDomainInteractor } from '~/domain/meet/interactors/meet.interactor';
import type { MeetDecisionDomainInterface } from '~/domain/meet/interfaces/meet-decision-domain.interface';
import { DomainToBlockchainUtils } from '~/infrastructure/blockchain/utils/domain-to-blockchain.utils';
let clientSocket: Socket | undefined;
let meetInteractor: MeetDomainInteractor;
export const setMeetInteractor = (interactor: MeetDomainInteractor): void => {
meetInteractor = interactor;
};
// Инициация подключения
export const initSocketConnection = (serverUrl: string): void => {
@@ -20,7 +27,7 @@ export const initSocketConnection = (serverUrl: string): void => {
});
clientSocket.on('connect_error', (error: Error) => {
logger.error('Fail connect to notification server', { source: 'initSocketConnection' });
logger.error(`Fail connect to notification server: ${error.message}`, { source: 'initSocketConnection' });
});
};
@@ -42,5 +49,39 @@ async function processEvent(event: IAction) {
if (event.name === 'createboard') {
wsService.updateBoard(event.data);
}
if (event.name === 'newgdecision') {
// Обработка события о решении собрания
try {
// Преобразуем блокчейн-документ в формат ISignedDocumentDomainInterface
const decisionDocument = DomainToBlockchainUtils.convertChainDocumentToSignedDocument2(event.data.decision);
// Нормализуем числовые значения
const decisionData: MeetDecisionDomainInterface = {
...event.data,
signed_ballots: Number(event.data.signed_ballots),
quorum_percent: Number(event.data.quorum_percent),
results: event.data.results.map((item) => ({
...item,
question_id: Number(item.question_id),
number: Number(item.number),
votes_for: Number(item.votes_for),
votes_against: Number(item.votes_against),
votes_abstained: Number(item.votes_abstained),
})),
decision: decisionDocument, // Документ решения из блокчейна
};
// Используем интерактор для сохранения данных о завершенном собрании
await meetInteractor.processMeetDecision({
action: event,
decisionData,
});
logger.info(`Processed meet decision for ${event.data.hash}`, { source: 'processEvent' });
} catch (error: any) {
logger.error(`Error processing meet decision: ${error.message}`, { source: 'processEvent', error });
}
}
}
}
@@ -1,13 +1,21 @@
// domain/account/account-domain.module.ts
import { forwardRef, Module } from '@nestjs/common';
import { Global, Module } from '@nestjs/common';
import { AccountDomainInteractor } from './interactors/account.interactor';
import { AccountDomainService } from '~/domain/account/services/account-domain.service';
import { DocumentDomainModule } from '../document/document.module';
import { AccountDomainService, ACCOUNT_DOMAIN_SERVICE } from './services/account-domain.service';
import { InfrastructureModule } from '~/infrastructure/infrastructure.module';
@Global()
@Module({
imports: [forwardRef(() => DocumentDomainModule)],
providers: [AccountDomainService, AccountDomainInteractor],
exports: [AccountDomainInteractor, AccountDomainService],
imports: [InfrastructureModule],
providers: [
AccountDomainInteractor,
AccountDomainService,
{
provide: ACCOUNT_DOMAIN_SERVICE,
useExisting: AccountDomainService,
},
],
exports: [AccountDomainInteractor, AccountDomainService, ACCOUNT_DOMAIN_SERVICE],
})
export class AccountDomainModule {}
@@ -39,7 +39,23 @@ export class AccountDomainInteractor {
private readonly logger = new Logger(AccountDomainInteractor.name);
async updateAccount(data: UpdateAccountDomainInterface): Promise<AccountDomainEntity> {
const user = await userService.updateUserByUsername(data.username, data);
let user;
if (data.individual_data) {
const email = data.individual_data.email;
user = await userService.updateUserByUsername(data.username, { email });
this.individualRepository.create({ ...data.individual_data, username: data.username });
} else if (data.organization_data) {
const email = data.organization_data.email;
user = await userService.updateUserByUsername(data.username, { email });
this.organizationRepository.create({ ...data.organization_data, username: data.username });
} else if (data.entrepreneur_data) {
const email = data.entrepreneur_data.email;
user = await userService.updateUserByUsername(data.username, { email });
this.entrepreneurRepository.create({ ...data.entrepreneur_data, username: data.username });
} else {
throw new Error('Не получены входные данные для обновления');
}
const account = await this.getAccount(user.username);
const result = new AccountDomainEntity(account);
@@ -56,11 +72,11 @@ export class AccountDomainInteractor {
): Promise<IndividualDomainEntity | OrganizationDomainEntity | EntrepreneurDomainEntity> {
switch (accountType) {
case AccountType.individual:
return this.individualRepository.findByUsername(username);
return new IndividualDomainEntity(await this.individualRepository.findByUsername(username));
case AccountType.organization:
return this.organizationRepository.findByUsername(username);
return new OrganizationDomainEntity(await this.organizationRepository.findByUsername(username));
case AccountType.entrepreneur:
return this.entrepreneurRepository.findByUsername(username);
return new EntrepreneurDomainEntity(await this.entrepreneurRepository.findByUsername(username));
default:
throw new Error(`Неизвестный тип аккаунта: ${accountType}`);
}
@@ -12,7 +12,7 @@ import { ENTREPRENEUR_REPOSITORY, EntrepreneurRepository } from '~/domain/common
import { ORGANIZATION_REPOSITORY, OrganizationRepository } from '~/domain/common/repositories/organization.repository';
import { INDIVIDUAL_REPOSITORY, IndividualRepository } from '~/domain/common/repositories/individual.repository';
import type { PrivateAccountDomainInterface } from '../interfaces/private-account-domain.interface';
import type { AccountType } from '~/modules/account/enum/account-type.enum';
import { AccountType } from '~/modules/account/enum/account-type.enum';
import type { IndividualDomainInterface } from '~/domain/common/interfaces/individual-domain.interface';
import type { OrganizationDomainInterface } from '~/domain/common/interfaces/organization-domain.interface';
import type { EntrepreneurDomainInterface } from '~/domain/common/interfaces/entrepreneur-domain.interface';
@@ -48,6 +48,29 @@ export class AccountDomainService {
}
async getAccount(username: string): Promise<AccountDomainEntity> {
// Исключение для организации-кооператива
// Очень плохие решения из за того что мы содержим ТРИ разных репозитория под приватные данные и не знаем наверняка в каком искать данные заранее!
if (config.coopname === username) {
const user_account = await this.getUserAccount(username);
const blockchain_account = await this.getBlockchainAccount(username);
const participant_account = await this.getParticipantAccount(config.coopname, username);
const organization_data = await this.organizationRepository.findByUsername(username);
const private_account: PrivateAccountDomainInterface = {
type: AccountType.organization,
individual_data: undefined,
organization_data,
entrepreneur_data: undefined,
};
return new AccountDomainEntity({
username,
user_account,
blockchain_account,
provider_account: null,
participant_account,
private_account,
});
}
const user_account = await this.getUserAccount(username);
const blockchain_account = await this.getBlockchainAccount(username);
const participant_account = await this.getParticipantAccount(config.coopname, username);
@@ -130,3 +153,5 @@ export class AccountDomainService {
return null;
}
}
export const ACCOUNT_DOMAIN_SERVICE = Symbol('ACCOUNT_DOMAIN_SERVICE');
@@ -71,6 +71,10 @@ export class AgendaDomainInteractor {
});
const action = actionResponse?.results?.[0];
// if (action)
//TODO: здесь нужно добавить в action actor_certificate
if (action) {
agenda.push({
table: decision,
@@ -1,10 +1,6 @@
import type { BlockchainActionDomainInterface } from '../../common/interfaces/blockchain-action-domain.interface';
import type { Cooperative } from 'cooptypes';
import type { UserCertificateDomainInterface } from '~/domain/user-certificate/interfaces/user-certificate-domain.interface';
export interface ExtendedBlockchainActionDomainInterface extends BlockchainActionDomainInterface {
user?:
| Cooperative.Users.IIndividualData
| Cooperative.Users.IEntrepreneurData
| Cooperative.Users.IOrganizationData
| null;
actor_certificate?: UserCertificateDomainInterface | null;
}
@@ -48,7 +48,7 @@ export class BranchDomainInteractor {
for (const trusted of branch.trusted) {
const tr = await this.individualRepository.findByUsername(trusted);
trustedData.push(tr);
trustedData.push(new IndividualDomainEntity(tr));
}
const bankAccount = new BankPaymentMethodDTO(
@@ -1,12 +1,12 @@
import { EntrepreneurDomainEntity } from '~/domain/branch/entities/entrepreneur-domain.entity';
import type { EntrepreneurDomainInterface } from '../interfaces/entrepreneur-domain.interface';
export interface EntrepreneurRepository {
/**
* Получить данные об ИП по имени аккаунта
* @param username Имя аккаунта
*/
findByUsername(username: string): Promise<EntrepreneurDomainEntity>;
create(data: EntrepreneurDomainEntity);
findByUsername(username: string): Promise<EntrepreneurDomainInterface>;
create(data: EntrepreneurDomainInterface): Promise<void>;
}
export const ENTREPRENEUR_REPOSITORY = Symbol('EntrepreneurRepository'); // Создаем уникальный токен
@@ -1,12 +1,12 @@
import type { IndividualDomainEntity } from '~/domain/branch/entities/individual-domain.entity';
import type { IndividualDomainInterface } from '../interfaces/individual-domain.interface';
export interface IndividualRepository {
/**
* Получить данные о физлице по имени аккаунта
* @param username Имя аккаунта
*/
findByUsername(username: string): Promise<IndividualDomainEntity>;
create(data: IndividualDomainEntity);
findByUsername(username: string): Promise<IndividualDomainInterface>;
create(data: IndividualDomainInterface): Promise<void>;
}
export const INDIVIDUAL_REPOSITORY = Symbol('IndividualRepository'); // Создаем уникальный токен
@@ -1,12 +1,12 @@
import type { OrganizationDomainEntity } from '~/domain/branch/entities/organization-domain.entity';
import type { OrganizationDomainInterface } from '../interfaces/organization-domain.interface';
export interface OrganizationRepository {
/**
* Получить данные об организации по имени кооператива
* @param username Имя кооператива
*/
findByUsername(username: string): Promise<OrganizationDomainEntity>;
create(data: OrganizationDomainEntity);
findByUsername(username: string): Promise<OrganizationDomainInterface>;
create(data: OrganizationDomainInterface): Promise<void>;
}
export const ORGANIZATION_REPOSITORY = Symbol('OrganizationRepository'); // Создаем уникальный токен
@@ -1,7 +1,11 @@
import { Injectable } from '@nestjs/common';
import { Inject, Injectable, forwardRef } from '@nestjs/common';
import { DocumentAggregator } from './document.aggregator';
import { DocumentPackageUtils } from './document-package-utils.aggregator';
import { AccountDomainService } from '~/domain/account/services/account-domain.service';
import { AccountDomainService, ACCOUNT_DOMAIN_SERVICE } from '~/domain/account/services/account-domain.service';
import {
UserCertificateDomainService,
USER_CERTIFICATE_DOMAIN_SERVICE,
} from '~/domain/user-certificate/services/user-certificate-domain.service';
import { Cooperative, SovietContract } from 'cooptypes';
import { getActions } from '~/utils/getFetch';
import type { DocumentPackageAggregateDomainInterface } from '../interfaces/document-package-aggregate-domain.interface';
@@ -9,13 +13,15 @@ import type { StatementDetailAggregateDomainInterface } from '../interfaces/stat
import type { DecisionDetailAggregateDomainInterface } from '../interfaces/decision-detail-aggregate-domain.interface';
import type { DocumentDomainAggregate } from '../aggregates/document-domain.aggregate';
import type { ISignedDocumentDomainInterface } from '../interfaces/signed-document-domain.interface';
import type { ExtendedBlockchainActionDomainInterface } from '~/domain/agenda/interfaces/extended-blockchain-action-domain.interface';
@Injectable()
export class DocumentPackageV0Aggregator {
constructor(
private readonly documentAggregator: DocumentAggregator,
private readonly documentPackageUtils: DocumentPackageUtils,
private readonly accountDomainService: AccountDomainService
@Inject(forwardRef(() => ACCOUNT_DOMAIN_SERVICE)) private readonly accountDomainService: AccountDomainService,
@Inject(USER_CERTIFICATE_DOMAIN_SERVICE) private readonly userCertificateService: UserCertificateDomainService
) {}
/**
@@ -93,9 +99,11 @@ export class DocumentPackageV0Aggregator {
const account = await this.accountDomainService.getPrivateAccount(rawData.username);
if (account && mainDocument) {
const extendedAction: Cooperative.Blockchain.IExtendedAction = {
const actor_certificate = this.userCertificateService.createCertificateFromUserData(account);
const extendedAction: ExtendedBlockchainActionDomainInterface = {
...rawAction,
user: account,
actor_certificate,
};
const signedMainDoc: ISignedDocumentDomainInterface = {
@@ -156,9 +164,11 @@ export class DocumentPackageV0Aggregator {
const account = await this.accountDomainService.getPrivateAccount(decisionAction.data?.username);
if (!account) return null;
const extendedDecisionAction: Cooperative.Blockchain.IExtendedAction = {
const actor_certificate = this.userCertificateService.createCertificateFromUserData(account);
const extendedDecisionAction: ExtendedBlockchainActionDomainInterface = {
...decisionAction,
user: account,
actor_certificate,
};
const decisionDocument = await this.documentPackageUtils.getDocumentByHash(decisionAction?.data?.document?.hash);
@@ -1,7 +1,11 @@
import { Injectable } from '@nestjs/common';
import { Inject, Injectable, forwardRef } from '@nestjs/common';
import { DocumentAggregator } from './document.aggregator';
import { DocumentPackageUtils } from './document-package-utils.aggregator';
import { AccountDomainService } from '~/domain/account/services/account-domain.service';
import { AccountDomainService, ACCOUNT_DOMAIN_SERVICE } from '~/domain/account/services/account-domain.service';
import {
UserCertificateDomainService,
USER_CERTIFICATE_DOMAIN_SERVICE,
} from '~/domain/user-certificate/services/user-certificate-domain.service';
import { Cooperative, SovietContract } from 'cooptypes';
import { getActions } from '~/utils/getFetch';
import type { DocumentPackageAggregateDomainInterface } from '../interfaces/document-package-aggregate-domain.interface';
@@ -9,13 +13,15 @@ import type { StatementDetailAggregateDomainInterface } from '../interfaces/stat
import type { DecisionDetailAggregateDomainInterface } from '../interfaces/decision-detail-aggregate-domain.interface';
import type { DocumentDomainAggregate } from '../aggregates/document-domain.aggregate';
import type { ISignedDocumentDomainInterface } from '../interfaces/signed-document-domain.interface';
import type { ExtendedBlockchainActionDomainInterface } from '~/domain/agenda/interfaces/extended-blockchain-action-domain.interface';
@Injectable()
export class DocumentPackageV1Aggregator {
constructor(
private readonly documentAggregator: DocumentAggregator,
private readonly documentPackageUtils: DocumentPackageUtils,
private readonly accountDomainService: AccountDomainService
@Inject(forwardRef(() => ACCOUNT_DOMAIN_SERVICE)) private readonly accountDomainService: AccountDomainService,
@Inject(USER_CERTIFICATE_DOMAIN_SERVICE) private readonly userCertificateService: UserCertificateDomainService
) {}
/**
@@ -29,7 +35,6 @@ export class DocumentPackageV1Aggregator {
// Инициализация частей
let statementDetail: StatementDetailAggregateDomainInterface | null = null;
let decisionDetail: DecisionDetailAggregateDomainInterface | null = null;
const actDetails = [];
const links: DocumentDomainAggregate[] = [];
// Обработка данных в новом формате
@@ -41,6 +46,12 @@ export class DocumentPackageV1Aggregator {
// Создаем решение (Decision)
decisionDetail = await this.buildDecisionV1(rawData, links);
// Создаем акты (Acts)
const actDetails = await this.buildActsV1(rawData, links);
// Собираем дополнительные связанные документы
await this.buildLinksV1(rawData, links);
// Возвращаем итоговый объект
return {
statement: statementDetail,
@@ -105,9 +116,11 @@ export class DocumentPackageV1Aggregator {
const account = await this.accountDomainService.getPrivateAccount(rawData.username);
if (!account) return null;
const extendedAction: Cooperative.Blockchain.IExtendedAction = {
const actor_certificate = this.userCertificateService.createCertificateFromUserData(account);
const extendedAction: ExtendedBlockchainActionDomainInterface = {
...rawAction,
user: account,
actor_certificate,
};
// signatures уже есть в rawData.document
@@ -158,9 +171,11 @@ export class DocumentPackageV1Aggregator {
const account = await this.accountDomainService.getPrivateAccount(decisionAction.data?.username);
if (!account) return null;
const extendedDecisionAction: Cooperative.Blockchain.IExtendedAction = {
const actor_certificate = this.userCertificateService.createCertificateFromUserData(account);
const extendedDecisionAction: ExtendedBlockchainActionDomainInterface = {
...decisionAction,
user: account,
actor_certificate,
};
const decisionDocument = await this.documentPackageUtils.getDocumentByHash(decisionAction?.data?.document?.doc_hash);
@@ -225,4 +240,136 @@ export class DocumentPackageV1Aggregator {
return null;
}
/**
* Создает акты (Acts) для версии 1+
* @param rawData Данные действия
* @param links Массив связанных документов
* @returns Массив деталей актов
*/
private async buildActsV1(
rawData: SovietContract.Actions.Registry.NewSubmitted.INewSubmitted,
links: DocumentDomainAggregate[]
): Promise<DecisionDetailAggregateDomainInterface[]> {
const actsActionResponse = await getActions(`${process.env.SIMPLE_EXPLORER_API}/get-actions`, {
filter: JSON.stringify({
account: SovietContract.contractName.production,
name: SovietContract.Actions.Registry.NewAct.actionName,
receiver: process.env.COOPNAME,
'data.package': String(rawData.package.toUpperCase()),
}),
page: 1,
limit: 100, // Получаем до 100 актов для пакета
});
const actsResults = actsActionResponse?.results || [];
const actsDetails: DecisionDetailAggregateDomainInterface[] = [];
for (const actAction of actsResults) {
const account = await this.accountDomainService.getPrivateAccount(actAction.data?.username);
if (!account) continue;
const actor_certificate = this.userCertificateService.createCertificateFromUserData(account);
const extendedActAction: ExtendedBlockchainActionDomainInterface = {
...actAction,
actor_certificate,
};
const actDocument = await this.documentPackageUtils.getDocumentByHash(actAction?.data?.document?.doc_hash);
if (!actDocument) continue;
if (actDocument?.meta?.links && Array.isArray(actDocument.meta.links)) {
for (const linkHash of actDocument.meta.links) {
const linkedDoc = await this.documentPackageUtils.getDocumentByHash(linkHash);
if (linkedDoc) {
// поиск соглашения по документу
const rawAgreement = await getActions(`${process.env.SIMPLE_EXPLORER_API}/get-actions`, {
filter: JSON.stringify({
account: SovietContract.contractName.production,
name: SovietContract.Actions.Registry.NewAgreement.actionName,
receiver: process.env.COOPNAME,
'data.document.doc_hash': String(linkHash.toUpperCase()),
}),
page: 1,
limit: 1,
});
const agreement: SovietContract.Actions.Registry.NewAgreement.INewAgreement = rawAgreement?.results?.[0]?.data;
if (agreement) {
const signedLinkedDoc: ISignedDocumentDomainInterface = {
version: actAction.data?.document?.version,
hash: linkedDoc.hash,
doc_hash: linkedDoc.hash,
meta_hash: agreement.document.meta_hash,
meta: JSON.parse(agreement.document.meta),
signatures: agreement.document.signatures || [],
};
const linkAggregate = await this.documentAggregator.buildDocumentAggregate(signedLinkedDoc);
if (linkAggregate) {
links.push(linkAggregate);
}
}
}
}
}
// signatures уже есть в actAction.data.document
const signedActDoc: ISignedDocumentDomainInterface = {
version: actAction.data?.document?.version,
hash: actAction.data?.document?.hash,
doc_hash: actAction.data?.document?.doc_hash,
meta_hash: actAction.data?.document?.meta_hash,
meta: actDocument.meta,
signatures: actAction.data?.document?.signatures || [],
};
const documentAggregate = await this.documentAggregator.buildDocumentAggregate(signedActDoc);
if (documentAggregate) {
actsDetails.push({
action: extendedActAction,
documentAggregate,
votes_for: [],
votes_against: [],
});
}
}
return actsDetails;
}
/**
* Собирает связанные документы (Links) для версии 1+
* @param rawData Данные действия
* @param links Массив связанных документов
*/
private async buildLinksV1(
rawData: SovietContract.Actions.Registry.NewSubmitted.INewSubmitted,
links: DocumentDomainAggregate[]
): Promise<void> {
const linksActionResponse = await getActions(`${process.env.SIMPLE_EXPLORER_API}/get-actions`, {
filter: JSON.stringify({
account: SovietContract.contractName.production,
name: SovietContract.Actions.Registry.NewLink.actionName,
receiver: process.env.COOPNAME,
'data.package': String(rawData.package.toUpperCase()),
}),
page: 1,
limit: 10000, // Получаем до 10000 связанных документов для пакета
});
const linksResults = linksActionResponse?.results || [];
for (const linkAction of linksResults) {
const linkDocument = await this.documentPackageUtils.getDocumentByHash(linkAction?.data?.document?.doc_hash);
if (!linkDocument) continue;
const linkAggregate = await this.documentAggregator.buildDocumentAggregate(linkAction?.data?.document);
if (linkAggregate) {
links.push(linkAggregate);
}
}
}
}
@@ -1,7 +1,7 @@
import { Injectable, Inject } from '@nestjs/common';
import { Injectable, Inject, forwardRef } from '@nestjs/common';
import { DOCUMENT_REPOSITORY, DocumentRepository } from '../repository/document.repository';
import { DocumentAggregator } from './document.aggregator';
import { AccountDomainService } from '~/domain/account/services/account-domain.service';
import { AccountDomainService, ACCOUNT_DOMAIN_SERVICE } from '~/domain/account/services/account-domain.service';
import { Cooperative } from 'cooptypes';
import type { DocumentPackageAggregateDomainInterface } from '../interfaces/document-package-aggregate-domain.interface';
import { DocumentPackageV0Aggregator } from './document-package-v0.aggregator';
@@ -13,7 +13,7 @@ export class DocumentPackageAggregator {
constructor(
@Inject(DOCUMENT_REPOSITORY) private readonly documentRepository: DocumentRepository,
private readonly documentAggregator: DocumentAggregator,
private readonly accountDomainService: AccountDomainService,
@Inject(forwardRef(() => ACCOUNT_DOMAIN_SERVICE)) private readonly accountDomainService: AccountDomainService,
private readonly documentPackageV0Aggregator: DocumentPackageV0Aggregator,
private readonly documentPackageV1Aggregator: DocumentPackageV1Aggregator,
private readonly documentPackageUtils: DocumentPackageUtils
@@ -28,8 +28,6 @@ export class DocumentPackageAggregator {
async buildDocumentPackageAggregate(
rawAction: Cooperative.Blockchain.IAction
): Promise<DocumentPackageAggregateDomainInterface> {
console.log('rawAction', rawAction);
// Определяем версию документа
const docVersion = this.determineDocumentVersion(rawAction);
@@ -1,21 +1,28 @@
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { BadRequestException, Inject, Injectable, forwardRef } from '@nestjs/common';
import { DOCUMENT_REPOSITORY, DocumentRepository } from '../repository/document.repository';
import { DocumentDomainAggregate } from '../aggregates/document-domain.aggregate';
import { DocumentDomainEntity } from '../entity/document-domain.entity';
import { AccountDomainService } from '~/domain/account/services/account-domain.service';
import type { DocumentMetaDomainInterface } from '../interfaces/document-meta-domain.interface';
import { AccountDomainService, ACCOUNT_DOMAIN_SERVICE } from '~/domain/account/services/account-domain.service';
import {
ExtendedSignedDocumentDomainInterface,
SignatureInfoDomainInterface,
} from '../interfaces/extended-signed-document-domain.interface';
import type { ISignedDocumentDomainInterface } from '../interfaces/signed-document-domain.interface';
import { Classes } from '@coopenomics/sdk';
import {
UserCertificateDomainService,
USER_CERTIFICATE_DOMAIN_SERVICE,
} from '~/domain/user-certificate/services/user-certificate-domain.service';
@Injectable()
export class DocumentAggregator {
constructor(
@Inject(DOCUMENT_REPOSITORY) private readonly documentRepository: DocumentRepository,
private readonly accountDomainService: AccountDomainService
@Inject(forwardRef(() => ACCOUNT_DOMAIN_SERVICE)) private readonly accountDomainService: AccountDomainService,
@Inject(USER_CERTIFICATE_DOMAIN_SERVICE) private readonly userCertificateService: UserCertificateDomainService
) {}
private readonly EMPTY_HASH = '0000000000000000000000000000000000000000000000000000000000000000';
/**
* Создает агрегатор документов на основе полного документа и подписанного документа
@@ -25,12 +32,9 @@ export class DocumentAggregator {
*/
public async buildDocumentAggregate(signedDoc: ISignedDocumentDomainInterface): Promise<DocumentDomainAggregate | null> {
// Получаем полный документ по хешу
const document = await this.getDocumentByHash(signedDoc.doc_hash);
if (signedDoc.doc_hash === this.EMPTY_HASH) return null;
// Проверяем, что в метаданных документа есть username
if (!signedDoc.meta || !signedDoc.meta.username) {
throw new BadRequestException('В документе отсутствует информация о пользователе (username)');
}
const document = await this.getDocumentByHash(signedDoc.doc_hash);
// Массив для хранения информации о подписантах
const signatureInfos: SignatureInfoDomainInterface[] = [];
@@ -41,6 +45,9 @@ export class DocumentAggregator {
// Получаем данные подписанта на основе signer из подписи
const signer = await this.accountDomainService.getPrivateAccount(signature.signer);
// Создаем сертификат подписанта из полной информации
const signerCertificate = this.userCertificateService.createCertificateFromUserData(signer);
// Формируем объект информации о подписи
const signatureInfo: SignatureInfoDomainInterface = {
id: signature.id,
@@ -50,7 +57,7 @@ export class DocumentAggregator {
signature: signature.signature,
signed_at: signature.signed_at,
is_valid: Classes.Document.validateSignature(signature),
signer_info: signer,
signer_certificate: signerCertificate,
meta: signature.meta,
};
@@ -1,16 +1,18 @@
import { forwardRef, Module } from '@nestjs/common';
import { Global, Module } from '@nestjs/common';
import { DocumentDomainInteractor } from './interactors/document.interactor';
import { InfrastructureModule } from '~/infrastructure/infrastructure.module';
import { DocumentDomainService } from './services/document-domain.service';
import { DocumentAggregator } from './aggregators/document.aggregator';
import { AccountDomainModule } from '~/domain/account/account-domain.module';
import { DocumentPackageAggregator } from './aggregators/document-package.aggregator';
import { DocumentPackageUtils } from './aggregators/document-package-utils.aggregator';
import { DocumentPackageV0Aggregator } from './aggregators/document-package-v0.aggregator';
import { DocumentPackageV1Aggregator } from './aggregators/document-package-v1.aggregator';
import { DocumentAggregationService } from './services/document-aggregation.service';
import { UserCertificateDomainModule } from '~/domain/user-certificate/user-certificate.module';
@Global()
@Module({
imports: [InfrastructureModule, forwardRef(() => AccountDomainModule)],
imports: [InfrastructureModule, UserCertificateDomainModule],
providers: [
DocumentDomainInteractor,
DocumentDomainService,
@@ -19,6 +21,7 @@ import { DocumentPackageV1Aggregator } from './aggregators/document-package-v1.a
DocumentPackageUtils,
DocumentPackageV0Aggregator,
DocumentPackageV1Aggregator,
DocumentAggregationService,
],
exports: [
DocumentDomainInteractor,
@@ -28,6 +31,7 @@ import { DocumentPackageV1Aggregator } from './aggregators/document-package-v1.a
DocumentPackageUtils,
DocumentPackageV0Aggregator,
DocumentPackageV1Aggregator,
DocumentAggregationService,
],
})
export class DocumentDomainModule {}
@@ -9,13 +9,17 @@ import { toDotNotation } from '~/utils/toDotNotation';
import { getActions } from '~/utils/getFetch';
import { SovietContract } from 'cooptypes';
import { DocumentPackageAggregator } from '../aggregators/document-package.aggregator';
import { DocumentAggregator } from '../aggregators/document.aggregator';
import type { DocumentAggregateDomainInterface } from '../interfaces/document-domain-aggregate.interface';
import type { ISignedDocumentDomainInterface } from '../interfaces/signed-document-domain.interface';
@Injectable()
export class DocumentDomainInteractor {
constructor(
private readonly documentDomainService: DocumentDomainService,
@Inject(forwardRef(() => DocumentPackageAggregator))
private readonly documentPackageAggregator: DocumentPackageAggregator
private readonly documentPackageAggregator: DocumentPackageAggregator,
private readonly documentAggregator: DocumentAggregator
) {}
public async generateDocument(data: GenerateDocumentDomainInterfaceWithOptions): Promise<DocumentDomainEntity> {
@@ -46,4 +50,17 @@ export class DocumentDomainInteractor {
return response;
}
/**
* Строит агрегат документа
* @param signedDocument Подписанный документ
* @returns Агрегат документа или null
*/
async buildDocumentAggregate(
signedDocument: ISignedDocumentDomainInterface | null | undefined
): Promise<DocumentAggregateDomainInterface | null> {
if (!signedDocument) return null;
return await this.documentAggregator.buildDocumentAggregate(signedDocument);
}
}
@@ -1,7 +1,7 @@
import type { Cooperative } from 'cooptypes';
import type { ExtendedBlockchainActionDomainInterface } from '~/domain/agenda/interfaces/extended-blockchain-action-domain.interface';
import type { DocumentAggregateDomainInterface } from './document-domain-aggregate.interface';
export interface ActDetailAggregateDomainInterface {
action: Cooperative.Blockchain.IExtendedAction;
action: ExtendedBlockchainActionDomainInterface;
documentAggregate: DocumentAggregateDomainInterface;
}
@@ -1,9 +1,9 @@
import type { Cooperative } from 'cooptypes';
import type { ExtendedBlockchainActionDomainInterface } from '~/domain/agenda/interfaces/extended-blockchain-action-domain.interface';
import type { DocumentAggregateDomainInterface } from './document-domain-aggregate.interface';
export interface DecisionDetailAggregateDomainInterface {
action: Cooperative.Blockchain.IExtendedAction;
action: ExtendedBlockchainActionDomainInterface;
documentAggregate: DocumentAggregateDomainInterface;
votes_for: Cooperative.Blockchain.IExtendedAction[];
votes_against: Cooperative.Blockchain.IExtendedAction[];
votes_for: ExtendedBlockchainActionDomainInterface[];
votes_against: ExtendedBlockchainActionDomainInterface[];
}
@@ -1,12 +1,10 @@
import type { EntrepreneurDomainInterface } from '~/domain/common/interfaces/entrepreneur-domain.interface';
import type { IndividualDomainInterface } from '~/domain/common/interfaces/individual-domain.interface';
import type { OrganizationDomainInterface } from '~/domain/common/interfaces/organization-domain.interface';
import type { ISignedDocumentDomainInterface } from './signed-document-domain.interface';
import type { ISignatureInfoDomainInterface } from './signature-info-domain.interface';
import type { UserCertificateDomainInterface } from '~/domain/user-certificate/interfaces/user-certificate-domain.interface';
export interface SignatureInfoDomainInterface extends ISignatureInfoDomainInterface {
is_valid?: boolean;
signer_info?: IndividualDomainInterface | OrganizationDomainInterface | EntrepreneurDomainInterface | null;
signer_certificate?: UserCertificateDomainInterface | null;
}
export interface ExtendedSignedDocumentDomainInterface extends ISignedDocumentDomainInterface {
@@ -1,7 +1,7 @@
import type { Cooperative } from 'cooptypes';
import type { ExtendedBlockchainActionDomainInterface } from '~/domain/agenda/interfaces/extended-blockchain-action-domain.interface';
import type { DocumentAggregateDomainInterface } from './document-domain-aggregate.interface';
export interface StatementDetailAggregateDomainInterface {
action: Cooperative.Blockchain.IExtendedAction;
action: ExtendedBlockchainActionDomainInterface;
documentAggregate: DocumentAggregateDomainInterface;
}
@@ -0,0 +1,32 @@
import { Injectable } from '@nestjs/common';
import { DocumentAggregator } from '../aggregators/document.aggregator';
import type { ISignedDocumentDomainInterface } from '../interfaces/signed-document-domain.interface';
import type { DocumentDomainAggregate } from '../aggregates/document-domain.aggregate';
/**
* Сервис для агрегации документов в доменном слое
* Выделен в отдельный сервис для избежания дублирования кода
* и соблюдения принципов чистой архитектуры
*/
@Injectable()
export class DocumentAggregationService {
// Пустой хеш для проверки документов
private readonly EMPTY_HASH = '0000000000000000000000000000000000000000000000000000000000000000';
constructor(private readonly documentAggregator: DocumentAggregator) {}
/**
* Создает агрегат документа из формата ISignedDocumentDomainInterface
* @param signedDocument Подписанный документ
* @returns Агрегат документа
*/
async buildDocumentAggregate(signedDocument: ISignedDocumentDomainInterface): Promise<DocumentDomainAggregate | null> {
if (!signedDocument) return null;
// Проверяем, что хеш документа не пустой
if (signedDocument.doc_hash === this.EMPTY_HASH) return null;
// Используем существующий агрегатор для сборки агрегата
return this.documentAggregator.buildDocumentAggregate(signedDocument);
}
}
@@ -59,6 +59,7 @@ export class DocumentDomainService {
page,
limit,
});
return response;
}
}
@@ -16,6 +16,8 @@ import { AgendaDomainModule } from './agenda/agenda-domain.module';
import { CooplaceDomainModule } from './cooplace/cooplace.module';
import { DesktopDomainModule } from './desktop/desktop-domain.module';
import { MeetDomainModule } from './meet/meet-domain.module';
import { ControllerWsMeetModule } from './meet/controllers-ws-meet.module';
import { UserCertificateDomainModule } from './user-certificate/user-certificate.module';
@Module({
imports: [
@@ -35,6 +37,8 @@ import { MeetDomainModule } from './meet/meet-domain.module';
ParticipantDomainModule,
CooplaceDomainModule,
MeetDomainModule,
ControllerWsMeetModule,
UserCertificateDomainModule,
],
exports: [
AuthDomainModule,
@@ -53,6 +57,7 @@ import { MeetDomainModule } from './meet/meet-domain.module';
ParticipantDomainModule,
CooplaceDomainModule,
MeetDomainModule,
UserCertificateDomainModule,
],
})
export class DomainModule {}
@@ -0,0 +1,25 @@
import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { AccountExtensionPort } from '../ports/account-extension-port';
import { AccountDomainInteractor } from '~/domain/account/interactors/account.interactor';
import { AccountDomainEntity } from '~/domain/account/entities/account-domain.entity';
import { PaginationInputDTO } from '~/modules/common/dto/pagination.dto';
import { PaginationResultDomainInterface } from '~/domain/common/interfaces/pagination.interface';
import type { GetAccountsInputDomainInterface } from '~/domain/account/interfaces/get-accounts-input.interface';
@Injectable()
export class AccountExtensionAdapter implements AccountExtensionPort {
constructor(
@Inject(forwardRef(() => AccountDomainInteractor)) private readonly accountInteractor: AccountDomainInteractor
) {}
async getAccounts(
data: GetAccountsInputDomainInterface = {},
options?: PaginationInputDTO
): Promise<PaginationResultDomainInterface<AccountDomainEntity>> {
return this.accountInteractor.getAccounts(data, options);
}
async getAccount(username: string): Promise<AccountDomainEntity> {
return this.accountInteractor.getAccount(username);
}
}
@@ -0,0 +1,19 @@
import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { MeetExtensionPort } from '../ports/meet-extension-port';
import { MeetDomainInteractor } from '~/domain/meet/interactors/meet.interactor';
import type { MeetAggregate } from '~/domain/meet/aggregates/meet-domain.aggregate';
import { GetMeetInputDomainInterface } from '~/domain/meet/interfaces/get-meet-input-domain.interface';
import { GetMeetsInputDomainInterface } from '~/domain/meet/interfaces/get-meets-input-domain.interface';
@Injectable()
export class MeetExtensionAdapter implements MeetExtensionPort {
constructor(@Inject(forwardRef(() => MeetDomainInteractor)) private readonly meetInteractor: MeetDomainInteractor) {}
async getMeets(data: GetMeetsInputDomainInterface, username?: string): Promise<MeetAggregate[]> {
return this.meetInteractor.getMeets(data, username);
}
async getMeet(data: GetMeetInputDomainInterface, username?: string): Promise<MeetAggregate> {
return this.meetInteractor.getMeet(data, username);
}
}
@@ -9,10 +9,12 @@ import { ExtensionDomainListingInteractor } from './interactors/extension-listin
import { ExtensionsModule } from '~/extensions/extensions.module';
import { nestApp } from '~/index';
import { ExtensionPortsModule } from './extension-ports.module';
@Module({
imports: [
ExtensionsModule.register(), // Регистрируем модуль расширений с доступом ко всем плагинам
ExtensionsModule.register(), // Регистрируем модуль расширений
ExtensionPortsModule, // Импортируем модуль с портами
],
providers: [
ExtensionDomainInteractor,
@@ -27,6 +29,7 @@ import { nestApp } from '~/index';
ExtensionDomainService,
ExtensionDomainListingService,
ExtensionDomainListingInteractor,
ExtensionPortsModule, // Экспортируем модуль с портами
], // Экспортируем интерактор и сервис для использования в других модулях
})
export class ExtensionDomainModule {
@@ -0,0 +1,29 @@
import { forwardRef, Module } from '@nestjs/common';
import { AccountExtensionAdapter } from './adapters/account-extension.adapter';
import { MeetExtensionAdapter } from './adapters/meet-extension.adapter';
import { ACCOUNT_EXTENSION_PORT } from './ports/account-extension-port';
import { MEET_EXTENSION_PORT } from './ports/meet-extension-port';
import { MeetDomainModule } from '~/domain/meet/meet-domain.module';
/**
* Модуль для предоставления портов расширениям
* Этот модуль не имеет зависимостей от других модулей для избежания циклических зависимостей
* AccountDomainModule импортировать не нужно, так как он глобальный
*/
@Module({
imports: [forwardRef(() => MeetDomainModule)],
providers: [
AccountExtensionAdapter,
MeetExtensionAdapter,
{
provide: ACCOUNT_EXTENSION_PORT,
useExisting: AccountExtensionAdapter,
},
{
provide: MEET_EXTENSION_PORT,
useExisting: MeetExtensionAdapter,
},
],
exports: [ACCOUNT_EXTENSION_PORT, MEET_EXTENSION_PORT],
})
export class ExtensionPortsModule {}
@@ -0,0 +1,15 @@
import { AccountDomainEntity } from '~/domain/account/entities/account-domain.entity';
import { PaginationInputDTO } from '~/modules/common/dto/pagination.dto';
import { PaginationResultDomainInterface } from '~/domain/common/interfaces/pagination.interface';
import type { GetAccountsInputDomainInterface } from '~/domain/account/interfaces/get-accounts-input.interface';
export interface AccountExtensionPort {
getAccounts(
data: GetAccountsInputDomainInterface,
options?: PaginationInputDTO
): Promise<PaginationResultDomainInterface<AccountDomainEntity>>;
getAccount(username: string): Promise<AccountDomainEntity>;
}
export const ACCOUNT_EXTENSION_PORT = Symbol('AccountExtensionPort');
@@ -0,0 +1,10 @@
import type { MeetAggregate } from '~/domain/meet/aggregates/meet-domain.aggregate';
import { GetMeetInputDomainInterface } from '~/domain/meet/interfaces/get-meet-input-domain.interface';
import { GetMeetsInputDomainInterface } from '~/domain/meet/interfaces/get-meets-input-domain.interface';
export interface MeetExtensionPort {
getMeets(data: GetMeetsInputDomainInterface, username?: string): Promise<MeetAggregate[]>;
getMeet(data: GetMeetInputDomainInterface, username?: string): Promise<MeetAggregate>;
}
export const MEET_EXTENSION_PORT = Symbol('MeetExtensionPort');
@@ -3,9 +3,9 @@ import type { MeetPreProcessingDomainEntity } from '../entities/meet-pre-domain.
import type { MeetProcessedDomainEntity } from '../entities/meet-processed-domain.entity';
import type { MeetProcessingDomainEntity } from '../entities/meet-processing-domain.entity';
import { assertSingleHash } from '~/shared/asserts/single-hash.assert';
import type { MeetDomainAggregate } from '../interfaces/meet-aggregate.interface';
import type { MeetDomainAggregateDomainInterface } from '../interfaces/meet-aggregate.interface';
export class MeetAggregate implements MeetDomainAggregate {
export class MeetAggregate implements MeetDomainAggregateDomainInterface {
public readonly hash: string;
constructor(
@@ -0,0 +1,23 @@
import { Module } from '@nestjs/common';
import { MeetDomainInteractor } from './interactors/meet.interactor';
import { DocumentDomainModule } from '../document/document.module';
import { InfrastructureModule } from '~/infrastructure/infrastructure.module';
import { setMeetInteractor } from '~/controllers/ws.controller';
@Module({
imports: [DocumentDomainModule, InfrastructureModule],
providers: [
MeetDomainInteractor,
{
provide: 'INIT_WS_CONTROLLER',
useFactory: (meetInteractor: MeetDomainInteractor) => {
// Устанавливаем интерактор для использования в WS контроллере
setMeetInteractor(meetInteractor);
return true;
},
inject: [MeetDomainInteractor],
},
],
exports: [MeetDomainInteractor],
})
export class ControllerWsMeetModule {}
@@ -1,13 +1,13 @@
import type { MeetPreProcessingDomainInterface } from '../interfaces/meet-pre-domain.interface';
import type { DocumentAggregateDomainInterface } from '~/domain/document/interfaces/document-domain-aggregate.interface';
import type { AgendaMeetPointInputDomainInterface } from '../interfaces/agenda-meet-point-input-domain.interface';
import type { AgendaGeneralMeetPointInputDomainInterface } from '../interfaces/agenda-meet-point-input-domain.interface';
export class MeetPreProcessingDomainEntity implements MeetPreProcessingDomainInterface {
public readonly coopname!: string;
public readonly hash!: string;
public readonly initiator!: string;
public readonly presider!: string;
public readonly secretary!: string;
public readonly agenda!: AgendaMeetPointInputDomainInterface[];
public readonly agenda!: AgendaGeneralMeetPointInputDomainInterface[];
public readonly open_at!: Date;
public readonly close_at!: Date;
public readonly proposal?: DocumentAggregateDomainInterface;
@@ -1,12 +1,37 @@
import type { MeetContract } from 'cooptypes';
import type { MeetProcessedDomainInterface } from '../interfaces/meet-processed-domain.interface';
import type { BlockchainActionDomainInterface } from '~/domain/common/interfaces/blockchain-action-domain.interface';
import type { ISignedDocumentDomainInterface } from '~/domain/document/interfaces/signed-document-domain.interface';
import type { UserCertificateDomainInterface } from '~/domain/user-certificate/interfaces/user-certificate-domain.interface';
export class MeetProcessedDomainEntity implements MeetProcessedDomainInterface {
public readonly coopname!: string;
public readonly hash!: string;
public readonly decision!: BlockchainActionDomainInterface<MeetContract.Actions.NewDecision.IInput>;
public readonly presider!: string;
public readonly secretary!: string;
public readonly presider_certificate?: UserCertificateDomainInterface | null;
public readonly secretary_certificate?: UserCertificateDomainInterface | null;
public readonly results!: any[];
public readonly signed_ballots!: number;
public readonly quorum_percent!: number;
public readonly quorum_passed!: boolean;
public readonly decision!: ISignedDocumentDomainInterface;
constructor(data: MeetProcessedDomainInterface) {
Object.assign(this, data);
constructor(
data: Partial<MeetProcessedDomainInterface> & {
decision: ISignedDocumentDomainInterface;
presider: string;
secretary: string;
}
) {
this.coopname = data.coopname || '';
this.hash = data.hash || '';
this.presider = data.presider;
this.secretary = data.secretary;
this.presider_certificate = data.presider_certificate || null;
this.secretary_certificate = data.secretary_certificate || null;
this.results = data.results || [];
this.signed_ballots = data.signed_ballots || 0;
this.quorum_percent = data.quorum_percent || 0;
this.quorum_passed = data.quorum_passed || false;
this.decision = data.decision;
}
}
@@ -1,12 +1,80 @@
import type { MeetProcessingDomainInterface } from '../interfaces/meet-processing-domain.interface';
import type { MeetRowProcessingDomainInterface } from '../interfaces/meet-row-processing-domain.interface';
import type { QuestionRowProcessingDomainInterface } from '../interfaces/question-row-processing-domain.interface';
import { ExtendedMeetStatus } from '../enums/extended-meet-status.enum';
export class MeetProcessingDomainEntity implements MeetProcessingDomainInterface {
public readonly hash!: string;
public readonly meet!: MeetRowProcessingDomainInterface;
public readonly questions!: QuestionRowProcessingDomainInterface[];
public readonly isVoted: boolean = false;
public readonly extendedStatus: ExtendedMeetStatus = ExtendedMeetStatus.NONE;
constructor(data: MeetProcessingDomainInterface) {
constructor(data: MeetProcessingDomainInterface, username?: string) {
Object.assign(this, data);
// Если передан username, проверяем, голосовал ли пользователь
if (username && this.questions) {
this.isVoted = this.questions.some(
(question) =>
question.voters_for.includes(username) ||
question.voters_against.includes(username) ||
question.voters_abstained.includes(username)
);
}
// Определяем расширенный статус собрания
this.extendedStatus = this.calculateExtendedStatus();
}
/**
* Вычисляет расширенный статус собрания на основе дат и состояния
*/
private calculateExtendedStatus(): ExtendedMeetStatus {
// Если нет данных о meet, возвращаем базовый статус
if (!this.meet) {
return ExtendedMeetStatus.NONE;
}
const { status, open_at, close_at, quorum_passed } = this.meet;
const now = new Date();
// Базовые статусы блокчейна
if (status === 'created') {
return ExtendedMeetStatus.CREATED;
} else if (status === 'preclosed') {
return ExtendedMeetStatus.PRECLOSED;
} else if (status === 'closed') {
return ExtendedMeetStatus.CLOSED;
} else if (status === 'onrestart') {
return ExtendedMeetStatus.ONRESTART;
}
// Расширенные статусы для 'authorized'
if (status === 'authorized') {
// Ожидание открытия
if (now < open_at) {
return ExtendedMeetStatus.WAITING_FOR_OPENING;
}
// Голосование идет
if (close_at && now >= open_at && now <= close_at) {
return ExtendedMeetStatus.VOTING_IN_PROGRESS;
}
// Голосование завершено
if (close_at && now > close_at) {
// Не достигнут кворум
if (!quorum_passed) {
return ExtendedMeetStatus.EXPIRED_NO_QUORUM;
}
// Кворум достигнут, ожидание подписей
return ExtendedMeetStatus.VOTING_COMPLETED;
}
}
// По умолчанию возвращаем статус из блокчейна
return status as ExtendedMeetStatus;
}
}
@@ -0,0 +1,58 @@
import { registerEnumType } from '@nestjs/graphql';
// Расширенные статусы собрания
export enum ExtendedMeetStatus {
// Начальное состояние
NONE = 'none',
// Базовые статусы блокчейна
CREATED = 'created',
AUTHORIZED = 'authorized',
PRECLOSED = 'preclosed',
CLOSED = 'closed',
// Дополнительные статусы
WAITING_FOR_OPENING = 'waitingForOpening', // Ожидает открытия
VOTING_IN_PROGRESS = 'votingInProgress', // Голосование идет
EXPIRED_NO_QUORUM = 'expiredNoQuorum', // Истекло без кворума
VOTING_COMPLETED = 'votingCompleted', // Ожидает подписей
ONRESTART = 'onrestart', // Ожидаем утверждения новой даты собрания
}
// Регистрируем перечисление для GraphQL
registerEnumType(ExtendedMeetStatus, {
name: 'ExtendedMeetStatus',
description: 'Расширенный статус собрания на основе дат и состояния',
valuesMap: {
NONE: {
description: 'Неопределенное состояние',
},
CREATED: {
description: 'Создано',
},
AUTHORIZED: {
description: 'Авторизовано',
},
PRECLOSED: {
description: 'Предварительно закрыто',
},
CLOSED: {
description: 'Закрыто',
},
ONRESTART: {
description: 'Ожидаем утверждения новой даты собрания',
},
WAITING_FOR_OPENING: {
description: 'Ожидает открытия',
},
VOTING_IN_PROGRESS: {
description: 'Голосование идет',
},
EXPIRED_NO_QUORUM: {
description: 'Истекло без кворума',
},
VOTING_COMPLETED: {
description: 'Голосование завершено, ожидает подписей',
},
},
});
@@ -3,7 +3,7 @@ import { DocumentDomainService } from '~/domain/document/services/document-domai
import { DocumentDomainEntity } from '~/domain/document/entity/document-domain.entity';
import { Inject, Injectable } from '@nestjs/common';
import { MEET_BLOCKCHAIN_PORT, MeetBlockchainPort } from '../ports/meet-blockchain.port';
import { MEET_REPOSITORY, MeetRepository } from '../repositories/meet.repository';
import { MeetPreProcessingRepository, MEET_REPOSITORY } from '../repositories/meet-pre.repository';
import { MeetAggregate } from '../aggregates/meet-domain.aggregate';
import type { CreateAnnualGeneralMeetInputDomainInterface } from '../interfaces/create-annual-meet-input-domain.interface';
import { DocumentAggregator } from '~/domain/document/aggregators/document.aggregator';
@@ -14,12 +14,18 @@ import { GetMeetInputDomainInterface } from '../interfaces/get-meet-input-domain
import { GetMeetsInputDomainInterface } from '../interfaces/get-meets-input-domain.interface';
import { SignBySecretaryOnAnnualGeneralMeetInputDomainInterface } from '../interfaces/sign-by-secretary-on-annual-general-meet-input-domain.interface';
import { SignByPresiderOnAnnualGeneralMeetInputDomainInterface } from '../interfaces/sign-by-presider-on-annual-general-meet-input-domain.interface';
import { MEET_PROCESSED_REPOSITORY, MeetProcessedRepository } from '../repositories/meet-processed.repository';
import { ProcessMeetDecisionInputDomainInterface } from '../interfaces/process-meet-decision-input-domain.interface';
import { MeetProcessedDomainEntity } from '../entities/meet-processed-domain.entity';
import { NotifyOnAnnualGeneralMeetInputDomainInterface } from '../interfaces/notify-on-annual-general-meet-input-domain.interface';
import { generateUniqueHash } from '~/utils/generate-hash.util';
@Injectable()
export class MeetDomainInteractor {
constructor(
private readonly documentDomainService: DocumentDomainService,
@Inject(MEET_REPOSITORY) private readonly meetRepository: MeetRepository,
@Inject(MEET_REPOSITORY) private readonly meetPreRepository: MeetPreProcessingRepository,
@Inject(MEET_PROCESSED_REPOSITORY) private readonly meetProcessedRepository: MeetProcessedRepository,
@Inject(MEET_BLOCKCHAIN_PORT) private readonly meetBlockchainPort: MeetBlockchainPort,
private readonly documentAggregator: DocumentAggregator
) {}
@@ -35,13 +41,24 @@ export class MeetDomainInteractor {
async createAnnualGeneralMeet(data: CreateAnnualGeneralMeetInputDomainInterface): Promise<MeetAggregate> {
// Создаем агрегатор документов с помощью агрегатора документов
const documentAggregate = await this.documentAggregator.buildDocumentAggregate(data.proposal);
const preProcessing = new MeetPreProcessingDomainEntity({ ...data, proposal: documentAggregate });
// Преобразуем null в undefined для соответствия доменному интерфейсу
const proposalAggregate = documentAggregate || undefined;
// Генерируем уникальный хэш для собрания
const hash = generateUniqueHash();
const preProcessing = new MeetPreProcessingDomainEntity({
...data,
hash,
proposal: proposalAggregate,
});
// Сохраняем данные в репозиторий
await this.meetRepository.create(preProcessing);
await this.meetPreRepository.create(preProcessing);
// Вызов блокчейн порта для создания собрания
await this.meetBlockchainPort.createMeet(data);
await this.meetBlockchainPort.createMeet({ ...data, hash });
// Получаем обновленные данные из блокчейна
const processingData = await this.meetBlockchainPort.getMeet({
@@ -50,7 +67,7 @@ export class MeetDomainInteractor {
});
// Получаем сохраненные данные из репозитория
const preMeet = await this.meetRepository.findByHash(preProcessing.hash);
const preMeet = await this.meetPreRepository.findByHash(preProcessing.hash);
// Создаем агрегат из обоих источников данных (репозиторий и блокчейн)
return new MeetAggregate(preMeet, processingData);
@@ -88,21 +105,33 @@ export class MeetDomainInteractor {
return await this.documentDomainService.generateDocument({ data, options });
}
async getMeet(data: GetMeetInputDomainInterface): Promise<MeetAggregate> {
async getMeet(data: GetMeetInputDomainInterface, username?: string): Promise<MeetAggregate> {
// Получаем данные из блокчейна через порт
const processingMeet = await this.meetBlockchainPort.getMeet(data);
const processingMeet = await this.meetBlockchainPort.getMeet({
coopname: data.coopname,
hash: data.hash,
username,
});
// Получаем данные из репозитория
const preMeet = await this.meetRepository.findByHash(data.hash);
const preMeet = await this.meetPreRepository.findByHash(data.hash);
// Получаем данные из репозитория обработанных собраний
const processedMeetEntity = await this.meetProcessedRepository.findByHash(data.hash);
// Создаем агрегат из обоих источников данных (репозиторий и блокчейн)
// MeetAggregate сам проверит наличие хотя бы одного элемента
return new MeetAggregate(preMeet, processingMeet);
const meetAggregate = new MeetAggregate(preMeet, processingMeet, processedMeetEntity);
// Вернем готовый агрегат
return meetAggregate;
}
async getMeets(data: GetMeetsInputDomainInterface): Promise<MeetAggregate[]> {
async getMeets(data: GetMeetsInputDomainInterface, username?: string): Promise<MeetAggregate[]> {
// Получаем данные из блокчейна через порт
const processingDataList = await this.meetBlockchainPort.getMeets(data);
const processingDataList = await this.meetBlockchainPort.getMeets({
coopname: data.coopname,
username,
});
// Если данных нет, возвращаем пустой массив
if (!processingDataList || processingDataList.length === 0) {
@@ -113,13 +142,16 @@ export class MeetDomainInteractor {
const meetsAggregates = await Promise.all(
processingDataList.map(async (processingData) => {
// Получаем данные pre из репозитория для каждого элемента по его хешу
const preMeet = await this.meetRepository.findByHash(processingData.hash);
const preMeet = await this.meetPreRepository.findByHash(processingData.hash);
// Получаем данные processed из репозитория
const processedMeetEntity = await this.meetProcessedRepository.findByHash(processingData.hash);
// Создаем агрегат с данными из репозитория и блокчейна
return new MeetAggregate(preMeet, processingData);
return new MeetAggregate(preMeet, processingData, processedMeetEntity);
})
);
console.dir(meetsAggregates, { depth: null });
return meetsAggregates;
}
@@ -131,28 +163,31 @@ export class MeetDomainInteractor {
const processingData = await this.meetBlockchainPort.getMeet({
coopname: data.coopname,
hash: data.hash,
username: data.username,
});
const preMeet = await this.meetRepository.findByHash(data.hash);
const preMeet = await this.meetPreRepository.findByHash(data.hash);
const processedMeetEntity = await this.meetProcessedRepository.findByHash(data.hash);
// Создаем агрегат из обоих источников данных
return new MeetAggregate(preMeet, processingData);
return new MeetAggregate(preMeet, processingData, processedMeetEntity);
}
async restartMeet(data: RestartAnnualGeneralMeetInputDomainInterface): Promise<MeetAggregate> {
// Вызов блокчейн порта для перезапуска собрания
await this.meetBlockchainPort.restartMeet(data);
const new_hash = await this.meetBlockchainPort.restartMeet(data);
// Получаем обновленные данные из обоих источников
const processingData = await this.meetBlockchainPort.getMeet({
coopname: data.coopname,
hash: data.hash,
hash: new_hash,
});
const preMeet = await this.meetRepository.findByHash(data.hash);
const preMeet = await this.meetPreRepository.findByHash(new_hash);
const processedMeetEntity = await this.meetProcessedRepository.findByHash(new_hash);
// Создаем агрегат из обоих источников данных
return new MeetAggregate(preMeet, processingData);
return new MeetAggregate(preMeet, processingData, processedMeetEntity);
}
async signBySecretaryOnAnnualGeneralMeet(
@@ -162,9 +197,12 @@ export class MeetDomainInteractor {
const processingData = await this.meetBlockchainPort.getMeet({
coopname: data.coopname,
hash: data.hash,
username: data.username,
});
const preMeet = await this.meetRepository.findByHash(data.hash);
return new MeetAggregate(preMeet, processingData);
const preMeet = await this.meetPreRepository.findByHash(data.hash);
const processedMeetEntity = await this.meetProcessedRepository.findByHash(data.hash);
return new MeetAggregate(preMeet, processingData, processedMeetEntity);
}
async signByPresiderOnAnnualGeneralMeet(
@@ -174,8 +212,48 @@ export class MeetDomainInteractor {
const processingData = await this.meetBlockchainPort.getMeet({
coopname: data.coopname,
hash: data.hash,
username: data.username,
});
const preMeet = await this.meetRepository.findByHash(data.hash);
return new MeetAggregate(preMeet, processingData);
const preMeet = await this.meetPreRepository.findByHash(data.hash);
const processedMeetEntity = await this.meetProcessedRepository.findByHash(data.hash);
return new MeetAggregate(preMeet, processingData, processedMeetEntity);
}
/**
* Обработка события о завершенном общем собрании и его результатах
* @param data Данные о решении общего собрания
*/
async processMeetDecision(data: ProcessMeetDecisionInputDomainInterface): Promise<void> {
// Создаем сущность для обработанных данных собрания
const processedEntity = new MeetProcessedDomainEntity({
...data.decisionData,
});
// Сохраняем данные в репозиторий
await this.meetProcessedRepository.save(processedEntity);
}
/**
* Уведомление о собрании
* @param data Данные уведомления
*/
async notifyOnAnnualGeneralMeet(data: NotifyOnAnnualGeneralMeetInputDomainInterface): Promise<MeetAggregate> {
// Вызов блокчейн порта для отправки уведомления
await this.meetBlockchainPort.notifyOnAnnualGeneralMeet(data);
// Получаем обновленные данные из блокчейна
const processingData = await this.meetBlockchainPort.getMeet({
coopname: data.coopname,
hash: data.meet_hash,
username: data.username,
});
// Получаем данные из репозитория
const preMeet = await this.meetPreRepository.findByHash(data.meet_hash);
const processedMeetEntity = await this.meetProcessedRepository.findByHash(data.meet_hash);
// Создаем агрегат из обоих источников данных
return new MeetAggregate(preMeet, processingData, processedMeetEntity);
}
}
@@ -1,4 +1,4 @@
export interface AgendaMeetPointInputDomainInterface {
export interface AgendaGeneralMeetPointInputDomainInterface {
context: string;
title: string;
decision: string;
@@ -1,11 +1,12 @@
import type { Cooperative, MeetContract } from 'cooptypes';
import type { MeetContract } from 'cooptypes';
import type { ISignedDocumentDomainInterface } from '~/domain/document/interfaces/signed-document-domain.interface';
export type CreateAnnualGeneralMeetInputDomainInterface = Omit<
MeetContract.Actions.CreateMeet.IInput,
'proposal' | 'open_at' | 'close_at'
'proposal' | 'open_at' | 'close_at' | 'hash'
> & {
proposal: ISignedDocumentDomainInterface<Cooperative.Registry.AnnualGeneralMeetingAgenda.Meta>;
proposal: ISignedDocumentDomainInterface;
open_at: Date;
close_at: Date;
hash?: string;
};
@@ -1,4 +1,5 @@
export interface GetMeetInputDomainInterface {
coopname: string;
hash: string;
username?: string;
}
@@ -1,3 +1,4 @@
export interface GetMeetsInputDomainInterface {
coopname: string;
username?: string;
}
@@ -4,7 +4,7 @@ import type { MeetProcessingDomainInterface } from './meet-processing-domain.int
// === Aggregated Lifecycle ===
export interface MeetDomainAggregate {
export interface MeetDomainAggregateDomainInterface {
hash: string;
pre?: MeetPreProcessingDomainInterface | null;
processing?: MeetProcessingDomainInterface | null;
@@ -0,0 +1,25 @@
import type { ISignedDocumentDomainInterface } from '~/domain/document/interfaces/signed-document-domain.interface';
export interface MeetQuestionResultDomainInterface {
question_id: number;
number: number;
title: string;
decision: string;
context: string;
votes_for: number;
votes_against: number;
votes_abstained: number;
accepted: boolean;
}
export interface MeetDecisionDomainInterface {
coopname: string;
hash: string;
presider: string;
secretary: string;
results: MeetQuestionResultDomainInterface[];
signed_ballots: number;
quorum_percent: number;
quorum_passed: boolean;
decision: ISignedDocumentDomainInterface;
}
@@ -1,9 +1,10 @@
// domain/interfaces/meet-lifecycle-domain.interface.ts
import type { Cooperative, MeetContract } from 'cooptypes';
import type { MeetContract } from 'cooptypes';
import { assertType, type AssertKeysMatch } from '~/shared/asserts/blockchain-type.assert';
import type { AgendaMeetPointInputDomainInterface } from './agenda-meet-point-input-domain.interface';
import type { AgendaGeneralMeetPointInputDomainInterface } from './agenda-meet-point-input-domain.interface';
import type { DocumentAggregateDomainInterface } from '~/domain/document/interfaces/document-domain-aggregate.interface';
import type { UserCertificateDomainInterface } from '~/domain/user-certificate/interfaces/user-certificate-domain.interface';
// === PreProcessing ===
@@ -11,14 +12,15 @@ export interface MeetPreProcessingDomainInterface {
coopname: string;
hash: string;
initiator: string;
initiator_certificate?: UserCertificateDomainInterface | null;
presider: string;
secretary: string;
agenda: AgendaMeetPointInputDomainInterface[];
presider_certificate?: UserCertificateDomainInterface | null;
secretary_certificate?: UserCertificateDomainInterface | null;
agenda: AgendaGeneralMeetPointInputDomainInterface[];
open_at: Date;
close_at: Date;
proposal?: DocumentAggregateDomainInterface<
Cooperative.Registry.AnnualGeneralMeetingAgenda.Action & Cooperative.Document.IMetaDocument
>;
proposal?: DocumentAggregateDomainInterface;
}
assertType<AssertKeysMatch<MeetContract.Actions.CreateMeet.IInput, MeetPreProcessingDomainInterface>>();
@@ -1,10 +1,20 @@
// === Processed ===
import type { ISignedDocumentDomainInterface } from '~/domain/document/interfaces/signed-document-domain.interface';
import type { UserCertificateDomainInterface } from '~/domain/user-certificate/interfaces/user-certificate-domain.interface';
import type { MeetContract } from 'cooptypes';
import type { BlockchainActionDomainInterface } from '~/domain/common/interfaces/blockchain-action-domain.interface';
// Доменный интерфейс для обработанных собраний
export interface MeetProcessedDomainInterface {
coopname: string;
hash: string;
decision: BlockchainActionDomainInterface<MeetContract.Actions.NewDecision.IInput>;
//documents
presider: string;
secretary: string;
presider_certificate?: UserCertificateDomainInterface | null;
secretary_certificate?: UserCertificateDomainInterface | null;
results: any[];
signed_ballots: number;
quorum_percent: number;
quorum_passed: boolean;
decision: ISignedDocumentDomainInterface;
// Агрегат не является частью доменной сущности,
// он создается временно для передачи в DTO
}
@@ -1,5 +1,6 @@
import type { MeetRowProcessingDomainInterface } from './meet-row-processing-domain.interface';
import type { QuestionRowProcessingDomainInterface } from './question-row-processing-domain.interface';
import { ExtendedMeetStatus } from '../enums/extended-meet-status.enum';
// === Processing ===
@@ -7,4 +8,6 @@ export interface MeetProcessingDomainInterface {
hash: string;
meet: MeetRowProcessingDomainInterface;
questions: QuestionRowProcessingDomainInterface[];
isVoted?: boolean;
extendedStatus?: ExtendedMeetStatus;
}
@@ -1,5 +1,5 @@
import type { Cooperative, MeetContract } from 'cooptypes';
import type { DocumentAggregateDomainInterface } from '~/domain/document/interfaces/document-domain-aggregate.interface';
import type { MeetContract } from 'cooptypes';
import type { ISignedDocumentDomainInterface } from '~/domain/document/interfaces/signed-document-domain.interface';
import { assertType, type AssertKeysMatch } from '~/shared/asserts/blockchain-type.assert';
export interface MeetRowProcessingDomainInterface {
@@ -7,6 +7,7 @@ export interface MeetRowProcessingDomainInterface {
hash: string;
coopname: string;
type: string;
level: string;
initiator: string;
presider: string;
secretary: string;
@@ -19,10 +20,11 @@ export interface MeetRowProcessingDomainInterface {
current_quorum_percent: number;
cycle: number;
quorum_passed: boolean;
proposal: DocumentAggregateDomainInterface;
authorization?: DocumentAggregateDomainInterface;
decision1?: DocumentAggregateDomainInterface;
decision2?: DocumentAggregateDomainInterface;
proposal: ISignedDocumentDomainInterface;
authorization?: ISignedDocumentDomainInterface;
decision1?: ISignedDocumentDomainInterface;
decision2?: ISignedDocumentDomainInterface;
notified_users: string[];
}
assertType<AssertKeysMatch<MeetContract.Tables.Meets.IOutput, MeetRowProcessingDomainInterface>>();
@@ -0,0 +1,8 @@
import { type ISignedDocumentDomainInterface } from '~/domain/document/interfaces/signed-document-domain.interface';
export interface NotifyOnAnnualGeneralMeetInputDomainInterface {
coopname: string;
meet_hash: string;
username: string;
notification: ISignedDocumentDomainInterface;
}

Some files were not shown because too many files have changed in this diff Show More