Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 11a891392e | |||
| 7930ef8ef6 | |||
| 8484a14905 | |||
| e7a0968b94 | |||
| 87241b2de0 | |||
| 4a5fb2af8b | |||
| b9acc67818 | |||
| 5be9fa6ae8 | |||
| e0f8d6a64c | |||
| 2276ab396e | |||
| 9de84afc29 | |||
| 747e567666 | |||
| 675376079e | |||
| 1fa0cee816 | |||
| 6a7f71eb5a | |||
| 6bda6948bf | |||
| abb43b4221 | |||
| 4193c45205 | |||
| 36eb50fef9 | |||
| ad0ebf82ee | |||
| 54d55e288f | |||
| 9fb51b00bd | |||
| d56dcc153a | |||
| d9f421acbd | |||
| 01ec687ba0 | |||
| 0ee1efac4e | |||
| fbf9d514a0 | |||
| be7a9e9e3a | |||
| bc3b3f701f | |||
| 0d8be7a489 | |||
| 2fdeaed3a0 | |||
| 13481d8f95 | |||
| f4b87baa2b | |||
| a337dbceb8 | |||
| 04592a9908 | |||
| 9d204ba496 | |||
| 64dd48b408 | |||
| 515a374f86 | |||
| ddae8d6a53 | |||
| 6e87047e1d | |||
| 62e13853c5 | |||
| a09959d751 | |||
| e2678df443 | |||
| fedec69e44 |
@@ -10,6 +10,7 @@ alwaysApply: false
|
||||
2. Типы IName, IChecksum256, ITimePointSec - это просто строки.
|
||||
3. Домен должен быть полностью изолирован от инфраструктурных деталей.
|
||||
4. Направление зависимостей - внутрь (к ядру, домену).
|
||||
5. Домен, инфраструктура и приложение связаны через App.module, НЕ НУЖНО импортировать их друг в друга, а достаточно просто импортировать домен на уровне приложения чтобы использовать все экспорты домена.
|
||||
|
||||
## Структура проекта
|
||||
- `domain/` - доменный слой (бизнес-логика, независимая от инфраструктуры)
|
||||
|
||||
@@ -10,3 +10,5 @@ alwaysApply: true
|
||||
- Стиль шаблонов PUG на composite API.
|
||||
- Всегда создаём index.ts файлы для vue компонент.
|
||||
- Создавай индексные файлы для vue через export {default as NAME} from './where', а на уровне выше, если это необходимо, делай export * from './ui'
|
||||
- Всегда удаляем неиспользуемые импорты.
|
||||
- Не используем emit избыточно. Вместо них используем композабл функции моделей фич, если необходимо. Emit только в крайнем случае.
|
||||
@@ -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' %}`
|
||||
@@ -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
|
||||
- Собрания с вопросами и решениями
|
||||
|
||||
Фабрика поддерживает полный цикл создания документов кооператива от заявлений до протоколов собраний с возможностью кастомизации под разные типы кооперативов и требования.
|
||||
@@ -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 }}'
|
||||
|
||||
@@ -1,3 +1,27 @@
|
||||
# v2025.5.14
|
||||
|
||||
В этом релизе реализован новый стандарт передачи и хранения документов по блокчейну с поддержкой неограниченного количества подписей и их валидацией. Также внесены улучшения в интерфейс и исправлены ошибки.
|
||||
|
||||
✨ Новые функции
|
||||
- [#252](https://github.com/coopenomics/mono/issues/252): Внедрение обновленного стандарта хранения и передачи документов по блокчейну
|
||||
- [#251](https://github.com/coopenomics/mono/issues/251): Реализация версионированного мигратора данных для контроллера кооператива
|
||||
- [#244](https://github.com/coopenomics/mono/issues/244): Обновление стандарта сборки документов и переход на хэш-идентификаторы
|
||||
|
||||
🐛 Исправления ошибок
|
||||
- [#249](https://github.com/coopenomics/mono/issues/249): Исправлена спутанная маршрутизация между рабочими столами кооперативов
|
||||
- [#253](https://github.com/coopenomics/mono/issues/253): Исправлена избыточная точность суммы оплаты в заявлении на вступление
|
||||
|
||||
🔧 Улучшения
|
||||
- [#259](https://github.com/coopenomics/mono/issues/259): Исправлены отступы в мобильной карточке пайщика на странице пайщиков
|
||||
- [#258](https://github.com/coopenomics/mono/issues/258): Удалён hover-эффект для документов и пайщиков в таблице
|
||||
- [#257](https://github.com/coopenomics/mono/issues/257): Добавлено сохранение светлой/тёмной темы в localStorage и восстановление при загрузке страницы
|
||||
- [#256](https://github.com/coopenomics/mono/issues/256): Отображение статуса членства каждого пайщика в разделе "Пайщики"
|
||||
- [#255](https://github.com/coopenomics/mono/issues/255): Отображение ФИО/Наименования плательщика в разделе "Платежи"
|
||||
|
||||
#releases
|
||||
|
||||
---
|
||||
|
||||
# v2025.5.2
|
||||
|
||||
В этом релизе рабочие столы переведены на серверный рендеринг, что улучшает стабильность развертывания и упрощает автоматизацию поставки ПО. Данный релиз является подготовительным к переходу на новый стандарт цифровых документов на платформе.
|
||||
|
||||
Vendored
BIN
Binary file not shown.
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@coopenomics/boot",
|
||||
"type": "module",
|
||||
"version": "2025.5.5-2",
|
||||
"version": "2025.6.13",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@9.0.6",
|
||||
"description": "",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "@coopenomics/cleos",
|
||||
"version": "2025.5.5-2",
|
||||
"version": "2025.6.13",
|
||||
"private": true,
|
||||
"description": "",
|
||||
"scripts": {
|
||||
|
||||
@@ -312,6 +312,7 @@ struct [[eosio::table, eosio::contract(REGISTRATOR)]] cooperative2 {
|
||||
eosio::binary_extension<eosio::name> status; ///< Статус процесса подключения
|
||||
eosio::binary_extension<eosio::time_point_sec> created_at; ///< Дата поступления заявки на подключение (pending | ... | active | blocked)
|
||||
eosio::binary_extension<document2> document; ///< Подписанный документ соглашения на подключение
|
||||
eosio::binary_extension<uint64_t> active_participants_count; ///< Счетчик активных пайщиков в кооперативе
|
||||
|
||||
|
||||
/**
|
||||
|
||||
@@ -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>>,
|
||||
|
||||
@@ -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, //клайм прироста капитализации из задания
|
||||
|
||||
@@ -42,13 +42,27 @@ struct document2 {
|
||||
std::vector<signature_info> signatures;
|
||||
};
|
||||
|
||||
void verify_document_or_fail(const document2 &doc)
|
||||
{
|
||||
void verify_document_or_fail(
|
||||
const document2 &doc,
|
||||
const std::vector<eosio::name>& required_signers = {}
|
||||
) {
|
||||
for (const auto &sig : doc.signatures) {
|
||||
// Проверка завершится прерыванием, если восстановление подписи провалится
|
||||
assert_recover_key(sig.signed_hash, sig.signature, sig.public_key);
|
||||
}
|
||||
};
|
||||
if (!required_signers.empty()) {
|
||||
for (const auto& required : required_signers) {
|
||||
bool found = false;
|
||||
for (const auto& sig : doc.signatures) {
|
||||
if (sig.signer == required) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
eosio::check(found, ("Не найдена подпись от обязательного подписанта: " + required.to_string()).c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool is_empty_document(const document2 &doc)
|
||||
{
|
||||
|
||||
@@ -51,4 +51,23 @@ namespace Registrator {
|
||||
|
||||
return *itr;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Получает количество активных пайщиков кооператива
|
||||
* @param coopname Имя кооператива
|
||||
* @return uint64_t Количество активных пайщиков
|
||||
*/
|
||||
inline uint64_t get_active_participants_count(eosio::name coopname) {
|
||||
using namespace eosio;
|
||||
|
||||
// Получаем информацию о кооперативе
|
||||
cooperatives2_index cooperatives(_registrator, _registrator.value);
|
||||
auto coop_itr = cooperatives.find(coopname.value);
|
||||
|
||||
if (coop_itr == cooperatives.end() || !coop_itr->is_cooperative || !coop_itr->active_participants_count.has_value()) {
|
||||
eosio::check(false, "Счетчик пайщиков кооператива не найден");
|
||||
}
|
||||
|
||||
return coop_itr->active_participants_count.value();
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,9 @@ using newdeclined_interface = void(NEWDECLINED_SIGNATURE);
|
||||
#define NEWACT_SIGNATURE name coopname, name username, name action, checksum256 package, document2 document
|
||||
using newact_interface = void(NEWACT_SIGNATURE);
|
||||
|
||||
#define NEWLINK_SIGNATURE name coopname, name username, name action, checksum256 package, document2 document
|
||||
using newlink_interface = void(NEWLINK_SIGNATURE);
|
||||
|
||||
#define NEWAGREEMENT_SIGNATURE name coopname, name username, name type, document2 document
|
||||
using newagreement_interface = void(NEWAGREEMENT_SIGNATURE);
|
||||
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
#define NEWGDECISION_SIGNATURE \
|
||||
name coopname, /* Имя кооператива */ \
|
||||
name presider, /* Имя председателя */ \
|
||||
name secretary, /* Имя секретаря */ \
|
||||
checksum256 hash, /* Хэш собрания */ \
|
||||
std::vector<question_result> results, /* Итог по вопросам */ \
|
||||
uint64_t signed_ballots, /* Число подписанных бюллетеней */ \
|
||||
uint64_t quorum_percent, /* Процент кворума */ \
|
||||
bool quorum_passed /* Флаг достигнутого кворума */
|
||||
double quorum_percent, /* Процент кворума */ \
|
||||
bool quorum_passed, /* Флаг достигнутого кворума */ \
|
||||
document2 decision /* Протокол общего собрания */
|
||||
|
||||
using newgdecision_interface = void(NEWGDECISION_SIGNATURE);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Например, во внешем header можно объявить структуру:
|
||||
struct question_result {
|
||||
uint64_t question_id;
|
||||
uint64_t number;
|
||||
std::string title;
|
||||
std::string decision;
|
||||
std::string context;
|
||||
|
||||
@@ -8,7 +8,8 @@ struct [[eosio::table, eosio::contract(MEET)]] meet {
|
||||
uint64_t id; /// Идентификатор собрания.
|
||||
checksum256 hash; /// Внешний хэш-идентификатор собрания.
|
||||
name coopname; ///< Имя кооператива.
|
||||
name type; ///< Тип общего собрания (annual | special)
|
||||
name type; ///< Тип общего собрания (regular | extra)
|
||||
name level; ///< Уровень общего собрания (cooperative | branch)
|
||||
name initiator; ///< Имя пользователя, который инициировал собрание.
|
||||
name presider; ///< Имя аккаунта председателя собрания.
|
||||
name secretary; ///< Имя аккаунта секретаря собрания
|
||||
@@ -18,12 +19,13 @@ struct [[eosio::table, eosio::contract(MEET)]] meet {
|
||||
time_point_sec created_at; ///< Дата создания предложения.
|
||||
time_point_sec open_at; ///< Дата начала собрания.
|
||||
time_point_sec close_at; ///< Дата завершения собрания.
|
||||
std::vector<name> notified_users; ///< Пользователи, которые подписали уведомление
|
||||
|
||||
uint64_t quorum_percent = 75; /// Цель по кворуму, чтобы собрание считалось состоявшимся
|
||||
double quorum_percent = 75.0; /// Цель по кворуму, чтобы собрание считалось состоявшимся
|
||||
uint64_t signed_ballots; /// Общее количество подписанных бюллетеней
|
||||
uint64_t current_quorum_percent; /// Текущий процент кворума
|
||||
double current_quorum_percent = 0; /// Текущий процент кворума
|
||||
uint64_t cycle = 1; /// Цикл общего собрания, который указывает на то, что были предыдущие собрания, которые не состоялись по кворуму
|
||||
bool quorum_passed; /// Флаг пройденного кворума
|
||||
bool quorum_passed = false; /// Флаг пройденного кворума
|
||||
|
||||
document2 proposal; ///< Предложение
|
||||
document2 authorization; ///< Решение совета
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// Заглушка для получения числа пайщиков кооператива.
|
||||
uint64_t get_total_participants(eosio::name coopname) {
|
||||
// Здесь можно реализовать получение актуальных данных, например, через inline action или вызов другого контракта.
|
||||
return 2; // Пример: 100 пайщиков
|
||||
// Используем метод из shared_registrator для получения актуального количества активных пайщиков
|
||||
return Registrator::get_active_participants_count(coopname);
|
||||
}
|
||||
|
||||
void meet::vote(name coopname, checksum256 hash, name username, document2 ballot, std::vector<vote_point> votes) {
|
||||
@@ -83,7 +83,7 @@ void meet::vote(name coopname, checksum256 hash, name username, document2 ballot
|
||||
genmeets.modify(meet_itr, coopname, [&](auto &m) {
|
||||
m.signed_ballots++; // регистрируем принятый бюллетень
|
||||
uint64_t total_participants = get_total_participants(coopname);
|
||||
m.current_quorum_percent = (m.signed_ballots * 100) / total_participants;
|
||||
m.current_quorum_percent = (static_cast<double>(m.signed_ballots) * 100.0) / static_cast<double>(total_participants);
|
||||
if (m.current_quorum_percent > m.quorum_percent) {
|
||||
m.quorum_passed = true;
|
||||
}
|
||||
@@ -100,4 +100,15 @@ void meet::vote(name coopname, checksum256 hash, name username, document2 ballot
|
||||
// Здесь можно при необходимости обновить поле решения, логирование или вызвать inline action.
|
||||
}
|
||||
}
|
||||
|
||||
Action::send<newlink_interface>(
|
||||
_soviet,
|
||||
"newlink"_n,
|
||||
_meet,
|
||||
coopname,
|
||||
username,
|
||||
get_valid_soviet_action("ballot"_n),
|
||||
hash,
|
||||
ballot
|
||||
);
|
||||
}
|
||||
@@ -21,5 +21,6 @@
|
||||
#include "src/coop/enabranches.cpp"
|
||||
#include "src/coop/disbranches.cpp"
|
||||
#include "src/coop/updatecoop.cpp"
|
||||
#include "src/coop/decparticpnt.cpp"
|
||||
|
||||
#include "src/verification/verificate.cpp"
|
||||
|
||||
@@ -67,5 +67,8 @@ public:
|
||||
|
||||
[[eosio::action]] void enabranches(eosio::name coopname);
|
||||
[[eosio::action]] void disbranches(eosio::name coopname);
|
||||
|
||||
// Уменьшение счетчика активных пайщиков при блокировке
|
||||
[[eosio::action]] void decparticpnt(eosio::name coopname, eosio::name username);
|
||||
|
||||
};
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* @brief Уменьшает счетчик активных пайщиков кооператива
|
||||
* @param coopname Имя кооператива
|
||||
* @param username Имя пайщика
|
||||
*/
|
||||
void registrator::decparticpnt(eosio::name coopname, eosio::name username)
|
||||
{
|
||||
// Проверка авторизации - может вызывать только контракт soviet
|
||||
require_auth(_soviet);
|
||||
|
||||
// Получаем информацию о кооперативе
|
||||
cooperatives2_index cooperatives(_registrator, _registrator.value);
|
||||
auto coop_itr = cooperatives.find(coopname.value);
|
||||
|
||||
if (coop_itr != cooperatives.end() && coop_itr->is_cooperative) {
|
||||
if (coop_itr->active_participants_count.has_value() && coop_itr->active_participants_count.value() > 0) {
|
||||
// Уменьшаем счетчик только если он больше нуля
|
||||
cooperatives.modify(coop_itr, _registrator, [&](auto &coop) {
|
||||
coop.active_participants_count = coop.active_participants_count.value() - 1;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,80 +1,35 @@
|
||||
[[eosio::action]] void registrator::migrate() {
|
||||
require_auth(_registrator);
|
||||
// Registrator::candidates_legacy_index candidates_legacy(_registrator, _provider.value);
|
||||
// auto candidate_legacy_itr = candidates_legacy.begin();
|
||||
// while (candidate_legacy_itr != candidates_legacy.end()) {
|
||||
// candidate_legacy_itr = candidates_legacy.erase(candidate_legacy_itr);
|
||||
// }
|
||||
|
||||
|
||||
|
||||
// Новый код: удаление всех записей из cooperatives2_index
|
||||
// cooperatives2_index coops2(_registrator, _registrator.value);
|
||||
// auto coop2_itr = coops2.begin();
|
||||
// while (coop2_itr != coops2.end()) {
|
||||
// coop2_itr = coops2.erase(coop2_itr);
|
||||
// }
|
||||
|
||||
cooperatives_index coops(_registrator, _registrator.value);
|
||||
// Миграция cooperative -> cooperative2
|
||||
// Получаем таблицу кооперативов
|
||||
cooperatives2_index coops2(_registrator, _registrator.value);
|
||||
|
||||
for (auto coop_itr = coops.begin(); coop_itr != coops.end(); ++coop_itr) {
|
||||
// Проверяем, что запись еще не существует в cooperatives2
|
||||
if (coops2.find(coop_itr->username.value) == coops2.end()) {
|
||||
// Создаем новую запись в cooperative2
|
||||
coops2.emplace(_registrator, [&](auto& new_coop) {
|
||||
new_coop.username = coop_itr->username;
|
||||
new_coop.parent_username = coop_itr->parent_username;
|
||||
new_coop.announce = coop_itr->announce;
|
||||
new_coop.description = coop_itr->description;
|
||||
new_coop.is_cooperative = coop_itr->is_cooperative;
|
||||
new_coop.is_branched = coop_itr->is_branched;
|
||||
new_coop.is_enrolled = coop_itr->is_enrolled;
|
||||
new_coop.coop_type = coop_itr->coop_type;
|
||||
new_coop.registration = coop_itr->registration;
|
||||
new_coop.initial = coop_itr->initial;
|
||||
new_coop.minimum = coop_itr->minimum;
|
||||
|
||||
if (coop_itr->org_registration.has_value())
|
||||
new_coop.org_registration = coop_itr->org_registration;
|
||||
if (coop_itr->org_initial.has_value())
|
||||
new_coop.org_initial = coop_itr->org_initial;
|
||||
if (coop_itr->org_minimum.has_value())
|
||||
new_coop.org_minimum = coop_itr->org_minimum;
|
||||
if (coop_itr->status.has_value())
|
||||
new_coop.status = coop_itr->status;
|
||||
if (coop_itr->created_at.has_value())
|
||||
new_coop.created_at = coop_itr->created_at;
|
||||
|
||||
// Если есть документ в кооперативе, преобразуем его в document2
|
||||
if (coop_itr->document.has_value()) {
|
||||
document2 new_doc;
|
||||
new_doc.version = "0";
|
||||
new_doc.hash = coop_itr->document.value().hash;
|
||||
new_doc.doc_hash = coop_itr->document.value().hash;
|
||||
new_doc.meta_hash = eosio::checksum256();
|
||||
new_doc.meta = coop_itr->document.value().meta;
|
||||
|
||||
// Добавляем информацию о подписи
|
||||
signature_info sig;
|
||||
sig.id = 0;
|
||||
sig.signer = coop_itr->username;
|
||||
sig.public_key = coop_itr->document.value().public_key;
|
||||
sig.signature = coop_itr->document.value().signature;
|
||||
sig.signed_hash = coop_itr->document.value().hash;
|
||||
// Используем created_at как время подписания, если доступно
|
||||
if (coop_itr->created_at.has_value()) {
|
||||
sig.signed_at = coop_itr->created_at.value();
|
||||
} else {
|
||||
// В противном случае используем текущее время
|
||||
sig.signed_at = eosio::time_point_sec(eosio::current_time_point());
|
||||
}
|
||||
|
||||
new_doc.signatures.push_back(sig);
|
||||
new_coop.document = new_doc;
|
||||
}
|
||||
});
|
||||
// Для каждого кооператива проверяем наличие счетчика активных пайщиков
|
||||
for (auto coop_itr = coops2.begin(); coop_itr != coops2.end(); ++coop_itr) {
|
||||
// Если счетчик активных пайщиков уже установлен, пропускаем кооператив
|
||||
if (coop_itr->active_participants_count.has_value()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Получаем имя кооператива для использования как scope
|
||||
eosio::name coopname = coop_itr->username;
|
||||
|
||||
// Получаем таблицу участников из контракта soviet с scope кооператива
|
||||
participants_index participants(_soviet, coopname.value);
|
||||
|
||||
// Счетчик активных пайщиков
|
||||
uint64_t active_count = 0;
|
||||
|
||||
// Проходим по всем участникам и считаем активных (has_vote == true)
|
||||
for (auto part_it = participants.begin(); part_it != participants.end(); ++part_it) {
|
||||
if (part_it->has_vote) {
|
||||
active_count++;
|
||||
}
|
||||
}
|
||||
|
||||
// Обновляем запись кооператива, устанавливая счетчик активных пайщиков
|
||||
coops2.modify(coop_itr, _registrator, [&](auto& coop) {
|
||||
coop.active_participants_count = active_count;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,20 @@ void registrator::confirmreg(eosio::name coopname, checksum256 registration_hash
|
||||
std::make_tuple(coopname, candidate -> initial)
|
||||
).send();
|
||||
|
||||
// Увеличиваем счетчик активных пайщиков
|
||||
cooperatives2_index cooperatives(_registrator, _registrator.value);
|
||||
auto coop_itr = cooperatives.find(coopname.value);
|
||||
|
||||
if (coop_itr != cooperatives.end() && coop_itr->is_cooperative) {
|
||||
cooperatives.modify(coop_itr, _registrator, [&](auto &coop) {
|
||||
if (coop.active_participants_count.has_value()) {
|
||||
coop.active_participants_count = coop.active_participants_count.value() + 1;
|
||||
} else {
|
||||
coop.active_participants_count = 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Registrator::candidates_index candidates(_registrator, coopname.value);
|
||||
auto it = candidates.find(candidate -> username.value);
|
||||
candidates.erase(it);
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
#include "src/agreement/declineagree.cpp"
|
||||
#include "src/agreement/sndagreement.cpp"
|
||||
#include "src/agreement/migrateagree.cpp"
|
||||
|
||||
#include "src/approve/confirmapprv.cpp"
|
||||
#include "src/approve/createapprv.cpp"
|
||||
#include "src/approve/declineapprv.cpp"
|
||||
@@ -41,6 +40,7 @@
|
||||
|
||||
#include "src/doc/declinedoc.cpp"
|
||||
#include "src/doc/newact.cpp"
|
||||
#include "src/doc/newlink.cpp"
|
||||
#include "src/doc/newdecision.cpp"
|
||||
#include "src/doc/newdeclined.cpp"
|
||||
#include "src/doc/newresolved.cpp"
|
||||
|
||||
@@ -50,6 +50,7 @@ public:
|
||||
[[eosio::action]] void newpackage(NEWPACKAGE_SIGNATURE);
|
||||
|
||||
[[eosio::action]] void newact(NEWACT_SIGNATURE);
|
||||
[[eosio::action]] void newlink(NEWLINK_SIGNATURE);
|
||||
[[eosio::action]] void newdeclined(NEWDECLINED_SIGNATURE);
|
||||
|
||||
[[eosio::action]] void declinedoc(eosio::name coopname, eosio::name username, checksum256 hash, document2 document);
|
||||
|
||||
@@ -39,7 +39,7 @@ void soviet::migrateagree(eosio::name coopname, uint64_t agreement_id) {
|
||||
new_doc.signatures.push_back(sig);
|
||||
|
||||
// Создаем новую запись в agreement2 с тем же ID
|
||||
eosio::multi_index<"agreements2"_n, agreement2> agreements2(_self, coopname.value);
|
||||
agreements2_index agreements2(_self, coopname.value);
|
||||
|
||||
agreements2.emplace(_self, [&](auto& new_agreement) {
|
||||
new_agreement.id = agreement_itr->id;
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
[[eosio::action]] void soviet::newlink(NEWLINK_SIGNATURE) {
|
||||
check_auth_and_get_payer_or_fail(contracts_whitelist);
|
||||
|
||||
require_recipient(coopname);
|
||||
require_recipient(username);
|
||||
};
|
||||
@@ -10,10 +10,25 @@ void soviet::block(eosio::name coopname, eosio::name admin, eosio::name username
|
||||
participants_index participants(_soviet, coopname.value);
|
||||
auto participant = participants.find(username.value);
|
||||
|
||||
// Сохраняем текущие значения статуса и права голоса
|
||||
bool had_vote = participant->has_vote;
|
||||
bool was_active = participant->status == "accepted"_n;
|
||||
|
||||
participants.modify(participant, _soviet, [&](auto &row){
|
||||
row.status = "blocked"_n;
|
||||
row.is_initial = false;
|
||||
row.is_minimum = false;
|
||||
row.has_vote = false;
|
||||
});
|
||||
|
||||
// Если участник был активен и имел право голоса, уменьшаем счетчик активных пайщиков
|
||||
if (was_active && had_vote) {
|
||||
// Отправляем уведомление в registrator для уменьшения счетчика
|
||||
action(
|
||||
permission_level{_soviet, "active"_n},
|
||||
_registrator,
|
||||
"decparticpnt"_n,
|
||||
std::make_tuple(coopname, username)
|
||||
).send();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@coopenomics/contracts",
|
||||
"version": "2025.5.5-2",
|
||||
"version": "2025.6.13",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build:all": "./build-all.sh",
|
||||
|
||||
@@ -97,6 +97,7 @@ export interface IQuestion {
|
||||
|
||||
export interface IQuestionResult {
|
||||
question_id: IUint64
|
||||
number: IUint64
|
||||
title: string
|
||||
decision: string
|
||||
context: string
|
||||
|
||||
@@ -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
|
||||
@@ -144,7 +144,7 @@
|
||||
<li><a href="#definition-AddParticipantInput">AddParticipantInput</a></li>
|
||||
<li><a href="#definition-AddTrustedAccountInput">AddTrustedAccountInput</a></li>
|
||||
<li><a href="#definition-AgendaMeetPoint">AgendaMeetPoint</a></li>
|
||||
<li><a href="#definition-AgendaMeetPointInput">AgendaMeetPointInput</a></li>
|
||||
<li><a href="#definition-AgendaGeneralMeetPointInput">AgendaGeneralMeetPointInput</a></li>
|
||||
<li><a href="#definition-AgendaWithDocuments">AgendaWithDocuments</a></li>
|
||||
<li><a href="#definition-AgreementInput">AgreementInput</a></li>
|
||||
<li><a href="#definition-AgreementVar">AgreementVar</a></li>
|
||||
@@ -9706,11 +9706,11 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="definition-AgendaMeetPointInput" class="definition definition-input-object" data-traverse-target="definition-AgendaMeetPointInput">
|
||||
<section id="definition-AgendaGeneralMeetPointInput" class="definition definition-input-object" data-traverse-target="definition-AgendaGeneralMeetPointInput">
|
||||
<div class="definition-group-name">
|
||||
<a href="#group-Types">Types</a>
|
||||
</div>
|
||||
<h2 class="definition-heading">AgendaMeetPointInput</h2>
|
||||
<h2 class="definition-heading">AgendaGeneralMeetPointInput</h2>
|
||||
<div class="doc-row">
|
||||
<div class="doc-copy">
|
||||
<div class="definition-description doc-copy-section">
|
||||
@@ -13616,7 +13616,7 @@
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<span class="property-name"><code>agenda</code></span> - <span class="property-type"><a href="#definition-AgendaMeetPointInput"><code>[AgendaMeetPointInput!]!</code></a></span>
|
||||
<span class="property-name"><code>agenda</code></span> - <span class="property-type"><a href="#definition-AgendaGeneralMeetPointInput"><code>[AgendaGeneralMeetPointInput!]!</code></a></span>
|
||||
</td>
|
||||
<td> Повестка собрания </td>
|
||||
</tr>
|
||||
@@ -13678,7 +13678,7 @@
|
||||
<html>
|
||||
<head></head>
|
||||
<body><pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
|
||||
<span class="hljs-attr">"agenda"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><span class="hljs-string">AgendaMeetPointInput</span><span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>
|
||||
<span class="hljs-attr">"agenda"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><span class="hljs-string">AgendaGeneralMeetPointInput</span><span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>
|
||||
<span class="hljs-attr">"close_at"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"2007-12-03T10:15:30Z"</span><span class="hljs-punctuation">,</span>
|
||||
<span class="hljs-attr">"coopname"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"abc123"</span><span class="hljs-punctuation">,</span>
|
||||
<span class="hljs-attr">"hash"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"xyz789"</span><span class="hljs-punctuation">,</span>
|
||||
@@ -24721,4 +24721,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -2,6 +2,7 @@ import mongoose from 'mongoose';
|
||||
|
||||
export default {
|
||||
name: 'Добавление поля fact_address для организаций',
|
||||
validUntil: new Date(), // Текущая дата, миграция больше не будет применяться
|
||||
|
||||
async up() {
|
||||
console.log('Выполнение миграции: Добавление поля fact_address для организаций');
|
||||
|
||||
@@ -2,6 +2,7 @@ import mongoose from 'mongoose';
|
||||
|
||||
export default {
|
||||
name: 'Преобразование числовых method_id в строковые',
|
||||
validUntil: new Date(), // Текущая дата, миграция больше не будет применяться
|
||||
|
||||
async up() {
|
||||
console.log('Выполнение миграции: Преобразование числовых method_id в строковые');
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { SovietContract } from 'cooptypes';
|
||||
export default {
|
||||
name: 'Синхронизация данных из блокчейна',
|
||||
isTest: false, // Включаем тестовый режим - миграция не будет сохранена в БД
|
||||
validUntil: new Date(), // Текущая дата, миграция больше не будет применяться
|
||||
|
||||
async up({ blockchain }: { blockchain: BlockchainService }): Promise<boolean> {
|
||||
console.log('Выполнение миграции: Синхронизация данных из блокчейна');
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import { BlockchainService } from '../src/infrastructure/blockchain/blockchain.service';
|
||||
import Vault from '../src/models/vault.model';
|
||||
import config from '../src/config/config';
|
||||
import type { SovietContract } from 'cooptypes';
|
||||
|
||||
export default {
|
||||
name: 'Синхронизация данных из блокчейна',
|
||||
isTest: false, // Включаем тестовый режим - миграция не будет сохранена в БД
|
||||
validUntil: new Date(), // Текущая дата, миграция больше не будет применяться
|
||||
|
||||
async up({ blockchain }: { blockchain: BlockchainService }): Promise<boolean> {
|
||||
console.log('Выполнение миграции: Синхронизация данных из блокчейна');
|
||||
|
||||
try {
|
||||
// Проверяем статус подключения к блокчейну без авторизации
|
||||
const info = await blockchain.getInfo();
|
||||
console.log(`Подключение к блокчейну успешно. Chain ID: ${info.chain_id}`);
|
||||
console.log(`Информация о блокчейне: Chain - ${info.chain_id}, Head Block: ${info.head_block_num}`);
|
||||
|
||||
// Имя кооператива, чьи данные будем синхронизировать
|
||||
const coopname = config.coopname; // Укажите имя вашего кооператива
|
||||
|
||||
// Получаем приватный ключ из Vault модели
|
||||
try {
|
||||
const wif = await Vault.getWif(coopname);
|
||||
|
||||
if (wif) {
|
||||
console.log(`Получен приватный ключ для аккаунта ${coopname}`);
|
||||
|
||||
// Инициализируем блокчейн-сервис с полученным ключом
|
||||
blockchain.initialize(coopname, wif);
|
||||
console.log(`Блокчейн-сервис инициализирован с аккаунтом ${coopname}`);
|
||||
|
||||
const oldRows = await blockchain.getAllRows<SovietContract.Tables.AgreementsLegacy.IAgreementLegacy>(
|
||||
'soviet',
|
||||
coopname,
|
||||
'agreements'
|
||||
);
|
||||
|
||||
console.log('Получено legacy соглашений:', oldRows.length);
|
||||
|
||||
// Получаем все строки из таблицы agreements2
|
||||
const newRows = await blockchain.getAllRows<SovietContract.Tables.Agreements.IAgreement>(
|
||||
'soviet',
|
||||
coopname,
|
||||
'agreements3'
|
||||
);
|
||||
|
||||
console.log('Получено новых соглашений:', newRows.length);
|
||||
|
||||
// Находим соглашения, которые ещё не были перенесены
|
||||
const newAgreementIds = new Set(newRows.map((row) => row.id));
|
||||
const notMigratedAgreements = oldRows.filter((row) => !newAgreementIds.has(row.id));
|
||||
|
||||
console.log('Соглашения, которые НЕ были перенесены:', notMigratedAgreements.length);
|
||||
console.log('Детали непереносимых соглашений:');
|
||||
notMigratedAgreements.forEach((agreement) => {
|
||||
console.log(
|
||||
`ID: ${agreement.id}, Пользователь: ${agreement.username}, Тип: ${agreement.type}, Статус: ${agreement.status}`
|
||||
);
|
||||
});
|
||||
|
||||
// Подготовка массива транзакций для миграции
|
||||
if (notMigratedAgreements.length > 0) {
|
||||
console.log('Подготовлен массив транзакций для миграции');
|
||||
console.log('COOPNAME: ', coopname);
|
||||
|
||||
// Последовательная миграция соглашений
|
||||
for (let i = 0; i < notMigratedAgreements.length; i++) {
|
||||
const agreement = notMigratedAgreements[i];
|
||||
console.log(`Миграция соглашения ${i + 1}/${notMigratedAgreements.length}, ID: ${agreement.id}`);
|
||||
|
||||
try {
|
||||
await blockchain.transact({
|
||||
account: 'soviet',
|
||||
name: 'migrateagree',
|
||||
authorization: [{ actor: coopname, permission: 'active' }],
|
||||
data: {
|
||||
coopname,
|
||||
agreement_id: agreement.id,
|
||||
},
|
||||
});
|
||||
console.log(`✅ Соглашение ID: ${agreement.id} успешно перенесено`);
|
||||
} catch (error) {
|
||||
console.error(`❌ Ошибка при переносе соглашения ID: ${agreement.id}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Миграция соглашений завершена');
|
||||
}
|
||||
|
||||
// Теперь можно выполнять транзакции от имени аккаунта
|
||||
// blockchain.transact(...);
|
||||
} else {
|
||||
console.log(`Приватный ключ для ${coopname} не найден в Vault`);
|
||||
}
|
||||
} catch (vaultError) {
|
||||
console.error('Ошибка при получении ключа из Vault:', vaultError);
|
||||
}
|
||||
|
||||
console.log('Миграция завершена: Синхронизация данных из блокчейна выполнена успешно');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при выполнении миграции блокчейн-данных:', error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
};
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "create-nodejs-express-app",
|
||||
"version": "2025.5.5-2",
|
||||
"version": "2025.6.13",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "create-nodejs-express-app",
|
||||
"version": "2025.5.5-2",
|
||||
"version": "2025.6.13",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@a2seven/yoo-checkout": "^1.1.4",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@coopenomics/controller",
|
||||
"version": "2025.5.5-2",
|
||||
"version": "2025.6.13",
|
||||
"description": "",
|
||||
"private": false,
|
||||
"bin": "bin/createNodejsApp.js",
|
||||
@@ -64,6 +64,7 @@
|
||||
"dependencies": {
|
||||
"@a2seven/yoo-checkout": "^1.1.4",
|
||||
"@coopenomics/factory": "workspace:*",
|
||||
"@coopenomics/sdk": "workspace:*",
|
||||
"@graphql-codegen/typescript-graphql-request": "^6.2.0",
|
||||
"@graphql-codegen/typescript-resolvers": "^4.4.0",
|
||||
"@graphql-tools/schema": "^10.0.7",
|
||||
@@ -126,6 +127,7 @@
|
||||
"jspdf": "^2.5.1",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.24.0",
|
||||
"moment-timezone": "^0.5.45",
|
||||
"mongoose": "^8.5.2",
|
||||
"mongoose-sequence": "^6.0.1",
|
||||
"morgan": "^1.9.1",
|
||||
@@ -155,8 +157,7 @@
|
||||
"ws": "^8.18.0",
|
||||
"xss-clean": "^0.1.1",
|
||||
"zod": "^3.23.8",
|
||||
"zod-to-json-schema": "^3.23.5",
|
||||
"@coopenomics/sdk": "workspace:*"
|
||||
"zod-to-json-schema": "^3.23.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^2.24.1",
|
||||
|
||||
@@ -159,8 +159,8 @@ input AddTrustedAccountInput {
|
||||
trusted: String!
|
||||
}
|
||||
|
||||
"""Пункт повестки собрания"""
|
||||
type AgendaMeetPoint {
|
||||
"""Пункт повестки общего собрания (для ввода)"""
|
||||
input AgendaGeneralMeetPointInput {
|
||||
"""Контекст или дополнительная информация по пункту повестки"""
|
||||
context: String!
|
||||
|
||||
@@ -171,8 +171,35 @@ type AgendaMeetPoint {
|
||||
title: String!
|
||||
}
|
||||
|
||||
"""Пункт повестки собрания (для ввода)"""
|
||||
input AgendaMeetPointInput {
|
||||
"""Вопрос повестки общего собрания"""
|
||||
input AgendaGeneralMeetQuestion {
|
||||
"""Контекст или дополнительная информация по вопросу"""
|
||||
context: String
|
||||
|
||||
"""Предлагаемое решение по вопросу повестки"""
|
||||
decision: String!
|
||||
|
||||
"""Номер вопроса в повестке"""
|
||||
number: String!
|
||||
|
||||
"""Заголовок вопроса повестки"""
|
||||
title: String!
|
||||
}
|
||||
|
||||
"""Данные собрания для повестки"""
|
||||
input AgendaMeet {
|
||||
"""Дата и время окончания собрания"""
|
||||
close_at_datetime: String!
|
||||
|
||||
"""Дата и время начала собрания"""
|
||||
open_at_datetime: String!
|
||||
|
||||
"""Тип собрания (очередное или внеочередное)"""
|
||||
type: String!
|
||||
}
|
||||
|
||||
"""Пункт повестки собрания"""
|
||||
type AgendaMeetPoint {
|
||||
"""Контекст или дополнительная информация по пункту повестки"""
|
||||
context: String!
|
||||
|
||||
@@ -222,6 +249,8 @@ input AnnualGeneralMeetingAgendaGenerateDocumentInput {
|
||||
|
||||
"""Ссылки, связанные с документом"""
|
||||
links: [String!]
|
||||
meet: AgendaMeet!
|
||||
questions: [AgendaGeneralMeetQuestion!]!
|
||||
|
||||
"""Часовой пояс, в котором был создан документ"""
|
||||
timezone: String
|
||||
@@ -274,6 +303,8 @@ input AnnualGeneralMeetingAgendaSignedMetaDocumentInput {
|
||||
|
||||
"""Ссылки, связанные с документом"""
|
||||
links: [String!]!
|
||||
meet: AgendaMeet!
|
||||
questions: [AgendaGeneralMeetQuestion!]!
|
||||
|
||||
"""ID документа в реестре"""
|
||||
registry_id: Int!
|
||||
@@ -310,6 +341,9 @@ input AnnualGeneralMeetingDecisionGenerateDocumentInput {
|
||||
"""Ссылки, связанные с документом"""
|
||||
links: [String!]
|
||||
|
||||
"""Хеш собрания"""
|
||||
meet_hash: String!
|
||||
|
||||
"""Часовой пояс, в котором был создан документ"""
|
||||
timezone: String
|
||||
|
||||
@@ -362,6 +396,9 @@ input AnnualGeneralMeetingDecisionSignedMetaDocumentInput {
|
||||
"""Ссылки, связанные с документом"""
|
||||
links: [String!]!
|
||||
|
||||
"""Хеш собрания"""
|
||||
meet_hash: String!
|
||||
|
||||
"""ID документа в реестре"""
|
||||
registry_id: Int!
|
||||
|
||||
@@ -397,6 +434,9 @@ input AnnualGeneralMeetingNotificationGenerateDocumentInput {
|
||||
"""Ссылки, связанные с документом"""
|
||||
links: [String!]
|
||||
|
||||
"""Хеш собрания"""
|
||||
meet_hash: String!
|
||||
|
||||
"""Часовой пояс, в котором был создан документ"""
|
||||
timezone: String
|
||||
|
||||
@@ -410,6 +450,64 @@ input AnnualGeneralMeetingNotificationGenerateDocumentInput {
|
||||
version: String
|
||||
}
|
||||
|
||||
input AnnualGeneralMeetingNotificationSignedDocumentInput {
|
||||
"""Хэш содержимого документа"""
|
||||
doc_hash: String!
|
||||
|
||||
"""Общий хэш (doc_hash + meta_hash)"""
|
||||
hash: String!
|
||||
|
||||
"""Метаинформация"""
|
||||
meta: AnnualGeneralMeetingNotificationSignedMetaDocumentInput!
|
||||
|
||||
"""Хэш мета-данных"""
|
||||
meta_hash: String!
|
||||
|
||||
"""Вектор подписей"""
|
||||
signatures: [SignatureInfoInput!]!
|
||||
|
||||
"""Версия стандарта документа"""
|
||||
version: String!
|
||||
}
|
||||
|
||||
input AnnualGeneralMeetingNotificationSignedMetaDocumentInput {
|
||||
"""Номер блока, на котором был создан документ"""
|
||||
block_num: Int!
|
||||
|
||||
"""Название кооператива, связанное с документом"""
|
||||
coopname: String!
|
||||
|
||||
"""Дата и время создания документа"""
|
||||
created_at: String!
|
||||
|
||||
"""Имя генератора, использованного для создания документа"""
|
||||
generator: String!
|
||||
|
||||
"""Язык документа"""
|
||||
lang: String!
|
||||
|
||||
"""Ссылки, связанные с документом"""
|
||||
links: [String!]!
|
||||
|
||||
"""Хеш собрания"""
|
||||
meet_hash: String!
|
||||
|
||||
"""ID документа в реестре"""
|
||||
registry_id: Int!
|
||||
|
||||
"""Часовой пояс, в котором был создан документ"""
|
||||
timezone: String!
|
||||
|
||||
"""Название документа"""
|
||||
title: String!
|
||||
|
||||
"""Имя пользователя, создавшего документ"""
|
||||
username: String!
|
||||
|
||||
"""Версия генератора, использованного для создания документа"""
|
||||
version: String!
|
||||
}
|
||||
|
||||
input AnnualGeneralMeetingSovietDecisionGenerateDocumentInput {
|
||||
"""Номер блока, на котором был создан документ"""
|
||||
block_num: Int
|
||||
@@ -420,6 +518,9 @@ input AnnualGeneralMeetingSovietDecisionGenerateDocumentInput {
|
||||
"""Дата и время создания документа"""
|
||||
created_at: String
|
||||
|
||||
"""ID решения совета"""
|
||||
decision_id: Float!
|
||||
|
||||
"""Имя генератора, использованного для создания документа"""
|
||||
generator: String
|
||||
|
||||
@@ -429,6 +530,9 @@ input AnnualGeneralMeetingSovietDecisionGenerateDocumentInput {
|
||||
"""Ссылки, связанные с документом"""
|
||||
links: [String!]
|
||||
|
||||
"""Хеш собрания"""
|
||||
meet_hash: String!
|
||||
|
||||
"""Часовой пояс, в котором был создан документ"""
|
||||
timezone: String
|
||||
|
||||
@@ -443,6 +547,9 @@ input AnnualGeneralMeetingSovietDecisionGenerateDocumentInput {
|
||||
}
|
||||
|
||||
input AnnualGeneralMeetingVotingBallotGenerateDocumentInput {
|
||||
"""Ответы голосования"""
|
||||
answers: [AnswerInput!]!
|
||||
|
||||
"""Номер блока, на котором был создан документ"""
|
||||
block_num: Int
|
||||
|
||||
@@ -461,13 +568,16 @@ input AnnualGeneralMeetingVotingBallotGenerateDocumentInput {
|
||||
"""Ссылки, связанные с документом"""
|
||||
links: [String!]
|
||||
|
||||
"""Хеш собрания"""
|
||||
meet_hash: String!
|
||||
|
||||
"""Часовой пояс, в котором был создан документ"""
|
||||
timezone: String
|
||||
|
||||
"""Название документа"""
|
||||
title: String
|
||||
|
||||
"""Имя пользователя, создавшего документ"""
|
||||
"""Имя пользователя голосующего"""
|
||||
username: String!
|
||||
|
||||
"""Версия генератора, использованного для создания документа"""
|
||||
@@ -495,6 +605,9 @@ input AnnualGeneralMeetingVotingBallotSignedDocumentInput {
|
||||
}
|
||||
|
||||
input AnnualGeneralMeetingVotingBallotSignedMetaDocumentInput {
|
||||
"""Ответы голосования"""
|
||||
answers: [AnswerInput!]!
|
||||
|
||||
"""Номер блока, на котором был создан документ"""
|
||||
block_num: Int!
|
||||
|
||||
@@ -513,6 +626,9 @@ input AnnualGeneralMeetingVotingBallotSignedMetaDocumentInput {
|
||||
"""Ссылки, связанные с документом"""
|
||||
links: [String!]!
|
||||
|
||||
"""Хеш собрания"""
|
||||
meet_hash: String!
|
||||
|
||||
"""ID документа в реестре"""
|
||||
registry_id: Int!
|
||||
|
||||
@@ -522,13 +638,24 @@ input AnnualGeneralMeetingVotingBallotSignedMetaDocumentInput {
|
||||
"""Название документа"""
|
||||
title: String!
|
||||
|
||||
"""Имя пользователя, создавшего документ"""
|
||||
"""Имя пользователя голосующего"""
|
||||
username: String!
|
||||
|
||||
"""Версия генератора, использованного для создания документа"""
|
||||
version: String!
|
||||
}
|
||||
|
||||
input AnswerInput {
|
||||
"""ID вопроса"""
|
||||
id: String!
|
||||
|
||||
"""Номер вопроса"""
|
||||
number: String!
|
||||
|
||||
"""Голос (за/против/воздержался)"""
|
||||
vote: String!
|
||||
}
|
||||
|
||||
input AssetContributionActGenerateDocumentInput {
|
||||
"""Идентификатор акта"""
|
||||
act_id: String!
|
||||
@@ -978,9 +1105,22 @@ type BlockchainDecision {
|
||||
statement: SignedBlockchainDocument!
|
||||
type: String!
|
||||
username: String!
|
||||
|
||||
"""Сертификат пользователя, создавшего решение"""
|
||||
username_certificate: UserCertificateUnion
|
||||
validated: Boolean!
|
||||
votes_against: [String!]!
|
||||
|
||||
"""
|
||||
Сертификаты пользователей, голосовавших "против"
|
||||
"""
|
||||
votes_against_certificates: [UserCertificateUnion!]!
|
||||
votes_for: [String!]!
|
||||
|
||||
"""
|
||||
Сертификаты пользователей, голосовавших "за"
|
||||
"""
|
||||
votes_for_certificates: [UserCertificateUnion!]!
|
||||
}
|
||||
|
||||
"""Информация о состоянии блокчейна"""
|
||||
@@ -1164,6 +1304,9 @@ type ContactsDTO {
|
||||
}
|
||||
|
||||
type CooperativeOperatorAccount {
|
||||
"""Количество активных участников"""
|
||||
active_participants_count: Float!
|
||||
|
||||
"""Объявление кооператива"""
|
||||
announce: String!
|
||||
|
||||
@@ -1244,7 +1387,7 @@ enum Country {
|
||||
|
||||
input CreateAnnualGeneralMeetInput {
|
||||
"""Повестка собрания"""
|
||||
agenda: [AgendaMeetPointInput!]!
|
||||
agenda: [AgendaGeneralMeetPointInput!]!
|
||||
|
||||
"""Время закрытия собрания"""
|
||||
close_at: DateTime!
|
||||
@@ -1252,9 +1395,6 @@ input CreateAnnualGeneralMeetInput {
|
||||
"""Имя аккаунта кооператива"""
|
||||
coopname: String!
|
||||
|
||||
"""Хеш"""
|
||||
hash: String!
|
||||
|
||||
"""Имя аккаунта инициатора"""
|
||||
initiator: String!
|
||||
|
||||
@@ -1713,6 +1853,26 @@ type Entrepreneur {
|
||||
username: String!
|
||||
}
|
||||
|
||||
type EntrepreneurCertificate {
|
||||
"""Имя"""
|
||||
first_name: String!
|
||||
|
||||
"""ИНН"""
|
||||
inn: String!
|
||||
|
||||
"""Фамилия"""
|
||||
last_name: String!
|
||||
|
||||
"""Отчество"""
|
||||
middle_name: String
|
||||
|
||||
"""Тип аккаунта"""
|
||||
type: AccountType!
|
||||
|
||||
"""Имя аккаунта"""
|
||||
username: String!
|
||||
}
|
||||
|
||||
type EntrepreneurDetails {
|
||||
"""ИНН"""
|
||||
inn: String!
|
||||
@@ -1730,12 +1890,15 @@ input EntrepreneurDetailsInput {
|
||||
}
|
||||
|
||||
"""
|
||||
Расширенное действие блокчейна с персональными данными пользователя, совершившего его.
|
||||
Расширенное действие блокчейна с сертификатом пользователя, совершившего его.
|
||||
"""
|
||||
type ExtendedBlockchainAction {
|
||||
account: String!
|
||||
account_ram_deltas: [AccountRamDelta!]!
|
||||
action_ordinal: Int!
|
||||
|
||||
"""Сертификат пользователя (сокращенная информация)"""
|
||||
actor_certificate: UserCertificateUnion
|
||||
authorization: [ActionAuthorization!]!
|
||||
block_id: String!
|
||||
block_num: Int!
|
||||
@@ -1752,9 +1915,39 @@ type ExtendedBlockchainAction {
|
||||
receipt: ActionReceipt!
|
||||
receiver: String!
|
||||
transaction_id: String!
|
||||
}
|
||||
|
||||
"""Доп. данные о пользователе (физ/ИП/организация)"""
|
||||
user: UserDataUnion
|
||||
"""Расширенный статус собрания на основе дат и состояния"""
|
||||
enum ExtendedMeetStatus {
|
||||
"""Авторизовано"""
|
||||
AUTHORIZED
|
||||
|
||||
"""Закрыто"""
|
||||
CLOSED
|
||||
|
||||
"""Создано"""
|
||||
CREATED
|
||||
|
||||
"""Истекло без кворума"""
|
||||
EXPIRED_NO_QUORUM
|
||||
|
||||
"""Неопределенное состояние"""
|
||||
NONE
|
||||
|
||||
"""Ожидаем утверждения новой даты собрания"""
|
||||
ONRESTART
|
||||
|
||||
"""Предварительно закрыто"""
|
||||
PRECLOSED
|
||||
|
||||
"""Голосование завершено, ожидает подписей"""
|
||||
VOTING_COMPLETED
|
||||
|
||||
"""Голосование идет"""
|
||||
VOTING_IN_PROGRESS
|
||||
|
||||
"""Ожидает открытия"""
|
||||
WAITING_FOR_OPENING
|
||||
}
|
||||
|
||||
type Extension {
|
||||
@@ -2040,6 +2233,23 @@ type Individual {
|
||||
username: String!
|
||||
}
|
||||
|
||||
type IndividualCertificate {
|
||||
"""Имя"""
|
||||
first_name: String!
|
||||
|
||||
"""Фамилия"""
|
||||
last_name: String!
|
||||
|
||||
"""Отчество"""
|
||||
middle_name: String
|
||||
|
||||
"""Тип аккаунта"""
|
||||
type: AccountType!
|
||||
|
||||
"""Имя аккаунта"""
|
||||
username: String!
|
||||
}
|
||||
|
||||
input Init {
|
||||
"""
|
||||
Объект организации кооператива, которая обслуживает данный экземпляр программного обеспечения MONO
|
||||
@@ -2114,6 +2324,12 @@ type Meet {
|
||||
"""Цикл собрания"""
|
||||
cycle: Float!
|
||||
|
||||
"""Документ с решением секретаря"""
|
||||
decision1: DocumentAggregate
|
||||
|
||||
"""Документ с решением председателя"""
|
||||
decision2: DocumentAggregate
|
||||
|
||||
"""Хеш собрания"""
|
||||
hash: String!
|
||||
|
||||
@@ -2123,14 +2339,23 @@ type Meet {
|
||||
"""Инициатор собрания"""
|
||||
initiator: String!
|
||||
|
||||
"""Сертификат инициатора собрания"""
|
||||
initiator_certificate: UserCertificateUnion
|
||||
|
||||
"""Список пользователей, которые подписали уведомление"""
|
||||
notified_users: [String!]!
|
||||
|
||||
"""Дата открытия собрания"""
|
||||
open_at: DateTime!
|
||||
|
||||
"""Председатель собрания"""
|
||||
presider: String!
|
||||
|
||||
"""Сертификат председателя собрания"""
|
||||
presider_certificate: UserCertificateUnion
|
||||
|
||||
"""Документ с повесткой собрания"""
|
||||
proposal: DocumentAggregate!
|
||||
proposal: DocumentAggregate
|
||||
|
||||
"""Флаг достижения кворума"""
|
||||
quorum_passed: Boolean!
|
||||
@@ -2141,6 +2366,9 @@ type Meet {
|
||||
"""Секретарь собрания"""
|
||||
secretary: String!
|
||||
|
||||
"""Сертификат секретаря собрания"""
|
||||
secretary_certificate: UserCertificateUnion
|
||||
|
||||
"""Количество подписанных бюллетеней"""
|
||||
signed_ballots: Float!
|
||||
|
||||
@@ -2183,33 +2411,78 @@ type MeetPreProcessing {
|
||||
"""Инициатор собрания"""
|
||||
initiator: String!
|
||||
|
||||
"""Сертификат инициатора собрания"""
|
||||
initiator_certificate: UserCertificateUnion
|
||||
|
||||
"""Дата открытия собрания"""
|
||||
open_at: DateTime!
|
||||
|
||||
"""Председатель собрания"""
|
||||
presider: String!
|
||||
|
||||
"""Сертификат председателя собрания"""
|
||||
presider_certificate: UserCertificateUnion
|
||||
|
||||
"""Документ с предложением повестки собрания"""
|
||||
proposal: DocumentAggregate
|
||||
|
||||
"""Секретарь собрания"""
|
||||
secretary: String!
|
||||
|
||||
"""Сертификат секретаря собрания"""
|
||||
secretary_certificate: UserCertificateUnion
|
||||
}
|
||||
|
||||
"""Данные о собрании после обработки"""
|
||||
type MeetProcessed {
|
||||
"""Решение по собранию в формате блокчейн-действия"""
|
||||
decision: BlockchainAction!
|
||||
"""Имя кооператива"""
|
||||
coopname: String!
|
||||
|
||||
"""Документ решения из блокчейна"""
|
||||
decision: SignedDigitalDocument!
|
||||
|
||||
"""Агрегат документа решения"""
|
||||
decisionAggregate: DocumentAggregate
|
||||
|
||||
"""Хеш собрания"""
|
||||
hash: String!
|
||||
|
||||
"""Председатель собрания"""
|
||||
presider: String!
|
||||
|
||||
"""Сертификат председателя собрания"""
|
||||
presider_certificate: UserCertificateUnion
|
||||
|
||||
"""Пройден ли кворум"""
|
||||
quorum_passed: Boolean!
|
||||
|
||||
"""Процент кворума"""
|
||||
quorum_percent: Int!
|
||||
|
||||
"""Результаты голосования по вопросам"""
|
||||
results: [MeetQuestionResult!]!
|
||||
|
||||
"""Секретарь собрания"""
|
||||
secretary: String!
|
||||
|
||||
"""Сертификат секретаря собрания"""
|
||||
secretary_certificate: UserCertificateUnion
|
||||
|
||||
"""Количество подписанных бюллетеней"""
|
||||
signed_ballots: Int!
|
||||
}
|
||||
|
||||
"""Данные о собрании в процессе обработки"""
|
||||
type MeetProcessing {
|
||||
"""Расширенный статус собрания на основе дат и состояния"""
|
||||
extendedStatus: ExtendedMeetStatus!
|
||||
|
||||
"""Хеш собрания"""
|
||||
hash: String!
|
||||
|
||||
"""Флаг указывающий, голосовал ли текущий пользователь"""
|
||||
isVoted: Boolean!
|
||||
|
||||
"""Основная информация о собрании"""
|
||||
meet: Meet!
|
||||
|
||||
@@ -2217,6 +2490,36 @@ type MeetProcessing {
|
||||
questions: [Question!]!
|
||||
}
|
||||
|
||||
"""Результат голосования по вопросу"""
|
||||
type MeetQuestionResult {
|
||||
"""Принят ли вопрос"""
|
||||
accepted: Boolean!
|
||||
|
||||
"""Контекст вопроса"""
|
||||
context: String!
|
||||
|
||||
"""Принятое решение"""
|
||||
decision: String!
|
||||
|
||||
"""Порядковый номер вопроса"""
|
||||
number: Int!
|
||||
|
||||
"""Идентификатор вопроса"""
|
||||
question_id: Int!
|
||||
|
||||
"""Заголовок вопроса"""
|
||||
title: String!
|
||||
|
||||
"""Количество воздержавшихся"""
|
||||
votes_abstained: Int!
|
||||
|
||||
"""Количество голосов против"""
|
||||
votes_against: Int!
|
||||
|
||||
"""Количество голосов за"""
|
||||
votes_for: Int!
|
||||
}
|
||||
|
||||
input MetaDocumentInput {
|
||||
"""Номер блока, на котором был создан документ"""
|
||||
block_num: Int!
|
||||
@@ -2477,6 +2780,9 @@ type Mutation {
|
||||
"""Модерировать заявку"""
|
||||
moderateRequest(data: ModerateRequestInput!): Transaction!
|
||||
|
||||
"""Уведомление о проведении общего собрания пайщиков"""
|
||||
notifyOnAnnualGeneralMeet(data: NotifyOnAnnualGeneralMeetInput!): MeetAggregate!
|
||||
|
||||
"""Отклонить модерацию по заявке"""
|
||||
prohibitRequest(data: ProhibitRequestInput!): Transaction!
|
||||
|
||||
@@ -2564,6 +2870,13 @@ type Mutation {
|
||||
voteOnAnnualGeneralMeet(data: VoteOnAnnualGeneralMeetInput!): MeetAggregate!
|
||||
}
|
||||
|
||||
input NotifyOnAnnualGeneralMeetInput {
|
||||
coopname: String!
|
||||
meet_hash: String!
|
||||
notification: AnnualGeneralMeetingNotificationSignedDocumentInput!
|
||||
username: String!
|
||||
}
|
||||
|
||||
type Organization {
|
||||
"""Город"""
|
||||
city: String!
|
||||
@@ -2602,6 +2915,26 @@ type Organization {
|
||||
username: String!
|
||||
}
|
||||
|
||||
type OrganizationCertificate {
|
||||
"""ИНН"""
|
||||
inn: String!
|
||||
|
||||
"""ОГРН"""
|
||||
ogrn: String!
|
||||
|
||||
"""Данные представителя"""
|
||||
represented_by: RepresentedByCertificate!
|
||||
|
||||
"""Короткое название организации"""
|
||||
short_name: String!
|
||||
|
||||
"""Тип аккаунта"""
|
||||
type: AccountType!
|
||||
|
||||
"""Имя аккаунта"""
|
||||
username: String!
|
||||
}
|
||||
|
||||
type OrganizationDetails {
|
||||
"""ИНН"""
|
||||
inn: String!
|
||||
@@ -2850,6 +3183,9 @@ input PassportInput {
|
||||
}
|
||||
|
||||
type Payment {
|
||||
"""Аккаунт пользователя, совершающего платеж"""
|
||||
account: Account!
|
||||
|
||||
"""Сумма платежа"""
|
||||
amount: Float!
|
||||
|
||||
@@ -3296,7 +3632,7 @@ input RegisterAccountInput {
|
||||
organization_data: CreateOrganizationDataInput
|
||||
|
||||
"""Публичный ключ"""
|
||||
public_key: String
|
||||
public_key: String!
|
||||
|
||||
"""Имя аккаунта реферера"""
|
||||
referer: String
|
||||
@@ -3358,6 +3694,20 @@ type RepresentedBy {
|
||||
position: String!
|
||||
}
|
||||
|
||||
type RepresentedByCertificate {
|
||||
"""Имя"""
|
||||
first_name: String!
|
||||
|
||||
"""Фамилия"""
|
||||
last_name: String!
|
||||
|
||||
"""Отчество"""
|
||||
middle_name: String!
|
||||
|
||||
"""Должность"""
|
||||
position: String!
|
||||
}
|
||||
|
||||
input RepresentedByInput {
|
||||
based_on: String!
|
||||
first_name: String!
|
||||
@@ -3838,7 +4188,9 @@ type SignatureInfo {
|
||||
signed_at: String!
|
||||
signed_hash: String!
|
||||
signer: String!
|
||||
signer_info: UserDataUnion
|
||||
|
||||
"""Сертификат подписанта (сокращенная информация)"""
|
||||
signer_certificate: UserCertificateUnion
|
||||
}
|
||||
|
||||
input SignatureInfoInput {
|
||||
@@ -3846,7 +4198,7 @@ input SignatureInfoInput {
|
||||
id: Float!
|
||||
|
||||
"""Мета-данные подписи"""
|
||||
meta: JSON!
|
||||
meta: String!
|
||||
|
||||
"""Публичный ключ"""
|
||||
public_key: String!
|
||||
@@ -4236,8 +4588,8 @@ type UserAccount {
|
||||
verifications: [Verification!]!
|
||||
}
|
||||
|
||||
"""Объединение информации о пользователях"""
|
||||
union UserDataUnion = Entrepreneur | Individual | Organization
|
||||
"""Объединение сертификатов пользователей (сокращенная информация)"""
|
||||
union UserCertificateUnion = EntrepreneurCertificate | IndividualCertificate | OrganizationCertificate
|
||||
|
||||
"""Статус пользователя"""
|
||||
enum UserStatus {
|
||||
|
||||
@@ -42,27 +42,32 @@ const envVarsSchema = z.object({
|
||||
SMTP_USERNAME: z.string().default(''), // Пустая строка для необязательного значения
|
||||
SMTP_PASSWORD: z.string().default(''), // Пустая строка для необязательного значения
|
||||
EMAIL_FROM: z.string().default(''), // Пустая строка для необязательного значения
|
||||
COOPNAME: z.string().default('default_coopname'), // Задаём дефолтное значение
|
||||
GRAPHQL_SERVICE: z.string().default('http://localhost:4090').describe('адрес сервиса GRAPHQL'),
|
||||
COOPNAME: z.string().min(1, { message: 'Не должно быть пустым' }).default('voskhod'), // Задаём дефолтное значение, пустая строка невалидна
|
||||
TIMEZONE: z.string().min(1, { message: 'Не должно быть пустым' }).default('Europe/Moscow'), // Пустая строка невалидна
|
||||
GRAPHQL_SERVICE: z
|
||||
.string()
|
||||
.min(1, { message: 'Не должно быть пустым' })
|
||||
.default('http://localhost:4090')
|
||||
.describe('адрес сервиса GRAPHQL'),
|
||||
|
||||
// Новые переменные для PostgreSQL
|
||||
POSTGRES_HOST: z.string().default('127.0.0.1'),
|
||||
POSTGRES_HOST: z.string().min(1, { message: 'Не должно быть пустым' }).default('127.0.0.1'),
|
||||
POSTGRES_PORT: z
|
||||
.string()
|
||||
.default('5432')
|
||||
.transform((val) => parseInt(val, 10)),
|
||||
POSTGRES_USERNAME: z.string(),
|
||||
POSTGRES_PASSWORD: z.string(),
|
||||
POSTGRES_DATABASE: z.string(),
|
||||
POSTGRES_USERNAME: z.string().min(1, { message: 'Не должно быть пустым' }),
|
||||
POSTGRES_PASSWORD: z.string().min(1, { message: 'Не должно быть пустым' }),
|
||||
POSTGRES_DATABASE: z.string().min(1, { message: 'Не должно быть пустым' }),
|
||||
|
||||
REDIS_HOST: z.string(),
|
||||
REDIS_HOST: z.string().min(1, { message: 'Не должно быть пустым' }),
|
||||
REDIS_PORT: z
|
||||
.string()
|
||||
.default('6379')
|
||||
.transform((val) => parseInt(val, 10)),
|
||||
REDIS_PASSWORD: z.string(),
|
||||
BLOCKCHAIN_RPC: z.string(),
|
||||
CHAIN_ID: z.string(),
|
||||
BLOCKCHAIN_RPC: z.string().min(1, { message: 'Не должно быть пустым' }),
|
||||
CHAIN_ID: z.string().min(1, { message: 'Не должно быть пустым' }),
|
||||
});
|
||||
|
||||
// Валидация переменных окружения
|
||||
@@ -86,6 +91,7 @@ export default {
|
||||
base_url: envVars.data.BASE_URL,
|
||||
port: envVars.data.PORT,
|
||||
server_secret: envVars.data.SERVER_SECRET,
|
||||
timezone: envVars.data.TIMEZONE,
|
||||
blockchain: {
|
||||
url: envVars.data.BLOCKCHAIN_RPC,
|
||||
id: envVars.data.CHAIN_ID,
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
import { io, Socket } from 'socket.io-client';
|
||||
import { IAction } from '../types/common';
|
||||
import { SovietContract } from 'cooptypes';
|
||||
import { wsService } from '../services';
|
||||
import logger from '../config/logger';
|
||||
import config from '../config/config';
|
||||
import { MeetDomainInteractor } from '~/domain/meet/interactors/meet.interactor';
|
||||
import type { MeetDecisionDomainInterface } from '~/domain/meet/interfaces/meet-decision-domain.interface';
|
||||
import { DomainToBlockchainUtils } from '~/infrastructure/blockchain/utils/domain-to-blockchain.utils';
|
||||
|
||||
let clientSocket: Socket | undefined;
|
||||
let meetInteractor: MeetDomainInteractor;
|
||||
|
||||
export const setMeetInteractor = (interactor: MeetDomainInteractor): void => {
|
||||
meetInteractor = interactor;
|
||||
};
|
||||
|
||||
// Инициация подключения
|
||||
export const initSocketConnection = (serverUrl: string): void => {
|
||||
@@ -20,7 +27,7 @@ export const initSocketConnection = (serverUrl: string): void => {
|
||||
});
|
||||
|
||||
clientSocket.on('connect_error', (error: Error) => {
|
||||
logger.error('Fail connect to notification server', { source: 'initSocketConnection' });
|
||||
logger.error(`Fail connect to notification server: ${error.message}`, { source: 'initSocketConnection' });
|
||||
});
|
||||
};
|
||||
|
||||
@@ -42,5 +49,39 @@ async function processEvent(event: IAction) {
|
||||
if (event.name === 'createboard') {
|
||||
wsService.updateBoard(event.data);
|
||||
}
|
||||
|
||||
if (event.name === 'newgdecision') {
|
||||
// Обработка события о решении собрания
|
||||
try {
|
||||
// Преобразуем блокчейн-документ в формат ISignedDocumentDomainInterface
|
||||
const decisionDocument = DomainToBlockchainUtils.convertChainDocumentToSignedDocument2(event.data.decision);
|
||||
|
||||
// Нормализуем числовые значения
|
||||
const decisionData: MeetDecisionDomainInterface = {
|
||||
...event.data,
|
||||
signed_ballots: Number(event.data.signed_ballots),
|
||||
quorum_percent: Number(event.data.quorum_percent),
|
||||
results: event.data.results.map((item) => ({
|
||||
...item,
|
||||
question_id: Number(item.question_id),
|
||||
number: Number(item.number),
|
||||
votes_for: Number(item.votes_for),
|
||||
votes_against: Number(item.votes_against),
|
||||
votes_abstained: Number(item.votes_abstained),
|
||||
})),
|
||||
decision: decisionDocument, // Документ решения из блокчейна
|
||||
};
|
||||
|
||||
// Используем интерактор для сохранения данных о завершенном собрании
|
||||
await meetInteractor.processMeetDecision({
|
||||
action: event,
|
||||
decisionData,
|
||||
});
|
||||
|
||||
logger.info(`Processed meet decision for ${event.data.hash}`, { source: 'processEvent' });
|
||||
} catch (error: any) {
|
||||
logger.error(`Error processing meet decision: ${error.message}`, { source: 'processEvent', error });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
// domain/account/account-domain.module.ts
|
||||
|
||||
import { forwardRef, Module } from '@nestjs/common';
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { AccountDomainInteractor } from './interactors/account.interactor';
|
||||
import { AccountDomainService } from '~/domain/account/services/account-domain.service';
|
||||
import { DocumentDomainModule } from '../document/document.module';
|
||||
import { AccountDomainService, ACCOUNT_DOMAIN_SERVICE } from './services/account-domain.service';
|
||||
import { InfrastructureModule } from '~/infrastructure/infrastructure.module';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [forwardRef(() => DocumentDomainModule)],
|
||||
providers: [AccountDomainService, AccountDomainInteractor],
|
||||
exports: [AccountDomainInteractor, AccountDomainService],
|
||||
imports: [InfrastructureModule],
|
||||
providers: [
|
||||
AccountDomainInteractor,
|
||||
AccountDomainService,
|
||||
{
|
||||
provide: ACCOUNT_DOMAIN_SERVICE,
|
||||
useExisting: AccountDomainService,
|
||||
},
|
||||
],
|
||||
exports: [AccountDomainInteractor, AccountDomainService, ACCOUNT_DOMAIN_SERVICE],
|
||||
})
|
||||
export class AccountDomainModule {}
|
||||
|
||||
@@ -39,7 +39,23 @@ export class AccountDomainInteractor {
|
||||
private readonly logger = new Logger(AccountDomainInteractor.name);
|
||||
|
||||
async updateAccount(data: UpdateAccountDomainInterface): Promise<AccountDomainEntity> {
|
||||
const user = await userService.updateUserByUsername(data.username, data);
|
||||
let user;
|
||||
if (data.individual_data) {
|
||||
const email = data.individual_data.email;
|
||||
user = await userService.updateUserByUsername(data.username, { email });
|
||||
this.individualRepository.create({ ...data.individual_data, username: data.username });
|
||||
} else if (data.organization_data) {
|
||||
const email = data.organization_data.email;
|
||||
user = await userService.updateUserByUsername(data.username, { email });
|
||||
this.organizationRepository.create({ ...data.organization_data, username: data.username });
|
||||
} else if (data.entrepreneur_data) {
|
||||
const email = data.entrepreneur_data.email;
|
||||
user = await userService.updateUserByUsername(data.username, { email });
|
||||
this.entrepreneurRepository.create({ ...data.entrepreneur_data, username: data.username });
|
||||
} else {
|
||||
throw new Error('Не получены входные данные для обновления');
|
||||
}
|
||||
|
||||
const account = await this.getAccount(user.username);
|
||||
|
||||
const result = new AccountDomainEntity(account);
|
||||
@@ -56,11 +72,11 @@ export class AccountDomainInteractor {
|
||||
): Promise<IndividualDomainEntity | OrganizationDomainEntity | EntrepreneurDomainEntity> {
|
||||
switch (accountType) {
|
||||
case AccountType.individual:
|
||||
return this.individualRepository.findByUsername(username);
|
||||
return new IndividualDomainEntity(await this.individualRepository.findByUsername(username));
|
||||
case AccountType.organization:
|
||||
return this.organizationRepository.findByUsername(username);
|
||||
return new OrganizationDomainEntity(await this.organizationRepository.findByUsername(username));
|
||||
case AccountType.entrepreneur:
|
||||
return this.entrepreneurRepository.findByUsername(username);
|
||||
return new EntrepreneurDomainEntity(await this.entrepreneurRepository.findByUsername(username));
|
||||
default:
|
||||
throw new Error(`Неизвестный тип аккаунта: ${accountType}`);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { ENTREPRENEUR_REPOSITORY, EntrepreneurRepository } from '~/domain/common
|
||||
import { ORGANIZATION_REPOSITORY, OrganizationRepository } from '~/domain/common/repositories/organization.repository';
|
||||
import { INDIVIDUAL_REPOSITORY, IndividualRepository } from '~/domain/common/repositories/individual.repository';
|
||||
import type { PrivateAccountDomainInterface } from '../interfaces/private-account-domain.interface';
|
||||
import type { AccountType } from '~/modules/account/enum/account-type.enum';
|
||||
import { AccountType } from '~/modules/account/enum/account-type.enum';
|
||||
import type { IndividualDomainInterface } from '~/domain/common/interfaces/individual-domain.interface';
|
||||
import type { OrganizationDomainInterface } from '~/domain/common/interfaces/organization-domain.interface';
|
||||
import type { EntrepreneurDomainInterface } from '~/domain/common/interfaces/entrepreneur-domain.interface';
|
||||
@@ -48,6 +48,29 @@ export class AccountDomainService {
|
||||
}
|
||||
|
||||
async getAccount(username: string): Promise<AccountDomainEntity> {
|
||||
// Исключение для организации-кооператива
|
||||
// Очень плохие решения из за того что мы содержим ТРИ разных репозитория под приватные данные и не знаем наверняка в каком искать данные заранее!
|
||||
if (config.coopname === username) {
|
||||
const user_account = await this.getUserAccount(username);
|
||||
const blockchain_account = await this.getBlockchainAccount(username);
|
||||
const participant_account = await this.getParticipantAccount(config.coopname, username);
|
||||
const organization_data = await this.organizationRepository.findByUsername(username);
|
||||
const private_account: PrivateAccountDomainInterface = {
|
||||
type: AccountType.organization,
|
||||
individual_data: undefined,
|
||||
organization_data,
|
||||
entrepreneur_data: undefined,
|
||||
};
|
||||
return new AccountDomainEntity({
|
||||
username,
|
||||
user_account,
|
||||
blockchain_account,
|
||||
provider_account: null,
|
||||
participant_account,
|
||||
private_account,
|
||||
});
|
||||
}
|
||||
|
||||
const user_account = await this.getUserAccount(username);
|
||||
const blockchain_account = await this.getBlockchainAccount(username);
|
||||
const participant_account = await this.getParticipantAccount(config.coopname, username);
|
||||
@@ -130,3 +153,5 @@ export class AccountDomainService {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export const ACCOUNT_DOMAIN_SERVICE = Symbol('ACCOUNT_DOMAIN_SERVICE');
|
||||
|
||||
@@ -71,6 +71,10 @@ export class AgendaDomainInteractor {
|
||||
});
|
||||
|
||||
const action = actionResponse?.results?.[0];
|
||||
|
||||
// if (action)
|
||||
//TODO: здесь нужно добавить в action actor_certificate
|
||||
|
||||
if (action) {
|
||||
agenda.push({
|
||||
table: decision,
|
||||
|
||||
+2
-6
@@ -1,10 +1,6 @@
|
||||
import type { BlockchainActionDomainInterface } from '../../common/interfaces/blockchain-action-domain.interface';
|
||||
import type { Cooperative } from 'cooptypes';
|
||||
import type { UserCertificateDomainInterface } from '~/domain/user-certificate/interfaces/user-certificate-domain.interface';
|
||||
|
||||
export interface ExtendedBlockchainActionDomainInterface extends BlockchainActionDomainInterface {
|
||||
user?:
|
||||
| Cooperative.Users.IIndividualData
|
||||
| Cooperative.Users.IEntrepreneurData
|
||||
| Cooperative.Users.IOrganizationData
|
||||
| null;
|
||||
actor_certificate?: UserCertificateDomainInterface | null;
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ export class BranchDomainInteractor {
|
||||
|
||||
for (const trusted of branch.trusted) {
|
||||
const tr = await this.individualRepository.findByUsername(trusted);
|
||||
trustedData.push(tr);
|
||||
trustedData.push(new IndividualDomainEntity(tr));
|
||||
}
|
||||
|
||||
const bankAccount = new BankPaymentMethodDTO(
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { EntrepreneurDomainEntity } from '~/domain/branch/entities/entrepreneur-domain.entity';
|
||||
import type { EntrepreneurDomainInterface } from '../interfaces/entrepreneur-domain.interface';
|
||||
|
||||
export interface EntrepreneurRepository {
|
||||
/**
|
||||
* Получить данные об ИП по имени аккаунта
|
||||
* @param username Имя аккаунта
|
||||
*/
|
||||
findByUsername(username: string): Promise<EntrepreneurDomainEntity>;
|
||||
create(data: EntrepreneurDomainEntity);
|
||||
findByUsername(username: string): Promise<EntrepreneurDomainInterface>;
|
||||
create(data: EntrepreneurDomainInterface): Promise<void>;
|
||||
}
|
||||
|
||||
export const ENTREPRENEUR_REPOSITORY = Symbol('EntrepreneurRepository'); // Создаем уникальный токен
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { IndividualDomainEntity } from '~/domain/branch/entities/individual-domain.entity';
|
||||
import type { IndividualDomainInterface } from '../interfaces/individual-domain.interface';
|
||||
|
||||
export interface IndividualRepository {
|
||||
/**
|
||||
* Получить данные о физлице по имени аккаунта
|
||||
* @param username Имя аккаунта
|
||||
*/
|
||||
findByUsername(username: string): Promise<IndividualDomainEntity>;
|
||||
create(data: IndividualDomainEntity);
|
||||
findByUsername(username: string): Promise<IndividualDomainInterface>;
|
||||
create(data: IndividualDomainInterface): Promise<void>;
|
||||
}
|
||||
|
||||
export const INDIVIDUAL_REPOSITORY = Symbol('IndividualRepository'); // Создаем уникальный токен
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { OrganizationDomainEntity } from '~/domain/branch/entities/organization-domain.entity';
|
||||
import type { OrganizationDomainInterface } from '../interfaces/organization-domain.interface';
|
||||
|
||||
export interface OrganizationRepository {
|
||||
/**
|
||||
* Получить данные об организации по имени кооператива
|
||||
* @param username Имя кооператива
|
||||
*/
|
||||
findByUsername(username: string): Promise<OrganizationDomainEntity>;
|
||||
create(data: OrganizationDomainEntity);
|
||||
findByUsername(username: string): Promise<OrganizationDomainInterface>;
|
||||
create(data: OrganizationDomainInterface): Promise<void>;
|
||||
}
|
||||
|
||||
export const ORGANIZATION_REPOSITORY = Symbol('OrganizationRepository'); // Создаем уникальный токен
|
||||
|
||||
+17
-7
@@ -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);
|
||||
|
||||
+155
-8
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-5
@@ -1,7 +1,7 @@
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { Injectable, Inject, forwardRef } from '@nestjs/common';
|
||||
import { DOCUMENT_REPOSITORY, DocumentRepository } from '../repository/document.repository';
|
||||
import { DocumentAggregator } from './document.aggregator';
|
||||
import { AccountDomainService } from '~/domain/account/services/account-domain.service';
|
||||
import { AccountDomainService, ACCOUNT_DOMAIN_SERVICE } from '~/domain/account/services/account-domain.service';
|
||||
import { Cooperative } from 'cooptypes';
|
||||
import type { DocumentPackageAggregateDomainInterface } from '../interfaces/document-package-aggregate-domain.interface';
|
||||
import { DocumentPackageV0Aggregator } from './document-package-v0.aggregator';
|
||||
@@ -13,7 +13,7 @@ export class DocumentPackageAggregator {
|
||||
constructor(
|
||||
@Inject(DOCUMENT_REPOSITORY) private readonly documentRepository: DocumentRepository,
|
||||
private readonly documentAggregator: DocumentAggregator,
|
||||
private readonly accountDomainService: AccountDomainService,
|
||||
@Inject(forwardRef(() => ACCOUNT_DOMAIN_SERVICE)) private readonly accountDomainService: AccountDomainService,
|
||||
private readonly documentPackageV0Aggregator: DocumentPackageV0Aggregator,
|
||||
private readonly documentPackageV1Aggregator: DocumentPackageV1Aggregator,
|
||||
private readonly documentPackageUtils: DocumentPackageUtils
|
||||
@@ -28,8 +28,6 @@ export class DocumentPackageAggregator {
|
||||
async buildDocumentPackageAggregate(
|
||||
rawAction: Cooperative.Blockchain.IAction
|
||||
): Promise<DocumentPackageAggregateDomainInterface> {
|
||||
console.log('rawAction', rawAction);
|
||||
|
||||
// Определяем версию документа
|
||||
const docVersion = this.determineDocumentVersion(rawAction);
|
||||
|
||||
|
||||
@@ -1,21 +1,28 @@
|
||||
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
||||
import { BadRequestException, Inject, Injectable, forwardRef } from '@nestjs/common';
|
||||
import { DOCUMENT_REPOSITORY, DocumentRepository } from '../repository/document.repository';
|
||||
import { DocumentDomainAggregate } from '../aggregates/document-domain.aggregate';
|
||||
import { DocumentDomainEntity } from '../entity/document-domain.entity';
|
||||
import { AccountDomainService } from '~/domain/account/services/account-domain.service';
|
||||
import type { DocumentMetaDomainInterface } from '../interfaces/document-meta-domain.interface';
|
||||
import { AccountDomainService, ACCOUNT_DOMAIN_SERVICE } from '~/domain/account/services/account-domain.service';
|
||||
|
||||
import {
|
||||
ExtendedSignedDocumentDomainInterface,
|
||||
SignatureInfoDomainInterface,
|
||||
} from '../interfaces/extended-signed-document-domain.interface';
|
||||
import type { ISignedDocumentDomainInterface } from '../interfaces/signed-document-domain.interface';
|
||||
import { Classes } from '@coopenomics/sdk';
|
||||
import {
|
||||
UserCertificateDomainService,
|
||||
USER_CERTIFICATE_DOMAIN_SERVICE,
|
||||
} from '~/domain/user-certificate/services/user-certificate-domain.service';
|
||||
|
||||
@Injectable()
|
||||
export class DocumentAggregator {
|
||||
constructor(
|
||||
@Inject(DOCUMENT_REPOSITORY) private readonly documentRepository: DocumentRepository,
|
||||
private readonly accountDomainService: AccountDomainService
|
||||
@Inject(forwardRef(() => ACCOUNT_DOMAIN_SERVICE)) private readonly accountDomainService: AccountDomainService,
|
||||
@Inject(USER_CERTIFICATE_DOMAIN_SERVICE) private readonly userCertificateService: UserCertificateDomainService
|
||||
) {}
|
||||
private readonly EMPTY_HASH = '0000000000000000000000000000000000000000000000000000000000000000';
|
||||
|
||||
/**
|
||||
* Создает агрегатор документов на основе полного документа и подписанного документа
|
||||
@@ -25,12 +32,9 @@ export class DocumentAggregator {
|
||||
*/
|
||||
public async buildDocumentAggregate(signedDoc: ISignedDocumentDomainInterface): Promise<DocumentDomainAggregate | null> {
|
||||
// Получаем полный документ по хешу
|
||||
const document = await this.getDocumentByHash(signedDoc.doc_hash);
|
||||
if (signedDoc.doc_hash === this.EMPTY_HASH) return null;
|
||||
|
||||
// Проверяем, что в метаданных документа есть username
|
||||
if (!signedDoc.meta || !signedDoc.meta.username) {
|
||||
throw new BadRequestException('В документе отсутствует информация о пользователе (username)');
|
||||
}
|
||||
const document = await this.getDocumentByHash(signedDoc.doc_hash);
|
||||
|
||||
// Массив для хранения информации о подписантах
|
||||
const signatureInfos: SignatureInfoDomainInterface[] = [];
|
||||
@@ -41,6 +45,9 @@ export class DocumentAggregator {
|
||||
// Получаем данные подписанта на основе signer из подписи
|
||||
const signer = await this.accountDomainService.getPrivateAccount(signature.signer);
|
||||
|
||||
// Создаем сертификат подписанта из полной информации
|
||||
const signerCertificate = this.userCertificateService.createCertificateFromUserData(signer);
|
||||
|
||||
// Формируем объект информации о подписи
|
||||
const signatureInfo: SignatureInfoDomainInterface = {
|
||||
id: signature.id,
|
||||
@@ -50,7 +57,7 @@ export class DocumentAggregator {
|
||||
signature: signature.signature,
|
||||
signed_at: signature.signed_at,
|
||||
is_valid: Classes.Document.validateSignature(signature),
|
||||
signer_info: signer,
|
||||
signer_certificate: signerCertificate,
|
||||
meta: signature.meta,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import { forwardRef, Module } from '@nestjs/common';
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { DocumentDomainInteractor } from './interactors/document.interactor';
|
||||
import { InfrastructureModule } from '~/infrastructure/infrastructure.module';
|
||||
import { DocumentDomainService } from './services/document-domain.service';
|
||||
import { DocumentAggregator } from './aggregators/document.aggregator';
|
||||
import { AccountDomainModule } from '~/domain/account/account-domain.module';
|
||||
import { DocumentPackageAggregator } from './aggregators/document-package.aggregator';
|
||||
import { DocumentPackageUtils } from './aggregators/document-package-utils.aggregator';
|
||||
import { DocumentPackageV0Aggregator } from './aggregators/document-package-v0.aggregator';
|
||||
import { DocumentPackageV1Aggregator } from './aggregators/document-package-v1.aggregator';
|
||||
import { DocumentAggregationService } from './services/document-aggregation.service';
|
||||
import { UserCertificateDomainModule } from '~/domain/user-certificate/user-certificate.module';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [InfrastructureModule, forwardRef(() => AccountDomainModule)],
|
||||
imports: [InfrastructureModule, UserCertificateDomainModule],
|
||||
providers: [
|
||||
DocumentDomainInteractor,
|
||||
DocumentDomainService,
|
||||
@@ -19,6 +21,7 @@ import { DocumentPackageV1Aggregator } from './aggregators/document-package-v1.a
|
||||
DocumentPackageUtils,
|
||||
DocumentPackageV0Aggregator,
|
||||
DocumentPackageV1Aggregator,
|
||||
DocumentAggregationService,
|
||||
],
|
||||
exports: [
|
||||
DocumentDomainInteractor,
|
||||
@@ -28,6 +31,7 @@ import { DocumentPackageV1Aggregator } from './aggregators/document-package-v1.a
|
||||
DocumentPackageUtils,
|
||||
DocumentPackageV0Aggregator,
|
||||
DocumentPackageV1Aggregator,
|
||||
DocumentAggregationService,
|
||||
],
|
||||
})
|
||||
export class DocumentDomainModule {}
|
||||
|
||||
@@ -9,13 +9,17 @@ import { toDotNotation } from '~/utils/toDotNotation';
|
||||
import { getActions } from '~/utils/getFetch';
|
||||
import { SovietContract } from 'cooptypes';
|
||||
import { DocumentPackageAggregator } from '../aggregators/document-package.aggregator';
|
||||
import { DocumentAggregator } from '../aggregators/document.aggregator';
|
||||
import type { DocumentAggregateDomainInterface } from '../interfaces/document-domain-aggregate.interface';
|
||||
import type { ISignedDocumentDomainInterface } from '../interfaces/signed-document-domain.interface';
|
||||
|
||||
@Injectable()
|
||||
export class DocumentDomainInteractor {
|
||||
constructor(
|
||||
private readonly documentDomainService: DocumentDomainService,
|
||||
@Inject(forwardRef(() => DocumentPackageAggregator))
|
||||
private readonly documentPackageAggregator: DocumentPackageAggregator
|
||||
private readonly documentPackageAggregator: DocumentPackageAggregator,
|
||||
private readonly documentAggregator: DocumentAggregator
|
||||
) {}
|
||||
|
||||
public async generateDocument(data: GenerateDocumentDomainInterfaceWithOptions): Promise<DocumentDomainEntity> {
|
||||
@@ -46,4 +50,17 @@ export class DocumentDomainInteractor {
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Строит агрегат документа
|
||||
* @param signedDocument Подписанный документ
|
||||
* @returns Агрегат документа или null
|
||||
*/
|
||||
async buildDocumentAggregate(
|
||||
signedDocument: ISignedDocumentDomainInterface | null | undefined
|
||||
): Promise<DocumentAggregateDomainInterface | null> {
|
||||
if (!signedDocument) return null;
|
||||
|
||||
return await this.documentAggregator.buildDocumentAggregate(signedDocument);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import type { Cooperative } from 'cooptypes';
|
||||
import type { ExtendedBlockchainActionDomainInterface } from '~/domain/agenda/interfaces/extended-blockchain-action-domain.interface';
|
||||
import type { DocumentAggregateDomainInterface } from './document-domain-aggregate.interface';
|
||||
|
||||
export interface ActDetailAggregateDomainInterface {
|
||||
action: Cooperative.Blockchain.IExtendedAction;
|
||||
action: ExtendedBlockchainActionDomainInterface;
|
||||
documentAggregate: DocumentAggregateDomainInterface;
|
||||
}
|
||||
|
||||
+4
-4
@@ -1,9 +1,9 @@
|
||||
import type { Cooperative } from 'cooptypes';
|
||||
import type { ExtendedBlockchainActionDomainInterface } from '~/domain/agenda/interfaces/extended-blockchain-action-domain.interface';
|
||||
import type { DocumentAggregateDomainInterface } from './document-domain-aggregate.interface';
|
||||
|
||||
export interface DecisionDetailAggregateDomainInterface {
|
||||
action: Cooperative.Blockchain.IExtendedAction;
|
||||
action: ExtendedBlockchainActionDomainInterface;
|
||||
documentAggregate: DocumentAggregateDomainInterface;
|
||||
votes_for: Cooperative.Blockchain.IExtendedAction[];
|
||||
votes_against: Cooperative.Blockchain.IExtendedAction[];
|
||||
votes_for: ExtendedBlockchainActionDomainInterface[];
|
||||
votes_against: ExtendedBlockchainActionDomainInterface[];
|
||||
}
|
||||
|
||||
+2
-4
@@ -1,12 +1,10 @@
|
||||
import type { EntrepreneurDomainInterface } from '~/domain/common/interfaces/entrepreneur-domain.interface';
|
||||
import type { IndividualDomainInterface } from '~/domain/common/interfaces/individual-domain.interface';
|
||||
import type { OrganizationDomainInterface } from '~/domain/common/interfaces/organization-domain.interface';
|
||||
import type { ISignedDocumentDomainInterface } from './signed-document-domain.interface';
|
||||
import type { ISignatureInfoDomainInterface } from './signature-info-domain.interface';
|
||||
import type { UserCertificateDomainInterface } from '~/domain/user-certificate/interfaces/user-certificate-domain.interface';
|
||||
|
||||
export interface SignatureInfoDomainInterface extends ISignatureInfoDomainInterface {
|
||||
is_valid?: boolean;
|
||||
signer_info?: IndividualDomainInterface | OrganizationDomainInterface | EntrepreneurDomainInterface | null;
|
||||
signer_certificate?: UserCertificateDomainInterface | null;
|
||||
}
|
||||
|
||||
export interface ExtendedSignedDocumentDomainInterface extends ISignedDocumentDomainInterface {
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import type { Cooperative } from 'cooptypes';
|
||||
import type { ExtendedBlockchainActionDomainInterface } from '~/domain/agenda/interfaces/extended-blockchain-action-domain.interface';
|
||||
import type { DocumentAggregateDomainInterface } from './document-domain-aggregate.interface';
|
||||
|
||||
export interface StatementDetailAggregateDomainInterface {
|
||||
action: Cooperative.Blockchain.IExtendedAction;
|
||||
action: ExtendedBlockchainActionDomainInterface;
|
||||
documentAggregate: DocumentAggregateDomainInterface;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DocumentAggregator } from '../aggregators/document.aggregator';
|
||||
import type { ISignedDocumentDomainInterface } from '../interfaces/signed-document-domain.interface';
|
||||
import type { DocumentDomainAggregate } from '../aggregates/document-domain.aggregate';
|
||||
|
||||
/**
|
||||
* Сервис для агрегации документов в доменном слое
|
||||
* Выделен в отдельный сервис для избежания дублирования кода
|
||||
* и соблюдения принципов чистой архитектуры
|
||||
*/
|
||||
@Injectable()
|
||||
export class DocumentAggregationService {
|
||||
// Пустой хеш для проверки документов
|
||||
private readonly EMPTY_HASH = '0000000000000000000000000000000000000000000000000000000000000000';
|
||||
|
||||
constructor(private readonly documentAggregator: DocumentAggregator) {}
|
||||
|
||||
/**
|
||||
* Создает агрегат документа из формата ISignedDocumentDomainInterface
|
||||
* @param signedDocument Подписанный документ
|
||||
* @returns Агрегат документа
|
||||
*/
|
||||
async buildDocumentAggregate(signedDocument: ISignedDocumentDomainInterface): Promise<DocumentDomainAggregate | null> {
|
||||
if (!signedDocument) return null;
|
||||
|
||||
// Проверяем, что хеш документа не пустой
|
||||
if (signedDocument.doc_hash === this.EMPTY_HASH) return null;
|
||||
|
||||
// Используем существующий агрегатор для сборки агрегата
|
||||
return this.documentAggregator.buildDocumentAggregate(signedDocument);
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,7 @@ export class DocumentDomainService {
|
||||
page,
|
||||
limit,
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ import { AgendaDomainModule } from './agenda/agenda-domain.module';
|
||||
import { CooplaceDomainModule } from './cooplace/cooplace.module';
|
||||
import { DesktopDomainModule } from './desktop/desktop-domain.module';
|
||||
import { MeetDomainModule } from './meet/meet-domain.module';
|
||||
import { ControllerWsMeetModule } from './meet/controllers-ws-meet.module';
|
||||
import { UserCertificateDomainModule } from './user-certificate/user-certificate.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -35,6 +37,8 @@ import { MeetDomainModule } from './meet/meet-domain.module';
|
||||
ParticipantDomainModule,
|
||||
CooplaceDomainModule,
|
||||
MeetDomainModule,
|
||||
ControllerWsMeetModule,
|
||||
UserCertificateDomainModule,
|
||||
],
|
||||
exports: [
|
||||
AuthDomainModule,
|
||||
@@ -53,6 +57,7 @@ import { MeetDomainModule } from './meet/meet-domain.module';
|
||||
ParticipantDomainModule,
|
||||
CooplaceDomainModule,
|
||||
MeetDomainModule,
|
||||
UserCertificateDomainModule,
|
||||
],
|
||||
})
|
||||
export class DomainModule {}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { forwardRef, Inject, Injectable } from '@nestjs/common';
|
||||
import { AccountExtensionPort } from '../ports/account-extension-port';
|
||||
import { AccountDomainInteractor } from '~/domain/account/interactors/account.interactor';
|
||||
import { AccountDomainEntity } from '~/domain/account/entities/account-domain.entity';
|
||||
import { PaginationInputDTO } from '~/modules/common/dto/pagination.dto';
|
||||
import { PaginationResultDomainInterface } from '~/domain/common/interfaces/pagination.interface';
|
||||
import type { GetAccountsInputDomainInterface } from '~/domain/account/interfaces/get-accounts-input.interface';
|
||||
|
||||
@Injectable()
|
||||
export class AccountExtensionAdapter implements AccountExtensionPort {
|
||||
constructor(
|
||||
@Inject(forwardRef(() => AccountDomainInteractor)) private readonly accountInteractor: AccountDomainInteractor
|
||||
) {}
|
||||
|
||||
async getAccounts(
|
||||
data: GetAccountsInputDomainInterface = {},
|
||||
options?: PaginationInputDTO
|
||||
): Promise<PaginationResultDomainInterface<AccountDomainEntity>> {
|
||||
return this.accountInteractor.getAccounts(data, options);
|
||||
}
|
||||
|
||||
async getAccount(username: string): Promise<AccountDomainEntity> {
|
||||
return this.accountInteractor.getAccount(username);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { forwardRef, Inject, Injectable } from '@nestjs/common';
|
||||
import { MeetExtensionPort } from '../ports/meet-extension-port';
|
||||
import { MeetDomainInteractor } from '~/domain/meet/interactors/meet.interactor';
|
||||
import type { MeetAggregate } from '~/domain/meet/aggregates/meet-domain.aggregate';
|
||||
import { GetMeetInputDomainInterface } from '~/domain/meet/interfaces/get-meet-input-domain.interface';
|
||||
import { GetMeetsInputDomainInterface } from '~/domain/meet/interfaces/get-meets-input-domain.interface';
|
||||
|
||||
@Injectable()
|
||||
export class MeetExtensionAdapter implements MeetExtensionPort {
|
||||
constructor(@Inject(forwardRef(() => MeetDomainInteractor)) private readonly meetInteractor: MeetDomainInteractor) {}
|
||||
|
||||
async getMeets(data: GetMeetsInputDomainInterface, username?: string): Promise<MeetAggregate[]> {
|
||||
return this.meetInteractor.getMeets(data, username);
|
||||
}
|
||||
|
||||
async getMeet(data: GetMeetInputDomainInterface, username?: string): Promise<MeetAggregate> {
|
||||
return this.meetInteractor.getMeet(data, username);
|
||||
}
|
||||
}
|
||||
@@ -9,10 +9,12 @@ import { ExtensionDomainListingInteractor } from './interactors/extension-listin
|
||||
|
||||
import { ExtensionsModule } from '~/extensions/extensions.module';
|
||||
import { nestApp } from '~/index';
|
||||
import { ExtensionPortsModule } from './extension-ports.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ExtensionsModule.register(), // Регистрируем модуль расширений с доступом ко всем плагинам
|
||||
ExtensionsModule.register(), // Регистрируем модуль расширений
|
||||
ExtensionPortsModule, // Импортируем модуль с портами
|
||||
],
|
||||
providers: [
|
||||
ExtensionDomainInteractor,
|
||||
@@ -27,6 +29,7 @@ import { nestApp } from '~/index';
|
||||
ExtensionDomainService,
|
||||
ExtensionDomainListingService,
|
||||
ExtensionDomainListingInteractor,
|
||||
ExtensionPortsModule, // Экспортируем модуль с портами
|
||||
], // Экспортируем интерактор и сервис для использования в других модулях
|
||||
})
|
||||
export class ExtensionDomainModule {
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { forwardRef, Module } from '@nestjs/common';
|
||||
import { AccountExtensionAdapter } from './adapters/account-extension.adapter';
|
||||
import { MeetExtensionAdapter } from './adapters/meet-extension.adapter';
|
||||
import { ACCOUNT_EXTENSION_PORT } from './ports/account-extension-port';
|
||||
import { MEET_EXTENSION_PORT } from './ports/meet-extension-port';
|
||||
import { MeetDomainModule } from '~/domain/meet/meet-domain.module';
|
||||
|
||||
/**
|
||||
* Модуль для предоставления портов расширениям
|
||||
* Этот модуль не имеет зависимостей от других модулей для избежания циклических зависимостей
|
||||
* AccountDomainModule импортировать не нужно, так как он глобальный
|
||||
*/
|
||||
@Module({
|
||||
imports: [forwardRef(() => MeetDomainModule)],
|
||||
providers: [
|
||||
AccountExtensionAdapter,
|
||||
MeetExtensionAdapter,
|
||||
{
|
||||
provide: ACCOUNT_EXTENSION_PORT,
|
||||
useExisting: AccountExtensionAdapter,
|
||||
},
|
||||
{
|
||||
provide: MEET_EXTENSION_PORT,
|
||||
useExisting: MeetExtensionAdapter,
|
||||
},
|
||||
],
|
||||
exports: [ACCOUNT_EXTENSION_PORT, MEET_EXTENSION_PORT],
|
||||
})
|
||||
export class ExtensionPortsModule {}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { AccountDomainEntity } from '~/domain/account/entities/account-domain.entity';
|
||||
import { PaginationInputDTO } from '~/modules/common/dto/pagination.dto';
|
||||
import { PaginationResultDomainInterface } from '~/domain/common/interfaces/pagination.interface';
|
||||
import type { GetAccountsInputDomainInterface } from '~/domain/account/interfaces/get-accounts-input.interface';
|
||||
|
||||
export interface AccountExtensionPort {
|
||||
getAccounts(
|
||||
data: GetAccountsInputDomainInterface,
|
||||
options?: PaginationInputDTO
|
||||
): Promise<PaginationResultDomainInterface<AccountDomainEntity>>;
|
||||
|
||||
getAccount(username: string): Promise<AccountDomainEntity>;
|
||||
}
|
||||
|
||||
export const ACCOUNT_EXTENSION_PORT = Symbol('AccountExtensionPort');
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { MeetAggregate } from '~/domain/meet/aggregates/meet-domain.aggregate';
|
||||
import { GetMeetInputDomainInterface } from '~/domain/meet/interfaces/get-meet-input-domain.interface';
|
||||
import { GetMeetsInputDomainInterface } from '~/domain/meet/interfaces/get-meets-input-domain.interface';
|
||||
|
||||
export interface MeetExtensionPort {
|
||||
getMeets(data: GetMeetsInputDomainInterface, username?: string): Promise<MeetAggregate[]>;
|
||||
getMeet(data: GetMeetInputDomainInterface, username?: string): Promise<MeetAggregate>;
|
||||
}
|
||||
|
||||
export const MEET_EXTENSION_PORT = Symbol('MeetExtensionPort');
|
||||
@@ -3,9 +3,9 @@ import type { MeetPreProcessingDomainEntity } from '../entities/meet-pre-domain.
|
||||
import type { MeetProcessedDomainEntity } from '../entities/meet-processed-domain.entity';
|
||||
import type { MeetProcessingDomainEntity } from '../entities/meet-processing-domain.entity';
|
||||
import { assertSingleHash } from '~/shared/asserts/single-hash.assert';
|
||||
import type { MeetDomainAggregate } from '../interfaces/meet-aggregate.interface';
|
||||
import type { MeetDomainAggregateDomainInterface } from '../interfaces/meet-aggregate.interface';
|
||||
|
||||
export class MeetAggregate implements MeetDomainAggregate {
|
||||
export class MeetAggregate implements MeetDomainAggregateDomainInterface {
|
||||
public readonly hash: string;
|
||||
|
||||
constructor(
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { MeetDomainInteractor } from './interactors/meet.interactor';
|
||||
import { DocumentDomainModule } from '../document/document.module';
|
||||
import { InfrastructureModule } from '~/infrastructure/infrastructure.module';
|
||||
import { setMeetInteractor } from '~/controllers/ws.controller';
|
||||
|
||||
@Module({
|
||||
imports: [DocumentDomainModule, InfrastructureModule],
|
||||
providers: [
|
||||
MeetDomainInteractor,
|
||||
{
|
||||
provide: 'INIT_WS_CONTROLLER',
|
||||
useFactory: (meetInteractor: MeetDomainInteractor) => {
|
||||
// Устанавливаем интерактор для использования в WS контроллере
|
||||
setMeetInteractor(meetInteractor);
|
||||
return true;
|
||||
},
|
||||
inject: [MeetDomainInteractor],
|
||||
},
|
||||
],
|
||||
exports: [MeetDomainInteractor],
|
||||
})
|
||||
export class ControllerWsMeetModule {}
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { MeetPreProcessingDomainInterface } from '../interfaces/meet-pre-domain.interface';
|
||||
import type { DocumentAggregateDomainInterface } from '~/domain/document/interfaces/document-domain-aggregate.interface';
|
||||
import type { AgendaMeetPointInputDomainInterface } from '../interfaces/agenda-meet-point-input-domain.interface';
|
||||
import type { AgendaGeneralMeetPointInputDomainInterface } from '../interfaces/agenda-meet-point-input-domain.interface';
|
||||
export class MeetPreProcessingDomainEntity implements MeetPreProcessingDomainInterface {
|
||||
public readonly coopname!: string;
|
||||
public readonly hash!: string;
|
||||
public readonly initiator!: string;
|
||||
public readonly presider!: string;
|
||||
public readonly secretary!: string;
|
||||
public readonly agenda!: AgendaMeetPointInputDomainInterface[];
|
||||
public readonly agenda!: AgendaGeneralMeetPointInputDomainInterface[];
|
||||
public readonly open_at!: Date;
|
||||
public readonly close_at!: Date;
|
||||
public readonly proposal?: DocumentAggregateDomainInterface;
|
||||
|
||||
@@ -1,12 +1,37 @@
|
||||
import type { MeetContract } from 'cooptypes';
|
||||
import type { MeetProcessedDomainInterface } from '../interfaces/meet-processed-domain.interface';
|
||||
import type { BlockchainActionDomainInterface } from '~/domain/common/interfaces/blockchain-action-domain.interface';
|
||||
import type { ISignedDocumentDomainInterface } from '~/domain/document/interfaces/signed-document-domain.interface';
|
||||
import type { UserCertificateDomainInterface } from '~/domain/user-certificate/interfaces/user-certificate-domain.interface';
|
||||
|
||||
export class MeetProcessedDomainEntity implements MeetProcessedDomainInterface {
|
||||
public readonly coopname!: string;
|
||||
public readonly hash!: string;
|
||||
public readonly decision!: BlockchainActionDomainInterface<MeetContract.Actions.NewDecision.IInput>;
|
||||
public readonly presider!: string;
|
||||
public readonly secretary!: string;
|
||||
public readonly presider_certificate?: UserCertificateDomainInterface | null;
|
||||
public readonly secretary_certificate?: UserCertificateDomainInterface | null;
|
||||
public readonly results!: any[];
|
||||
public readonly signed_ballots!: number;
|
||||
public readonly quorum_percent!: number;
|
||||
public readonly quorum_passed!: boolean;
|
||||
public readonly decision!: ISignedDocumentDomainInterface;
|
||||
|
||||
constructor(data: MeetProcessedDomainInterface) {
|
||||
Object.assign(this, data);
|
||||
constructor(
|
||||
data: Partial<MeetProcessedDomainInterface> & {
|
||||
decision: ISignedDocumentDomainInterface;
|
||||
presider: string;
|
||||
secretary: string;
|
||||
}
|
||||
) {
|
||||
this.coopname = data.coopname || '';
|
||||
this.hash = data.hash || '';
|
||||
this.presider = data.presider;
|
||||
this.secretary = data.secretary;
|
||||
this.presider_certificate = data.presider_certificate || null;
|
||||
this.secretary_certificate = data.secretary_certificate || null;
|
||||
this.results = data.results || [];
|
||||
this.signed_ballots = data.signed_ballots || 0;
|
||||
this.quorum_percent = data.quorum_percent || 0;
|
||||
this.quorum_passed = data.quorum_passed || false;
|
||||
this.decision = data.decision;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,80 @@
|
||||
import type { MeetProcessingDomainInterface } from '../interfaces/meet-processing-domain.interface';
|
||||
import type { MeetRowProcessingDomainInterface } from '../interfaces/meet-row-processing-domain.interface';
|
||||
import type { QuestionRowProcessingDomainInterface } from '../interfaces/question-row-processing-domain.interface';
|
||||
import { ExtendedMeetStatus } from '../enums/extended-meet-status.enum';
|
||||
|
||||
export class MeetProcessingDomainEntity implements MeetProcessingDomainInterface {
|
||||
public readonly hash!: string;
|
||||
public readonly meet!: MeetRowProcessingDomainInterface;
|
||||
public readonly questions!: QuestionRowProcessingDomainInterface[];
|
||||
public readonly isVoted: boolean = false;
|
||||
public readonly extendedStatus: ExtendedMeetStatus = ExtendedMeetStatus.NONE;
|
||||
|
||||
constructor(data: MeetProcessingDomainInterface) {
|
||||
constructor(data: MeetProcessingDomainInterface, username?: string) {
|
||||
Object.assign(this, data);
|
||||
|
||||
// Если передан username, проверяем, голосовал ли пользователь
|
||||
if (username && this.questions) {
|
||||
this.isVoted = this.questions.some(
|
||||
(question) =>
|
||||
question.voters_for.includes(username) ||
|
||||
question.voters_against.includes(username) ||
|
||||
question.voters_abstained.includes(username)
|
||||
);
|
||||
}
|
||||
|
||||
// Определяем расширенный статус собрания
|
||||
this.extendedStatus = this.calculateExtendedStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Вычисляет расширенный статус собрания на основе дат и состояния
|
||||
*/
|
||||
private calculateExtendedStatus(): ExtendedMeetStatus {
|
||||
// Если нет данных о meet, возвращаем базовый статус
|
||||
if (!this.meet) {
|
||||
return ExtendedMeetStatus.NONE;
|
||||
}
|
||||
|
||||
const { status, open_at, close_at, quorum_passed } = this.meet;
|
||||
const now = new Date();
|
||||
|
||||
// Базовые статусы блокчейна
|
||||
if (status === 'created') {
|
||||
return ExtendedMeetStatus.CREATED;
|
||||
} else if (status === 'preclosed') {
|
||||
return ExtendedMeetStatus.PRECLOSED;
|
||||
} else if (status === 'closed') {
|
||||
return ExtendedMeetStatus.CLOSED;
|
||||
} else if (status === 'onrestart') {
|
||||
return ExtendedMeetStatus.ONRESTART;
|
||||
}
|
||||
|
||||
// Расширенные статусы для 'authorized'
|
||||
if (status === 'authorized') {
|
||||
// Ожидание открытия
|
||||
if (now < open_at) {
|
||||
return ExtendedMeetStatus.WAITING_FOR_OPENING;
|
||||
}
|
||||
|
||||
// Голосование идет
|
||||
if (close_at && now >= open_at && now <= close_at) {
|
||||
return ExtendedMeetStatus.VOTING_IN_PROGRESS;
|
||||
}
|
||||
|
||||
// Голосование завершено
|
||||
if (close_at && now > close_at) {
|
||||
// Не достигнут кворум
|
||||
if (!quorum_passed) {
|
||||
return ExtendedMeetStatus.EXPIRED_NO_QUORUM;
|
||||
}
|
||||
|
||||
// Кворум достигнут, ожидание подписей
|
||||
return ExtendedMeetStatus.VOTING_COMPLETED;
|
||||
}
|
||||
}
|
||||
|
||||
// По умолчанию возвращаем статус из блокчейна
|
||||
return status as ExtendedMeetStatus;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { registerEnumType } from '@nestjs/graphql';
|
||||
|
||||
// Расширенные статусы собрания
|
||||
export enum ExtendedMeetStatus {
|
||||
// Начальное состояние
|
||||
NONE = 'none',
|
||||
|
||||
// Базовые статусы блокчейна
|
||||
CREATED = 'created',
|
||||
AUTHORIZED = 'authorized',
|
||||
PRECLOSED = 'preclosed',
|
||||
CLOSED = 'closed',
|
||||
|
||||
// Дополнительные статусы
|
||||
WAITING_FOR_OPENING = 'waitingForOpening', // Ожидает открытия
|
||||
VOTING_IN_PROGRESS = 'votingInProgress', // Голосование идет
|
||||
EXPIRED_NO_QUORUM = 'expiredNoQuorum', // Истекло без кворума
|
||||
VOTING_COMPLETED = 'votingCompleted', // Ожидает подписей
|
||||
ONRESTART = 'onrestart', // Ожидаем утверждения новой даты собрания
|
||||
}
|
||||
|
||||
// Регистрируем перечисление для GraphQL
|
||||
registerEnumType(ExtendedMeetStatus, {
|
||||
name: 'ExtendedMeetStatus',
|
||||
description: 'Расширенный статус собрания на основе дат и состояния',
|
||||
valuesMap: {
|
||||
NONE: {
|
||||
description: 'Неопределенное состояние',
|
||||
},
|
||||
CREATED: {
|
||||
description: 'Создано',
|
||||
},
|
||||
AUTHORIZED: {
|
||||
description: 'Авторизовано',
|
||||
},
|
||||
PRECLOSED: {
|
||||
description: 'Предварительно закрыто',
|
||||
},
|
||||
CLOSED: {
|
||||
description: 'Закрыто',
|
||||
},
|
||||
ONRESTART: {
|
||||
description: 'Ожидаем утверждения новой даты собрания',
|
||||
},
|
||||
WAITING_FOR_OPENING: {
|
||||
description: 'Ожидает открытия',
|
||||
},
|
||||
VOTING_IN_PROGRESS: {
|
||||
description: 'Голосование идет',
|
||||
},
|
||||
EXPIRED_NO_QUORUM: {
|
||||
description: 'Истекло без кворума',
|
||||
},
|
||||
VOTING_COMPLETED: {
|
||||
description: 'Голосование завершено, ожидает подписей',
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -3,7 +3,7 @@ import { DocumentDomainService } from '~/domain/document/services/document-domai
|
||||
import { DocumentDomainEntity } from '~/domain/document/entity/document-domain.entity';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { MEET_BLOCKCHAIN_PORT, MeetBlockchainPort } from '../ports/meet-blockchain.port';
|
||||
import { MEET_REPOSITORY, MeetRepository } from '../repositories/meet.repository';
|
||||
import { MeetPreProcessingRepository, MEET_REPOSITORY } from '../repositories/meet-pre.repository';
|
||||
import { MeetAggregate } from '../aggregates/meet-domain.aggregate';
|
||||
import type { CreateAnnualGeneralMeetInputDomainInterface } from '../interfaces/create-annual-meet-input-domain.interface';
|
||||
import { DocumentAggregator } from '~/domain/document/aggregators/document.aggregator';
|
||||
@@ -14,12 +14,18 @@ import { GetMeetInputDomainInterface } from '../interfaces/get-meet-input-domain
|
||||
import { GetMeetsInputDomainInterface } from '../interfaces/get-meets-input-domain.interface';
|
||||
import { SignBySecretaryOnAnnualGeneralMeetInputDomainInterface } from '../interfaces/sign-by-secretary-on-annual-general-meet-input-domain.interface';
|
||||
import { SignByPresiderOnAnnualGeneralMeetInputDomainInterface } from '../interfaces/sign-by-presider-on-annual-general-meet-input-domain.interface';
|
||||
import { MEET_PROCESSED_REPOSITORY, MeetProcessedRepository } from '../repositories/meet-processed.repository';
|
||||
import { ProcessMeetDecisionInputDomainInterface } from '../interfaces/process-meet-decision-input-domain.interface';
|
||||
import { MeetProcessedDomainEntity } from '../entities/meet-processed-domain.entity';
|
||||
import { NotifyOnAnnualGeneralMeetInputDomainInterface } from '../interfaces/notify-on-annual-general-meet-input-domain.interface';
|
||||
import { generateUniqueHash } from '~/utils/generate-hash.util';
|
||||
|
||||
@Injectable()
|
||||
export class MeetDomainInteractor {
|
||||
constructor(
|
||||
private readonly documentDomainService: DocumentDomainService,
|
||||
@Inject(MEET_REPOSITORY) private readonly meetRepository: MeetRepository,
|
||||
@Inject(MEET_REPOSITORY) private readonly meetPreRepository: MeetPreProcessingRepository,
|
||||
@Inject(MEET_PROCESSED_REPOSITORY) private readonly meetProcessedRepository: MeetProcessedRepository,
|
||||
@Inject(MEET_BLOCKCHAIN_PORT) private readonly meetBlockchainPort: MeetBlockchainPort,
|
||||
private readonly documentAggregator: DocumentAggregator
|
||||
) {}
|
||||
@@ -35,13 +41,24 @@ export class MeetDomainInteractor {
|
||||
async createAnnualGeneralMeet(data: CreateAnnualGeneralMeetInputDomainInterface): Promise<MeetAggregate> {
|
||||
// Создаем агрегатор документов с помощью агрегатора документов
|
||||
const documentAggregate = await this.documentAggregator.buildDocumentAggregate(data.proposal);
|
||||
const preProcessing = new MeetPreProcessingDomainEntity({ ...data, proposal: documentAggregate });
|
||||
|
||||
// Преобразуем null в undefined для соответствия доменному интерфейсу
|
||||
const proposalAggregate = documentAggregate || undefined;
|
||||
|
||||
// Генерируем уникальный хэш для собрания
|
||||
const hash = generateUniqueHash();
|
||||
|
||||
const preProcessing = new MeetPreProcessingDomainEntity({
|
||||
...data,
|
||||
hash,
|
||||
proposal: proposalAggregate,
|
||||
});
|
||||
|
||||
// Сохраняем данные в репозиторий
|
||||
await this.meetRepository.create(preProcessing);
|
||||
await this.meetPreRepository.create(preProcessing);
|
||||
|
||||
// Вызов блокчейн порта для создания собрания
|
||||
await this.meetBlockchainPort.createMeet(data);
|
||||
await this.meetBlockchainPort.createMeet({ ...data, hash });
|
||||
|
||||
// Получаем обновленные данные из блокчейна
|
||||
const processingData = await this.meetBlockchainPort.getMeet({
|
||||
@@ -50,7 +67,7 @@ export class MeetDomainInteractor {
|
||||
});
|
||||
|
||||
// Получаем сохраненные данные из репозитория
|
||||
const preMeet = await this.meetRepository.findByHash(preProcessing.hash);
|
||||
const preMeet = await this.meetPreRepository.findByHash(preProcessing.hash);
|
||||
|
||||
// Создаем агрегат из обоих источников данных (репозиторий и блокчейн)
|
||||
return new MeetAggregate(preMeet, processingData);
|
||||
@@ -88,21 +105,33 @@ export class MeetDomainInteractor {
|
||||
return await this.documentDomainService.generateDocument({ data, options });
|
||||
}
|
||||
|
||||
async getMeet(data: GetMeetInputDomainInterface): Promise<MeetAggregate> {
|
||||
async getMeet(data: GetMeetInputDomainInterface, username?: string): Promise<MeetAggregate> {
|
||||
// Получаем данные из блокчейна через порт
|
||||
const processingMeet = await this.meetBlockchainPort.getMeet(data);
|
||||
const processingMeet = await this.meetBlockchainPort.getMeet({
|
||||
coopname: data.coopname,
|
||||
hash: data.hash,
|
||||
username,
|
||||
});
|
||||
|
||||
// Получаем данные из репозитория
|
||||
const preMeet = await this.meetRepository.findByHash(data.hash);
|
||||
const preMeet = await this.meetPreRepository.findByHash(data.hash);
|
||||
|
||||
// Получаем данные из репозитория обработанных собраний
|
||||
const processedMeetEntity = await this.meetProcessedRepository.findByHash(data.hash);
|
||||
|
||||
// Создаем агрегат из обоих источников данных (репозиторий и блокчейн)
|
||||
// MeetAggregate сам проверит наличие хотя бы одного элемента
|
||||
return new MeetAggregate(preMeet, processingMeet);
|
||||
const meetAggregate = new MeetAggregate(preMeet, processingMeet, processedMeetEntity);
|
||||
|
||||
// Вернем готовый агрегат
|
||||
return meetAggregate;
|
||||
}
|
||||
|
||||
async getMeets(data: GetMeetsInputDomainInterface): Promise<MeetAggregate[]> {
|
||||
async getMeets(data: GetMeetsInputDomainInterface, username?: string): Promise<MeetAggregate[]> {
|
||||
// Получаем данные из блокчейна через порт
|
||||
const processingDataList = await this.meetBlockchainPort.getMeets(data);
|
||||
const processingDataList = await this.meetBlockchainPort.getMeets({
|
||||
coopname: data.coopname,
|
||||
username,
|
||||
});
|
||||
|
||||
// Если данных нет, возвращаем пустой массив
|
||||
if (!processingDataList || processingDataList.length === 0) {
|
||||
@@ -113,13 +142,16 @@ export class MeetDomainInteractor {
|
||||
const meetsAggregates = await Promise.all(
|
||||
processingDataList.map(async (processingData) => {
|
||||
// Получаем данные pre из репозитория для каждого элемента по его хешу
|
||||
const preMeet = await this.meetRepository.findByHash(processingData.hash);
|
||||
const preMeet = await this.meetPreRepository.findByHash(processingData.hash);
|
||||
|
||||
// Получаем данные processed из репозитория
|
||||
const processedMeetEntity = await this.meetProcessedRepository.findByHash(processingData.hash);
|
||||
|
||||
// Создаем агрегат с данными из репозитория и блокчейна
|
||||
return new MeetAggregate(preMeet, processingData);
|
||||
return new MeetAggregate(preMeet, processingData, processedMeetEntity);
|
||||
})
|
||||
);
|
||||
console.dir(meetsAggregates, { depth: null });
|
||||
|
||||
return meetsAggregates;
|
||||
}
|
||||
|
||||
@@ -131,28 +163,31 @@ export class MeetDomainInteractor {
|
||||
const processingData = await this.meetBlockchainPort.getMeet({
|
||||
coopname: data.coopname,
|
||||
hash: data.hash,
|
||||
username: data.username,
|
||||
});
|
||||
|
||||
const preMeet = await this.meetRepository.findByHash(data.hash);
|
||||
const preMeet = await this.meetPreRepository.findByHash(data.hash);
|
||||
const processedMeetEntity = await this.meetProcessedRepository.findByHash(data.hash);
|
||||
|
||||
// Создаем агрегат из обоих источников данных
|
||||
return new MeetAggregate(preMeet, processingData);
|
||||
return new MeetAggregate(preMeet, processingData, processedMeetEntity);
|
||||
}
|
||||
|
||||
async restartMeet(data: RestartAnnualGeneralMeetInputDomainInterface): Promise<MeetAggregate> {
|
||||
// Вызов блокчейн порта для перезапуска собрания
|
||||
await this.meetBlockchainPort.restartMeet(data);
|
||||
const new_hash = await this.meetBlockchainPort.restartMeet(data);
|
||||
|
||||
// Получаем обновленные данные из обоих источников
|
||||
const processingData = await this.meetBlockchainPort.getMeet({
|
||||
coopname: data.coopname,
|
||||
hash: data.hash,
|
||||
hash: new_hash,
|
||||
});
|
||||
|
||||
const preMeet = await this.meetRepository.findByHash(data.hash);
|
||||
const preMeet = await this.meetPreRepository.findByHash(new_hash);
|
||||
const processedMeetEntity = await this.meetProcessedRepository.findByHash(new_hash);
|
||||
|
||||
// Создаем агрегат из обоих источников данных
|
||||
return new MeetAggregate(preMeet, processingData);
|
||||
return new MeetAggregate(preMeet, processingData, processedMeetEntity);
|
||||
}
|
||||
|
||||
async signBySecretaryOnAnnualGeneralMeet(
|
||||
@@ -162,9 +197,12 @@ export class MeetDomainInteractor {
|
||||
const processingData = await this.meetBlockchainPort.getMeet({
|
||||
coopname: data.coopname,
|
||||
hash: data.hash,
|
||||
username: data.username,
|
||||
});
|
||||
const preMeet = await this.meetRepository.findByHash(data.hash);
|
||||
return new MeetAggregate(preMeet, processingData);
|
||||
const preMeet = await this.meetPreRepository.findByHash(data.hash);
|
||||
const processedMeetEntity = await this.meetProcessedRepository.findByHash(data.hash);
|
||||
|
||||
return new MeetAggregate(preMeet, processingData, processedMeetEntity);
|
||||
}
|
||||
|
||||
async signByPresiderOnAnnualGeneralMeet(
|
||||
@@ -174,8 +212,48 @@ export class MeetDomainInteractor {
|
||||
const processingData = await this.meetBlockchainPort.getMeet({
|
||||
coopname: data.coopname,
|
||||
hash: data.hash,
|
||||
username: data.username,
|
||||
});
|
||||
const preMeet = await this.meetRepository.findByHash(data.hash);
|
||||
return new MeetAggregate(preMeet, processingData);
|
||||
const preMeet = await this.meetPreRepository.findByHash(data.hash);
|
||||
const processedMeetEntity = await this.meetProcessedRepository.findByHash(data.hash);
|
||||
|
||||
return new MeetAggregate(preMeet, processingData, processedMeetEntity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Обработка события о завершенном общем собрании и его результатах
|
||||
* @param data Данные о решении общего собрания
|
||||
*/
|
||||
async processMeetDecision(data: ProcessMeetDecisionInputDomainInterface): Promise<void> {
|
||||
// Создаем сущность для обработанных данных собрания
|
||||
const processedEntity = new MeetProcessedDomainEntity({
|
||||
...data.decisionData,
|
||||
});
|
||||
|
||||
// Сохраняем данные в репозиторий
|
||||
await this.meetProcessedRepository.save(processedEntity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Уведомление о собрании
|
||||
* @param data Данные уведомления
|
||||
*/
|
||||
async notifyOnAnnualGeneralMeet(data: NotifyOnAnnualGeneralMeetInputDomainInterface): Promise<MeetAggregate> {
|
||||
// Вызов блокчейн порта для отправки уведомления
|
||||
await this.meetBlockchainPort.notifyOnAnnualGeneralMeet(data);
|
||||
|
||||
// Получаем обновленные данные из блокчейна
|
||||
const processingData = await this.meetBlockchainPort.getMeet({
|
||||
coopname: data.coopname,
|
||||
hash: data.meet_hash,
|
||||
username: data.username,
|
||||
});
|
||||
|
||||
// Получаем данные из репозитория
|
||||
const preMeet = await this.meetPreRepository.findByHash(data.meet_hash);
|
||||
const processedMeetEntity = await this.meetProcessedRepository.findByHash(data.meet_hash);
|
||||
|
||||
// Создаем агрегат из обоих источников данных
|
||||
return new MeetAggregate(preMeet, processingData, processedMeetEntity);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
export interface AgendaMeetPointInputDomainInterface {
|
||||
export interface AgendaGeneralMeetPointInputDomainInterface {
|
||||
context: string;
|
||||
title: string;
|
||||
decision: string;
|
||||
|
||||
+4
-3
@@ -1,11 +1,12 @@
|
||||
import type { Cooperative, MeetContract } from 'cooptypes';
|
||||
import type { MeetContract } from 'cooptypes';
|
||||
import type { ISignedDocumentDomainInterface } from '~/domain/document/interfaces/signed-document-domain.interface';
|
||||
|
||||
export type CreateAnnualGeneralMeetInputDomainInterface = Omit<
|
||||
MeetContract.Actions.CreateMeet.IInput,
|
||||
'proposal' | 'open_at' | 'close_at'
|
||||
'proposal' | 'open_at' | 'close_at' | 'hash'
|
||||
> & {
|
||||
proposal: ISignedDocumentDomainInterface<Cooperative.Registry.AnnualGeneralMeetingAgenda.Meta>;
|
||||
proposal: ISignedDocumentDomainInterface;
|
||||
open_at: Date;
|
||||
close_at: Date;
|
||||
hash?: string;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export interface GetMeetInputDomainInterface {
|
||||
coopname: string;
|
||||
hash: string;
|
||||
username?: string;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export interface GetMeetsInputDomainInterface {
|
||||
coopname: string;
|
||||
username?: string;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { MeetProcessingDomainInterface } from './meet-processing-domain.int
|
||||
|
||||
// === Aggregated Lifecycle ===
|
||||
|
||||
export interface MeetDomainAggregate {
|
||||
export interface MeetDomainAggregateDomainInterface {
|
||||
hash: string;
|
||||
pre?: MeetPreProcessingDomainInterface | null;
|
||||
processing?: MeetProcessingDomainInterface | null;
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { ISignedDocumentDomainInterface } from '~/domain/document/interfaces/signed-document-domain.interface';
|
||||
|
||||
export interface MeetQuestionResultDomainInterface {
|
||||
question_id: number;
|
||||
number: number;
|
||||
title: string;
|
||||
decision: string;
|
||||
context: string;
|
||||
votes_for: number;
|
||||
votes_against: number;
|
||||
votes_abstained: number;
|
||||
accepted: boolean;
|
||||
}
|
||||
|
||||
export interface MeetDecisionDomainInterface {
|
||||
coopname: string;
|
||||
hash: string;
|
||||
presider: string;
|
||||
secretary: string;
|
||||
results: MeetQuestionResultDomainInterface[];
|
||||
signed_ballots: number;
|
||||
quorum_percent: number;
|
||||
quorum_passed: boolean;
|
||||
decision: ISignedDocumentDomainInterface;
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
// domain/interfaces/meet-lifecycle-domain.interface.ts
|
||||
|
||||
import type { Cooperative, MeetContract } from 'cooptypes';
|
||||
import type { MeetContract } from 'cooptypes';
|
||||
import { assertType, type AssertKeysMatch } from '~/shared/asserts/blockchain-type.assert';
|
||||
import type { AgendaMeetPointInputDomainInterface } from './agenda-meet-point-input-domain.interface';
|
||||
import type { AgendaGeneralMeetPointInputDomainInterface } from './agenda-meet-point-input-domain.interface';
|
||||
import type { DocumentAggregateDomainInterface } from '~/domain/document/interfaces/document-domain-aggregate.interface';
|
||||
import type { UserCertificateDomainInterface } from '~/domain/user-certificate/interfaces/user-certificate-domain.interface';
|
||||
|
||||
// === PreProcessing ===
|
||||
|
||||
@@ -11,14 +12,15 @@ export interface MeetPreProcessingDomainInterface {
|
||||
coopname: string;
|
||||
hash: string;
|
||||
initiator: string;
|
||||
initiator_certificate?: UserCertificateDomainInterface | null;
|
||||
presider: string;
|
||||
secretary: string;
|
||||
agenda: AgendaMeetPointInputDomainInterface[];
|
||||
presider_certificate?: UserCertificateDomainInterface | null;
|
||||
secretary_certificate?: UserCertificateDomainInterface | null;
|
||||
agenda: AgendaGeneralMeetPointInputDomainInterface[];
|
||||
open_at: Date;
|
||||
close_at: Date;
|
||||
proposal?: DocumentAggregateDomainInterface<
|
||||
Cooperative.Registry.AnnualGeneralMeetingAgenda.Action & Cooperative.Document.IMetaDocument
|
||||
>;
|
||||
proposal?: DocumentAggregateDomainInterface;
|
||||
}
|
||||
|
||||
assertType<AssertKeysMatch<MeetContract.Actions.CreateMeet.IInput, MeetPreProcessingDomainInterface>>();
|
||||
|
||||
+15
-5
@@ -1,10 +1,20 @@
|
||||
// === Processed ===
|
||||
import type { ISignedDocumentDomainInterface } from '~/domain/document/interfaces/signed-document-domain.interface';
|
||||
import type { UserCertificateDomainInterface } from '~/domain/user-certificate/interfaces/user-certificate-domain.interface';
|
||||
|
||||
import type { MeetContract } from 'cooptypes';
|
||||
import type { BlockchainActionDomainInterface } from '~/domain/common/interfaces/blockchain-action-domain.interface';
|
||||
|
||||
// Доменный интерфейс для обработанных собраний
|
||||
export interface MeetProcessedDomainInterface {
|
||||
coopname: string;
|
||||
hash: string;
|
||||
decision: BlockchainActionDomainInterface<MeetContract.Actions.NewDecision.IInput>;
|
||||
//documents
|
||||
presider: string;
|
||||
secretary: string;
|
||||
presider_certificate?: UserCertificateDomainInterface | null;
|
||||
secretary_certificate?: UserCertificateDomainInterface | null;
|
||||
results: any[];
|
||||
signed_ballots: number;
|
||||
quorum_percent: number;
|
||||
quorum_passed: boolean;
|
||||
decision: ISignedDocumentDomainInterface;
|
||||
// Агрегат не является частью доменной сущности,
|
||||
// он создается временно для передачи в DTO
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { MeetRowProcessingDomainInterface } from './meet-row-processing-domain.interface';
|
||||
import type { QuestionRowProcessingDomainInterface } from './question-row-processing-domain.interface';
|
||||
import { ExtendedMeetStatus } from '../enums/extended-meet-status.enum';
|
||||
|
||||
// === Processing ===
|
||||
|
||||
@@ -7,4 +8,6 @@ export interface MeetProcessingDomainInterface {
|
||||
hash: string;
|
||||
meet: MeetRowProcessingDomainInterface;
|
||||
questions: QuestionRowProcessingDomainInterface[];
|
||||
isVoted?: boolean;
|
||||
extendedStatus?: ExtendedMeetStatus;
|
||||
}
|
||||
|
||||
+8
-6
@@ -1,5 +1,5 @@
|
||||
import type { Cooperative, MeetContract } from 'cooptypes';
|
||||
import type { DocumentAggregateDomainInterface } from '~/domain/document/interfaces/document-domain-aggregate.interface';
|
||||
import type { MeetContract } from 'cooptypes';
|
||||
import type { ISignedDocumentDomainInterface } from '~/domain/document/interfaces/signed-document-domain.interface';
|
||||
import { assertType, type AssertKeysMatch } from '~/shared/asserts/blockchain-type.assert';
|
||||
|
||||
export interface MeetRowProcessingDomainInterface {
|
||||
@@ -7,6 +7,7 @@ export interface MeetRowProcessingDomainInterface {
|
||||
hash: string;
|
||||
coopname: string;
|
||||
type: string;
|
||||
level: string;
|
||||
initiator: string;
|
||||
presider: string;
|
||||
secretary: string;
|
||||
@@ -19,10 +20,11 @@ export interface MeetRowProcessingDomainInterface {
|
||||
current_quorum_percent: number;
|
||||
cycle: number;
|
||||
quorum_passed: boolean;
|
||||
proposal: DocumentAggregateDomainInterface;
|
||||
authorization?: DocumentAggregateDomainInterface;
|
||||
decision1?: DocumentAggregateDomainInterface;
|
||||
decision2?: DocumentAggregateDomainInterface;
|
||||
proposal: ISignedDocumentDomainInterface;
|
||||
authorization?: ISignedDocumentDomainInterface;
|
||||
decision1?: ISignedDocumentDomainInterface;
|
||||
decision2?: ISignedDocumentDomainInterface;
|
||||
notified_users: string[];
|
||||
}
|
||||
|
||||
assertType<AssertKeysMatch<MeetContract.Tables.Meets.IOutput, MeetRowProcessingDomainInterface>>();
|
||||
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import { type ISignedDocumentDomainInterface } from '~/domain/document/interfaces/signed-document-domain.interface';
|
||||
|
||||
export interface NotifyOnAnnualGeneralMeetInputDomainInterface {
|
||||
coopname: string;
|
||||
meet_hash: string;
|
||||
username: string;
|
||||
notification: ISignedDocumentDomainInterface;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user