Compare commits
64 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6e2c9a1bf8 | |||
| 137d1ca8a4 | |||
| 59744e9d23 | |||
| ac8836628e | |||
| 1f649c8951 | |||
| 310468fb69 | |||
| 7f782e0a92 | |||
| d0c9f8bffc | |||
| 60a35f6f76 | |||
| a3a2923751 | |||
| 32e6a45f05 | |||
| 59665c6df4 | |||
| d23b4e3aac | |||
| 20897cf199 | |||
| 0b57232606 | |||
| 1bd8f003f7 | |||
| fcb614cd90 | |||
| 1c376c003a | |||
| a433ceaf1d | |||
| 850478b7f7 | |||
| 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 |
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"agentCanUpdateSnapshot": true
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
ВАЖНО!!! Мы работаем в МОНО-репозитории pnpm, любая установка пакетов производится ЧЕРЕЗ ФИЛЬТР компонента в корне: pnpm add glob --filter notificator2
|
||||
@@ -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 }}'
|
||||
|
||||
Vendored
+8
-5
@@ -1,16 +1,19 @@
|
||||
{
|
||||
"editor.bracketPairColorization.enabled": true,
|
||||
"editor.guides.bracketPairs": true,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.tabCompletion": "onlySnippets",
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint":"explicit"
|
||||
},
|
||||
// "editor.formatOnSave": true,
|
||||
// "editor.codeActionsOnSave": {
|
||||
// "source.addMissingImports.ts": "explicit",
|
||||
// "source.fixAll.ts": "explicit",
|
||||
// "source.fixAll.eslint": "explicit"
|
||||
// },
|
||||
"eslint.run": "onType",
|
||||
"eslint.validate": ["javascript", "typescript", "vue"],
|
||||
"i18n-ally.localesPaths": ["src/i18n"],
|
||||
"typescript.format.enable": true,
|
||||
"typescript.format.indentSwitchCase": false,
|
||||
"typescript.validate.enable": true
|
||||
"typescript.validate.enable": true,
|
||||
"notebook.defaultFormatter": "Vue.volar"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,60 @@
|
||||
# v2025.6.14
|
||||
|
||||
Разработан модуль для проведения очередных общих собраний пайщиков. Исправлены баги, внесены улучшения пользовательского интерфейса.
|
||||
|
||||
✨ Новые функции
|
||||
- [#264](https://github.com/coopenomics/mono/issues/264): Разработан смарт-контракт общего собрания пайщиков (meet)
|
||||
- [#263](https://github.com/coopenomics/mono/issues/263): Реализован модуль оповещений на электронные почты по жизненному циклу общего собрания пайщиков
|
||||
- [#273](https://github.com/coopenomics/mono/issues/273): Встроены реальные шаблоны документов общего собрания
|
||||
- [#268](https://github.com/coopenomics/mono/issues/268): Добавлена страница результатов общего собрания
|
||||
- [#267](https://github.com/coopenomics/mono/issues/267): Добавлена страница просмотра и скачивания бюллетеней и уведомлений по собранию
|
||||
- [#270](https://github.com/coopenomics/mono/issues/270): Ссылка в оповещении ведет на страницу собрания с документом-уведомлением для подписи
|
||||
|
||||
🐛 Исправления ошибок
|
||||
- [#262](https://github.com/coopenomics/mono/issues/262): Исправлено некорректное открытие рабочего стола совета
|
||||
- [#261](https://github.com/coopenomics/mono/issues/261): Исправлена ошибка с редиректом при первой загрузке и прямом переходе на собрание
|
||||
- [#259](https://github.com/coopenomics/mono/issues/259): Исправлены отступы в мобильной карточке пайщика
|
||||
- [#258](https://github.com/coopenomics/mono/issues/258): Убран hover-эффект на документе и пайщике в таблице
|
||||
|
||||
🔧 Улучшения
|
||||
- [#274](https://github.com/coopenomics/mono/issues/274): Настроена рассылка оповещений на почту при получении решения о проведении собрания
|
||||
- [#272](https://github.com/coopenomics/mono/issues/272): Подписанные уведомления сохраняются и извлекаются из реестра по Graph-QL
|
||||
- [#271](https://github.com/coopenomics/mono/issues/271): Ведется подсчет количества пайщиков в каждом кооперативе при добавлении и удалении
|
||||
- [#260](https://github.com/coopenomics/mono/issues/260): Введен счетчик количества пайщиков в кооперативе на контракте регистратора
|
||||
- [#256](https://github.com/coopenomics/mono/issues/256): Отображается статус членства каждого пайщика
|
||||
- [#255](https://github.com/coopenomics/mono/issues/255): В разделе Платежи отображается ФИО/Наименование плательщика
|
||||
- [#269](https://github.com/coopenomics/mono/issues/269): Реализована рассылка уведомлений перед началом собрания
|
||||
- [#265](https://github.com/coopenomics/mono/issues/265): Проведена отладка и тестирование процесса общего собрания пайщиков
|
||||
- [#266](https://github.com/coopenomics/mono/issues/266): Протестированы все процессы общего собрания
|
||||
|
||||
#releases
|
||||
|
||||
---
|
||||
|
||||
# v2025.5.14
|
||||
|
||||
В этом релизе реализован новый стандарт передачи и хранения документов по блокчейну с поддержкой неограниченного количества подписей и их валидацией. Также внесены улучшения в интерфейс и исправлены ошибки.
|
||||
|
||||
✨ Новые функции
|
||||
- [#252](https://github.com/coopenomics/mono/issues/252): Внедрение обновленного стандарта хранения и передачи документов по блокчейну
|
||||
- [#251](https://github.com/coopenomics/mono/issues/251): Реализация версионированного мигратора данных для контроллера кооператива
|
||||
- [#244](https://github.com/coopenomics/mono/issues/244): Обновление стандарта сборки документов и переход на хэш-идентификаторы
|
||||
|
||||
🐛 Исправления ошибок
|
||||
- [#249](https://github.com/coopenomics/mono/issues/249): Исправлена спутанная маршрутизация между рабочими столами кооперативов
|
||||
- [#253](https://github.com/coopenomics/mono/issues/253): Исправлена избыточная точность суммы оплаты в заявлении на вступление
|
||||
|
||||
🔧 Улучшения
|
||||
- [#259](https://github.com/coopenomics/mono/issues/259): Исправлены отступы в мобильной карточке пайщика на странице пайщиков
|
||||
- [#258](https://github.com/coopenomics/mono/issues/258): Удалён hover-эффект для документов и пайщиков в таблице
|
||||
- [#257](https://github.com/coopenomics/mono/issues/257): Добавлено сохранение светлой/тёмной темы в localStorage и восстановление при загрузке страницы
|
||||
- [#256](https://github.com/coopenomics/mono/issues/256): Отображение статуса членства каждого пайщика в разделе "Пайщики"
|
||||
- [#255](https://github.com/coopenomics/mono/issues/255): Отображение ФИО/Наименования плательщика в разделе "Платежи"
|
||||
|
||||
#releases
|
||||
|
||||
---
|
||||
|
||||
# v2025.5.2
|
||||
|
||||
В этом релизе рабочие столы переведены на серверный рендеринг, что улучшает стабильность развертывания и упрощает автоматизацию поставки ПО. Данный релиз является подготовительным к переходу на новый стандарт цифровых документов на платформе.
|
||||
|
||||
@@ -0,0 +1,316 @@
|
||||
# 🚀 Notificator2 Web Push Service - Implementation Complete
|
||||
|
||||
## ✅ Что реализовано
|
||||
|
||||
Создан полноценный сервис для web-push уведомлений на базе NestJS с интеграцией NOVU, который полностью заменяет Firebase для push уведомлений и предоставляет все необходимые возможности для работы с NOVU.
|
||||
|
||||
## 📦 Архитектура компонентов
|
||||
|
||||
### База данных (PostgreSQL + TypeORM)
|
||||
- **Entity**: `PushSubscription` для хранения подписок пользователей
|
||||
- **Автоматические миграции** и управление схемой БД
|
||||
- **Индексы** для оптимизации запросов по userId и endpoint
|
||||
- **UUID** как первичный ключ для безопасности
|
||||
|
||||
### Сервисы
|
||||
|
||||
#### 🔐 WebPushService
|
||||
**Основной сервис для web-push функциональности:**
|
||||
- Автогенерация и управление VAPID ключами
|
||||
- CRUD операции с push подписками
|
||||
- Отправка уведомлений с полной поддержкой Web Push API
|
||||
- Автоматическая деактивация недействительных подписок
|
||||
- Статистика и мониторинг
|
||||
|
||||
#### 🔗 NovuWebPushService
|
||||
**Интеграция с NOVU:**
|
||||
- Создание NOVU workflows с поддержкой web-push
|
||||
- Обработка webhook событий от NOVU
|
||||
- Template processing с поддержкой переменных
|
||||
- Fallback отправка через web-push API
|
||||
|
||||
#### 🧹 CleanupService
|
||||
**Автоматическое обслуживание:**
|
||||
- Cron job для ежедневной очистки (2:00 AM)
|
||||
- Еженедельная статистика (воскресенье, 12:00)
|
||||
- Ручное управление очисткой через API
|
||||
|
||||
### Контроллеры
|
||||
|
||||
#### 📱 PushController (`/api/push/`)
|
||||
**Основной API для web-push:**
|
||||
```
|
||||
GET /vapid-public-key - Получение VAPID ключа
|
||||
POST /subscribe - Подписка на уведомления
|
||||
DELETE /unsubscribe - Отписка от уведомлений
|
||||
POST /send - Отправка уведомления
|
||||
GET /subscriptions/:userId - Подписки пользователя
|
||||
POST /test/:userId - Тестовое уведомление
|
||||
GET /stats - Статистика подписок
|
||||
DELETE /cleanup - Очистка старых подписок
|
||||
```
|
||||
|
||||
#### 🎯 NovuWebhookController (`/api/novu/`)
|
||||
**NOVU интеграция:**
|
||||
```
|
||||
POST /webhook/push - Webhook для NOVU событий
|
||||
POST /subscribe - Подписка через NOVU
|
||||
GET /integration/config - Конфигурация интеграции
|
||||
POST /test - Тест интеграции
|
||||
POST /workflow/create - Создание workflow
|
||||
```
|
||||
|
||||
## 🎯 Возможности
|
||||
|
||||
### Web Push Features
|
||||
- ✅ **Полная поддержка Web Push API**: title, body, icon, badge, image, actions
|
||||
- ✅ **VAPID аутентификация**: автогенерация ключей
|
||||
- ✅ **Rich notifications**: кнопки, изображения, custom data
|
||||
- ✅ **Множественные подписки** на пользователя
|
||||
- ✅ **Автоматическая очистка** недействительных подписок
|
||||
|
||||
### NOVU Integration
|
||||
- ✅ **Webhook интеграция**: автообработка NOVU событий
|
||||
- ✅ **Workflow создание**: программное создание workflows
|
||||
- ✅ **Template поддержка**: переменные в уведомлениях
|
||||
- ✅ **Dual отправка**: через NOVU + прямая отправка как fallback
|
||||
|
||||
### Мониторинг и обслуживание
|
||||
- ✅ **Автоматизация**: cron jobs для очистки и статистики
|
||||
- ✅ **Статистика**: активные/неактивные подписки, уникальные пользователи
|
||||
- ✅ **Логирование**: подробные логи всех операций
|
||||
- ✅ **Health checks**: проверка работоспособности системы
|
||||
|
||||
## 🛠️ Настройка и запуск
|
||||
|
||||
### 1. Переменные окружения (.env)
|
||||
```env
|
||||
# Приложение
|
||||
PORT=3000
|
||||
CORS_ORIGIN=*
|
||||
|
||||
# NOVU
|
||||
NOVU_API_KEY=your_novu_api_key
|
||||
NOVU_API_URL=https://api.novu.co
|
||||
|
||||
# VAPID (генерируются автоматически)
|
||||
VAPID_PUBLIC_KEY=
|
||||
VAPID_PRIVATE_KEY=
|
||||
VAPID_SUBJECT=mailto:admin@coopenomics.io
|
||||
|
||||
# PostgreSQL
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_USERNAME=postgres
|
||||
DB_PASSWORD=postgres
|
||||
DB_DATABASE=notificator
|
||||
DB_SYNCHRONIZE=true
|
||||
```
|
||||
|
||||
### 2. Запуск
|
||||
```bash
|
||||
# В корне проекта
|
||||
cd components/notificator2
|
||||
|
||||
# Установка зависимостей
|
||||
pnpm install
|
||||
|
||||
# Сборка
|
||||
pnpm build
|
||||
|
||||
# Запуск в production
|
||||
pnpm start:prod
|
||||
|
||||
# Или для разработки
|
||||
pnpm start:dev
|
||||
```
|
||||
|
||||
### 3. База данных
|
||||
```sql
|
||||
-- Создать БД в PostgreSQL
|
||||
CREATE DATABASE notificator;
|
||||
```
|
||||
|
||||
## 💻 Интеграция с фронтендом
|
||||
|
||||
### Service Worker (public/sw.js)
|
||||
```javascript
|
||||
self.addEventListener('push', function(event) {
|
||||
const payload = JSON.parse(event.data.text());
|
||||
const options = {
|
||||
body: payload.body,
|
||||
icon: payload.icon,
|
||||
badge: payload.badge,
|
||||
image: payload.image,
|
||||
actions: payload.actions,
|
||||
data: payload.data,
|
||||
tag: payload.tag,
|
||||
requireInteraction: payload.requireInteraction
|
||||
};
|
||||
|
||||
event.waitUntil(
|
||||
self.registration.showNotification(payload.title, options)
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('notificationclick', function(event) {
|
||||
event.notification.close();
|
||||
if (event.action === 'view' || !event.action) {
|
||||
const url = event.notification.data?.url || '/';
|
||||
event.waitUntil(clients.openWindow(url));
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Подписка пользователя
|
||||
```javascript
|
||||
async function subscribeToPush(userId) {
|
||||
// 1. Регистрация Service Worker
|
||||
const registration = await navigator.serviceWorker.register('/sw.js');
|
||||
|
||||
// 2. Получение VAPID ключа
|
||||
const { publicKey } = await fetch('/api/push/vapid-public-key')
|
||||
.then(r => r.json());
|
||||
|
||||
// 3. Создание подписки
|
||||
const subscription = await registration.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: publicKey
|
||||
});
|
||||
|
||||
// 4. Отправка на сервер
|
||||
await fetch('/api/push/subscribe', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ userId, subscription })
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Отправка уведомления
|
||||
```javascript
|
||||
// Через прямой API
|
||||
await fetch('/api/push/send', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
userId: 'user123',
|
||||
title: 'Новое сообщение',
|
||||
body: 'У вас есть новое сообщение',
|
||||
icon: '/icons/message.png',
|
||||
url: '/messages',
|
||||
actions: [
|
||||
{ action: 'view', title: 'Открыть' },
|
||||
{ action: 'dismiss', title: 'Закрыть' }
|
||||
]
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
## 🔗 Интеграция с NOVU
|
||||
|
||||
### 1. Настройка webhook в NOVU
|
||||
- URL: `https://your-domain.com/api/novu/webhook/push`
|
||||
- События: `workflow.step.push.sent`
|
||||
|
||||
### 2. Создание workflow с push поддержкой
|
||||
```javascript
|
||||
// Через API сервиса
|
||||
await fetch('/api/novu/workflow/create', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
workflowId: 'user-notifications',
|
||||
workflowName: 'Уведомления пользователей'
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Триггер через NOVU SDK
|
||||
```javascript
|
||||
import { Novu } from '@novu/node';
|
||||
const novu = new Novu(process.env.NOVU_API_KEY);
|
||||
|
||||
await novu.trigger('user-notifications', {
|
||||
to: { subscriberId: 'user123' },
|
||||
payload: {
|
||||
title: 'Новое уведомление',
|
||||
body: 'Описание уведомления',
|
||||
icon: '/icons/notification.png',
|
||||
url: '/dashboard'
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## 📊 Мониторинг и API
|
||||
|
||||
### Получение статистики
|
||||
```bash
|
||||
curl http://localhost:3000/api/push/stats
|
||||
```
|
||||
|
||||
**Ответ:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"total": 150,
|
||||
"active": 120,
|
||||
"inactive": 30,
|
||||
"uniqueUsers": 85
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Тестирование
|
||||
```bash
|
||||
# Отправить тестовое уведомление
|
||||
curl -X POST http://localhost:3000/api/push/test/user123
|
||||
|
||||
# Проверить NOVU интеграцию
|
||||
curl -X POST http://localhost:3000/api/novu/test \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"userId": "user123"}'
|
||||
```
|
||||
|
||||
## 🔒 Безопасность
|
||||
|
||||
- ✅ **VAPID аутентификация** для всех push уведомлений
|
||||
- ✅ **Валидация данных** на всех endpoints
|
||||
- ✅ **CORS настройки** для безопасного API доступа
|
||||
- ✅ **Автоматическая очистка** недействительных подписок
|
||||
- ✅ **Webhook подпись валидация** (готово к настройке для production)
|
||||
|
||||
## ⚡ Производительность
|
||||
|
||||
- ✅ **Индексы БД** для быстрых запросов
|
||||
- ✅ **Batch отправка** уведомлений
|
||||
- ✅ **Connection pooling** для PostgreSQL
|
||||
- ✅ **Graceful error handling** с retry логикой
|
||||
- ✅ **Автоматическая очистка** для поддержания производительности
|
||||
|
||||
## 🎉 Результат
|
||||
|
||||
**Полностью функциональный web-push сервис готов к production использованию!**
|
||||
|
||||
### Для разработчиков:
|
||||
- 📚 Простое REST API с TypeScript типизацией
|
||||
- 🧪 Тестовые скрипты и подробная документация
|
||||
- 🔧 Гибкая настройка и конфигурация
|
||||
|
||||
### Для пользователей:
|
||||
- 📱 Кроссплатформенные push уведомления
|
||||
- 🎨 Rich notifications с кнопками и изображениями
|
||||
- ⚡ Быстрая и надежная доставка
|
||||
|
||||
### Для администраторов:
|
||||
- 📊 Автоматическое обслуживание и статистика
|
||||
- 🔍 Подробные логи и мониторинг
|
||||
- ⚙️ Масштабируемая архитектура
|
||||
|
||||
### Для NOVU интеграции:
|
||||
- 🔗 Полная интеграция с workflow системой
|
||||
- 📨 Webhook поддержка для автоматической отправки
|
||||
- 🔄 Fallback механизмы для надежности
|
||||
|
||||
**Сервис полностью заменяет Firebase для push уведомлений и предоставляет все необходимые возможности для интеграции с NOVU!** 🚀
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
# 📋 Резюме создания компонентов Notifications
|
||||
|
||||
## ✅ Что создано
|
||||
|
||||
### 1. 📚 Библиотека `@monocoop/notifications`
|
||||
**Расположение:** `components/notifications/`
|
||||
|
||||
**Функции:**
|
||||
- ✅ Типизированные интерфейсы для workflow с Zod
|
||||
- ✅ Builder паттерн для создания workflow
|
||||
- ✅ Базовые шаблоны для email, in-app, push, SMS
|
||||
- ✅ Автоматическая конвертация Zod схем в JSON Schema для Novu
|
||||
- ✅ Экспорт всех workflow для использования в других пакетах
|
||||
|
||||
**Структура:**
|
||||
```
|
||||
src/
|
||||
├── types/ # Базовые типы и интерфейсы
|
||||
├── base/ # Утилиты и настройки по умолчанию
|
||||
├── workflows/ # Папки с workflow
|
||||
│ └── welcome/ # Пример приветственного workflow
|
||||
└── index.ts # Главный экспорт
|
||||
```
|
||||
|
||||
### 2. 🚀 NestJS приложение `@monocoop/notificator2`
|
||||
**Расположение:** `components/notificator2/`
|
||||
|
||||
**Функции:**
|
||||
- ✅ Автоматический upsert всех workflow в Novu при запуске
|
||||
- ✅ RESTful API для триггера уведомлений
|
||||
- ✅ Типизированная валидация payload
|
||||
- ✅ Health check endpoints
|
||||
- ✅ Использует библиотеку notifications для типов
|
||||
|
||||
**API Endpoints:**
|
||||
- `GET /api/notifications/health` - Health check
|
||||
- `GET /api/notifications/workflows` - Список workflow
|
||||
- `POST /api/notifications/trigger` - Универсальный триггер
|
||||
- `POST /api/notifications/trigger/welcome` - Триггер welcome workflow
|
||||
- `POST /api/notifications/workflows/upsert-all` - Принудительный upsert
|
||||
|
||||
## 🔧 Как использовать
|
||||
|
||||
### 1. Настройка библиотеки notifications
|
||||
```bash
|
||||
cd components/notifications
|
||||
pnpm install # или npm install
|
||||
pnpm build # для компиляции TypeScript
|
||||
```
|
||||
|
||||
### 2. Настройка notificator2
|
||||
```bash
|
||||
cd components/notificator2
|
||||
pnpm install # или npm install
|
||||
|
||||
# Настройка окружения
|
||||
cp .env.example .env
|
||||
# Добавить NOVU_API_KEY в .env
|
||||
|
||||
# Запуск
|
||||
pnpm start:dev
|
||||
```
|
||||
|
||||
### 3. Добавление нового workflow
|
||||
|
||||
**В библиотеке notifications:**
|
||||
```typescript
|
||||
// components/notifications/src/workflows/order/order-workflow.ts
|
||||
export const orderWorkflow = WorkflowBuilder
|
||||
.create<OrderPayload>()
|
||||
.name('Order Confirmation')
|
||||
.workflowId('order-confirmation')
|
||||
.payloadSchema(orderPayloadSchema)
|
||||
.addSteps([...])
|
||||
.build();
|
||||
```
|
||||
|
||||
**Регистрация в index.ts:**
|
||||
```typescript
|
||||
// components/notifications/src/workflows/index.ts
|
||||
import { orderWorkflow } from './order';
|
||||
|
||||
export const allWorkflows = [
|
||||
welcomeWorkflow,
|
||||
orderWorkflow, // ← добавить новый workflow
|
||||
];
|
||||
```
|
||||
|
||||
### 4. Использование API
|
||||
|
||||
**Триггер welcome workflow:**
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/notifications/trigger/welcome \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"subscriberId": "user-123",
|
||||
"email": "user@example.com",
|
||||
"payload": {
|
||||
"userName": "Иван Иванов",
|
||||
"userEmail": "user@example.com",
|
||||
"age": 25
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
## 🎯 Преимущества архитектуры
|
||||
|
||||
1. **Типобезопасность** - Zod схемы обеспечивают валидацию на уровне TypeScript и runtime
|
||||
2. **Разделение ответственности** - Библиотека содержит только типы, сервер - логику
|
||||
3. **Расширяемость** - Легко добавлять новые workflow
|
||||
4. **Автоматизация** - Workflow автоматически синхронизируются с Novu
|
||||
5. **Переиспользование** - Типы можно использовать в других частях системы
|
||||
|
||||
## 🔄 Workflow при запуске
|
||||
|
||||
1. **Запуск notificator2** →
|
||||
2. **Чтение всех workflow из библиотеки** →
|
||||
3. **Проверка существования в Novu** →
|
||||
4. **Создание/обновление workflow** →
|
||||
5. **Готов к приёму запросов на триггеры**
|
||||
|
||||
## 📁 Структура как в testFramework2.ts
|
||||
|
||||
Вся логика из `testFramework2.ts` была перенесена в structured архитектуру:
|
||||
- ✅ `buildWorkflowData` → `WorkflowBuilder`
|
||||
- ✅ `baseSteps` → `createEmailStep`, `createInAppStep`, etc.
|
||||
- ✅ `upsertWorkflow` → `NovuService.upsertWorkflow`
|
||||
- ✅ `triggerWorkflow` → `NovuService.triggerWorkflow`
|
||||
- ✅ Типизация payload → Zod схемы + TypeScript типы
|
||||
Vendored
BIN
Binary file not shown.
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@coopenomics/boot",
|
||||
"type": "module",
|
||||
"version": "2025.5.5-2",
|
||||
"version": "2025.7.1",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@9.0.6",
|
||||
"description": "",
|
||||
|
||||
@@ -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.7.1",
|
||||
"private": true,
|
||||
"description": "",
|
||||
"scripts": {
|
||||
|
||||
@@ -14,6 +14,12 @@ void gateway::createoutpay(eosio::name coopname, eosio::name username, checksum2
|
||||
eosio::check(confirm_callback != ""_n, "Действие для коллбэка-успеха должено быть установлено");
|
||||
eosio::check(decline_callback != ""_n, "Действие для коллбэка-отлонения должно быть установлено");
|
||||
|
||||
// Валидируем callback действия если они от wallet контракта
|
||||
if (callback_contract == _wallet) {
|
||||
Wallet::get_valid_wallet_action(confirm_callback);
|
||||
Wallet::get_valid_wallet_action(decline_callback);
|
||||
}
|
||||
|
||||
auto outcome = Gateway::get_outcome(coopname, outcome_hash);
|
||||
|
||||
eosio::check(!outcome.has_value(), "Объект возврата уже существует с указанным хэшем");
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
void gateway::outcomplete(eosio::name coopname, checksum256 outcome_hash){
|
||||
require_auth(coopname);
|
||||
|
||||
auto exist_outcome = Gateway::get_outcome(coopname, outcome_hash);
|
||||
eosio::check(exist_outcome.has_value(), "Объект возврата не существует с указанным хэшем");
|
||||
|
||||
@@ -9,13 +8,15 @@ void gateway::outcomplete(eosio::name coopname, checksum256 outcome_hash){
|
||||
auto outcome = outcomes.find(exist_outcome -> id);
|
||||
|
||||
eosio::check(outcome -> status == "pending"_n, "Только принятые заявления на вывод могут быть обработаны");
|
||||
|
||||
action(
|
||||
permission_level{ _gateway, "active"_n},
|
||||
|
||||
// Используем интерфейс для типизированного вызова callback действия
|
||||
Action::send<completewthd_interface>(
|
||||
outcome -> callback_contract,
|
||||
outcome -> confirm_callback,
|
||||
std::make_tuple(coopname, outcome -> outcome_hash)
|
||||
).send();
|
||||
_gateway,
|
||||
coopname,
|
||||
outcome -> outcome_hash
|
||||
);
|
||||
|
||||
outcomes.erase(outcome);
|
||||
};
|
||||
|
||||
@@ -2,19 +2,22 @@ void gateway::outdecline(eosio::name coopname, checksum256 outcome_hash, std::st
|
||||
require_auth(coopname);
|
||||
|
||||
auto exist_outcome = Gateway::get_outcome(coopname, outcome_hash);
|
||||
eosio::check(!exist_outcome.has_value(), "Объект возврата уже существует с указанным хэшем");
|
||||
eosio::check(exist_outcome.has_value(), "Объект возврата не существует с указанным хэшем");
|
||||
Gateway::outcomes_index outcomes(_gateway, coopname.value);
|
||||
|
||||
auto outcome = outcomes.find(exist_outcome -> id);
|
||||
|
||||
eosio::check(outcome -> status == "pending"_n, "Только принятые заявления на вывод могут быть обработаны");
|
||||
|
||||
action(
|
||||
permission_level{ _gateway, "active"_n},
|
||||
outcome -> callback_contract,
|
||||
outcome -> decline_callback,
|
||||
std::make_tuple(coopname, outcome -> outcome_hash, reason)
|
||||
).send();
|
||||
// Используем интерфейс для типизированного вызова callback действия
|
||||
Action::send<declinewthd_interface>(
|
||||
outcome -> callback_contract,
|
||||
outcome -> decline_callback,
|
||||
_gateway,
|
||||
coopname,
|
||||
outcome -> outcome_hash,
|
||||
reason
|
||||
);
|
||||
|
||||
outcomes.erase(outcome);
|
||||
};
|
||||
|
||||
@@ -312,6 +312,7 @@ struct [[eosio::table, eosio::contract(REGISTRATOR)]] cooperative2 {
|
||||
eosio::binary_extension<eosio::name> status; ///< Статус процесса подключения
|
||||
eosio::binary_extension<eosio::time_point_sec> created_at; ///< Дата поступления заявки на подключение (pending | ... | active | blocked)
|
||||
eosio::binary_extension<document2> document; ///< Подписанный документ соглашения на подключение
|
||||
eosio::binary_extension<uint64_t> active_participants_count; ///< Счетчик активных пайщиков в кооперативе
|
||||
|
||||
|
||||
/**
|
||||
|
||||
@@ -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>>,
|
||||
|
||||
@@ -21,5 +21,6 @@
|
||||
#include "shared_approver.hpp"
|
||||
#include "shared_registrator.hpp"
|
||||
#include "shared_loan.hpp"
|
||||
#include "shared_branch.hpp"
|
||||
#include "approves.hpp"
|
||||
#include "debts.hpp"
|
||||
@@ -104,7 +104,8 @@ static const std::set<eosio::name> soviet_actions = {
|
||||
//MEET
|
||||
"creategm"_n,//предложение повестки планового общего собрание
|
||||
"completegm"_n, //решение общего собрания пайщиков
|
||||
|
||||
"ballot"_n, //бюллетень участника общего собрания пайщиков
|
||||
"gmnotify"_n, //уведомление участника общего собрания пайщиков
|
||||
//CAPITAL
|
||||
"capitalinvst"_n, //заявление на инвестиции по договору УХД
|
||||
"createresult"_n, //клайм прироста капитализации из задания
|
||||
@@ -113,17 +114,11 @@ static const std::set<eosio::name> soviet_actions = {
|
||||
"capwthdrprog"_n, //произвести возврат накопленных членских взносов по программе на капиталиста
|
||||
"capwthdrproj"_n, //произвести возврат накопленных членских взносов по проекту на актора
|
||||
"capwthdrres"_n, //произвести возврат из задания
|
||||
};
|
||||
|
||||
static const std::set<eosio::name> gateway_income_actions = {
|
||||
"deposit"_n, //паевой взнос по ЦПП Кошелёк
|
||||
//WALLET
|
||||
"createwthd"_n, //создать заявление на возврат паевого взноса
|
||||
};
|
||||
|
||||
static const std::set<eosio::name> gateway_outcome_actions = {
|
||||
"withdraw"_n, //возврат паевого взноса по ЦПП Кошелёк
|
||||
};
|
||||
|
||||
|
||||
//program_ids
|
||||
static constexpr uint64_t _wallet_program_id = 1;
|
||||
static constexpr uint64_t _sosedi_program_id = 2;
|
||||
|
||||
@@ -42,13 +42,27 @@ struct document2 {
|
||||
std::vector<signature_info> signatures;
|
||||
};
|
||||
|
||||
void verify_document_or_fail(const document2 &doc)
|
||||
{
|
||||
void verify_document_or_fail(
|
||||
const document2 &doc,
|
||||
const std::vector<eosio::name>& required_signers = {}
|
||||
) {
|
||||
for (const auto &sig : doc.signatures) {
|
||||
// Проверка завершится прерыванием, если восстановление подписи провалится
|
||||
assert_recover_key(sig.signed_hash, sig.signature, sig.public_key);
|
||||
}
|
||||
};
|
||||
if (!required_signers.empty()) {
|
||||
for (const auto& required : required_signers) {
|
||||
bool found = false;
|
||||
for (const auto& sig : doc.signatures) {
|
||||
if (sig.signer == required) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
eosio::check(found, ("Не найдена подпись от обязательного подписанта: " + required.to_string()).c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool is_empty_document(const document2 &doc)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
#pragma once
|
||||
|
||||
namespace Branch {
|
||||
/**
|
||||
* @brief Получает количество кооперативных участков для данного кооператива
|
||||
* @param coopname Имя кооператива
|
||||
* @return Количество участков кооператива
|
||||
*/
|
||||
uint64_t get_branch_count(eosio::name coopname) {
|
||||
branchstat_index stat(_branch, _branch.value);
|
||||
auto st = stat.find(coopname.value);
|
||||
|
||||
if (st == stat.end()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return st->count;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Проверяет, является ли пользователь уполномоченным лицом (trustee) кооперативного участка
|
||||
* @param coopname Имя кооператива
|
||||
* @param username Имя пользователя для проверки
|
||||
* @return true если пользователь является уполномоченным, false в противном случае
|
||||
*/
|
||||
bool is_trustee(eosio::name coopname, eosio::name username) {
|
||||
branch_index branches(_branch, coopname.value);
|
||||
|
||||
auto by_trustee_index = branches.get_index<"bytrustee"_n>();
|
||||
auto trustee_itr = by_trustee_index.find(username.value);
|
||||
|
||||
return trustee_itr != by_trustee_index.end();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Проверяет, является ли пользователь доверенным лицом (trusted) в указанном участке кооператива
|
||||
* @param coopname Имя кооператива
|
||||
* @param braname Имя участка
|
||||
* @param username Имя пользователя для проверки
|
||||
* @return true если пользователь является доверенным, false в противном случае
|
||||
*/
|
||||
bool is_trusted(eosio::name coopname, eosio::name braname, eosio::name username) {
|
||||
branch_index branches(_branch, coopname.value);
|
||||
auto branch_itr = branches.find(braname.value);
|
||||
|
||||
if (branch_itr == branches.end()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return branch_itr->is_account_in_trusted(username);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,6 +3,16 @@
|
||||
using createoutpay_interface = void(CREATEOUTPAY_SIGNATURE);
|
||||
|
||||
namespace Gateway {
|
||||
|
||||
static const std::set<eosio::name> gateway_income_actions = {
|
||||
"deposit"_n, //паевой взнос по ЦПП Кошелёк
|
||||
};
|
||||
|
||||
static const std::set<eosio::name> gateway_outcome_actions = {
|
||||
"withdraw"_n, //возврат паевого взноса по ЦПП Кошелёк
|
||||
};
|
||||
|
||||
|
||||
inline eosio::name get_valid_income_action(const eosio::name& action) {
|
||||
eosio::check(gateway_income_actions.contains(action), "Недопустимое имя действия");
|
||||
return action;
|
||||
|
||||
@@ -51,4 +51,23 @@ namespace Registrator {
|
||||
|
||||
return *itr;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Получает количество активных пайщиков кооператива
|
||||
* @param coopname Имя кооператива
|
||||
* @return uint64_t Количество активных пайщиков
|
||||
*/
|
||||
inline uint64_t get_active_participants_count(eosio::name coopname) {
|
||||
using namespace eosio;
|
||||
|
||||
// Получаем информацию о кооперативе
|
||||
cooperatives2_index cooperatives(_registrator, _registrator.value);
|
||||
auto coop_itr = cooperatives.find(coopname.value);
|
||||
|
||||
if (coop_itr == cooperatives.end() || !coop_itr->is_cooperative || !coop_itr->active_participants_count.has_value()) {
|
||||
eosio::check(false, "Счетчик пайщиков кооператива не найден");
|
||||
}
|
||||
|
||||
return coop_itr->active_participants_count.value();
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,9 @@ using newdeclined_interface = void(NEWDECLINED_SIGNATURE);
|
||||
#define NEWACT_SIGNATURE name coopname, name username, name action, checksum256 package, document2 document
|
||||
using newact_interface = void(NEWACT_SIGNATURE);
|
||||
|
||||
#define NEWLINK_SIGNATURE name coopname, name username, name action, checksum256 package, document2 document
|
||||
using newlink_interface = void(NEWLINK_SIGNATURE);
|
||||
|
||||
#define NEWAGREEMENT_SIGNATURE name coopname, name username, name type, document2 document
|
||||
using newagreement_interface = void(NEWAGREEMENT_SIGNATURE);
|
||||
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
#define COMPLETEWTHD_SIGNATURE name coopname, checksum256 withdraw_hash
|
||||
#define DECLINEWTHD_SIGNATURE name coopname, checksum256 withdraw_hash, std::string reason
|
||||
|
||||
using completewthd_interface = void(COMPLETEWTHD_SIGNATURE);
|
||||
using declinewthd_interface = void(DECLINEWTHD_SIGNATURE);
|
||||
|
||||
namespace Wallet {
|
||||
|
||||
static const std::set<eosio::name> wallet_callback_actions = {
|
||||
"authwthd"_n, // авторизация возврата паевого взноса
|
||||
"declinewthd"_n, // отклонение возврата паевого взноса
|
||||
"completewthd"_n, // завершение возврата паевого взноса
|
||||
};
|
||||
|
||||
inline eosio::name get_valid_wallet_action(const eosio::name& action) {
|
||||
eosio::check(wallet_callback_actions.contains(action), "Недопустимое имя действия wallet");
|
||||
return action;
|
||||
}
|
||||
|
||||
inline void validate_asset(const eosio::asset& amount) {
|
||||
check(amount.symbol == _root_govern_symbol, "Invalid token symbol");
|
||||
check(amount.is_valid(), "Invalid asset");
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
#define NEWGDECISION_SIGNATURE \
|
||||
name coopname, /* Имя кооператива */ \
|
||||
name presider, /* Имя председателя */ \
|
||||
name secretary, /* Имя секретаря */ \
|
||||
checksum256 hash, /* Хэш собрания */ \
|
||||
std::vector<question_result> results, /* Итог по вопросам */ \
|
||||
uint64_t signed_ballots, /* Число подписанных бюллетеней */ \
|
||||
uint64_t quorum_percent, /* Процент кворума */ \
|
||||
bool quorum_passed /* Флаг достигнутого кворума */
|
||||
double quorum_percent, /* Процент кворума */ \
|
||||
bool quorum_passed, /* Флаг достигнутого кворума */ \
|
||||
document2 decision /* Протокол общего собрания */
|
||||
|
||||
using newgdecision_interface = void(NEWGDECISION_SIGNATURE);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Например, во внешем header можно объявить структуру:
|
||||
struct question_result {
|
||||
uint64_t question_id;
|
||||
uint64_t number;
|
||||
std::string title;
|
||||
std::string decision;
|
||||
std::string context;
|
||||
|
||||
@@ -8,7 +8,8 @@ struct [[eosio::table, eosio::contract(MEET)]] meet {
|
||||
uint64_t id; /// Идентификатор собрания.
|
||||
checksum256 hash; /// Внешний хэш-идентификатор собрания.
|
||||
name coopname; ///< Имя кооператива.
|
||||
name type; ///< Тип общего собрания (annual | special)
|
||||
name type; ///< Тип общего собрания (regular | extra)
|
||||
name level; ///< Уровень общего собрания (cooperative | branch)
|
||||
name initiator; ///< Имя пользователя, который инициировал собрание.
|
||||
name presider; ///< Имя аккаунта председателя собрания.
|
||||
name secretary; ///< Имя аккаунта секретаря собрания
|
||||
@@ -18,12 +19,13 @@ struct [[eosio::table, eosio::contract(MEET)]] meet {
|
||||
time_point_sec created_at; ///< Дата создания предложения.
|
||||
time_point_sec open_at; ///< Дата начала собрания.
|
||||
time_point_sec close_at; ///< Дата завершения собрания.
|
||||
std::vector<name> notified_users; ///< Пользователи, которые подписали уведомление
|
||||
|
||||
uint64_t quorum_percent = 75; /// Цель по кворуму, чтобы собрание считалось состоявшимся
|
||||
double quorum_percent = 75.0; /// Цель по кворуму, чтобы собрание считалось состоявшимся
|
||||
uint64_t signed_ballots; /// Общее количество подписанных бюллетеней
|
||||
uint64_t current_quorum_percent; /// Текущий процент кворума
|
||||
double current_quorum_percent = 0; /// Текущий процент кворума
|
||||
uint64_t cycle = 1; /// Цикл общего собрания, который указывает на то, что были предыдущие собрания, которые не состоялись по кворуму
|
||||
bool quorum_passed; /// Флаг пройденного кворума
|
||||
bool quorum_passed = false; /// Флаг пройденного кворума
|
||||
|
||||
document2 proposal; ///< Предложение
|
||||
document2 authorization; ///< Решение совета
|
||||
|
||||
@@ -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,15 @@
|
||||
// Заглушка для получения числа пайщиков кооператива.
|
||||
uint64_t get_total_participants(eosio::name coopname) {
|
||||
// Здесь можно реализовать получение актуальных данных, например, через inline action или вызов другого контракта.
|
||||
return 2; // Пример: 100 пайщиков
|
||||
// Получаем кооператив и проверяем режим участков
|
||||
auto cooperative = get_cooperative_or_fail(coopname);
|
||||
|
||||
// Если включен режим кооперативных участков, используем количество участков
|
||||
if (cooperative.is_branched) {
|
||||
return Branch::get_branch_count(coopname);
|
||||
}
|
||||
|
||||
// Иначе используем обычный подсчет активных пайщиков
|
||||
return Registrator::get_active_participants_count(coopname);
|
||||
}
|
||||
|
||||
void meet::vote(name coopname, checksum256 hash, name username, document2 ballot, std::vector<vote_point> votes) {
|
||||
@@ -14,6 +22,14 @@ void meet::vote(name coopname, checksum256 hash, name username, document2 ballot
|
||||
eosio::check(meet_opt.has_value(), "Собрание не найдено");
|
||||
auto meet_record = meet_opt.value();
|
||||
|
||||
// Получаем кооператив для проверки режима участков
|
||||
auto cooperative = get_cooperative_or_fail(coopname);
|
||||
|
||||
// Если включен режим кооперативных участков, проверяем что голосующий является уполномоченным
|
||||
if (cooperative.is_branched) {
|
||||
eosio::check(Branch::is_trustee(coopname, username), "Только уполномоченные кооперативных участков могут голосовать при режиме участков");
|
||||
}
|
||||
|
||||
// Проверяем статус собрания и временное окно голосования
|
||||
eosio::check(meet_record.status == "authorized"_n, "Собрание не авторизовано для голосования");
|
||||
auto now = current_time_point();
|
||||
@@ -83,7 +99,7 @@ void meet::vote(name coopname, checksum256 hash, name username, document2 ballot
|
||||
genmeets.modify(meet_itr, coopname, [&](auto &m) {
|
||||
m.signed_ballots++; // регистрируем принятый бюллетень
|
||||
uint64_t total_participants = get_total_participants(coopname);
|
||||
m.current_quorum_percent = (m.signed_ballots * 100) / total_participants;
|
||||
m.current_quorum_percent = (static_cast<double>(m.signed_ballots) * 100.0) / static_cast<double>(total_participants);
|
||||
if (m.current_quorum_percent > m.quorum_percent) {
|
||||
m.quorum_passed = true;
|
||||
}
|
||||
@@ -100,4 +116,15 @@ void meet::vote(name coopname, checksum256 hash, name username, document2 ballot
|
||||
// Здесь можно при необходимости обновить поле решения, логирование или вызвать inline action.
|
||||
}
|
||||
}
|
||||
|
||||
Action::send<newlink_interface>(
|
||||
_soviet,
|
||||
"newlink"_n,
|
||||
_meet,
|
||||
coopname,
|
||||
username,
|
||||
get_valid_soviet_action("ballot"_n),
|
||||
hash,
|
||||
ballot
|
||||
);
|
||||
}
|
||||
@@ -21,5 +21,6 @@
|
||||
#include "src/coop/enabranches.cpp"
|
||||
#include "src/coop/disbranches.cpp"
|
||||
#include "src/coop/updatecoop.cpp"
|
||||
#include "src/coop/decparticpnt.cpp"
|
||||
|
||||
#include "src/verification/verificate.cpp"
|
||||
|
||||
@@ -67,5 +67,8 @@ public:
|
||||
|
||||
[[eosio::action]] void enabranches(eosio::name coopname);
|
||||
[[eosio::action]] void disbranches(eosio::name coopname);
|
||||
|
||||
// Уменьшение счетчика активных пайщиков при блокировке
|
||||
[[eosio::action]] void decparticpnt(eosio::name coopname, eosio::name username);
|
||||
|
||||
};
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* @brief Уменьшает счетчик активных пайщиков кооператива
|
||||
* @param coopname Имя кооператива
|
||||
* @param username Имя пайщика
|
||||
*/
|
||||
void registrator::decparticpnt(eosio::name coopname, eosio::name username)
|
||||
{
|
||||
// Проверка авторизации - может вызывать только контракт soviet
|
||||
require_auth(_soviet);
|
||||
|
||||
// Получаем информацию о кооперативе
|
||||
cooperatives2_index cooperatives(_registrator, _registrator.value);
|
||||
auto coop_itr = cooperatives.find(coopname.value);
|
||||
|
||||
if (coop_itr != cooperatives.end() && coop_itr->is_cooperative) {
|
||||
if (coop_itr->active_participants_count.has_value() && coop_itr->active_participants_count.value() > 0) {
|
||||
// Уменьшаем счетчик только если он больше нуля
|
||||
cooperatives.modify(coop_itr, _registrator, [&](auto &coop) {
|
||||
coop.active_participants_count = coop.active_participants_count.value() - 1;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,80 +1,35 @@
|
||||
[[eosio::action]] void registrator::migrate() {
|
||||
require_auth(_registrator);
|
||||
// Registrator::candidates_legacy_index candidates_legacy(_registrator, _provider.value);
|
||||
// auto candidate_legacy_itr = candidates_legacy.begin();
|
||||
// while (candidate_legacy_itr != candidates_legacy.end()) {
|
||||
// candidate_legacy_itr = candidates_legacy.erase(candidate_legacy_itr);
|
||||
// }
|
||||
|
||||
|
||||
|
||||
// Новый код: удаление всех записей из cooperatives2_index
|
||||
// cooperatives2_index coops2(_registrator, _registrator.value);
|
||||
// auto coop2_itr = coops2.begin();
|
||||
// while (coop2_itr != coops2.end()) {
|
||||
// coop2_itr = coops2.erase(coop2_itr);
|
||||
// }
|
||||
|
||||
cooperatives_index coops(_registrator, _registrator.value);
|
||||
// Миграция cooperative -> cooperative2
|
||||
// Получаем таблицу кооперативов
|
||||
cooperatives2_index coops2(_registrator, _registrator.value);
|
||||
|
||||
for (auto coop_itr = coops.begin(); coop_itr != coops.end(); ++coop_itr) {
|
||||
// Проверяем, что запись еще не существует в cooperatives2
|
||||
if (coops2.find(coop_itr->username.value) == coops2.end()) {
|
||||
// Создаем новую запись в cooperative2
|
||||
coops2.emplace(_registrator, [&](auto& new_coop) {
|
||||
new_coop.username = coop_itr->username;
|
||||
new_coop.parent_username = coop_itr->parent_username;
|
||||
new_coop.announce = coop_itr->announce;
|
||||
new_coop.description = coop_itr->description;
|
||||
new_coop.is_cooperative = coop_itr->is_cooperative;
|
||||
new_coop.is_branched = coop_itr->is_branched;
|
||||
new_coop.is_enrolled = coop_itr->is_enrolled;
|
||||
new_coop.coop_type = coop_itr->coop_type;
|
||||
new_coop.registration = coop_itr->registration;
|
||||
new_coop.initial = coop_itr->initial;
|
||||
new_coop.minimum = coop_itr->minimum;
|
||||
|
||||
if (coop_itr->org_registration.has_value())
|
||||
new_coop.org_registration = coop_itr->org_registration;
|
||||
if (coop_itr->org_initial.has_value())
|
||||
new_coop.org_initial = coop_itr->org_initial;
|
||||
if (coop_itr->org_minimum.has_value())
|
||||
new_coop.org_minimum = coop_itr->org_minimum;
|
||||
if (coop_itr->status.has_value())
|
||||
new_coop.status = coop_itr->status;
|
||||
if (coop_itr->created_at.has_value())
|
||||
new_coop.created_at = coop_itr->created_at;
|
||||
|
||||
// Если есть документ в кооперативе, преобразуем его в document2
|
||||
if (coop_itr->document.has_value()) {
|
||||
document2 new_doc;
|
||||
new_doc.version = "0";
|
||||
new_doc.hash = coop_itr->document.value().hash;
|
||||
new_doc.doc_hash = coop_itr->document.value().hash;
|
||||
new_doc.meta_hash = eosio::checksum256();
|
||||
new_doc.meta = coop_itr->document.value().meta;
|
||||
|
||||
// Добавляем информацию о подписи
|
||||
signature_info sig;
|
||||
sig.id = 0;
|
||||
sig.signer = coop_itr->username;
|
||||
sig.public_key = coop_itr->document.value().public_key;
|
||||
sig.signature = coop_itr->document.value().signature;
|
||||
sig.signed_hash = coop_itr->document.value().hash;
|
||||
// Используем created_at как время подписания, если доступно
|
||||
if (coop_itr->created_at.has_value()) {
|
||||
sig.signed_at = coop_itr->created_at.value();
|
||||
} else {
|
||||
// В противном случае используем текущее время
|
||||
sig.signed_at = eosio::time_point_sec(eosio::current_time_point());
|
||||
}
|
||||
|
||||
new_doc.signatures.push_back(sig);
|
||||
new_coop.document = new_doc;
|
||||
}
|
||||
});
|
||||
// Для каждого кооператива проверяем наличие счетчика активных пайщиков
|
||||
for (auto coop_itr = coops2.begin(); coop_itr != coops2.end(); ++coop_itr) {
|
||||
// Если счетчик активных пайщиков уже установлен, пропускаем кооператив
|
||||
if (coop_itr->active_participants_count.has_value()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Получаем имя кооператива для использования как scope
|
||||
eosio::name coopname = coop_itr->username;
|
||||
|
||||
// Получаем таблицу участников из контракта soviet с scope кооператива
|
||||
participants_index participants(_soviet, coopname.value);
|
||||
|
||||
// Счетчик активных пайщиков
|
||||
uint64_t active_count = 0;
|
||||
|
||||
// Проходим по всем участникам и считаем активных (has_vote == true)
|
||||
for (auto part_it = participants.begin(); part_it != participants.end(); ++part_it) {
|
||||
if (part_it->has_vote) {
|
||||
active_count++;
|
||||
}
|
||||
}
|
||||
|
||||
// Обновляем запись кооператива, устанавливая счетчик активных пайщиков
|
||||
coops2.modify(coop_itr, _registrator, [&](auto& coop) {
|
||||
coop.active_participants_count = active_count;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,20 @@ void registrator::confirmreg(eosio::name coopname, checksum256 registration_hash
|
||||
std::make_tuple(coopname, candidate -> initial)
|
||||
).send();
|
||||
|
||||
// Увеличиваем счетчик активных пайщиков
|
||||
cooperatives2_index cooperatives(_registrator, _registrator.value);
|
||||
auto coop_itr = cooperatives.find(coopname.value);
|
||||
|
||||
if (coop_itr != cooperatives.end() && coop_itr->is_cooperative) {
|
||||
cooperatives.modify(coop_itr, _registrator, [&](auto &coop) {
|
||||
if (coop.active_participants_count.has_value()) {
|
||||
coop.active_participants_count = coop.active_participants_count.value() + 1;
|
||||
} else {
|
||||
coop.active_participants_count = 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Registrator::candidates_index candidates(_registrator, coopname.value);
|
||||
auto it = candidates.find(candidate -> username.value);
|
||||
candidates.erase(it);
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
#include "src/agreement/declineagree.cpp"
|
||||
#include "src/agreement/sndagreement.cpp"
|
||||
#include "src/agreement/migrateagree.cpp"
|
||||
|
||||
#include "src/approve/confirmapprv.cpp"
|
||||
#include "src/approve/createapprv.cpp"
|
||||
#include "src/approve/declineapprv.cpp"
|
||||
@@ -41,6 +40,7 @@
|
||||
|
||||
#include "src/doc/declinedoc.cpp"
|
||||
#include "src/doc/newact.cpp"
|
||||
#include "src/doc/newlink.cpp"
|
||||
#include "src/doc/newdecision.cpp"
|
||||
#include "src/doc/newdeclined.cpp"
|
||||
#include "src/doc/newresolved.cpp"
|
||||
|
||||
@@ -50,6 +50,7 @@ public:
|
||||
[[eosio::action]] void newpackage(NEWPACKAGE_SIGNATURE);
|
||||
|
||||
[[eosio::action]] void newact(NEWACT_SIGNATURE);
|
||||
[[eosio::action]] void newlink(NEWLINK_SIGNATURE);
|
||||
[[eosio::action]] void newdeclined(NEWDECLINED_SIGNATURE);
|
||||
|
||||
[[eosio::action]] void declinedoc(eosio::name coopname, eosio::name username, checksum256 hash, document2 document);
|
||||
|
||||
@@ -39,7 +39,7 @@ void soviet::migrateagree(eosio::name coopname, uint64_t agreement_id) {
|
||||
new_doc.signatures.push_back(sig);
|
||||
|
||||
// Создаем новую запись в agreement2 с тем же ID
|
||||
eosio::multi_index<"agreements2"_n, agreement2> agreements2(_self, coopname.value);
|
||||
agreements2_index agreements2(_self, coopname.value);
|
||||
|
||||
agreements2.emplace(_self, [&](auto& new_agreement) {
|
||||
new_agreement.id = agreement_itr->id;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//TODO: нужно вырезать этот метод, заменив его на что-то другое.
|
||||
// сейчас мы в лоб проходим по всем пайщикам и отключаем их от кооперативного участка, в случае, если он удален.
|
||||
// так вообще не то. Годится только на малых масштабах.
|
||||
// так вообще не то. Годится только на малых масштабах (до 100 пайщиков на участке будет ок)
|
||||
/**
|
||||
* @brief Эффект удаления кооперативного участка
|
||||
* @param coopname Имя кооператива
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
[[eosio::action]] void soviet::newlink(NEWLINK_SIGNATURE) {
|
||||
check_auth_and_get_payer_or_fail(contracts_whitelist);
|
||||
|
||||
require_recipient(coopname);
|
||||
require_recipient(username);
|
||||
};
|
||||
@@ -10,10 +10,25 @@ void soviet::block(eosio::name coopname, eosio::name admin, eosio::name username
|
||||
participants_index participants(_soviet, coopname.value);
|
||||
auto participant = participants.find(username.value);
|
||||
|
||||
// Сохраняем текущие значения статуса и права голоса
|
||||
bool had_vote = participant->has_vote;
|
||||
bool was_active = participant->status == "accepted"_n;
|
||||
|
||||
participants.modify(participant, _soviet, [&](auto &row){
|
||||
row.status = "blocked"_n;
|
||||
row.is_initial = false;
|
||||
row.is_minimum = false;
|
||||
row.has_vote = false;
|
||||
});
|
||||
|
||||
// Если участник был активен и имел право голоса, уменьшаем счетчик активных пайщиков
|
||||
if (was_active && had_vote) {
|
||||
// Отправляем уведомление в registrator для уменьшения счетчика
|
||||
action(
|
||||
permission_level{_soviet, "active"_n},
|
||||
_registrator,
|
||||
"decparticpnt"_n,
|
||||
std::make_tuple(coopname, username)
|
||||
).send();
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,6 @@ void wallet::approvewthd(eosio::name coopname, checksum256 withdraw_hash, docume
|
||||
withdraw -> statement,
|
||||
std::string("")
|
||||
)
|
||||
).send();
|
||||
).send();
|
||||
|
||||
}
|
||||
@@ -14,16 +14,16 @@ void wallet::authwthd(eosio::name coopname, checksum256 withdraw_hash) {
|
||||
});
|
||||
|
||||
// создаём объект исходящего платежа в gateway с коллбэком после обработки
|
||||
action(permission_level{ _capital, "active"_n}, _gateway, "createoutpay"_n,
|
||||
std::make_tuple(
|
||||
coopname,
|
||||
withdraw -> username,
|
||||
withdraw_hash,
|
||||
withdraw -> quantity,
|
||||
_wallet,
|
||||
"completewthd"_n,
|
||||
"declinewthd"_n
|
||||
)
|
||||
).send();
|
||||
|
||||
Action::send<createoutpay_interface>(
|
||||
_gateway,
|
||||
"createoutpay"_n,
|
||||
_wallet,
|
||||
coopname,
|
||||
withdraw -> username,
|
||||
withdraw_hash,
|
||||
withdraw -> quantity,
|
||||
_wallet,
|
||||
Wallet::get_valid_wallet_action("completewthd"_n),
|
||||
Wallet::get_valid_wallet_action("declinewthd"_n)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
void wallet::completewthd(eosio::name coopname, checksum256 withdraw_hash, std::string memo) {
|
||||
void wallet::completewthd(COMPLETEWTHD_SIGNATURE) {
|
||||
require_auth(_gateway);
|
||||
|
||||
auto exist = Wallet::get_withdraw(coopname, withdraw_hash);
|
||||
@@ -16,7 +16,7 @@ void wallet::completewthd(eosio::name coopname, checksum256 withdraw_hash, std::
|
||||
std::make_tuple(coopname, withdraw -> quantity, false)
|
||||
).send();
|
||||
|
||||
std::string memo_in = "Возврат части целевого паевого взноса по ЦПП 'Цифровой Кошелёк'";
|
||||
std::string memo_in = "Возврат части паевого взноса по ЦПП 'Цифровой Кошелёк'";
|
||||
Wallet::sub_blocked_funds(_wallet, coopname, withdraw -> username, withdraw -> quantity, _wallet_program, memo_in);
|
||||
|
||||
withdraws.erase(withdraw);
|
||||
|
||||
@@ -28,22 +28,20 @@ void wallet::createwthd(eosio::name coopname, eosio::name username, checksum256
|
||||
d.created_at = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
|
||||
});
|
||||
|
||||
//TODO: перевести approve на hash-идентификатор???
|
||||
action(
|
||||
permission_level{_capital, "active"_n}, // кто вызывает
|
||||
Action::send<createagenda_interface>(
|
||||
_soviet,
|
||||
"createapprv"_n,
|
||||
std::make_tuple(
|
||||
coopname,
|
||||
username,
|
||||
statement,
|
||||
withdraw_hash,
|
||||
_wallet, // callback_contract (текущий контракт)
|
||||
"approvewthd"_n, // callback_action_approve
|
||||
"declinewthd"_n, // callback_action_decline
|
||||
std::string("")
|
||||
)
|
||||
).send();
|
||||
"createagenda"_n,
|
||||
_wallet,
|
||||
coopname,
|
||||
username,
|
||||
get_valid_soviet_action("createwthd"_n),
|
||||
withdraw_hash,
|
||||
_wallet, // callback_contract (текущий контракт)
|
||||
Wallet::get_valid_wallet_action("authwthd"_n), // callback_action_approve
|
||||
Wallet::get_valid_wallet_action("declinewthd"_n), // callback_action_decline
|
||||
statement,
|
||||
std::string("")
|
||||
);
|
||||
|
||||
std::string memo_in = "Возврат части целевого паевого взноса по ЦПП 'Цифровой Кошелёк'";
|
||||
Wallet::block_funds(_wallet, coopname, username, quantity, _wallet_program, memo_in);
|
||||
|
||||
@@ -25,8 +25,8 @@ public:
|
||||
|
||||
//возврат паевого взноса
|
||||
[[eosio::action]] void createwthd(eosio::name coopname, eosio::name username, checksum256 withdraw_hash, eosio::asset quantity, document2 statement);
|
||||
[[eosio::action]] void completewthd(eosio::name coopname, checksum256 withdraw_hash, std::string memo);
|
||||
[[eosio::action]] void declinewthd(eosio::name coopname, checksum256 withdraw_hash, std::string reason);
|
||||
[[eosio::action]] void completewthd(COMPLETEWTHD_SIGNATURE);
|
||||
[[eosio::action]] void declinewthd(DECLINEWTHD_SIGNATURE);
|
||||
[[eosio::action]] void authwthd(eosio::name coopname, checksum256 withdraw_hash);
|
||||
[[eosio::action]] void approvewthd(eosio::name coopname, checksum256 withdraw_hash, document2 approved_statement);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@coopenomics/contracts",
|
||||
"version": "2025.5.5-2",
|
||||
"version": "2025.7.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build:all": "./build-all.sh",
|
||||
|
||||
@@ -97,6 +97,7 @@ export interface IQuestion {
|
||||
|
||||
export interface IQuestionResult {
|
||||
question_id: IUint64
|
||||
number: IUint64
|
||||
title: string
|
||||
decision: string
|
||||
context: string
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,14 @@
|
||||
|
||||
# =============================================================================
|
||||
# PUSH NOTIFICATIONS
|
||||
# =============================================================================
|
||||
# URL сервиса notificator2
|
||||
NOTIFICATOR2_URL=http://localhost:3001
|
||||
|
||||
# Секретный ключ для аутентификации запросов к notificator2
|
||||
SERVER_SECRET=your-super-secret-key-here
|
||||
|
||||
NOVU_APP_ID=
|
||||
NOVU_BACKEND_URL=https://novu.coopenomics.world/api
|
||||
NOVU_SOCKET_URL=https://novu.coopenomics.world/ws
|
||||
NOVU_API_KEY=
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
-- Исправление PRIMARY KEY в таблице payments
|
||||
|
||||
-- Сначала проверим текущую структуру
|
||||
\d payments
|
||||
|
||||
-- Если таблица существует и у неё есть PK на hash, то:
|
||||
|
||||
-- 1. Удаляем старый PRIMARY KEY (если есть)
|
||||
DO $$
|
||||
DECLARE
|
||||
pk_name TEXT;
|
||||
BEGIN
|
||||
SELECT conname INTO pk_name
|
||||
FROM pg_constraint
|
||||
WHERE conrelid = 'payments'::regclass AND contype = 'p';
|
||||
|
||||
IF pk_name IS NOT NULL THEN
|
||||
EXECUTE 'ALTER TABLE payments DROP CONSTRAINT ' || pk_name;
|
||||
RAISE NOTICE 'Удален PRIMARY KEY: %', pk_name;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- 2. Добавляем колонку id если её нет
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'payments' AND column_name = 'id') THEN
|
||||
ALTER TABLE payments ADD COLUMN id uuid DEFAULT gen_random_uuid();
|
||||
UPDATE payments SET id = gen_random_uuid() WHERE id IS NULL;
|
||||
ALTER TABLE payments ALTER COLUMN id SET NOT NULL;
|
||||
RAISE NOTICE 'Добавлена колонка id';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- 3. Создаем новый PRIMARY KEY по id
|
||||
ALTER TABLE payments ADD PRIMARY KEY (id);
|
||||
|
||||
-- 4. Делаем hash nullable
|
||||
ALTER TABLE payments ALTER COLUMN hash DROP NOT NULL;
|
||||
|
||||
-- Проверяем результат
|
||||
\d payments
|
||||
@@ -0,0 +1,19 @@
|
||||
// jest.setup.js
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
// Мокаем config если нужно
|
||||
jest.mock('~/config/config', () => ({
|
||||
default: {
|
||||
jwt: {
|
||||
secret: 'test-secret',
|
||||
accessExpirationMinutes: '60m',
|
||||
},
|
||||
coopname: 'test-coop',
|
||||
novu: {
|
||||
backend_url: 'http://localhost:3000',
|
||||
socket_url: 'ws://localhost:3000',
|
||||
api_key: 'test-api-key',
|
||||
app_id: 'test-app-id',
|
||||
},
|
||||
},
|
||||
}));
|
||||
@@ -0,0 +1,32 @@
|
||||
# Миграции
|
||||
|
||||
## Правила именования
|
||||
|
||||
- Обычные миграции: `V{версия}__описание.ts` (например, `V1.0.0__initial.ts`)
|
||||
- Тестовые миграции: `V{версия}__описание__test.ts` (например, `V1.0.2__blockchain_data_sync__test.ts`)
|
||||
|
||||
## Поведение
|
||||
|
||||
- Обычные миграции применяются только один раз, если их версия ещё не применялась.
|
||||
- Тестовые миграции (файлы с `__test` в имени) запускаются всегда, даже если версия уже применялась.
|
||||
- Флаг `isTest` в коде миграции больше не используется — только по имени файла.
|
||||
- Если в миграции указан параметр `validUntil` (дата), то миграция не будет применена, если эта дата уже прошла (по UTC).
|
||||
|
||||
## Пример
|
||||
|
||||
- `V1.0.0__initial.ts` — обычная миграция
|
||||
- `V1.0.2__blockchain_data_sync__test.ts` — тестовая миграция
|
||||
|
||||
## validUntil
|
||||
|
||||
В объекте миграции можно указать поле `validUntil` типа `Date`. Если текущая дата превышает это значение, миграция не будет применена.
|
||||
|
||||
```ts
|
||||
export default {
|
||||
name: 'example',
|
||||
validUntil: new Date('2024-07-01T00:00:00Z'),
|
||||
up: async ({ blockchain }) => {
|
||||
/* ... */
|
||||
},
|
||||
};
|
||||
```
|
||||
@@ -2,6 +2,7 @@ import mongoose from 'mongoose';
|
||||
|
||||
export default {
|
||||
name: 'Добавление поля fact_address для организаций',
|
||||
validUntil: new Date(), // Текущая дата, миграция больше не будет применяться
|
||||
|
||||
async up() {
|
||||
console.log('Выполнение миграции: Добавление поля fact_address для организаций');
|
||||
|
||||
@@ -2,6 +2,7 @@ import mongoose from 'mongoose';
|
||||
|
||||
export default {
|
||||
name: 'Преобразование числовых method_id в строковые',
|
||||
validUntil: new Date(), // Текущая дата, миграция больше не будет применяться
|
||||
|
||||
async up() {
|
||||
console.log('Выполнение миграции: Преобразование числовых method_id в строковые');
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { SovietContract } from 'cooptypes';
|
||||
export default {
|
||||
name: 'Синхронизация данных из блокчейна',
|
||||
isTest: false, // Включаем тестовый режим - миграция не будет сохранена в БД
|
||||
validUntil: new Date(), // Текущая дата, миграция больше не будет применяться
|
||||
|
||||
async up({ blockchain }: { blockchain: BlockchainService }): Promise<boolean> {
|
||||
console.log('Выполнение миграции: Синхронизация данных из блокчейна');
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import { BlockchainService } from '../src/infrastructure/blockchain/blockchain.service';
|
||||
import Vault from '../src/models/vault.model';
|
||||
import config from '../src/config/config';
|
||||
import type { SovietContract } from 'cooptypes';
|
||||
|
||||
export default {
|
||||
name: 'Синхронизация данных из блокчейна',
|
||||
isTest: false, // Включаем тестовый режим - миграция не будет сохранена в БД
|
||||
validUntil: new Date(), // Текущая дата, миграция больше не будет применяться
|
||||
|
||||
async up({ blockchain }: { blockchain: BlockchainService }): Promise<boolean> {
|
||||
console.log('Выполнение миграции: Синхронизация данных из блокчейна');
|
||||
|
||||
try {
|
||||
// Проверяем статус подключения к блокчейну без авторизации
|
||||
const info = await blockchain.getInfo();
|
||||
console.log(`Подключение к блокчейну успешно. Chain ID: ${info.chain_id}`);
|
||||
console.log(`Информация о блокчейне: Chain - ${info.chain_id}, Head Block: ${info.head_block_num}`);
|
||||
|
||||
// Имя кооператива, чьи данные будем синхронизировать
|
||||
const coopname = config.coopname; // Укажите имя вашего кооператива
|
||||
|
||||
// Получаем приватный ключ из Vault модели
|
||||
try {
|
||||
const wif = await Vault.getWif(coopname);
|
||||
|
||||
if (wif) {
|
||||
console.log(`Получен приватный ключ для аккаунта ${coopname}`);
|
||||
|
||||
// Инициализируем блокчейн-сервис с полученным ключом
|
||||
blockchain.initialize(coopname, wif);
|
||||
console.log(`Блокчейн-сервис инициализирован с аккаунтом ${coopname}`);
|
||||
|
||||
const oldRows = await blockchain.getAllRows<SovietContract.Tables.AgreementsLegacy.IAgreementLegacy>(
|
||||
'soviet',
|
||||
coopname,
|
||||
'agreements'
|
||||
);
|
||||
|
||||
console.log('Получено legacy соглашений:', oldRows.length);
|
||||
|
||||
// Получаем все строки из таблицы agreements2
|
||||
const newRows = await blockchain.getAllRows<SovietContract.Tables.Agreements.IAgreement>(
|
||||
'soviet',
|
||||
coopname,
|
||||
'agreements3'
|
||||
);
|
||||
|
||||
console.log('Получено новых соглашений:', newRows.length);
|
||||
|
||||
// Находим соглашения, которые ещё не были перенесены
|
||||
const newAgreementIds = new Set(newRows.map((row) => row.id));
|
||||
const notMigratedAgreements = oldRows.filter((row) => !newAgreementIds.has(row.id));
|
||||
|
||||
console.log('Соглашения, которые НЕ были перенесены:', notMigratedAgreements.length);
|
||||
console.log('Детали непереносимых соглашений:');
|
||||
notMigratedAgreements.forEach((agreement) => {
|
||||
console.log(
|
||||
`ID: ${agreement.id}, Пользователь: ${agreement.username}, Тип: ${agreement.type}, Статус: ${agreement.status}`
|
||||
);
|
||||
});
|
||||
|
||||
// Подготовка массива транзакций для миграции
|
||||
if (notMigratedAgreements.length > 0) {
|
||||
console.log('Подготовлен массив транзакций для миграции');
|
||||
console.log('COOPNAME: ', coopname);
|
||||
|
||||
// Последовательная миграция соглашений
|
||||
for (let i = 0; i < notMigratedAgreements.length; i++) {
|
||||
const agreement = notMigratedAgreements[i];
|
||||
console.log(`Миграция соглашения ${i + 1}/${notMigratedAgreements.length}, ID: ${agreement.id}`);
|
||||
|
||||
try {
|
||||
await blockchain.transact({
|
||||
account: 'soviet',
|
||||
name: 'migrateagree',
|
||||
authorization: [{ actor: coopname, permission: 'active' }],
|
||||
data: {
|
||||
coopname,
|
||||
agreement_id: agreement.id,
|
||||
},
|
||||
});
|
||||
console.log(`✅ Соглашение ID: ${agreement.id} успешно перенесено`);
|
||||
} catch (error) {
|
||||
console.error(`❌ Ошибка при переносе соглашения ID: ${agreement.id}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Миграция соглашений завершена');
|
||||
}
|
||||
|
||||
// Теперь можно выполнять транзакции от имени аккаунта
|
||||
// blockchain.transact(...);
|
||||
} else {
|
||||
console.log(`Приватный ключ для ${coopname} не найден в Vault`);
|
||||
}
|
||||
} catch (vaultError) {
|
||||
console.error('Ошибка при получении ключа из Vault:', vaultError);
|
||||
}
|
||||
|
||||
console.log('Миграция завершена: Синхронизация данных из блокчейна выполнена успешно');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при выполнении миграции блокчейн-данных:', error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,265 @@
|
||||
import { Order } from '../src/models';
|
||||
import { TypeOrmPaymentRepository } from '../src/infrastructure/database/typeorm/repositories/typeorm-payment.repository';
|
||||
import { PaymentEntity } from '../src/infrastructure/database/typeorm/entities/payment.entity';
|
||||
import { DataSource } from 'typeorm';
|
||||
import config from '../src/config/config';
|
||||
import { PaymentStatusEnum } from '../src/domain/gateway/enums/payment-status.enum';
|
||||
import { PaymentTypeEnum, PaymentDirectionEnum, getPaymentDirection } from '../src/domain/gateway/enums/payment-type.enum';
|
||||
import type { PaymentDomainInterface } from '../src/domain/gateway/interfaces/payment-domain.interface';
|
||||
import { sha256 } from '../src/utils/sha256';
|
||||
import { generator } from '../src/services/document.service';
|
||||
|
||||
export default {
|
||||
name: 'Миграция платежей из MongoDB в PostgreSQL (унифицированная модель)',
|
||||
validUntil: new Date('2025-07-03'), // Действует до
|
||||
|
||||
async up(): Promise<boolean> {
|
||||
console.log('Выполнение миграции: Перенос платежей из MongoDB в PostgreSQL (унифицированная модель)');
|
||||
|
||||
try {
|
||||
await generator.connect(config.mongoose.url);
|
||||
|
||||
// Инициализируем подключение к PostgreSQL
|
||||
const dataSource = new DataSource({
|
||||
type: 'postgres',
|
||||
host: config.postgres.host,
|
||||
port: Number(config.postgres.port),
|
||||
username: config.postgres.username,
|
||||
password: config.postgres.password,
|
||||
database: config.postgres.database,
|
||||
entities: [PaymentEntity],
|
||||
synchronize: true, // Создаем таблицу если она не существует
|
||||
});
|
||||
|
||||
await dataSource.initialize();
|
||||
console.log('Подключение к PostgreSQL установлено');
|
||||
|
||||
// Создаем репозиторий для работы с платежами
|
||||
const paymentRepository = new TypeOrmPaymentRepository(dataSource.getRepository(PaymentEntity));
|
||||
|
||||
// Получаем все заказы из MongoDB
|
||||
const orders = await Order.find();
|
||||
console.log(`Найдено ${orders.length} заказов в MongoDB`);
|
||||
|
||||
let migratedCount = 0;
|
||||
let skippedCount = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
for (const order of orders) {
|
||||
try {
|
||||
// Генерируем hash из order_num для поиска существующих платежей
|
||||
const hash = sha256(String(order.order_num));
|
||||
|
||||
// Проверяем, существует ли уже платеж с таким hash
|
||||
const existingPayment = await paymentRepository.findByHash(hash);
|
||||
|
||||
if (existingPayment) {
|
||||
console.log(`Платеж с hash ${hash} (order_num: ${order.order_num}) уже существует, пропускаем`);
|
||||
skippedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Мапим старые статусы на новые
|
||||
const statusMapping: Record<string, PaymentStatusEnum> = {
|
||||
pending: PaymentStatusEnum.PENDING,
|
||||
paid: PaymentStatusEnum.PAID,
|
||||
completed: PaymentStatusEnum.COMPLETED,
|
||||
failed: PaymentStatusEnum.FAILED,
|
||||
expired: PaymentStatusEnum.EXPIRED,
|
||||
refunded: PaymentStatusEnum.REFUNDED,
|
||||
};
|
||||
|
||||
// Мапим старые типы на новые
|
||||
const typeMapping: Record<string, PaymentTypeEnum> = {
|
||||
registration: PaymentTypeEnum.REGISTRATION,
|
||||
deposit: PaymentTypeEnum.DEPOSIT,
|
||||
};
|
||||
|
||||
const paymentType = typeMapping[order.type] || PaymentTypeEnum.REGISTRATION;
|
||||
const paymentDirection = getPaymentDirection(paymentType);
|
||||
const paymentStatus = statusMapping[order.status] || PaymentStatusEnum.PENDING;
|
||||
|
||||
// Проверяем, что все мигрируемые платежи действительно входящие
|
||||
if (paymentDirection !== PaymentDirectionEnum.INCOMING) {
|
||||
console.warn(`⚠️ Платеж ${order.order_num} имеет неожиданное направление ${paymentDirection}, пропускаем`);
|
||||
skippedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Создаем объект платежа согласно доменному интерфейсу
|
||||
const paymentData: PaymentDomainInterface = {
|
||||
hash, // используем сгенерированный hash вместо blockchain_num
|
||||
coopname: order.creator,
|
||||
username: order.username,
|
||||
quantity: parseFloat(order.quantity),
|
||||
symbol: order.symbol,
|
||||
type: paymentType,
|
||||
direction: paymentDirection,
|
||||
status: paymentStatus,
|
||||
provider: order.provider,
|
||||
secret: order.secret,
|
||||
message: order.message,
|
||||
expired_at: order.expired_at,
|
||||
created_at: (order as any).createdAt || new Date(), // Mongoose timestamps
|
||||
updated_at: (order as any).updatedAt || new Date(),
|
||||
payment_details: order.details
|
||||
? {
|
||||
data: order.details.data,
|
||||
amount_plus_fee: order.details.amount_plus_fee,
|
||||
amount_without_fee: order.details.amount_without_fee,
|
||||
fee_amount: order.details.fee_amount,
|
||||
fee_percent: order.details.fee_percent,
|
||||
fact_fee_percent: order.details.fact_fee_percent,
|
||||
tolerance_percent: order.details.tolerance_percent,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
|
||||
// Создаем платеж через репозиторий
|
||||
await paymentRepository.create(paymentData);
|
||||
|
||||
migratedCount++;
|
||||
console.log(
|
||||
`✅ Мигрирован платеж ${order.order_num} -> hash: ${hash} (${paymentType}/${paymentDirection}, ${paymentStatus})`
|
||||
);
|
||||
} catch (error) {
|
||||
errorCount++;
|
||||
console.error(`❌ Ошибка при миграции платежа ${order.order_num}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n=== Результаты миграции ===');
|
||||
console.log(`Мигрировано: ${migratedCount}`);
|
||||
console.log(`Пропущено: ${skippedCount}`);
|
||||
console.log(`Ошибок: ${errorCount}`);
|
||||
console.log(`Всего обработано: ${orders.length}`);
|
||||
|
||||
// Проверяем, что все мигрированные платежи действительно входящие
|
||||
console.log('\n=== Проверка миграции ===');
|
||||
const allPayments = await paymentRepository.getAllPayments({}, { page: 1, limit: 1000, sortOrder: 'ASC' });
|
||||
const incomingCount = allPayments.items.filter(
|
||||
(p: PaymentDomainInterface) => p.direction === PaymentDirectionEnum.INCOMING
|
||||
).length;
|
||||
const outgoingCount = allPayments.items.filter(
|
||||
(p: PaymentDomainInterface) => p.direction === PaymentDirectionEnum.OUTGOING
|
||||
).length;
|
||||
|
||||
console.log(`Входящих платежей: ${incomingCount}`);
|
||||
console.log(`Исходящих платежей: ${outgoingCount}`);
|
||||
|
||||
if (outgoingCount > 0) {
|
||||
console.warn(`⚠️ Обнаружены исходящие платежи после миграции! Это может указывать на проблему.`);
|
||||
}
|
||||
|
||||
// Дополнительная валидация данных
|
||||
console.log('\n=== Валидация данных ===');
|
||||
const registrationPayments = allPayments.items.filter(
|
||||
(p: PaymentDomainInterface) => p.type === PaymentTypeEnum.REGISTRATION
|
||||
).length;
|
||||
const depositPayments = allPayments.items.filter(
|
||||
(p: PaymentDomainInterface) => p.type === PaymentTypeEnum.DEPOSIT
|
||||
).length;
|
||||
|
||||
console.log(`Регистрационных платежей: ${registrationPayments}`);
|
||||
console.log(`Депозитных платежей: ${depositPayments}`);
|
||||
|
||||
// Проверяем обязательные поля
|
||||
const paymentsWithMissingData = allPayments.items.filter(
|
||||
(p: PaymentDomainInterface) => !p.coopname || !p.username || !p.quantity || !p.symbol || !p.provider || !p.secret
|
||||
);
|
||||
|
||||
if (paymentsWithMissingData.length > 0) {
|
||||
console.warn(`⚠️ Найдено ${paymentsWithMissingData.length} платежей с отсутствующими обязательными данными`);
|
||||
paymentsWithMissingData.forEach((p: PaymentDomainInterface) => {
|
||||
console.warn(` Платеж ${p.hash}: отсутствуют поля`, {
|
||||
coopname: !p.coopname,
|
||||
username: !p.username,
|
||||
quantity: !p.quantity,
|
||||
symbol: !p.symbol,
|
||||
provider: !p.provider,
|
||||
secret: !p.secret,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
console.log('✅ Все платежи имеют обязательные поля');
|
||||
}
|
||||
|
||||
// Проверяем корректность типов и направлений
|
||||
const invalidDirections = allPayments.items.filter((p: PaymentDomainInterface) => {
|
||||
const expectedDirection = getPaymentDirection(p.type);
|
||||
return p.direction !== expectedDirection;
|
||||
});
|
||||
|
||||
if (invalidDirections.length > 0) {
|
||||
console.warn(`⚠️ Найдено ${invalidDirections.length} платежей с некорректным направлением`);
|
||||
} else {
|
||||
console.log('✅ Все платежи имеют корректные направления');
|
||||
}
|
||||
|
||||
// Создаем базовые индексы для оптимизации запросов
|
||||
console.log('\n=== Создание индексов ===');
|
||||
|
||||
try {
|
||||
await dataSource.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_payments_status ON payments(status);
|
||||
`);
|
||||
|
||||
await dataSource.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_payments_type ON payments(type);
|
||||
`);
|
||||
|
||||
await dataSource.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_payments_direction ON payments(direction);
|
||||
`);
|
||||
|
||||
await dataSource.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_payments_username ON payments(username);
|
||||
`);
|
||||
|
||||
await dataSource.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_payments_coopname ON payments(coopname);
|
||||
`);
|
||||
|
||||
await dataSource.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_payments_created_at ON payments(created_at);
|
||||
`);
|
||||
|
||||
await dataSource.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_payments_secret ON payments(secret);
|
||||
`);
|
||||
|
||||
await dataSource.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_payments_hash ON payments(hash);
|
||||
`);
|
||||
|
||||
await dataSource.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_payments_quantity ON payments(quantity);
|
||||
`);
|
||||
await dataSource.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_payments_symbol ON payments(symbol);
|
||||
`);
|
||||
await dataSource.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_payments_expired_at ON payments(expired_at);
|
||||
`);
|
||||
|
||||
await dataSource.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_payments_composite ON payments(coopname, username, status, type);
|
||||
`);
|
||||
|
||||
console.log('Базовые индексы созданы');
|
||||
} catch (indexError) {
|
||||
console.warn('Ошибка при создании индексов:', indexError);
|
||||
}
|
||||
|
||||
// Закрываем подключение к PostgreSQL
|
||||
await dataSource.destroy();
|
||||
console.log('Подключение к PostgreSQL закрыто');
|
||||
|
||||
console.log('Миграция завершена: Перенос платежей в унифицированную модель выполнен успешно');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при выполнении миграции платежей:', error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
};
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "create-nodejs-express-app",
|
||||
"version": "2025.5.5-2",
|
||||
"version": "2025.7.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "create-nodejs-express-app",
|
||||
"version": "2025.5.5-2",
|
||||
"version": "2025.7.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@a2seven/yoo-checkout": "^1.1.4",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@coopenomics/controller",
|
||||
"version": "2025.5.5-2",
|
||||
"version": "2025.7.1",
|
||||
"description": "",
|
||||
"private": false,
|
||||
"bin": "bin/createNodejsApp.js",
|
||||
@@ -64,6 +64,7 @@
|
||||
"dependencies": {
|
||||
"@a2seven/yoo-checkout": "^1.1.4",
|
||||
"@coopenomics/factory": "workspace:*",
|
||||
"@coopenomics/sdk": "workspace:*",
|
||||
"@graphql-codegen/typescript-graphql-request": "^6.2.0",
|
||||
"@graphql-codegen/typescript-resolvers": "^4.4.0",
|
||||
"@graphql-tools/schema": "^10.0.7",
|
||||
@@ -126,6 +127,7 @@
|
||||
"jspdf": "^2.5.1",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.24.0",
|
||||
"moment-timezone": "^0.5.45",
|
||||
"mongoose": "^8.5.2",
|
||||
"mongoose-sequence": "^6.0.1",
|
||||
"morgan": "^1.9.1",
|
||||
@@ -155,8 +157,7 @@
|
||||
"ws": "^8.18.0",
|
||||
"xss-clean": "^0.1.1",
|
||||
"zod": "^3.23.8",
|
||||
"zod-to-json-schema": "^3.23.5",
|
||||
"@coopenomics/sdk": "workspace:*"
|
||||
"zod-to-json-schema": "^3.23.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^2.24.1",
|
||||
|
||||
+755
-104
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
import swaggerJsdoc from 'swagger-jsdoc';
|
||||
import swaggerDefinition from '../src/docs/swaggerDef';
|
||||
import fs from 'fs'
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const specs = swaggerJsdoc({
|
||||
@@ -8,5 +8,4 @@ const specs = swaggerJsdoc({
|
||||
apis: ['src/docs/*.yml', 'src/**/*.ts'],
|
||||
});
|
||||
|
||||
|
||||
fs.writeFileSync(path.join(__dirname, '../src/docs/swagger.json'), JSON.stringify(specs, null, 2));
|
||||
|
||||
@@ -2,7 +2,7 @@ spectaql:
|
||||
targetDir: ./docs
|
||||
introspection:
|
||||
# schemaFile: ./schema.gql
|
||||
url: 'http://localhost:2998/v1/graphql'
|
||||
url: 'https://testnet.coopenomics.world/api/v1/graphql'
|
||||
|
||||
info:
|
||||
title: 'API-Документация'
|
||||
|
||||
@@ -42,27 +42,44 @@ const envVarsSchema = z.object({
|
||||
SMTP_USERNAME: z.string().default(''), // Пустая строка для необязательного значения
|
||||
SMTP_PASSWORD: z.string().default(''), // Пустая строка для необязательного значения
|
||||
EMAIL_FROM: z.string().default(''), // Пустая строка для необязательного значения
|
||||
COOPNAME: z.string().default('default_coopname'), // Задаём дефолтное значение
|
||||
GRAPHQL_SERVICE: z.string().default('http://localhost:4090').describe('адрес сервиса GRAPHQL'),
|
||||
COOPNAME: z.string().min(1, { message: 'Не должно быть пустым' }).default('voskhod'), // Задаём дефолтное значение, пустая строка невалидна
|
||||
TIMEZONE: z.string().min(1, { message: 'Не должно быть пустым' }).default('Europe/Moscow'), // Пустая строка невалидна
|
||||
GRAPHQL_SERVICE: z
|
||||
.string()
|
||||
.min(1, { message: 'Не должно быть пустым' })
|
||||
.default('http://localhost:4090')
|
||||
.describe('адрес сервиса GRAPHQL'),
|
||||
|
||||
// Новые переменные для PostgreSQL
|
||||
POSTGRES_HOST: z.string().default('127.0.0.1'),
|
||||
POSTGRES_HOST: z.string().min(1, { message: 'Не должно быть пустым' }).default('127.0.0.1'),
|
||||
POSTGRES_PORT: z
|
||||
.string()
|
||||
.default('5432')
|
||||
.transform((val) => parseInt(val, 10)),
|
||||
POSTGRES_USERNAME: z.string(),
|
||||
POSTGRES_PASSWORD: z.string(),
|
||||
POSTGRES_DATABASE: z.string(),
|
||||
POSTGRES_USERNAME: z.string().min(1, { message: 'Не должно быть пустым' }),
|
||||
POSTGRES_PASSWORD: z.string().min(1, { message: 'Не должно быть пустым' }),
|
||||
POSTGRES_DATABASE: z.string().min(1, { message: 'Не должно быть пустым' }),
|
||||
|
||||
REDIS_HOST: z.string(),
|
||||
REDIS_HOST: z.string().min(1, { message: 'Не должно быть пустым' }),
|
||||
REDIS_PORT: z
|
||||
.string()
|
||||
.default('6379')
|
||||
.transform((val) => parseInt(val, 10)),
|
||||
REDIS_PASSWORD: z.string(),
|
||||
BLOCKCHAIN_RPC: z.string(),
|
||||
CHAIN_ID: z.string(),
|
||||
BLOCKCHAIN_RPC: z.string().min(1, { message: 'Не должно быть пустым' }),
|
||||
CHAIN_ID: z.string().min(1, { message: 'Не должно быть пустым' }),
|
||||
|
||||
// Параметры блокчейна
|
||||
ROOT_SYMBOL: z.string().default('AXON'),
|
||||
ROOT_GOVERN_SYMBOL: z.string().default('RUB'),
|
||||
ROOT_PRECISION: z
|
||||
.string()
|
||||
.default('4')
|
||||
.transform((val) => parseInt(val, 10)),
|
||||
ROOT_GOVERN_PRECISION: z
|
||||
.string()
|
||||
.default('4')
|
||||
.transform((val) => parseInt(val, 10)),
|
||||
});
|
||||
|
||||
// Валидация переменных окружения
|
||||
@@ -86,9 +103,14 @@ export default {
|
||||
base_url: envVars.data.BASE_URL,
|
||||
port: envVars.data.PORT,
|
||||
server_secret: envVars.data.SERVER_SECRET,
|
||||
timezone: envVars.data.TIMEZONE,
|
||||
blockchain: {
|
||||
url: envVars.data.BLOCKCHAIN_RPC,
|
||||
id: envVars.data.CHAIN_ID,
|
||||
root_symbol: envVars.data.ROOT_SYMBOL,
|
||||
root_govern_symbol: envVars.data.ROOT_GOVERN_SYMBOL,
|
||||
root_precision: envVars.data.ROOT_PRECISION,
|
||||
root_govern_precision: envVars.data.ROOT_GOVERN_PRECISION,
|
||||
},
|
||||
mongoose: {
|
||||
url: envVars.data.MONGODB_URL + (envVars.data.NODE_ENV === 'test' ? '-test' : ''),
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { default as config } from './config';
|
||||
@@ -1,7 +1,6 @@
|
||||
export * as authController from './auth.controller';
|
||||
export * as userController from './user.controller';
|
||||
export * as documentController from './document.controller';
|
||||
export * as orderController from './order.controller';
|
||||
export * as coopController from './coop.controller';
|
||||
export * as notifyController from './notify.controller';
|
||||
export * as systemController from './system.controller';
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import http from 'http-status';
|
||||
import catchAsync from '../utils/catchAsync';
|
||||
import { orderService } from '../services';
|
||||
import { ICreateDeposit } from '../types';
|
||||
import { Request, Response } from 'express';
|
||||
import httpStatus from 'http-status';
|
||||
import pick from '../utils/pick';
|
||||
import logger from '../config/logger';
|
||||
import { ProviderInteractor } from '~/domain/provider/provider.interactor';
|
||||
import { nestApp } from '..';
|
||||
|
||||
export const catchIPN = catchAsync(async (req: Request, res: Response) => {
|
||||
const providerName = req.params.provider;
|
||||
logger.info(`Recieve new IPN for provider ${providerName}`, { body: req.body, source: 'catchIPN' });
|
||||
|
||||
const providerInteractor = nestApp.get(ProviderInteractor);
|
||||
const provider = providerInteractor.getProvider(providerName);
|
||||
|
||||
// Обрабатываем IPN данные
|
||||
await provider.handleIPN(req.body);
|
||||
res.status(200).send();
|
||||
});
|
||||
|
||||
export const createDeposit = catchAsync(async (req: any, res: Response) => {
|
||||
const username = req.user.username;
|
||||
|
||||
const result = await orderService.createDeposit(username, req.body as ICreateDeposit);
|
||||
res.status(http.CREATED).send(result);
|
||||
});
|
||||
|
||||
export const createInitialPayment = catchAsync(async (req: any, res: Response) => {
|
||||
const username = req.user.username;
|
||||
const result = await orderService.createInitialOrder(username);
|
||||
res.status(http.CREATED).send(result);
|
||||
});
|
||||
|
||||
export const getOrders = catchAsync(async (req: Request, res: Response) => {
|
||||
const filter = pick(req.query, ['username', 'id', 'order_num', 'status']);
|
||||
const options = pick(req.query, ['sortBy', 'limit', 'page']);
|
||||
|
||||
const orders = await orderService.getOrders(filter, options);
|
||||
res.status(httpStatus.OK).send(orders);
|
||||
});
|
||||
|
||||
export const getMyOrders = catchAsync(async (req: Request, res: Response) => {
|
||||
const filter = pick(req.query, ['status', 'id']);
|
||||
const options = pick(req.query, ['sortBy', 'limit', 'page']);
|
||||
|
||||
filter.username = req.params.username;
|
||||
|
||||
const orders = await orderService.getOrders(filter, options);
|
||||
res.status(httpStatus.OK).send(orders);
|
||||
});
|
||||
|
||||
export const setStatus = catchAsync(async (req: Request, res: Response) => {
|
||||
const { status, id } = req.body;
|
||||
|
||||
await orderService.setStatus(id, status);
|
||||
|
||||
res.status(httpStatus.OK).send();
|
||||
});
|
||||
@@ -1,11 +1,18 @@
|
||||
import { io, Socket } from 'socket.io-client';
|
||||
import { IAction } from '../types/common';
|
||||
import { SovietContract } from 'cooptypes';
|
||||
import { wsService } from '../services';
|
||||
import logger from '../config/logger';
|
||||
import config from '../config/config';
|
||||
import { MeetDomainInteractor } from '~/domain/meet/interactors/meet.interactor';
|
||||
import type { MeetDecisionDomainInterface } from '~/domain/meet/interfaces/meet-decision-domain.interface';
|
||||
import { DomainToBlockchainUtils } from '~/infrastructure/blockchain/utils/domain-to-blockchain.utils';
|
||||
|
||||
let clientSocket: Socket | undefined;
|
||||
let meetInteractor: MeetDomainInteractor;
|
||||
|
||||
export const setMeetInteractor = (interactor: MeetDomainInteractor): void => {
|
||||
meetInteractor = interactor;
|
||||
};
|
||||
|
||||
// Инициация подключения
|
||||
export const initSocketConnection = (serverUrl: string): void => {
|
||||
@@ -20,7 +27,7 @@ export const initSocketConnection = (serverUrl: string): void => {
|
||||
});
|
||||
|
||||
clientSocket.on('connect_error', (error: Error) => {
|
||||
logger.error('Fail connect to notification server', { source: 'initSocketConnection' });
|
||||
logger.error(`Fail connect to notification server: ${error.message}`, { source: 'initSocketConnection' });
|
||||
});
|
||||
};
|
||||
|
||||
@@ -42,5 +49,39 @@ async function processEvent(event: IAction) {
|
||||
if (event.name === 'createboard') {
|
||||
wsService.updateBoard(event.data);
|
||||
}
|
||||
|
||||
if (event.name === 'newgdecision') {
|
||||
// Обработка события о решении собрания
|
||||
try {
|
||||
// Преобразуем блокчейн-документ в формат ISignedDocumentDomainInterface
|
||||
const decisionDocument = DomainToBlockchainUtils.convertChainDocumentToSignedDocument2(event.data.decision);
|
||||
|
||||
// Нормализуем числовые значения
|
||||
const decisionData: MeetDecisionDomainInterface = {
|
||||
...event.data,
|
||||
signed_ballots: Number(event.data.signed_ballots),
|
||||
quorum_percent: Number(event.data.quorum_percent),
|
||||
results: event.data.results.map((item) => ({
|
||||
...item,
|
||||
question_id: Number(item.question_id),
|
||||
number: Number(item.number),
|
||||
votes_for: Number(item.votes_for),
|
||||
votes_against: Number(item.votes_against),
|
||||
votes_abstained: Number(item.votes_abstained),
|
||||
})),
|
||||
decision: decisionDocument, // Документ решения из блокчейна
|
||||
};
|
||||
|
||||
// Используем интерактор для сохранения данных о завершенном собрании
|
||||
await meetInteractor.processMeetDecision({
|
||||
action: event,
|
||||
decisionData,
|
||||
});
|
||||
|
||||
logger.info(`Processed meet decision for ${event.data.hash}`, { source: 'processEvent' });
|
||||
} catch (error: any) {
|
||||
logger.error(`Error processing meet decision: ${error.message}`, { source: 'processEvent', error });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
// domain/account/account-domain.module.ts
|
||||
|
||||
import { forwardRef, Module } from '@nestjs/common';
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { AccountDomainInteractor } from './interactors/account.interactor';
|
||||
import { AccountDomainService } from '~/domain/account/services/account-domain.service';
|
||||
import { DocumentDomainModule } from '../document/document.module';
|
||||
import { AccountDomainService, ACCOUNT_DOMAIN_SERVICE } from './services/account-domain.service';
|
||||
import { InfrastructureModule } from '~/infrastructure/infrastructure.module';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [forwardRef(() => DocumentDomainModule)],
|
||||
providers: [AccountDomainService, AccountDomainInteractor],
|
||||
exports: [AccountDomainInteractor, AccountDomainService],
|
||||
imports: [InfrastructureModule],
|
||||
providers: [
|
||||
AccountDomainInteractor,
|
||||
AccountDomainService,
|
||||
{
|
||||
provide: ACCOUNT_DOMAIN_SERVICE,
|
||||
useExisting: AccountDomainService,
|
||||
},
|
||||
],
|
||||
exports: [AccountDomainInteractor, AccountDomainService, ACCOUNT_DOMAIN_SERVICE],
|
||||
})
|
||||
export class AccountDomainModule {}
|
||||
|
||||
@@ -17,6 +17,10 @@ import { AccountType } from '~/modules/account/enum/account-type.enum';
|
||||
import { ORGANIZATION_REPOSITORY, OrganizationRepository } from '~/domain/common/repositories/organization.repository';
|
||||
import { INDIVIDUAL_REPOSITORY, IndividualRepository } from '~/domain/common/repositories/individual.repository';
|
||||
import { ENTREPRENEUR_REPOSITORY, EntrepreneurRepository } from '~/domain/common/repositories/entrepreneur.repository';
|
||||
import {
|
||||
SEARCH_PRIVATE_ACCOUNTS_REPOSITORY,
|
||||
SearchPrivateAccountsRepository,
|
||||
} from '~/domain/common/repositories/search-private-accounts.repository';
|
||||
import { IndividualDomainEntity } from '~/domain/branch/entities/individual-domain.entity';
|
||||
import { OrganizationDomainEntity } from '~/domain/branch/entities/organization-domain.entity';
|
||||
import { EntrepreneurDomainEntity } from '~/domain/branch/entities/entrepreneur-domain.entity';
|
||||
@@ -24,6 +28,10 @@ import { ACCOUNT_BLOCKCHAIN_PORT, AccountBlockchainPort } from '../interfaces/ac
|
||||
import { CANDIDATE_REPOSITORY, CandidateRepository } from '../repository/candidate.repository';
|
||||
import { userStatus } from '~/types/user.types';
|
||||
import { sha256 } from '~/utils/sha256';
|
||||
import type {
|
||||
SearchPrivateAccountsInputDomainInterface,
|
||||
PrivateAccountSearchResultDomainInterface,
|
||||
} from '~/domain/common/interfaces/search-private-accounts-domain.interface';
|
||||
|
||||
@Injectable()
|
||||
export class AccountDomainInteractor {
|
||||
@@ -32,6 +40,8 @@ export class AccountDomainInteractor {
|
||||
@Inject(ORGANIZATION_REPOSITORY) private readonly organizationRepository: OrganizationRepository,
|
||||
@Inject(INDIVIDUAL_REPOSITORY) private readonly individualRepository: IndividualRepository,
|
||||
@Inject(ENTREPRENEUR_REPOSITORY) private readonly entrepreneurRepository: EntrepreneurRepository,
|
||||
@Inject(SEARCH_PRIVATE_ACCOUNTS_REPOSITORY)
|
||||
private readonly searchPrivateAccountsRepository: SearchPrivateAccountsRepository,
|
||||
@Inject(ACCOUNT_BLOCKCHAIN_PORT) private readonly accountBlockchainPort: AccountBlockchainPort,
|
||||
@Inject(CANDIDATE_REPOSITORY) private readonly candidateRepository: CandidateRepository
|
||||
) {}
|
||||
@@ -39,7 +49,23 @@ export class AccountDomainInteractor {
|
||||
private readonly logger = new Logger(AccountDomainInteractor.name);
|
||||
|
||||
async updateAccount(data: UpdateAccountDomainInterface): Promise<AccountDomainEntity> {
|
||||
const user = await userService.updateUserByUsername(data.username, data);
|
||||
let user;
|
||||
if (data.individual_data) {
|
||||
const email = data.individual_data.email;
|
||||
user = await userService.updateUserByUsername(data.username, { email });
|
||||
this.individualRepository.create({ ...data.individual_data, username: data.username });
|
||||
} else if (data.organization_data) {
|
||||
const email = data.organization_data.email;
|
||||
user = await userService.updateUserByUsername(data.username, { email });
|
||||
this.organizationRepository.create({ ...data.organization_data, username: data.username });
|
||||
} else if (data.entrepreneur_data) {
|
||||
const email = data.entrepreneur_data.email;
|
||||
user = await userService.updateUserByUsername(data.username, { email });
|
||||
this.entrepreneurRepository.create({ ...data.entrepreneur_data, username: data.username });
|
||||
} else {
|
||||
throw new Error('Не получены входные данные для обновления');
|
||||
}
|
||||
|
||||
const account = await this.getAccount(user.username);
|
||||
|
||||
const result = new AccountDomainEntity(account);
|
||||
@@ -56,11 +82,11 @@ export class AccountDomainInteractor {
|
||||
): Promise<IndividualDomainEntity | OrganizationDomainEntity | EntrepreneurDomainEntity> {
|
||||
switch (accountType) {
|
||||
case AccountType.individual:
|
||||
return this.individualRepository.findByUsername(username);
|
||||
return new IndividualDomainEntity(await this.individualRepository.findByUsername(username));
|
||||
case AccountType.organization:
|
||||
return this.organizationRepository.findByUsername(username);
|
||||
return new OrganizationDomainEntity(await this.organizationRepository.findByUsername(username));
|
||||
case AccountType.entrepreneur:
|
||||
return this.entrepreneurRepository.findByUsername(username);
|
||||
return new EntrepreneurDomainEntity(await this.entrepreneurRepository.findByUsername(username));
|
||||
default:
|
||||
throw new Error(`Неизвестный тип аккаунта: ${accountType}`);
|
||||
}
|
||||
@@ -160,4 +186,21 @@ export class AccountDomainInteractor {
|
||||
throw new HttpException(`Ошибка при регистрации в блокчейне: ${error.message}`, HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Поиск приватных аккаунтов по запросу
|
||||
* @param input Входные данные для поиска
|
||||
* @returns Массив результатов поиска
|
||||
*/
|
||||
async searchPrivateAccounts(
|
||||
input: SearchPrivateAccountsInputDomainInterface
|
||||
): Promise<PrivateAccountSearchResultDomainInterface[]> {
|
||||
this.logger.log(`Поиск приватных аккаунтов по запросу: "${input.query}"`);
|
||||
|
||||
const results = await this.searchPrivateAccountsRepository.searchPrivateAccounts(input);
|
||||
|
||||
this.logger.log(`Найдено ${results.length} приватных аккаунтов`);
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { ENTREPRENEUR_REPOSITORY, EntrepreneurRepository } from '~/domain/common
|
||||
import { ORGANIZATION_REPOSITORY, OrganizationRepository } from '~/domain/common/repositories/organization.repository';
|
||||
import { INDIVIDUAL_REPOSITORY, IndividualRepository } from '~/domain/common/repositories/individual.repository';
|
||||
import type { PrivateAccountDomainInterface } from '../interfaces/private-account-domain.interface';
|
||||
import type { AccountType } from '~/modules/account/enum/account-type.enum';
|
||||
import { AccountType } from '~/modules/account/enum/account-type.enum';
|
||||
import type { IndividualDomainInterface } from '~/domain/common/interfaces/individual-domain.interface';
|
||||
import type { OrganizationDomainInterface } from '~/domain/common/interfaces/organization-domain.interface';
|
||||
import type { EntrepreneurDomainInterface } from '~/domain/common/interfaces/entrepreneur-domain.interface';
|
||||
@@ -48,6 +48,29 @@ export class AccountDomainService {
|
||||
}
|
||||
|
||||
async getAccount(username: string): Promise<AccountDomainEntity> {
|
||||
// Исключение для организации-кооператива
|
||||
// Очень плохие решения из за того что мы содержим ТРИ разных репозитория под приватные данные и не знаем наверняка в каком искать данные заранее!
|
||||
if (config.coopname === username) {
|
||||
const user_account = await this.getUserAccount(username);
|
||||
const blockchain_account = await this.getBlockchainAccount(username);
|
||||
const participant_account = await this.getParticipantAccount(config.coopname, username);
|
||||
const organization_data = await this.organizationRepository.findByUsername(username);
|
||||
const private_account: PrivateAccountDomainInterface = {
|
||||
type: AccountType.organization,
|
||||
individual_data: undefined,
|
||||
organization_data,
|
||||
entrepreneur_data: undefined,
|
||||
};
|
||||
return new AccountDomainEntity({
|
||||
username,
|
||||
user_account,
|
||||
blockchain_account,
|
||||
provider_account: null,
|
||||
participant_account,
|
||||
private_account,
|
||||
});
|
||||
}
|
||||
|
||||
const user_account = await this.getUserAccount(username);
|
||||
const blockchain_account = await this.getBlockchainAccount(username);
|
||||
const participant_account = await this.getParticipantAccount(config.coopname, username);
|
||||
@@ -130,3 +153,5 @@ export class AccountDomainService {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export const ACCOUNT_DOMAIN_SERVICE = Symbol('ACCOUNT_DOMAIN_SERVICE');
|
||||
|
||||
@@ -71,6 +71,10 @@ export class AgendaDomainInteractor {
|
||||
});
|
||||
|
||||
const action = actionResponse?.results?.[0];
|
||||
|
||||
// if (action)
|
||||
//TODO: здесь нужно добавить в action actor_certificate
|
||||
|
||||
if (action) {
|
||||
agenda.push({
|
||||
table: decision,
|
||||
|
||||
+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(
|
||||
@@ -242,7 +242,7 @@ export class BranchDomainInteractor {
|
||||
|
||||
async selectBranch(data: SelectBranchInputDomainInterface): Promise<boolean> {
|
||||
// TODO move it to separate document domain service for validate
|
||||
const document = await this.documentRepository.findByHash(data.document.hash);
|
||||
const document = await this.documentRepository.findByHash(data.document.doc_hash);
|
||||
if (!document) throw new BadRequestException('Документ не найден');
|
||||
|
||||
if (data.document.meta.registry_id != Cooperative.Registry.SelectBranchStatement.registry_id)
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import type { IndividualDomainInterface } from './individual-domain.interface';
|
||||
import type { EntrepreneurDomainInterface } from './entrepreneur-domain.interface';
|
||||
import type { OrganizationDomainInterface } from './organization-domain.interface';
|
||||
|
||||
/**
|
||||
* Интерфейс для входных данных поиска приватных аккаунтов
|
||||
*/
|
||||
export interface SearchPrivateAccountsInputDomainInterface {
|
||||
query: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Тип данных результата поиска - union type всех возможных типов приватных данных
|
||||
*/
|
||||
export type PrivateAccountSearchDataDomainInterface =
|
||||
| IndividualDomainInterface
|
||||
| EntrepreneurDomainInterface
|
||||
| OrganizationDomainInterface;
|
||||
|
||||
/**
|
||||
* Интерфейс результата поиска приватных аккаунтов
|
||||
*/
|
||||
export interface PrivateAccountSearchResultDomainInterface {
|
||||
type: 'individual' | 'entrepreneur' | 'organization';
|
||||
data: PrivateAccountSearchDataDomainInterface;
|
||||
score?: number;
|
||||
highlightedFields?: string[];
|
||||
}
|
||||
@@ -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'); // Создаем уникальный токен
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import type {
|
||||
SearchPrivateAccountsInputDomainInterface,
|
||||
PrivateAccountSearchResultDomainInterface,
|
||||
} from '../interfaces/search-private-accounts-domain.interface';
|
||||
|
||||
/**
|
||||
* Доменный репозиторий для поиска приватных аккаунтов
|
||||
*/
|
||||
export interface SearchPrivateAccountsRepository {
|
||||
/**
|
||||
* Поиск приватных аккаунтов по запросу
|
||||
* @param input Входные данные для поиска
|
||||
* @returns Массив результатов поиска
|
||||
*/
|
||||
searchPrivateAccounts(
|
||||
input: SearchPrivateAccountsInputDomainInterface
|
||||
): Promise<PrivateAccountSearchResultDomainInterface[]>;
|
||||
}
|
||||
|
||||
export const SEARCH_PRIVATE_ACCOUNTS_REPOSITORY = Symbol('SearchPrivateAccountsRepository');
|
||||
+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 {}
|
||||
|
||||
@@ -5,17 +5,18 @@ import type { DocumentDomainEntity } from '../entity/document-domain.entity';
|
||||
import type { PaginationResultDomainInterface } from '~/domain/common/interfaces/pagination.interface';
|
||||
import type { DocumentPackageAggregateDomainInterface } from '../interfaces/document-package-aggregate-domain.interface';
|
||||
import type { GetDocumentsInputDomainInterface } from '../interfaces/get-documents-input-domain.interface';
|
||||
import { toDotNotation } from '~/utils/toDotNotation';
|
||||
import { getActions } from '~/utils/getFetch';
|
||||
import { SovietContract } from 'cooptypes';
|
||||
import { DocumentPackageAggregator } from '../aggregators/document-package.aggregator';
|
||||
import { DocumentAggregator } from '../aggregators/document.aggregator';
|
||||
import type { DocumentAggregateDomainInterface } from '../interfaces/document-domain-aggregate.interface';
|
||||
import type { ISignedDocumentDomainInterface } from '../interfaces/signed-document-domain.interface';
|
||||
|
||||
@Injectable()
|
||||
export class DocumentDomainInteractor {
|
||||
constructor(
|
||||
private readonly documentDomainService: DocumentDomainService,
|
||||
@Inject(forwardRef(() => DocumentPackageAggregator))
|
||||
private readonly documentPackageAggregator: DocumentPackageAggregator
|
||||
private readonly documentPackageAggregator: DocumentPackageAggregator,
|
||||
private readonly documentAggregator: DocumentAggregator
|
||||
) {}
|
||||
|
||||
public async generateDocument(data: GenerateDocumentDomainInterfaceWithOptions): Promise<DocumentDomainEntity> {
|
||||
@@ -46,4 +47,17 @@ export class DocumentDomainInteractor {
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Строит агрегат документа
|
||||
* @param signedDocument Подписанный документ
|
||||
* @returns Агрегат документа или null
|
||||
*/
|
||||
async buildDocumentAggregate(
|
||||
signedDocument: ISignedDocumentDomainInterface | null | undefined
|
||||
): Promise<DocumentAggregateDomainInterface | null> {
|
||||
if (!signedDocument) return null;
|
||||
|
||||
return await this.documentAggregator.buildDocumentAggregate(signedDocument);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user