Compare commits

...

64 Commits

Author SHA1 Message Date
Alex Ant 6e2c9a1bf8 чуть не забыл 2025-07-03 18:03:31 +05:00
Alex Ant 137d1ca8a4 migration 2025-07-01 20:31:21 +05:00
Alex Ant 59744e9d23 добавлены анимации для входа и выхода меню, улучшена логика рендеринга. Обновлены стили и добавлен ключ для перерендера при смене маршрутов. Исправлены мелкие недочеты в других компонентах. 2025-07-01 16:46:33 +05:00
Alex Ant ac8836628e chore(release): publish 2025-07-01 16:04:01 +05:00
Alex Ant 1f649c8951 Упрощен компонент ShadowHtml: удалена проверка на клиентскую среду, добавлены логи для отладки, обновлено состояние миграции с изменением тестового файла. 2025-07-01 15:45:10 +05:00
Alex Ant 310468fb69 Исправлено название типа платежа "Возврат средств" на "Возврат взноса" в PaymentDomainEntity. 2025-07-01 13:36:13 +05:00
Alex Ant 7f782e0a92 Рефакторинг пользователя в сессии на фронте, правки визуального отображения всех страниц, управление исходящими платежами в шлюзе, возврат паевого взноса в кошельке. 2025-07-01 13:34:46 +05:00
Alex Ant d0c9f8bffc Новая архитектура для входящих и исходящих платежей 2025-06-26 20:20:58 +05:00
Alex Ant 60a35f6f76 returnByMoneyDecision Document 2025-06-25 20:34:12 +05:00
Alex Ant a3a2923751 Добавляем шаблон заявления на возврат паевого взноса с ЦК 2025-06-24 16:18:34 +05:00
Alex Ant 32e6a45f05 chore(release): publish 2025-06-24 09:07:26 +05:00
Alex Ant 59665c6df4 fix navigation guard bug 2025-06-24 08:50:05 +05:00
Alex Ant d23b4e3aac поле ввода для поиска пайщика и извлечения его username 2025-06-23 20:56:07 +05:00
Alex Ant 20897cf199 search by private account 2025-06-23 13:30:34 +05:00
Alex Ant 0b57232606 Корректное удаление кооперативного участка. Общее собранее уполномоченных. Правки title шаблонов. 2025-06-19 12:09:10 +05:00
Alex Ant 1bd8f003f7 chore(release): publish 2025-06-19 10:00:51 +05:00
Alex Ant fcb614cd90 Правка сообщений с ошибками на фронте, расширение шаблонов общих собраний на повторяемость. 2025-06-19 09:41:41 +05:00
Alex Ant 1c376c003a chore(release): publish 2025-06-14 21:27:59 +05:00
Alex Ant a433ceaf1d FATALITY BUG FIX 2025-06-14 21:26:41 +05:00
Alex Ant 850478b7f7 Revert "откатываем функционал"
This reverts commit 8484a14905.
2025-06-14 20:28:02 +05:00
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
803 changed files with 33180 additions and 7112 deletions
Vendored
BIN
View File
Binary file not shown.
+3
View File
@@ -0,0 +1,3 @@
{
"agentCanUpdateSnapshot": true
}
+6
View File
@@ -0,0 +1,6 @@
---
description:
globs:
alwaysApply: true
---
ВАЖНО!!! Мы работаем в МОНО-репозитории pnpm, любая установка пакетов производится ЧЕРЕЗ ФИЛЬТР компонента в корне: pnpm add glob --filter notificator2
+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 }}'
+8 -5
View File
@@ -1,16 +1,19 @@
{
"editor.bracketPairColorization.enabled": true,
"editor.guides.bracketPairs": true,
"editor.formatOnSave": true,
"editor.tabCompletion": "onlySnippets",
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint":"explicit"
},
// "editor.formatOnSave": true,
// "editor.codeActionsOnSave": {
// "source.addMissingImports.ts": "explicit",
// "source.fixAll.ts": "explicit",
// "source.fixAll.eslint": "explicit"
// },
"eslint.run": "onType",
"eslint.validate": ["javascript", "typescript", "vue"],
"i18n-ally.localesPaths": ["src/i18n"],
"typescript.format.enable": true,
"typescript.format.indentSwitchCase": false,
"typescript.validate.enable": true
"typescript.validate.enable": true,
"notebook.defaultFormatter": "Vue.volar"
}
+57
View File
@@ -1,3 +1,60 @@
# v2025.6.14
Разработан модуль для проведения очередных общих собраний пайщиков. Исправлены баги, внесены улучшения пользовательского интерфейса.
✨ Новые функции
- [#264](https://github.com/coopenomics/mono/issues/264): Разработан смарт-контракт общего собрания пайщиков (meet)
- [#263](https://github.com/coopenomics/mono/issues/263): Реализован модуль оповещений на электронные почты по жизненному циклу общего собрания пайщиков
- [#273](https://github.com/coopenomics/mono/issues/273): Встроены реальные шаблоны документов общего собрания
- [#268](https://github.com/coopenomics/mono/issues/268): Добавлена страница результатов общего собрания
- [#267](https://github.com/coopenomics/mono/issues/267): Добавлена страница просмотра и скачивания бюллетеней и уведомлений по собранию
- [#270](https://github.com/coopenomics/mono/issues/270): Ссылка в оповещении ведет на страницу собрания с документом-уведомлением для подписи
🐛 Исправления ошибок
- [#262](https://github.com/coopenomics/mono/issues/262): Исправлено некорректное открытие рабочего стола совета
- [#261](https://github.com/coopenomics/mono/issues/261): Исправлена ошибка с редиректом при первой загрузке и прямом переходе на собрание
- [#259](https://github.com/coopenomics/mono/issues/259): Исправлены отступы в мобильной карточке пайщика
- [#258](https://github.com/coopenomics/mono/issues/258): Убран hover-эффект на документе и пайщике в таблице
🔧 Улучшения
- [#274](https://github.com/coopenomics/mono/issues/274): Настроена рассылка оповещений на почту при получении решения о проведении собрания
- [#272](https://github.com/coopenomics/mono/issues/272): Подписанные уведомления сохраняются и извлекаются из реестра по Graph-QL
- [#271](https://github.com/coopenomics/mono/issues/271): Ведется подсчет количества пайщиков в каждом кооперативе при добавлении и удалении
- [#260](https://github.com/coopenomics/mono/issues/260): Введен счетчик количества пайщиков в кооперативе на контракте регистратора
- [#256](https://github.com/coopenomics/mono/issues/256): Отображается статус членства каждого пайщика
- [#255](https://github.com/coopenomics/mono/issues/255): В разделе Платежи отображается ФИО/Наименование плательщика
- [#269](https://github.com/coopenomics/mono/issues/269): Реализована рассылка уведомлений перед началом собрания
- [#265](https://github.com/coopenomics/mono/issues/265): Проведена отладка и тестирование процесса общего собрания пайщиков
- [#266](https://github.com/coopenomics/mono/issues/266): Протестированы все процессы общего собрания
#releases
---
# v2025.5.14
В этом релизе реализован новый стандарт передачи и хранения документов по блокчейну с поддержкой неограниченного количества подписей и их валидацией. Также внесены улучшения в интерфейс и исправлены ошибки.
✨ Новые функции
- [#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
В этом релизе рабочие столы переведены на серверный рендеринг, что улучшает стабильность развертывания и упрощает автоматизацию поставки ПО. Данный релиз является подготовительным к переходу на новый стандарт цифровых документов на платформе.
+316
View File
@@ -0,0 +1,316 @@
# 🚀 Notificator2 Web Push Service - Implementation Complete
## ✅ Что реализовано
Создан полноценный сервис для web-push уведомлений на базе NestJS с интеграцией NOVU, который полностью заменяет Firebase для push уведомлений и предоставляет все необходимые возможности для работы с NOVU.
## 📦 Архитектура компонентов
### База данных (PostgreSQL + TypeORM)
- **Entity**: `PushSubscription` для хранения подписок пользователей
- **Автоматические миграции** и управление схемой БД
- **Индексы** для оптимизации запросов по userId и endpoint
- **UUID** как первичный ключ для безопасности
### Сервисы
#### 🔐 WebPushService
**Основной сервис для web-push функциональности:**
- Автогенерация и управление VAPID ключами
- CRUD операции с push подписками
- Отправка уведомлений с полной поддержкой Web Push API
- Автоматическая деактивация недействительных подписок
- Статистика и мониторинг
#### 🔗 NovuWebPushService
**Интеграция с NOVU:**
- Создание NOVU workflows с поддержкой web-push
- Обработка webhook событий от NOVU
- Template processing с поддержкой переменных
- Fallback отправка через web-push API
#### 🧹 CleanupService
**Автоматическое обслуживание:**
- Cron job для ежедневной очистки (2:00 AM)
- Еженедельная статистика (воскресенье, 12:00)
- Ручное управление очисткой через API
### Контроллеры
#### 📱 PushController (`/api/push/`)
**Основной API для web-push:**
```
GET /vapid-public-key - Получение VAPID ключа
POST /subscribe - Подписка на уведомления
DELETE /unsubscribe - Отписка от уведомлений
POST /send - Отправка уведомления
GET /subscriptions/:userId - Подписки пользователя
POST /test/:userId - Тестовое уведомление
GET /stats - Статистика подписок
DELETE /cleanup - Очистка старых подписок
```
#### 🎯 NovuWebhookController (`/api/novu/`)
**NOVU интеграция:**
```
POST /webhook/push - Webhook для NOVU событий
POST /subscribe - Подписка через NOVU
GET /integration/config - Конфигурация интеграции
POST /test - Тест интеграции
POST /workflow/create - Создание workflow
```
## 🎯 Возможности
### Web Push Features
-**Полная поддержка Web Push API**: title, body, icon, badge, image, actions
-**VAPID аутентификация**: автогенерация ключей
-**Rich notifications**: кнопки, изображения, custom data
-**Множественные подписки** на пользователя
-**Автоматическая очистка** недействительных подписок
### NOVU Integration
-**Webhook интеграция**: автообработка NOVU событий
-**Workflow создание**: программное создание workflows
-**Template поддержка**: переменные в уведомлениях
-**Dual отправка**: через NOVU + прямая отправка как fallback
### Мониторинг и обслуживание
-**Автоматизация**: cron jobs для очистки и статистики
-**Статистика**: активные/неактивные подписки, уникальные пользователи
-**Логирование**: подробные логи всех операций
-**Health checks**: проверка работоспособности системы
## 🛠️ Настройка и запуск
### 1. Переменные окружения (.env)
```env
# Приложение
PORT=3000
CORS_ORIGIN=*
# NOVU
NOVU_API_KEY=your_novu_api_key
NOVU_API_URL=https://api.novu.co
# VAPID (генерируются автоматически)
VAPID_PUBLIC_KEY=
VAPID_PRIVATE_KEY=
VAPID_SUBJECT=mailto:admin@coopenomics.io
# PostgreSQL
DB_HOST=localhost
DB_PORT=5432
DB_USERNAME=postgres
DB_PASSWORD=postgres
DB_DATABASE=notificator
DB_SYNCHRONIZE=true
```
### 2. Запуск
```bash
# В корне проекта
cd components/notificator2
# Установка зависимостей
pnpm install
# Сборка
pnpm build
# Запуск в production
pnpm start:prod
# Или для разработки
pnpm start:dev
```
### 3. База данных
```sql
-- Создать БД в PostgreSQL
CREATE DATABASE notificator;
```
## 💻 Интеграция с фронтендом
### Service Worker (public/sw.js)
```javascript
self.addEventListener('push', function(event) {
const payload = JSON.parse(event.data.text());
const options = {
body: payload.body,
icon: payload.icon,
badge: payload.badge,
image: payload.image,
actions: payload.actions,
data: payload.data,
tag: payload.tag,
requireInteraction: payload.requireInteraction
};
event.waitUntil(
self.registration.showNotification(payload.title, options)
);
});
self.addEventListener('notificationclick', function(event) {
event.notification.close();
if (event.action === 'view' || !event.action) {
const url = event.notification.data?.url || '/';
event.waitUntil(clients.openWindow(url));
}
});
```
### Подписка пользователя
```javascript
async function subscribeToPush(userId) {
// 1. Регистрация Service Worker
const registration = await navigator.serviceWorker.register('/sw.js');
// 2. Получение VAPID ключа
const { publicKey } = await fetch('/api/push/vapid-public-key')
.then(r => r.json());
// 3. Создание подписки
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: publicKey
});
// 4. Отправка на сервер
await fetch('/api/push/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId, subscription })
});
}
```
### Отправка уведомления
```javascript
// Через прямой API
await fetch('/api/push/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userId: 'user123',
title: 'Новое сообщение',
body: 'У вас есть новое сообщение',
icon: '/icons/message.png',
url: '/messages',
actions: [
{ action: 'view', title: 'Открыть' },
{ action: 'dismiss', title: 'Закрыть' }
]
})
});
```
## 🔗 Интеграция с NOVU
### 1. Настройка webhook в NOVU
- URL: `https://your-domain.com/api/novu/webhook/push`
- События: `workflow.step.push.sent`
### 2. Создание workflow с push поддержкой
```javascript
// Через API сервиса
await fetch('/api/novu/workflow/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
workflowId: 'user-notifications',
workflowName: 'Уведомления пользователей'
})
});
```
### 3. Триггер через NOVU SDK
```javascript
import { Novu } from '@novu/node';
const novu = new Novu(process.env.NOVU_API_KEY);
await novu.trigger('user-notifications', {
to: { subscriberId: 'user123' },
payload: {
title: 'Новое уведомление',
body: 'Описание уведомления',
icon: '/icons/notification.png',
url: '/dashboard'
}
});
```
## 📊 Мониторинг и API
### Получение статистики
```bash
curl http://localhost:3000/api/push/stats
```
**Ответ:**
```json
{
"success": true,
"data": {
"total": 150,
"active": 120,
"inactive": 30,
"uniqueUsers": 85
}
}
```
### Тестирование
```bash
# Отправить тестовое уведомление
curl -X POST http://localhost:3000/api/push/test/user123
# Проверить NOVU интеграцию
curl -X POST http://localhost:3000/api/novu/test \
-H "Content-Type: application/json" \
-d '{"userId": "user123"}'
```
## 🔒 Безопасность
-**VAPID аутентификация** для всех push уведомлений
-**Валидация данных** на всех endpoints
-**CORS настройки** для безопасного API доступа
-**Автоматическая очистка** недействительных подписок
-**Webhook подпись валидация** (готово к настройке для production)
## ⚡ Производительность
-**Индексы БД** для быстрых запросов
-**Batch отправка** уведомлений
-**Connection pooling** для PostgreSQL
-**Graceful error handling** с retry логикой
-**Автоматическая очистка** для поддержания производительности
## 🎉 Результат
**Полностью функциональный web-push сервис готов к production использованию!**
### Для разработчиков:
- 📚 Простое REST API с TypeScript типизацией
- 🧪 Тестовые скрипты и подробная документация
- 🔧 Гибкая настройка и конфигурация
### Для пользователей:
- 📱 Кроссплатформенные push уведомления
- 🎨 Rich notifications с кнопками и изображениями
- ⚡ Быстрая и надежная доставка
### Для администраторов:
- 📊 Автоматическое обслуживание и статистика
- 🔍 Подробные логи и мониторинг
- ⚙️ Масштабируемая архитектура
### Для NOVU интеграции:
- 🔗 Полная интеграция с workflow системой
- 📨 Webhook поддержка для автоматической отправки
- 🔄 Fallback механизмы для надежности
**Сервис полностью заменяет Firebase для push уведомлений и предоставляет все необходимые возможности для интеграции с NOVU!** 🚀
+129
View File
@@ -0,0 +1,129 @@
# 📋 Резюме создания компонентов Notifications
## ✅ Что создано
### 1. 📚 Библиотека `@monocoop/notifications`
**Расположение:** `components/notifications/`
**Функции:**
- ✅ Типизированные интерфейсы для workflow с Zod
- ✅ Builder паттерн для создания workflow
- ✅ Базовые шаблоны для email, in-app, push, SMS
- ✅ Автоматическая конвертация Zod схем в JSON Schema для Novu
- ✅ Экспорт всех workflow для использования в других пакетах
**Структура:**
```
src/
├── types/ # Базовые типы и интерфейсы
├── base/ # Утилиты и настройки по умолчанию
├── workflows/ # Папки с workflow
│ └── welcome/ # Пример приветственного workflow
└── index.ts # Главный экспорт
```
### 2. 🚀 NestJS приложение `@monocoop/notificator2`
**Расположение:** `components/notificator2/`
**Функции:**
- ✅ Автоматический upsert всех workflow в Novu при запуске
- ✅ RESTful API для триггера уведомлений
- ✅ Типизированная валидация payload
- ✅ Health check endpoints
- ✅ Использует библиотеку notifications для типов
**API Endpoints:**
- `GET /api/notifications/health` - Health check
- `GET /api/notifications/workflows` - Список workflow
- `POST /api/notifications/trigger` - Универсальный триггер
- `POST /api/notifications/trigger/welcome` - Триггер welcome workflow
- `POST /api/notifications/workflows/upsert-all` - Принудительный upsert
## 🔧 Как использовать
### 1. Настройка библиотеки notifications
```bash
cd components/notifications
pnpm install # или npm install
pnpm build # для компиляции TypeScript
```
### 2. Настройка notificator2
```bash
cd components/notificator2
pnpm install # или npm install
# Настройка окружения
cp .env.example .env
# Добавить NOVU_API_KEY в .env
# Запуск
pnpm start:dev
```
### 3. Добавление нового workflow
**В библиотеке notifications:**
```typescript
// components/notifications/src/workflows/order/order-workflow.ts
export const orderWorkflow = WorkflowBuilder
.create<OrderPayload>()
.name('Order Confirmation')
.workflowId('order-confirmation')
.payloadSchema(orderPayloadSchema)
.addSteps([...])
.build();
```
**Регистрация в index.ts:**
```typescript
// components/notifications/src/workflows/index.ts
import { orderWorkflow } from './order';
export const allWorkflows = [
welcomeWorkflow,
orderWorkflow, // ← добавить новый workflow
];
```
### 4. Использование API
**Триггер welcome workflow:**
```bash
curl -X POST http://localhost:3000/api/notifications/trigger/welcome \
-H "Content-Type: application/json" \
-d '{
"subscriberId": "user-123",
"email": "user@example.com",
"payload": {
"userName": "Иван Иванов",
"userEmail": "user@example.com",
"age": 25
}
}'
```
## 🎯 Преимущества архитектуры
1. **Типобезопасность** - Zod схемы обеспечивают валидацию на уровне TypeScript и runtime
2. **Разделение ответственности** - Библиотека содержит только типы, сервер - логику
3. **Расширяемость** - Легко добавлять новые workflow
4. **Автоматизация** - Workflow автоматически синхронизируются с Novu
5. **Переиспользование** - Типы можно использовать в других частях системы
## 🔄 Workflow при запуске
1. **Запуск notificator2**
2. **Чтение всех workflow из библиотеки**
3. **Проверка существования в Novu**
4. **Создание/обновление workflow**
5. **Готов к приёму запросов на триггеры**
## 📁 Структура как в testFramework2.ts
Вся логика из `testFramework2.ts` была перенесена в structured архитектуру:
-`buildWorkflowData``WorkflowBuilder`
-`baseSteps``createEmailStep`, `createInAppStep`, etc.
-`upsertWorkflow``NovuService.upsertWorkflow`
-`triggerWorkflow``NovuService.triggerWorkflow`
- ✅ Типизация payload → Zod схемы + TypeScript типы
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.7.1",
"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.7.1",
"private": true,
"description": "",
"scripts": {
@@ -14,6 +14,12 @@ void gateway::createoutpay(eosio::name coopname, eosio::name username, checksum2
eosio::check(confirm_callback != ""_n, "Действие для коллбэка-успеха должено быть установлено");
eosio::check(decline_callback != ""_n, "Действие для коллбэка-отлонения должно быть установлено");
// Валидируем callback действия если они от wallet контракта
if (callback_contract == _wallet) {
Wallet::get_valid_wallet_action(confirm_callback);
Wallet::get_valid_wallet_action(decline_callback);
}
auto outcome = Gateway::get_outcome(coopname, outcome_hash);
eosio::check(!outcome.has_value(), "Объект возврата уже существует с указанным хэшем");
@@ -1,6 +1,5 @@
void gateway::outcomplete(eosio::name coopname, checksum256 outcome_hash){
require_auth(coopname);
auto exist_outcome = Gateway::get_outcome(coopname, outcome_hash);
eosio::check(exist_outcome.has_value(), "Объект возврата не существует с указанным хэшем");
@@ -9,13 +8,15 @@ void gateway::outcomplete(eosio::name coopname, checksum256 outcome_hash){
auto outcome = outcomes.find(exist_outcome -> id);
eosio::check(outcome -> status == "pending"_n, "Только принятые заявления на вывод могут быть обработаны");
action(
permission_level{ _gateway, "active"_n},
// Используем интерфейс для типизированного вызова callback действия
Action::send<completewthd_interface>(
outcome -> callback_contract,
outcome -> confirm_callback,
std::make_tuple(coopname, outcome -> outcome_hash)
).send();
_gateway,
coopname,
outcome -> outcome_hash
);
outcomes.erase(outcome);
};
@@ -2,19 +2,22 @@ void gateway::outdecline(eosio::name coopname, checksum256 outcome_hash, std::st
require_auth(coopname);
auto exist_outcome = Gateway::get_outcome(coopname, outcome_hash);
eosio::check(!exist_outcome.has_value(), "Объект возврата уже существует с указанным хэшем");
eosio::check(exist_outcome.has_value(), "Объект возврата не существует с указанным хэшем");
Gateway::outcomes_index outcomes(_gateway, coopname.value);
auto outcome = outcomes.find(exist_outcome -> id);
eosio::check(outcome -> status == "pending"_n, "Только принятые заявления на вывод могут быть обработаны");
action(
permission_level{ _gateway, "active"_n},
outcome -> callback_contract,
outcome -> decline_callback,
std::make_tuple(coopname, outcome -> outcome_hash, reason)
).send();
// Используем интерфейс для типизированного вызова callback действия
Action::send<declinewthd_interface>(
outcome -> callback_contract,
outcome -> decline_callback,
_gateway,
coopname,
outcome -> outcome_hash,
reason
);
outcomes.erase(outcome);
};
@@ -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>>,
+1
View File
@@ -21,5 +21,6 @@
#include "shared_approver.hpp"
#include "shared_registrator.hpp"
#include "shared_loan.hpp"
#include "shared_branch.hpp"
#include "approves.hpp"
#include "debts.hpp"
+4 -9
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, //клайм прироста капитализации из задания
@@ -113,17 +114,11 @@ static const std::set<eosio::name> soviet_actions = {
"capwthdrprog"_n, //произвести возврат накопленных членских взносов по программе на капиталиста
"capwthdrproj"_n, //произвести возврат накопленных членских взносов по проекту на актора
"capwthdrres"_n, //произвести возврат из задания
};
static const std::set<eosio::name> gateway_income_actions = {
"deposit"_n, //паевой взнос по ЦПП Кошелёк
//WALLET
"createwthd"_n, //создать заявление на возврат паевого взноса
};
static const std::set<eosio::name> gateway_outcome_actions = {
"withdraw"_n, //возврат паевого взноса по ЦПП Кошелёк
};
//program_ids
static constexpr uint64_t _wallet_program_id = 1;
static constexpr uint64_t _sosedi_program_id = 2;
+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)
{
@@ -0,0 +1,53 @@
#pragma once
namespace Branch {
/**
* @brief Получает количество кооперативных участков для данного кооператива
* @param coopname Имя кооператива
* @return Количество участков кооператива
*/
uint64_t get_branch_count(eosio::name coopname) {
branchstat_index stat(_branch, _branch.value);
auto st = stat.find(coopname.value);
if (st == stat.end()) {
return 0;
}
return st->count;
}
/**
* @brief Проверяет, является ли пользователь уполномоченным лицом (trustee) кооперативного участка
* @param coopname Имя кооператива
* @param username Имя пользователя для проверки
* @return true если пользователь является уполномоченным, false в противном случае
*/
bool is_trustee(eosio::name coopname, eosio::name username) {
branch_index branches(_branch, coopname.value);
auto by_trustee_index = branches.get_index<"bytrustee"_n>();
auto trustee_itr = by_trustee_index.find(username.value);
return trustee_itr != by_trustee_index.end();
}
/**
* @brief Проверяет, является ли пользователь доверенным лицом (trusted) в указанном участке кооператива
* @param coopname Имя кооператива
* @param braname Имя участка
* @param username Имя пользователя для проверки
* @return true если пользователь является доверенным, false в противном случае
*/
bool is_trusted(eosio::name coopname, eosio::name braname, eosio::name username) {
branch_index branches(_branch, coopname.value);
auto branch_itr = branches.find(braname.value);
if (branch_itr == branches.end()) {
return false;
}
return branch_itr->is_account_in_trusted(username);
}
}
@@ -3,6 +3,16 @@
using createoutpay_interface = void(CREATEOUTPAY_SIGNATURE);
namespace Gateway {
static const std::set<eosio::name> gateway_income_actions = {
"deposit"_n, //паевой взнос по ЦПП Кошелёк
};
static const std::set<eosio::name> gateway_outcome_actions = {
"withdraw"_n, //возврат паевого взноса по ЦПП Кошелёк
};
inline eosio::name get_valid_income_action(const eosio::name& action) {
eosio::check(gateway_income_actions.contains(action), "Недопустимое имя действия");
return action;
@@ -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,5 +1,22 @@
#define COMPLETEWTHD_SIGNATURE name coopname, checksum256 withdraw_hash
#define DECLINEWTHD_SIGNATURE name coopname, checksum256 withdraw_hash, std::string reason
using completewthd_interface = void(COMPLETEWTHD_SIGNATURE);
using declinewthd_interface = void(DECLINEWTHD_SIGNATURE);
namespace Wallet {
static const std::set<eosio::name> wallet_callback_actions = {
"authwthd"_n, // авторизация возврата паевого взноса
"declinewthd"_n, // отклонение возврата паевого взноса
"completewthd"_n, // завершение возврата паевого взноса
};
inline eosio::name get_valid_wallet_action(const eosio::name& action) {
eosio::check(wallet_callback_actions.contains(action), "Недопустимое имя действия wallet");
return action;
}
inline void validate_asset(const eosio::asset& amount) {
check(amount.symbol == _root_govern_symbol, "Invalid token symbol");
check(amount.is_valid(), "Invalid asset");
@@ -1,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
);
}
+30 -3
View File
@@ -1,7 +1,15 @@
// Заглушка для получения числа пайщиков кооператива.
uint64_t get_total_participants(eosio::name coopname) {
// Здесь можно реализовать получение актуальных данных, например, через inline action или вызов другого контракта.
return 2; // Пример: 100 пайщиков
// Получаем кооператив и проверяем режим участков
auto cooperative = get_cooperative_or_fail(coopname);
// Если включен режим кооперативных участков, используем количество участков
if (cooperative.is_branched) {
return Branch::get_branch_count(coopname);
}
// Иначе используем обычный подсчет активных пайщиков
return Registrator::get_active_participants_count(coopname);
}
void meet::vote(name coopname, checksum256 hash, name username, document2 ballot, std::vector<vote_point> votes) {
@@ -14,6 +22,14 @@ void meet::vote(name coopname, checksum256 hash, name username, document2 ballot
eosio::check(meet_opt.has_value(), "Собрание не найдено");
auto meet_record = meet_opt.value();
// Получаем кооператив для проверки режима участков
auto cooperative = get_cooperative_or_fail(coopname);
// Если включен режим кооперативных участков, проверяем что голосующий является уполномоченным
if (cooperative.is_branched) {
eosio::check(Branch::is_trustee(coopname, username), "Только уполномоченные кооперативных участков могут голосовать при режиме участков");
}
// Проверяем статус собрания и временное окно голосования
eosio::check(meet_record.status == "authorized"_n, "Собрание не авторизовано для голосования");
auto now = current_time_point();
@@ -83,7 +99,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 +116,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;
@@ -1,6 +1,6 @@
//TODO: нужно вырезать этот метод, заменив его на что-то другое.
// сейчас мы в лоб проходим по всем пайщикам и отключаем их от кооперативного участка, в случае, если он удален.
// так вообще не то. Годится только на малых масштабах.
// так вообще не то. Годится только на малых масштабах (до 100 пайщиков на участке будет ок)
/**
* @brief Эффект удаления кооперативного участка
* @param coopname Имя кооператива
@@ -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();
}
}
@@ -25,6 +25,6 @@ void wallet::approvewthd(eosio::name coopname, checksum256 withdraw_hash, docume
withdraw -> statement,
std::string("")
)
).send();
).send();
}
@@ -14,16 +14,16 @@ void wallet::authwthd(eosio::name coopname, checksum256 withdraw_hash) {
});
// создаём объект исходящего платежа в gateway с коллбэком после обработки
action(permission_level{ _capital, "active"_n}, _gateway, "createoutpay"_n,
std::make_tuple(
coopname,
withdraw -> username,
withdraw_hash,
withdraw -> quantity,
_wallet,
"completewthd"_n,
"declinewthd"_n
)
).send();
Action::send<createoutpay_interface>(
_gateway,
"createoutpay"_n,
_wallet,
coopname,
withdraw -> username,
withdraw_hash,
withdraw -> quantity,
_wallet,
Wallet::get_valid_wallet_action("completewthd"_n),
Wallet::get_valid_wallet_action("declinewthd"_n)
);
}
@@ -1,4 +1,4 @@
void wallet::completewthd(eosio::name coopname, checksum256 withdraw_hash, std::string memo) {
void wallet::completewthd(COMPLETEWTHD_SIGNATURE) {
require_auth(_gateway);
auto exist = Wallet::get_withdraw(coopname, withdraw_hash);
@@ -16,7 +16,7 @@ void wallet::completewthd(eosio::name coopname, checksum256 withdraw_hash, std::
std::make_tuple(coopname, withdraw -> quantity, false)
).send();
std::string memo_in = "Возврат части целевого паевого взноса по ЦПП 'Цифровой Кошелёк'";
std::string memo_in = "Возврат части паевого взноса по ЦПП 'Цифровой Кошелёк'";
Wallet::sub_blocked_funds(_wallet, coopname, withdraw -> username, withdraw -> quantity, _wallet_program, memo_in);
withdraws.erase(withdraw);
@@ -28,22 +28,20 @@ void wallet::createwthd(eosio::name coopname, eosio::name username, checksum256
d.created_at = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
});
//TODO: перевести approve на hash-идентификатор???
action(
permission_level{_capital, "active"_n}, // кто вызывает
Action::send<createagenda_interface>(
_soviet,
"createapprv"_n,
std::make_tuple(
coopname,
username,
statement,
withdraw_hash,
_wallet, // callback_contract (текущий контракт)
"approvewthd"_n, // callback_action_approve
"declinewthd"_n, // callback_action_decline
std::string("")
)
).send();
"createagenda"_n,
_wallet,
coopname,
username,
get_valid_soviet_action("createwthd"_n),
withdraw_hash,
_wallet, // callback_contract (текущий контракт)
Wallet::get_valid_wallet_action("authwthd"_n), // callback_action_approve
Wallet::get_valid_wallet_action("declinewthd"_n), // callback_action_decline
statement,
std::string("")
);
std::string memo_in = "Возврат части целевого паевого взноса по ЦПП 'Цифровой Кошелёк'";
Wallet::block_funds(_wallet, coopname, username, quantity, _wallet_program, memo_in);
+2 -2
View File
@@ -25,8 +25,8 @@ public:
//возврат паевого взноса
[[eosio::action]] void createwthd(eosio::name coopname, eosio::name username, checksum256 withdraw_hash, eosio::asset quantity, document2 statement);
[[eosio::action]] void completewthd(eosio::name coopname, checksum256 withdraw_hash, std::string memo);
[[eosio::action]] void declinewthd(eosio::name coopname, checksum256 withdraw_hash, std::string reason);
[[eosio::action]] void completewthd(COMPLETEWTHD_SIGNATURE);
[[eosio::action]] void declinewthd(DECLINEWTHD_SIGNATURE);
[[eosio::action]] void authwthd(eosio::name coopname, checksum256 withdraw_hash);
[[eosio::action]] void approvewthd(eosio::name coopname, checksum256 withdraw_hash, document2 approved_statement);
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@coopenomics/contracts",
"version": "2025.5.5-2",
"version": "2025.7.1",
"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
+14
View File
@@ -0,0 +1,14 @@
# =============================================================================
# PUSH NOTIFICATIONS
# =============================================================================
# URL сервиса notificator2
NOTIFICATOR2_URL=http://localhost:3001
# Секретный ключ для аутентификации запросов к notificator2
SERVER_SECRET=your-super-secret-key-here
NOVU_APP_ID=
NOVU_BACKEND_URL=https://novu.coopenomics.world/api
NOVU_SOCKET_URL=https://novu.coopenomics.world/ws
NOVU_API_KEY=
+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>
+42
View File
@@ -0,0 +1,42 @@
-- Исправление PRIMARY KEY в таблице payments
-- Сначала проверим текущую структуру
\d payments
-- Если таблица существует и у неё есть PK на hash, то:
-- 1. Удаляем старый PRIMARY KEY (если есть)
DO $$
DECLARE
pk_name TEXT;
BEGIN
SELECT conname INTO pk_name
FROM pg_constraint
WHERE conrelid = 'payments'::regclass AND contype = 'p';
IF pk_name IS NOT NULL THEN
EXECUTE 'ALTER TABLE payments DROP CONSTRAINT ' || pk_name;
RAISE NOTICE 'Удален PRIMARY KEY: %', pk_name;
END IF;
END $$;
-- 2. Добавляем колонку id если её нет
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'payments' AND column_name = 'id') THEN
ALTER TABLE payments ADD COLUMN id uuid DEFAULT gen_random_uuid();
UPDATE payments SET id = gen_random_uuid() WHERE id IS NULL;
ALTER TABLE payments ALTER COLUMN id SET NOT NULL;
RAISE NOTICE 'Добавлена колонка id';
END IF;
END $$;
-- 3. Создаем новый PRIMARY KEY по id
ALTER TABLE payments ADD PRIMARY KEY (id);
-- 4. Делаем hash nullable
ALTER TABLE payments ALTER COLUMN hash DROP NOT NULL;
-- Проверяем результат
\d payments
+19
View File
@@ -0,0 +1,19 @@
// jest.setup.js
process.env.NODE_ENV = 'test';
// Мокаем config если нужно
jest.mock('~/config/config', () => ({
default: {
jwt: {
secret: 'test-secret',
accessExpirationMinutes: '60m',
},
coopname: 'test-coop',
novu: {
backend_url: 'http://localhost:3000',
socket_url: 'ws://localhost:3000',
api_key: 'test-api-key',
app_id: 'test-app-id',
},
},
}));
@@ -0,0 +1,32 @@
# Миграции
## Правила именования
- Обычные миграции: `V{версия}__описание.ts` (например, `V1.0.0__initial.ts`)
- Тестовые миграции: `V{версия}__описание__test.ts` (например, `V1.0.2__blockchain_data_sync__test.ts`)
## Поведение
- Обычные миграции применяются только один раз, если их версия ещё не применялась.
- Тестовые миграции (файлы с `__test` в имени) запускаются всегда, даже если версия уже применялась.
- Флаг `isTest` в коде миграции больше не используется — только по имени файла.
- Если в миграции указан параметр `validUntil` (дата), то миграция не будет применена, если эта дата уже прошла (по UTC).
## Пример
- `V1.0.0__initial.ts` — обычная миграция
- `V1.0.2__blockchain_data_sync__test.ts` — тестовая миграция
## validUntil
В объекте миграции можно указать поле `validUntil` типа `Date`. Если текущая дата превышает это значение, миграция не будет применена.
```ts
export default {
name: 'example',
validUntil: new Date('2024-07-01T00:00:00Z'),
up: async ({ blockchain }) => {
/* ... */
},
};
```
@@ -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;
}
},
};
@@ -0,0 +1,265 @@
import { Order } from '../src/models';
import { TypeOrmPaymentRepository } from '../src/infrastructure/database/typeorm/repositories/typeorm-payment.repository';
import { PaymentEntity } from '../src/infrastructure/database/typeorm/entities/payment.entity';
import { DataSource } from 'typeorm';
import config from '../src/config/config';
import { PaymentStatusEnum } from '../src/domain/gateway/enums/payment-status.enum';
import { PaymentTypeEnum, PaymentDirectionEnum, getPaymentDirection } from '../src/domain/gateway/enums/payment-type.enum';
import type { PaymentDomainInterface } from '../src/domain/gateway/interfaces/payment-domain.interface';
import { sha256 } from '../src/utils/sha256';
import { generator } from '../src/services/document.service';
export default {
name: 'Миграция платежей из MongoDB в PostgreSQL (унифицированная модель)',
validUntil: new Date('2025-07-03'), // Действует до
async up(): Promise<boolean> {
console.log('Выполнение миграции: Перенос платежей из MongoDB в PostgreSQL (унифицированная модель)');
try {
await generator.connect(config.mongoose.url);
// Инициализируем подключение к PostgreSQL
const dataSource = new DataSource({
type: 'postgres',
host: config.postgres.host,
port: Number(config.postgres.port),
username: config.postgres.username,
password: config.postgres.password,
database: config.postgres.database,
entities: [PaymentEntity],
synchronize: true, // Создаем таблицу если она не существует
});
await dataSource.initialize();
console.log('Подключение к PostgreSQL установлено');
// Создаем репозиторий для работы с платежами
const paymentRepository = new TypeOrmPaymentRepository(dataSource.getRepository(PaymentEntity));
// Получаем все заказы из MongoDB
const orders = await Order.find();
console.log(`Найдено ${orders.length} заказов в MongoDB`);
let migratedCount = 0;
let skippedCount = 0;
let errorCount = 0;
for (const order of orders) {
try {
// Генерируем hash из order_num для поиска существующих платежей
const hash = sha256(String(order.order_num));
// Проверяем, существует ли уже платеж с таким hash
const existingPayment = await paymentRepository.findByHash(hash);
if (existingPayment) {
console.log(`Платеж с hash ${hash} (order_num: ${order.order_num}) уже существует, пропускаем`);
skippedCount++;
continue;
}
// Мапим старые статусы на новые
const statusMapping: Record<string, PaymentStatusEnum> = {
pending: PaymentStatusEnum.PENDING,
paid: PaymentStatusEnum.PAID,
completed: PaymentStatusEnum.COMPLETED,
failed: PaymentStatusEnum.FAILED,
expired: PaymentStatusEnum.EXPIRED,
refunded: PaymentStatusEnum.REFUNDED,
};
// Мапим старые типы на новые
const typeMapping: Record<string, PaymentTypeEnum> = {
registration: PaymentTypeEnum.REGISTRATION,
deposit: PaymentTypeEnum.DEPOSIT,
};
const paymentType = typeMapping[order.type] || PaymentTypeEnum.REGISTRATION;
const paymentDirection = getPaymentDirection(paymentType);
const paymentStatus = statusMapping[order.status] || PaymentStatusEnum.PENDING;
// Проверяем, что все мигрируемые платежи действительно входящие
if (paymentDirection !== PaymentDirectionEnum.INCOMING) {
console.warn(`⚠️ Платеж ${order.order_num} имеет неожиданное направление ${paymentDirection}, пропускаем`);
skippedCount++;
continue;
}
// Создаем объект платежа согласно доменному интерфейсу
const paymentData: PaymentDomainInterface = {
hash, // используем сгенерированный hash вместо blockchain_num
coopname: order.creator,
username: order.username,
quantity: parseFloat(order.quantity),
symbol: order.symbol,
type: paymentType,
direction: paymentDirection,
status: paymentStatus,
provider: order.provider,
secret: order.secret,
message: order.message,
expired_at: order.expired_at,
created_at: (order as any).createdAt || new Date(), // Mongoose timestamps
updated_at: (order as any).updatedAt || new Date(),
payment_details: order.details
? {
data: order.details.data,
amount_plus_fee: order.details.amount_plus_fee,
amount_without_fee: order.details.amount_without_fee,
fee_amount: order.details.fee_amount,
fee_percent: order.details.fee_percent,
fact_fee_percent: order.details.fact_fee_percent,
tolerance_percent: order.details.tolerance_percent,
}
: undefined,
};
// Создаем платеж через репозиторий
await paymentRepository.create(paymentData);
migratedCount++;
console.log(
`✅ Мигрирован платеж ${order.order_num} -> hash: ${hash} (${paymentType}/${paymentDirection}, ${paymentStatus})`
);
} catch (error) {
errorCount++;
console.error(`❌ Ошибка при миграции платежа ${order.order_num}:`, error);
}
}
console.log('\n=== Результаты миграции ===');
console.log(`Мигрировано: ${migratedCount}`);
console.log(`Пропущено: ${skippedCount}`);
console.log(`Ошибок: ${errorCount}`);
console.log(`Всего обработано: ${orders.length}`);
// Проверяем, что все мигрированные платежи действительно входящие
console.log('\n=== Проверка миграции ===');
const allPayments = await paymentRepository.getAllPayments({}, { page: 1, limit: 1000, sortOrder: 'ASC' });
const incomingCount = allPayments.items.filter(
(p: PaymentDomainInterface) => p.direction === PaymentDirectionEnum.INCOMING
).length;
const outgoingCount = allPayments.items.filter(
(p: PaymentDomainInterface) => p.direction === PaymentDirectionEnum.OUTGOING
).length;
console.log(`Входящих платежей: ${incomingCount}`);
console.log(`Исходящих платежей: ${outgoingCount}`);
if (outgoingCount > 0) {
console.warn(`⚠️ Обнаружены исходящие платежи после миграции! Это может указывать на проблему.`);
}
// Дополнительная валидация данных
console.log('\n=== Валидация данных ===');
const registrationPayments = allPayments.items.filter(
(p: PaymentDomainInterface) => p.type === PaymentTypeEnum.REGISTRATION
).length;
const depositPayments = allPayments.items.filter(
(p: PaymentDomainInterface) => p.type === PaymentTypeEnum.DEPOSIT
).length;
console.log(`Регистрационных платежей: ${registrationPayments}`);
console.log(`Депозитных платежей: ${depositPayments}`);
// Проверяем обязательные поля
const paymentsWithMissingData = allPayments.items.filter(
(p: PaymentDomainInterface) => !p.coopname || !p.username || !p.quantity || !p.symbol || !p.provider || !p.secret
);
if (paymentsWithMissingData.length > 0) {
console.warn(`⚠️ Найдено ${paymentsWithMissingData.length} платежей с отсутствующими обязательными данными`);
paymentsWithMissingData.forEach((p: PaymentDomainInterface) => {
console.warn(` Платеж ${p.hash}: отсутствуют поля`, {
coopname: !p.coopname,
username: !p.username,
quantity: !p.quantity,
symbol: !p.symbol,
provider: !p.provider,
secret: !p.secret,
});
});
} else {
console.log('✅ Все платежи имеют обязательные поля');
}
// Проверяем корректность типов и направлений
const invalidDirections = allPayments.items.filter((p: PaymentDomainInterface) => {
const expectedDirection = getPaymentDirection(p.type);
return p.direction !== expectedDirection;
});
if (invalidDirections.length > 0) {
console.warn(`⚠️ Найдено ${invalidDirections.length} платежей с некорректным направлением`);
} else {
console.log('✅ Все платежи имеют корректные направления');
}
// Создаем базовые индексы для оптимизации запросов
console.log('\n=== Создание индексов ===');
try {
await dataSource.query(`
CREATE INDEX IF NOT EXISTS idx_payments_status ON payments(status);
`);
await dataSource.query(`
CREATE INDEX IF NOT EXISTS idx_payments_type ON payments(type);
`);
await dataSource.query(`
CREATE INDEX IF NOT EXISTS idx_payments_direction ON payments(direction);
`);
await dataSource.query(`
CREATE INDEX IF NOT EXISTS idx_payments_username ON payments(username);
`);
await dataSource.query(`
CREATE INDEX IF NOT EXISTS idx_payments_coopname ON payments(coopname);
`);
await dataSource.query(`
CREATE INDEX IF NOT EXISTS idx_payments_created_at ON payments(created_at);
`);
await dataSource.query(`
CREATE INDEX IF NOT EXISTS idx_payments_secret ON payments(secret);
`);
await dataSource.query(`
CREATE INDEX IF NOT EXISTS idx_payments_hash ON payments(hash);
`);
await dataSource.query(`
CREATE INDEX IF NOT EXISTS idx_payments_quantity ON payments(quantity);
`);
await dataSource.query(`
CREATE INDEX IF NOT EXISTS idx_payments_symbol ON payments(symbol);
`);
await dataSource.query(`
CREATE INDEX IF NOT EXISTS idx_payments_expired_at ON payments(expired_at);
`);
await dataSource.query(`
CREATE INDEX IF NOT EXISTS idx_payments_composite ON payments(coopname, username, status, type);
`);
console.log('Базовые индексы созданы');
} catch (indexError) {
console.warn('Ошибка при создании индексов:', indexError);
}
// Закрываем подключение к PostgreSQL
await dataSource.destroy();
console.log('Подключение к PostgreSQL закрыто');
console.log('Миграция завершена: Перенос платежей в унифицированную модель выполнен успешно');
return true;
} catch (error) {
console.error('Ошибка при выполнении миграции платежей:', error);
return false;
}
},
};
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "create-nodejs-express-app",
"version": "2025.5.5-2",
"version": "2025.7.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "create-nodejs-express-app",
"version": "2025.5.5-2",
"version": "2025.7.1",
"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.7.1",
"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",
File diff suppressed because it is too large Load Diff
+1 -2
View File
@@ -1,6 +1,6 @@
import swaggerJsdoc from 'swagger-jsdoc';
import swaggerDefinition from '../src/docs/swaggerDef';
import fs from 'fs'
import fs from 'fs';
import path from 'path';
const specs = swaggerJsdoc({
@@ -8,5 +8,4 @@ const specs = swaggerJsdoc({
apis: ['src/docs/*.yml', 'src/**/*.ts'],
});
fs.writeFileSync(path.join(__dirname, '../src/docs/swagger.json'), JSON.stringify(specs, null, 2));
+1 -1
View File
@@ -2,7 +2,7 @@ spectaql:
targetDir: ./docs
introspection:
# schemaFile: ./schema.gql
url: 'http://localhost:2998/v1/graphql'
url: 'https://testnet.coopenomics.world/api/v1/graphql'
info:
title: 'API-Документация'
+31 -9
View File
@@ -42,27 +42,44 @@ 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: 'Не должно быть пустым' }),
// Параметры блокчейна
ROOT_SYMBOL: z.string().default('AXON'),
ROOT_GOVERN_SYMBOL: z.string().default('RUB'),
ROOT_PRECISION: z
.string()
.default('4')
.transform((val) => parseInt(val, 10)),
ROOT_GOVERN_PRECISION: z
.string()
.default('4')
.transform((val) => parseInt(val, 10)),
});
// Валидация переменных окружения
@@ -86,9 +103,14 @@ 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,
root_symbol: envVars.data.ROOT_SYMBOL,
root_govern_symbol: envVars.data.ROOT_GOVERN_SYMBOL,
root_precision: envVars.data.ROOT_PRECISION,
root_govern_precision: envVars.data.ROOT_GOVERN_PRECISION,
},
mongoose: {
url: envVars.data.MONGODB_URL + (envVars.data.NODE_ENV === 'test' ? '-test' : ''),
@@ -0,0 +1 @@
export { default as config } from './config';
@@ -1,7 +1,6 @@
export * as authController from './auth.controller';
export * as userController from './user.controller';
export * as documentController from './document.controller';
export * as orderController from './order.controller';
export * as coopController from './coop.controller';
export * as notifyController from './notify.controller';
export * as systemController from './system.controller';
@@ -1,61 +0,0 @@
import http from 'http-status';
import catchAsync from '../utils/catchAsync';
import { orderService } from '../services';
import { ICreateDeposit } from '../types';
import { Request, Response } from 'express';
import httpStatus from 'http-status';
import pick from '../utils/pick';
import logger from '../config/logger';
import { ProviderInteractor } from '~/domain/provider/provider.interactor';
import { nestApp } from '..';
export const catchIPN = catchAsync(async (req: Request, res: Response) => {
const providerName = req.params.provider;
logger.info(`Recieve new IPN for provider ${providerName}`, { body: req.body, source: 'catchIPN' });
const providerInteractor = nestApp.get(ProviderInteractor);
const provider = providerInteractor.getProvider(providerName);
// Обрабатываем IPN данные
await provider.handleIPN(req.body);
res.status(200).send();
});
export const createDeposit = catchAsync(async (req: any, res: Response) => {
const username = req.user.username;
const result = await orderService.createDeposit(username, req.body as ICreateDeposit);
res.status(http.CREATED).send(result);
});
export const createInitialPayment = catchAsync(async (req: any, res: Response) => {
const username = req.user.username;
const result = await orderService.createInitialOrder(username);
res.status(http.CREATED).send(result);
});
export const getOrders = catchAsync(async (req: Request, res: Response) => {
const filter = pick(req.query, ['username', 'id', 'order_num', 'status']);
const options = pick(req.query, ['sortBy', 'limit', 'page']);
const orders = await orderService.getOrders(filter, options);
res.status(httpStatus.OK).send(orders);
});
export const getMyOrders = catchAsync(async (req: Request, res: Response) => {
const filter = pick(req.query, ['status', 'id']);
const options = pick(req.query, ['sortBy', 'limit', 'page']);
filter.username = req.params.username;
const orders = await orderService.getOrders(filter, options);
res.status(httpStatus.OK).send(orders);
});
export const setStatus = catchAsync(async (req: Request, res: Response) => {
const { status, id } = req.body;
await orderService.setStatus(id, status);
res.status(httpStatus.OK).send();
});
@@ -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 {}
@@ -17,6 +17,10 @@ import { AccountType } from '~/modules/account/enum/account-type.enum';
import { ORGANIZATION_REPOSITORY, OrganizationRepository } from '~/domain/common/repositories/organization.repository';
import { INDIVIDUAL_REPOSITORY, IndividualRepository } from '~/domain/common/repositories/individual.repository';
import { ENTREPRENEUR_REPOSITORY, EntrepreneurRepository } from '~/domain/common/repositories/entrepreneur.repository';
import {
SEARCH_PRIVATE_ACCOUNTS_REPOSITORY,
SearchPrivateAccountsRepository,
} from '~/domain/common/repositories/search-private-accounts.repository';
import { IndividualDomainEntity } from '~/domain/branch/entities/individual-domain.entity';
import { OrganizationDomainEntity } from '~/domain/branch/entities/organization-domain.entity';
import { EntrepreneurDomainEntity } from '~/domain/branch/entities/entrepreneur-domain.entity';
@@ -24,6 +28,10 @@ import { ACCOUNT_BLOCKCHAIN_PORT, AccountBlockchainPort } from '../interfaces/ac
import { CANDIDATE_REPOSITORY, CandidateRepository } from '../repository/candidate.repository';
import { userStatus } from '~/types/user.types';
import { sha256 } from '~/utils/sha256';
import type {
SearchPrivateAccountsInputDomainInterface,
PrivateAccountSearchResultDomainInterface,
} from '~/domain/common/interfaces/search-private-accounts-domain.interface';
@Injectable()
export class AccountDomainInteractor {
@@ -32,6 +40,8 @@ export class AccountDomainInteractor {
@Inject(ORGANIZATION_REPOSITORY) private readonly organizationRepository: OrganizationRepository,
@Inject(INDIVIDUAL_REPOSITORY) private readonly individualRepository: IndividualRepository,
@Inject(ENTREPRENEUR_REPOSITORY) private readonly entrepreneurRepository: EntrepreneurRepository,
@Inject(SEARCH_PRIVATE_ACCOUNTS_REPOSITORY)
private readonly searchPrivateAccountsRepository: SearchPrivateAccountsRepository,
@Inject(ACCOUNT_BLOCKCHAIN_PORT) private readonly accountBlockchainPort: AccountBlockchainPort,
@Inject(CANDIDATE_REPOSITORY) private readonly candidateRepository: CandidateRepository
) {}
@@ -39,7 +49,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 +82,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}`);
}
@@ -160,4 +186,21 @@ export class AccountDomainInteractor {
throw new HttpException(`Ошибка при регистрации в блокчейне: ${error.message}`, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
/**
* Поиск приватных аккаунтов по запросу
* @param input Входные данные для поиска
* @returns Массив результатов поиска
*/
async searchPrivateAccounts(
input: SearchPrivateAccountsInputDomainInterface
): Promise<PrivateAccountSearchResultDomainInterface[]> {
this.logger.log(`Поиск приватных аккаунтов по запросу: "${input.query}"`);
const results = await this.searchPrivateAccountsRepository.searchPrivateAccounts(input);
this.logger.log(`Найдено ${results.length} приватных аккаунтов`);
return results;
}
}
@@ -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(
@@ -242,7 +242,7 @@ export class BranchDomainInteractor {
async selectBranch(data: SelectBranchInputDomainInterface): Promise<boolean> {
// TODO move it to separate document domain service for validate
const document = await this.documentRepository.findByHash(data.document.hash);
const document = await this.documentRepository.findByHash(data.document.doc_hash);
if (!document) throw new BadRequestException('Документ не найден');
if (data.document.meta.registry_id != Cooperative.Registry.SelectBranchStatement.registry_id)
@@ -0,0 +1,28 @@
import type { IndividualDomainInterface } from './individual-domain.interface';
import type { EntrepreneurDomainInterface } from './entrepreneur-domain.interface';
import type { OrganizationDomainInterface } from './organization-domain.interface';
/**
* Интерфейс для входных данных поиска приватных аккаунтов
*/
export interface SearchPrivateAccountsInputDomainInterface {
query: string;
}
/**
* Тип данных результата поиска - union type всех возможных типов приватных данных
*/
export type PrivateAccountSearchDataDomainInterface =
| IndividualDomainInterface
| EntrepreneurDomainInterface
| OrganizationDomainInterface;
/**
* Интерфейс результата поиска приватных аккаунтов
*/
export interface PrivateAccountSearchResultDomainInterface {
type: 'individual' | 'entrepreneur' | 'organization';
data: PrivateAccountSearchDataDomainInterface;
score?: number;
highlightedFields?: string[];
}
@@ -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'); // Создаем уникальный токен
@@ -0,0 +1,20 @@
import type {
SearchPrivateAccountsInputDomainInterface,
PrivateAccountSearchResultDomainInterface,
} from '../interfaces/search-private-accounts-domain.interface';
/**
* Доменный репозиторий для поиска приватных аккаунтов
*/
export interface SearchPrivateAccountsRepository {
/**
* Поиск приватных аккаунтов по запросу
* @param input Входные данные для поиска
* @returns Массив результатов поиска
*/
searchPrivateAccounts(
input: SearchPrivateAccountsInputDomainInterface
): Promise<PrivateAccountSearchResultDomainInterface[]>;
}
export const SEARCH_PRIVATE_ACCOUNTS_REPOSITORY = Symbol('SearchPrivateAccountsRepository');
@@ -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 {}
@@ -5,17 +5,18 @@ import type { DocumentDomainEntity } from '../entity/document-domain.entity';
import type { PaginationResultDomainInterface } from '~/domain/common/interfaces/pagination.interface';
import type { DocumentPackageAggregateDomainInterface } from '../interfaces/document-package-aggregate-domain.interface';
import type { GetDocumentsInputDomainInterface } from '../interfaces/get-documents-input-domain.interface';
import { toDotNotation } from '~/utils/toDotNotation';
import { getActions } from '~/utils/getFetch';
import { SovietContract } from 'cooptypes';
import { DocumentPackageAggregator } from '../aggregators/document-package.aggregator';
import { DocumentAggregator } from '../aggregators/document.aggregator';
import type { DocumentAggregateDomainInterface } from '../interfaces/document-domain-aggregate.interface';
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 +47,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);
}
}

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