Compare commits
126 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0f617cf1a1 | |||
| f8881cfcde | |||
| 0f5c7ad108 | |||
| b1ccb70b5b | |||
| dedfd68980 | |||
| e47ef47283 | |||
| a7e675f5ad | |||
| 67f1922ca2 | |||
| 702eb927bd | |||
| a6b15f0749 | |||
| bf80a839fa | |||
| 585afb16eb | |||
| 7ea382058b | |||
| a531bb1ab4 | |||
| cd08236dd6 | |||
| 1d72bd3960 | |||
| 3c08215671 | |||
| 4c45d1d701 | |||
| 8e10c57af2 | |||
| 0aeeb69581 | |||
| db86a8e001 | |||
| 61e33a2959 | |||
| a68068ded4 | |||
| 99dc260013 | |||
| e1f4fa066d | |||
| 0d60f4004d | |||
| 297055cb7c | |||
| be7c5fd864 | |||
| 818fa8b0a5 | |||
| 4ea43712d2 | |||
| d023beee30 | |||
| 2d1bef7087 | |||
| 3d3ba56282 | |||
| 729816ed26 | |||
| c1e6b1a433 | |||
| 1e8e5c4412 | |||
| 6c72942a30 | |||
| 7dff06d5b9 | |||
| 830b09ea2c | |||
| 8f2454d4fc | |||
| 9953f7e980 | |||
| fcb816b0e6 | |||
| b294c61b4e | |||
| 6435b50c72 | |||
| 7c804da6b2 | |||
| eaae515860 | |||
| 9bcfd3a654 | |||
| 109f4aaf98 | |||
| beb87b0c00 | |||
| 947077d011 | |||
| 3297616ad1 | |||
| 90c830fdb1 | |||
| 130c544191 | |||
| 63dcdde872 | |||
| e1a496759b | |||
| 99283dfdd7 | |||
| adee9ee032 | |||
| aaf3b5b98f | |||
| 3dbad0ff30 | |||
| 615eba3a84 | |||
| e7de054de2 | |||
| 6f911605d9 | |||
| 00a8461b5b | |||
| 7154c7f0e0 | |||
| 0f07666aeb | |||
| 489dc482e8 | |||
| 55c4759780 | |||
| 16b4fe0080 | |||
| 9cd995c5f2 | |||
| e60d6a5d67 | |||
| b274af05c3 | |||
| 40e725f2ed | |||
| 0f091eefc9 | |||
| 2b00c58e9b | |||
| 6d9681fb8a | |||
| acc4d27677 | |||
| ffe581a92b | |||
| 6e643515ce | |||
| 1cc7aca53f | |||
| e5111622a6 | |||
| 99b98cb5c9 | |||
| 20447cfb46 | |||
| 723a1813c9 | |||
| 04d52a0631 | |||
| 097d18cd56 | |||
| 5043995d14 | |||
| 804cfc8b72 | |||
| a0dc6f3333 | |||
| a53957271e | |||
| abae483dd3 | |||
| 4418e09211 | |||
| ca79457c19 | |||
| d2cd947b9c | |||
| ea28285639 | |||
| 6174ddea9a | |||
| ce56adc3c8 | |||
| 59334bb13a | |||
| 21cd1f5217 | |||
| 515df53c96 | |||
| cbf7f48c44 | |||
| ed37aeb615 | |||
| ea89558696 | |||
| c0bad75a37 | |||
| bdc227f110 | |||
| a9c3a1b6de | |||
| f0e7b0b63a | |||
| a709e2f1e3 | |||
| 3a34bd74db | |||
| b755cb715a | |||
| 42753fae0a | |||
| d8b8fa2d6e | |||
| a75277e0b8 | |||
| f95b83674b | |||
| bb13e780af | |||
| 6e80d5187c | |||
| db9b704d1d | |||
| 65a2b5361c | |||
| 7bca73cd7b | |||
| 59383014be | |||
| 60a5296f5f | |||
| eb4fe333a7 | |||
| 8d06d6d817 | |||
| b6afb536b6 | |||
| 790047fa8a | |||
| baf55b6b6d | |||
| 92d85244f1 |
@@ -0,0 +1,99 @@
|
||||
# AGENTS.md
|
||||
|
||||
## Cursor Cloud specific instructions
|
||||
|
||||
### Обзор
|
||||
|
||||
Монорепозиторий «Цифровой Кооператив» (monocoop) — платформа управления кооперативами на блокчейне EOSIO. pnpm v9 + Lerna. Node.js v20.
|
||||
|
||||
### Сервисы
|
||||
|
||||
| Компонент | Контейнер | Порт | Описание |
|
||||
|-----------|-----------|------|----------|
|
||||
| controller | coopback | 2998 | NestJS GraphQL API |
|
||||
| desktop | desktop | 2999 | Vue 3 + Quasar SPA |
|
||||
| parser | cooparser | 4000 | Индексация блокчейна через SHiP |
|
||||
| blockchain | node | 8888, 8070 | EOSIO node + State History Plugin |
|
||||
| MongoDB | mongo | 27017 | Основная БД (replica set) |
|
||||
| Redis | monoredis | 6379 | Кэш и стримы |
|
||||
| PG | (см. compose) | 5532→5432 | Реляционная БД |
|
||||
|
||||
### Полный перезапуск (одна команда)
|
||||
|
||||
```
|
||||
pnpm run reboot
|
||||
```
|
||||
|
||||
Делает: останавливает контейнеры → чистит blockchain data и volumes → поднимает инфру → ждёт готовности → `pnpm run boot` → запускает parser и controller.
|
||||
|
||||
### Первоначальная настройка Cloud-окружения
|
||||
|
||||
1. **`/etc/hosts`** — обязательно для boot (запускается на хосте, обращается к MongoDB по docker hostname):
|
||||
```
|
||||
echo "127.0.0.1 mongo" | sudo tee -a /etc/hosts
|
||||
echo "127.0.0.1 monoredis" | sudo tee -a /etc/hosts
|
||||
```
|
||||
|
||||
2. **WeasyPrint** — системная зависимость для генерации PDF:
|
||||
```
|
||||
sudo apt-get install -y python3 python3-venv libpango-1.0-0 libcairo2 libffi-dev libjpeg-dev libopenjp2-7-dev libharfbuzz-dev
|
||||
sudo python3 -m venv /opt/weasyprint-venv && sudo /opt/weasyprint-venv/bin/pip install WeasyPrint==67
|
||||
sudo ln -sf /opt/weasyprint-venv/bin/weasyprint /usr/local/bin/weasyprint
|
||||
```
|
||||
|
||||
3. **Контракты** (test-режим — позволяет boot с 1 членом совета):
|
||||
```
|
||||
cd components/contracts && sudo rm -rf build && bash build-all.sh test
|
||||
```
|
||||
|
||||
4. **Shared-библиотеки** (порядок важен):
|
||||
```
|
||||
pnpm --filter cooptypes run build
|
||||
pnpm --filter @coopenomics/factory run build
|
||||
pnpm --filter @coopenomics/sdk run build
|
||||
pnpm --filter @coopenomics/notifications run build
|
||||
```
|
||||
|
||||
5. **`.env` файлы** — скопировать из `.env-example`, адаптировать hostnames:
|
||||
- Controller/Parser (в Docker): хосты по именам контейнеров из docker-compose (порт БД 5432)
|
||||
- Boot (на хосте): `127.0.0.1`, PG порт `5532`, mongo через `/etc/hosts`
|
||||
- Desktop: `127.0.0.1`
|
||||
- **CHAIN_ID**: берётся из `curl http://localhost:8888/v1/chain/get_info` после старта ноды
|
||||
- Controller требует `VAPID_PUBLIC_KEY` и `VAPID_PRIVATE_KEY`
|
||||
|
||||
6. **Запуск**: `pnpm run reboot`, затем `docker compose up -d --force-recreate coopback cooparser` (если .env менялись)
|
||||
|
||||
### Запуск тестов
|
||||
|
||||
- **Factory** (`components/factory`): нужен только MongoDB. Запуск:
|
||||
```
|
||||
NODE_ENV=test SOURCE=local MONGO_URI=$MONGO_URI SKIP_BLOCK_FETCH=TRUE pnpm --filter @coopenomics/factory test
|
||||
```
|
||||
`MONGO_URI` по умолчанию: `mongodb://<host>:27017/cooperative-x`.
|
||||
- **Boot** (`components/boot`): требует полный EOSIO blockchain + MongoDB + PG. Запуск после `pnpm run reboot`:
|
||||
```
|
||||
pnpm --filter @coopenomics/boot test
|
||||
```
|
||||
- **Duplicate transaction** — в boot-тестах EOSIO отклоняет транзакции с одинаковым хешем (TAPOS block + action data). При повторном вызове `refreshSegment` для того же участника — добавить `await sleep(500)` перед ним. Паттерн уже используется (см. комментарий на строке ~720 capital.test.ts).
|
||||
|
||||
### Маркетплейс (cooplace extension)
|
||||
|
||||
**Архитектура:**
|
||||
- `cooplace` = бэкенд маркетплейса (карточки, категории, настройки, match, cycles)
|
||||
- `marketplace` = фронтенд расширение (витрина, заказы — использует API cooplace)
|
||||
- `market-admin` = фронтенд админки (настройки, модерация)
|
||||
|
||||
**Ключевой принцип:** Каждая встречная заявка сразу в блокчейн → блокировка средств. Карточки в БД, заявки в blockchain.
|
||||
|
||||
**Циклы:** min_units/deadline — порог для supply, не для match. При истечении → cancel через blockchain.
|
||||
|
||||
**Настройки** (MarketplaceSettings): lead_request_policy (offers_only/orders_only/both), publish_access_policy (all_members/whitelist/council_only), moderation, cycles, delivery types.
|
||||
|
||||
### Критические gotchas
|
||||
|
||||
- **SHiP порт 8070** — `state-history-endpoint = 0.0.0.0:8070` в config.ini. Парсер: `SHIP=ws://node:8070`.
|
||||
- **Парсер START_BLOCK**: при `START_BLOCK=1` на чистой БД стартует с HEAD и делает частичную инициализацию. Для полного replay: временно `START_BLOCK=2`, после первого запуска вернуть `1`.
|
||||
- **SSR desktop в dev** — расширения не рендерятся из-за Pinia SSR-сериализации компонентов. Dev — SPA (`quasar dev`), production build SSR работает.
|
||||
- **Тестовые учётные данные**: email `ivanov@example.com`, ключ — дефолтный EOSIO dev key (см. `components/boot/.env-example`), пользователь `ant` (председатель).
|
||||
- **Docker hostnames**: без `network_mode: host` — контейнеры обращаются друг к другу по именам контейнеров из docker-compose.
|
||||
- **Установка пакетов**: только через фильтр — `pnpm add <pkg> --filter <component>`.
|
||||
+367
@@ -0,0 +1,367 @@
|
||||
# MARKET-LOGIC.md — Бизнес-процессы маркетплейса
|
||||
|
||||
## 2А. БИЗНЕС-ПРОЦЕССЫ СМАРТ-КОНТРАКТОВ
|
||||
|
||||
### Основные бизнес-сценарии
|
||||
|
||||
**Сценарий 1: Поставка-приобретение имущества**
|
||||
- **Описание:** Обмен имущества между пайщиками через кооператив с блокировкой средств и документооборотом
|
||||
- **Участники:** Заказчик, Поставщик, Совет кооператива, Председатель КУ
|
||||
- **Бизнес-ценность:** Основной процесс кооперативного маркетплейса
|
||||
|
||||
**Сценарий 1А.** Прямая поставка (OFFER→ORDER) — поставщик публикует, заказчик откликается
|
||||
**Сценарий 1Б.** Обратная поставка (ORDER→OFFER) — заказчик публикует, поставщики откликаются
|
||||
**Сценарий 1В.** Из запасов кооператива (COOPSTOCK) — имущество уже на балансе
|
||||
|
||||
**Сценарий 2: Гарантийный возврат**
|
||||
- **Описание:** Возврат бракованного имущества в течение гарантийного срока
|
||||
- **Участники:** Заказчик, Поставщик, Председатель КУ, Совет
|
||||
- **Бизнес-ценность:** Защита интересов пайщика и качества имущества
|
||||
|
||||
**Сценарий 3: Уничтожение/перепредложение**
|
||||
- **Описание:** Утилизация просроченного или перепродажа по новой цене
|
||||
- **Участники:** Председатель КУ, Совет
|
||||
- **Бизнес-ценность:** Управление складскими запасами и минимизация потерь
|
||||
|
||||
**Сценарий 4: Транспортировка**
|
||||
- **Описание:** Перевозка имущества между КУ группами
|
||||
- **Участники:** Председатель КУ отправителя, Водитель, Председатель КУ получателя
|
||||
- **Бизнес-ценность:** Логистика распределённой кооперативной сети
|
||||
|
||||
---
|
||||
|
||||
### Процесс 1А: Прямая поставка (OFFER→ORDER)
|
||||
|
||||
**Предусловия:**
|
||||
- Поставщик создал карточку товара в БД (статус: `published`)
|
||||
- Заказчик нашёл карточку на витрине и решил заказать
|
||||
- У заказчика достаточно средств на цифровом кошельке
|
||||
|
||||
**Последовательность шагов:**
|
||||
|
||||
#### Шаг 1: orderoffer — Заказчик создаёт заявку
|
||||
|
||||
- **Предусловие:** Карточка товара в БД (published). При создании заявки происходит **match** — заявка публикуется в блокчейн
|
||||
- **Исполнитель:** Заказчик (через контроллер, авторизация от кооператива)
|
||||
- **Подписываемые документы:** Заявление на конвертацию из кошелька (convert_in)
|
||||
- **Проводки по кошелькам:**
|
||||
- Списать у заказчика `total_cost` из ЦПП «Цифровой Кошелёк»
|
||||
- Начислить заказчику `total_cost` в ЦПП «Маркетплейс»
|
||||
- Заблокировать у заказчика `total_cost` в ЦПП «Маркетплейс»
|
||||
- **Статус заявки:** `active`
|
||||
- **Параметры:** `delivery_type` (internal/external), `contribution_type` (share/member)
|
||||
|
||||
#### Шаг 2: accept — Поставщик принимает заявку
|
||||
|
||||
- **Исполнитель:** Поставщик
|
||||
- **Подписываемые документы:**
|
||||
- Заявление на конвертацию в кошелёк (convert_out)
|
||||
- Заявление на имущественный паевой взнос (contribution_statement)
|
||||
- **Проводки:** Нет
|
||||
- **Эффект:** Создаётся **одно** заявление в совет — `authcontrib` (авторизация взноса)
|
||||
- **Статус заявки:** `accepted`
|
||||
|
||||
#### Шаг 3: authcontrib — Совет авторизует взнос
|
||||
|
||||
- **Исполнитель:** Совет кооператива (автоматически через soviet контракт)
|
||||
- **Подписываемые документы:** Решение совета об авторизации взноса
|
||||
- **Проводки:** Нет
|
||||
- **Статус заявки:** `authorized` — поставщик может начинать поставку
|
||||
|
||||
#### Шаг 4: supply — Поставщик поставляет имущество на КУ
|
||||
|
||||
- **Исполнитель:** Поставщик
|
||||
- **Подписываемые документы:** Акт поставки (supply_act)
|
||||
- **Проводки:** Нет
|
||||
- **Статус заявки:** `supplied1`
|
||||
|
||||
#### Шаг 5: supplcnf — Председатель КУ подтверждает поставку
|
||||
|
||||
- **Исполнитель:** Председатель КУ поставщика
|
||||
- **Подписываемые документы:** Акт подтверждения поставки (supply_act_conf)
|
||||
- **Проводки по кошелькам:**
|
||||
- Начислить поставщику `base_cost` в ЦПП «Маркетплейс»
|
||||
- Заблокировать у поставщика `base_cost` в ЦПП «Маркетплейс»
|
||||
- **Проводки по ledger:**
|
||||
- Увеличить паевой фонд (счёт 80) на `total_cost`
|
||||
- **Статус заявки:** `supplied2`, имущество на складе КУ
|
||||
|
||||
#### Шаг 6: [Транспортировка] — При необходимости (см. Сценарий 4)
|
||||
|
||||
#### Шаг 7: delivered — Готово к выдаче
|
||||
|
||||
- **Исполнитель:** Председатель КУ получателя
|
||||
- **Подписываемые документы:** Нет
|
||||
- **Проводки:** Нет
|
||||
- **Статус заявки:** `delivered`
|
||||
|
||||
#### Шаг 8: reqreturn — Заказчик запрашивает возврат
|
||||
|
||||
- **Исполнитель:** Заказчик
|
||||
- **Подписываемые документы:** Заявление на возврат паевого взноса имуществом (return_statement) — с актуальными данными о весе/составе
|
||||
- **Проводки:** Нет
|
||||
- **Эффект:** Создаётся заявление в совет — `authreturn`
|
||||
- **Статус заявки:** `reqreturn`
|
||||
|
||||
#### Шаг 9: authreturn — Совет авторизует возврат
|
||||
|
||||
- **Исполнитель:** Совет кооператива
|
||||
- **Подписываемые документы:** Решение совета об авторизации возврата
|
||||
- **Проводки:** Нет
|
||||
- **Статус заявки:** `retauthorized`
|
||||
|
||||
#### Шаг 10: receive — Председатель КУ передаёт имущество
|
||||
|
||||
- **Исполнитель:** Председатель КУ
|
||||
- **Подписываемые документы:** Акт приёма-передачи (receive_act)
|
||||
- **Проводки:** Нет
|
||||
- **Статус заявки:** `received1`
|
||||
|
||||
#### Шаг 11: receivecnf — Заказчик подтверждает получение
|
||||
|
||||
- **Исполнитель:** Заказчик
|
||||
- **Подписываемые документы:** Акт подтверждения получения (receive_act_conf)
|
||||
- **Проводки по кошелькам:**
|
||||
- Списать заблокированный баланс заказчика `total_cost` из ЦПП «Маркетплейс»
|
||||
- **Проводки по ledger:**
|
||||
- Уменьшить паевой фонд (счёт 80) на `base_cost`
|
||||
- **Статус заявки:** `received2`
|
||||
- **Эффект:** Устанавливается `warranty_delay_until` (текущее время + гарантийный срок)
|
||||
|
||||
#### Шаг 12: complete — Завершение после гарантии
|
||||
|
||||
- **Исполнитель:** Система (после истечения `warranty_delay_until`)
|
||||
- **Проводки по кошелькам:**
|
||||
- Списать заблокированный баланс поставщика `base_cost` из ЦПП «Маркетплейс»
|
||||
- Начислить поставщику `base_cost` в ЦПП «Цифровой Кошелёк»
|
||||
- **Статус:** Заявка удаляется из блокчейна
|
||||
|
||||
**Постусловия:**
|
||||
- Заказчик получил имущество
|
||||
- Поставщик получил средства на кошелёк
|
||||
- Членские взносы распределены по фондам кооператива
|
||||
|
||||
---
|
||||
|
||||
### Процесс 1Б: Обратная поставка (ORDER→OFFER)
|
||||
|
||||
**Предусловия:**
|
||||
- Заказчик создал карточку заказа в БД (статус: `published`, тип: `order`)
|
||||
- Поставщик нашёл заказ и готов поставить
|
||||
|
||||
**Последовательность шагов:**
|
||||
|
||||
#### Шаг 1: createorder — Заказчик публикует заказ
|
||||
|
||||
- **Предусловие:** Карточка заказа в БД. При публикации — **match** в блокчейн
|
||||
- **Проводки:** Списание + блокировка `total_cost` заказчика (аналогично orderoffer)
|
||||
- **Статус:** `active`
|
||||
|
||||
#### Шаг 2: respondoffer — Поставщик откликается
|
||||
|
||||
- **Исполнитель:** Поставщик
|
||||
- **Подписываемые документы:** Заявление на взнос + конвертация
|
||||
- **Эффект:** Создаётся заявление в совет `authcontrib`
|
||||
- **Статус предложения:** `accepted`
|
||||
|
||||
#### Шаги 3-12: Аналогичны Процессу 1А (authcontrib → complete)
|
||||
|
||||
---
|
||||
|
||||
### Процесс 1В: Из запасов кооператива (COOPSTOCK)
|
||||
|
||||
**Предусловия:**
|
||||
- Имущество уже на балансе кооператива (на складе КУ)
|
||||
- Председатель КУ создаёт предложение coopstock
|
||||
|
||||
**Последовательность шагов:**
|
||||
|
||||
#### Шаг 1: coopstock — Создание предложения
|
||||
|
||||
- **Исполнитель:** Председатель КУ
|
||||
- **Проводки:** Нет (имущество уже на балансе)
|
||||
- **Статус:** `delivered` (сразу готово к выдаче)
|
||||
|
||||
#### Шаг 2: acceptstock — Заказчик принимает
|
||||
|
||||
- **Исполнитель:** Заказчик
|
||||
- **Подписываемые документы:** Конвертация + заявление на возврат
|
||||
- **Проводки:** Блокировка `total_cost` заказчика
|
||||
- **Эффект:** Сразу создаётся заявление в совет `authreturn`
|
||||
- **Статус:** `reqreturn`
|
||||
|
||||
#### Шаги 3-6: authreturn → receive → receivecnf → complete (аналогично 1А шаги 9-12)
|
||||
|
||||
**Особенности:**
|
||||
- Пропущены шаги accept, authcontrib, supply, supplcnf — не нужны
|
||||
- Нет проводок по паевому фонду при поставке (имущество уже на балансе)
|
||||
|
||||
---
|
||||
|
||||
### Процесс 2: Гарантийный возврат
|
||||
|
||||
**Предусловия:**
|
||||
- Заявка в статусе `received2` (имущество получено)
|
||||
- Не истёк `warranty_delay_until`
|
||||
- Заказчик обнаружил дефект
|
||||
|
||||
**Последовательность шагов:**
|
||||
|
||||
#### Шаг 1: dispute — Заказчик подаёт претензию
|
||||
|
||||
- **Подписываемые документы:** Претензия (wdispute) + фото/видео
|
||||
- **Эффект:** Деньги поставщика дополнительно блокируются
|
||||
|
||||
#### Шаг 2: Рассмотрение на КУ (вне контракта)
|
||||
|
||||
- **Исполнитель:** Председатель КУ
|
||||
- **Действия:** Осмотр, подтверждение/отклонение претензии
|
||||
|
||||
#### Шаг 3: wauthorize — Совет авторизует возврат
|
||||
|
||||
- **Подписываемые документы:** Решение о возврате (wreturn_auth) + решение о выдаче поставщику (wsupply_auth)
|
||||
|
||||
#### Шаг 4: wreturn — Возврат имущества в кооператив
|
||||
|
||||
- **Проводки:** Разблокировка средств заказчика, начисление на кошелёк
|
||||
|
||||
#### Шаг 5: woffer — Предложение имущества поставщику
|
||||
|
||||
#### Шаг 6: waccept — Поставщик принимает/отказывается
|
||||
|
||||
**Альтернативные потоки:**
|
||||
- **Отклонение претензии:** Заказчик забирает имущество обратно
|
||||
- **Поставщик не забрал:** Имущество перепредлагается (→ reoffer) или уничтожается (→ destroy)
|
||||
|
||||
---
|
||||
|
||||
### Процесс 3: Уничтожение имущества
|
||||
|
||||
**Предусловия:**
|
||||
- Заявка в статусе `delivered` или `supplied2`
|
||||
- Истёк `deadline_for_receipt` (заказчик не пришёл)
|
||||
|
||||
**Шаг 1: destroy — Уничтожение**
|
||||
|
||||
- **Исполнитель:** Председатель КУ (chairman)
|
||||
- **Подписываемые документы:** Акт уничтожения (destruction_act)
|
||||
- **Проводки:**
|
||||
- Возврат заказчику: `total_cost - cancellation_fee` → разблокировать и вернуть в кошелёк
|
||||
- Штраф `cancellation_fee` → в фонд членских взносов (spreadamount)
|
||||
- Поставщику: `base_cost` → разблокировать и вернуть в кошелёк
|
||||
- **Эффект:** Заявка удаляется из блокчейна
|
||||
|
||||
---
|
||||
|
||||
### Процесс 3Б: Перепредложение (reoffer)
|
||||
|
||||
**Предусловия:**
|
||||
- Аналогичны destroy, но срок годности НЕ истёк
|
||||
|
||||
**Шаг 1: reoffer — Перепредложение по новой цене**
|
||||
|
||||
- **Исполнитель:** Председатель КУ
|
||||
- **Проводки:** Аналогичны destroy (возврат средств)
|
||||
- **Эффект:** Старая заявка удаляется, создаётся новая типа `coopstock` со статусом `delivered`
|
||||
|
||||
---
|
||||
|
||||
### Процесс 4: Транспортировка (Shipment)
|
||||
|
||||
**Предусловия:**
|
||||
- Имущество на складе КУ (статус `supplied2` или `shiprecvd`)
|
||||
- Нужно доставить на другой КУ
|
||||
|
||||
**Последовательность шагов:**
|
||||
|
||||
#### Шаг 1: createship — Создание перевозки
|
||||
- **Исполнитель:** Представитель КУ отправителя
|
||||
- **Документ:** Акт передачи (shsendact)
|
||||
- **Статус перевозки:** `loading`
|
||||
|
||||
#### Шаг 2: signbydriver — Подпись водителя
|
||||
- **Исполнитель:** Водитель-пайщик
|
||||
- **Документ:** Акт приёма (shloadact)
|
||||
- **Эффект:** Товары снимаются со склада
|
||||
- **Статус:** `transit`
|
||||
|
||||
#### Шаг 3: arrived — Прибытие
|
||||
- **Исполнитель:** Водитель
|
||||
- **Документ:** Акт доставки (sharriveact)
|
||||
- **Статус:** `arrived`
|
||||
|
||||
#### Шаг 4: receiveshipm — Приём на складе
|
||||
- **Исполнитель:** Представитель КУ получателя
|
||||
- **Документ:** Акт приёма на складе (shrecvact)
|
||||
- **Эффект:** Товары ставятся на склад КУ назначения, перевозка удаляется
|
||||
- **Статус заявок:** `shiprecvd`
|
||||
|
||||
**Альтернативный поток:** retransport — промежуточная перегрузка на другой маршрут
|
||||
|
||||
---
|
||||
|
||||
### Связи между процессами
|
||||
|
||||
- **Процесс 1 → Процесс 4:** После `supplcnf` (шаг 5) товар может пойти на транспортировку перед `delivered`
|
||||
- **Процесс 1 → Процесс 2:** После `receivecnf` (шаг 11) возможен гарантийный возврат до истечения `warranty_delay_until`
|
||||
- **Процесс 2 → Процесс 3:** Если поставщик не забирает возвращённое имущество → destroy/reoffer
|
||||
- **Процесс 1 → Процесс 3:** Если заказчик не приходит за товаром → destroy/reoffer
|
||||
- **Процесс 3Б → Процесс 1В:** reoffer создаёт coopstock → новый цикл 1В
|
||||
|
||||
---
|
||||
|
||||
## Ключевой принцип: Match = Блокчейн
|
||||
|
||||
**Каждая встречная заявка сразу публикуется в блокчейн.**
|
||||
|
||||
Карточки хранятся в БД (draft → moderation → published).
|
||||
Но как только появляется встречная заявка — она немедленно уходит в блокчейн.
|
||||
Блокировка средств — дело смарт-контракта, не контроллера.
|
||||
|
||||
### Циклы поставки (min_units + cycle_deadline)
|
||||
|
||||
Циклы НЕ влияют на match. Они определяют:
|
||||
- **Когда поставщик начинает поставку** (supply) — при наборе min_units
|
||||
- **Когда цикл истекает** — cancel всех заявок через блокчейн → возврат средств
|
||||
|
||||
Поток:
|
||||
1. Карточка published, `min_units: 100`, `cycle_deadline: 7 февраля`
|
||||
2. Заказчик 1 → order на 30 ед. → **сразу в блокчейн** → средства заблокированы
|
||||
3. Заказчик 2 → order на 50 ед. → **сразу в блокчейн** → средства заблокированы
|
||||
4. Заказчик 3 → order на 25 ед. → **сразу в блокчейн** → средства заблокированы
|
||||
5. Итого 105 ≥ 100 → поставщику разрешено начать supply
|
||||
6. Если бы до 7 февраля набрали только 70 → cancel всех заявок → возврат через блокчейн → новый цикл
|
||||
|
||||
---
|
||||
|
||||
### Бизнес-правила и ограничения
|
||||
|
||||
**Правило 1: Блокировка средств при заказе**
|
||||
- **Описание:** Средства заказчика блокируются в момент создания заявки в блокчейне (match)
|
||||
- **Применение:** orderoffer, createorder, acceptstock
|
||||
- **Последствия нарушения:** Заявка не может быть создана без достаточных средств
|
||||
|
||||
**Правило 2: Одно заявление в совет при принятии**
|
||||
- **Описание:** При accept создаётся только authcontrib (на взнос). authreturn — перед получением
|
||||
- **Применение:** accept, reqreturn
|
||||
- **Обоснование:** Заявление на возврат содержит точные данные (вес), доступные после доставки
|
||||
|
||||
**Правило 3: Тип доставки определяет маршрут**
|
||||
- **Описание:** `delivery_type: internal` — между КУ через Shipment, `external` — через внешний сервис (СДЭК)
|
||||
- **Применение:** Определяется в карточке при создании
|
||||
|
||||
**Правило 4: Тип взноса определяет проводки**
|
||||
- **Описание:** `contribution_type: share` — паевой взнос (возврат имуществом), `member` — членский взнос (кооператив покупает)
|
||||
- **Применение:** Влияет на проводки при complete
|
||||
|
||||
**Правило 5: Гарантийный период**
|
||||
- **Описание:** `warranty_delay_until = received_at + warranty_period_secs`. До истечения — complete невозможен, dispute возможен
|
||||
- **Применение:** complete, dispute
|
||||
|
||||
**Правило 6: Карточки в БД, заявки в блокчейне**
|
||||
- **Описание:** Карточки хранятся в PostgreSQL (draft → moderation → published). Каждая встречная заявка сразу публикуется в блокчейн — средства блокируются смарт-контрактом
|
||||
- **Применение:** MatchService при каждой встречной заявке
|
||||
|
||||
**Правило 7: Циклы — порог для supply, не для match**
|
||||
- **Описание:** min_units определяет когда поставщику разрешено начать supply. cycle_deadline — когда cancel при ненаборе. Каждый заказ сразу в блокчейн независимо от цикла
|
||||
- **Применение:** CycleService (cron каждые 5 мин)
|
||||
@@ -1,135 +1,120 @@
|
||||
# Цифровой Кооператив
|
||||
|
||||
Система управления взаимоотношениями в кооперативе. Включает в себя полный комплект программного обеспечения для подключения к платформе "Кооперативная Экономика" и управления взаимоотношениями с пайщиками в кооперативе и самим кооперативом на основе смарт-контрактов и простой электронной подписи.
|
||||
<!-- badges -->
|
||||

|
||||

|
||||

|
||||
|
||||
## Ветки
|
||||
Платформа «Цифровой Кооператив» — комплексное программное обеспечение для управления кооперативными организациями на основе блокчейна EOSIO. Система обеспечивает полный цикл управления кооперативом: от регистрации пайщиков и электронного документооборота до проведения собраний и финансового учёта. Построена на принципах прозрачности, децентрализации и простой электронной подписи.
|
||||
|
||||
- Для разработки используется ветка `dev`.
|
||||
- Для стейджа используется ветка `testnet`.
|
||||
- Для продакшена используется ветка `main`.
|
||||
Проект является частью экосистемы [Кооперативная Экономика](https://coopenomics.world).
|
||||
|
||||
## Установка
|
||||
## Архитектура
|
||||
|
||||
```sh
|
||||
| Компонент | Пакет | Описание |
|
||||
|-----------|-------|----------|
|
||||
| [boot](components/boot) | `@coopenomics/boot` | CLI для инициализации и управления блокчейн-инфраструктурой |
|
||||
| [cleos](components/cleos) | `@coopenomics/cleos` | Утилита командной строки для работы с блокчейн-кошельком |
|
||||
| [contracts](components/contracts) | `@coopenomics/contracts` | Смарт-контракты EOSIO на C++ |
|
||||
| [controller](components/controller) | `@coopenomics/controller` | GraphQL API сервер (NestJS) |
|
||||
| [cooptypes](components/cooptypes) | `cooptypes` | Общие типы и интерфейсы блокчейн-контрактов |
|
||||
| [desktop](components/desktop) | `@coopenomics/desktop` | Рабочий стол кооператива (Vue 3 + Quasar) |
|
||||
| [factory](components/factory) | `@coopenomics/factory` | Генератор юридических документов |
|
||||
| [migrator](components/migrator) | `migrator` | Утилита миграции данных |
|
||||
| [notifications](components/notifications) | `@coopenomics/notifications` | Библиотека уведомлений на основе Novu |
|
||||
| [parser](components/parser) | `@coopenomics/parser` | Индексатор блокчейна через State History Plugin |
|
||||
| [sdk](components/sdk) | `@coopenomics/sdk` | TypeScript SDK для GraphQL API |
|
||||
| [setup](components/setup) | `@coopenomics/setup` | Мастер первоначальной настройки |
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
### Предварительные требования
|
||||
|
||||
- Node.js >= 20
|
||||
- pnpm 9
|
||||
- Docker и Docker Compose
|
||||
- [WeasyPrint](https://doc.courtbouillon.org/weasyprint/stable/first_steps.html#installation) (для генерации PDF)
|
||||
|
||||
### Установка
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
Также для работы потребуется установленная фабрика документов `weasyprint` для вашей операционной системы: <https://doc.courtbouillon.org/weasyprint/stable/first_steps.html#installation>
|
||||
### Конфигурация
|
||||
|
||||
## Конфигурация
|
||||
|
||||
Для конфигурации потребуетcя установленный `docker` и `docker-compose`. Выполните команду и следуйте инструкциям:
|
||||
|
||||
```sh
|
||||
```bash
|
||||
pnpm run setup
|
||||
```
|
||||
|
||||
## Запуск
|
||||
Интерактивный мастер создаст необходимые `.env` файлы для всех компонентов.
|
||||
|
||||
### Инфраструктура
|
||||
### Запуск инфраструктуры
|
||||
|
||||
Для работы системы необходимы базы `mongo`, `postgres` и `redis`.
|
||||
|
||||
Команда запуска баз данных в контейнерах для работы в фоновом режиме:
|
||||
|
||||
```sh
|
||||
```bash
|
||||
docker compose up -d
|
||||
pnpm run reboot
|
||||
```
|
||||
|
||||
### Блокчейн и контракты
|
||||
## Разработка
|
||||
|
||||
```sh
|
||||
pnpm boot
|
||||
```
|
||||
### Бэкенд (controller + parser)
|
||||
|
||||
Команда запустит блокчейн и установит все смарт-контракты, подготовив систему к локальной разработке или применению.
|
||||
|
||||
### Бэкенд
|
||||
|
||||
```sh
|
||||
```bash
|
||||
pnpm run dev:backend
|
||||
```
|
||||
|
||||
Команда запустит набор сервисов бэкенда и точку доступа GraphQL-API на 2998 порту.
|
||||
### Фронтенд (desktop)
|
||||
|
||||
### Фронтенд
|
||||
|
||||
Команда запустит рабочий стол кооператива на 3005 порту:
|
||||
|
||||
```sh
|
||||
```bash
|
||||
pnpm run dev:desktop
|
||||
```
|
||||
|
||||
### Очистка базы
|
||||
### Библиотеки (factory + cooptypes)
|
||||
|
||||
Команда очистит базу mongodb и начнет производство блоков с начала:
|
||||
|
||||
```sh
|
||||
pnpm run clear
|
||||
```bash
|
||||
pnpm run dev:lib
|
||||
```
|
||||
|
||||
Правильная последовательность команд при переустановке проекта "с нуля":
|
||||
### Все сервисы одновременно
|
||||
|
||||
```sh
|
||||
git pull
|
||||
pnpm install
|
||||
pnpm run setup
|
||||
pnpm run clear
|
||||
pnpm run boot
|
||||
pnpm run dev:backend
|
||||
pnpm run dev:desktop
|
||||
```bash
|
||||
pnpm run dev:all
|
||||
```
|
||||
|
||||
### Кошелёк
|
||||
Для работы с кошельком блокчейна используется утилита cleos, которая позволяет извлекать таблицы смарт-контрактов и отправлять транзакции в блокчейн если это требуется.
|
||||
> **Примечание:** установка пакетов производится только через фильтр: `pnpm add <пакет> --filter <компонент>`
|
||||
|
||||
Для работы с командным кошельком `cleos` локально необходимо чтобы инфраструктура была поднята:
|
||||
## Тестирование
|
||||
|
||||
```
|
||||
docker compose up -d
|
||||
```bash
|
||||
# Все тесты
|
||||
pnpm run test
|
||||
|
||||
# Юнит-тесты (cooptypes, parser, notifications)
|
||||
pnpm run test:unit
|
||||
|
||||
# Компонентные тесты (factory)
|
||||
pnpm run test:component
|
||||
|
||||
# Интеграционные тесты (boot + blockchain)
|
||||
pnpm run test:integration
|
||||
```
|
||||
|
||||
Запускаем контейнер с кошельком и входим в него командой:
|
||||
## Сборка
|
||||
|
||||
```
|
||||
pnpm run cleos
|
||||
```
|
||||
```bash
|
||||
# Библиотеки (cooptypes, factory)
|
||||
pnpm run build:lib
|
||||
|
||||
В кошельке уже находится дефолтный ключ для работы с локальной средой. Этот ключ зашифрован паролем. Пароль хранится в файле `password` и используется автоматически при разблокировке. Для разблокировки кошелька, находясь в контейнере выполняем:
|
||||
# Смарт-контракты
|
||||
pnpm run build:contracts:all
|
||||
|
||||
# Desktop (SSR)
|
||||
pnpm --filter @coopenomics/desktop run build
|
||||
```
|
||||
./unlock.sh
|
||||
```
|
||||
|
||||
Для использования кошелька выполняем:
|
||||
```
|
||||
./cleos.sh <command>
|
||||
```
|
||||
|
||||
Для получения полного списка команд вызываем кошелек без команд:
|
||||
```
|
||||
./cleos.sh
|
||||
```
|
||||
|
||||
Например, для получения информации о текущем состоянии блокчейна:
|
||||
```
|
||||
./cleos.sh get info
|
||||
```
|
||||
|
||||
### Возможные проблемы
|
||||
В случае, если при выполнении ./unlock.sh возникает проблема:
|
||||
`Unable to connect to keosd, if keosd is running please kill the process and try again`, то выполните команду:
|
||||
|
||||
```
|
||||
./reset.sh
|
||||
```
|
||||
|
||||
Данная команда удалит созданный ранее сокет кошелька, после чего, выполните ./unlock.sh повторно.
|
||||
|
||||
## Лицензия
|
||||
|
||||
Продукт Потребительского Кооператива "ВОСХОД" распространяется по лицензии BY-NC-SA 4.0.
|
||||
Продукт Потребительского Кооператива «ВОСХОД» распространяется по лицензии [BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode.ru).
|
||||
|
||||
Разрешено делиться, копировать и распространять материал на любом носителе и форме, адаптировать, делать ремиксы, видоизменять и создавать новое, опираясь на этот материал. При использовании, Вы должны обеспечить указание авторства, предоставить ссылку, и обозначить изменения, если таковые были сделаны. Если вы перерабатываете, преобразовываете материал или берёте его за основу для производного произведения, вы должны распространять переделанные вами части материала на условиях той же лицензии , в соответствии с которой распространяется оригинал. Запрещено коммерческое использование материала. Использование в коммерческих целях – это использование, в первую очередь направленное на получение коммерческого преимущества или денежного вознаграждения.
|
||||
|
||||
Юридический текст лицензии: <https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode.ru>
|
||||
Разрешено делиться, копировать и распространять материал, адаптировать и создавать производные произведения при условии указания авторства и сохранения той же лицензии. Коммерческое использование запрещено.
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
# TASKS.md — Прогресс выполнения задач
|
||||
|
||||
## Завершённые задачи
|
||||
- ✅ 1. Dev-окружение
|
||||
- ✅ 2. Security updates
|
||||
- ✅ 3. Unified test pipeline (162/163)
|
||||
- ✅ 4. README + описания компонентов
|
||||
- ✅ 5. AGENTS.md для всех компонентов
|
||||
- ✅ 6. Setup — профессиональный установщик
|
||||
- ✅ 7. Поисковая система документов (OpenSearch)
|
||||
- ✅ 8. Процессы (Capital extension) — бэкенд + фронтенд
|
||||
- ✅ 9. Отчёты ФНС — 8 генераторов, фабрика, GraphQL API
|
||||
|
||||
---
|
||||
|
||||
## Активные задачи
|
||||
|
||||
### 10. Генерация отчётов ФНС (доработка) ✅
|
||||
- [x] Фабрика генераторов (ReportRegistryService)
|
||||
- [x] 8 генераторов (Бухбаланс, 6-НДФЛ, РСВ, ПСВ, ДУСН, 4-ФСС, Увед. взносы, УУСН)
|
||||
- [x] GraphQL API (getAvailableReports, generateReport)
|
||||
- [x] Генераторы переписаны по XSD — структура соответствует схемам ФНС
|
||||
- [x] 48 unit-тестов для всех генераторов
|
||||
- [x] Desktop UI (страница отчётов) — расширение reports
|
||||
- [x] Интеграция с реальными данными ledger через LedgerInteractor
|
||||
- [x] OrganizationDataInput DTO для передачи данных организации
|
||||
|
||||
### 11. CASL + гранулированные права доступа + шаринг страниц
|
||||
|
||||
#### 11.1 CASL — система прав доступа ✅
|
||||
- [x] @casl/ability установлен
|
||||
- [x] Action enum: manage, read, create, update, delete, execute, share
|
||||
- [x] Subject enum: 30+ subjects для всех ресурсов
|
||||
- [x] CaslAbilityFactory: role-based + granular permissions
|
||||
- [x] CaslGuard + @CheckAbility decorator (обратная совместимость с RolesGuard)
|
||||
- [x] role в user сохранено (chairman, member, user)
|
||||
- [x] Тесты: 22/22 unit-тестов (chairman, member, user, granular permissions)
|
||||
|
||||
#### 11.2 Шаринг страниц ✅
|
||||
- [x] ShareTokenEntity с JWT, guest/member targets
|
||||
- [x] ShareService: create/verify/revoke
|
||||
- [x] GraphQL: createShareLink, revokeShareLink, getMyShareLinks, getSharedWithMe
|
||||
- [x] Два уровня: гости (linkName) и пайщики (targetUsername)
|
||||
- [x] Granular allowedActions per share link
|
||||
- [x] Кнопка "Поделиться" в Desktop Header (ShareHeaderAction)
|
||||
- [x] Диалог управления правами (ShareDialog)
|
||||
- [x] Страница "Доступные мне" на рабочем столе Пайщика (SharedWithMePage)
|
||||
|
||||
#### 11.3 Интеграция в desktop ✅
|
||||
- [x] useShareButtonProcess: авто-регистрация на всех страницах
|
||||
- [x] ShareButton + ShareDialog компоненты
|
||||
- [x] Интеграция в init-app process
|
||||
- [x] Валидация share tokens в route guard (frontend) — bypass ролей при наличии share_token
|
||||
|
||||
### 12. API ключи кооператива ✅
|
||||
- [x] ApiKeyEntity: хеш ключа (sha256), префикс, операции, срок
|
||||
- [x] ApiKeyService: create, validate, list, revoke
|
||||
- [x] ApiKeyGuard: аутентификация через x-api-key header
|
||||
- [x] GraphQL: createApiKey, getApiKeys, revokeApiKey (chairman only)
|
||||
- [x] Безопасность: ключ показывается ТОЛЬКО при создании, хранится хеш
|
||||
- [x] Desktop: ApiKeysPage в chairman extension
|
||||
- [x] UI: таблица ключей, создание, отзыв, копирование
|
||||
|
||||
### 15. Стол заказов (Marketplace) — полная реализация
|
||||
|
||||
#### 15.1 Смарт-контракт marketplace — реорганизация
|
||||
- [x] Исправить процесс: одно заявление в совет при accept (authcontrib), не два
|
||||
- [x] Новый action requestreturn — заявление на возврат подаётся перед получением
|
||||
- [x] authcontrib сразу ставит authorized
|
||||
- [x] authreturn работает для статуса reqreturn → retauthorized
|
||||
- [x] receive требует retauthorized (после авторизации возврата)
|
||||
- [x] orderoffer убран product_return_statement (подаётся позже)
|
||||
- [x] Путь coopstock — имущество уже в кооперативе
|
||||
- [x] acceptstock — заказчик принимает coopstock (сразу в requestreturn)
|
||||
- [x] Document::remove_document утилита
|
||||
- [x] Уничтожение просроченного (destroy) — возврат средств, штраф, выплата поставщику
|
||||
- [x] Перепредложение (reoffer) — закрытие старой заявки, создание coopstock с новой ценой
|
||||
- [x] Система перевозок — уже реализована (shipment/)
|
||||
- [x] Гарантийный возврат — уже реализован (dispute_on_offer/)
|
||||
- [x] Сборка marketplace.wasm + marketplace.abi (test mode) — OK
|
||||
- [x] ORDER→OFFER: createorder + respondoffer (заказчик публикует → поставщики откликаются)
|
||||
- [x] delivery_type: internal (между КУ) / external (СДЭК и т.д.) в request struct
|
||||
- [x] contribution_type: share (паевой) / member (членский) в request struct
|
||||
- [x] Убраны ВСЕ any из controller marketplace (service, interactor)
|
||||
- [x] Импортировано 87 файлов marketplace extension из ветки marketplace
|
||||
- [x] Domain: 11 entities, 7 repositories, 5 services (БД-first архитектура)
|
||||
- [x] Application: 30+ DTOs, 4 resolvers (категории, атрибуты, заявки)
|
||||
- [x] Infrastructure: TypeORM entities, mappers, adapters
|
||||
- [x] Архитектурный принцип: заявки в БД (draft→published), блокчейн при match
|
||||
- [x] Накопительный характер заявок — CycleService (canStartSupply, checkExpiredCycles, cron)
|
||||
|
||||
#### 15.2 Генерация типов
|
||||
- [x] Интерфейсы cooptypes: RequestReturn, Coopstock, AcceptStock, Destroy, Reoffer
|
||||
- [x] ABI marketplace сохранён (31 action) для генерации типов
|
||||
|
||||
#### 15.3 Controller (бэкенд)
|
||||
- [x] CooplaceBlockchainPort: 5 новых портов
|
||||
- [x] CooplaceBlockchainAdapter: 5 реализаций (reqReturn, coopstock, acceptStock, destroy, reoffer)
|
||||
- [x] DTOs: ReqReturnInput, CoopstockInput, AcceptStockInput, DestroyRequestInput, ReofferRequestInput
|
||||
- [x] GraphQL resolvers: 5 новых мутаций с авторизацией
|
||||
- [x] Service + Interactor: проброс до blockchain
|
||||
- [x] ShipmentResolver: createShipment, signByDriver, arrived, receiveShipment
|
||||
- [x] Интеграция с документами — через SignedDigitalDocumentInputDTO
|
||||
- [x] MarketplaceEventService: 13 event handlers для синхронизации с блокчейном
|
||||
|
||||
#### 15.4 Desktop (фронтенд)
|
||||
- [x] Маршруты: витрина, предложение, создание, мои заказы, модерация
|
||||
- [x] install.ts расширения market обновлён с agreementsBase и roles
|
||||
- [x] ReqReturnStep — шаг подачи заявления на возврат (step 7)
|
||||
- [x] RetAuthorizedStep — шаг авторизации возврата советом (step 8)
|
||||
- [x] Base.vue: новые статусы (reqreturn, retauthorized), badge coopstock
|
||||
- [x] Панель председателя КУ: WarehousePage (склад, выдача, destroy/reoffer)
|
||||
- [x] ShipmentsPage: таблица перевозок, timeline 4 этапов, создание
|
||||
- [x] DisputePage: подача претензии, список, timeline спора, решение совета
|
||||
|
||||
#### 15.X Разделение cooplace на бэкенд и фронтенд расширения
|
||||
- [x] CooplaceExtensionModule — бэкенд (карточки, категории, blockchain actions)
|
||||
- [x] ProductCardResolver: CRUD карточек (draft→published→archived)
|
||||
- [x] CategoryResolver: CRUD дерева категорий (chairman)
|
||||
- [x] Domain entities: ProductCard (тип, статус, delivery_type, contribution_type), Category, SupplyOrder
|
||||
- [x] Repositories: interfaces для карточек, категорий, заявок
|
||||
- [x] GraphQL: 3 queries + 6 mutations для карточек/категорий
|
||||
- [x] marketplace (desktop) — чистый фронтенд, использует API cooplace
|
||||
- [x] market-admin (desktop) — уже существует (extensions/market-admin)
|
||||
- [x] Controller стартует, 22+9 = 31 GraphQL endpoints
|
||||
|
||||
#### 15.Y Бэкенд карточек с циклами
|
||||
- [x] TypeORM entities: ProductCard (циклы), Category, SupplyOrder
|
||||
- [x] Repository adapters: CRUD + фильтры для всех entities
|
||||
- [x] ProductCardService: модерация (draft→moderation→published), циклы (min_units, deadline)
|
||||
- [x] addOrderToCard: инкремент cycle_collected_units, проверка min_units
|
||||
- [x] checkCycleDeadline: истечение → возврат → новый цикл (cycle_number++)
|
||||
- [x] ProductCardResolver: 9 queries/mutations с реальной БД логикой
|
||||
- [x] CooplaceExtensionModule: полная DI с TypeORM
|
||||
- [x] Исправлен белый экран desktop (vite-plugin-checker + share-button crash)
|
||||
|
||||
#### 15.Z Админка маркетплейса (настройки)
|
||||
- [x] MarketplaceSettingsEntity: lead_request_policy, publish_access_policy, whitelist, moderation, cycles, delivery, prices
|
||||
- [x] MarketplaceSettingsTypeormEntity + TypeORM repository (upsert)
|
||||
- [x] MarketplaceSettingsResolver: get/update settings, add/remove whitelist
|
||||
- [x] Перенесены ВСЕ DTOs/entities/repositories/services из extensions/marketplace → extensions/cooplace
|
||||
- [x] Desktop SettingsPage: radio groups, toggles, whitelist chips, price inputs
|
||||
- [x] market-admin: маршрут настроек как дефолтный + модерация + все заказы
|
||||
|
||||
#### 15.W Правильная логика match + cycles
|
||||
- [x] MatchService: каждая встречная заявка → сразу в блокчейн (блокировка средств смарт-контрактом)
|
||||
- [x] CycleService: min_units/deadline → порог для supply, НЕ для match
|
||||
- [x] CycleService: cron каждые 5 мин — проверка истёкших циклов → cancel через блокчейн
|
||||
- [x] MARKET-LOGIC.md обновлён с правильным принципом
|
||||
- [x] AGENTS.md обновлён с описанием маркетплейса
|
||||
|
||||
#### 15.5 Тесты
|
||||
- [x] Unit-тесты controller: 8 тестов marketplace actions + statuses (90/90 total)
|
||||
- [x] cooptypes собран с новыми actions
|
||||
- [x] Boot интеграционный тест: marketplace.test.ts (orderoffer + coopstock flows)
|
||||
- [x] Boot тест marketplace.test.ts: orderoffer + coopstock flows
|
||||
|
||||
### 16. Документация (docs)
|
||||
- [x] Раздел Cooplace: обзор архитектуры, жизненный цикл карточки, match, циклы
|
||||
- [x] Раздел Cooplace/dev: GraphQL API reference (настройки, карточки, поставки, перевозки, диспуты)
|
||||
- [x] Раздел Marketplace: пользовательская документация (витрина, заказ, администрирование)
|
||||
- [x] TypeDoc JSON сгенерирован из SDK (137MB)
|
||||
- [x] mkdocs.yml nav обновлён с «Стол заказов»
|
||||
- [x] .gitignore для typedoc.json (генерируется при сборке)
|
||||
|
||||
### 13. GraphQL Subscriptions ✅
|
||||
- [x] PubSubModule: глобальный PubSub provider
|
||||
- [x] WebSocket support в GraphQL module (graphql-ws)
|
||||
- [x] 5 Capital subscriptions: issueUpdated/Created, commitCreated/Updated, dataChanged
|
||||
- [x] systemStatusChanged — подписка на статус системы (install/init/update)
|
||||
- [x] sovietDataChanged — подписка на события собраний/решений
|
||||
- [x] Events publishing: GenerationService, ProjectManagementService, VotingService, SystemService, MeetEventService
|
||||
- [x] SDK: Subscriptions namespace (Capital)
|
||||
- [x] Auth через connectionParams.token
|
||||
- [x] `useGraphqlSubscription` composable на фронтенде
|
||||
- [x] **7 страниц Capital** переведены с polling на подписки
|
||||
- [x] **System store** — WebSocket мониторинг вместо setTimeout
|
||||
- [x] **init-wallet** — удалён рекурсивный setTimeout(run, 10_000)
|
||||
- [x] **MeetDetails, ListOfAgenda, WaitingRegistration** — подписки вместо setInterval
|
||||
- [x] **Provider, ConnectionAgreement** — удалён setInterval polling
|
||||
- [x] 0 использований setInterval в прикладном коде (только UI-анимация энергии)
|
||||
@@ -0,0 +1,64 @@
|
||||
# Test Plan — pnpm run test
|
||||
|
||||
## Архитектура
|
||||
```
|
||||
pnpm run test
|
||||
├── cooptypes — vitest run (smoke tests exports) ✅ 4/4
|
||||
├── parser — vitest run (config smoke tests) ✅ 3/3
|
||||
├── factory — vitest run (document generation tests) 🔧 17/85 → need mocks
|
||||
├── sdk — vitest run (API integration tests) 🔧 TODO
|
||||
├── notifications — vitest run (workflow tests) 🔧 TODO
|
||||
├── controller — jest / vitest (NestJS unit tests) 🔧 TODO
|
||||
└── boot — vitest run (blockchain integration) 🔧 53/60
|
||||
```
|
||||
|
||||
## Статус по компонентам
|
||||
|
||||
### cooptypes ✅ DONE
|
||||
- 4 smoke-теста экспортов
|
||||
- Не требует инфраструктуры
|
||||
|
||||
### parser ✅ DONE
|
||||
- 3 smoke-теста конфигурации
|
||||
- Не требует инфраструктуры
|
||||
|
||||
### factory 🔧 IN PROGRESS
|
||||
- **Проблема**: тесты обращаются к parser API (`SIMPLE_EXPLORER_API`) для get-tables/get-actions
|
||||
- **Решение**: Уже есть мок-система в `src/Utils/testMocks.ts` + `matchMock.ts`
|
||||
- **Нужно**: Добавить моки для ВСЕХ документов:
|
||||
- [ ] cooperative data mock (registrator.coops table)
|
||||
- [ ] soviet boards mock — уже есть через test setup в MongoDB
|
||||
- [ ] draft templates mock (draft.drafts + draft.translations tables)
|
||||
- [ ] decision data mocks (soviet.decisions table)
|
||||
- [ ] Мок для ReturnByMoney документов
|
||||
- [ ] Все документы 1000+ серии
|
||||
- **Текущие рабочие моки**: meet tables, votefor actions, returnByMoneyDecision actions
|
||||
- **После мокирования**: все 85 тестов должны проходить
|
||||
|
||||
### boot 🔧 NEEDS REBOOT
|
||||
- capital.test — 53/60 тестов проходят (после чистого reboot)
|
||||
- wallet.test — нужен полный boot с agreements
|
||||
- registrator.test — нужен полный boot
|
||||
- capital-import.test — отдельный тест импорта
|
||||
- **Требует**: pnpm run reboot перед запуском
|
||||
|
||||
### sdk 🔧 TODO
|
||||
- 1 тест файл с login + fetch extensions
|
||||
- Требует запущенный controller
|
||||
- Нужно: обновить chain_id, api_url, credentials
|
||||
|
||||
### controller 🔧 TODO
|
||||
- Все старые тесты удалены (устаревшие)
|
||||
- NestJS-приложение — нужны тесты через @nestjs/testing
|
||||
- Минимум: unit-тесты domain-логики, smoke-тест GraphQL API
|
||||
|
||||
### notifications 🔧 TODO
|
||||
- Нет тестов
|
||||
- Нужно: smoke-тесты workflow builder
|
||||
|
||||
### desktop — SKIP (нет тестов, UI-тестирование)
|
||||
|
||||
## Root script требования
|
||||
- `pnpm run test` — запускает ВСЕ тесты
|
||||
- fail-fast: если один пакет падает — весь pipeline падает
|
||||
- Последовательный запуск (не параллельный)
|
||||
@@ -0,0 +1,102 @@
|
||||
# @coopenomics/boot
|
||||
|
||||
## Назначение
|
||||
|
||||
CLI-утилита для инициализации блокчейна EOSIO и развёртывания кооператива. Выполняет полный цикл: запуск ноды → создание аккаунтов → активация фич → деплой контрактов → выпуск токенов → загрузка шаблонов → создание кооператива и совета.
|
||||
|
||||
## Структура
|
||||
|
||||
```
|
||||
src/
|
||||
├── index.ts — CLI точка входа (commander): boot, boot:clean, boot:extra,
|
||||
│ start, stop, clear, cleos, deploy, create-coop, unlock
|
||||
├── init/
|
||||
│ ├── booter.ts — Главные сценарии: boot(), bootClean(), bootExtra()
|
||||
│ ├── infra.ts — startInfra() — базовая инфраструктура блокчейна
|
||||
│ │ installInitialData() — начальные данные (пользователи, совет)
|
||||
│ │ installExtraData() — расширенные данные
|
||||
│ └── cooperative.ts — CooperativeClass — создание программ ЦПП, регистрация
|
||||
│ └── participant.ts — Логика регистрации пайщика
|
||||
├── blockchain/
|
||||
│ └── index.ts — Класс Blockchain — обёртка над eosjs для работы с EOSIO
|
||||
│ (создание аккаунтов, деплой контрактов, транзакции, токены)
|
||||
├── configs/
|
||||
│ ├── index.ts — Главная конфигурация (аккаунты, ключи, токен, эмиссия)
|
||||
│ ├── networks.ts — Настройки сети (протокол, хост, порт)
|
||||
│ ├── contracts.ts — Список контрактов и путей к их WASM/ABI файлам
|
||||
│ └── protocol_features/ — JSON-файлы фич для активации на блокчейне
|
||||
├── docker/ — Управление Docker-контейнером ноды блокчейна
|
||||
│ ├── container.ts, run.ts, stop.ts, find.ts, exec.ts, deploy.ts
|
||||
│ ├── health.ts — Проверка готовности ноды
|
||||
│ └── purge.ts — Очистка данных блокчейна и БД
|
||||
├── *-init.ts — Инициализация реляционной БД (пользователи, vault, статус, extensions)
|
||||
├── tests/ — Интеграционные тесты
|
||||
│ ├── capital.test.ts — Основной тест (полный цикл ЦПП)
|
||||
│ ├── wallet.test.ts — Тесты кошелька
|
||||
│ ├── registrator.test.ts — Тесты регистрации
|
||||
│ └── capital/ — Вспомогательные функции для тестов капитала
|
||||
└── utils/ — Утилиты (sleep, randomHash, randomUsername, и т.д.)
|
||||
```
|
||||
|
||||
## Порядок загрузки (boot flow)
|
||||
|
||||
1. **startInfra()** — основа:
|
||||
- Инициализация `Blockchain` с ключами
|
||||
- Создание системных аккаунтов (eosio.token, registrator, soviet, и т.д.)
|
||||
- Активация фич протокола (PREACTIVATE_FEATURE → остальные)
|
||||
- Деплой смарт-контрактов (eosio.boot → eosio.system → все остальные)
|
||||
- Создание и выпуск токенов
|
||||
- Загрузка шаблонов документов из `@coopenomics/factory` Registry
|
||||
- Настройка ресурсов (powerup)
|
||||
|
||||
2. **installInitialData()** — данные кооператива:
|
||||
- Сохранение организации, платёжных методов, физлиц в MongoDB
|
||||
- Сохранение переменных кооператива (vars)
|
||||
- Регистрация пользователей в блокчейне
|
||||
- Инициализация реляционной БД (пользователи, vault)
|
||||
- Создание совета (board)
|
||||
- Создание программ ЦПП (Благорост, маркетплейс)
|
||||
|
||||
3. **bootClean()** — минимальная загрузка (только инфра + программы, без данных)
|
||||
|
||||
4. **bootExtra()** — расширенная загрузка (5 членов совета, дополнительные пайщики)
|
||||
|
||||
## Режимы контрактов
|
||||
|
||||
- **test** — контракты собранные с флагом `test` (позволяют boot с 1 членом совета)
|
||||
- **production** — стандартные контракты (требуют кворум)
|
||||
|
||||
Пути к WASM/ABI файлам указаны в `configs/contracts.ts`, относительно `../contracts/build/contracts/`.
|
||||
|
||||
## Тесты
|
||||
|
||||
- Фреймворк: **Vitest**
|
||||
- Основной тест: `src/tests/capital.test.ts` — полный цикл ЦПП (создание программ, проектов, инвестирование, голосование, конвертация)
|
||||
- Требует: полностью загруженный блокчейн (`pnpm run reboot`), MongoDB, реляционную БД
|
||||
|
||||
### Запуск
|
||||
|
||||
```
|
||||
pnpm --filter @coopenomics/boot test
|
||||
```
|
||||
|
||||
### Важно: Duplicate transaction
|
||||
|
||||
EOSIO отклоняет транзакции с одинаковым хешем (TAPOS block + action data). При повторных вызовах одинаковых действий нужен `await sleep(500)` между ними.
|
||||
|
||||
## Скрипты package.json
|
||||
|
||||
| Скрипт | Описание |
|
||||
|--------|----------|
|
||||
| `boot` | Полная загрузка: инфра + данные + кооператив |
|
||||
| `boot:clean` | Чистая загрузка: только инфра + программы |
|
||||
| `boot:extra` | Расширенная загрузка: инфра + 5 членов совета |
|
||||
| `create-coop` | Создание кооператива после boot:clean |
|
||||
| `test` | Vitest (capital.test.ts, timeout 240s) |
|
||||
| `test:all` | Все тесты |
|
||||
| `clear` | Очистка блокчейна и перезапуск ноды |
|
||||
|
||||
## Зависимости от других компонентов
|
||||
|
||||
- `cooptypes` (workspace) — типы контрактов
|
||||
- `@coopenomics/factory` (workspace) — реестр шаблонов, сохранение данных в MongoDB
|
||||
+100
-1
@@ -1 +1,100 @@
|
||||
# Загрузчик.
|
||||
# 🚀 @coopenomics/boot
|
||||
|
||||
CLI-утилита для инициализации блокчейн-инфраструктуры кооператива. Создаёт системные аккаунты, устанавливает смарт-контракты, настраивает токены и разворачивает кооперативную среду. Включает интеграционные тесты для основных подсистем платформы.
|
||||
|
||||
## Основные возможности
|
||||
|
||||
- Полный цикл развёртывания блокчейна EOSIO — от запуска ноды до создания кооператива
|
||||
- Установка системных и прикладных смарт-контрактов
|
||||
- Инициализация токенов и создание тестовых данных
|
||||
- Управление Docker-контейнерами блокчейн-ноды
|
||||
- Интеграционные тесты: паевые взносы (capital), кошелёк (wallet), регистрация участников (registrator)
|
||||
- Поддержка нескольких режимов загрузки: стандартный, чистый и расширенный
|
||||
|
||||
## Установка
|
||||
|
||||
Компонент является частью монорепозитория. Установка зависимостей из корня проекта:
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
Или только для этого компонента:
|
||||
|
||||
```bash
|
||||
pnpm install --filter @coopenomics/boot
|
||||
```
|
||||
|
||||
## Скрипты
|
||||
|
||||
| Скрипт | Команда | Описание |
|
||||
|--------|---------|----------|
|
||||
| `boot` | `pnpm run boot` | Запуск блокчейна и установка контрактов |
|
||||
| `boot:clean` | `pnpm run boot:clean` | Чистый запуск с полной переустановкой |
|
||||
| `boot:extra` | `pnpm run boot:extra` | Расширенная инициализация с дополнительными данными |
|
||||
| `deploy` | `pnpm run deploy` | Развёртывание контрактов на запущенной ноде |
|
||||
| `cli` | `pnpm run cli` | Интерактивный командный интерфейс (Commander.js) |
|
||||
| `clear` | `pnpm run clear` | Очистка данных блокчейна |
|
||||
| `start` | `pnpm run start` | Перезапуск ноды |
|
||||
| `stop` | `pnpm run stop` | Остановка ноды |
|
||||
| `test` | `pnpm run test` | Интеграционные тесты (capital) |
|
||||
| `test:all` | `pnpm run test:all` | Все интеграционные тесты |
|
||||
|
||||
## Конфигурация
|
||||
|
||||
Скопируйте `.env-example` в `.env` и настройте переменные окружения:
|
||||
|
||||
- Endpoint блокчейн-ноды (API)
|
||||
- Строка подключения к MongoDB
|
||||
- Строка подключения к реляционной БД
|
||||
- Приватные ключи для развёртывания контрактов
|
||||
|
||||
Подробное описание переменных — в файле `.env-example`.
|
||||
|
||||
## Тестирование
|
||||
|
||||
Перед запуском тестов необходима работающая инфраструктура (см. `docker-compose.yaml` в корне проекта):
|
||||
|
||||
```bash
|
||||
pnpm --filter @coopenomics/boot run test
|
||||
pnpm --filter @coopenomics/boot run test:all
|
||||
```
|
||||
|
||||
Тесты используют **Vitest** с таймаутом 240 секунд — это связано с ожиданием подтверждения блокчейн-транзакций.
|
||||
|
||||
## Архитектура
|
||||
|
||||
```
|
||||
src/
|
||||
├── index.ts # Точка входа CLI (Commander.js)
|
||||
├── init/ # Логика инициализации
|
||||
│ ├── booter.ts # Загрузчик блокчейна
|
||||
│ ├── cooperative.ts # Настройка кооператива
|
||||
│ ├── infra.ts # Инфраструктурная инициализация
|
||||
│ └── participant.ts # Создание участников
|
||||
├── blockchain/ # Взаимодействие с EOSIO (eosjs)
|
||||
├── docker/ # Управление Docker-контейнерами
|
||||
├── configs/ # Конфигурация ноды и протокол-фичи
|
||||
├── tests/ # Интеграционные тесты
|
||||
│ ├── capital.test.ts # Тесты паевых взносов
|
||||
│ ├── wallet.test.ts # Тесты кошелька
|
||||
│ └── registrator.test.ts # Тесты регистрации
|
||||
└── utils/ # Вспомогательные утилиты
|
||||
scripts/
|
||||
├── reboot.sh # Полный перезапуск
|
||||
├── clean_reboot.sh # Чистый перезапуск
|
||||
└── extra_reboot.sh # Расширенный перезапуск
|
||||
```
|
||||
|
||||
## Ключевые зависимости
|
||||
|
||||
- **eosjs** — взаимодействие с блокчейном EOSIO
|
||||
- **commander** — CLI-фреймворк
|
||||
- **dockerode** — управление Docker-контейнерами из Node.js
|
||||
- **mongoose** — работа с MongoDB
|
||||
- **pg** — работа с реляционной БД
|
||||
- **vitest** — тестирование
|
||||
|
||||
## Лицензия
|
||||
|
||||
[BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode.ru)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"name": "@coopenomics/boot",
|
||||
"type": "module",
|
||||
"version": "2026.2.21-alpha-4",
|
||||
"version": "2026.2.22-2",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@9.0.6",
|
||||
"description": "",
|
||||
"description": "CLI-утилита инициализации блокчейна и кооператива",
|
||||
"author": "Alex Ant <dacom.dark.sun@gmail.com>",
|
||||
"license": "",
|
||||
"keywords": [],
|
||||
@@ -22,12 +22,14 @@
|
||||
"create-coop": "esno src/index.ts create-coop",
|
||||
"clear": "./scripts/clear.sh",
|
||||
"start": "./scripts/restart.sh",
|
||||
"stop": "./scripts/stop.sh"
|
||||
"stop": "./scripts/stop.sh",
|
||||
"test": "vitest run src/tests/capital.test.ts --testTimeout=240000",
|
||||
"test:all": "vitest run --testTimeout=240000"
|
||||
},
|
||||
"dependencies": {
|
||||
"@coopenomics/factory": "workspace:*",
|
||||
"@types/pg": "^8.16.0",
|
||||
"axios": "^1.6.8",
|
||||
"axios": "^1.13.5",
|
||||
"chai": "^5.1.2",
|
||||
"commander": "^12.1.0",
|
||||
"cooptypes": "workspace:*",
|
||||
@@ -38,7 +40,7 @@
|
||||
"eosjs-ecc": "^4.0.7",
|
||||
"execa": "^9.5.2",
|
||||
"mocha": "^10.7.3",
|
||||
"mongoose": "^8.10.0",
|
||||
"mongoose": "^9.2.2",
|
||||
"pg": "^8.16.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -58,4 +60,4 @@
|
||||
"vite": "^5.2.11",
|
||||
"vitest": "^1.6.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
# Останавливаем и удаляем контейнеры вместе с volumes
|
||||
echo "Останавливаем и удаляем контейнеры с volumes..."
|
||||
docker compose down -v mongo postgres cooparser || true
|
||||
docker compose down -v mongo postgres opensearch cooparser || true
|
||||
|
||||
# Останавливаем blockchain контейнер перед удалением данных
|
||||
echo "Останавливаем blockchain контейнер..."
|
||||
@@ -15,7 +15,7 @@ sudo rm -rf ../blockchain-data/
|
||||
|
||||
# Пересоздаем и запускаем базы данных
|
||||
echo "Пересоздаем и запускаем базы данных..."
|
||||
docker compose up -d mongo postgres
|
||||
docker compose up -d mongo postgres opensearch
|
||||
|
||||
# Ждем готовности MongoDB
|
||||
echo "Ждем готовности MongoDB..."
|
||||
|
||||
@@ -1631,6 +1631,7 @@ describe('тест контракта CAPITAL', () => {
|
||||
})
|
||||
|
||||
it('конвертируем сегмент investor1 в капитализацию', async () => {
|
||||
await sleep(500)
|
||||
// Обновляем сегмент инвестора перед конвертацией
|
||||
await refreshSegment(blockchain, 'voskhod', componentProject.project_hash, investor1)
|
||||
|
||||
@@ -1666,6 +1667,7 @@ describe('тест контракта CAPITAL', () => {
|
||||
})
|
||||
|
||||
it('конвертируем сегмент investor2', async () => {
|
||||
await sleep(500)
|
||||
// Обновляем сегмент инвестора перед конвертацией
|
||||
await refreshSegment(blockchain, 'voskhod', componentProject.project_hash, investor2)
|
||||
|
||||
@@ -1716,6 +1718,7 @@ describe('тест контракта CAPITAL', () => {
|
||||
})
|
||||
|
||||
it('конвертируем сегмент investor3', async () => {
|
||||
await sleep(500)
|
||||
// Обновляем сегмент инвестора перед конвертацией
|
||||
await refreshSegment(blockchain, 'voskhod', componentProject.project_hash, investor3)
|
||||
|
||||
@@ -2272,6 +2275,7 @@ describe('тест контракта CAPITAL', () => {
|
||||
// })
|
||||
|
||||
it('тест ВЫСОКОЙ ТОЧНОСТИ: 1 млрд инвестиций и 10к часов', async () => {
|
||||
await sleep(1000)
|
||||
console.log('\n🚀 СТАРТ ТЕСТА ВЫСОКОЙ ТОЧНОСТИ (50M RUB) 🚀\n')
|
||||
const auditHistory: any[] = []
|
||||
const this_investor = investor3
|
||||
@@ -2430,6 +2434,7 @@ describe('тест контракта CAPITAL', () => {
|
||||
|
||||
for (const p of participants) {
|
||||
await refreshSegment(blockchain, 'voskhod', highPrecisionHash, p)
|
||||
await sleep(200)
|
||||
}
|
||||
|
||||
await auditParticipants('1. ИНВЕСТИЦИЯ', highPrecisionHash)
|
||||
@@ -2437,11 +2442,13 @@ describe('тест контракта CAPITAL', () => {
|
||||
// 7. Два взноса по 5000 часов
|
||||
console.log('--- Взнос результата: 5000 часов от tester1 ---')
|
||||
await commitToResult(blockchain, 'voskhod', highPrecisionHash, tester1, 5000)
|
||||
await sleep(500)
|
||||
console.log('--- Взнос результата: 5000 часов от tester2 ---')
|
||||
await commitToResult(blockchain, 'voskhod', highPrecisionHash, tester2, 5000)
|
||||
|
||||
for (const p of participants) {
|
||||
await refreshSegment(blockchain, 'voskhod', highPrecisionHash, p)
|
||||
await sleep(200)
|
||||
}
|
||||
|
||||
await auditParticipants('2. ВЗНОСЫ ВРЕМЕНЕМ', highPrecisionHash)
|
||||
@@ -2470,6 +2477,7 @@ describe('тест контракта CAPITAL', () => {
|
||||
await processCalculateVotes(blockchain, { coopname: 'voskhod', username: p, project_hash: highPrecisionHash })
|
||||
}
|
||||
await refreshSegment(blockchain, 'voskhod', highPrecisionHash, p)
|
||||
await sleep(200)
|
||||
}
|
||||
|
||||
await auditParticipants('4. ФИНАЛ (ОБНОВЛ. СЕГМЕНТЫ)', highPrecisionHash)
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
|
||||
import Blockchain from '../blockchain'
|
||||
import config from '../configs'
|
||||
import { getTotalRamUsage, globalRamStats } from '../utils/getTotalRamUsage'
|
||||
import { addUser } from '../init/participant'
|
||||
import { generateRandomUsername } from '../utils/randomUsername'
|
||||
|
||||
const blockchain = new Blockchain(config.network, config.private_keys)
|
||||
|
||||
let supplier: string
|
||||
let customer: string
|
||||
|
||||
const fakeDocument = {
|
||||
hash: '157192B276DA23CC84AB078FC8755C051C5F0430BF4802E55718221E6B76C777',
|
||||
public_key: 'PUB_K1_5JhMfxbsNebajHcTEK8yC9uNN9Dit9hEmzE8ri8yMhhzzEtUA4',
|
||||
signature: 'SIG_K1_KmKWPBC8dZGGDGhbKEoZEzPr3h5crRrR2uLdGRF5DJbeibY1MY1bZ9sPwHsgmPfiGFv9psfoCVsXFh9TekcLuvaeuxRKA8',
|
||||
meta: '{}',
|
||||
}
|
||||
|
||||
const testHash = '0000000000000000000000000000000000000000000000000000000000000001'
|
||||
|
||||
beforeAll(async () => {
|
||||
await blockchain.update_pass_instance()
|
||||
|
||||
supplier = generateRandomUsername()
|
||||
customer = generateRandomUsername()
|
||||
|
||||
console.log('supplier:', supplier)
|
||||
console.log('customer:', customer)
|
||||
|
||||
await addUser(supplier)
|
||||
await addUser(customer)
|
||||
}, 500_000)
|
||||
|
||||
afterAll(() => {
|
||||
console.log('\n📊 **MARKETPLACE RAM USAGE** 📊')
|
||||
let total = 0
|
||||
for (const [key, ram] of Object.entries(globalRamStats)) {
|
||||
console.log(` ${key} = ${(ram / 1024).toFixed(2)} kb`)
|
||||
total += ram
|
||||
}
|
||||
console.log(`\n💾 **TOTAL**: ${(total / 1024).toFixed(2)} kb\n`)
|
||||
})
|
||||
|
||||
describe('Marketplace — orderoffer flow', () => {
|
||||
it('контракт marketplace задеплоен', async () => {
|
||||
const info = await blockchain.api.v1.chain.get_info()
|
||||
expect(info).toBeDefined()
|
||||
expect(info.chain_id).toBeDefined()
|
||||
})
|
||||
|
||||
it('создание заявки orderoffer (заказчик)', async () => {
|
||||
const coopname = config.coopname
|
||||
|
||||
const result = await blockchain.transact({
|
||||
account: 'marketplace',
|
||||
name: 'orderoffer',
|
||||
authorization: [{ actor: coopname, permission: 'active' }],
|
||||
data: {
|
||||
coopname,
|
||||
receiver_braname: coopname,
|
||||
username: customer,
|
||||
hash: testHash,
|
||||
units: 10,
|
||||
unit_cost: '100.0000 RUB',
|
||||
product_lifecycle_secs: 2592000,
|
||||
warranty_period_secs: 604800,
|
||||
membership_fee_amount: '50.0000 RUB',
|
||||
cancellation_fee_amount: '10.0000 RUB',
|
||||
convert_in: fakeDocument,
|
||||
meta: JSON.stringify({ title: 'Тестовый товар', description: 'Описание' }),
|
||||
},
|
||||
})
|
||||
|
||||
expect(result).toBeDefined()
|
||||
console.log('orderoffer tx:', result?.response?.transaction_id?.substring(0, 16))
|
||||
|
||||
const ramUsed = await getTotalRamUsage(coopname)
|
||||
globalRamStats['orderoffer'] = ramUsed
|
||||
})
|
||||
|
||||
it('принятие заявки поставщиком (accept)', async () => {
|
||||
const coopname = config.coopname
|
||||
|
||||
const result = await blockchain.transact({
|
||||
account: 'marketplace',
|
||||
name: 'accept',
|
||||
authorization: [{ actor: coopname, permission: 'active' }],
|
||||
data: {
|
||||
coopname,
|
||||
supplier_braname: coopname,
|
||||
username: supplier,
|
||||
request_hash: testHash,
|
||||
convert_out: fakeDocument,
|
||||
product_contribution_statement: fakeDocument,
|
||||
},
|
||||
})
|
||||
|
||||
expect(result).toBeDefined()
|
||||
console.log('accept tx:', result?.response?.transaction_id?.substring(0, 16))
|
||||
|
||||
const ramUsed = await getTotalRamUsage(coopname)
|
||||
globalRamStats['accept'] = ramUsed
|
||||
})
|
||||
})
|
||||
|
||||
describe('Marketplace — coopstock flow', () => {
|
||||
const stockHash = '0000000000000000000000000000000000000000000000000000000000000002'
|
||||
|
||||
it('создание предложения из запасов кооператива', async () => {
|
||||
const coopname = config.coopname
|
||||
|
||||
const result = await blockchain.transact({
|
||||
account: 'marketplace',
|
||||
name: 'coopstock',
|
||||
authorization: [{ actor: coopname, permission: 'active' }],
|
||||
data: {
|
||||
coopname,
|
||||
braname: coopname,
|
||||
hash: stockHash,
|
||||
units: 5,
|
||||
unit_cost: '50.0000 RUB',
|
||||
product_lifecycle_secs: 1296000,
|
||||
warranty_period_secs: 302400,
|
||||
membership_fee_amount: '25.0000 RUB',
|
||||
meta: JSON.stringify({ title: 'Уценённый товар', description: 'Из запасов' }),
|
||||
},
|
||||
})
|
||||
|
||||
expect(result).toBeDefined()
|
||||
console.log('coopstock tx:', result?.response?.transaction_id?.substring(0, 16))
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,11 @@
|
||||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
testTimeout: 240000,
|
||||
hookTimeout: 240000,
|
||||
sequence: {
|
||||
concurrent: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,77 @@
|
||||
# 🔑 @coopenomics/cleos
|
||||
|
||||
Обёртка над утилитой `cleos` из набора инструментов EOSIO. Запускает Docker-контейнер с предустановленным кошельком для прямого взаимодействия с блокчейном — отправки транзакций, просмотра таблиц контрактов, управления ключами и аккаунтами.
|
||||
|
||||
## Основные возможности
|
||||
|
||||
- Готовая среда с кошельком и ключами для локальной разработки
|
||||
- Отправка транзакций в блокчейн
|
||||
- Просмотр таблиц смарт-контрактов
|
||||
- Управление аккаунтами и ключами блокчейна
|
||||
- Разблокировка и сброс кошелька через вспомогательные скрипты
|
||||
|
||||
## Установка
|
||||
|
||||
Компонент является частью монорепозитория. Установка зависимостей из корня проекта:
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
## Скрипты
|
||||
|
||||
| Скрипт | Команда | Описание |
|
||||
|--------|---------|----------|
|
||||
| `run` | `pnpm run run` | Запуск Docker-контейнера с кошельком |
|
||||
|
||||
Из корня монорепозитория можно использовать:
|
||||
|
||||
```bash
|
||||
pnpm run enter
|
||||
```
|
||||
|
||||
## Использование
|
||||
|
||||
Для работы необходима запущенная инфраструктура (см. `docker-compose.yaml` в корне проекта).
|
||||
|
||||
### Запуск
|
||||
|
||||
```bash
|
||||
pnpm run enter
|
||||
```
|
||||
|
||||
### Внутри контейнера
|
||||
|
||||
```bash
|
||||
./unlock.sh # Разблокировка кошелька
|
||||
./cleos.sh get info # Информация о блокчейне
|
||||
./cleos.sh get table <contract> <scope> <table> # Просмотр таблиц
|
||||
```
|
||||
|
||||
### Вспомогательные скрипты
|
||||
|
||||
| Скрипт | Назначение |
|
||||
|--------|-----------|
|
||||
| `unlock.sh` | Разблокировка кошелька (пароль применяется автоматически) |
|
||||
| `cleos.sh` | Прокси-вызов `cleos` с настроенным подключением к ноде |
|
||||
| `reset.sh` | Сброс состояния кошелька |
|
||||
|
||||
## Конфигурация
|
||||
|
||||
Кошелёк использует дефолтный ключ для локальной среды разработки. Пароль хранится в файле `password` внутри директории `eosio-wallet` и применяется автоматически при разблокировке.
|
||||
|
||||
## Архитектура
|
||||
|
||||
```
|
||||
├── run.sh # Скрипт запуска Docker-контейнера
|
||||
├── eosio-wallet/ # Хранилище кошелька
|
||||
│ └── password # Пароль для разблокировки
|
||||
└── scripts/ # Вспомогательные скрипты
|
||||
├── unlock.sh # Разблокировка кошелька
|
||||
├── cleos.sh # Прокси-вызов cleos
|
||||
└── reset.sh # Сброс кошелька
|
||||
```
|
||||
|
||||
## Лицензия
|
||||
|
||||
[BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode.ru)
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@coopenomics/cleos",
|
||||
"version": "2026.2.21-alpha-4",
|
||||
"version": "2026.2.22-2",
|
||||
"private": true,
|
||||
"description": "",
|
||||
"description": "Обёртка над кошельком cleos для EOSIO блокчейна",
|
||||
"scripts": {
|
||||
"run": "./run.sh"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC"
|
||||
}
|
||||
}
|
||||
+87
-122
@@ -1,156 +1,121 @@
|
||||
# Введение
|
||||
# ⛓️ @coopenomics/contracts
|
||||
|
||||
Репозиторий содержит в себе смарт-контракты операционной системы coopOS, образующих протокол COOPENOMICS.
|
||||
> Смарт-контракты EOSIO для кооперативного управления на блокчейне
|
||||
|
||||
## Окружение
|
||||
В качестве окружения используется контейнер операционной системы coopOS. Для запуска выполните команду из корня репозитория:
|
||||
## Описание
|
||||
|
||||
```
|
||||
pnpm run enter
|
||||
Набор смарт-контрактов на C++, образующих протокол COOPENOMICS — операционную систему кооператива на блокчейне EOSIO. Контракты реализуют полный цикл управления: регистрация участников, голосование совета, паевые взносы, документооборот, финансовый учёт, маркетплейс и проведение собраний.
|
||||
|
||||
Архитектура контрактов следует принципам **чистой архитектуры**: `app/` (оркестрация действий), `domain/entity/` (сущности и таблицы), `domain/core/` (межсущностная бизнес-логика).
|
||||
|
||||
## Контракты
|
||||
|
||||
| Контракт | Назначение |
|
||||
|----------|------------|
|
||||
| `soviet` | Совет кооператива — голосование, решения, повестки |
|
||||
| `registrator` | Регистрация и управление аккаунтами пользователей и организаций |
|
||||
| `capital` | Управление интеллектуальной капитализацией и паевыми взносами |
|
||||
| `wallet` | Финансовые операции — взносы и возвраты |
|
||||
| `draft` | Шаблоны документов и их переводы в блокчейне |
|
||||
| `gateway` | Платёжный шлюз — депозиты и выводы |
|
||||
| `fund` | Управление фондами кооператива |
|
||||
| `ledger` | Бухгалтерская книга кооператива |
|
||||
| `meet` | Общие собрания пайщиков |
|
||||
| `marketplace` | Маркетплейс товаров и услуг |
|
||||
| `branch` | Создание и управление кооперативными участками |
|
||||
| `contributor` | Управление вкладчиками |
|
||||
| `loan` | Управление займами |
|
||||
|
||||
## Установка
|
||||
|
||||
```bash
|
||||
pnpm install --filter @coopenomics/contracts
|
||||
```
|
||||
|
||||
После выполнения команды и входа в контейнер, выполняйте команды компиляции внутри него.
|
||||
## Скрипты
|
||||
|
||||
| Скрипт | Описание |
|
||||
|--------|----------|
|
||||
| `pnpm run build:all` | Компиляция всех контрактов |
|
||||
| `pnpm run build:all:test` | Компиляция всех контрактов в тестовом режиме |
|
||||
| `pnpm run build:one -- <имя>` | Компиляция одного контракта по имени |
|
||||
| `pnpm run enter` | Вход в Docker-контейнер для ручной сборки |
|
||||
|
||||
> Все скрипты запускаются из корня монорепозитория через фильтр: `pnpm --filter @coopenomics/contracts run <скрипт>`
|
||||
|
||||
## Компиляция
|
||||
Для компиляции выполните команду:
|
||||
|
||||
```
|
||||
mkdir build
|
||||
cd build
|
||||
Компиляция выполняется внутри Docker-контейнера с необходимым EOSIO CDT:
|
||||
|
||||
```bash
|
||||
pnpm run enter # Вход в контейнер сборки
|
||||
mkdir build && cd build
|
||||
cmake -DBUILD_TARGET= ..
|
||||
make
|
||||
|
||||
```
|
||||
Компиляция всех контрактов произойдёт в папку build/contracts. Для указания конкретного контракта, который необходимо скомпилировать, используйте названия папок из директории cpp в качестве параметра -DBUILD_TARGET. Например, для контракта fund:
|
||||
|
||||
Для компиляции с тестами:
|
||||
|
||||
```bash
|
||||
cmake -DBUILD_TARGET= -DBUILD_TESTS=ON ..
|
||||
make
|
||||
```
|
||||
|
||||
Для компиляции одного контракта:
|
||||
|
||||
```bash
|
||||
cmake -DBUILD_TARGET=fund ..
|
||||
make
|
||||
```
|
||||
|
||||
Для компиляции всех контрактов передавайте пустой параметр -DBUILD_TARGET= . Без явной передачи параметра используйте кэшированный, ранее переданный параметр, что может привести к путанице. Поэтому, всегда явно передавайте параметр цели компиляции.
|
||||
|
||||
|
||||
## Загрузка
|
||||
Перед автоматической загрузкой контрактов в локальную версию блокчейна - скомпилируйте их как показано выше. После чего, выйдите из контейнера и выполните команду установки загрузчика в вашем окружении:
|
||||
## Архитектура
|
||||
|
||||
```
|
||||
pnpm install
|
||||
cpp/
|
||||
├── soviet/ # Совет кооператива
|
||||
│ ├── app/ # Оркестрация действий
|
||||
│ └── domain/ # Бизнес-логика
|
||||
│ ├── entity/ # Сущности, таблицы, индексы
|
||||
│ └── core/ # Межсущностная логика
|
||||
├── capital/ # Паевые взносы
|
||||
├── registrator/ # Регистрация аккаунтов
|
||||
├── wallet/ # Финансовые операции
|
||||
├── draft/ # Шаблоны документов
|
||||
├── gateway/ # Платёжный шлюз
|
||||
├── fund/ # Фонды кооператива
|
||||
├── ledger/ # Бухгалтерский учёт
|
||||
├── meet/ # Собрания пайщиков
|
||||
├── marketplace/ # Маркетплейс
|
||||
├── branch/ # Кооперативные участки
|
||||
├── contributor/ # Вкладчики
|
||||
└── loan/ # Займы
|
||||
build/
|
||||
└── contracts/ # Скомпилированные .wasm и .abi файлы
|
||||
```
|
||||
|
||||
Скопируйте файл .env-example в .env:
|
||||
### Принципы чистой архитектуры
|
||||
|
||||
```
|
||||
cp .env-example .env
|
||||
```
|
||||
- **`app/`** — уровень приложения; действия, вызываемые контрактом, которые оркестрируют бизнес-логику
|
||||
- **`domain/entity/`** — сущности с таблицами, индексами и простыми бизнес-функциями; запрещено вызывать другие сущности напрямую
|
||||
- **`domain/core/`** — корневая бизнес-логика, объединяющая несколько сущностей
|
||||
|
||||
И запустите загрузку:
|
||||
## Тестирование
|
||||
|
||||
```
|
||||
pnpm run cli boot
|
||||
```
|
||||
|
||||
Команда загрузки запустит блокчейн и произведет установку всех контрактов для начала работы. Параметры конфигурации блокчейна можно найти в файле scripts/restart.sh
|
||||
|
||||
Блокчейн будет запущен в контейнере и остановит свою работу при завершении работы программы. Данные сохраняются в ./blockchain-data корневой директории репозитория. Данные кошелька пишутся в ./wallet-data.
|
||||
|
||||
После остановки контейнера вы можете перезапустить его командной:
|
||||
|
||||
```
|
||||
pnpm run cli start
|
||||
|
||||
```
|
||||
|
||||
Для перезагрузки блокчейна вновь вызовите:
|
||||
|
||||
```
|
||||
pnpm run boot
|
||||
```
|
||||
|
||||
Последняя команда очистит всю историю цепочки блоков и перезагрузит локальный блокчейн до начального состояния.
|
||||
|
||||
## Кошелёк
|
||||
Для доступа командному кошельку cleos используйте команду
|
||||
|
||||
```
|
||||
pnpm run cli cleos
|
||||
|
||||
```
|
||||
|
||||
Для отображения полного списка команд воспользуйтесь:
|
||||
```
|
||||
pnpm run cli cleos --help
|
||||
```
|
||||
|
||||
Командный кошелёк позволяет совершать любые транзакции и просматривать любые таблицы в блокчейне. Для более удобной постоянной работы занесите алиас в файл ~/.bashrc для быстрого вызова:
|
||||
|
||||
```
|
||||
alias=cd <REPLACE_TO_YOUR_LOCAL_REPOSITORY_DIR/boot> && pnpm run cli cleos
|
||||
```
|
||||
|
||||
Подтяните изменения профиля:
|
||||
```
|
||||
source ~/.bashrc
|
||||
```
|
||||
|
||||
Теперь утилита cleos доступна простой командной:
|
||||
|
||||
```
|
||||
cleos get info
|
||||
```
|
||||
|
||||
|
||||
## Тесты
|
||||
|
||||
### Полные тесты
|
||||
Тесты по-умолчанию НЕ компилируются вместе с контрактами. Для компиляции необходимо передать флаг конфигурации сборки -DBUILD_TESTS=ON:
|
||||
```
|
||||
cmake -DBUILD_TARGET= -DBUILD_TESTS=ON
|
||||
```bash
|
||||
cd build
|
||||
cmake -DBUILD_TARGET= -DBUILD_TESTS=ON ..
|
||||
make
|
||||
```
|
||||
Все контракты пересоберутся вместе со всеми тестами.
|
||||
|
||||
Для запуска тестов (из директории build):
|
||||
```
|
||||
ctest --test-dir contracts/tests --output-on-failure
|
||||
```
|
||||
|
||||
### Выборочные тесты
|
||||
Для выборочного указания тестового файла используйте из директории build:
|
||||
Выборочный тест одного контракта:
|
||||
|
||||
```
|
||||
cmake -DBUILD_TARGET=fund -DBUILD_TESTS=ON -DTEST_TARGET=fund.tester.cpp ..
|
||||
```bash
|
||||
cmake -DBUILD_TARGET=fund -DTEST_TARGET=fund.tester.cpp ..
|
||||
make
|
||||
```
|
||||
|
||||
Команды пересоберет указанный контракт и его тест. Выборочная сборка и тесты значительно ускоряют отладку.
|
||||
|
||||
|
||||
### Информативные тесты
|
||||
Для большей детализации тестового процесса добавьте флаг -DVERBOSE=ON при конфигурации сборки:
|
||||
```
|
||||
cmake -DBUILD_TARGET=fund -DTEST_TARGET=fund.tester.cpp -DVERBOSE=ON ..
|
||||
make
|
||||
```
|
||||
|
||||
Дополнительная вербозность показывает отладочную информацию из тестов, включая консоль контрактов, в которую информация выводится через методы print.
|
||||
|
||||
|
||||
## Документация
|
||||
Для генерации документации используйте команду Doxygen версии от 1.9.3:
|
||||
```
|
||||
git submodule update --init --recursive
|
||||
doxygen
|
||||
```
|
||||
|
||||
Документация будет собрана в папке docs/html, откройте файл index.html в браузере:
|
||||
```
|
||||
open docs/html/index.html
|
||||
ctest --test-dir contracts/tests --output-on-failure
|
||||
```
|
||||
|
||||
## Лицензия
|
||||
Продукт Потребительского Кооператива "ВОСХОД" распространяется по лицензии BY-NC-SA 4.0.
|
||||
|
||||
Разрешено делиться, копировать и распространять материал на любом носителе и форме, адаптировать, делать ремиксы, видоизменять и создавать новое, опираясь на этот материал. При использовании, Вы должны обеспечить указание авторства, предоставить ссылку, и обозначить изменения, если таковые были сделаны. Если вы перерабатываете, преобразовываете материал или берёте его за основу для производного произведения, вы должны распространять переделанные вами части материала на условиях той же лицензии , в соответствии с которой распространяется оригинал. Запрещено коммерческое использование материала. Использование в коммерческих целях – это использование, в первую очередь направленное на получение коммерческого преимущества или денежного вознаграждения.
|
||||
|
||||
Юридический текст лицензии: https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode.ru
|
||||
[BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode.ru)
|
||||
|
||||
@@ -225,7 +225,7 @@ static constexpr uint64_t _capital_program_id = 4;
|
||||
|
||||
static constexpr uint64_t _deposit_expiration_seconds = 3600;
|
||||
|
||||
static constexpr uint64_t _decision_expiration = 3 * 86400;
|
||||
static constexpr uint64_t _decision_expiration = 30 * 86400;
|
||||
|
||||
static constexpr uint64_t _producers_percent = 900000; // 90%
|
||||
static constexpr uint64_t _fund_percent = 100000; // 10%
|
||||
|
||||
@@ -26,6 +26,18 @@ namespace Document {
|
||||
inline void add_document(std::vector<named_document>& docs, const name& name, const document2& doc) {
|
||||
docs.push_back({name, doc});
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Удалить документ по имени (если существует)
|
||||
* @param docs Вектор документов
|
||||
* @param name Имя документа для удаления
|
||||
*/
|
||||
inline void remove_document(std::vector<named_document>& docs, const name& name) {
|
||||
docs.erase(
|
||||
std::remove_if(docs.begin(), docs.end(), [&](const named_document& nd) { return nd.name == name; }),
|
||||
docs.end()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Найти документ по имени
|
||||
|
||||
@@ -154,6 +154,9 @@ struct [[eosio::table, eosio::contract(MARKETPLACE)]] request {
|
||||
eosio::time_point_sec warranty_delay_until;
|
||||
eosio::time_point_sec deadline_for_receipt;
|
||||
|
||||
eosio::name delivery_type; /*!< тип доставки: "internal" (между КУ) или "external" (внешний сервис) */
|
||||
eosio::name contribution_type; /*!< тип взноса: "share" (паевой) или "member" (членский - кооператив покупает) */
|
||||
|
||||
bool is_warranty_return = false;
|
||||
uint64_t warranty_return_id;
|
||||
|
||||
@@ -234,7 +237,9 @@ typedef eosio::multi_index<
|
||||
> requests_index;
|
||||
|
||||
static const std::set<eosio::name> marketplace_callback_actions = {
|
||||
"authoffs2c"_n, // авторизация сегмента contribute для направления OFFER → ORDER
|
||||
"authcontrib"_n, // авторизация взноса имуществом
|
||||
"authreturn"_n, // авторизация возврата имуществом
|
||||
"authoffs2c"_n, // авторизация сегмента contribute для направления OFFER → ORDER
|
||||
"authoffc2r"_n, // авторизация сегмента return для направления OFFER → ORDER
|
||||
"authordcont"_n, // авторизация сегмента contribute для направления ORDER → OFFER
|
||||
"authordret"_n, // авторизация сегмента return для направления ORDER → OFFER
|
||||
@@ -316,6 +321,9 @@ namespace DocumentNames {
|
||||
static constexpr const name WRETURN_ACT = "wreturnact"_n; // акт гарантийного возврата
|
||||
static constexpr const name WOFFER_ACT = "wofferact"_n; // акт предложения товара поставщику
|
||||
static constexpr const name WACCEPT_ACT = "wacceptact"_n; // акт принятия/отказа поставщика
|
||||
|
||||
// Документы уничтожения
|
||||
static constexpr const name DESTROY_ACT = "destroyact"_n; // акт уничтожения имущества
|
||||
}
|
||||
|
||||
} // namespace Marketplace
|
||||
@@ -10,6 +10,9 @@
|
||||
#include "src/deliver_on_offer/supply.cpp"
|
||||
#include "src/deliver_on_offer/supplcnf.cpp"
|
||||
|
||||
#include "src/deliver_on_offer/coopstock.cpp"
|
||||
#include "src/deliver_on_offer/acceptstock.cpp"
|
||||
#include "src/deliver_on_offer/reqreturn.cpp"
|
||||
#include "src/deliver_on_offer/delivered.cpp"
|
||||
#include "src/deliver_on_offer/receive.cpp"
|
||||
#include "src/deliver_on_offer/receivecnf.cpp"
|
||||
@@ -24,6 +27,14 @@
|
||||
#include "src/shipment/receiveshipm.cpp"
|
||||
#include "src/shipment/retransport.cpp"
|
||||
|
||||
// ORDER→OFFER direction
|
||||
#include "src/deliver_on_order/createorder.cpp"
|
||||
#include "src/deliver_on_order/respondoffer.cpp"
|
||||
|
||||
// Уничтожение и перепредложение
|
||||
#include "src/deliver_on_offer/destroy.cpp"
|
||||
#include "src/deliver_on_offer/reoffer.cpp"
|
||||
|
||||
// Диспуты
|
||||
#include "src/dispute_on_offer/dispute.cpp"
|
||||
#include "src/dispute_on_offer/wauthorize.cpp"
|
||||
|
||||
@@ -51,7 +51,7 @@ public:
|
||||
[[eosio::action]] void migrate();
|
||||
|
||||
// Действия для создания заявок
|
||||
[[eosio::action]] void orderoffer(eosio::name coopname, eosio::name receiver_braname, eosio::name username, checksum256 hash, uint64_t units, eosio::asset unit_cost, uint32_t product_lifecycle_secs, uint32_t warranty_period_secs, eosio::asset membership_fee_amount, eosio::asset cancellation_fee_amount, document2 product_return_statement, document2 convert_in, std::string meta);
|
||||
[[eosio::action]] void orderoffer(eosio::name coopname, eosio::name receiver_braname, eosio::name username, checksum256 hash, uint64_t units, eosio::asset unit_cost, uint32_t product_lifecycle_secs, uint32_t warranty_period_secs, eosio::asset membership_fee_amount, eosio::asset cancellation_fee_amount, document2 convert_in, eosio::name delivery_type, eosio::name contribution_type, std::string meta);
|
||||
|
||||
static void cancel_request(eosio::name coopname, eosio::name username, checksum256 request_hash);
|
||||
|
||||
@@ -73,6 +73,17 @@ public:
|
||||
[[eosio::action]] void receiveshipm(eosio::name coopname, checksum256 hash, document2 warehouse_receipt_act);
|
||||
[[eosio::action]] void retransport(eosio::name coopname, checksum256 completed_hash, eosio::name new_driver_username, eosio::name source_braname, eosio::name new_destination_braname, std::vector<checksum256> request_hashes, document2 transport_act_sender);
|
||||
|
||||
// ORDER→OFFER direction (заказчик публикует → поставщики откликаются)
|
||||
[[eosio::action]] void createorder(eosio::name coopname, eosio::name receiver_braname, eosio::name username, checksum256 hash, uint64_t units, eosio::asset unit_cost, uint32_t product_lifecycle_secs, uint32_t warranty_period_secs, eosio::asset membership_fee_amount, eosio::asset cancellation_fee_amount, document2 convert_in, eosio::name delivery_type, std::string meta);
|
||||
[[eosio::action]] void respondoffer(eosio::name coopname, eosio::name supplier_braname, eosio::name username, checksum256 order_hash, checksum256 offer_hash, uint64_t units, uint32_t product_lifecycle_secs, document2 contribution_statement, document2 convert_out);
|
||||
|
||||
// Имущество из запасов кооператива
|
||||
[[eosio::action]] void coopstock(eosio::name coopname, eosio::name braname, checksum256 hash, uint64_t units, eosio::asset unit_cost, uint32_t product_lifecycle_secs, uint32_t warranty_period_secs, eosio::asset membership_fee_amount, std::string meta);
|
||||
[[eosio::action]] void acceptstock(eosio::name coopname, eosio::name username, checksum256 request_hash, document2 convert_in, document2 return_statement);
|
||||
|
||||
// Запрос возврата перед получением
|
||||
[[eosio::action]] void reqreturn(eosio::name coopname, eosio::name username, checksum256 request_hash, document2 return_statement);
|
||||
|
||||
// Доставка заказчику
|
||||
[[eosio::action]] void delivered(eosio::name coopname, eosio::name username, checksum256 request_hash);
|
||||
[[eosio::action]] void receive(eosio::name coopname, eosio::name username, checksum256 request_hash, document2 document);
|
||||
@@ -81,6 +92,10 @@ public:
|
||||
[[eosio::action]] void decline(eosio::name coopname, eosio::name username, checksum256 request_hash, std::string meta);
|
||||
[[eosio::action]] void cancel(eosio::name coopname, eosio::name username, checksum256 request_hash);
|
||||
|
||||
// Уничтожение и перепредложение
|
||||
[[eosio::action]] void destroy(eosio::name coopname, checksum256 request_hash, document2 destruction_act);
|
||||
[[eosio::action]] void reoffer(eosio::name coopname, checksum256 request_hash, checksum256 new_hash, eosio::asset new_unit_cost, std::string new_meta);
|
||||
|
||||
// Методы для работы с диспутом (гарантийный возврат)
|
||||
[[eosio::action]] void dispute(eosio::name coopname, eosio::name username, checksum256 request_hash, document2 document);
|
||||
[[eosio::action]] void wauthorize(eosio::name coopname, checksum256 request_hash, uint64_t wreturn_decision_id, document2 wreturn_authorization, uint64_t wsupply_decision_id, document2 wsupply_authorization);
|
||||
|
||||
@@ -2,13 +2,15 @@
|
||||
\ingroup public_actions
|
||||
\brief Принятие заявки поставщиком.
|
||||
|
||||
@details Поставщик принимает заявку orderoffer на поставку имущества и предоставляет необходимые документы.
|
||||
@details Поставщик принимает заявку orderoffer на поставку имущества.
|
||||
При принятии создаётся ОДНО заявление в совет — на имущественный паевой взнос.
|
||||
Заявление на возврат будет создано позже, перед получением имущества заказчиком.
|
||||
|
||||
@param coopname Имя кооператива
|
||||
@param supplier_braname Имя кооперативного участка поставщика
|
||||
@param username Имя поставщика
|
||||
@param request_hash Хэш заявки
|
||||
@param convert_out Заявление на конвертацию
|
||||
@param convert_out Заявление на конвертацию в кошелек (сохраняется для будущего использования)
|
||||
@param product_contribution_statement Заявление на имущественный паевой взнос
|
||||
|
||||
@note Авторизация требуется от аккаунта: @p coopname
|
||||
@@ -24,23 +26,14 @@
|
||||
eosio::check(change.status == "active"_n, "Только активная заявка может быть принята");
|
||||
eosio::check(change.type == "orderoffer"_n, "Метод accept применим только к заявкам типа orderoffer");
|
||||
|
||||
// Проверяем существование кооперативного участка поставщика
|
||||
get_branch_or_fail(coopname, supplier_braname);
|
||||
|
||||
// Проверяем подписи документов
|
||||
verify_document_or_fail(convert_out);
|
||||
verify_document_or_fail(product_contribution_statement);
|
||||
|
||||
// Валидируем документы по registry_id (пока что нули)
|
||||
Document::validate_registry_id(convert_out, 0);
|
||||
Document::validate_registry_id(product_contribution_statement, 0);
|
||||
|
||||
// Получаем первоначальный документ возврата из заявки по имени
|
||||
document2 initial_return_statement;
|
||||
bool found = Document::find_document(change.documents, DocumentNames::RETURN_STMT, initial_return_statement);
|
||||
eosio::check(found, "В заявке отсутствует заявление на возврат имущества");
|
||||
|
||||
// Обновляем заявку
|
||||
auto change_itr = requests.find(change.id);
|
||||
eosio::check(change_itr != requests.end(), "Заявка не найдена для обновления");
|
||||
requests.modify(change_itr, _marketplace, [&](auto &o){
|
||||
@@ -48,20 +41,18 @@
|
||||
o.accepted_at = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
|
||||
o.product_contributor = username;
|
||||
o.supplier_braname = supplier_braname;
|
||||
// Добавляем новые документы с именами
|
||||
Document::add_document(o.documents, DocumentNames::CONVERT_TO, convert_out);
|
||||
Document::add_document(o.documents, DocumentNames::CONTRIB_STMT, product_contribution_statement);
|
||||
});
|
||||
|
||||
// Используем хэш заявки как идентификатор пакета решений
|
||||
checksum256 agenda_hash = change.hash;
|
||||
|
||||
// Отправляем ПЕРВЫЙ вопрос в совет - по заявлению на имущественный паевой взнос
|
||||
// Одно заявление в совет — на имущественный паевой взнос
|
||||
::Soviet::create_agenda(
|
||||
_marketplace,
|
||||
coopname,
|
||||
username,
|
||||
get_valid_soviet_action("authcontrib"_n), // авторизация взноса имуществом
|
||||
get_valid_soviet_action("authcontrib"_n),
|
||||
agenda_hash,
|
||||
_marketplace,
|
||||
Marketplace::get_valid_marketplace_action("authcontrib"_n),
|
||||
@@ -69,18 +60,4 @@
|
||||
product_contribution_statement,
|
||||
std::string("")
|
||||
);
|
||||
|
||||
// Отправляем ВТОРОЙ вопрос в совет - по заявлению на возврат имущества
|
||||
::Soviet::create_agenda(
|
||||
_marketplace,
|
||||
coopname,
|
||||
username,
|
||||
get_valid_soviet_action("authreturn"_n), // авторизация возврата имуществом
|
||||
agenda_hash,
|
||||
_marketplace,
|
||||
Marketplace::get_valid_marketplace_action("authreturn"_n),
|
||||
"declineacc"_n,
|
||||
initial_return_statement,
|
||||
std::string("")
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
\ingroup public_actions
|
||||
\brief Заказчик принимает предложение из запасов кооператива.
|
||||
|
||||
@details Заказчик блокирует средства и запрашивает возврат.
|
||||
Поскольку имущество уже на балансе — авторизация взноса не нужна,
|
||||
сразу создаётся заявление на возврат в совет.
|
||||
|
||||
@param coopname Имя кооператива
|
||||
@param username Имя заказчика
|
||||
@param request_hash Хэш заявки
|
||||
@param convert_in Заявление на конвертацию из кошелька
|
||||
@param return_statement Заявление на возврат паевого взноса имуществом
|
||||
|
||||
@note Авторизация требуется от аккаунта: @p coopname
|
||||
**/
|
||||
[[eosio::action]] void marketplace::acceptstock(eosio::name coopname, eosio::name username, checksum256 request_hash, document2 convert_in, document2 return_statement) {
|
||||
require_auth(coopname);
|
||||
|
||||
requests_index requests(_marketplace, coopname.value);
|
||||
auto change_opt = Marketplace::get_request_by_hash(coopname, request_hash);
|
||||
eosio::check(change_opt.has_value(), "Заявка не найдена");
|
||||
auto change = change_opt.value();
|
||||
|
||||
eosio::check(change.type == "coopstock"_n, "Метод acceptstock применим только к заявкам типа coopstock");
|
||||
eosio::check(change.status == "delivered"_n, "Только доставленная заявка может быть принята");
|
||||
|
||||
get_participant_or_fail(coopname, username);
|
||||
|
||||
verify_document_or_fail(convert_in);
|
||||
verify_document_or_fail(return_statement);
|
||||
|
||||
auto change_itr = requests.find(change.id);
|
||||
eosio::check(change_itr != requests.end(), "Заявка не найдена для обновления");
|
||||
|
||||
requests.modify(change_itr, _marketplace, [&](auto &o) {
|
||||
o.money_contributor = username;
|
||||
o.status = "reqreturn"_n;
|
||||
Document::add_document(o.documents, DocumentNames::CONVERT_FROM, convert_in);
|
||||
Document::add_document(o.documents, DocumentNames::RETURN_STMT, return_statement);
|
||||
});
|
||||
|
||||
// Блокируем средства заказчика
|
||||
std::string memo = "Блокировка средств для заказа из запасов кооператива №" + std::to_string(change.id);
|
||||
Wallet::sub_available_funds(_marketplace, coopname, username, change.total_cost, _wallet_program, memo);
|
||||
Wallet::add_available_funds(_marketplace, coopname, username, change.total_cost, _marketplace_program, memo);
|
||||
Wallet::block_funds(_marketplace, coopname, username, change.total_cost, _marketplace_program, memo);
|
||||
|
||||
// Сразу отправляем заявление на возврат в совет
|
||||
::Soviet::create_agenda(
|
||||
_marketplace,
|
||||
coopname,
|
||||
username,
|
||||
get_valid_soviet_action("authreturn"_n),
|
||||
change.hash,
|
||||
_marketplace,
|
||||
Marketplace::get_valid_marketplace_action("authreturn"_n),
|
||||
"declineacc"_n,
|
||||
return_statement,
|
||||
std::string("")
|
||||
);
|
||||
};
|
||||
@@ -2,7 +2,9 @@
|
||||
\ingroup public_actions
|
||||
\brief Авторизация заявления на имущественный паевой взнос советом кооператива.
|
||||
|
||||
@details Совет кооператива авторизует заявление на имущественный паевой взнос.
|
||||
@details Совет авторизует заявление поставщика на взнос имуществом.
|
||||
После авторизации заявка переходит в статус authorized — поставщик может начинать поставку.
|
||||
Заявление на возврат будет подано и авторизовано отдельно, перед выдачей заказчику.
|
||||
|
||||
@param coopname Имя кооператива
|
||||
@param request_hash Хэш заявки
|
||||
@@ -20,26 +22,13 @@
|
||||
|
||||
eosio::check(change.status == "accepted"_n, "Только принятая заявка может быть авторизована");
|
||||
|
||||
// Проверяем подпись документа
|
||||
verify_document_or_fail(authorization);
|
||||
|
||||
// Валидируем документ по registry_id (пока что ноль)
|
||||
Document::validate_registry_id(authorization, 0);
|
||||
|
||||
// Обновляем заявку
|
||||
auto change_itr = requests.find(change.id);
|
||||
eosio::check(change_itr != requests.end(), "Заявка не найдена для обновления");
|
||||
requests.modify(change_itr, _marketplace, [&](auto &o){
|
||||
// Добавляем документ авторизации взноса с именем
|
||||
Document::add_document(o.documents, DocumentNames::CONTRIB_AUTH, authorization);
|
||||
|
||||
// Проверяем, есть ли уже оба документа авторизации
|
||||
bool has_contrib_auth = Document::has_document(o.documents, DocumentNames::CONTRIB_AUTH);
|
||||
bool has_return_auth = Document::has_document(o.documents, DocumentNames::RETURN_AUTH);
|
||||
|
||||
// Если получили оба документа авторизации, переводим в статус authorized
|
||||
if (has_contrib_auth && has_return_auth) {
|
||||
o.status = "authorized"_n;
|
||||
}
|
||||
o.status = "authorized"_n;
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
/**
|
||||
\ingroup public_actions
|
||||
\brief Авторизация заявления на возврат имущества советом кооператива.
|
||||
\brief Авторизация заявления на возврат паевого взноса имуществом.
|
||||
|
||||
@details Совет кооператива авторизует заявление на возврат имущества.
|
||||
@details Совет авторизует заявление заказчика на возврат паевого взноса.
|
||||
Вызывается ПОСЛЕ подачи заявления заказчиком (requestreturn),
|
||||
когда имущество уже доставлено на КУ и готово к выдаче.
|
||||
|
||||
@param coopname Имя кооператива
|
||||
@param request_hash Хэш заявки
|
||||
@@ -18,28 +20,16 @@
|
||||
eosio::check(change_opt.has_value(), "Заявка не найдена");
|
||||
auto change = change_opt.value();
|
||||
|
||||
eosio::check(change.status == "accepted"_n, "Только принятая заявка может быть авторизована");
|
||||
// Заявка должна быть в статусе delivered (имущество доставлено и ждёт выдачи)
|
||||
eosio::check(change.status == "reqreturn"_n, "Возврат можно авторизовать только для заявки с запрошенным возвратом");
|
||||
|
||||
// Проверяем подпись документа
|
||||
verify_document_or_fail(authorization);
|
||||
|
||||
// Валидируем документ по registry_id (пока что ноль)
|
||||
Document::validate_registry_id(authorization, 0);
|
||||
|
||||
// Обновляем заявку
|
||||
auto change_itr = requests.find(change.id);
|
||||
eosio::check(change_itr != requests.end(), "Заявка не найдена для обновления");
|
||||
requests.modify(change_itr, _marketplace, [&](auto &o){
|
||||
// Добавляем документ авторизации возврата с именем
|
||||
Document::add_document(o.documents, DocumentNames::RETURN_AUTH, authorization);
|
||||
|
||||
// Проверяем, есть ли уже оба документа авторизации
|
||||
bool has_contrib_auth = Document::has_document(o.documents, DocumentNames::CONTRIB_AUTH);
|
||||
bool has_return_auth = Document::has_document(o.documents, DocumentNames::RETURN_AUTH);
|
||||
|
||||
// Если получили оба документа авторизации, переводим в статус authorized
|
||||
if (has_contrib_auth && has_return_auth) {
|
||||
o.status = "authorized"_n;
|
||||
}
|
||||
o.status = "retauthorized"_n;
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
\ingroup public_actions
|
||||
\brief Создать заявку на имущество из запасов кооператива.
|
||||
|
||||
@details Кооператив (представитель КУ) создаёт предложение на имущество,
|
||||
которое УЖЕ находится на балансе кооператива. В этом случае:
|
||||
- Не требуется внешний поставщик
|
||||
- Не требуется взнос имуществом (имущество уже на балансе)
|
||||
- Не требуется авторизация взноса советом
|
||||
- Требуется только авторизация возврата при получении заказчиком
|
||||
|
||||
Заявка создаётся со статусом supplied2 (имущество уже на складе КУ).
|
||||
Это упрощённый путь для реализации уценённого или перепредложенного имущества.
|
||||
|
||||
@param coopname Имя кооператива
|
||||
@param braname КУ, где хранится имущество
|
||||
@param units Количество единиц
|
||||
@param unit_cost Стоимость за единицу (может быть уценённой)
|
||||
@param hash Хэш заявки
|
||||
@param product_lifecycle_secs Срок годности
|
||||
@param warranty_period_secs Гарантийный срок
|
||||
@param membership_fee_amount Членский взнос
|
||||
@param meta Метаданные (описание, фото)
|
||||
|
||||
@note Авторизация требуется от аккаунта: @p coopname
|
||||
**/
|
||||
[[eosio::action]] void marketplace::coopstock(eosio::name coopname, eosio::name braname, checksum256 hash, uint64_t units, eosio::asset unit_cost, uint32_t product_lifecycle_secs, uint32_t warranty_period_secs, eosio::asset membership_fee_amount, std::string meta) {
|
||||
require_auth(coopname);
|
||||
|
||||
auto existing = get_request_by_hash(coopname, hash);
|
||||
eosio::check(!existing.has_value(), "Заявка с таким хэшем уже существует");
|
||||
|
||||
auto coop = get_cooperative_or_fail(coopname);
|
||||
eosio::check(unit_cost.symbol == coop.initial.symbol, "Неверный символ токена");
|
||||
eosio::check(membership_fee_amount.symbol == coop.initial.symbol, "Неверный символ токена для членского взноса");
|
||||
eosio::check(units > 0, "Количество единиц должно быть больше нуля");
|
||||
|
||||
get_branch_or_fail(coopname, braname);
|
||||
|
||||
eosio::asset base_cost = unit_cost * units;
|
||||
eosio::asset total_cost = base_cost + membership_fee_amount;
|
||||
eosio::asset zero_fee = eosio::asset(0, coop.initial.symbol);
|
||||
|
||||
requests_index requests(_marketplace, coopname.value);
|
||||
uint64_t request_id = get_global_id(_marketplace, "requests"_n);
|
||||
|
||||
requests.emplace(_marketplace, [&](auto &i) {
|
||||
i.id = request_id;
|
||||
i.hash = hash;
|
||||
i.type = "coopstock"_n;
|
||||
i.username = coopname;
|
||||
i.coopname = coopname;
|
||||
i.status = "delivered"_n;
|
||||
i.units = units;
|
||||
i.unit_cost = unit_cost;
|
||||
i.base_cost = base_cost;
|
||||
i.membership_fee_amount = membership_fee_amount;
|
||||
i.total_cost = total_cost;
|
||||
i.product_lifecycle_secs = product_lifecycle_secs;
|
||||
i.warranty_period_secs = warranty_period_secs;
|
||||
i.money_contributor = ""_n;
|
||||
i.product_contributor = coopname;
|
||||
i.meta = meta;
|
||||
i.created_at = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
|
||||
i.supplied_at = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
|
||||
i.delivered_at = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
|
||||
i.cancellation_fee_amount = zero_fee;
|
||||
i.receiver_braname = braname;
|
||||
i.supplier_braname = braname;
|
||||
i.warehouse = braname;
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
\ingroup public_actions
|
||||
\brief Уничтожение просроченного имущества.
|
||||
|
||||
@details Если имущество не получено заказчиком в срок (deadline_for_receipt)
|
||||
и его срок годности истёк, кооператив может уничтожить его.
|
||||
Требуется комиссионный акт и видео/фото подтверждение.
|
||||
|
||||
При уничтожении:
|
||||
- Заблокированные средства заказчика разблокируются за вычетом cancellation_fee
|
||||
- cancellation_fee зачисляется в фонд членских взносов
|
||||
- Из фонда членских взносов поставщику выплачивается стоимость
|
||||
|
||||
@param coopname Имя кооператива
|
||||
@param request_hash Хэш заявки
|
||||
@param destruction_act Акт уничтожения имущества (с комиссией)
|
||||
|
||||
@note Авторизация требуется от аккаунта: @p coopname
|
||||
**/
|
||||
[[eosio::action]] void marketplace::destroy(eosio::name coopname, checksum256 request_hash, document2 destruction_act) {
|
||||
require_auth(coopname);
|
||||
|
||||
requests_index requests(_marketplace, coopname.value);
|
||||
auto change_opt = Marketplace::get_request_by_hash(coopname, request_hash);
|
||||
eosio::check(change_opt.has_value(), "Заявка не найдена");
|
||||
auto change = change_opt.value();
|
||||
|
||||
// Уничтожение возможно только для доставленного или непринятого имущества
|
||||
eosio::check(
|
||||
change.status == "delivered"_n || change.status == "supplied2"_n,
|
||||
"Уничтожение возможно только для доставленного/поставленного имущества"
|
||||
);
|
||||
|
||||
// Проверяем что срок получения истёк (если был установлен)
|
||||
if (change.deadline_for_receipt.sec_since_epoch() > 0) {
|
||||
eosio::check(
|
||||
eosio::current_time_point().sec_since_epoch() > change.deadline_for_receipt.sec_since_epoch(),
|
||||
"Срок получения ещё не истёк"
|
||||
);
|
||||
}
|
||||
|
||||
verify_document_or_fail(destruction_act);
|
||||
|
||||
auto change_itr = requests.find(change.id);
|
||||
eosio::check(change_itr != requests.end(), "Заявка не найдена");
|
||||
|
||||
// Возврат средств заказчику за вычетом штрафа
|
||||
if (change.money_contributor.value != 0) {
|
||||
eosio::asset refund = change.total_cost - change.cancellation_fee_amount;
|
||||
|
||||
if (refund.amount > 0) {
|
||||
std::string memo = "Возврат за уничтоженное имущество по заявке №" + std::to_string(change.id);
|
||||
Wallet::unblock_funds(_marketplace, coopname, change.money_contributor, change.total_cost, _marketplace_program, memo);
|
||||
Wallet::sub_available_funds(_marketplace, coopname, change.money_contributor, change.total_cost, _marketplace_program, memo);
|
||||
Wallet::add_available_funds(_marketplace, coopname, change.money_contributor, refund, _wallet_program, memo);
|
||||
}
|
||||
|
||||
// Штраф — в фонд членских взносов
|
||||
if (change.cancellation_fee_amount.amount > 0) {
|
||||
eosio::action(
|
||||
eosio::permission_level{ _marketplace, "active"_n },
|
||||
_fund,
|
||||
"spreadamount"_n,
|
||||
std::make_tuple(coopname, change.cancellation_fee_amount)
|
||||
).send();
|
||||
}
|
||||
}
|
||||
|
||||
// Выплата поставщику (если был)
|
||||
if (change.product_contributor.value != 0 && change.product_contributor != coopname) {
|
||||
std::string memo = "Выплата поставщику за уничтоженное имущество по заявке №" + std::to_string(change.id);
|
||||
Wallet::unblock_funds(_marketplace, coopname, change.product_contributor, change.base_cost, _marketplace_program, memo);
|
||||
Wallet::sub_available_funds(_marketplace, coopname, change.product_contributor, change.base_cost, _marketplace_program, memo);
|
||||
Wallet::add_available_funds(_marketplace, coopname, change.product_contributor, change.base_cost, _wallet_program, memo);
|
||||
}
|
||||
|
||||
// Удаляем заявку
|
||||
requests.erase(change_itr);
|
||||
};
|
||||
@@ -1,79 +1,64 @@
|
||||
/**
|
||||
\ingroup public_actions
|
||||
\brief Создать заявку orderoffer - заказчик создает заявку на поставку товара от поставщика.
|
||||
*
|
||||
* Данный метод позволяет заказчику создать заявку на поставку товара от поставщика.
|
||||
* Заявка содержит всю информацию о товаре, стоимости, документах и сразу блокирует средства заказчика.
|
||||
*
|
||||
* @param coopname Имя кооператива
|
||||
* @param receiver_braname Имя кооперативного участка заказчика для получения товара
|
||||
* @param username Имя заказчика
|
||||
* @param hash Хэш заявки (уникальный идентификатор)
|
||||
* @param units Количество единиц товара
|
||||
* @param unit_cost Цена за единицу товара
|
||||
* @param product_lifecycle_secs Время жизни продукта
|
||||
* @param warranty_period_secs Гарантийный срок в секундах
|
||||
* @param membership_fee_amount Сумма членского взноса
|
||||
* @param cancellation_fee_amount Сумма комиссии за отмену заявки
|
||||
* @param product_return_statement Заявление на возврат паевого взноса имуществом
|
||||
* @param convert_in Заявление на конвертацию из кошелька в маркетплейс
|
||||
* @param meta Метаданные о заявке
|
||||
*
|
||||
* @note Авторизация требуется от аккаунта: @p coopname
|
||||
\brief Создать заявку orderoffer — заказчик создаёт заявку на поставку товара.
|
||||
|
||||
@details Заказчик создаёт заявку на поставку. Средства блокируются сразу.
|
||||
Заявление на возврат НЕ подаётся на этом этапе — оно будет подано перед получением
|
||||
(requestreturn), когда известен точный вес/состав имущества.
|
||||
|
||||
@param coopname Имя кооператива
|
||||
@param receiver_braname КУ заказчика для получения товара
|
||||
@param username Имя заказчика
|
||||
@param hash Хэш заявки
|
||||
@param units Количество единиц товара
|
||||
@param unit_cost Цена за единицу
|
||||
@param product_lifecycle_secs Срок годности продукта
|
||||
@param warranty_period_secs Гарантийный срок
|
||||
@param membership_fee_amount Членский взнос
|
||||
@param cancellation_fee_amount Штраф за отмену
|
||||
@param convert_in Заявление на конвертацию из кошелька в маркетплейс
|
||||
@param meta Метаданные
|
||||
|
||||
@note Авторизация требуется от аккаунта: @p coopname
|
||||
*/
|
||||
[[eosio::action]] void marketplace::orderoffer(eosio::name coopname, eosio::name receiver_braname, eosio::name username, checksum256 hash, uint64_t units, eosio::asset unit_cost, uint32_t product_lifecycle_secs, uint32_t warranty_period_secs, eosio::asset membership_fee_amount, eosio::asset cancellation_fee_amount, document2 product_return_statement, document2 convert_in, std::string meta) {
|
||||
[[eosio::action]] void marketplace::orderoffer(eosio::name coopname, eosio::name receiver_braname, eosio::name username, checksum256 hash, uint64_t units, eosio::asset unit_cost, uint32_t product_lifecycle_secs, uint32_t warranty_period_secs, eosio::asset membership_fee_amount, eosio::asset cancellation_fee_amount, document2 convert_in, eosio::name delivery_type, eosio::name contribution_type, std::string meta) {
|
||||
require_auth(coopname);
|
||||
|
||||
// Проверяем, что заявка с таким хэшем не существует
|
||||
auto existing_request = get_request_by_hash(coopname, hash);
|
||||
eosio::check(!existing_request.has_value(), "Заявка с таким хэшем уже существует");
|
||||
|
||||
// Проверяем, что символ токена совпадает с символом токена кооператива
|
||||
auto coop = get_cooperative_or_fail(coopname);
|
||||
eosio::check(unit_cost.symbol == coop.initial.symbol, "Неверный символ токена");
|
||||
eosio::check(membership_fee_amount.symbol == coop.initial.symbol, "Неверный символ токена для членского взноса");
|
||||
eosio::check(cancellation_fee_amount.symbol == coop.initial.symbol, "Неверный символ токена для комиссии отмены");
|
||||
eosio::check(units > 0, "Количество единиц в заявке должно быть больше нуля");
|
||||
eosio::check(units > 0, "Количество единиц должно быть больше нуля");
|
||||
eosio::check(unit_cost.amount >= 0, "Цена не может быть отрицательной");
|
||||
eosio::check(membership_fee_amount.amount >= 0, "Членский взнос не может быть отрицательным");
|
||||
eosio::check(cancellation_fee_amount.amount >= 0, "Комиссия за отмену не может быть отрицательной");
|
||||
|
||||
// Проверяем, что пользователь является пайщиком кооператива
|
||||
get_participant_or_fail(coopname, username);
|
||||
|
||||
// Проверяем существование кооперативного участка заказчика
|
||||
get_branch_or_fail(coopname, receiver_braname);
|
||||
|
||||
// Проверяем существование программы маркетплейса
|
||||
auto program = get_program_or_fail(coopname, _marketplace_program_id);
|
||||
|
||||
// Гарантийный срок возврата должен быть установлен
|
||||
eosio::check(product_lifecycle_secs > 0, "Гарантийный срок возврата для имущества должен быть установлен");
|
||||
eosio::check(product_lifecycle_secs > 0, "Срок годности должен быть установлен");
|
||||
eosio::check(warranty_period_secs > 0, "Гарантийный срок должен быть больше нуля");
|
||||
|
||||
// Проводим проверку подписи документов
|
||||
verify_document_or_fail(product_return_statement);
|
||||
verify_document_or_fail(convert_in);
|
||||
|
||||
// Валидируем документы по registry_id (пока что нули как просил пользователь)
|
||||
Document::validate_registry_id(product_return_statement, 0);
|
||||
Document::validate_registry_id(convert_in, 0);
|
||||
|
||||
// Рассчитываем стоимость
|
||||
eosio::asset base_cost = unit_cost * units;
|
||||
eosio::asset total_cost = base_cost + membership_fee_amount;
|
||||
|
||||
// Проверяем что комиссия за отмену не превышает общую стоимость
|
||||
eosio::check(cancellation_fee_amount <= total_cost, "Комиссия за отмену не может превышать общую стоимость заявки");
|
||||
eosio::check(cancellation_fee_amount <= total_cost, "Комиссия за отмену не может превышать общую стоимость");
|
||||
eosio::check(delivery_type == "internal"_n || delivery_type == "external"_n, "Тип доставки: internal или external");
|
||||
eosio::check(contribution_type == "share"_n || contribution_type == "member"_n, "Тип взноса: share или member");
|
||||
|
||||
requests_index requests(_marketplace, coopname.value);
|
||||
uint64_t request_id = get_global_id(_marketplace, "requests"_n);
|
||||
|
||||
std::string memo = "Начало поставки по программе №" + std::to_string(_marketplace_program_id) + " с ID: " + std::to_string(request_id);
|
||||
|
||||
// Создаем вектор именованных документов
|
||||
std::vector<Document::named_document> documents;
|
||||
Document::add_document(documents, DocumentNames::RETURN_STMT, product_return_statement);
|
||||
Document::add_document(documents, DocumentNames::CONVERT_FROM, convert_in);
|
||||
|
||||
requests.emplace(_marketplace, [&](auto &i) {
|
||||
@@ -95,16 +80,13 @@
|
||||
i.created_at = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
|
||||
i.cancellation_fee_amount = cancellation_fee_amount;
|
||||
i.receiver_braname = receiver_braname;
|
||||
i.delivery_type = delivery_type;
|
||||
i.contribution_type = contribution_type;
|
||||
i.documents = documents;
|
||||
});
|
||||
|
||||
// Конвертируем и блокируем средства заказчика
|
||||
std::string convert_memo = "Конвертация средств из ЦПП 'Цифровой Кошелёк' в ЦПП 'Маркетплейс' для заказа №" + std::to_string(request_id);
|
||||
std::string convert_memo = "Конвертация средств для заказа №" + std::to_string(request_id);
|
||||
Wallet::sub_available_funds(_marketplace, coopname, username, total_cost, _wallet_program, convert_memo);
|
||||
|
||||
// Добавляем средства на ЦПП маркетплейса
|
||||
Wallet::add_available_funds(_marketplace, coopname, username, total_cost, _marketplace_program, convert_memo);
|
||||
|
||||
// Блокируем средства на программе маркетплейса
|
||||
Wallet::block_funds(_marketplace, coopname, username, total_cost, _marketplace_program, memo);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
\ingroup public_actions
|
||||
\brief Получение товара заказчиком.
|
||||
|
||||
@details Заказчик приходит на КУ для получения имущества. Председатель КУ подписывает акт и передаёт имущество заказчику.
|
||||
@details Заказчик приходит на КУ для получения имущества.
|
||||
Председатель КУ подписывает акт и передаёт имущество заказчику.
|
||||
Получение возможно ТОЛЬКО после авторизации возврата советом (статус retauthorized).
|
||||
|
||||
@param coopname Имя кооператива
|
||||
@param username Имя заказчика
|
||||
@param username Имя заказчика (председатель КУ подписывает от лица кооператива)
|
||||
@param request_hash Хэш заявки
|
||||
@param document Акт получения имущества
|
||||
@param document Акт получения имущества (подпись председателя КУ)
|
||||
|
||||
@note Авторизация требуется от аккаунта: @p coopname
|
||||
**/
|
||||
@@ -19,21 +21,17 @@
|
||||
eosio::check(change_opt.has_value(), "Заявка не найдена");
|
||||
auto change = change_opt.value();
|
||||
|
||||
eosio::check(change.status == "delivered"_n, "Получение возможно только после статуса delivered");
|
||||
// Получение возможно только после авторизации возврата советом
|
||||
eosio::check(change.status == "retauthorized"_n, "Получение возможно только после авторизации возврата советом");
|
||||
eosio::check(change.money_contributor == username, "Недостаточно прав доступа");
|
||||
|
||||
// Проверяем подпись документа
|
||||
verify_document_or_fail(document);
|
||||
|
||||
// Валидируем документ по registry_id (пока что ноль)
|
||||
Document::validate_registry_id(document, 0);
|
||||
|
||||
// Обновляем заявку
|
||||
auto change_itr = requests.find(change.id);
|
||||
eosio::check(change_itr != requests.end(), "Заявка не найдена для обновления");
|
||||
requests.modify(change_itr, _marketplace, [&](auto &o){
|
||||
o.status = "received1"_n;
|
||||
// Добавляем акт получения с именем
|
||||
Document::add_document(o.documents, DocumentNames::RECEIVE_ACT, document);
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
\ingroup public_actions
|
||||
\brief Перепредложение непринятого имущества по пути coopstock.
|
||||
|
||||
@details Если заказчик не получил имущество в срок, но срок годности ещё не истёк,
|
||||
председатель КУ может перепредложить его другим пайщикам. Это создаёт новую заявку
|
||||
типа coopstock на основе текущей, с возможностью изменения цены.
|
||||
|
||||
Текущая заявка закрывается (средства возвращаются заказчику за вычетом штрафа),
|
||||
создаётся новая заявка coopstock.
|
||||
|
||||
@param coopname Имя кооператива
|
||||
@param request_hash Хэш текущей заявки
|
||||
@param new_hash Хэш новой заявки
|
||||
@param new_unit_cost Новая цена за единицу (может быть уценена)
|
||||
@param new_meta Новые метаданные
|
||||
|
||||
@note Авторизация требуется от аккаунта: @p coopname
|
||||
**/
|
||||
[[eosio::action]] void marketplace::reoffer(eosio::name coopname, checksum256 request_hash, checksum256 new_hash, eosio::asset new_unit_cost, std::string new_meta) {
|
||||
require_auth(coopname);
|
||||
|
||||
requests_index requests(_marketplace, coopname.value);
|
||||
auto change_opt = Marketplace::get_request_by_hash(coopname, request_hash);
|
||||
eosio::check(change_opt.has_value(), "Заявка не найдена");
|
||||
auto change = change_opt.value();
|
||||
|
||||
eosio::check(
|
||||
change.status == "delivered"_n || change.status == "supplied2"_n,
|
||||
"Перепредложение возможно только для доставленного имущества"
|
||||
);
|
||||
|
||||
auto coop = get_cooperative_or_fail(coopname);
|
||||
eosio::check(new_unit_cost.symbol == coop.initial.symbol, "Неверный символ токена");
|
||||
|
||||
// Возврат средств заказчику за вычетом штрафа (если заказчик был)
|
||||
if (change.money_contributor.value != 0) {
|
||||
eosio::asset refund = change.total_cost - change.cancellation_fee_amount;
|
||||
|
||||
if (refund.amount > 0) {
|
||||
std::string memo = "Возврат при перепредложении имущества по заявке №" + std::to_string(change.id);
|
||||
Wallet::unblock_funds(_marketplace, coopname, change.money_contributor, change.total_cost, _marketplace_program, memo);
|
||||
Wallet::sub_available_funds(_marketplace, coopname, change.money_contributor, change.total_cost, _marketplace_program, memo);
|
||||
Wallet::add_available_funds(_marketplace, coopname, change.money_contributor, refund, _wallet_program, memo);
|
||||
}
|
||||
|
||||
if (change.cancellation_fee_amount.amount > 0) {
|
||||
eosio::action(
|
||||
eosio::permission_level{ _marketplace, "active"_n },
|
||||
_fund,
|
||||
"spreadamount"_n,
|
||||
std::make_tuple(coopname, change.cancellation_fee_amount)
|
||||
).send();
|
||||
}
|
||||
}
|
||||
|
||||
// Удаляем старую заявку
|
||||
auto change_itr = requests.find(change.id);
|
||||
requests.erase(change_itr);
|
||||
|
||||
// Создаём новую заявку типа coopstock
|
||||
eosio::asset zero_fee = eosio::asset(0, coop.initial.symbol);
|
||||
eosio::asset new_base_cost = new_unit_cost * change.units;
|
||||
eosio::asset new_total_cost = new_base_cost + change.membership_fee_amount;
|
||||
|
||||
auto new_existing = get_request_by_hash(coopname, new_hash);
|
||||
eosio::check(!new_existing.has_value(), "Заявка с новым хэшем уже существует");
|
||||
|
||||
uint64_t new_id = get_global_id(_marketplace, "requests"_n);
|
||||
|
||||
requests.emplace(_marketplace, [&](auto &i) {
|
||||
i.id = new_id;
|
||||
i.hash = new_hash;
|
||||
i.type = "coopstock"_n;
|
||||
i.username = coopname;
|
||||
i.coopname = coopname;
|
||||
i.status = "delivered"_n;
|
||||
i.units = change.units;
|
||||
i.unit_cost = new_unit_cost;
|
||||
i.base_cost = new_base_cost;
|
||||
i.membership_fee_amount = change.membership_fee_amount;
|
||||
i.total_cost = new_total_cost;
|
||||
i.product_lifecycle_secs = change.product_lifecycle_secs;
|
||||
i.warranty_period_secs = change.warranty_period_secs;
|
||||
i.money_contributor = ""_n;
|
||||
i.product_contributor = coopname;
|
||||
i.meta = new_meta;
|
||||
i.created_at = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
|
||||
i.delivered_at = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
|
||||
i.cancellation_fee_amount = zero_fee;
|
||||
i.receiver_braname = change.warehouse;
|
||||
i.supplier_braname = change.warehouse;
|
||||
i.warehouse = change.warehouse;
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
\ingroup public_actions
|
||||
\brief Запрос на возврат паевого взноса имуществом — перед получением.
|
||||
|
||||
@details Заказчик подаёт заявление на возврат паевого взноса имуществом.
|
||||
Вызывается когда имущество доставлено на КУ получателя (статус delivered).
|
||||
Председатель КУ фиксирует факт выдачи и точные параметры имущества (вес, количество).
|
||||
|
||||
Создаёт пункт повестки для совета на авторизацию возврата.
|
||||
|
||||
@param coopname Имя кооператива
|
||||
@param username Имя заказчика
|
||||
@param request_hash Хэш заявки
|
||||
@param return_statement Заявление на возврат паевого взноса (обновлённое, с точным весом)
|
||||
|
||||
@note Авторизация требуется от аккаунта: @p coopname
|
||||
**/
|
||||
[[eosio::action]] void marketplace::reqreturn(eosio::name coopname, eosio::name username, checksum256 request_hash, document2 return_statement) {
|
||||
require_auth(coopname);
|
||||
|
||||
requests_index requests(_marketplace, coopname.value);
|
||||
auto change_opt = Marketplace::get_request_by_hash(coopname, request_hash);
|
||||
eosio::check(change_opt.has_value(), "Заявка не найдена");
|
||||
auto change = change_opt.value();
|
||||
|
||||
// Имущество должно быть доставлено и готово к выдаче
|
||||
eosio::check(change.status == "delivered"_n, "Запрос возврата возможен только для доставленного имущества");
|
||||
eosio::check(change.money_contributor == username, "Только заказчик может запросить возврат");
|
||||
|
||||
verify_document_or_fail(return_statement);
|
||||
Document::validate_registry_id(return_statement, 0);
|
||||
|
||||
auto change_itr = requests.find(change.id);
|
||||
eosio::check(change_itr != requests.end(), "Заявка не найдена для обновления");
|
||||
|
||||
// Обновляем/добавляем заявление на возврат с актуальными данными
|
||||
requests.modify(change_itr, _marketplace, [&](auto &o){
|
||||
// Удаляем старое заявление на возврат если было
|
||||
Document::remove_document(o.documents, DocumentNames::RETURN_STMT);
|
||||
Document::add_document(o.documents, DocumentNames::RETURN_STMT, return_statement);
|
||||
o.status = "reqreturn"_n;
|
||||
});
|
||||
|
||||
// Отправляем заявление на возврат в совет
|
||||
::Soviet::create_agenda(
|
||||
_marketplace,
|
||||
coopname,
|
||||
username,
|
||||
get_valid_soviet_action("authreturn"_n),
|
||||
change.hash,
|
||||
_marketplace,
|
||||
Marketplace::get_valid_marketplace_action("authreturn"_n),
|
||||
"declineacc"_n,
|
||||
return_statement,
|
||||
std::string("")
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
\ingroup public_actions
|
||||
\brief Создать заказ — заказчик публикует запрос «поставьте мне».
|
||||
|
||||
@details Заказчик создаёт заказ типа order с указанием желаемого количества,
|
||||
стоимости, характеристик. Средства блокируются сразу.
|
||||
Поставщики могут откликнуться встречными предложениями (offer).
|
||||
|
||||
@param coopname Имя кооператива
|
||||
@param receiver_braname КУ заказчика
|
||||
@param username Имя заказчика
|
||||
@param hash Хэш заказа
|
||||
@param units Желаемое количество
|
||||
@param unit_cost Предлагаемая цена за единицу
|
||||
@param product_lifecycle_secs Желаемый срок годности
|
||||
@param warranty_period_secs Желаемый гарантийный срок
|
||||
@param membership_fee_amount Членский взнос
|
||||
@param cancellation_fee_amount Штраф за отмену
|
||||
@param convert_in Заявление на конвертацию
|
||||
@param delivery_type Тип доставки: "internal" или "external"
|
||||
@param meta Метаданные
|
||||
|
||||
@note Авторизация: @p coopname
|
||||
**/
|
||||
[[eosio::action]] void marketplace::createorder(eosio::name coopname, eosio::name receiver_braname, eosio::name username, checksum256 hash, uint64_t units, eosio::asset unit_cost, uint32_t product_lifecycle_secs, uint32_t warranty_period_secs, eosio::asset membership_fee_amount, eosio::asset cancellation_fee_amount, document2 convert_in, eosio::name delivery_type, std::string meta) {
|
||||
require_auth(coopname);
|
||||
|
||||
auto existing = get_request_by_hash(coopname, hash);
|
||||
eosio::check(!existing.has_value(), "Заказ с таким хэшем уже существует");
|
||||
|
||||
auto coop = get_cooperative_or_fail(coopname);
|
||||
eosio::check(unit_cost.symbol == coop.initial.symbol, "Неверный символ токена");
|
||||
eosio::check(membership_fee_amount.symbol == coop.initial.symbol, "Неверный символ для членского взноса");
|
||||
eosio::check(cancellation_fee_amount.symbol == coop.initial.symbol, "Неверный символ для штрафа");
|
||||
eosio::check(units > 0, "Количество должно быть больше нуля");
|
||||
eosio::check(delivery_type == "internal"_n || delivery_type == "external"_n, "Тип доставки: internal или external");
|
||||
|
||||
get_participant_or_fail(coopname, username);
|
||||
get_branch_or_fail(coopname, receiver_braname);
|
||||
auto program = get_program_or_fail(coopname, _marketplace_program_id);
|
||||
|
||||
verify_document_or_fail(convert_in);
|
||||
|
||||
eosio::asset base_cost = unit_cost * units;
|
||||
eosio::asset total_cost = base_cost + membership_fee_amount;
|
||||
eosio::check(cancellation_fee_amount <= total_cost, "Штраф не может превышать общую стоимость");
|
||||
|
||||
requests_index requests(_marketplace, coopname.value);
|
||||
uint64_t request_id = get_global_id(_marketplace, "requests"_n);
|
||||
|
||||
std::string memo = "Заказ №" + std::to_string(request_id);
|
||||
|
||||
std::vector<Document::named_document> documents;
|
||||
Document::add_document(documents, DocumentNames::CONVERT_FROM, convert_in);
|
||||
|
||||
requests.emplace(_marketplace, [&](auto &i) {
|
||||
i.id = request_id;
|
||||
i.hash = hash;
|
||||
i.type = "order"_n;
|
||||
i.username = username;
|
||||
i.coopname = coopname;
|
||||
i.status = "active"_n;
|
||||
i.units = units;
|
||||
i.unit_cost = unit_cost;
|
||||
i.base_cost = base_cost;
|
||||
i.membership_fee_amount = membership_fee_amount;
|
||||
i.total_cost = total_cost;
|
||||
i.product_lifecycle_secs = product_lifecycle_secs;
|
||||
i.warranty_period_secs = warranty_period_secs;
|
||||
i.money_contributor = username;
|
||||
i.meta = meta;
|
||||
i.created_at = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
|
||||
i.cancellation_fee_amount = cancellation_fee_amount;
|
||||
i.receiver_braname = receiver_braname;
|
||||
i.documents = documents;
|
||||
});
|
||||
|
||||
// Блокировка средств заказчика
|
||||
Wallet::sub_available_funds(_marketplace, coopname, username, total_cost, _wallet_program, memo);
|
||||
Wallet::add_available_funds(_marketplace, coopname, username, total_cost, _marketplace_program, memo);
|
||||
Wallet::block_funds(_marketplace, coopname, username, total_cost, _marketplace_program, memo);
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
\ingroup public_actions
|
||||
\brief Поставщик откликается на заказ встречным предложением.
|
||||
|
||||
@details Поставщик видит заказ и предлагает поставку.
|
||||
Создаёт встречную заявку типа offer, привязанную к order.
|
||||
Заявление на взнос имуществом отправляется в совет.
|
||||
|
||||
@param coopname Имя кооператива
|
||||
@param supplier_braname КУ поставщика
|
||||
@param username Имя поставщика
|
||||
@param order_hash Хэш заказа
|
||||
@param offer_hash Хэш предложения
|
||||
@param units Количество предлагаемых единиц
|
||||
@param product_lifecycle_secs Срок годности от поставщика
|
||||
@param contribution_statement Заявление на взнос имуществом
|
||||
@param convert_out Заявление на конвертацию
|
||||
|
||||
@note Авторизация: @p coopname
|
||||
**/
|
||||
[[eosio::action]] void marketplace::respondoffer(eosio::name coopname, eosio::name supplier_braname, eosio::name username, checksum256 order_hash, checksum256 offer_hash, uint64_t units, uint32_t product_lifecycle_secs, document2 contribution_statement, document2 convert_out) {
|
||||
require_auth(coopname);
|
||||
|
||||
auto order_opt = Marketplace::get_request_by_hash(coopname, order_hash);
|
||||
eosio::check(order_opt.has_value(), "Заказ не найден");
|
||||
auto order = order_opt.value();
|
||||
|
||||
eosio::check(order.type == "order"_n, "respondoffer применим только к заказам типа order");
|
||||
eosio::check(order.status == "active"_n, "Заказ должен быть активным");
|
||||
eosio::check(units > 0 && units <= order.units, "Недопустимое количество единиц");
|
||||
|
||||
auto existing_offer = get_request_by_hash(coopname, offer_hash);
|
||||
eosio::check(!existing_offer.has_value(), "Предложение с таким хэшем уже существует");
|
||||
|
||||
get_participant_or_fail(coopname, username);
|
||||
get_branch_or_fail(coopname, supplier_braname);
|
||||
|
||||
verify_document_or_fail(contribution_statement);
|
||||
verify_document_or_fail(convert_out);
|
||||
|
||||
eosio::asset supplier_amount = order.unit_cost * units;
|
||||
|
||||
requests_index requests(_marketplace, coopname.value);
|
||||
uint64_t offer_id = get_global_id(_marketplace, "requests"_n);
|
||||
|
||||
std::vector<Document::named_document> documents;
|
||||
Document::add_document(documents, DocumentNames::CONTRIB_STMT, contribution_statement);
|
||||
Document::add_document(documents, DocumentNames::CONVERT_TO, convert_out);
|
||||
|
||||
requests.emplace(_marketplace, [&](auto &i) {
|
||||
i.id = offer_id;
|
||||
i.hash = offer_hash;
|
||||
i.type = "offer"_n;
|
||||
i.username = username;
|
||||
i.coopname = coopname;
|
||||
i.status = "accepted"_n;
|
||||
i.units = units;
|
||||
i.unit_cost = order.unit_cost;
|
||||
i.base_cost = supplier_amount;
|
||||
i.membership_fee_amount = eosio::asset(0, order.unit_cost.symbol);
|
||||
i.total_cost = supplier_amount;
|
||||
i.product_lifecycle_secs = product_lifecycle_secs;
|
||||
i.warranty_period_secs = order.warranty_period_secs;
|
||||
i.money_contributor = order.money_contributor;
|
||||
i.product_contributor = username;
|
||||
i.meta = "";
|
||||
i.created_at = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
|
||||
i.accepted_at = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
|
||||
i.cancellation_fee_amount = eosio::asset(0, order.unit_cost.symbol);
|
||||
i.receiver_braname = order.receiver_braname;
|
||||
i.supplier_braname = supplier_braname;
|
||||
i.documents = documents;
|
||||
});
|
||||
|
||||
// Отправляем заявление на взнос в совет
|
||||
::Soviet::create_agenda(
|
||||
_marketplace,
|
||||
coopname,
|
||||
username,
|
||||
get_valid_soviet_action("authcontrib"_n),
|
||||
offer_hash,
|
||||
_marketplace,
|
||||
Marketplace::get_valid_marketplace_action("authcontrib"_n),
|
||||
"declineacc"_n,
|
||||
contribution_statement,
|
||||
std::string("")
|
||||
);
|
||||
};
|
||||
@@ -27,7 +27,11 @@ void soviet::cancelexprd(eosio::name coopname, uint64_t decision_id) {
|
||||
// Проверка наличия всех необходимых полей для отправки коллбэка
|
||||
if (decision->callback_contract.has_value() &&
|
||||
decision->decline_callback.has_value() &&
|
||||
decision->hash.has_value()) {
|
||||
decision->hash.has_value() &&
|
||||
decision->callback_contract.value() != ""_n &&
|
||||
decision->confirm_callback.value() != ""_n &&
|
||||
decision->decline_callback.value() != ""_n
|
||||
) {
|
||||
|
||||
// Отправка коллбэка отклонения
|
||||
Action::send<decline_callback_interface>(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@coopenomics/contracts",
|
||||
"version": "2026.2.21-alpha-4",
|
||||
"version": "2026.2.22-2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -11,29 +11,25 @@ export type IUint64 = number | string
|
||||
|
||||
export interface IAccept {
|
||||
coopname: IName
|
||||
supplier_braname: IName
|
||||
username: IName
|
||||
request_hash: IChecksum256
|
||||
convert_out: IDocument2
|
||||
return_document: IDocument2
|
||||
exchange_id: IUint64
|
||||
document: IDocument2
|
||||
}
|
||||
|
||||
export interface IArrived {
|
||||
export interface IAddunits {
|
||||
coopname: IName
|
||||
hash: IChecksum256
|
||||
transport_act_delivery: IDocument2
|
||||
username: IName
|
||||
exchange_id: IUint64
|
||||
units: IUint64
|
||||
}
|
||||
|
||||
export interface IAuthcontrib {
|
||||
export interface IAuthorize {
|
||||
coopname: IName
|
||||
request_hash: IChecksum256
|
||||
authorization: IDocument2
|
||||
}
|
||||
|
||||
export interface IAuthreturn {
|
||||
coopname: IName
|
||||
request_hash: IChecksum256
|
||||
authorization: IDocument2
|
||||
exchange_id: IUint64
|
||||
contribution_product_decision_id: IUint64
|
||||
contribution_product_authorization: IDocument2
|
||||
return_product_decision_id: IUint64
|
||||
return_product_authorization: IDocument2
|
||||
}
|
||||
|
||||
export interface IBalances extends IBalancesBase {
|
||||
@@ -48,13 +44,13 @@ export interface IBalancesBase {
|
||||
export interface ICancel {
|
||||
coopname: IName
|
||||
username: IName
|
||||
request_hash: IChecksum256
|
||||
exchange_id: IUint64
|
||||
}
|
||||
|
||||
export interface IComplete {
|
||||
coopname: IName
|
||||
username: IName
|
||||
request_hash: IChecksum256
|
||||
exchange_id: IUint64
|
||||
}
|
||||
|
||||
export interface ICounts extends ICountsBase {
|
||||
@@ -66,39 +62,23 @@ export interface ICountsBase {
|
||||
value: IUint64
|
||||
}
|
||||
|
||||
export interface ICreateship {
|
||||
coopname: IName
|
||||
hash: IChecksum256
|
||||
driver_username: IName
|
||||
source_braname: IName
|
||||
destination_braname: IName
|
||||
request_hashes: IChecksum256[]
|
||||
transport_act_sender: IDocument2
|
||||
}
|
||||
|
||||
export interface IDecline {
|
||||
coopname: IName
|
||||
username: IName
|
||||
request_hash: IChecksum256
|
||||
exchange_id: IUint64
|
||||
meta: string
|
||||
}
|
||||
|
||||
export interface IDeclineacc {
|
||||
coopname: IName
|
||||
hash: IChecksum256
|
||||
reason: string
|
||||
}
|
||||
|
||||
export interface IDelivered {
|
||||
coopname: IName
|
||||
username: IName
|
||||
request_hash: IChecksum256
|
||||
exchange_id: IUint64
|
||||
}
|
||||
|
||||
export interface IDispute {
|
||||
coopname: IName
|
||||
username: IName
|
||||
request_hash: IChecksum256
|
||||
exchange_id: IUint64
|
||||
document: IDocument2
|
||||
}
|
||||
|
||||
@@ -111,137 +91,116 @@ export interface IDocument2 {
|
||||
signatures: ISignatureInfo[]
|
||||
}
|
||||
|
||||
export interface IMigrate {
|
||||
}
|
||||
|
||||
export interface INamedDocument {
|
||||
name: IName
|
||||
document: IDocument2
|
||||
}
|
||||
|
||||
export interface IOrderoffer {
|
||||
coopname: IName
|
||||
receiver_braname: IName
|
||||
export interface IExchangeParams {
|
||||
username: IName
|
||||
hash: IChecksum256
|
||||
parent_id: IUint64
|
||||
program_id: IUint64
|
||||
coopname: IName
|
||||
units: IUint64
|
||||
unit_cost: IAsset
|
||||
product_lifecycle_secs: IUint32
|
||||
warranty_period_secs: IUint32
|
||||
membership_fee_amount: IAsset
|
||||
cancellation_fee_amount: IAsset
|
||||
product_return_statement: IDocument2
|
||||
convert_in: IDocument2
|
||||
product_lifecycle_secs: IUint64
|
||||
document?: IDocument2
|
||||
data: string
|
||||
meta: string
|
||||
}
|
||||
|
||||
export interface IReceive {
|
||||
export interface IMigrate {
|
||||
}
|
||||
|
||||
export interface IModerate {
|
||||
coopname: IName
|
||||
username: IName
|
||||
request_hash: IChecksum256
|
||||
exchange_id: IUint64
|
||||
cancellation_fee: IUint64
|
||||
}
|
||||
|
||||
export interface INewid {
|
||||
id: IUint64
|
||||
type: IName
|
||||
}
|
||||
|
||||
export interface IOffer {
|
||||
params: IExchangeParams
|
||||
}
|
||||
|
||||
export interface IOrder {
|
||||
params: IExchangeParams
|
||||
}
|
||||
|
||||
export interface IProhibit {
|
||||
coopname: IName
|
||||
username: IName
|
||||
exchange_id: IUint64
|
||||
meta: string
|
||||
}
|
||||
|
||||
export interface IPublish {
|
||||
coopname: IName
|
||||
username: IName
|
||||
exchange_id: IUint64
|
||||
}
|
||||
|
||||
export interface IRecieve {
|
||||
coopname: IName
|
||||
username: IName
|
||||
exchange_id: IUint64
|
||||
document: IDocument2
|
||||
}
|
||||
|
||||
export interface IReceivecnf {
|
||||
export interface IRecievecnfrm {
|
||||
coopname: IName
|
||||
username: IName
|
||||
request_hash: IChecksum256
|
||||
exchange_id: IUint64
|
||||
document: IDocument2
|
||||
}
|
||||
|
||||
export interface IReceiveshipm {
|
||||
coopname: IName
|
||||
hash: IChecksum256
|
||||
warehouse_receipt_act: IDocument2
|
||||
}
|
||||
|
||||
export interface IRequest {
|
||||
id: IUint64
|
||||
hash: IChecksum256
|
||||
parent_id: IUint64
|
||||
program_id: IUint64
|
||||
coopname: IName
|
||||
type: IName
|
||||
status: IName
|
||||
username: IName
|
||||
braname: IName
|
||||
warehouse: IName
|
||||
parent_username: IName
|
||||
token_contract: IName
|
||||
receiver_braname: IName
|
||||
supplier_braname: IName
|
||||
unit_cost: IAsset
|
||||
base_cost: IAsset
|
||||
membership_fee_amount: IAsset
|
||||
supplier_amount: IAsset
|
||||
total_cost: IAsset
|
||||
units: IUint64
|
||||
membership_fee: IAsset
|
||||
remain_units: IUint64
|
||||
blocked_units: IUint64
|
||||
delivered_units: IUint64
|
||||
data: string
|
||||
meta: string
|
||||
money_contributor: IName
|
||||
product_contributor: IName
|
||||
documents: INamedDocument[]
|
||||
contribute_product_statement: IDocument2
|
||||
return_product_statement: IDocument2
|
||||
contribution_product_decision_id: IUint64
|
||||
contribution_product_authorization: IDocument2
|
||||
return_product_decision_id: IUint64
|
||||
return_product_authorization: IDocument2
|
||||
product_contribution_act_validation: IDocument2
|
||||
product_contribution_act: IDocument2
|
||||
product_recieve_act: IDocument2
|
||||
product_recieve_act_validation: IDocument2
|
||||
product_lifecycle_secs: IUint64
|
||||
warranty_period_secs: IUint64
|
||||
cancellation_fee: IUint64
|
||||
cancellation_fee_amount: IAsset
|
||||
warranty_delay_until: ITimePointSec
|
||||
deadline_for_receipt: ITimePointSec
|
||||
is_warranty_return: boolean
|
||||
warranty_return_id: IUint64
|
||||
created_at: ITimePointSec
|
||||
accepted_at: ITimePointSec
|
||||
supplied_at: ITimePointSec
|
||||
delivered_at: ITimePointSec
|
||||
received_at: ITimePointSec
|
||||
recieved_at: ITimePointSec
|
||||
completed_at: ITimePointSec
|
||||
declined_at: ITimePointSec
|
||||
disputed_at: ITimePointSec
|
||||
canceled_at: ITimePointSec
|
||||
}
|
||||
|
||||
export interface IRetransport {
|
||||
coopname: IName
|
||||
completed_hash: IChecksum256
|
||||
new_driver_username: IName
|
||||
source_braname: IName
|
||||
new_destination_braname: IName
|
||||
request_hashes: IChecksum256[]
|
||||
transport_act_sender: IDocument2
|
||||
}
|
||||
|
||||
export interface ISegment {
|
||||
id: IUint64
|
||||
request_id: IUint64
|
||||
type: IName
|
||||
status: IName
|
||||
convert_in: IDocument2
|
||||
statement: IDocument2
|
||||
decision_id: IUint64
|
||||
authorization: IDocument2
|
||||
act1: IDocument2
|
||||
act2: IDocument2
|
||||
convert_out: IDocument2
|
||||
transport_act_1: IDocument2
|
||||
transport_act_2: IDocument2
|
||||
transport_act_3: IDocument2
|
||||
transport_act_4: IDocument2
|
||||
coopactor: IName
|
||||
username: IName
|
||||
driver_username: IName
|
||||
receive_from_driver_coopactor: IName
|
||||
created_at: ITimePointSec
|
||||
updated_at: ITimePointSec
|
||||
}
|
||||
|
||||
export interface IShipment {
|
||||
id: IUint64
|
||||
hash: IChecksum256
|
||||
coopname: IName
|
||||
driver_username: IName
|
||||
source_braname: IName
|
||||
destination_braname: IName
|
||||
status: IName
|
||||
request_hashes: IChecksum256[]
|
||||
documents: INamedDocument[]
|
||||
created_at: ITimePointSec
|
||||
loaded_at: ITimePointSec
|
||||
delivered_at: ITimePointSec
|
||||
completed_at: ITimePointSec
|
||||
warranty_delay_until: ITimePointSec
|
||||
deadline_for_receipt: ITimePointSec
|
||||
is_warranty_return: boolean
|
||||
warranty_return_id: IUint64
|
||||
}
|
||||
|
||||
export interface ISignatureInfo {
|
||||
@@ -254,54 +213,33 @@ export interface ISignatureInfo {
|
||||
meta: string
|
||||
}
|
||||
|
||||
export interface ISignbydriver {
|
||||
coopname: IName
|
||||
hash: IChecksum256
|
||||
transport_act_driver: IDocument2
|
||||
}
|
||||
|
||||
export interface ISupplcnf {
|
||||
coopname: IName
|
||||
username: IName
|
||||
request_hash: IChecksum256
|
||||
act: IDocument2
|
||||
}
|
||||
|
||||
export interface ISupply {
|
||||
coopname: IName
|
||||
username: IName
|
||||
request_hash: IChecksum256
|
||||
act: IDocument2
|
||||
}
|
||||
|
||||
export interface IWaccept {
|
||||
coopname: IName
|
||||
username: IName
|
||||
request_hash: IChecksum256
|
||||
accept: boolean
|
||||
exchange_id: IUint64
|
||||
document: IDocument2
|
||||
}
|
||||
|
||||
export interface IWauthorize {
|
||||
coopname: IName
|
||||
request_hash: IChecksum256
|
||||
wreturn_decision_id: IUint64
|
||||
wreturn_authorization: IDocument2
|
||||
wsupply_decision_id: IUint64
|
||||
wsupply_authorization: IDocument2
|
||||
}
|
||||
|
||||
export interface IWoffer {
|
||||
export interface ISupplycnfrm {
|
||||
coopname: IName
|
||||
username: IName
|
||||
request_hash: IChecksum256
|
||||
exchange_id: IUint64
|
||||
document: IDocument2
|
||||
}
|
||||
|
||||
export interface IWreturn {
|
||||
export interface IUnpublish {
|
||||
coopname: IName
|
||||
username: IName
|
||||
request_hash: IChecksum256
|
||||
document: IDocument2
|
||||
exchange_id: IUint64
|
||||
}
|
||||
|
||||
export interface IUpdate {
|
||||
coopname: IName
|
||||
username: IName
|
||||
exchange_id: IUint64
|
||||
remain_units: IUint64
|
||||
unit_cost: IAsset
|
||||
data: string
|
||||
meta: string
|
||||
}
|
||||
|
||||
|
||||
@@ -66,3 +66,9 @@ MATRIX_ADMIN_PASSWORD=
|
||||
SENTRY_DSN=
|
||||
GITHUB_TOKEN=
|
||||
|
||||
|
||||
# OpenSearch (поисковая система документов, опционально)
|
||||
OPENSEARCH_NODE=https://opensearch:9200
|
||||
OPENSEARCH_ENABLED=false
|
||||
OPENSEARCH_USERNAME=admin
|
||||
OPENSEARCH_PASSWORD=
|
||||
|
||||
@@ -0,0 +1,763 @@
|
||||
# AGENTS.md — @coopenomics/controller
|
||||
|
||||
Основной бэкенд-сервис платформы «Цифровой Кооператив». NestJS GraphQL API с чистой архитектурой.
|
||||
|
||||
---
|
||||
|
||||
## Оглавление
|
||||
|
||||
- [Обзор архитектуры](#обзор-архитектуры)
|
||||
- [Дерево директорий](#дерево-директорий)
|
||||
- [Чистая архитектура — слои](#чистая-архитектура--слои)
|
||||
- [Доменный слой (domain/)](#доменный-слой-domain)
|
||||
- [Инфраструктурный слой (infrastructure/)](#инфраструктурный-слой-infrastructure)
|
||||
- [Прикладной слой (application/)](#прикладной-слой-application)
|
||||
- [Система расширений (extensions/)](#система-расширений-extensions)
|
||||
- [Описание каждого расширения](#описание-каждого-расширения)
|
||||
- [GraphQL API](#graphql-api)
|
||||
- [Аутентификация и авторизация](#аутентификация-и-авторизация)
|
||||
- [Блокчейн-взаимодействие](#блокчейн-взаимодействие)
|
||||
- [Генерация документов](#генерация-документов)
|
||||
- [База данных](#база-данных)
|
||||
- [Платёжный шлюз](#платёжный-шлюз)
|
||||
- [Конфигурация](#конфигурация)
|
||||
- [Ключевые зависимости](#ключевые-зависимости)
|
||||
|
||||
---
|
||||
|
||||
## Обзор архитектуры
|
||||
|
||||
Проект построен по принципам **чистой архитектуры (Clean Architecture)** с тремя основными слоями:
|
||||
|
||||
```
|
||||
Резолвер (GraphQL) → Сервис (application) → Интерактор (domain) → Порт (domain) → Адаптер (infrastructure)
|
||||
```
|
||||
|
||||
Направление зависимостей — **внутрь, к домену**. Домен не зависит от инфраструктуры. Слои связываются через `AppModule` (`src/app.module.ts`) — не нужно импортировать их друг в друга напрямую.
|
||||
|
||||
Точка входа: `src/index.ts` — создаёт NestJS-приложение поверх Express, подключает MongoDB (Mongoose), запускает миграции и слушает порт.
|
||||
|
||||
---
|
||||
|
||||
## Дерево директорий
|
||||
|
||||
```
|
||||
src/
|
||||
├── app.module.ts # Корневой модуль — собирает все слои
|
||||
├── index.ts # Точка входа (bootstrap)
|
||||
├── app.ts # Express-приложение (middleware)
|
||||
│
|
||||
├── config/ # Конфигурация
|
||||
│ ├── config.ts # Zod-валидация env-переменных, экспорт config-объекта
|
||||
│ ├── logger.ts # Конфигурация Winston-логгера
|
||||
│ └── morgan.ts # HTTP-логирование
|
||||
│
|
||||
├── domain/ # 🟢 Доменный слой (бизнес-логика)
|
||||
│ ├── account/ # Аккаунты пользователей
|
||||
│ ├── agenda/ # Повестки собраний
|
||||
│ ├── agreement/ # Соглашения кооператива
|
||||
│ ├── auth/ # Доменная логика аутентификации
|
||||
│ ├── blockchain/ # Типы блокчейна
|
||||
│ ├── branch/ # Кооперативные участки
|
||||
│ ├── common/ # Общие порты, репозитории, value-objects
|
||||
│ ├── cooplace/ # Маркетплейс (кооперативная площадка)
|
||||
│ ├── decision-tracking/ # Отслеживание решений совета
|
||||
│ ├── desktop/ # Рабочие столы
|
||||
│ ├── document/ # Документооборот (агрегаты, агрегаторы, порты)
|
||||
│ ├── extension/ # Система расширений (сущности, сервисы, репозитории)
|
||||
│ ├── free-decision/ # Свободные решения совета
|
||||
│ ├── gateway/ # Платёжный шлюз (провайдеры, порты, сущности)
|
||||
│ ├── ledger/ # Бухгалтерский учёт
|
||||
│ ├── meet/ # Общие собрания (агрегаты, голосования)
|
||||
│ ├── mutation-log/ # Логирование мутаций
|
||||
│ ├── notification/ # Уведомления
|
||||
│ ├── parser/ # Парсинг блокчейн-данных
|
||||
│ ├── participant/ # Пайщики кооператива
|
||||
│ ├── payment-method/ # Платёжные методы
|
||||
│ ├── registration/ # Регистрация пайщиков
|
||||
│ ├── settings/ # Системные настройки
|
||||
│ ├── system/ # Системное управление
|
||||
│ ├── token/ # Токены (refresh/access)
|
||||
│ ├── user/ # Пользователи
|
||||
│ ├── vault/ # Хранилище ключей
|
||||
│ └── wallet/ # Кошельки пайщиков
|
||||
│
|
||||
├── infrastructure/ # 🔵 Инфраструктурный слой (адаптеры)
|
||||
│ ├── blockchain/ # Адаптер блокчейна EOSIO (wharfkit)
|
||||
│ │ ├── adapters/ # Конкретные адаптеры (account, branch, gateway, meet, ...)
|
||||
│ │ ├── services/ # Вспомогательные сервисы блокчейна
|
||||
│ │ └── blockchain.service.ts # Основной сервис (реализует BlockchainPort)
|
||||
│ ├── database/ # Адаптеры БД
|
||||
│ │ ├── typeorm/ # PG (реляционная БД) через TypeORM
|
||||
│ │ │ ├── entities/ # TypeORM-сущности (~23 таблицы)
|
||||
│ │ │ ├── repositories/ # Реализации репозиториев
|
||||
│ │ │ ├── mappers/ # Мапперы блокчейн-дельт
|
||||
│ │ │ └── services/ # Сервисы синхронизации
|
||||
│ │ └── generator-repositories/ # Репозитории для Generator (MongoDB)
|
||||
│ ├── graphql/ # Конфигурация GraphQL (Apollo, директивы, фильтры)
|
||||
│ ├── redis/ # Redis-адаптер (pub/sub, кэш)
|
||||
│ ├── novu/ # Интеграция с Novu (уведомления)
|
||||
│ ├── events/ # Шина событий (EventEmitter)
|
||||
│ ├── vault/ # Хранение приватных ключей
|
||||
│ ├── gateway/ # Инфраструктура платёжного шлюза
|
||||
│ ├── account/ # Инфраструктура аккаунтов
|
||||
│ ├── document/ # Инфраструктура документов
|
||||
│ ├── generator/ # Генератор документов (обёртка над @coopenomics/factory)
|
||||
│ ├── registration/ # Инфраструктура регистрации
|
||||
│ ├── meet/ # Инфраструктура собраний
|
||||
│ ├── settings/ # Инфраструктура настроек
|
||||
│ ├── system/ # Системная инфраструктура
|
||||
│ ├── user/ # Инфраструктура пользователей
|
||||
│ ├── wallet/ # Инфраструктура кошельков
|
||||
│ ├── free-decision/ # Инфраструктура свободных решений
|
||||
│ ├── decision-tracking/ # Инфраструктура отслеживания решений
|
||||
│ ├── payment-method/ # Инфраструктура платёжных методов
|
||||
│ └── di/ # Вспомогательные DI-модули
|
||||
│
|
||||
├── application/ # 🟡 Прикладной слой (NestJS-модули)
|
||||
│ ├── account/ # Модуль аккаунтов
|
||||
│ ├── agenda/ # Модуль повесток
|
||||
│ ├── agreement/ # Модуль соглашений
|
||||
│ ├── appstore/ # Магазин расширений (CRUD расширений)
|
||||
│ ├── auth/ # Аутентификация (JWT, guards, strategies)
|
||||
│ ├── blockchain-explorer/ # Обозреватель блокчейна
|
||||
│ ├── branch/ # Кооперативные участки
|
||||
│ ├── common/ # Общие элементы (interceptors, dto)
|
||||
│ ├── cooplace/ # Маркетплейс
|
||||
│ ├── desktop/ # Рабочие столы
|
||||
│ ├── document/ # Документооборот
|
||||
│ ├── free-decision/ # Свободные решения
|
||||
│ ├── gateway/ # Платёжный шлюз (контроллеры IPN, резолверы)
|
||||
│ ├── ledger/ # Бухгалтерский учёт
|
||||
│ ├── logger/ # Логирование (Winston)
|
||||
│ ├── meet/ # Общие собрания
|
||||
│ ├── notification/ # Уведомления (web-push, Novu)
|
||||
│ ├── participant/ # Управление пайщиками
|
||||
│ ├── payment-method/ # Платёжные методы
|
||||
│ ├── provider/ # Провайдер-аккаунт
|
||||
│ ├── queue/ # Очереди задач (Bull + Redis)
|
||||
│ ├── redis/ # Redis application-модуль
|
||||
│ ├── registration/ # Регистрация пайщиков
|
||||
│ ├── settings/ # Настройки кооператива
|
||||
│ ├── system/ # Системный модуль (инициализация, статус)
|
||||
│ ├── token/ # Токены авторизации
|
||||
│ ├── user/ # Пользователи
|
||||
│ └── wallet/ # Кошельки
|
||||
│
|
||||
├── extensions/ # 🟣 Система расширений (плагины)
|
||||
│ ├── base.extension.module.ts # Абстрактный базовый класс расширения
|
||||
│ ├── extensions.module.ts # Динамический модуль регистрации всех расширений
|
||||
│ ├── extensions.registry.ts # Реестр расширений (AppRegistry)
|
||||
│ ├── builtin/ # Встроенное расширение-заглушка
|
||||
│ ├── chairman/ # Стол Председателя
|
||||
│ ├── capital/ # Благорост (целевая программа)
|
||||
│ ├── chatcoop/ # Стол связи (Matrix, звонки, транскрипция)
|
||||
│ ├── participant/ # Стол Пайщика (уведомления, трекер собраний)
|
||||
│ ├── powerup/ # Вычислительные ресурсы (блокчейн powerup)
|
||||
│ ├── yookassa/ # Платёжный провайдер ЮKassa
|
||||
│ ├── sberpoll/ # Автоматический приём через Сбербанк
|
||||
│ ├── qrpay/ # QR-оплата через банк
|
||||
│ ├── 1ccoop/ # Интеграция с 1С
|
||||
│ └── orders/ (заглушка) # Стол заказов (в разработке)
|
||||
│
|
||||
├── shared/ # Общие утилиты и абстракции
|
||||
│ ├── utils/ # Утилиты (domain-to-blockchain, payments)
|
||||
│ ├── sync/ # Версионирование сущностей (для форков блокчейна)
|
||||
│ ├── dto/ # Общие DTO
|
||||
│ └── interfaces/ # Общие интерфейсы
|
||||
│
|
||||
├── migrator/ # Система миграций данных
|
||||
├── routes/ # REST-маршруты (v1)
|
||||
├── scripts/ # Утилитные скрипты
|
||||
├── types/ # Общие TypeScript-типы
|
||||
└── utils/ # Утилиты (HttpApiError и пр.)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Чистая архитектура — слои
|
||||
|
||||
### Поток данных
|
||||
|
||||
```
|
||||
GraphQL-запрос
|
||||
→ Резолвер (application) : принимает DTO, вызывает сервис
|
||||
→ Сервис (application) : оркестрирует, передаёт DTO в интерактор
|
||||
→ Интерактор (domain) : бизнес-логика, работает с портами
|
||||
→ Порт (domain, interface): абстрактный контракт
|
||||
→ Адаптер (infrastructure): конкретная реализация (БД, блокчейн, API)
|
||||
← Результат возвращается обратно по цепочке
|
||||
```
|
||||
|
||||
### Правила зависимостей
|
||||
|
||||
1. **Домен** не зависит от инфраструктуры и application. Не импортирует `cooptypes` (инфраструктурный контракт).
|
||||
2. **Инфраструктура** реализует доменные порты. Преобразование типов (Date → string, JSON и т.д.) — **только здесь**.
|
||||
3. **Application** импортирует доменные интерфейсы. DTO имплементируют доменные интерфейсы напрямую.
|
||||
4. Связь между слоями — через **NestJS DI** (токены `Symbol`), регистрация в `app.module.ts`.
|
||||
|
||||
### Типичная структура модуля (application)
|
||||
|
||||
```
|
||||
application/account/
|
||||
├── dto/ # GraphQL Input/Object типы, имплементируют доменные интерфейсы
|
||||
├── resolvers/ # GraphQL-резолверы (@Query, @Mutation)
|
||||
├── services/ # Оркестрация вызовов интеракторов
|
||||
├── interactors/ # Бизнес-логика на уровне приложения
|
||||
└── account.module.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Доменный слой (domain/)
|
||||
|
||||
Каждый домен содержит:
|
||||
- `entities/` — доменные сущности (чистые классы без зависимостей от фреймворков)
|
||||
- `interfaces/` — доменные интерфейсы (контракты для DTO и сущностей)
|
||||
- `ports/` — абстрактные интерфейсы для внешних взаимодействий (БД, блокчейн)
|
||||
- `repositories/` — абстрактные репозитории (символы + интерфейсы)
|
||||
- `services/` — доменные сервисы
|
||||
- `enums/` — перечисления
|
||||
|
||||
### Ключевые домены
|
||||
|
||||
| Домен | Назначение |
|
||||
|-------|-----------|
|
||||
| `account` | Аккаунты в блокчейне и системе провайдера. Сущность `AccountDomainEntity` — агрегат из blockchain_account, participant_account, provider_account, user_account |
|
||||
| `agreement` | Соглашения кооператива (пользовательское, о подписи, о кошельке, приватности). Синхронизируются из блокчейна через `AgreementSyncService` |
|
||||
| `auth` | JWT-аутентификация. `AuthDomainService` — генерация/валидация токенов |
|
||||
| `branch` | Кооперативные участки. `BranchBlockchainPort` — чтение из блокчейна |
|
||||
| `document` | Электронный документооборот. `DocumentDomainAggregate` — связывает хэш, подписанный документ и raw-документ. `GeneratorPort` — генерация PDF. `DocumentDataPort` — хранение |
|
||||
| `extension` | Расширения. `ExtensionDomainEntity<TConfig>` — сущность с конфигурацией. `ExtensionDomainService` — CRUD, дефолтные приложения |
|
||||
| `gateway` | Платёжный шлюз. `PaymentProviderPort` — абстракция провайдера (createPayment). `ProviderDomainService` — реестр провайдеров. `GatewayExpiryCronService` — автоматическая экспирация |
|
||||
| `meet` | Общие собрания. Агрегаты: `MeetDomainAggregate` (собрание с вопросами повестки). `MeetBlockchainPort` — взаимодействие с контрактом `soviet` |
|
||||
| `participant` | Пайщики кооператива — данные из блокчейна |
|
||||
| `registration` | Регистрация новых пайщиков — workflow с этапами |
|
||||
| `user` | Пользователи системы (реляционная БД). `UserDomainEntity` — id, username, email, role, public_key |
|
||||
| `vault` | Хранилище приватных ключей (шифрованные WIF) |
|
||||
| `wallet` | Кошельки пайщиков — балансы и программы |
|
||||
| `ledger` | Бухгалтерские операции — входящие/исходящие транзакции |
|
||||
| `cooplace` | Маркетплейс — товары, обмены, заказы через блокчейн |
|
||||
| `parser` | Парсинг блокчейн-данных: actions, deltas, forks, sync states |
|
||||
| `notification` | Уведомления — web-push подписки |
|
||||
| `token` | Refresh/access токены — хранение в реляционной БД |
|
||||
| `settings` | Настройки кооператива — key/value хранилище |
|
||||
| `mutation-log` | Логирование всех GraphQL-мутаций |
|
||||
|
||||
### Общие порты (domain/common/ports/)
|
||||
|
||||
| Порт | Назначение |
|
||||
|------|-----------|
|
||||
| `BlockchainPort` | Базовый порт блокчейна (transact, getInfo, getAccount, getSingleRow, getAllRows, query) |
|
||||
| `SovietBlockchainPort` | Порт для контракта `soviet` (решения совета) |
|
||||
| `RedisPort` | Pub/Sub и кэш |
|
||||
| `UdataDocumentParametersPort` | Параметры документов |
|
||||
|
||||
---
|
||||
|
||||
## Инфраструктурный слой (infrastructure/)
|
||||
|
||||
### Блокчейн (infrastructure/blockchain/)
|
||||
|
||||
`BlockchainService` — основная реализация `BlockchainPort`:
|
||||
- Использует `@wharfkit/antelope`, `@wharfkit/session`, `@wharfkit/contract`
|
||||
- Подключается к EOSIO-ноде через `config.blockchain.url`
|
||||
- `initialize(username, wif)` — создаёт сессию для транзакций
|
||||
- `transact(actions)` — отправка транзакций
|
||||
- `getSingleRow/getAllRows/query` — чтение таблиц смарт-контрактов
|
||||
|
||||
**Блокчейн-адаптеры** (`infrastructure/blockchain/adapters/`):
|
||||
|
||||
| Адаптер | Порт | Контракт |
|
||||
|---------|------|----------|
|
||||
| `AccountBlockchainAdapter` | `ACCOUNT_BLOCKCHAIN_PORT` | `registrator` |
|
||||
| `BranchBlockchainAdapter` | `BRANCH_BLOCKCHAIN_PORT` | `soviet` |
|
||||
| `SystemBlockchainAdapter` | `SYSTEM_BLOCKCHAIN_PORT` | `registrator`, `soviet` |
|
||||
| `SovietBlockchainAdapter` | `SOVIET_BLOCKCHAIN_PORT` | `soviet` |
|
||||
| `CooplaceBlockchainAdapter` | `COOPLACE_BLOCKCHAIN_PORT` | `marketplace` |
|
||||
| `MeetBlockchainAdapter` | `MEET_BLOCKCHAIN_PORT` | `soviet` (собрания) |
|
||||
| `GatewayBlockchainAdapter` | `GATEWAY_BLOCKCHAIN_PORT` | `gateway` |
|
||||
| `WalletBlockchainAdapter` | `WALLET_BLOCKCHAIN_PORT` | `soviet`, `fund` |
|
||||
| `LedgerBlockchainAdapter` | `LEDGER_BLOCKCHAIN_PORT` | `fund` |
|
||||
|
||||
Каждый адаптер преобразует доменные объекты в формат блокчейна (JSON-сериализация, даты → строки) с помощью `DomainToBlockchainUtils`.
|
||||
|
||||
### База данных (infrastructure/database/)
|
||||
|
||||
**Реляционная БД (TypeORM, PG)**:
|
||||
- Конфигурация: `typeorm.module.ts`, `ormconfig.ts`
|
||||
- `synchronize: true` — автоматическая синхронизация схемы (временно)
|
||||
- Сущности загружаются из `src/infrastructure/**/entities/`, `src/extensions/**/entities/`, `src/shared/**/entities/`
|
||||
|
||||
**TypeORM-сущности** (23+ таблиц):
|
||||
|
||||
| Сущность | Таблица | Назначение |
|
||||
|----------|---------|-----------|
|
||||
| `ExtensionEntity` | extensions | Расширения (конфигурация, состояние) |
|
||||
| `LogExtensionEntity` | log_extensions | Логи расширений |
|
||||
| `PaymentEntity` | payments | Платежи шлюза |
|
||||
| `PaymentStateEntity` | payment_states | Состояния платежей |
|
||||
| `IpnEntity` | ipns | IPN-уведомления от провайдеров |
|
||||
| `CandidateEntity` | candidates | Кандидаты на вступление |
|
||||
| `MeetPreEntity` | meet_pre | Подготовленные собрания |
|
||||
| `MeetProcessedEntity` | meet_processed | Обработанные собрания |
|
||||
| `MigrationEntity` | migrations | Миграции данных |
|
||||
| `WebPushSubscriptionEntity` | web_push_subscriptions | Push-подписки |
|
||||
| `LedgerOperationEntity` | ledger_operations | Бухгалтерские операции |
|
||||
| `AgreementTypeormEntity` | agreements | Соглашения |
|
||||
| `ActionEntity` | actions | Блокчейн-действия (парсер) |
|
||||
| `DeltaEntity` | deltas | Блокчейн-дельты (парсер) |
|
||||
| `ForkEntity` | forks | Форки блокчейна |
|
||||
| `SyncStateEntity` | sync_states | Состояние синхронизации |
|
||||
| `EntityVersionTypeormEntity` | entity_versions | Версионирование сущностей |
|
||||
| `SettingsEntity` | settings | Настройки |
|
||||
| `TokenEntity` | tokens | JWT-токены |
|
||||
| `UserEntity` | users | Пользователи |
|
||||
| `VaultEntity` | vault | Хранилище ключей |
|
||||
| `SystemStatusEntity` | system_status | Статус системы |
|
||||
| `MutationLogEntity` | mutation_logs | Логи мутаций |
|
||||
| `ProgramWalletTypeormEntity` | program_wallets | Программные кошельки |
|
||||
|
||||
**MongoDB (Mongoose)**:
|
||||
- Подключается через `MongooseModule.forRoot(config.mongoose.url)`
|
||||
- Используется для:
|
||||
- Хранения и генерации документов (`@coopenomics/factory`)
|
||||
- Хранения переменных кооператива (`vars`)
|
||||
- Генератор-репозитории (`generator-repositories/`)
|
||||
|
||||
**DI-паттерн для репозиториев**:
|
||||
```typescript
|
||||
// Доменный порт (символ + интерфейс)
|
||||
export const PAYMENT_REPOSITORY = Symbol('PaymentRepository');
|
||||
export interface PaymentRepository { ... }
|
||||
|
||||
// Инфраструктурная реализация
|
||||
@Injectable()
|
||||
export class TypeOrmPaymentRepository implements PaymentRepository { ... }
|
||||
|
||||
// Регистрация в модуле
|
||||
{ provide: PAYMENT_REPOSITORY, useClass: TypeOrmPaymentRepository }
|
||||
```
|
||||
|
||||
### Генератор документов (infrastructure/generator/)
|
||||
|
||||
`GeneratorInfrastructureService` — обёртка над `@coopenomics/factory`:
|
||||
- `generate(data, options)` → генерация PDF-документа
|
||||
- `constructCooperative(username)` → построение модели кооператива
|
||||
- `search(query)` → поиск документов
|
||||
- Подключается к MongoDB при инициализации модуля
|
||||
|
||||
---
|
||||
|
||||
## Прикладной слой (application/)
|
||||
|
||||
### Структура модуля
|
||||
|
||||
Каждый модуль в `application/` содержит:
|
||||
- `dto/` — GraphQL типы (`@InputType`, `@ObjectType`), имплементируют доменные интерфейсы
|
||||
- `resolvers/` — GraphQL-резолверы (`@Resolver`, `@Query`, `@Mutation`)
|
||||
- `services/` — оркестрация (связь между резолверами и доменными интеракторами)
|
||||
- `interactors/` или `use-cases/` — бизнес-логика на уровне приложения
|
||||
- `*.module.ts` — NestJS-модуль
|
||||
|
||||
### Ключевые модули
|
||||
|
||||
| Модуль | Описание | Резолверы |
|
||||
|--------|----------|-----------|
|
||||
| `account` | Аккаунты пользователей | getAccount, getAccounts, ... |
|
||||
| `auth` | Аутентификация | login, refresh, logout, forgotPassword, resetPassword, verifyEmail |
|
||||
| `registration` | Регистрация пайщиков | startRegistration, confirmRegistration, ... |
|
||||
| `gateway` | Платёжный шлюз | createPaymentOrder, getPaymentOrders, ... |
|
||||
| `document` | Документооборот | getDocuments, generateDocument, ... |
|
||||
| `meet` | Общие собрания | getMeetings, createMeeting, vote, ... |
|
||||
| `branch` | Кооперативные участки | getBranches, createBranch, ... |
|
||||
| `agreement` | Соглашения | getAgreements, signAgreement, ... |
|
||||
| `cooplace` | Маркетплейс | getProducts, createExchange, ... |
|
||||
| `wallet` | Кошельки | getWallet, ... |
|
||||
| `ledger` | Бухгалтерия | getLedgerOperations, ... |
|
||||
| `participant` | Пайщики | getParticipants, ... |
|
||||
| `notification` | Уведомления | subscribe, getNotifications, ... |
|
||||
| `system` | Система | getSystemStatus, installSystem, ... |
|
||||
| `desktop` | Рабочие столы | getDesktops, ... |
|
||||
| `appstore` | Расширения (CRUD) | getExtensions, installExtension, updateExtension, ... |
|
||||
| `blockchain-explorer` | Обозреватель | getActions, getTransaction, ... |
|
||||
| `provider` | Провайдер-данные | getProviderData, ... |
|
||||
| `free-decision` | Свободные решения | createFreeDecision, ... |
|
||||
| `settings` | Настройки | getSettings, updateSettings |
|
||||
|
||||
---
|
||||
|
||||
## Система расширений (extensions/)
|
||||
|
||||
### Архитектура
|
||||
|
||||
Расширения — это модульные плагины, подключаемые к основному приложению. Каждое расширение:
|
||||
|
||||
1. **Наследует** `BaseExtModule` (или специализированный класс, например `PaymentProvider`, `IPNProvider`, `PollingProvider`)
|
||||
2. **Реализует** `OnModuleInit` (NestJS lifecycle)
|
||||
3. **Регистрируется** в `AppRegistry` (`extensions.registry.ts`)
|
||||
4. **Загружается** через `ExtensionsModule.register()` (динамический NestJS-модуль)
|
||||
5. **Имеет** Zod-схему конфигурации, которая валидирует и описывает поля для UI
|
||||
|
||||
### Базовый класс расширения
|
||||
|
||||
```typescript
|
||||
@Injectable()
|
||||
export abstract class BaseExtModule implements OnModuleInit {
|
||||
abstract name: string; // Уникальное имя
|
||||
abstract plugin: ExtensionDomainEntity; // Сущность из БД
|
||||
abstract defaultConfig: Record<string, any>; // Дефолтная конфигурация
|
||||
public configSchemas: ZodObject<any>; // Zod-схема конфигурации
|
||||
abstract initialize(): Promise<void>; // Инициализация при старте
|
||||
}
|
||||
```
|
||||
|
||||
### Реестр расширений (AppRegistry)
|
||||
|
||||
`extensions.registry.ts` содержит глобальный объект `AppRegistry` с описанием всех расширений:
|
||||
|
||||
```typescript
|
||||
AppRegistry = {
|
||||
extensionName: {
|
||||
is_builtin: boolean, // Встроенное?
|
||||
is_internal: boolean, // Внутреннее?
|
||||
is_available: boolean, // Доступно для установки?
|
||||
desktops?: IDesktopConfig[], // Рабочие столы (если есть — это desktop-расширение)
|
||||
title: string,
|
||||
description: string,
|
||||
class: Module, // NestJS-модуль расширения
|
||||
pluginClass: Class, // Класс плагина (для миграций)
|
||||
schema: ZodSchema, // Zod-схема конфигурации
|
||||
readme: Promise<string>, // Содержимое README.md
|
||||
instructions: Promise<string>, // Содержимое INSTALL.md
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Типы расширений
|
||||
|
||||
- **Desktop-расширения** — предоставляют рабочий стол (UI workspace): `soviet`, `chairman`, `capital`, `participant`, `chatcoop`, `powerup`, `orders`
|
||||
- **Платёжные расширения** — реализуют `PaymentProvider` / `IPNProvider` / `PollingProvider`: `yookassa`, `sberpoll`, `qrpay`
|
||||
- **Интеграционные расширения** — синхронизация с внешними системами: `1ccoop`
|
||||
|
||||
### Иерархия платёжных провайдеров
|
||||
|
||||
```
|
||||
BaseExtModule
|
||||
├── PaymentProvider (extends BaseExtModule, implements PaymentProviderPort)
|
||||
│ └── QrPayPlugin — генерация QR-кода для оплаты
|
||||
├── IPNProvider (extends BaseExtModule)
|
||||
│ └── YookassaPlugin — приём IPN-уведомлений от ЮKassa
|
||||
└── PollingProvider (extends BaseExtModule)
|
||||
└── SberpollPlugin — опрос API Сбербанка
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Описание каждого расширения
|
||||
|
||||
### `builtin` — Встроенное расширение-заглушка
|
||||
- **Файл**: `builtin/builtin-extension.module.ts`
|
||||
- **Назначение**: Минимальное расширение без логики. Используется как класс плагина для расширений, которые не имеют собственной серверной логики (`soviet`, `trustee`, `participant`, `orders`)
|
||||
- **Конфигурация**: Пустая Zod-схема `z.object({})`
|
||||
|
||||
### `chairman` — Стол Председателя
|
||||
- **Структура**: `domain/`, `infrastructure/`, `application/` — полная чистая архитектура
|
||||
- **Назначение**: Управление решениями совета кооператива. Председатель утверждает или отклоняет решения
|
||||
- **Ключевые сервисы**:
|
||||
- `ApprovalService` — обработка утверждений
|
||||
- `ApprovalNotificationService` — уведомления о новых решениях
|
||||
- `ChairmanOnboardingService` — мастер настройки кооператива
|
||||
- `ChairmanSyncInteractor` — синхронизация решений из блокчейна
|
||||
- `ChairmanBlockchainAdapter` — взаимодействие с контрактом `soviet`
|
||||
- **Резолверы**: `ApprovalResolver`, `ChairmanOnboardingResolver`
|
||||
- **Крон**: Проверка решений каждые N минут (`checkInterval`)
|
||||
- **Конфигурация**: `checkInterval`, `cancelApprovedDecisions`, `onboarding_*` — флаги прохождения мастера настройки
|
||||
|
||||
### `capital` — Благорост (целевая программа)
|
||||
- **Структура**: `domain/`, `infrastructure/`, `application/` — самое крупное расширение
|
||||
- **Назначение**: Управление интеллектуальными и имущественными вкладами по целевой программе «Благорост». Включает:
|
||||
- Управление проектами и участниками
|
||||
- Трекер времени
|
||||
- Инвестиционные взносы
|
||||
- Управление долгами и расходами
|
||||
- Голосование по методу Водянова
|
||||
- Распределение результатов
|
||||
- Генерация документов (договоры, акты, отчёты)
|
||||
- Регистрация участников программы
|
||||
- **Резолверы** (14 штук): `project-management`, `participation-management`, `invests-management`, `debt-management`, `expenses-management`, `distribution-management`, `property-management`, `result-submission`, `time-tracker`, `voting`, `generation`, `segments`, `log`, `onboarding`, `capital-registration`
|
||||
- **Синхронизация**: Синхронизация с GitHub-репозиторием (проекты ↔ issues)
|
||||
- **Конфигурация**: `creators_voting_percent`, `authors_voting_percent`, `coordinator_bonus_percent`, `voting_period_in_days`, `energy_*`, `level_*`
|
||||
|
||||
### `chatcoop` — Стол связи
|
||||
- **Структура**: `domain/`, `infrastructure/`, `application/` — полная архитектура
|
||||
- **Назначение**: Интеграция с Matrix (чаты, звонки), транскрипция через LiveKit + Whisper
|
||||
- **Ключевые сервисы**:
|
||||
- `MatrixApiService` — API Matrix-сервера (создание комнат, пользователей)
|
||||
- `MatrixUserManagementService` — управление пользователями в Matrix
|
||||
- `UnionChatService` — комнаты связи союза кооперативов
|
||||
- `SecretaryAgentService` — бот-секретарь для транскрипции звонков
|
||||
- `WhisperSttService` — Speech-to-Text через OpenAI Whisper
|
||||
- `TranscriptionManagementService` — управление транскрипциями
|
||||
- **Резолверы**: `ChatCoopResolver`, `TranscriptionResolver`
|
||||
- **REST-контроллер**: `LiveKitWebhookController` — обработка вебхуков LiveKit
|
||||
- **При инициализации**: создаёт Matrix-пространство, комнаты пайщиков и совета, аккаунт секретаря
|
||||
- **Конфигурация**: `spaceId`, `membersRoomId`, `councilRoomId`, `isInitialized`, `secretaryMatrixUserId`
|
||||
|
||||
### `participant` — Стол Пайщика
|
||||
- **Файлы**: Один модуль без domain/infrastructure разделения
|
||||
- **Назначение**: Уведомления пайщиков о собраниях, трекинг собраний
|
||||
- **Ключевые сервисы**:
|
||||
- `MeetTrackerService` — отслеживание состояния собраний
|
||||
- `NotificationSenderService` — отправка уведомлений (email, push)
|
||||
- `MeetWorkflowNotificationService` — уведомления о workflow собраний
|
||||
- **Крон**: Периодическая проверка состояния собраний
|
||||
- **Конфигурация**: `checkInterval`, `meetNotifications` (настройки для каждого собрания)
|
||||
|
||||
### `powerup` — Вычислительные ресурсы
|
||||
- **Файл**: `powerup/powerup-extension.module.ts`
|
||||
- **Назначение**: Автоматическое пополнение вычислительных ресурсов блокчейн-аккаунта кооператива (CPU, NET, RAM)
|
||||
- **Взаимодействие**: `BlockchainPort.powerUp()` — отправка транзакции powerup
|
||||
- **Крон-задачи**:
|
||||
- Ежедневное пополнение (`0 0 * * *`)
|
||||
- Проверка ресурсов каждую минуту (`* * * * *`) — пополнение при превышении пороговых значений
|
||||
- **Конфигурация**: `dailyPackageSize`, `thresholds.cpu/net/ram`, `systemSymbol`
|
||||
|
||||
### `yookassa` — ЮKassa (платёжный провайдер)
|
||||
- **Файл**: `yookassa/yookassa-extension.module.ts`
|
||||
- **Назначение**: Приём платежей через ЮKassa. Реализует `IPNProvider`
|
||||
- **Методы**:
|
||||
- `createPayment(hash)` — создание платежа в ЮKassa (`embedded` confirmation)
|
||||
- `handleIPN(request)` — обработка IPN-уведомлений (`payment.succeeded`, `payment.failed`)
|
||||
- **Комиссия**: `fee_percent: 3.5%`
|
||||
- **Конфигурация**: `client` (shopId), `secret` (secretKey)
|
||||
|
||||
### `sberpoll` — Автоматический приём через Сбербанк
|
||||
- **Файл**: `sberpoll/sberpoll-extension.module.ts`
|
||||
- **Назначение**: Автоматический опрос API выписок Сбербанка для обнаружения поступлений. Реализует `PollingProvider`
|
||||
- **Принцип**: Периодический опрос (`checkInterval`) → сопоставление поступлений с ожидаемыми платежами по назначению платежа
|
||||
- **Конфигурация**: `secret`, `scope`, `checkInterval`, `accountNumber`
|
||||
|
||||
### `qrpay` — QR-оплата
|
||||
- **Файл**: `qrpay/qrpay-extension.module.ts`
|
||||
- **Назначение**: Генерация QR-кода для оплаты через любое банковское приложение. Реализует `PaymentProvider`
|
||||
- **Метод**: `createPayment(hash)` — формирует URL оплаты с реквизитами кооператива из `@coopenomics/factory`
|
||||
- **Комиссия**: `fee_percent: 0%`
|
||||
- **Конфигурация**: Пустая (использует реквизиты из `constructCooperative`)
|
||||
|
||||
### `1ccoop` — Интеграция с 1С
|
||||
- **Структура**: `domain/`, `infrastructure/`, `application/`
|
||||
- **Назначение**: Двухсторонняя синхронизация документов кооператива с бухгалтерией 1С
|
||||
- **Ключевые сервисы**:
|
||||
- `OneCoopApplicationService` — основной сервис синхронизации
|
||||
- `OneCoopBlockchainService` — чтение данных из блокчейна для 1С
|
||||
- `DocumentAdapterFactoryService` — фабрика адаптеров документов
|
||||
- `JoinCoopDocumentAdapter` — адаптер для документа вступления
|
||||
- **REST-API**: Защищён `OneCoopSecretKeyGuard` (API-ключ в заголовке)
|
||||
- **Резолвер**: `OneCoopResolver` — GraphQL-интерфейс для 1С
|
||||
- **Конфигурация**: `secret_key` (авто-генерация), `base_url`
|
||||
|
||||
---
|
||||
|
||||
## GraphQL API
|
||||
|
||||
### Конфигурация
|
||||
|
||||
- **Драйвер**: Apollo (через `@nestjs/apollo`)
|
||||
- **Генерация схемы**: `autoSchemaFile: 'schema.gql'` (code-first, авто-генерация ~10680 строк)
|
||||
- **Endpoint**: `/v1/graphql`
|
||||
- **Playground**: Включён (`/v1/graphql`)
|
||||
- **Introspection**: Включена
|
||||
|
||||
### Директивы
|
||||
|
||||
- `@auth(roles: [...])` — документирует требуемые роли для поля/мутации
|
||||
- `fieldAuth` — авторизация на уровне полей
|
||||
|
||||
### Основные типы GraphQL-схемы
|
||||
|
||||
**Корневые типы**:
|
||||
- `Account` — агрегат аккаунта (blockchain_account + participant_account + provider_account + user_account)
|
||||
- `DocumentAggregate` — агрегат документа (hash + document + rawDocument)
|
||||
- `Payment` — платёж шлюза
|
||||
- `Extension` — расширение
|
||||
- `Branch` — кооперативный участок
|
||||
|
||||
**Перечисления**: `AccountType` (individual, entrepreneur, organization), `PaymentStatus`, `PaymentDirection` и др.
|
||||
|
||||
**Пагинация**: Типы `*PaginationResult` (items, totalCount, totalPages, currentPage)
|
||||
|
||||
---
|
||||
|
||||
## Аутентификация и авторизация
|
||||
|
||||
### JWT-стратегия
|
||||
|
||||
1. `JwtAuthStrategy` (passport-jwt) — извлекает токен из `Authorization: Bearer`
|
||||
2. Валидирует `payload.type === ACCESS`
|
||||
3. Загружает пользователя из `UserRepository` (поддержка legacy MongoDB ObjectId и UUID)
|
||||
4. Возвращает объект пользователя в `request.user`
|
||||
|
||||
### Guards
|
||||
|
||||
| Guard | Назначение |
|
||||
|-------|-----------|
|
||||
| `HttpJwtAuthGuard` | Применяет JWT-стратегию для HTTP-запросов |
|
||||
| `GraphqlJwtAuthGuard` | JWT-guard для GraphQL-контекста |
|
||||
| `RolesGuard` | Проверка ролей + server-secret обход |
|
||||
|
||||
### Декораторы
|
||||
|
||||
- `@AuthRoles(['admin', 'chairman'])` — указание требуемых ролей + GraphQL-директива для документации
|
||||
- `@CurrentUser` — извлечение текущего пользователя из контекста
|
||||
|
||||
### Авторизация
|
||||
|
||||
`RolesGuard` проверяет:
|
||||
1. Заголовок `server-secret` → полный доступ (для inter-service вызовов)
|
||||
2. Роли не заданы → свободный доступ
|
||||
3. `data.username === user.username` → доступ к своим ресурсам
|
||||
4. `user.role ∈ allowedRoles` → ролевой доступ
|
||||
5. Иначе → 401 Unauthorized
|
||||
|
||||
---
|
||||
|
||||
## Блокчейн-взаимодействие
|
||||
|
||||
### Стек
|
||||
|
||||
- **EOSIO/Antelope** — блокчейн-платформа
|
||||
- **@wharfkit/antelope, @wharfkit/session, @wharfkit/contract** — клиентские библиотеки
|
||||
- **cooptypes** — типы смарт-контрактов (используются только в инфраструктуре!)
|
||||
|
||||
### Смарт-контракты
|
||||
|
||||
| Контракт | Назначение |
|
||||
|----------|-----------|
|
||||
| `registrator` | Регистрация аккаунтов, управление пользователями |
|
||||
| `soviet` | Совет кооператива, решения, собрания, голосования |
|
||||
| `marketplace` | Кооперативная площадка (cooplace) |
|
||||
| `gateway` | Платёжный шлюз |
|
||||
| `fund` | Фонд кооператива, кошельки |
|
||||
| `eosio` | Системный контракт (powerup) |
|
||||
|
||||
### Паттерн работы
|
||||
|
||||
1. **Чтение** — через `BlockchainService.getSingleRow/getAllRows/query`
|
||||
2. **Запись** — через `BlockchainService.transact(actions)`
|
||||
3. **Авторизация транзакций** — через `VaultDomainService.getWif(username)` → `session.initialize(username, wif)`
|
||||
4. **Преобразование типов** — в адаптерах с помощью `DomainToBlockchainUtils`
|
||||
|
||||
### Синхронизация данных
|
||||
|
||||
Для синхронизации состояния блокчейна с реляционной БД используются:
|
||||
- `*DeltaMapper` — маппинг блокчейн-дельт в сущности БД
|
||||
- `*SyncService` — сервисы синхронизации (Agreement, ProgramWallet, Approval)
|
||||
- `EntityVersioningService` — версионирование при форках
|
||||
|
||||
---
|
||||
|
||||
## Генерация документов
|
||||
|
||||
### Поток
|
||||
|
||||
1. **Запрос** → Резолвер → Сервис → `GeneratorPort.generateDocument(data, options)`
|
||||
2. **Инфраструктура** → `GeneratorInfrastructureService` → `@coopenomics/factory` (Generator)
|
||||
3. **Factory** подключается к MongoDB, генерирует PDF по шаблону с данными кооператива
|
||||
4. Документ сохраняется в MongoDB с хэшем
|
||||
5. Хэш документа используется для подписания в блокчейне
|
||||
|
||||
### Ключевые интерфейсы
|
||||
|
||||
- `GeneratorPort` — доменный порт для генерации
|
||||
- `DocumentDataPort` — порт для работы с данными документов
|
||||
- `DocumentDomainAggregate` — агрегат (hash + signedDocument + rawDocument)
|
||||
- `DocumentDomainEntity` — сущность сгенерированного документа
|
||||
|
||||
---
|
||||
|
||||
## Платёжный шлюз
|
||||
|
||||
### Архитектура
|
||||
|
||||
```
|
||||
Пользователь → createPaymentOrder (mutation)
|
||||
→ GatewayService → создаёт Payment в БД
|
||||
→ ProviderDomainService.getProvider(name) → конкретный провайдер
|
||||
→ provider.createPayment(hash) → возвращает PaymentDetails
|
||||
```
|
||||
|
||||
### Типы провайдеров
|
||||
|
||||
1. **PaymentProvider** — генерирует данные для оплаты (qrpay)
|
||||
2. **IPNProvider** — принимает IPN-уведомления (yookassa)
|
||||
3. **PollingProvider** — опрашивает API банка (sberpoll)
|
||||
|
||||
### Регистрация провайдеров
|
||||
|
||||
При инициализации каждый платёжный плагин вызывает:
|
||||
```typescript
|
||||
this.providerPort.registerProvider(this.name, this);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Конфигурация
|
||||
|
||||
### Переменные окружения (.env)
|
||||
|
||||
Все переменные валидируются Zod-схемой в `src/config/config.ts`. При ошибке — процесс завершается.
|
||||
|
||||
**Обязательные**:
|
||||
- `NODE_ENV`, `BASE_URL`, `SERVER_SECRET`, `PORT`
|
||||
- `MONGODB_URL`, `COOPNAME`
|
||||
- `JWT_SECRET`
|
||||
- Переменные PG-подключения: `HOST, PORT, USER, PASS, DB`
|
||||
- `REDIS_HOST/PORT/PASSWORD`
|
||||
- `BLOCKCHAIN_RPC`, `CHAIN_ID`
|
||||
- `NOVU_APP_ID/API_KEY`
|
||||
- `VAPID_PUBLIC_KEY/PRIVATE_KEY`
|
||||
- `MATRIX_ADMIN_USERNAME/PASSWORD`
|
||||
|
||||
**Опциональные**:
|
||||
- `SENTRY_DSN`, `GITHUB_TOKEN`
|
||||
- `LIVEKIT_URL/API_KEY/API_SECRET` (для видеозвонков)
|
||||
- `OPENAI_API_KEY/BASE_URL` (для транскрипции)
|
||||
- `SMTP_*` (email)
|
||||
|
||||
---
|
||||
|
||||
## Ключевые зависимости
|
||||
|
||||
| Пакет | Версия | Назначение |
|
||||
|-------|--------|-----------|
|
||||
| `@nestjs/*` | ^10-12 | Фреймворк бэкенда |
|
||||
| `graphql`, `@nestjs/graphql`, `@nestjs/apollo` | ^16, ^12 | GraphQL API |
|
||||
| `mongoose` | ^9 | MongoDB ODM |
|
||||
| `typeorm` | 0.3.20 | Реляционный ORM (PG) |
|
||||
| `@wharfkit/*` | ^1 | Взаимодействие с блокчейном EOSIO |
|
||||
| `cooptypes` | workspace:* | Типы смарт-контрактов |
|
||||
| `@coopenomics/factory` | workspace:* | Генератор документов |
|
||||
| `@coopenomics/notifications` | workspace:* | Уведомления |
|
||||
| `@coopenomics/sdk` | workspace:* | SDK |
|
||||
| `ioredis` | ^5 | Redis-клиент |
|
||||
| `@novu/api` | ^1 | Push/email уведомления |
|
||||
| `livekit-server-sdk` | ^2 | Видеоконференции |
|
||||
| `passport-jwt` | ^4 | JWT-аутентификация |
|
||||
| `zod` | ^3 | Валидация схем |
|
||||
| `winston` | ^3 | Логирование |
|
||||
| `node-cron` | ^3 | Периодические задачи |
|
||||
| `pdf-lib` / `html-pdf-node` | — | Генерация PDF |
|
||||
| `@sentry/nestjs` | ^10 | Мониторинг ошибок |
|
||||
|
||||
---
|
||||
|
||||
## Cursor Cloud specific instructions
|
||||
|
||||
- **Линтинг**: `pnpm run lint --filter @coopenomics/controller` (ESLint)
|
||||
- **Проверка типов**: `pnpm run typecheck --filter @coopenomics/controller`
|
||||
- **Тесты**: интеграционные тесты запускаются через `@coopenomics/boot`, не в самом controller. Команда `pnpm run test --filter @coopenomics/controller` — заглушка (echo + exit 0)
|
||||
- **Запуск dev**: `pnpm run dev --filter @coopenomics/controller` (nodemon + ts-node), требует MongoDB, PG (реляционную БД), Redis, блокчейн-ноду
|
||||
- **Скрипты и команды**: см. таблицу в README.md
|
||||
- **Установка зависимостей**: всегда через корень монорепы: `pnpm add <pkg> --filter @coopenomics/controller`
|
||||
- Путь `~` в импортах — это алиас на `src/` (через `tsconfig-paths`)
|
||||
@@ -1 +1,127 @@
|
||||
# CONTROLLER
|
||||
# 🎛️ @coopenomics/controller
|
||||
|
||||
Основной бэкенд-сервис платформы «Цифровой Кооператив». GraphQL API на NestJS с чистой архитектурой — обрабатывает все запросы от рабочего стола, управляет кооперативными процессами, генерирует документы и взаимодействует с блокчейном EOSIO.
|
||||
|
||||
## Основные возможности
|
||||
|
||||
- GraphQL API для всех операций платформы
|
||||
- Чистая архитектура: domain → infrastructure → modules
|
||||
- Управление пайщиками, кошельками и финансовыми операциями
|
||||
- Электронный документооборот с ЭП через блокчейн
|
||||
- Система расширений (extensions) для модульного подключения функциональности
|
||||
- Платёжный шлюз с поддержкой нескольких провайдеров
|
||||
- JWT-аутентификация и авторизация
|
||||
- Миграции данных через TypeORM
|
||||
- Интеграция с Novu для уведомлений и LiveKit для видеоконференций
|
||||
|
||||
## Установка
|
||||
|
||||
Компонент является частью монорепозитория. Установка зависимостей из корня проекта:
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
Или только для этого компонента:
|
||||
|
||||
```bash
|
||||
pnpm install --filter @coopenomics/controller
|
||||
```
|
||||
|
||||
## Скрипты
|
||||
|
||||
| Скрипт | Команда | Описание |
|
||||
|--------|---------|----------|
|
||||
| `dev` | `pnpm run dev` | Запуск в режиме разработки (nodemon + ts-node) |
|
||||
| `start` | `pnpm run start` | Запуск в production-режиме |
|
||||
| `lint` | `pnpm run lint` | Проверка кода (ESLint) |
|
||||
| `lint:fix` | `pnpm run lint:fix` | Автоисправление lint-ошибок |
|
||||
| `typecheck` | `pnpm run typecheck` | Проверка типов TypeScript |
|
||||
| `migration:generate` | `pnpm run migration:generate` | Генерация новой миграции |
|
||||
| `migration:run` | `pnpm run migration:run` | Применение всех миграций |
|
||||
| `migration:status` | `pnpm run migration:status` | Текущий статус миграций |
|
||||
| `migration:rollback` | `pnpm run migration:rollback` | Откат миграций |
|
||||
| `migration:list` | `pnpm run migration:list` | Список всех миграций |
|
||||
| `docs` | `pnpm run docs` | Генерация документации API (SpectaQL) |
|
||||
|
||||
## Конфигурация
|
||||
|
||||
Скопируйте `.env-example` в `.env` и настройте переменные окружения:
|
||||
|
||||
- Строки подключения к MongoDB, реляционной БД, Redis
|
||||
- Endpoint блокчейн-ноды и `CHAIN_ID`
|
||||
- `VAPID_PUBLIC_KEY` / `VAPID_PRIVATE_KEY` — ключи для push-уведомлений
|
||||
- Настройки JWT-аутентификации
|
||||
- Конфигурация платёжных провайдеров
|
||||
- Настройки Sentry для мониторинга ошибок
|
||||
|
||||
Подробное описание переменных — в файле `.env-example`.
|
||||
|
||||
## Тестирование
|
||||
|
||||
Интеграционные тесты выполняются через компонент `@coopenomics/boot`, который разворачивает полную инфраструктуру:
|
||||
|
||||
```bash
|
||||
pnpm --filter @coopenomics/boot run test
|
||||
```
|
||||
|
||||
## Архитектура
|
||||
|
||||
Проект следует принципам **чистой архитектуры** с направлением зависимостей внутрь (к домену):
|
||||
|
||||
```
|
||||
src/
|
||||
├── domain/ # Доменный слой (бизнес-логика)
|
||||
│ ├── account/ # Аккаунты пользователей
|
||||
│ ├── agreement/ # Соглашения
|
||||
│ ├── auth/ # Аутентификация
|
||||
│ ├── blockchain/ # Интерфейсы блокчейна
|
||||
│ ├── branch/ # Кооперативные участки
|
||||
│ ├── capital/ # Паевые взносы
|
||||
│ ├── cooplace/ # Маркетплейс
|
||||
│ ├── document/ # Документооборот
|
||||
│ ├── extension/ # Расширения
|
||||
│ ├── gateway/ # Платёжный шлюз
|
||||
│ ├── ledger/ # Бухгалтерский учёт
|
||||
│ ├── meet/ # Собрания
|
||||
│ ├── participant/ # Участники кооператива
|
||||
│ ├── registration/ # Регистрация
|
||||
│ ├── wallet/ # Кошельки
|
||||
│ └── common/ # Общие порты и репозитории
|
||||
├── infrastructure/ # Инфраструктурный слой (адаптеры)
|
||||
│ ├── blockchain/ # Адаптер блокчейна EOSIO
|
||||
│ ├── database/ # Адаптеры БД (Mongoose, TypeORM)
|
||||
│ └── external/ # Внешние сервисы
|
||||
├── modules/ # Прикладной слой (NestJS-модули)
|
||||
│ ├── auth/ # Модуль аутентификации
|
||||
│ ├── cooperative/ # Модуль кооператива
|
||||
│ └── ... # Остальные модули
|
||||
└── extensions/ # Система подключаемых расширений
|
||||
├── extensions.registry.ts
|
||||
└── base.extension.module.ts
|
||||
migrations/ # Миграции данных (TypeORM)
|
||||
```
|
||||
|
||||
### Поток данных
|
||||
|
||||
1. **Резолвер** принимает входные данные и передаёт в сервис
|
||||
2. **Сервис** вызывает интерактор домена, передавая DTO (реализующие доменный интерфейс)
|
||||
3. **Интерактор** выполняет бизнес-логику и взаимодействует с портами
|
||||
4. **Адаптеры** (инфраструктура) преобразуют доменные объекты в формат внешних систем
|
||||
5. Результаты возвращаются обратно по цепочке
|
||||
|
||||
## Ключевые зависимости
|
||||
|
||||
- **@nestjs/\*** — фреймворк бэкенда
|
||||
- **graphql / @nestjs/graphql** — GraphQL API
|
||||
- **mongoose** — MongoDB ODM
|
||||
- **typeorm** — реляционный ORM с миграциями
|
||||
- **eosjs** — взаимодействие с блокчейном EOSIO
|
||||
- **ioredis** — Redis-клиент
|
||||
- **@novu/api** — сервис уведомлений
|
||||
- **livekit-server-sdk** — видеоконференции
|
||||
- **winston** — логирование
|
||||
|
||||
## Лицензия
|
||||
|
||||
[BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode.ru)
|
||||
|
||||
@@ -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": "2026.2.21-alpha-4",
|
||||
"version": "2026.2.22-2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "create-nodejs-express-app",
|
||||
"version": "2026.2.21-alpha-4",
|
||||
"version": "2026.2.22-2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@a2seven/yoo-checkout": "^1.1.4",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@coopenomics/controller",
|
||||
"version": "2026.2.21-alpha-4",
|
||||
"description": "",
|
||||
"version": "2026.2.22-2",
|
||||
"description": "Бэкенд GraphQL API кооператива на NestJS",
|
||||
"private": true,
|
||||
"bin": "bin/createNodejsApp.js",
|
||||
"main": "src/index.ts",
|
||||
@@ -19,8 +19,8 @@
|
||||
"deploy-testnet": "git checkout testnet && git merge main && git push origin testnet && git checkout main",
|
||||
"deploy-production": "git checkout production && git merge testnet && git push origin production && git checkout main",
|
||||
"start": "ts-node -r tsconfig-paths/register src/index.ts",
|
||||
"dev": "concurrently -n 'CONTROLLER' -c 'bgCyan.black' \"nodemon --watch src --watch migrations --ext ts,js,env --exec 'ts-node -r tsconfig-paths/register' src/index.ts\"",
|
||||
"test": "jest -i --colors --testTimeout=120000",
|
||||
"dev": "nodemon --watch src --watch migrations --ext ts,js,env --exec 'ts-node -r tsconfig-paths/register' src/index.ts",
|
||||
"test": "echo 'Controller tests: integration tests run via @coopenomics/boot' && exit 0",
|
||||
"test:watch": "jest -i --watchAll",
|
||||
"coverage": "jest -i --coverage",
|
||||
"coverage:coveralls": "jest -i --coverage --coverageReporters=text-lcov | coveralls",
|
||||
@@ -69,6 +69,7 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@a2seven/yoo-checkout": "^1.1.4",
|
||||
"@casl/ability": "^6.8.0",
|
||||
"@coopenomics/factory": "workspace:*",
|
||||
"@coopenomics/notifications": "workspace:*",
|
||||
"@coopenomics/provider-client": "2025.11.12-alpha-1",
|
||||
@@ -94,6 +95,7 @@
|
||||
"@nestjs/typeorm": "^10.0.2",
|
||||
"@novu/api": "^1.4.0",
|
||||
"@octokit/rest": "^22.0.1",
|
||||
"@opensearch-project/opensearch": "^3.5.1",
|
||||
"@sentry/nestjs": "^10.32.1",
|
||||
"@sentry/node": "^10.32.1",
|
||||
"@types/jest": "^29.5.12",
|
||||
@@ -103,11 +105,11 @@
|
||||
"@wharfkit/session": "^1.4.0",
|
||||
"@wharfkit/wallet-plugin-privatekey": "^1.1.0",
|
||||
"apollo-server-express": "^3.13.0",
|
||||
"axios": "^1.6.2",
|
||||
"axios": "^1.13.5",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.1",
|
||||
"compression": "^1.7.4",
|
||||
"compression": "^1.8.1",
|
||||
"cooptypes": "workspace:*",
|
||||
"cors": "^2.8.5",
|
||||
"cross-env": "^7.0.0",
|
||||
@@ -118,20 +120,22 @@
|
||||
"eosjs-account-name": "^2.3.0",
|
||||
"eosjs-api": "^7.0.4",
|
||||
"eosjs-ecc": "^4.0.7",
|
||||
"express": "^4.17.1",
|
||||
"express": "^4.22.1",
|
||||
"express-mongo-sanitize": "^2.0.0",
|
||||
"express-rate-limit": "^5.0.0",
|
||||
"glob": "^11.0.0",
|
||||
"graphql": "^16.9.0",
|
||||
"graphql-request": "^7.1.2",
|
||||
"graphql-scalars": "^1.24.0",
|
||||
"graphql-subscriptions": "^3.0.0",
|
||||
"graphql-tools": "^9.0.2",
|
||||
"graphql-type-json": "^0.3.2",
|
||||
"graphql-ws": "^5.16.0",
|
||||
"helmet": "^4.1.0",
|
||||
"helmet": "^8.1.0",
|
||||
"html-pdf-node": "^1.0.8",
|
||||
"http-proxy-middleware": "^3.0.3",
|
||||
"http-status": "^1.4.0",
|
||||
"iconv-lite": "^0.7.2",
|
||||
"ioredis": "^5.4.1",
|
||||
"ip": "^2.0.1",
|
||||
"isomorphic-fetch": "^3.0.0",
|
||||
@@ -139,18 +143,19 @@
|
||||
"joi": "^17.13.1",
|
||||
"joi-to-json": "^4.3.0",
|
||||
"joi-to-typescript": "^4.13.0",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"jspdf": "^2.5.1",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"jspdf": "^4.2.0",
|
||||
"libxmljs2": "^0.37.0",
|
||||
"livekit-server-sdk": "^2.15.0",
|
||||
"lodash": "^4.17.21",
|
||||
"markdown-it": "^14.1.0",
|
||||
"moment": "^2.24.0",
|
||||
"moment": "^2.30.1",
|
||||
"moment-timezone": "^0.5.45",
|
||||
"mongoose": "^8.5.2",
|
||||
"mongoose": "^9.2.2",
|
||||
"mongoose-sequence": "^6.0.1",
|
||||
"morgan": "^1.9.1",
|
||||
"morgan": "^1.10.1",
|
||||
"node-cron": "^3.0.3",
|
||||
"nodemailer": "^6.3.1",
|
||||
"nodemailer": "^8.0.1",
|
||||
"nunjucks": "^3.2.4",
|
||||
"passport-jwt": "^4.0.0",
|
||||
"pdf-lib": "^1.17.1",
|
||||
@@ -169,11 +174,12 @@
|
||||
"typedoc": "^0.25.13",
|
||||
"typeorm": "0.3.20",
|
||||
"uuid": "^11.0.0",
|
||||
"validator": "^13.0.0",
|
||||
"validator": "^13.15.26",
|
||||
"web-push": "^3.6.7",
|
||||
"winston": "^3.2.1",
|
||||
"winston-daily-rotate-file": "^5.0.0",
|
||||
"ws": "^8.18.0",
|
||||
"ws": "^8.19.0",
|
||||
"xmlbuilder2": "^4.0.3",
|
||||
"xss-clean": "^0.1.1",
|
||||
"zod": "^3.23.8",
|
||||
"zod-to-json-schema": "^3.23.5"
|
||||
@@ -191,6 +197,7 @@
|
||||
"@typescript-eslint/parser": "^8.0.0",
|
||||
"concurrently": "^8.2.2",
|
||||
"coveralls": "^3.0.7",
|
||||
"cross-spawn": "^7.0.6",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-airbnb-base": "^14.0.0",
|
||||
"eslint-config-prettier": "^8.1.0",
|
||||
@@ -200,13 +207,13 @@
|
||||
"eslint-plugin-security": "^1.4.0",
|
||||
"graphql-zeus": "^7.0.1",
|
||||
"node-mocks-http": "^1.8.0",
|
||||
"nodemon": "^2.0.0",
|
||||
"nodemon": "^3.1.14",
|
||||
"prettier": "^2.8.8",
|
||||
"prettier-plugin-sort-imports": "^1.8.9",
|
||||
"supertest": "^6.0.1",
|
||||
"supertest": "^7.2.2",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"type-fest": "^4.41.0",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"gitHead": "b05c17bee481d90c4cd82aa2a34ac428f8263a5f"
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,13 @@ input AcceptChildOrderInput {
|
||||
username: String!
|
||||
}
|
||||
|
||||
input AcceptStockInput {
|
||||
convert_in: SignedDigitalDocumentInput!
|
||||
request_hash: String!
|
||||
return_statement: SignedDigitalDocumentInput!
|
||||
username: String!
|
||||
}
|
||||
|
||||
type Account {
|
||||
"""
|
||||
объект аккаунта в блокчейне содержит системную информацию, такую как публичные ключи доступа, доступные вычислительные ресурсы, информация об установленном смарт-контракте, и т.д. и т.п. Это системный уровень обслуживания, где у каждого пайщика есть аккаунт, но не каждый аккаунт может быть пайщиком в каком-либо кооперативе. Все смарт-контракты устанавливаются и исполняются на этом уровне.
|
||||
@@ -31,7 +38,7 @@ type Account {
|
||||
private_account: PrivateAccount
|
||||
|
||||
"""
|
||||
объект аккаунта в системе учёта провайдера, т.е. MONO. Здесь хранится приватная информация о пайщике кооператива, которая содержит его приватные данные. Эти данные не публикуются в блокчейне и не выходят за пределы базы данных провайдера. Они используются для заполнения шаблонов документов при нажатии соответствующих кнопок на платформе.
|
||||
объект аккаунта в системе учёта провайдера, т.е. MONO. Здесь хранится приватная информация о пайщике кооператива, которая содержит его приватные данные. Эти данные не публикуются в блокчейне и не выходят за пределы базы данных провайдера. Они используются для заполнения шаблонов документов при нажатии соответствующих кнопок на платформе.
|
||||
"""
|
||||
provider_account: MonoAccount
|
||||
|
||||
@@ -788,6 +795,30 @@ input AnswerInput {
|
||||
vote: String!
|
||||
}
|
||||
|
||||
type ApiKeyCreated {
|
||||
allowed_operations: [String!]!
|
||||
created_at: DateTime!
|
||||
expires_at: DateTime
|
||||
id: String!
|
||||
|
||||
"""Полный ключ — показывается ТОЛЬКО при создании"""
|
||||
key: String!
|
||||
key_prefix: String!
|
||||
name: String!
|
||||
}
|
||||
|
||||
type ApiKeyInfo {
|
||||
allowed_operations: [String!]!
|
||||
created_at: DateTime!
|
||||
created_by: String!
|
||||
expires_at: DateTime
|
||||
id: String!
|
||||
is_active: Boolean!
|
||||
key_prefix: String!
|
||||
last_used_at: DateTime
|
||||
name: String!
|
||||
}
|
||||
|
||||
"""Одобрение документа председателем совета"""
|
||||
type Approval {
|
||||
"""Дата создания записи"""
|
||||
@@ -1137,6 +1168,13 @@ type Authority {
|
||||
waits: [WaitWeight!]!
|
||||
}
|
||||
|
||||
type AvailableReport {
|
||||
deadline: String!
|
||||
name: String!
|
||||
period: String!
|
||||
type: ReportType!
|
||||
}
|
||||
|
||||
type BankAccount {
|
||||
"""Номер банковского счета"""
|
||||
account_number: String!
|
||||
@@ -1982,6 +2020,12 @@ input CapitalCycleFilter {
|
||||
status: CycleStatus
|
||||
}
|
||||
|
||||
type CapitalDataChange {
|
||||
action: String!
|
||||
entity: String!
|
||||
id: String
|
||||
}
|
||||
|
||||
"""Долг в системе CAPITAL"""
|
||||
type CapitalDebt {
|
||||
"""Дата создания записи"""
|
||||
@@ -2895,12 +2939,12 @@ type CapitalProjectPlanPool {
|
||||
"""Общий генерационный пул"""
|
||||
total_generation_pool: String!
|
||||
|
||||
"""Сумма инвестиций"""
|
||||
total_with_investments: String!
|
||||
|
||||
"""Общий объем полученных инвестиций"""
|
||||
total_received_investments: String!
|
||||
|
||||
"""Общая сумма"""
|
||||
total_with_investments: String!
|
||||
|
||||
"""Процент использования инвестиций"""
|
||||
use_invest_percent: Float!
|
||||
}
|
||||
@@ -3677,6 +3721,11 @@ input CompleteCapitalRegistrationInputDTO {
|
||||
username: String!
|
||||
}
|
||||
|
||||
input CompleteProcessStepInput {
|
||||
instance_id: String!
|
||||
step_id: String!
|
||||
}
|
||||
|
||||
input CompleteRequestInput {
|
||||
"""Имя аккаунта кооператива"""
|
||||
coopname: String!
|
||||
@@ -4067,6 +4116,17 @@ type CooperativeOperatorAccount {
|
||||
verifications: [Verification!]!
|
||||
}
|
||||
|
||||
input CoopstockInput {
|
||||
braname: String!
|
||||
hash: String!
|
||||
membership_fee_amount: String!
|
||||
meta: String!
|
||||
product_lifecycle_secs: Int!
|
||||
unit_cost: String!
|
||||
units: Int!
|
||||
warranty_period_secs: Int!
|
||||
}
|
||||
|
||||
"""Страна регистрации пользователя"""
|
||||
enum Country {
|
||||
Russia
|
||||
@@ -4098,6 +4158,12 @@ input CreateAnnualGeneralMeetInput {
|
||||
secretary: String!
|
||||
}
|
||||
|
||||
input CreateApiKeyInput {
|
||||
allowedOperations: [String!] = ["*"]
|
||||
expiresInDays: Int
|
||||
name: String!
|
||||
}
|
||||
|
||||
input CreateBranchInput {
|
||||
"""
|
||||
Документ, на основании которого действует Уполномоченный (решение совета №СС-.. от ..)
|
||||
@@ -4469,6 +4535,12 @@ input CreateParentOfferInput {
|
||||
username: String!
|
||||
}
|
||||
|
||||
input CreateProcessTemplateInput {
|
||||
description: String
|
||||
project_hash: String!
|
||||
title: String!
|
||||
}
|
||||
|
||||
input CreateProgramPropertyInput {
|
||||
"""Имя аккаунта кооператива"""
|
||||
coopname: String!
|
||||
@@ -4563,6 +4635,25 @@ input CreateProjectPropertyInput {
|
||||
username: String!
|
||||
}
|
||||
|
||||
input CreateShareLinkInput {
|
||||
allowedActions: [String!]!
|
||||
expiresInDays: Float
|
||||
linkName: String
|
||||
pageName: String!
|
||||
pagePath: String!
|
||||
targetType: ShareTargetType!
|
||||
targetUsername: String
|
||||
}
|
||||
|
||||
input CreateShipmentInput {
|
||||
destination_braname: String!
|
||||
driver_username: String!
|
||||
hash: String!
|
||||
request_hashes: [String!]!
|
||||
source_braname: String!
|
||||
transport_act: SignedDigitalDocumentInput!
|
||||
}
|
||||
|
||||
input CreateSovietIndividualDataInput {
|
||||
"""Дата рождения"""
|
||||
birthdate: String!
|
||||
@@ -4996,6 +5087,11 @@ type DesktopWorkspace {
|
||||
title: String!
|
||||
}
|
||||
|
||||
input DestroyRequestInput {
|
||||
destruction_act: SignedDigitalDocumentInput!
|
||||
request_hash: String!
|
||||
}
|
||||
|
||||
input DisputeOnRequestInput {
|
||||
"""Имя аккаунта кооператива"""
|
||||
coopname: String!
|
||||
@@ -5630,6 +5726,12 @@ type GenerateRegistrationDocumentsOutput {
|
||||
username: String!
|
||||
}
|
||||
|
||||
input GenerateReportInput {
|
||||
period: Int
|
||||
reportType: ReportType!
|
||||
year: Int!
|
||||
}
|
||||
|
||||
type GeneratedDocument {
|
||||
"""Бинарное содержимое документа (base64)"""
|
||||
binary: String!
|
||||
@@ -5678,6 +5780,14 @@ type GeneratedRegistrationDocument {
|
||||
title: String!
|
||||
}
|
||||
|
||||
type GeneratedReport {
|
||||
errors: [String!]!
|
||||
fileName: String!
|
||||
isValid: Boolean!
|
||||
reportType: ReportType!
|
||||
xml: String!
|
||||
}
|
||||
|
||||
input GenerationContractGenerateDocumentInput {
|
||||
"""Номер блока, на котором был создан документ"""
|
||||
block_num: Int
|
||||
@@ -6318,6 +6428,12 @@ type KeyWeight {
|
||||
weight: Int!
|
||||
}
|
||||
|
||||
enum LeadRequestPolicy {
|
||||
BOTH
|
||||
OFFERS_ONLY
|
||||
ORDERS_ONLY
|
||||
}
|
||||
|
||||
type LedgerHistoryResponse {
|
||||
"""Текущая страница"""
|
||||
currentPage: Int!
|
||||
@@ -6466,6 +6582,22 @@ input MakeClearanceInput {
|
||||
username: String!
|
||||
}
|
||||
|
||||
type MarketplaceSettings {
|
||||
allowed_category_ids: [String!]!
|
||||
coopname: String!
|
||||
cycles_enabled: Boolean!
|
||||
external_delivery_enabled: Boolean!
|
||||
id: String!
|
||||
internal_delivery_enabled: Boolean!
|
||||
lead_request_policy: LeadRequestPolicy!
|
||||
max_cycle_days: Int!
|
||||
max_unit_cost: String
|
||||
min_unit_cost: String
|
||||
moderation_required: Boolean!
|
||||
publish_access_policy: PublishAccessPolicy!
|
||||
publish_whitelist: [String!]!
|
||||
}
|
||||
|
||||
type MatrixAccountStatusResponseDTO {
|
||||
hasAccount: Boolean!
|
||||
iframeUrl: String
|
||||
@@ -6755,6 +6887,9 @@ type Mutation {
|
||||
"""Подтвердить поставку имущества на заявку"""
|
||||
acceptChildOrder(data: AcceptChildOrderInput!): Transaction!
|
||||
|
||||
"""Принять предложение из запасов кооператива"""
|
||||
acceptStock(data: AcceptStockInput!): Transaction!
|
||||
|
||||
"""
|
||||
Добавить активного пайщика, который вступил в кооператив, не используя платформу (заполнив заявление собственноручно, оплатив вступительный и минимальный паевый взносы, и получив протокол решения совета)
|
||||
"""
|
||||
@@ -6763,6 +6898,9 @@ type Mutation {
|
||||
"""Добавить метод оплаты (банковский счёт или СБП)"""
|
||||
addPaymentMethod(data: AddPaymentMethodInput!): PaymentMethod!
|
||||
|
||||
"""Добавить пайщика в белый список публикации"""
|
||||
addToPublishWhitelist(username: String!): Boolean!
|
||||
|
||||
"""Добавить доверенное лицо кооперативного участка"""
|
||||
addTrustedAccount(data: AddTrustedAccountInput!): Branch!
|
||||
|
||||
@@ -6781,6 +6919,9 @@ type Mutation {
|
||||
"""Закрытие проекта от инвестиций в CAPITAL контракте"""
|
||||
capitalCloseProject(data: CloseProjectInput!): CapitalProject!
|
||||
|
||||
"""Завершение шага процесса"""
|
||||
capitalCompleteProcessStep(data: CompleteProcessStepInput!): ProcessInstance!
|
||||
|
||||
"""
|
||||
Завершение регистрации в Capital через отправку документов в блокчейн (regcontrib)
|
||||
"""
|
||||
@@ -6807,6 +6948,9 @@ type Mutation {
|
||||
"""Создание задачи в CAPITAL контракте"""
|
||||
capitalCreateIssue(data: CreateIssueInput!): CapitalIssue!
|
||||
|
||||
"""Создание шаблона процесса"""
|
||||
capitalCreateProcessTemplate(data: CreateProcessTemplateInput!): ProcessTemplate!
|
||||
|
||||
"""Создание программного имущественного взноса в CAPITAL контракте"""
|
||||
capitalCreateProgramProperty(data: CreateProgramPropertyInput!): Transaction!
|
||||
|
||||
@@ -6828,6 +6972,9 @@ type Mutation {
|
||||
"""Удаление задачи по хэшу"""
|
||||
capitalDeleteIssue(data: DeleteCapitalIssueByHashInput!): Boolean!
|
||||
|
||||
"""Удаление шаблона процесса"""
|
||||
capitalDeleteProcessTemplate(id: String!): Boolean!
|
||||
|
||||
"""Удаление проекта в CAPITAL контракте"""
|
||||
capitalDeleteProject(data: DeleteProjectInput!): Transaction!
|
||||
|
||||
@@ -6960,6 +7107,9 @@ type Mutation {
|
||||
"""Подписание акта о вкладе результатов участником"""
|
||||
capitalSignActAsContributor(data: SignActAsContributorInput!): CapitalSegment!
|
||||
|
||||
"""Запуск экземпляра процесса"""
|
||||
capitalStartProcess(data: StartProcessInput!): ProcessInstance!
|
||||
|
||||
"""Запуск проекта в CAPITAL контракте"""
|
||||
capitalStartProject(data: StartProjectInput!): CapitalProject!
|
||||
|
||||
@@ -6975,6 +7125,9 @@ type Mutation {
|
||||
"""Обновление задачи в CAPITAL контракте"""
|
||||
capitalUpdateIssue(data: UpdateIssueInput!): CapitalIssue!
|
||||
|
||||
"""Обновление шаблона процесса (шаги, рёбра, статус)"""
|
||||
capitalUpdateProcessTemplate(data: UpdateProcessTemplateInput!): ProcessTemplate!
|
||||
|
||||
"""Обновление истории в CAPITAL контракте"""
|
||||
capitalUpdateStory(data: UpdateStoryInput!): CapitalStory!
|
||||
|
||||
@@ -7014,11 +7167,19 @@ type Mutation {
|
||||
"""
|
||||
confirmSupplyOnRequest(data: ConfirmSupplyOnRequestInput!): Transaction!
|
||||
|
||||
"""Создать предложение из запасов кооператива"""
|
||||
coopstock(data: CoopstockInput!): Transaction!
|
||||
|
||||
"""
|
||||
Сгенерировать документ предложения повестки очередного общего собрания пайщиков
|
||||
"""
|
||||
createAnnualGeneralMeet(data: CreateAnnualGeneralMeetInput!): MeetAggregate!
|
||||
|
||||
"""
|
||||
Создать API ключ кооператива. Полный ключ показывается только при создании.
|
||||
"""
|
||||
createApiKey(data: CreateApiKeyInput!): ApiKeyCreated!
|
||||
|
||||
"""Создать кооперативный участок"""
|
||||
createBranch(data: CreateBranchInput!): Branch!
|
||||
|
||||
@@ -7043,6 +7204,12 @@ type Mutation {
|
||||
"""
|
||||
createProjectOfFreeDecision(data: CreateProjectFreeDecisionInput!): CreatedProjectFreeDecision!
|
||||
|
||||
"""Создать ссылку доступа к странице"""
|
||||
createShareLink(data: CreateShareLinkInput!): ShareLink!
|
||||
|
||||
"""Создать перевозку (КУ отправителя)"""
|
||||
createShipment(data: CreateShipmentInput!): Transaction!
|
||||
|
||||
"""Создать веб-пуш подписку для пользователя"""
|
||||
createWebPushSubscription(data: CreateSubscriptionInput!): CreateSubscriptionResponse!
|
||||
|
||||
@@ -7070,6 +7237,9 @@ type Mutation {
|
||||
"""Подтвердить доставку имущества Заказчику по заявке"""
|
||||
deliverOnRequest(data: DeliverOnRequestInput!): Transaction!
|
||||
|
||||
"""Уничтожить просроченное имущество"""
|
||||
destroyRequest(data: DestroyRequestInput!): Transaction!
|
||||
|
||||
"""Открыть спор по заявке"""
|
||||
disputeOnRequest(data: DisputeOnRequestInput!): Transaction!
|
||||
|
||||
@@ -7127,6 +7297,9 @@ type Mutation {
|
||||
"""
|
||||
generateRegistrationDocuments(data: GenerateRegistrationDocumentsInput!): GenerateRegistrationDocumentsOutput!
|
||||
|
||||
"""Генерация отчёта для ФНС/ФСС"""
|
||||
generateReport(data: GenerateReportInput!, organization: OrganizationDataInput!): GeneratedReport!
|
||||
|
||||
"""Сгенерировать документ акта возврата имущества."""
|
||||
generateReturnByAssetAct(data: ReturnByAssetActGenerateDocumentInput!, options: GenerateDocumentOptionsInput): GeneratedDocument!
|
||||
|
||||
@@ -7209,6 +7382,9 @@ type Mutation {
|
||||
"""
|
||||
receiveOnRequest(data: ReceiveOnRequestInput!): Transaction!
|
||||
|
||||
"""Приём перевозки на складе КУ получателя"""
|
||||
receiveShipment(data: SignShipmentInput!): Transaction!
|
||||
|
||||
"""Обновить токен доступа аккаунта"""
|
||||
refresh(data: RefreshInput!): RegisteredAccount!
|
||||
|
||||
@@ -7220,12 +7396,27 @@ type Mutation {
|
||||
"""
|
||||
registerParticipant(data: RegisterParticipantInput!): Account!
|
||||
|
||||
"""Удалить пайщика из белого списка публикации"""
|
||||
removeFromPublishWhitelist(username: String!): Boolean!
|
||||
|
||||
"""Перепредложить имущество по новой цене"""
|
||||
reofferRequest(data: ReofferRequestInput!): Transaction!
|
||||
|
||||
"""Запросить возврат паевого взноса имуществом (перед получением)"""
|
||||
reqReturn(data: ReqReturnInput!): Transaction!
|
||||
|
||||
"""Заменить приватный ключ аккаунта"""
|
||||
resetKey(data: ResetKeyInput!): Boolean!
|
||||
|
||||
"""Перезапуск общего собрания пайщиков"""
|
||||
restartAnnualGeneralMeet(data: RestartAnnualGeneralMeetInput!): MeetAggregate!
|
||||
|
||||
"""Отозвать API ключ"""
|
||||
revokeApiKey(id: String!): Boolean!
|
||||
|
||||
"""Отозвать ссылку доступа"""
|
||||
revokeShareLink(id: String!): Boolean!
|
||||
|
||||
"""Выбрать кооперативный участок"""
|
||||
selectBranch(data: SelectBranchInput!): Boolean!
|
||||
|
||||
@@ -7240,12 +7431,18 @@ type Mutation {
|
||||
"""Сохранить приватный ключ в зашифрованном серверном хранилище"""
|
||||
setWif(data: SetWifInput!): Boolean!
|
||||
|
||||
"""Водитель отмечает прибытие"""
|
||||
shipmentArrived(data: SignShipmentInput!): Transaction!
|
||||
|
||||
"""Подписание решения председателем на общем собрании пайщиков"""
|
||||
signByPresiderOnAnnualGeneralMeet(data: SignByPresiderOnAnnualGeneralMeetInput!): MeetAggregate!
|
||||
|
||||
"""Подписание решения секретарём на общем собрании пайщиков"""
|
||||
signBySecretaryOnAnnualGeneralMeet(data: SignBySecretaryOnAnnualGeneralMeetInput!): MeetAggregate!
|
||||
|
||||
"""Подпись водителя на перевозке"""
|
||||
signShipmentByDriver(data: SignShipmentInput!): Transaction!
|
||||
|
||||
"""
|
||||
Начать процесс установки кооператива, установить ключ и получить код установки
|
||||
"""
|
||||
@@ -7283,6 +7480,9 @@ type Mutation {
|
||||
"""Обновить расширение"""
|
||||
updateExtension(data: ExtensionInput!): Extension!
|
||||
|
||||
"""Обновить настройки маркетплейса (только chairman)"""
|
||||
updateMarketplaceSettings(data: UpdateMarketplaceSettingsInput!): MarketplaceSettings!
|
||||
|
||||
"""Обновить заявку"""
|
||||
updateRequest(data: UpdateRequestInput!): Transaction!
|
||||
|
||||
@@ -7411,6 +7611,23 @@ type OrganizationCertificate {
|
||||
username: String!
|
||||
}
|
||||
|
||||
input OrganizationDataInput {
|
||||
address: String
|
||||
inn: String!
|
||||
kpp: String!
|
||||
ogrn: String!
|
||||
okfs: String
|
||||
okopf: String
|
||||
oktmo: String!
|
||||
okved: String!
|
||||
orgName: String!
|
||||
phone: String
|
||||
signerFirstName: String!
|
||||
signerLastName: String!
|
||||
signerMiddleName: String
|
||||
signerSnils: String
|
||||
}
|
||||
|
||||
type OrganizationDetails {
|
||||
"""ИНН"""
|
||||
inn: String!
|
||||
@@ -8208,6 +8425,99 @@ input ProcessConvertToAxonStatementInput {
|
||||
username: String!
|
||||
}
|
||||
|
||||
type ProcessEdge {
|
||||
id: String!
|
||||
source: String!
|
||||
target: String!
|
||||
}
|
||||
|
||||
input ProcessEdgeInput {
|
||||
id: String!
|
||||
source: String!
|
||||
target: String!
|
||||
}
|
||||
|
||||
type ProcessInstance {
|
||||
completed_at: DateTime
|
||||
coopname: String!
|
||||
cycle: Int!
|
||||
id: String!
|
||||
project_hash: String!
|
||||
started_at: DateTime!
|
||||
started_by: String!
|
||||
status: ProcessInstanceStatus!
|
||||
step_states: [ProcessStepState!]!
|
||||
template_id: String!
|
||||
}
|
||||
|
||||
enum ProcessInstanceStatus {
|
||||
CANCELLED
|
||||
COMPLETED
|
||||
RUNNING
|
||||
}
|
||||
|
||||
type ProcessStepPosition {
|
||||
x: Float!
|
||||
y: Float!
|
||||
}
|
||||
|
||||
input ProcessStepPositionInput {
|
||||
x: Float!
|
||||
y: Float!
|
||||
}
|
||||
|
||||
type ProcessStepState {
|
||||
completed_at: DateTime
|
||||
issue_hash: String
|
||||
status: ProcessStepStatus!
|
||||
step_id: String!
|
||||
}
|
||||
|
||||
enum ProcessStepStatus {
|
||||
ACTIVE
|
||||
CANCELLED
|
||||
COMPLETED
|
||||
PENDING
|
||||
}
|
||||
|
||||
type ProcessStepTemplate {
|
||||
description: String
|
||||
estimate: Float
|
||||
id: String!
|
||||
is_start: Boolean
|
||||
position: ProcessStepPosition!
|
||||
title: String!
|
||||
}
|
||||
|
||||
input ProcessStepTemplateInput {
|
||||
description: String
|
||||
estimate: Float
|
||||
id: String!
|
||||
is_start: Boolean
|
||||
position: ProcessStepPositionInput!
|
||||
title: String!
|
||||
}
|
||||
|
||||
type ProcessTemplate {
|
||||
coopname: String!
|
||||
created_at: DateTime!
|
||||
created_by: String!
|
||||
description: String
|
||||
edges: [ProcessEdge!]!
|
||||
id: String!
|
||||
project_hash: String!
|
||||
status: ProcessTemplateStatus!
|
||||
steps: [ProcessStepTemplate!]!
|
||||
title: String!
|
||||
updated_at: DateTime!
|
||||
}
|
||||
|
||||
enum ProcessTemplateStatus {
|
||||
ACTIVE
|
||||
ARCHIVED
|
||||
DRAFT
|
||||
}
|
||||
|
||||
"""Статус программной инвестиции в системе CAPITAL"""
|
||||
enum ProgramInvestStatus {
|
||||
CREATED
|
||||
@@ -8510,6 +8820,12 @@ type PublicChairman {
|
||||
middle_name: String!
|
||||
}
|
||||
|
||||
enum PublishAccessPolicy {
|
||||
ALL_MEMBERS
|
||||
COUNCIL_ONLY
|
||||
WHITELIST
|
||||
}
|
||||
|
||||
input PublishProjectFreeDecisionInput {
|
||||
"""Имя аккаунта кооператива"""
|
||||
coopname: String!
|
||||
@@ -8585,6 +8901,18 @@ type Query {
|
||||
"""Получение списка расходов кооператива с фильтрацией"""
|
||||
capitalExpenses(filter: ExpenseFilter, options: PaginationInput): PaginatedCapitalExpensesPaginationResult!
|
||||
|
||||
"""Получение экземпляра процесса по ID"""
|
||||
capitalGetProcessInstance(id: String!): ProcessInstance
|
||||
|
||||
"""Получение экземпляров процессов для проекта"""
|
||||
capitalGetProcessInstances(project_hash: String!): [ProcessInstance!]!
|
||||
|
||||
"""Получение шаблона процесса по ID"""
|
||||
capitalGetProcessTemplate(id: String!): ProcessTemplate
|
||||
|
||||
"""Получение шаблонов процессов для проекта"""
|
||||
capitalGetProcessTemplates(project_hash: String): [ProcessTemplate!]!
|
||||
|
||||
"""Получение инвестиции по внутреннему ID базы данных"""
|
||||
capitalInvest(data: GetInvestInput!): CapitalInvest
|
||||
|
||||
@@ -8682,6 +9010,12 @@ type Query {
|
||||
"""Получить список вопросов совета кооператива для голосования"""
|
||||
getAgenda: [AgendaWithDocuments!]!
|
||||
|
||||
"""Получить список API ключей кооператива"""
|
||||
getApiKeys: [ApiKeyInfo!]!
|
||||
|
||||
"""Получить список доступных типов отчётов"""
|
||||
getAvailableReports: [AvailableReport!]!
|
||||
|
||||
"""Получить список кооперативных участков"""
|
||||
getBranches(data: GetBranchesInput!): [Branch!]!
|
||||
|
||||
@@ -8733,12 +9067,18 @@ type Query {
|
||||
"""
|
||||
getLedgerHistory(data: GetLedgerHistoryInput!): LedgerHistoryResponse!
|
||||
|
||||
"""Получить настройки маркетплейса"""
|
||||
getMarketplaceSettings: MarketplaceSettings!
|
||||
|
||||
"""Получить данные собрания по хешу"""
|
||||
getMeet(data: GetMeetInput!): MeetAggregate!
|
||||
|
||||
"""Получить список всех собраний кооператива"""
|
||||
getMeets(data: GetMeetsInput!): [MeetAggregate!]!
|
||||
|
||||
"""Получить созданные мной ссылки доступа"""
|
||||
getMyShareLinks: [ShareLink!]!
|
||||
|
||||
"""Получить список методов оплаты"""
|
||||
getPaymentMethods(data: GetPaymentMethodsInput): PaymentMethodPaginationResult!
|
||||
|
||||
@@ -8762,6 +9102,9 @@ type Query {
|
||||
"""Получить конфигурацию программ регистрации для кооператива"""
|
||||
getRegistrationConfig(account_type: AccountType!, coopname: String!): RegistrationConfig!
|
||||
|
||||
"""Получить страницы, к которым мне предоставлен доступ"""
|
||||
getSharedWithMe: [ShareLink!]!
|
||||
|
||||
"""Получить сводную публичную информацию о системе"""
|
||||
getSystemInfo: SystemInfo!
|
||||
|
||||
@@ -8776,6 +9119,9 @@ type Query {
|
||||
"""
|
||||
onecoopGetDocuments(data: GetOneCoopDocumentsInput!): OneCoopDocumentsResponse!
|
||||
|
||||
"""Полнотекстовый поиск по документам кооператива"""
|
||||
searchDocuments(data: SearchDocumentsInput!): [SearchResult!]!
|
||||
|
||||
"""
|
||||
Поиск приватных данных аккаунтов по запросу. Поиск осуществляется по полям ФИО, ИНН, ОГРН, наименованию организации и другим приватным данным.
|
||||
"""
|
||||
@@ -9019,6 +9365,24 @@ type RegistrationProgram {
|
||||
title: String!
|
||||
}
|
||||
|
||||
input ReofferRequestInput {
|
||||
new_hash: String!
|
||||
new_meta: String!
|
||||
new_unit_cost: String!
|
||||
request_hash: String!
|
||||
}
|
||||
|
||||
enum ReportType {
|
||||
BUHOTCH
|
||||
DUSN
|
||||
FSS4
|
||||
NDFL6
|
||||
PSV
|
||||
RSV
|
||||
UUSN
|
||||
UV_VZNOSY
|
||||
}
|
||||
|
||||
type RepresentedBy {
|
||||
"""На основании чего действует"""
|
||||
based_on: String!
|
||||
@@ -9058,6 +9422,12 @@ input RepresentedByInput {
|
||||
position: String!
|
||||
}
|
||||
|
||||
input ReqReturnInput {
|
||||
request_hash: String!
|
||||
return_statement: SignedDigitalDocumentInput!
|
||||
username: String!
|
||||
}
|
||||
|
||||
input ResetKeyInput {
|
||||
"""Публичный ключ для замены"""
|
||||
public_key: String!
|
||||
@@ -9577,11 +9947,42 @@ input SbpDataInput {
|
||||
phone: String!
|
||||
}
|
||||
|
||||
input SearchDocumentsInput {
|
||||
"""Максимальное количество результатов"""
|
||||
limit: Int = 20
|
||||
|
||||
"""Поисковый запрос"""
|
||||
query: String!
|
||||
}
|
||||
|
||||
input SearchPrivateAccountsInput {
|
||||
"""Поисковый запрос для поиска приватных аккаунтов"""
|
||||
query: String!
|
||||
}
|
||||
|
||||
type SearchResult {
|
||||
"""Кооператив"""
|
||||
coopname: String!
|
||||
|
||||
"""Дата создания"""
|
||||
created_at: String
|
||||
|
||||
"""Полный заголовок документа"""
|
||||
full_title: String!
|
||||
|
||||
"""Хеш документа"""
|
||||
hash: String!
|
||||
|
||||
"""Найденные фрагменты с подсветкой"""
|
||||
highlights: [String!]!
|
||||
|
||||
"""ID реестра документа"""
|
||||
registry_id: Int!
|
||||
|
||||
"""Имя пользователя"""
|
||||
username: String!
|
||||
}
|
||||
|
||||
"""Статус сегмента участника в проекте CAPITAL"""
|
||||
enum SegmentStatus {
|
||||
ACT1
|
||||
@@ -9591,6 +9992,7 @@ enum SegmentStatus {
|
||||
FINALIZED
|
||||
GENERATION
|
||||
READY
|
||||
SKIPPED
|
||||
STATEMENT
|
||||
UNDEFINED
|
||||
}
|
||||
@@ -9824,6 +10226,25 @@ type Settings {
|
||||
updated_at: DateTime!
|
||||
}
|
||||
|
||||
type ShareLink {
|
||||
allowed_actions: [String!]!
|
||||
created_at: DateTime!
|
||||
expires_at: DateTime
|
||||
id: String!
|
||||
is_active: Boolean!
|
||||
link_name: String
|
||||
page_name: String!
|
||||
page_path: String!
|
||||
target_type: ShareTargetType!
|
||||
target_username: String
|
||||
token: String!
|
||||
}
|
||||
|
||||
enum ShareTargetType {
|
||||
GUEST
|
||||
MEMBER
|
||||
}
|
||||
|
||||
input SignActAsChairmanInput {
|
||||
"""Акт о вкладе результатов"""
|
||||
act: SignedDigitalDocumentInput!
|
||||
@@ -9876,6 +10297,11 @@ input SignBySecretaryOnAnnualGeneralMeetInput {
|
||||
username: String!
|
||||
}
|
||||
|
||||
input SignShipmentInput {
|
||||
document: SignedDigitalDocumentInput!
|
||||
hash: String!
|
||||
}
|
||||
|
||||
type SignatureInfo {
|
||||
id: Float!
|
||||
is_valid: Boolean
|
||||
@@ -9962,6 +10388,11 @@ input SignedDigitalDocumentInput {
|
||||
version: String!
|
||||
}
|
||||
|
||||
type SovietDataChange {
|
||||
action: String!
|
||||
entity: String!
|
||||
}
|
||||
|
||||
input SovietMemberInput {
|
||||
individual_data: CreateSovietIndividualDataInput!
|
||||
role: String!
|
||||
@@ -9980,6 +10411,11 @@ type StartInstallResult {
|
||||
install_code: String!
|
||||
}
|
||||
|
||||
input StartProcessInput {
|
||||
project_hash: String!
|
||||
template_id: String!
|
||||
}
|
||||
|
||||
input StartProjectInput {
|
||||
"""Имя аккаунта кооператива"""
|
||||
coopname: String!
|
||||
@@ -10035,6 +10471,31 @@ input SubmitVoteInput {
|
||||
votes: [VoteDistributionInput!]!
|
||||
}
|
||||
|
||||
type Subscription {
|
||||
"""Подписка на создание коммитов"""
|
||||
capitalCommitCreated(project_hash: String): CapitalCommit!
|
||||
|
||||
"""Подписка на обновления коммитов"""
|
||||
capitalCommitUpdated(project_hash: String): CapitalCommit!
|
||||
|
||||
"""
|
||||
Уведомление об изменении данных Capital (проекты, участники, голосования)
|
||||
"""
|
||||
capitalDataChanged: CapitalDataChange!
|
||||
|
||||
"""Подписка на создание задач"""
|
||||
capitalIssueCreated(project_hash: String): CapitalIssue!
|
||||
|
||||
"""Подписка на обновления задач"""
|
||||
capitalIssueUpdated(project_hash: String): CapitalIssue!
|
||||
|
||||
"""Уведомление об изменениях собраний, решений, повестки"""
|
||||
sovietDataChanged: SovietDataChange!
|
||||
|
||||
"""Уведомление об изменении состояния системы (статус, конфигурация)"""
|
||||
systemStatusChanged: SystemStatusChange!
|
||||
}
|
||||
|
||||
type SubscriptionStatsDto {
|
||||
"""Количество активных подписок"""
|
||||
active: Int!
|
||||
@@ -10077,6 +10538,11 @@ type Symbols {
|
||||
root_symbol: String!
|
||||
}
|
||||
|
||||
type SystemFeatures {
|
||||
"""Доступен ли полнотекстовый поиск по документам"""
|
||||
search: Boolean!
|
||||
}
|
||||
|
||||
type SystemInfo {
|
||||
"""Объект системного аккаунта кооператива в блокчейне"""
|
||||
blockchain_account: BlockchainAccount!
|
||||
@@ -10096,6 +10562,9 @@ type SystemInfo {
|
||||
"""Имя аккаунта кооператива"""
|
||||
coopname: String!
|
||||
|
||||
"""Доступные функции платформы"""
|
||||
features: SystemFeatures!
|
||||
|
||||
"""Доступен ли функционал провайдера для подписок и запуска ПО"""
|
||||
is_providered: Boolean!
|
||||
|
||||
@@ -10128,6 +10597,11 @@ enum SystemStatus {
|
||||
maintenance
|
||||
}
|
||||
|
||||
type SystemStatusChange {
|
||||
message: String
|
||||
status: String!
|
||||
}
|
||||
|
||||
type Token {
|
||||
"""Дата истечения токена доступа"""
|
||||
expires: DateTime!
|
||||
@@ -10363,6 +10837,20 @@ input UpdateIssueInput {
|
||||
title: String
|
||||
}
|
||||
|
||||
input UpdateMarketplaceSettingsInput {
|
||||
allowed_category_ids: [String!]
|
||||
cycles_enabled: Boolean
|
||||
external_delivery_enabled: Boolean
|
||||
internal_delivery_enabled: Boolean
|
||||
lead_request_policy: LeadRequestPolicy
|
||||
max_cycle_days: Int
|
||||
max_unit_cost: String
|
||||
min_unit_cost: String
|
||||
moderation_required: Boolean
|
||||
publish_access_policy: PublishAccessPolicy
|
||||
publish_whitelist: [String!]
|
||||
}
|
||||
|
||||
input UpdateOrganizationDataInput {
|
||||
"""Город"""
|
||||
city: String!
|
||||
@@ -10401,6 +10889,15 @@ input UpdateOrganizationDataInput {
|
||||
username: String!
|
||||
}
|
||||
|
||||
input UpdateProcessTemplateInput {
|
||||
description: String
|
||||
edges: [ProcessEdgeInput!]
|
||||
id: String!
|
||||
status: ProcessTemplateStatus
|
||||
steps: [ProcessStepTemplateInput!]
|
||||
title: String
|
||||
}
|
||||
|
||||
input UpdateRequestInput {
|
||||
"""Имя аккаунта кооператива"""
|
||||
coopname: String!
|
||||
@@ -10677,4 +11174,4 @@ input WebPushSubscriptionKeysInput {
|
||||
|
||||
"""P256DH ключ для шифрования"""
|
||||
p256dh: String!
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,13 @@ import { NovuModule } from './infrastructure/novu/novu.module';
|
||||
import { EventsInfrastructureModule } from './infrastructure/events/events.module';
|
||||
import { FreeDecisionInfrastructureModule } from './infrastructure/free-decision/free-decision-infrastructure.module';
|
||||
import { DecisionTrackingInfrastructureModule } from './infrastructure/decision-tracking/decision-tracking-infrastructure.module';
|
||||
import { SearchInfrastructureModule } from './infrastructure/search/search-infrastructure.module';
|
||||
import { CaslModule } from './infrastructure/casl/casl.module';
|
||||
import { PubSubModule } from './infrastructure/pubsub/pubsub.module';
|
||||
import { ShareModule } from './infrastructure/share/share.module';
|
||||
import { ApiKeyModule } from './infrastructure/api-keys/api-key.module';
|
||||
import { ApiKeysAppModule } from './application/api-keys/api-keys.module';
|
||||
import { ShareAppModule } from './application/share/share.module';
|
||||
|
||||
// Domain modules
|
||||
import { AccountDomainModule } from './domain/account/account-domain.module';
|
||||
@@ -70,6 +77,9 @@ import { UserModule } from './application/user/user.module';
|
||||
import { TokenApplicationModule } from './application/token/token-application.module';
|
||||
import { SettingsApplicationModule } from './application/settings/settings.module';
|
||||
import { RegistrationModule } from './application/registration/registration.module';
|
||||
import { SearchModule } from './application/search/search.module';
|
||||
import { ReportsExtensionModule } from './extensions/reports/reports-extension.module';
|
||||
import { CooplaceExtensionModule } from './extensions/cooplace/cooplace-extension.module';
|
||||
import { MutationLoggingInterceptor } from './application/common/interceptors/mutation-logging.interceptor';
|
||||
|
||||
@Module({
|
||||
@@ -91,6 +101,11 @@ import { MutationLoggingInterceptor } from './application/common/interceptors/mu
|
||||
GeneratorInfrastructureModule,
|
||||
RedisModule,
|
||||
NovuModule,
|
||||
SearchInfrastructureModule,
|
||||
CaslModule,
|
||||
PubSubModule,
|
||||
ShareModule,
|
||||
ApiKeyModule,
|
||||
EventsInfrastructureModule,
|
||||
FreeDecisionInfrastructureModule,
|
||||
DecisionTrackingInfrastructureModule,
|
||||
@@ -147,6 +162,11 @@ import { MutationLoggingInterceptor } from './application/common/interceptors/mu
|
||||
TokenApplicationModule,
|
||||
SettingsApplicationModule,
|
||||
RegistrationModule,
|
||||
SearchModule,
|
||||
ShareAppModule,
|
||||
ApiKeysAppModule,
|
||||
ReportsExtensionModule,
|
||||
CooplaceExtensionModule,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ApiKeyResolver } from './resolvers/api-key.resolver';
|
||||
|
||||
@Module({
|
||||
providers: [ApiKeyResolver],
|
||||
})
|
||||
export class ApiKeysAppModule {}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { ObjectType, Field, InputType, Int } from '@nestjs/graphql';
|
||||
import { IsString, IsOptional, IsInt, IsArray } from 'class-validator';
|
||||
|
||||
@InputType('CreateApiKeyInput')
|
||||
export class CreateApiKeyInputDTO {
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
name!: string;
|
||||
|
||||
@Field(() => [String], { nullable: true, defaultValue: ['*'] })
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
allowedOperations?: string[];
|
||||
|
||||
@Field(() => Int, { nullable: true })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
expiresInDays?: number;
|
||||
}
|
||||
|
||||
@ObjectType('ApiKeyCreated')
|
||||
export class ApiKeyCreatedDTO {
|
||||
@Field(() => String)
|
||||
id!: string;
|
||||
|
||||
@Field(() => String)
|
||||
name!: string;
|
||||
|
||||
@Field(() => String, { description: 'Полный ключ — показывается ТОЛЬКО при создании' })
|
||||
key!: string;
|
||||
|
||||
@Field(() => String)
|
||||
key_prefix!: string;
|
||||
|
||||
@Field(() => [String])
|
||||
allowed_operations!: string[];
|
||||
|
||||
@Field(() => Date, { nullable: true })
|
||||
expires_at?: Date;
|
||||
|
||||
@Field(() => Date)
|
||||
created_at!: Date;
|
||||
}
|
||||
|
||||
@ObjectType('ApiKeyInfo')
|
||||
export class ApiKeyInfoDTO {
|
||||
@Field(() => String)
|
||||
id!: string;
|
||||
|
||||
@Field(() => String)
|
||||
name!: string;
|
||||
|
||||
@Field(() => String)
|
||||
key_prefix!: string;
|
||||
|
||||
@Field(() => String)
|
||||
created_by!: string;
|
||||
|
||||
@Field(() => [String])
|
||||
allowed_operations!: string[];
|
||||
|
||||
@Field(() => Boolean)
|
||||
is_active!: boolean;
|
||||
|
||||
@Field(() => Date, { nullable: true })
|
||||
expires_at?: Date;
|
||||
|
||||
@Field(() => Date, { nullable: true })
|
||||
last_used_at?: Date;
|
||||
|
||||
@Field(() => Date)
|
||||
created_at!: Date;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Resolver, Query, Mutation, Args } from '@nestjs/graphql';
|
||||
import { UseGuards } from '@nestjs/common';
|
||||
import { GqlJwtAuthGuard } from '~/application/auth/guards/graphql-jwt-auth.guard';
|
||||
import { RolesGuard } from '~/application/auth/guards/roles.guard';
|
||||
import { AuthRoles } from '~/application/auth/decorators/auth.decorator';
|
||||
import { CurrentUser } from '~/application/auth/decorators/current-user.decorator';
|
||||
import type { MonoAccountDomainInterface } from '~/domain/account/interfaces/mono-account-domain.interface';
|
||||
import { ApiKeyService } from '~/infrastructure/api-keys/api-key.service';
|
||||
import { CreateApiKeyInputDTO, ApiKeyCreatedDTO, ApiKeyInfoDTO } from '../dto/api-key.dto';
|
||||
import { config } from '~/config';
|
||||
|
||||
@Resolver()
|
||||
export class ApiKeyResolver {
|
||||
constructor(private readonly apiKeyService: ApiKeyService) {}
|
||||
|
||||
@Mutation(() => ApiKeyCreatedDTO, {
|
||||
name: 'createApiKey',
|
||||
description: 'Создать API ключ кооператива. Полный ключ показывается только при создании.',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['chairman'])
|
||||
async createApiKey(
|
||||
@Args('data') data: CreateApiKeyInputDTO,
|
||||
@CurrentUser() user: MonoAccountDomainInterface,
|
||||
): Promise<ApiKeyCreatedDTO> {
|
||||
return this.apiKeyService.createKey({
|
||||
coopname: config.coopname,
|
||||
name: data.name,
|
||||
createdBy: user.username,
|
||||
allowedOperations: data.allowedOperations,
|
||||
expiresInDays: data.expiresInDays,
|
||||
});
|
||||
}
|
||||
|
||||
@Query(() => [ApiKeyInfoDTO], {
|
||||
name: 'getApiKeys',
|
||||
description: 'Получить список API ключей кооператива',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['chairman'])
|
||||
async getApiKeys(): Promise<ApiKeyInfoDTO[]> {
|
||||
return this.apiKeyService.listKeys(config.coopname) as any;
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean, {
|
||||
name: 'revokeApiKey',
|
||||
description: 'Отозвать API ключ',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['chairman'])
|
||||
async revokeApiKey(
|
||||
@Args('id') id: string,
|
||||
@CurrentUser() user: MonoAccountDomainInterface,
|
||||
): Promise<boolean> {
|
||||
await this.apiKeyService.revokeKey(id, user.username);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,16 @@ export class RolesGuard implements CanActivate {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Проверка: есть ли у пользователя делегированные права (через share tokens)
|
||||
if (user.grantedPermissions && Array.isArray(user.grantedPermissions)) {
|
||||
const hasGranted = user.grantedPermissions.some(
|
||||
(p: any) => p.action === 'read' || p.action === 'manage'
|
||||
);
|
||||
if (hasGranted) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
throw new UnauthorizedException(`Недостаточно прав доступа`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { InputType, Field } from '@nestjs/graphql';
|
||||
import { IsString } from 'class-validator';
|
||||
import { SignedDigitalDocumentInputDTO } from '~/application/document/dto/signed-digital-document-input.dto';
|
||||
|
||||
@InputType('AcceptStockInput')
|
||||
export class AcceptStockInputDTO {
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
username!: string;
|
||||
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
request_hash!: string;
|
||||
|
||||
@Field(() => SignedDigitalDocumentInputDTO)
|
||||
convert_in!: SignedDigitalDocumentInputDTO;
|
||||
|
||||
@Field(() => SignedDigitalDocumentInputDTO)
|
||||
return_statement!: SignedDigitalDocumentInputDTO;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { InputType, Field, Int } from '@nestjs/graphql';
|
||||
import { IsString, IsInt } from 'class-validator';
|
||||
|
||||
@InputType('CoopstockInput')
|
||||
export class CoopstockInputDTO {
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
braname!: string;
|
||||
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
hash!: string;
|
||||
|
||||
@Field(() => Int)
|
||||
@IsInt()
|
||||
units!: number;
|
||||
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
unit_cost!: string;
|
||||
|
||||
@Field(() => Int)
|
||||
@IsInt()
|
||||
product_lifecycle_secs!: number;
|
||||
|
||||
@Field(() => Int)
|
||||
@IsInt()
|
||||
warranty_period_secs!: number;
|
||||
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
membership_fee_amount!: string;
|
||||
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
meta!: string;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { InputType, Field } from '@nestjs/graphql';
|
||||
import { IsString } from 'class-validator';
|
||||
import { SignedDigitalDocumentInputDTO } from '~/application/document/dto/signed-digital-document-input.dto';
|
||||
|
||||
@InputType('DestroyRequestInput')
|
||||
export class DestroyRequestInputDTO {
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
request_hash!: string;
|
||||
|
||||
@Field(() => SignedDigitalDocumentInputDTO)
|
||||
destruction_act!: SignedDigitalDocumentInputDTO;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { InputType, Field } from '@nestjs/graphql';
|
||||
import { IsString } from 'class-validator';
|
||||
|
||||
@InputType('ReofferRequestInput')
|
||||
export class ReofferRequestInputDTO {
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
request_hash!: string;
|
||||
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
new_hash!: string;
|
||||
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
new_unit_cost!: string;
|
||||
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
new_meta!: string;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { InputType, Field } from '@nestjs/graphql';
|
||||
import { IsString } from 'class-validator';
|
||||
import { SignedDigitalDocumentInputDTO } from '~/application/document/dto/signed-digital-document-input.dto';
|
||||
|
||||
@InputType('ReqReturnInput')
|
||||
export class ReqReturnInputDTO {
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
username!: string;
|
||||
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
request_hash!: string;
|
||||
|
||||
@Field(() => SignedDigitalDocumentInputDTO)
|
||||
return_statement!: SignedDigitalDocumentInputDTO;
|
||||
}
|
||||
@@ -11,8 +11,17 @@ import type { CreateChildOrderInputDomainInterface } from '~/domain/cooplace/int
|
||||
import type { ReceiveOnRequestInputDomainInterface } from '~/domain/cooplace/interfaces/receive-on-request-input.interface';
|
||||
import type { SupplyOnRequestInputDomainInterface } from '~/domain/cooplace/interfaces/supply-on-request-input.interface';
|
||||
|
||||
import { config } from '~/config';
|
||||
import type { ReqReturnInputDTO } from '../dto/req-return-input.dto';
|
||||
import type { CoopstockInputDTO } from '../dto/coopstock-input.dto';
|
||||
import type { AcceptStockInputDTO } from '../dto/accept-stock-input.dto';
|
||||
import type { DestroyRequestInputDTO } from '../dto/destroy-request-input.dto';
|
||||
import type { ReofferRequestInputDTO } from '../dto/reoffer-request-input.dto';
|
||||
|
||||
@Injectable()
|
||||
export class CooplaceInteractor {
|
||||
private readonly config = config;
|
||||
|
||||
constructor(
|
||||
private readonly documentDomainService: DocumentDomainService,
|
||||
@Inject(COOPLACE_BLOCKCHAIN_PORT) private readonly cooplaceBlockchainPort: CooplaceBlockchainPort
|
||||
@@ -170,4 +179,63 @@ export class CooplaceInteractor {
|
||||
const result = await this.cooplaceBlockchainPort.updateRequest(data);
|
||||
return result;
|
||||
}
|
||||
|
||||
public async reqReturn(data: ReqReturnInputDTO): Promise<TransactResult> {
|
||||
const doc: Record<string, any> = { ...(data.return_statement as any) };
|
||||
doc.meta = JSON.stringify(doc.meta);
|
||||
return await this.cooplaceBlockchainPort.reqReturn({
|
||||
coopname: this.config.coopname,
|
||||
username: data.username,
|
||||
request_hash: data.request_hash,
|
||||
return_statement: doc,
|
||||
});
|
||||
}
|
||||
|
||||
public async coopstock(data: CoopstockInputDTO): Promise<TransactResult> {
|
||||
return await this.cooplaceBlockchainPort.coopstock({
|
||||
coopname: this.config.coopname,
|
||||
braname: data.braname,
|
||||
hash: data.hash,
|
||||
units: data.units,
|
||||
unit_cost: data.unit_cost,
|
||||
product_lifecycle_secs: data.product_lifecycle_secs,
|
||||
warranty_period_secs: data.warranty_period_secs,
|
||||
membership_fee_amount: data.membership_fee_amount,
|
||||
meta: data.meta,
|
||||
});
|
||||
}
|
||||
|
||||
public async acceptStock(data: AcceptStockInputDTO): Promise<TransactResult> {
|
||||
const cin: Record<string, any> = { ...(data.convert_in as any) };
|
||||
cin.meta = JSON.stringify(cin.meta);
|
||||
const rs: Record<string, any> = { ...(data.return_statement as any) };
|
||||
rs.meta = JSON.stringify(rs.meta);
|
||||
return await this.cooplaceBlockchainPort.acceptStock({
|
||||
coopname: this.config.coopname,
|
||||
username: data.username,
|
||||
request_hash: data.request_hash,
|
||||
convert_in: cin,
|
||||
return_statement: rs,
|
||||
});
|
||||
}
|
||||
|
||||
public async destroyRequest(data: DestroyRequestInputDTO): Promise<TransactResult> {
|
||||
const act: Record<string, any> = { ...(data.destruction_act as any) };
|
||||
act.meta = JSON.stringify(act.meta);
|
||||
return await this.cooplaceBlockchainPort.destroy({
|
||||
coopname: this.config.coopname,
|
||||
request_hash: data.request_hash,
|
||||
destruction_act: act,
|
||||
});
|
||||
}
|
||||
|
||||
public async reofferRequest(data: ReofferRequestInputDTO): Promise<TransactResult> {
|
||||
return await this.cooplaceBlockchainPort.reoffer({
|
||||
coopname: this.config.coopname,
|
||||
request_hash: data.request_hash,
|
||||
new_hash: data.new_hash,
|
||||
new_unit_cost: data.new_unit_cost,
|
||||
new_meta: data.new_meta,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,11 @@ import { ReceiveOnRequestInputDTO } from '../dto/receive-on-request-input.dto';
|
||||
import { SupplyOnRequestInputDTO } from '../dto/supply-on-request-input.dto';
|
||||
import { UnpublishRequestInputDTO } from '../dto/unpublish-request-input.dto';
|
||||
import { UpdateRequestInputDTO } from '../dto/update-request-input.dto';
|
||||
import { ReqReturnInputDTO } from '../dto/req-return-input.dto';
|
||||
import { CoopstockInputDTO } from '../dto/coopstock-input.dto';
|
||||
import { AcceptStockInputDTO } from '../dto/accept-stock-input.dto';
|
||||
import { DestroyRequestInputDTO } from '../dto/destroy-request-input.dto';
|
||||
import { ReofferRequestInputDTO } from '../dto/reoffer-request-input.dto';
|
||||
import { GeneratedDocumentDTO } from '~/application/document/dto/generated-document.dto';
|
||||
|
||||
@Resolver()
|
||||
@@ -301,4 +306,69 @@ export class CooplaceResolver {
|
||||
): Promise<TransactionDTO> {
|
||||
return this.cooplaceService.updateRequest(data);
|
||||
}
|
||||
|
||||
@Mutation(() => TransactionDTO, {
|
||||
name: 'reqReturn',
|
||||
description: 'Запросить возврат паевого взноса имуществом (перед получением)',
|
||||
})
|
||||
@Throttle({ default: { limit: 3, ttl: 60000 } })
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['chairman', 'member', 'user'])
|
||||
async reqReturn(
|
||||
@Args('data', { type: () => ReqReturnInputDTO }) data: ReqReturnInputDTO
|
||||
): Promise<TransactionDTO> {
|
||||
return this.cooplaceService.reqReturn(data);
|
||||
}
|
||||
|
||||
@Mutation(() => TransactionDTO, {
|
||||
name: 'coopstock',
|
||||
description: 'Создать предложение из запасов кооператива',
|
||||
})
|
||||
@Throttle({ default: { limit: 3, ttl: 60000 } })
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['chairman'])
|
||||
async coopstock(
|
||||
@Args('data', { type: () => CoopstockInputDTO }) data: CoopstockInputDTO
|
||||
): Promise<TransactionDTO> {
|
||||
return this.cooplaceService.coopstock(data);
|
||||
}
|
||||
|
||||
@Mutation(() => TransactionDTO, {
|
||||
name: 'acceptStock',
|
||||
description: 'Принять предложение из запасов кооператива',
|
||||
})
|
||||
@Throttle({ default: { limit: 3, ttl: 60000 } })
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['chairman', 'member', 'user'])
|
||||
async acceptStock(
|
||||
@Args('data', { type: () => AcceptStockInputDTO }) data: AcceptStockInputDTO
|
||||
): Promise<TransactionDTO> {
|
||||
return this.cooplaceService.acceptStock(data);
|
||||
}
|
||||
|
||||
@Mutation(() => TransactionDTO, {
|
||||
name: 'destroyRequest',
|
||||
description: 'Уничтожить просроченное имущество',
|
||||
})
|
||||
@Throttle({ default: { limit: 3, ttl: 60000 } })
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['chairman'])
|
||||
async destroyRequest(
|
||||
@Args('data', { type: () => DestroyRequestInputDTO }) data: DestroyRequestInputDTO
|
||||
): Promise<TransactionDTO> {
|
||||
return this.cooplaceService.destroyRequest(data);
|
||||
}
|
||||
|
||||
@Mutation(() => TransactionDTO, {
|
||||
name: 'reofferRequest',
|
||||
description: 'Перепредложить имущество по новой цене',
|
||||
})
|
||||
@Throttle({ default: { limit: 3, ttl: 60000 } })
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['chairman'])
|
||||
async reofferRequest(
|
||||
@Args('data', { type: () => ReofferRequestInputDTO }) data: ReofferRequestInputDTO
|
||||
): Promise<TransactionDTO> {
|
||||
return this.cooplaceService.reofferRequest(data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,11 @@ import type { ReturnByAssetActGenerateDocumentInputDTO } from '../../document/do
|
||||
import type { ReturnByAssetDecisionGenerateDocumentInputDTO } from '../../document/documents-dto/return-by-asset-decision-document.dto';
|
||||
import type { ReturnByAssetStatementGenerateDocumentInputDTO } from '../../document/documents-dto/return-by-asset-statement-document.dto';
|
||||
import { CooplaceInteractor } from '../interactors/cooplace.interactor';
|
||||
import type { ReqReturnInputDTO } from '../dto/req-return-input.dto';
|
||||
import type { CoopstockInputDTO } from '../dto/coopstock-input.dto';
|
||||
import type { AcceptStockInputDTO } from '../dto/accept-stock-input.dto';
|
||||
import type { DestroyRequestInputDTO } from '../dto/destroy-request-input.dto';
|
||||
import type { ReofferRequestInputDTO } from '../dto/reoffer-request-input.dto';
|
||||
import type { AcceptChildOrderInputDTO } from '../dto/accept-child-order-input.dto';
|
||||
import type { CancelRequestInputDTO } from '../dto/cancel-request-input.dto';
|
||||
import type { CompleteRequestInputDTO } from '../dto/complete-request-input.dto';
|
||||
@@ -163,4 +168,24 @@ export class CooplaceService {
|
||||
const result = await this.cooplaceInteractor.updateRequest(data);
|
||||
return result;
|
||||
}
|
||||
|
||||
public async reqReturn(data: ReqReturnInputDTO): Promise<TransactionDTO> {
|
||||
return await this.cooplaceInteractor.reqReturn(data);
|
||||
}
|
||||
|
||||
public async coopstock(data: CoopstockInputDTO): Promise<TransactionDTO> {
|
||||
return await this.cooplaceInteractor.coopstock(data);
|
||||
}
|
||||
|
||||
public async acceptStock(data: AcceptStockInputDTO): Promise<TransactionDTO> {
|
||||
return await this.cooplaceInteractor.acceptStock(data);
|
||||
}
|
||||
|
||||
public async destroyRequest(data: DestroyRequestInputDTO): Promise<TransactionDTO> {
|
||||
return await this.cooplaceInteractor.destroyRequest(data);
|
||||
}
|
||||
|
||||
public async reofferRequest(data: ReofferRequestInputDTO): Promise<TransactionDTO> {
|
||||
return await this.cooplaceInteractor.reofferRequest(data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,5 +12,6 @@ import { LedgerDomainModule } from '~/domain/ledger/ledger-domain.module';
|
||||
@Module({
|
||||
imports: [LedgerDomainModule],
|
||||
providers: [LedgerResolver, LedgerService, LedgerEventService, LedgerInteractor],
|
||||
exports: [LedgerInteractor],
|
||||
})
|
||||
export class LedgerModule {}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { DocumentModule } from '../document/document.module';
|
||||
import { MeetResolver } from './resolvers/meet.resolver';
|
||||
import { MeetSubscriptionResolver } from './resolvers/meet-subscription.resolver';
|
||||
import { MeetService } from './services/meet.service';
|
||||
import { MeetEventService } from './services/meet-event.service';
|
||||
import { MeetInteractor } from './interactors/meet.interactor';
|
||||
import { AgendaNotificationService } from '../agenda/services/agenda-notification.service';
|
||||
import { DecisionNotificationService } from '../agenda/services/decision-notification.service';
|
||||
import { NovuModule } from '~/infrastructure/novu/novu.module';
|
||||
import { PubSubModule } from '~/infrastructure/pubsub/pubsub.module';
|
||||
import { MeetDomainModule } from '~/domain/meet/meet-domain.module';
|
||||
import { UserDomainModule } from '~/domain/user/user-domain.module';
|
||||
import { UserInfrastructureModule } from '~/infrastructure/user/user-infrastructure.module';
|
||||
@@ -23,6 +25,7 @@ import { DocumentDomainModule } from '~/domain/document/document.module';
|
||||
DatabaseModule,
|
||||
BlockchainModule,
|
||||
NovuModule,
|
||||
PubSubModule,
|
||||
MeetDomainModule,
|
||||
UserInfrastructureModule,
|
||||
UserDomainModule,
|
||||
@@ -32,6 +35,7 @@ import { DocumentDomainModule } from '~/domain/document/document.module';
|
||||
controllers: [],
|
||||
providers: [
|
||||
MeetResolver,
|
||||
MeetSubscriptionResolver,
|
||||
MeetService,
|
||||
MeetEventService,
|
||||
MeetInteractor,
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Resolver, Subscription, ObjectType, Field } from '@nestjs/graphql';
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { PubSub } from 'graphql-subscriptions';
|
||||
import { PUB_SUB } from '~/infrastructure/pubsub/pubsub.module';
|
||||
|
||||
export const SOVIET_EVENTS = {
|
||||
DATA_CHANGED: 'sovietDataChanged',
|
||||
};
|
||||
|
||||
@ObjectType('SovietDataChange')
|
||||
export class SovietDataChangeDTO {
|
||||
@Field(() => String)
|
||||
entity!: string;
|
||||
|
||||
@Field(() => String)
|
||||
action!: string;
|
||||
}
|
||||
|
||||
@Resolver()
|
||||
export class MeetSubscriptionResolver {
|
||||
constructor(@Inject(PUB_SUB) private readonly pubSub: PubSub) {}
|
||||
|
||||
@Subscription(() => SovietDataChangeDTO, {
|
||||
name: 'sovietDataChanged',
|
||||
description: 'Уведомление об изменениях собраний, решений, повестки',
|
||||
})
|
||||
sovietDataChanged() {
|
||||
return this.pubSub.asyncIterableIterator(SOVIET_EVENTS.DATA_CHANGED);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,16 @@
|
||||
// application/meet/services/meet-event.service.ts
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { PubSub } from 'graphql-subscriptions';
|
||||
import { MeetInteractor } from '../interactors/meet.interactor';
|
||||
import { WinstonLoggerService } from '~/application/logger/logger-app.service';
|
||||
import { DomainToBlockchainUtils } from '~/shared/utils/domain-to-blockchain.utils';
|
||||
import type { MeetDecisionDomainInterface } from '~/domain/meet/interfaces/meet-decision-domain.interface';
|
||||
import { MeetContract } from 'cooptypes';
|
||||
import type { IAction } from '~/types';
|
||||
import { PUB_SUB } from '~/infrastructure/pubsub/pubsub.module';
|
||||
import { SOVIET_EVENTS } from '../resolvers/meet-subscription.resolver';
|
||||
|
||||
/**
|
||||
* Сервис обработки событий собраний
|
||||
@@ -15,10 +18,20 @@ import type { IAction } from '~/types';
|
||||
*/
|
||||
@Injectable()
|
||||
export class MeetEventService {
|
||||
constructor(private readonly meetInteractor: MeetInteractor, private readonly logger: WinstonLoggerService) {
|
||||
constructor(
|
||||
private readonly meetInteractor: MeetInteractor,
|
||||
private readonly logger: WinstonLoggerService,
|
||||
@Inject(PUB_SUB) private readonly pubSub: PubSub,
|
||||
) {
|
||||
this.logger.setContext(MeetEventService.name);
|
||||
}
|
||||
|
||||
private notifySovietChanged(entity: string, action: string) {
|
||||
this.pubSub.publish(SOVIET_EVENTS.DATA_CHANGED, {
|
||||
sovietDataChanged: { entity, action },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Обработчик события решения собрания из блокчейна
|
||||
*/
|
||||
@@ -51,6 +64,7 @@ export class MeetEventService {
|
||||
});
|
||||
|
||||
this.logger.info(`Processed meet decision for ${event.data.hash}`);
|
||||
this.notifySovietChanged('decision', 'processed');
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Error processing meet decision: ${error.message}`, error.stack);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { InputType, Field, Int } from '@nestjs/graphql';
|
||||
import { IsString, IsOptional, IsInt, Min, Max } from 'class-validator';
|
||||
|
||||
@InputType('SearchDocumentsInput')
|
||||
export class SearchDocumentsInputDTO {
|
||||
@Field(() => String, { description: 'Поисковый запрос' })
|
||||
@IsString()
|
||||
query!: string;
|
||||
|
||||
@Field(() => Int, { description: 'Максимальное количество результатов', nullable: true, defaultValue: 20 })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Max(100)
|
||||
limit?: number;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ObjectType, Field, Int } from '@nestjs/graphql';
|
||||
|
||||
@ObjectType('SearchResult')
|
||||
export class SearchResultDTO {
|
||||
@Field(() => String, { description: 'Хеш документа' })
|
||||
hash!: string;
|
||||
|
||||
@Field(() => String, { description: 'Полный заголовок документа' })
|
||||
full_title!: string;
|
||||
|
||||
@Field(() => String, { description: 'Имя пользователя' })
|
||||
username!: string;
|
||||
|
||||
@Field(() => String, { description: 'Кооператив' })
|
||||
coopname!: string;
|
||||
|
||||
@Field(() => Int, { description: 'ID реестра документа' })
|
||||
registry_id!: number;
|
||||
|
||||
@Field(() => String, { description: 'Дата создания', nullable: true })
|
||||
created_at?: string;
|
||||
|
||||
@Field(() => [String], { description: 'Найденные фрагменты с подсветкой' })
|
||||
highlights!: string[];
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Resolver, Query, Args } from '@nestjs/graphql';
|
||||
import { UseGuards } from '@nestjs/common';
|
||||
import { SearchResultDTO } from '../dto/search-result.dto';
|
||||
import { SearchDocumentsInputDTO } from '../dto/search-input.dto';
|
||||
import { DocumentSearchService } from '~/infrastructure/search/opensearch.service';
|
||||
import { GqlJwtAuthGuard } from '~/application/auth/guards/graphql-jwt-auth.guard';
|
||||
import config from '~/config/config';
|
||||
|
||||
@Resolver()
|
||||
export class SearchResolver {
|
||||
constructor(private readonly documentSearch: DocumentSearchService) {}
|
||||
|
||||
@Query(() => [SearchResultDTO], { description: 'Полнотекстовый поиск по документам кооператива' })
|
||||
@UseGuards(GqlJwtAuthGuard)
|
||||
async searchDocuments(
|
||||
@Args('data') input: SearchDocumentsInputDTO,
|
||||
): Promise<SearchResultDTO[]> {
|
||||
if (!this.documentSearch.isAvailable()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.documentSearch.search(input.query, config.coopname, input.limit || 20);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { SearchResolver } from './resolvers/search.resolver';
|
||||
import { SearchEventService } from './services/search-event.service';
|
||||
|
||||
@Module({
|
||||
providers: [SearchResolver, SearchEventService],
|
||||
})
|
||||
export class SearchModule {}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { SovietContract } from 'cooptypes';
|
||||
import { DocumentSearchService } from '~/infrastructure/search/opensearch.service';
|
||||
import { GeneratorInfrastructureService } from '~/infrastructure/generator/generator.service';
|
||||
import type { IAction } from '~/types/common';
|
||||
|
||||
/**
|
||||
* Сервис индексации документов по событию blockchain.
|
||||
* Слушает action::soviet::newsubmitted — подписанные и принятые документы.
|
||||
*/
|
||||
@Injectable()
|
||||
export class SearchEventService {
|
||||
private readonly logger = new Logger(SearchEventService.name);
|
||||
|
||||
constructor(
|
||||
private readonly documentSearch: DocumentSearchService,
|
||||
private readonly generatorService: GeneratorInfrastructureService,
|
||||
) {}
|
||||
|
||||
@OnEvent(`action::${SovietContract.contractName.production}::${SovietContract.Actions.Registry.NewSubmitted.actionName}`)
|
||||
async onDocumentSubmitted(action: IAction): Promise<void> {
|
||||
if (!this.documentSearch.isAvailable()) return;
|
||||
|
||||
try {
|
||||
const docHash = action.data?.document?.hash;
|
||||
if (!docHash) {
|
||||
this.logger.debug('Событие newsubmitted без хеша документа, пропускаем');
|
||||
return;
|
||||
}
|
||||
|
||||
const document = await this.generatorService.getDocument({ hash: docHash });
|
||||
if (!document) {
|
||||
this.logger.debug(`Документ ${docHash.substring(0, 8)} не найден в MongoDB`);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.documentSearch.indexDocument({
|
||||
hash: document.hash,
|
||||
full_title: document.full_title || '',
|
||||
html: document.html || '',
|
||||
username: document.meta?.username || action.data?.username || '',
|
||||
coopname: document.meta?.coopname || action.data?.coopname || '',
|
||||
registry_id: document.meta?.registry_id || 0,
|
||||
created_at: document.meta?.created_at || '',
|
||||
});
|
||||
|
||||
this.logger.log(`Документ проиндексирован: ${document.full_title?.substring(0, 50)}`);
|
||||
} catch (error: any) {
|
||||
this.logger.warn(`Ошибка индексации документа по событию newsubmitted: ${error?.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { ObjectType, Field, InputType, registerEnumType } from '@nestjs/graphql';
|
||||
import { IsString, IsOptional, IsArray, IsInt, IsEnum } from 'class-validator';
|
||||
import { ShareTargetType } from '~/infrastructure/share/share-token.entity';
|
||||
|
||||
registerEnumType(ShareTargetType, { name: 'ShareTargetType' });
|
||||
|
||||
@InputType('CreateShareLinkInput')
|
||||
export class CreateShareLinkInputDTO {
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
pagePath!: string;
|
||||
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
pageName!: string;
|
||||
|
||||
@Field(() => ShareTargetType)
|
||||
@IsEnum(ShareTargetType)
|
||||
targetType!: ShareTargetType;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
targetUsername?: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
linkName?: string;
|
||||
|
||||
@Field(() => [String])
|
||||
@IsArray()
|
||||
allowedActions!: string[];
|
||||
|
||||
@Field(() => Number, { nullable: true })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
expiresInDays?: number;
|
||||
}
|
||||
|
||||
@ObjectType('ShareLink')
|
||||
export class ShareLinkDTO {
|
||||
@Field(() => String)
|
||||
id!: string;
|
||||
|
||||
@Field(() => String)
|
||||
page_path!: string;
|
||||
|
||||
@Field(() => String)
|
||||
page_name!: string;
|
||||
|
||||
@Field(() => ShareTargetType)
|
||||
target_type!: ShareTargetType;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
target_username?: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
link_name?: string;
|
||||
|
||||
@Field(() => [String])
|
||||
allowed_actions!: string[];
|
||||
|
||||
@Field(() => String)
|
||||
token!: string;
|
||||
|
||||
@Field(() => Boolean)
|
||||
is_active!: boolean;
|
||||
|
||||
@Field(() => Date)
|
||||
created_at!: Date;
|
||||
|
||||
@Field(() => Date, { nullable: true })
|
||||
expires_at?: Date;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { Resolver, Query, Mutation, Args } from '@nestjs/graphql';
|
||||
import { UseGuards } from '@nestjs/common';
|
||||
import { GqlJwtAuthGuard } from '~/application/auth/guards/graphql-jwt-auth.guard';
|
||||
import { CurrentUser } from '~/application/auth/decorators/current-user.decorator';
|
||||
import type { MonoAccountDomainInterface } from '~/domain/account/interfaces/mono-account-domain.interface';
|
||||
import { ShareService } from '~/infrastructure/share/share.service';
|
||||
import { ShareLinkDTO, CreateShareLinkInputDTO } from '../dto/share.dto';
|
||||
import { config } from '~/config';
|
||||
|
||||
@Resolver()
|
||||
export class ShareResolver {
|
||||
constructor(private readonly shareService: ShareService) {}
|
||||
|
||||
@Mutation(() => ShareLinkDTO, {
|
||||
name: 'createShareLink',
|
||||
description: 'Создать ссылку доступа к странице',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard)
|
||||
async createShareLink(
|
||||
@Args('data') data: CreateShareLinkInputDTO,
|
||||
@CurrentUser() user: MonoAccountDomainInterface,
|
||||
): Promise<ShareLinkDTO> {
|
||||
return this.shareService.createShareLink({
|
||||
coopname: config.coopname,
|
||||
createdBy: user.username,
|
||||
pagePath: data.pagePath,
|
||||
pageName: data.pageName,
|
||||
targetType: data.targetType,
|
||||
targetUsername: data.targetUsername,
|
||||
linkName: data.linkName,
|
||||
allowedActions: data.allowedActions,
|
||||
expiresInDays: data.expiresInDays,
|
||||
}) as any;
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean, {
|
||||
name: 'revokeShareLink',
|
||||
description: 'Отозвать ссылку доступа',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard)
|
||||
async revokeShareLink(
|
||||
@Args('id') id: string,
|
||||
@CurrentUser() user: MonoAccountDomainInterface,
|
||||
): Promise<boolean> {
|
||||
await this.shareService.revokeShareLink(id, user.username);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Query(() => [ShareLinkDTO], {
|
||||
name: 'getMyShareLinks',
|
||||
description: 'Получить созданные мной ссылки доступа',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard)
|
||||
async getMyShareLinks(
|
||||
@CurrentUser() user: MonoAccountDomainInterface,
|
||||
): Promise<ShareLinkDTO[]> {
|
||||
return this.shareService.getShareLinks(config.coopname, user.username) as any;
|
||||
}
|
||||
|
||||
@Query(() => [ShareLinkDTO], {
|
||||
name: 'getSharedWithMe',
|
||||
description: 'Получить страницы, к которым мне предоставлен доступ',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard)
|
||||
async getSharedWithMe(
|
||||
@CurrentUser() user: MonoAccountDomainInterface,
|
||||
): Promise<ShareLinkDTO[]> {
|
||||
return this.shareService.getSharedWithMe(config.coopname, user.username) as any;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ShareResolver } from './resolvers/share.resolver';
|
||||
|
||||
@Module({
|
||||
providers: [ShareResolver],
|
||||
})
|
||||
export class ShareAppModule {}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { ObjectType, Field } from '@nestjs/graphql';
|
||||
|
||||
@ObjectType('SystemFeatures')
|
||||
export class SystemFeaturesDTO {
|
||||
@Field(() => Boolean, { description: 'Доступен ли полнотекстовый поиск по документам' })
|
||||
search!: boolean;
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import { VarsDTO } from './vars.dto';
|
||||
import { SymbolsDTO } from './symbols.dto';
|
||||
import { SettingsDTO } from './settings.dto';
|
||||
import { BoardMemberDTO } from './board-member.dto';
|
||||
import { SystemFeaturesDTO } from './system-features.dto';
|
||||
|
||||
@ObjectType('SystemInfo')
|
||||
export class SystemInfoDTO {
|
||||
@@ -67,6 +68,10 @@ export class SystemInfoDTO {
|
||||
@ValidateNested()
|
||||
public readonly board_members?: BoardMemberDTO[];
|
||||
|
||||
@Field(() => SystemFeaturesDTO, { description: 'Доступные функции платформы' })
|
||||
@ValidateNested()
|
||||
public readonly features!: SystemFeaturesDTO;
|
||||
|
||||
constructor(entity: SystemInfoDomainEntity, isProvidered = false) {
|
||||
this.coopname = entity.coopname;
|
||||
this.contacts = entity.contacts;
|
||||
@@ -85,5 +90,6 @@ export class SystemInfoDTO {
|
||||
this.is_unioned = entity.is_unioned;
|
||||
this.union_link = entity.union_link;
|
||||
this.board_members = entity.board_members?.map((member) => new BoardMemberDTO(member));
|
||||
this.features = entity.features || { search: false };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,6 +230,9 @@ export class SystemInteractor {
|
||||
is_unioned: config.union.is_unioned,
|
||||
union_link: config.union.link,
|
||||
board_members,
|
||||
features: {
|
||||
search: process.env.OPENSEARCH_ENABLED === 'true',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import { Resolver, Subscription, ObjectType, Field } from '@nestjs/graphql';
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { PubSub } from 'graphql-subscriptions';
|
||||
import { PUB_SUB } from '~/infrastructure/pubsub/pubsub.module';
|
||||
|
||||
export const SYSTEM_EVENTS = {
|
||||
STATUS_CHANGED: 'systemStatusChanged',
|
||||
};
|
||||
|
||||
@ObjectType('SystemStatusChange')
|
||||
export class SystemStatusChangeDTO {
|
||||
@Field(() => String)
|
||||
status!: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
message?: string;
|
||||
}
|
||||
|
||||
@Resolver()
|
||||
export class SystemSubscriptionResolver {
|
||||
constructor(@Inject(PUB_SUB) private readonly pubSub: PubSub) {}
|
||||
|
||||
@Subscription(() => SystemStatusChangeDTO, {
|
||||
name: 'systemStatusChanged',
|
||||
description: 'Уведомление об изменении состояния системы (статус, конфигурация)',
|
||||
})
|
||||
systemStatusChanged() {
|
||||
return this.pubSub.asyncIterableIterator(SYSTEM_EVENTS.STATUS_CHANGED);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { PubSub } from 'graphql-subscriptions';
|
||||
import { SystemInfoDTO } from '../dto/system.dto';
|
||||
import { SystemInteractor } from '../interactors/system.interactor';
|
||||
import { ProviderService } from '~/application/provider/services/provider.service';
|
||||
@@ -20,6 +21,8 @@ import {
|
||||
AGREEMENT_CONFIGURATION_SERVICE,
|
||||
} from '~/domain/registration/services/agreement-configuration.service';
|
||||
import type { AccountType } from '~/application/account/enum/account-type.enum';
|
||||
import { PUB_SUB } from '~/infrastructure/pubsub/pubsub.module';
|
||||
import { SYSTEM_EVENTS } from '../resolvers/system-subscription.resolver';
|
||||
|
||||
@Injectable()
|
||||
export class SystemService {
|
||||
@@ -27,7 +30,8 @@ export class SystemService {
|
||||
private readonly systemInteractor: SystemInteractor,
|
||||
private readonly providerService: ProviderService,
|
||||
@Inject(AGREEMENT_CONFIGURATION_SERVICE)
|
||||
private readonly agreementConfigService: AgreementConfigurationService
|
||||
private readonly agreementConfigService: AgreementConfigurationService,
|
||||
@Inject(PUB_SUB) private readonly pubSub: PubSub,
|
||||
) {}
|
||||
|
||||
public async getInfo(): Promise<SystemInfoDTO> {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { SystemService } from './services/system.service';
|
||||
import { SystemResolver } from './resolvers/system.resolver';
|
||||
import { SystemSubscriptionResolver } from './resolvers/system-subscription.resolver';
|
||||
import { SystemDomainModule } from '~/domain/system/system-domain.module';
|
||||
import { ProviderModule } from '~/application/provider/provider.module';
|
||||
import { SystemInteractor } from './interactors/system.interactor';
|
||||
@@ -16,6 +17,7 @@ import { PaymentMethodInfrastructureModule } from '~/infrastructure/payment-meth
|
||||
import { AccountDomainModule } from '~/domain/account/account-domain.module';
|
||||
import { SettingsInfrastructureModule } from '~/infrastructure/settings/settings-infrastructure.module';
|
||||
import { VaultDomainModule } from '~/domain/vault/vault-domain.module';
|
||||
import { PubSubModule } from '~/infrastructure/pubsub/pubsub.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -27,11 +29,13 @@ import { VaultDomainModule } from '~/domain/vault/vault-domain.module';
|
||||
PaymentMethodInfrastructureModule,
|
||||
AccountDomainModule,
|
||||
SettingsInfrastructureModule,
|
||||
PubSubModule,
|
||||
],
|
||||
controllers: [],
|
||||
providers: [
|
||||
SystemService,
|
||||
SystemResolver,
|
||||
SystemSubscriptionResolver,
|
||||
SystemInteractor,
|
||||
LoadContactsInteractor,
|
||||
WifInteractor,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { MarketContract } from 'cooptypes';
|
||||
import type { TransactResult } from '@wharfkit/session';
|
||||
|
||||
type DocumentInput = any;
|
||||
|
||||
export interface CooplaceBlockchainPort {
|
||||
acceptRequest(data: MarketContract.Actions.AcceptRequest.IAcceptRequest): Promise<TransactResult>;
|
||||
cancelRequest(data: MarketContract.Actions.CancelRequest.ICancelRequest): Promise<TransactResult>;
|
||||
@@ -19,6 +21,13 @@ export interface CooplaceBlockchainPort {
|
||||
supplyOnRequest(data: MarketContract.Actions.SupplyOnRequest.ISupplyOnRequest): Promise<TransactResult>;
|
||||
unpublishRequest(data: MarketContract.Actions.UnpublishRequest.IUnpublishRequest): Promise<TransactResult>;
|
||||
updateRequest(data: MarketContract.Actions.UpdateRequest.IUpdateRequest): Promise<TransactResult>;
|
||||
|
||||
// Новые actions
|
||||
reqReturn(data: { coopname: string; username: string; request_hash: string; return_statement: DocumentInput }): Promise<TransactResult>;
|
||||
coopstock(data: { coopname: string; braname: string; hash: string; units: number; unit_cost: string; product_lifecycle_secs: number; warranty_period_secs: number; membership_fee_amount: string; meta: string }): Promise<TransactResult>;
|
||||
acceptStock(data: { coopname: string; username: string; request_hash: string; convert_in: DocumentInput; return_statement: DocumentInput }): Promise<TransactResult>;
|
||||
destroy(data: { coopname: string; request_hash: string; destruction_act: DocumentInput }): Promise<TransactResult>;
|
||||
reoffer(data: { coopname: string; request_hash: string; new_hash: string; new_unit_cost: string; new_meta: string }): Promise<TransactResult>;
|
||||
}
|
||||
|
||||
export const COOPLACE_BLOCKCHAIN_PORT = Symbol('CooplaceBlockchainPort');
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { VarsDomainInterface } from '../interfaces/vars-domain.interface';
|
||||
import { SymbolsDTO } from '~/application/system/dto/symbols.dto';
|
||||
import type { SettingsDomainInterface } from '~/domain/settings/interfaces/settings-domain.interface';
|
||||
import type { BoardMemberDomainInterface } from '../interfaces/board-member-domain.interface';
|
||||
import type { SystemFeaturesDomainInterface } from '../interfaces/system-features-domain.interface';
|
||||
|
||||
export class SystemInfoDomainEntity {
|
||||
public readonly coopname!: string;
|
||||
@@ -22,6 +23,7 @@ export class SystemInfoDomainEntity {
|
||||
public readonly is_unioned!: boolean;
|
||||
public readonly union_link!: string;
|
||||
public readonly board_members?: BoardMemberDomainInterface[];
|
||||
public readonly features!: SystemFeaturesDomainInterface;
|
||||
|
||||
constructor(data: SystemInfoDomainEntity) {
|
||||
Object.assign(this, data);
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface SystemFeaturesDomainInterface {
|
||||
/** Доступен ли полнотекстовый поиск по документам */
|
||||
search: boolean;
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import { ObjectType, Field, InputType } from '@nestjs/graphql';
|
||||
import { IsString } from 'class-validator';
|
||||
|
||||
@ObjectType('ProcessEdge')
|
||||
export class ProcessEdgeDTO {
|
||||
@Field(() => String)
|
||||
id!: string;
|
||||
|
||||
@Field(() => String)
|
||||
source!: string;
|
||||
|
||||
@Field(() => String)
|
||||
target!: string;
|
||||
}
|
||||
|
||||
@InputType('ProcessEdgeInput')
|
||||
export class ProcessEdgeInputDTO {
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
id!: string;
|
||||
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
source!: string;
|
||||
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
target!: string;
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
import { ObjectType, Field, InputType, Int, registerEnumType } from '@nestjs/graphql';
|
||||
import { IsString, IsOptional } from 'class-validator';
|
||||
import { ProcessInstanceStatus, ProcessStepStatus } from '../../../domain/enums/process-status.enum';
|
||||
|
||||
registerEnumType(ProcessInstanceStatus, { name: 'ProcessInstanceStatus' });
|
||||
registerEnumType(ProcessStepStatus, { name: 'ProcessStepStatus' });
|
||||
|
||||
@ObjectType('ProcessStepState')
|
||||
export class ProcessStepStateDTO {
|
||||
@Field(() => String)
|
||||
step_id!: string;
|
||||
|
||||
@Field(() => ProcessStepStatus)
|
||||
status!: ProcessStepStatus;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
issue_hash?: string;
|
||||
|
||||
@Field(() => Date, { nullable: true })
|
||||
completed_at?: Date;
|
||||
}
|
||||
|
||||
@ObjectType('ProcessInstance')
|
||||
export class ProcessInstanceDTO {
|
||||
@Field(() => String)
|
||||
id!: string;
|
||||
|
||||
@Field(() => String)
|
||||
coopname!: string;
|
||||
|
||||
@Field(() => String)
|
||||
template_id!: string;
|
||||
|
||||
@Field(() => String)
|
||||
project_hash!: string;
|
||||
|
||||
@Field(() => ProcessInstanceStatus)
|
||||
status!: ProcessInstanceStatus;
|
||||
|
||||
@Field(() => String)
|
||||
started_by!: string;
|
||||
|
||||
@Field(() => Int)
|
||||
cycle!: number;
|
||||
|
||||
@Field(() => [ProcessStepStateDTO])
|
||||
step_states!: ProcessStepStateDTO[];
|
||||
|
||||
@Field(() => Date)
|
||||
started_at!: Date;
|
||||
|
||||
@Field(() => Date, { nullable: true })
|
||||
completed_at?: Date;
|
||||
}
|
||||
|
||||
@InputType('StartProcessInput')
|
||||
export class StartProcessInputDTO {
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
template_id!: string;
|
||||
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
project_hash!: string;
|
||||
}
|
||||
|
||||
@InputType('CompleteProcessStepInput')
|
||||
export class CompleteProcessStepInputDTO {
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
instance_id!: string;
|
||||
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
step_id!: string;
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
import { ObjectType, Field, InputType, Float } from '@nestjs/graphql';
|
||||
import { IsString, IsOptional, IsNumber } from 'class-validator';
|
||||
|
||||
@ObjectType('ProcessStepPosition')
|
||||
@InputType('ProcessStepPositionInput')
|
||||
export class ProcessStepPositionDTO {
|
||||
@Field(() => Float)
|
||||
@IsNumber()
|
||||
x!: number;
|
||||
|
||||
@Field(() => Float)
|
||||
@IsNumber()
|
||||
y!: number;
|
||||
}
|
||||
|
||||
@ObjectType('ProcessStepTemplate')
|
||||
export class ProcessStepTemplateDTO {
|
||||
@Field(() => String)
|
||||
id!: string;
|
||||
|
||||
@Field(() => String)
|
||||
title!: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
description?: string;
|
||||
|
||||
@Field(() => Float, { nullable: true })
|
||||
estimate?: number;
|
||||
|
||||
@Field(() => ProcessStepPositionDTO)
|
||||
position!: ProcessStepPositionDTO;
|
||||
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
is_start?: boolean;
|
||||
}
|
||||
|
||||
@InputType('ProcessStepTemplateInput')
|
||||
export class ProcessStepTemplateInputDTO {
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
id!: string;
|
||||
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
title!: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
|
||||
@Field(() => Float, { nullable: true })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
estimate?: number;
|
||||
|
||||
@Field(() => ProcessStepPositionDTO)
|
||||
position!: ProcessStepPositionDTO;
|
||||
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
@IsOptional()
|
||||
is_start?: boolean;
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
import { ObjectType, Field, InputType, registerEnumType } from '@nestjs/graphql';
|
||||
import { IsString, IsOptional, IsArray, ValidateNested } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { ProcessTemplateStatus } from '../../../domain/enums/process-status.enum';
|
||||
import { ProcessStepTemplateDTO, ProcessStepTemplateInputDTO } from './process-step.dto';
|
||||
import { ProcessEdgeDTO, ProcessEdgeInputDTO } from './process-edge.dto';
|
||||
|
||||
registerEnumType(ProcessTemplateStatus, { name: 'ProcessTemplateStatus' });
|
||||
|
||||
@ObjectType('ProcessTemplate')
|
||||
export class ProcessTemplateDTO {
|
||||
@Field(() => String)
|
||||
id!: string;
|
||||
|
||||
@Field(() => String)
|
||||
coopname!: string;
|
||||
|
||||
@Field(() => String)
|
||||
project_hash!: string;
|
||||
|
||||
@Field(() => String)
|
||||
title!: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
description?: string;
|
||||
|
||||
@Field(() => ProcessTemplateStatus)
|
||||
status!: ProcessTemplateStatus;
|
||||
|
||||
@Field(() => String)
|
||||
created_by!: string;
|
||||
|
||||
@Field(() => [ProcessStepTemplateDTO])
|
||||
steps!: ProcessStepTemplateDTO[];
|
||||
|
||||
@Field(() => [ProcessEdgeDTO])
|
||||
edges!: ProcessEdgeDTO[];
|
||||
|
||||
@Field(() => Date)
|
||||
created_at!: Date;
|
||||
|
||||
@Field(() => Date)
|
||||
updated_at!: Date;
|
||||
}
|
||||
|
||||
@InputType('CreateProcessTemplateInput')
|
||||
export class CreateProcessTemplateInputDTO {
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
project_hash!: string;
|
||||
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
title!: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
}
|
||||
|
||||
@InputType('UpdateProcessTemplateInput')
|
||||
export class UpdateProcessTemplateInputDTO {
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
id!: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
title?: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
|
||||
@Field(() => ProcessTemplateStatus, { nullable: true })
|
||||
@IsOptional()
|
||||
status?: ProcessTemplateStatus;
|
||||
|
||||
@Field(() => [ProcessStepTemplateInputDTO], { nullable: true })
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => ProcessStepTemplateInputDTO)
|
||||
steps?: ProcessStepTemplateInputDTO[];
|
||||
|
||||
@Field(() => [ProcessEdgeInputDTO], { nullable: true })
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => ProcessEdgeInputDTO)
|
||||
edges?: ProcessEdgeInputDTO[];
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
import { Resolver, Subscription, Args, ObjectType, Field } from '@nestjs/graphql';
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { PubSub } from 'graphql-subscriptions';
|
||||
import { PUB_SUB } from '~/infrastructure/pubsub/pubsub.module';
|
||||
import { IssueOutputDTO } from '../dto/generation/issue.dto';
|
||||
import { CommitOutputDTO } from '../dto/generation/commit.dto';
|
||||
|
||||
export const CAPITAL_EVENTS = {
|
||||
ISSUE_UPDATED: 'capitalIssueUpdated',
|
||||
ISSUE_CREATED: 'capitalIssueCreated',
|
||||
COMMIT_CREATED: 'capitalCommitCreated',
|
||||
COMMIT_UPDATED: 'capitalCommitUpdated',
|
||||
DATA_CHANGED: 'capitalDataChanged',
|
||||
};
|
||||
|
||||
@ObjectType('CapitalDataChange')
|
||||
export class CapitalDataChangeDTO {
|
||||
@Field(() => String)
|
||||
entity!: string;
|
||||
|
||||
@Field(() => String)
|
||||
action!: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
id?: string;
|
||||
}
|
||||
|
||||
@Resolver()
|
||||
export class CapitalSubscriptionResolver {
|
||||
constructor(@Inject(PUB_SUB) private readonly pubSub: PubSub) {}
|
||||
|
||||
@Subscription(() => IssueOutputDTO, {
|
||||
name: 'capitalIssueUpdated',
|
||||
description: 'Подписка на обновления задач',
|
||||
filter: (payload: any, variables: any) => {
|
||||
if (variables.project_hash && payload.capitalIssueUpdated.project_hash !== variables.project_hash) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
})
|
||||
capitalIssueUpdated(
|
||||
@Args('project_hash', { nullable: true }) _projectHash?: string,
|
||||
) {
|
||||
return this.pubSub.asyncIterableIterator(CAPITAL_EVENTS.ISSUE_UPDATED);
|
||||
}
|
||||
|
||||
@Subscription(() => IssueOutputDTO, {
|
||||
name: 'capitalIssueCreated',
|
||||
description: 'Подписка на создание задач',
|
||||
filter: (payload: any, variables: any) => {
|
||||
if (variables.project_hash && payload.capitalIssueCreated.project_hash !== variables.project_hash) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
})
|
||||
capitalIssueCreated(
|
||||
@Args('project_hash', { nullable: true }) _projectHash?: string,
|
||||
) {
|
||||
return this.pubSub.asyncIterableIterator(CAPITAL_EVENTS.ISSUE_CREATED);
|
||||
}
|
||||
|
||||
@Subscription(() => CommitOutputDTO, {
|
||||
name: 'capitalCommitCreated',
|
||||
description: 'Подписка на создание коммитов',
|
||||
filter: (payload: any, variables: any) => {
|
||||
if (variables.project_hash && payload.capitalCommitCreated.project_hash !== variables.project_hash) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
})
|
||||
capitalCommitCreated(
|
||||
@Args('project_hash', { nullable: true }) _projectHash?: string,
|
||||
) {
|
||||
return this.pubSub.asyncIterableIterator(CAPITAL_EVENTS.COMMIT_CREATED);
|
||||
}
|
||||
|
||||
@Subscription(() => CommitOutputDTO, {
|
||||
name: 'capitalCommitUpdated',
|
||||
description: 'Подписка на обновления коммитов',
|
||||
filter: (payload: any, variables: any) => {
|
||||
if (variables.project_hash && payload.capitalCommitUpdated.project_hash !== variables.project_hash) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
})
|
||||
capitalCommitUpdated(
|
||||
@Args('project_hash', { nullable: true }) _projectHash?: string,
|
||||
) {
|
||||
return this.pubSub.asyncIterableIterator(CAPITAL_EVENTS.COMMIT_UPDATED);
|
||||
}
|
||||
|
||||
@Subscription(() => CapitalDataChangeDTO, {
|
||||
name: 'capitalDataChanged',
|
||||
description: 'Уведомление об изменении данных Capital (проекты, участники, голосования)',
|
||||
})
|
||||
capitalDataChanged() {
|
||||
return this.pubSub.asyncIterableIterator(CAPITAL_EVENTS.DATA_CHANGED);
|
||||
}
|
||||
}
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
import { Resolver, Mutation, Args, Query } from '@nestjs/graphql';
|
||||
import { UseGuards } from '@nestjs/common';
|
||||
import { GqlJwtAuthGuard } from '~/application/auth/guards/graphql-jwt-auth.guard';
|
||||
import { RolesGuard } from '~/application/auth/guards/roles.guard';
|
||||
import { AuthRoles } from '~/application/auth/decorators/auth.decorator';
|
||||
import { CurrentUser } from '~/application/auth/decorators/current-user.decorator';
|
||||
import type { MonoAccountDomainInterface } from '~/domain/account/interfaces/mono-account-domain.interface';
|
||||
import { ProcessService } from '../services/process.service';
|
||||
import { ProcessTemplateDTO, CreateProcessTemplateInputDTO, UpdateProcessTemplateInputDTO } from '../dto/process/process-template.dto';
|
||||
import { ProcessInstanceDTO, StartProcessInputDTO, CompleteProcessStepInputDTO } from '../dto/process/process-instance.dto';
|
||||
import { config } from '~/config';
|
||||
|
||||
@Resolver()
|
||||
export class ProcessResolver {
|
||||
constructor(private readonly processService: ProcessService) {}
|
||||
|
||||
// ──── ШАБЛОНЫ (chairman/member) ────
|
||||
|
||||
@Mutation(() => ProcessTemplateDTO, {
|
||||
name: 'capitalCreateProcessTemplate',
|
||||
description: 'Создание шаблона процесса',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['chairman', 'member'])
|
||||
async createProcessTemplate(
|
||||
@Args('data') data: CreateProcessTemplateInputDTO,
|
||||
@CurrentUser() user: MonoAccountDomainInterface,
|
||||
): Promise<ProcessTemplateDTO> {
|
||||
return this.processService.createTemplate({
|
||||
coopname: config.coopname,
|
||||
project_hash: data.project_hash,
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
created_by: user.username,
|
||||
}) as any;
|
||||
}
|
||||
|
||||
@Mutation(() => ProcessTemplateDTO, {
|
||||
name: 'capitalUpdateProcessTemplate',
|
||||
description: 'Обновление шаблона процесса (шаги, рёбра, статус)',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['chairman', 'member'])
|
||||
async updateProcessTemplate(
|
||||
@Args('data') data: UpdateProcessTemplateInputDTO,
|
||||
): Promise<ProcessTemplateDTO> {
|
||||
return this.processService.updateTemplate(data.id, data) as any;
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean, {
|
||||
name: 'capitalDeleteProcessTemplate',
|
||||
description: 'Удаление шаблона процесса',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['chairman', 'member'])
|
||||
async deleteProcessTemplate(
|
||||
@Args('id') id: string,
|
||||
): Promise<boolean> {
|
||||
await this.processService.deleteTemplate(id);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Query(() => [ProcessTemplateDTO], {
|
||||
name: 'capitalGetProcessTemplates',
|
||||
description: 'Получение шаблонов процессов для проекта',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard)
|
||||
async getProcessTemplates(
|
||||
@Args('project_hash', { nullable: true }) projectHash?: string,
|
||||
): Promise<ProcessTemplateDTO[]> {
|
||||
if (projectHash) {
|
||||
return this.processService.getTemplatesByProject(projectHash) as any;
|
||||
}
|
||||
return this.processService.getTemplatesByCoopname(config.coopname) as any;
|
||||
}
|
||||
|
||||
@Query(() => ProcessTemplateDTO, {
|
||||
name: 'capitalGetProcessTemplate',
|
||||
description: 'Получение шаблона процесса по ID',
|
||||
nullable: true,
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard)
|
||||
async getProcessTemplate(
|
||||
@Args('id') id: string,
|
||||
): Promise<ProcessTemplateDTO | null> {
|
||||
return this.processService.getTemplate(id) as any;
|
||||
}
|
||||
|
||||
// ──── ЭКЗЕМПЛЯРЫ (chairman/member/user) ────
|
||||
|
||||
@Mutation(() => ProcessInstanceDTO, {
|
||||
name: 'capitalStartProcess',
|
||||
description: 'Запуск экземпляра процесса',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['chairman', 'member', 'user'])
|
||||
async startProcess(
|
||||
@Args('data') data: StartProcessInputDTO,
|
||||
@CurrentUser() user: MonoAccountDomainInterface,
|
||||
): Promise<ProcessInstanceDTO> {
|
||||
return this.processService.startProcess({
|
||||
template_id: data.template_id,
|
||||
project_hash: data.project_hash,
|
||||
started_by: user.username,
|
||||
coopname: config.coopname,
|
||||
}) as any;
|
||||
}
|
||||
|
||||
@Mutation(() => ProcessInstanceDTO, {
|
||||
name: 'capitalCompleteProcessStep',
|
||||
description: 'Завершение шага процесса',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['chairman', 'member', 'user'])
|
||||
async completeProcessStep(
|
||||
@Args('data') data: CompleteProcessStepInputDTO,
|
||||
): Promise<ProcessInstanceDTO> {
|
||||
return this.processService.completeStep(data.instance_id, data.step_id) as any;
|
||||
}
|
||||
|
||||
@Query(() => [ProcessInstanceDTO], {
|
||||
name: 'capitalGetProcessInstances',
|
||||
description: 'Получение экземпляров процессов для проекта',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard)
|
||||
async getProcessInstances(
|
||||
@Args('project_hash') projectHash: string,
|
||||
): Promise<ProcessInstanceDTO[]> {
|
||||
return this.processService.getInstancesByProject(projectHash) as any;
|
||||
}
|
||||
|
||||
@Query(() => ProcessInstanceDTO, {
|
||||
name: 'capitalGetProcessInstance',
|
||||
description: 'Получение экземпляра процесса по ID',
|
||||
nullable: true,
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard)
|
||||
async getProcessInstance(
|
||||
@Args('id') id: string,
|
||||
): Promise<ProcessInstanceDTO | null> {
|
||||
return this.processService.getInstance(id) as any;
|
||||
}
|
||||
}
|
||||
+42
-13
@@ -1,4 +1,7 @@
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { PubSub } from 'graphql-subscriptions';
|
||||
import { PUB_SUB } from '~/infrastructure/pubsub/pubsub.module';
|
||||
import { CAPITAL_EVENTS } from '../resolvers/capital-subscription.resolver';
|
||||
import { generateUniqueHash } from '~/utils/generate-hash.util';
|
||||
import { GenerationInteractor } from '../use-cases/generation.interactor';
|
||||
import type { CreateCommitInputDTO } from '../dto/generation/create-commit-input.dto';
|
||||
@@ -57,6 +60,7 @@ import type { MonoAccountDomainInterface } from '~/domain/account/interfaces/mon
|
||||
@Injectable()
|
||||
export class GenerationService {
|
||||
constructor(
|
||||
@Inject(PUB_SUB) private readonly pubSub: PubSub,
|
||||
private readonly generationInteractor: GenerationInteractor,
|
||||
@Inject(STORY_REPOSITORY)
|
||||
private readonly storyRepository: StoryRepository,
|
||||
@@ -84,7 +88,13 @@ export class GenerationService {
|
||||
*/
|
||||
async createCommit(data: CreateCommitInputDTO, currentUser: MonoAccountDomainInterface): Promise<CommitOutputDTO> {
|
||||
const commitEntity = await this.generationInteractor.createCommit(data, currentUser);
|
||||
return await this.commitMapperService.toDTO(commitEntity, currentUser);
|
||||
const result = await this.commitMapperService.toDTO(commitEntity, currentUser);
|
||||
|
||||
this.pubSub.publish(CAPITAL_EVENTS.COMMIT_CREATED, {
|
||||
[CAPITAL_EVENTS.COMMIT_CREATED]: result,
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -97,7 +107,13 @@ export class GenerationService {
|
||||
};
|
||||
|
||||
const commitEntity = await this.generationInteractor.approveCommit(domainInput);
|
||||
return await this.commitMapperService.toDTO(commitEntity, currentUser);
|
||||
const result = await this.commitMapperService.toDTO(commitEntity, currentUser);
|
||||
|
||||
this.pubSub.publish(CAPITAL_EVENTS.COMMIT_UPDATED, {
|
||||
[CAPITAL_EVENTS.COMMIT_UPDATED]: result,
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -109,8 +125,15 @@ export class GenerationService {
|
||||
master: currentUser.username,
|
||||
};
|
||||
|
||||
|
||||
const commitEntity = await this.generationInteractor.declineCommit(domainInput);
|
||||
return await this.commitMapperService.toDTO(commitEntity, currentUser);
|
||||
const result = await this.commitMapperService.toDTO(commitEntity, currentUser);
|
||||
|
||||
this.pubSub.publish(CAPITAL_EVENTS.COMMIT_UPDATED, {
|
||||
[CAPITAL_EVENTS.COMMIT_UPDATED]: result,
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ============ STORY METHODS ============
|
||||
@@ -424,11 +447,14 @@ export class GenerationService {
|
||||
// Рассчитываем права доступа для задачи
|
||||
const permissions = await this.permissionsService.calculateIssuePermissions(savedIssue, currentUser);
|
||||
|
||||
// Возвращаем задачу с правами доступа
|
||||
return {
|
||||
...savedIssue,
|
||||
permissions,
|
||||
} as IssueOutputDTO;
|
||||
const issueResult = { ...savedIssue, permissions } as IssueOutputDTO;
|
||||
|
||||
// Публикуем событие для подписчиков
|
||||
this.pubSub.publish(CAPITAL_EVENTS.ISSUE_CREATED, {
|
||||
[CAPITAL_EVENTS.ISSUE_CREATED]: issueResult,
|
||||
});
|
||||
|
||||
return issueResult;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -552,11 +578,14 @@ export class GenerationService {
|
||||
// Рассчитываем права доступа для задачи
|
||||
const permissions = await this.permissionsService.calculateIssuePermissions(updatedIssue, currentUser);
|
||||
|
||||
// Возвращаем задачу с правами доступа
|
||||
return {
|
||||
...updatedIssue,
|
||||
permissions,
|
||||
} as IssueOutputDTO;
|
||||
const issueResult = { ...updatedIssue, permissions } as IssueOutputDTO;
|
||||
|
||||
// Публикуем событие для подписчиков
|
||||
this.pubSub.publish(CAPITAL_EVENTS.ISSUE_UPDATED, {
|
||||
[CAPITAL_EVENTS.ISSUE_UPDATED]: issueResult,
|
||||
});
|
||||
|
||||
return issueResult;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
import { Injectable, Inject, Logger } from '@nestjs/common';
|
||||
import { PROCESS_TEMPLATE_REPOSITORY, PROCESS_INSTANCE_REPOSITORY } from '../../domain/repositories/process.repository';
|
||||
import type { ProcessTemplateRepository, ProcessInstanceRepository } from '../../domain/repositories/process.repository';
|
||||
import type { ProcessTemplateDomainEntity } from '../../domain/entities/process-template.entity';
|
||||
import type { ProcessInstanceDomainEntity, ProcessStepState } from '../../domain/entities/process-instance.entity';
|
||||
import { ProcessTemplateStatus, ProcessInstanceStatus, ProcessStepStatus } from '../../domain/enums/process-status.enum';
|
||||
import { ISSUE_REPOSITORY } from '../../domain/repositories/issue.repository';
|
||||
import type { IssueRepository } from '../../domain/repositories/issue.repository';
|
||||
import { IssueDomainEntity } from '../../domain/entities/issue.entity';
|
||||
import { IssueStatus } from '../../domain/enums/issue-status.enum';
|
||||
import { IssuePriority } from '../../domain/enums/issue-priority.enum';
|
||||
import { config } from '~/config';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
@Injectable()
|
||||
export class ProcessService {
|
||||
private readonly logger = new Logger(ProcessService.name);
|
||||
|
||||
constructor(
|
||||
@Inject(PROCESS_TEMPLATE_REPOSITORY) private readonly templateRepo: ProcessTemplateRepository,
|
||||
@Inject(PROCESS_INSTANCE_REPOSITORY) private readonly instanceRepo: ProcessInstanceRepository,
|
||||
@Inject(ISSUE_REPOSITORY) private readonly issueRepo: IssueRepository,
|
||||
) {}
|
||||
|
||||
// ──── ШАБЛОНЫ ────
|
||||
|
||||
async createTemplate(data: {
|
||||
coopname: string;
|
||||
project_hash: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
created_by: string;
|
||||
}): Promise<ProcessTemplateDomainEntity> {
|
||||
return this.templateRepo.create({
|
||||
...data,
|
||||
status: ProcessTemplateStatus.DRAFT,
|
||||
steps: [],
|
||||
edges: [],
|
||||
});
|
||||
}
|
||||
|
||||
async getTemplate(id: string): Promise<ProcessTemplateDomainEntity | null> {
|
||||
return this.templateRepo.findById(id);
|
||||
}
|
||||
|
||||
async getTemplatesByProject(projectHash: string): Promise<ProcessTemplateDomainEntity[]> {
|
||||
return this.templateRepo.findByProjectHash(projectHash);
|
||||
}
|
||||
|
||||
async getTemplatesByCoopname(coopname: string): Promise<ProcessTemplateDomainEntity[]> {
|
||||
return this.templateRepo.findByCoopname(coopname);
|
||||
}
|
||||
|
||||
async updateTemplate(id: string, data: Partial<ProcessTemplateDomainEntity>): Promise<ProcessTemplateDomainEntity> {
|
||||
return this.templateRepo.update(id, data);
|
||||
}
|
||||
|
||||
async deleteTemplate(id: string): Promise<void> {
|
||||
return this.templateRepo.delete(id);
|
||||
}
|
||||
|
||||
// ──── ЭКЗЕМПЛЯРЫ (ИСПОЛНЕНИЕ) ────
|
||||
|
||||
async startProcess(data: {
|
||||
template_id: string;
|
||||
project_hash: string;
|
||||
started_by: string;
|
||||
coopname: string;
|
||||
}): Promise<ProcessInstanceDomainEntity> {
|
||||
const template = await this.templateRepo.findById(data.template_id);
|
||||
if (!template) throw new Error('Шаблон процесса не найден');
|
||||
if (template.status !== ProcessTemplateStatus.ACTIVE) {
|
||||
throw new Error('Шаблон процесса должен быть в статусе ACTIVE');
|
||||
}
|
||||
|
||||
const startSteps = template.steps.filter(s => s.is_start);
|
||||
if (startSteps.length === 0) throw new Error('В шаблоне нет стартовых шагов');
|
||||
|
||||
const stepStates: ProcessStepState[] = template.steps.map(step => ({
|
||||
step_id: step.id,
|
||||
status: step.is_start ? ProcessStepStatus.ACTIVE : ProcessStepStatus.PENDING,
|
||||
}));
|
||||
|
||||
const instance = await this.instanceRepo.create({
|
||||
coopname: data.coopname,
|
||||
template_id: data.template_id,
|
||||
project_hash: data.project_hash,
|
||||
status: ProcessInstanceStatus.RUNNING,
|
||||
started_by: data.started_by,
|
||||
cycle: 1,
|
||||
step_states: stepStates,
|
||||
});
|
||||
|
||||
for (const step of startSteps) {
|
||||
await this.createIssueForStep(instance, template, step.id, data.started_by);
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
async completeStep(instanceId: string, stepId: string): Promise<ProcessInstanceDomainEntity> {
|
||||
const instance = await this.instanceRepo.findById(instanceId);
|
||||
if (!instance) throw new Error('Экземпляр процесса не найден');
|
||||
|
||||
const template = await this.templateRepo.findById(instance.template_id);
|
||||
if (!template) throw new Error('Шаблон процесса не найден');
|
||||
|
||||
const stepState = instance.step_states.find(s => s.step_id === stepId);
|
||||
if (!stepState) throw new Error('Шаг не найден');
|
||||
if (stepState.status === ProcessStepStatus.COMPLETED) return instance;
|
||||
|
||||
stepState.status = ProcessStepStatus.COMPLETED;
|
||||
stepState.completed_at = new Date();
|
||||
|
||||
const nextStepIds = template.edges
|
||||
.filter(e => e.source === stepId)
|
||||
.map(e => e.target);
|
||||
|
||||
for (const nextId of nextStepIds) {
|
||||
const incomingEdges = template.edges.filter(e => e.target === nextId);
|
||||
const allSourcesCompleted = incomingEdges.every(e => {
|
||||
const srcState = instance.step_states.find(s => s.step_id === e.source);
|
||||
return srcState?.status === ProcessStepStatus.COMPLETED;
|
||||
});
|
||||
|
||||
if (allSourcesCompleted) {
|
||||
const nextState = instance.step_states.find(s => s.step_id === nextId);
|
||||
if (nextState && nextState.status === ProcessStepStatus.PENDING) {
|
||||
nextState.status = ProcessStepStatus.ACTIVE;
|
||||
await this.createIssueForStep(instance, template, nextId, instance.started_by);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const terminalStepIds = template.steps
|
||||
.filter(s => !template.edges.some(e => e.source === s.id))
|
||||
.map(s => s.id);
|
||||
|
||||
const allTerminalDone = terminalStepIds.every(id => {
|
||||
const state = instance.step_states.find(s => s.step_id === id);
|
||||
return state?.status === ProcessStepStatus.COMPLETED || state?.status === ProcessStepStatus.CANCELLED;
|
||||
});
|
||||
|
||||
if (allTerminalDone) {
|
||||
instance.status = ProcessInstanceStatus.COMPLETED;
|
||||
instance.completed_at = new Date();
|
||||
}
|
||||
|
||||
return this.instanceRepo.update(instance.id, {
|
||||
step_states: instance.step_states,
|
||||
status: instance.status,
|
||||
completed_at: instance.completed_at,
|
||||
});
|
||||
}
|
||||
|
||||
async getInstancesByProject(projectHash: string): Promise<ProcessInstanceDomainEntity[]> {
|
||||
return this.instanceRepo.findByProjectHash(projectHash);
|
||||
}
|
||||
|
||||
async getInstance(id: string): Promise<ProcessInstanceDomainEntity | null> {
|
||||
return this.instanceRepo.findById(id);
|
||||
}
|
||||
|
||||
// ──── СОЗДАНИЕ ЗАДАЧ ────
|
||||
|
||||
private async createIssueForStep(
|
||||
instance: ProcessInstanceDomainEntity,
|
||||
template: ProcessTemplateDomainEntity,
|
||||
stepId: string,
|
||||
createdBy: string,
|
||||
): Promise<void> {
|
||||
const step = template.steps.find(s => s.id === stepId);
|
||||
if (!step) return;
|
||||
|
||||
try {
|
||||
const issueHash = uuid().replace(/-/g, '').toUpperCase();
|
||||
const prefix = instance.project_hash.substring(0, 3).toUpperCase();
|
||||
const issueId = `${prefix}-P${Date.now().toString(36).toUpperCase()}`;
|
||||
|
||||
const issue = new IssueDomainEntity({
|
||||
_id: '' as any,
|
||||
_created_at: new Date(),
|
||||
_updated_at: new Date(),
|
||||
present: true,
|
||||
id: issueId,
|
||||
issue_hash: issueHash,
|
||||
coopname: instance.coopname,
|
||||
title: `[${template.title}] ${step.title}`,
|
||||
description: step.description || `Задача процесса "${template.title}", цикл ${instance.cycle}`,
|
||||
priority: IssuePriority.MEDIUM,
|
||||
status: IssueStatus.TODO,
|
||||
estimate: step.estimate || 0,
|
||||
sort_order: 0,
|
||||
created_by: createdBy,
|
||||
creators: [createdBy],
|
||||
project_hash: instance.project_hash,
|
||||
metadata: { labels: ['process', template.title], attachments: [] },
|
||||
} as any);
|
||||
|
||||
const savedIssue = await this.issueRepo.create(issue);
|
||||
|
||||
const stepState = instance.step_states.find(s => s.step_id === stepId);
|
||||
if (stepState) {
|
||||
stepState.issue_hash = savedIssue.issue_hash;
|
||||
}
|
||||
|
||||
await this.instanceRepo.update(instance.id, { step_states: instance.step_states });
|
||||
|
||||
this.logger.log(`Задача "${step.title}" создана для процесса "${template.title}"`);
|
||||
} catch (error: any) {
|
||||
this.logger.warn(`Не удалось создать задачу для шага ${stepId}: ${error?.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
+18
-8
@@ -1,4 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { PubSub } from 'graphql-subscriptions';
|
||||
import { PUB_SUB } from '~/infrastructure/pubsub/pubsub.module';
|
||||
import { CAPITAL_EVENTS } from '../resolvers/capital-subscription.resolver';
|
||||
import { ProjectManagementInteractor } from '../use-cases/project-management.interactor';
|
||||
import type { CreateProjectInputDTO } from '../dto/project_management';
|
||||
import type { TransactResult } from '@wharfkit/session';
|
||||
@@ -22,30 +25,37 @@ import { DocumentInteractor } from '~/application/document/interactors/document.
|
||||
import { ProjectMapperService } from './project-mapper.service';
|
||||
import type { MonoAccountDomainInterface } from '~/domain/account/interfaces/mono-account-domain.interface';
|
||||
|
||||
/**
|
||||
* Сервис уровня приложения для управления проектами CAPITAL
|
||||
* Обрабатывает запросы от ProjectManagementResolver
|
||||
*/
|
||||
@Injectable()
|
||||
export class ProjectManagementService {
|
||||
constructor(
|
||||
private readonly projectManagementInteractor: ProjectManagementInteractor,
|
||||
private readonly documentInteractor: DocumentInteractor,
|
||||
private readonly projectMapperService: ProjectMapperService
|
||||
private readonly projectMapperService: ProjectMapperService,
|
||||
@Inject(PUB_SUB) private readonly pubSub: PubSub,
|
||||
) {}
|
||||
|
||||
private notifyDataChanged(entity: string, action: string, id?: string) {
|
||||
this.pubSub.publish(CAPITAL_EVENTS.DATA_CHANGED, {
|
||||
[CAPITAL_EVENTS.DATA_CHANGED]: { entity, action, id },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Создание проекта в CAPITAL контракте
|
||||
*/
|
||||
async createProject(data: CreateProjectInputDTO, currentUser: MonoAccountDomainInterface): Promise<TransactResult> {
|
||||
return await this.projectManagementInteractor.createProject(data, currentUser);
|
||||
const result = await this.projectManagementInteractor.createProject(data, currentUser);
|
||||
this.notifyDataChanged('project', 'created');
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Редактирование проекта в CAPITAL контракте
|
||||
*/
|
||||
async editProject(data: EditProjectInputDTO): Promise<TransactResult> {
|
||||
return await this.projectManagementInteractor.editProject(data);
|
||||
const result = await this.projectManagementInteractor.editProject(data);
|
||||
this.notifyDataChanged('project', 'updated');
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+24
-9
@@ -1,4 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { PubSub } from 'graphql-subscriptions';
|
||||
import { PUB_SUB } from '~/infrastructure/pubsub/pubsub.module';
|
||||
import { CAPITAL_EVENTS } from '../resolvers/capital-subscription.resolver';
|
||||
import { VotingInteractor } from '../use-cases/voting.interactor';
|
||||
import type { StartVotingInputDTO } from '../dto/voting/start-voting-input.dto';
|
||||
import type { SubmitVoteInputDTO } from '../dto/voting/submit-vote-input.dto';
|
||||
@@ -12,33 +15,45 @@ import type { PaginationInputDomainInterface } from '~/domain/common/interfaces/
|
||||
import { SegmentMapper } from '../../infrastructure/mappers/segment.mapper';
|
||||
import { SegmentOutputDTO } from '../dto/segments/segment.dto';
|
||||
|
||||
/**
|
||||
* Сервис уровня приложения для голосования в CAPITAL
|
||||
* Обрабатывает запросы от VotingResolver
|
||||
*/
|
||||
@Injectable()
|
||||
export class VotingService {
|
||||
constructor(private readonly votingInteractor: VotingInteractor, private readonly segmentMapper: SegmentMapper) {}
|
||||
constructor(
|
||||
private readonly votingInteractor: VotingInteractor,
|
||||
private readonly segmentMapper: SegmentMapper,
|
||||
@Inject(PUB_SUB) private readonly pubSub: PubSub,
|
||||
) {}
|
||||
|
||||
private notifyDataChanged(entity: string, action: string) {
|
||||
this.pubSub.publish(CAPITAL_EVENTS.DATA_CHANGED, {
|
||||
[CAPITAL_EVENTS.DATA_CHANGED]: { entity, action },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Запуск голосования в CAPITAL контракте
|
||||
*/
|
||||
async startVoting(data: StartVotingInputDTO): Promise<TransactResult> {
|
||||
return await this.votingInteractor.startVoting(data);
|
||||
const result = await this.votingInteractor.startVoting(data);
|
||||
this.notifyDataChanged('voting', 'started');
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Голосование в CAPITAL контракте
|
||||
*/
|
||||
async submitVote(data: SubmitVoteInputDTO, username: string): Promise<TransactResult> {
|
||||
return await this.votingInteractor.submitVote({ ...data, voter: username });
|
||||
const result = await this.votingInteractor.submitVote({ ...data, voter: username });
|
||||
this.notifyDataChanged('voting', 'voted');
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Завершение голосования в CAPITAL контракте
|
||||
*/
|
||||
async completeVoting(data: CompleteVotingInputDTO): Promise<TransactResult> {
|
||||
return await this.votingInteractor.completeVoting(data);
|
||||
const result = await this.votingInteractor.completeVoting(data);
|
||||
this.notifyDataChanged('voting', 'completed');
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { forwardRef, Module } from '@nestjs/common';
|
||||
import { BaseExtModule } from '../base.extension.module';
|
||||
import { PubSubModule } from '~/infrastructure/pubsub/pubsub.module';
|
||||
import { CapitalDatabaseModule } from './infrastructure/database/capital-database.module';
|
||||
import { RegistrationInfrastructureModule } from '~/infrastructure/registration/registration-infrastructure.module';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
@@ -213,6 +214,14 @@ import { ProjectPropertyTypeormRepository } from './infrastructure/repositories/
|
||||
import { ProgramWalletTypeormRepository } from './infrastructure/repositories/program-wallet.typeorm-repository';
|
||||
import { CycleTypeormRepository } from './infrastructure/repositories/cycle.typeorm-repository';
|
||||
import { IssueTypeormRepository } from './infrastructure/repositories/issue.typeorm-repository';
|
||||
import { ProcessTemplateTypeormRepository } from './infrastructure/repositories/process-template.typeorm-repository';
|
||||
import { ProcessInstanceTypeormRepository } from './infrastructure/repositories/process-instance.typeorm-repository';
|
||||
import { PROCESS_TEMPLATE_REPOSITORY, PROCESS_INSTANCE_REPOSITORY } from './domain/repositories/process.repository';
|
||||
import { ProcessService } from './application/services/process.service';
|
||||
import { ProcessResolver } from './application/resolvers/process.resolver';
|
||||
import { CapitalSubscriptionResolver } from './application/resolvers/capital-subscription.resolver';
|
||||
import { ProcessTemplateTypeormEntity } from './infrastructure/entities/process-template.entity';
|
||||
import { ProcessInstanceTypeormEntity } from './infrastructure/entities/process-instance.entity';
|
||||
import { CommentTypeormRepository } from './infrastructure/repositories/comment.typeorm-repository';
|
||||
import { StoryTypeormRepository } from './infrastructure/repositories/story.typeorm-repository';
|
||||
import { VoteTypeormRepository } from './infrastructure/repositories/vote.typeorm-repository';
|
||||
@@ -513,6 +522,7 @@ export class CapitalPlugin extends BaseExtModule {
|
||||
forwardRef(() => RegistrationModule),
|
||||
RegistrationInfrastructureModule,
|
||||
EventEmitterModule.forRoot(),
|
||||
PubSubModule,
|
||||
],
|
||||
providers: [
|
||||
// Plugin
|
||||
@@ -656,6 +666,17 @@ IssueIdGenerationService,
|
||||
provide: ISSUE_REPOSITORY,
|
||||
useClass: IssueTypeormRepository,
|
||||
},
|
||||
{
|
||||
provide: PROCESS_TEMPLATE_REPOSITORY,
|
||||
useClass: ProcessTemplateTypeormRepository,
|
||||
},
|
||||
{
|
||||
provide: PROCESS_INSTANCE_REPOSITORY,
|
||||
useClass: ProcessInstanceTypeormRepository,
|
||||
},
|
||||
ProcessService,
|
||||
ProcessResolver,
|
||||
CapitalSubscriptionResolver,
|
||||
{
|
||||
provide: COMMENT_REPOSITORY,
|
||||
useClass: CommentTypeormRepository,
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import { ProcessInstanceStatus, ProcessStepStatus } from '../enums/process-status.enum';
|
||||
|
||||
/**
|
||||
* Экземпляр процесса — конкретный запуск шаблона.
|
||||
* Создаёт задачи (issues) по мере продвижения.
|
||||
*/
|
||||
export class ProcessInstanceDomainEntity {
|
||||
public id!: string;
|
||||
public coopname!: string;
|
||||
public template_id!: string;
|
||||
public project_hash!: string;
|
||||
public status!: ProcessInstanceStatus;
|
||||
public started_by!: string;
|
||||
public started_at!: Date;
|
||||
public completed_at?: Date;
|
||||
public cycle!: number;
|
||||
|
||||
/**
|
||||
* Состояние шагов текущего цикла.
|
||||
* Каждый шаг привязан к issue_hash (если задача создана).
|
||||
*/
|
||||
public step_states!: ProcessStepState[];
|
||||
}
|
||||
|
||||
export interface ProcessStepState {
|
||||
step_id: string;
|
||||
status: ProcessStepStatus;
|
||||
issue_hash?: string;
|
||||
completed_at?: Date;
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import { ProcessTemplateStatus } from '../enums/process-status.enum';
|
||||
|
||||
/**
|
||||
* Шаблон процесса — описывает структуру процесса (шаги и связи).
|
||||
* Создаётся chairman/member. Привязан к компоненту.
|
||||
*/
|
||||
export class ProcessTemplateDomainEntity {
|
||||
public id!: string;
|
||||
public coopname!: string;
|
||||
public project_hash!: string;
|
||||
public title!: string;
|
||||
public description?: string;
|
||||
public status!: ProcessTemplateStatus;
|
||||
public created_by!: string;
|
||||
public created_at!: Date;
|
||||
public updated_at!: Date;
|
||||
|
||||
/**
|
||||
* Шаги процесса — узлы Vue Flow.
|
||||
* Каждый шаг = шаблон задачи с позицией на холсте.
|
||||
*/
|
||||
public steps!: ProcessStepTemplate[];
|
||||
|
||||
/**
|
||||
* Связи между шагами — рёбра Vue Flow.
|
||||
* source → target: target создаётся после завершения source.
|
||||
*/
|
||||
public edges!: ProcessEdge[];
|
||||
}
|
||||
|
||||
export interface ProcessStepTemplate {
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
estimate?: number;
|
||||
position: { x: number; y: number };
|
||||
is_start?: boolean;
|
||||
}
|
||||
|
||||
export interface ProcessEdge {
|
||||
id: string;
|
||||
source: string;
|
||||
target: string;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
export enum ProcessTemplateStatus {
|
||||
DRAFT = 'draft',
|
||||
ACTIVE = 'active',
|
||||
ARCHIVED = 'archived',
|
||||
}
|
||||
|
||||
export enum ProcessInstanceStatus {
|
||||
RUNNING = 'running',
|
||||
COMPLETED = 'completed',
|
||||
CANCELLED = 'cancelled',
|
||||
}
|
||||
|
||||
export enum ProcessStepStatus {
|
||||
PENDING = 'pending',
|
||||
ACTIVE = 'active',
|
||||
COMPLETED = 'completed',
|
||||
CANCELLED = 'cancelled',
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import type { ProcessTemplateDomainEntity } from '../entities/process-template.entity';
|
||||
import type { ProcessInstanceDomainEntity } from '../entities/process-instance.entity';
|
||||
|
||||
export interface ProcessTemplateRepository {
|
||||
create(template: Partial<ProcessTemplateDomainEntity>): Promise<ProcessTemplateDomainEntity>;
|
||||
findById(id: string): Promise<ProcessTemplateDomainEntity | null>;
|
||||
findByProjectHash(projectHash: string): Promise<ProcessTemplateDomainEntity[]>;
|
||||
findByCoopname(coopname: string): Promise<ProcessTemplateDomainEntity[]>;
|
||||
update(id: string, data: Partial<ProcessTemplateDomainEntity>): Promise<ProcessTemplateDomainEntity>;
|
||||
delete(id: string): Promise<void>;
|
||||
}
|
||||
|
||||
export interface ProcessInstanceRepository {
|
||||
create(instance: Partial<ProcessInstanceDomainEntity>): Promise<ProcessInstanceDomainEntity>;
|
||||
findById(id: string): Promise<ProcessInstanceDomainEntity | null>;
|
||||
findByTemplateId(templateId: string): Promise<ProcessInstanceDomainEntity[]>;
|
||||
findByProjectHash(projectHash: string): Promise<ProcessInstanceDomainEntity[]>;
|
||||
update(id: string, data: Partial<ProcessInstanceDomainEntity>): Promise<ProcessInstanceDomainEntity>;
|
||||
}
|
||||
|
||||
export const PROCESS_TEMPLATE_REPOSITORY = Symbol('ProcessTemplateRepository');
|
||||
export const PROCESS_INSTANCE_REPOSITORY = Symbol('ProcessInstanceRepository');
|
||||
+6
@@ -21,6 +21,8 @@ import { CommitTypeormEntity } from '../entities/commit.typeorm-entity';
|
||||
import { StateTypeormEntity } from '../entities/state.typeorm-entity';
|
||||
import { TimeEntryEntity } from '../entities/time-entry.entity';
|
||||
import { SegmentTypeormEntity } from '../entities/segment.typeorm-entity';
|
||||
import { ProcessTemplateTypeormEntity } from '../entities/process-template.entity';
|
||||
import { ProcessInstanceTypeormEntity } from '../entities/process-instance.entity';
|
||||
import { GitHubFileIndexTypeormEntity } from '../entities/github-file-index.typeorm-entity';
|
||||
import { config } from '~/config';
|
||||
import { EntityVersionTypeormEntity } from '~/shared/sync/entities/entity-version.typeorm-entity';
|
||||
@@ -64,6 +66,8 @@ export const CAPITAL_DATABASE_CONNECTION = 'capital';
|
||||
SegmentTypeormEntity,
|
||||
GitHubFileIndexTypeormEntity,
|
||||
EntityVersionTypeormEntity,
|
||||
ProcessTemplateTypeormEntity,
|
||||
ProcessInstanceTypeormEntity,
|
||||
],
|
||||
// synchronize: config.env === 'development', // Используем миграции для production
|
||||
synchronize: true, // Временно всегда синхронизируем
|
||||
@@ -95,6 +99,8 @@ export const CAPITAL_DATABASE_CONNECTION = 'capital';
|
||||
SegmentTypeormEntity,
|
||||
GitHubFileIndexTypeormEntity,
|
||||
EntityVersionTypeormEntity,
|
||||
ProcessTemplateTypeormEntity,
|
||||
ProcessInstanceTypeormEntity,
|
||||
],
|
||||
CAPITAL_DATABASE_CONNECTION
|
||||
), // Указываем connection name
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from 'typeorm';
|
||||
import { ProcessInstanceStatus } from '../../domain/enums/process-status.enum';
|
||||
import type { ProcessStepState } from '../../domain/entities/process-instance.entity';
|
||||
|
||||
@Entity('capital_process_instances')
|
||||
export class ProcessInstanceTypeormEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 12 })
|
||||
coopname!: string;
|
||||
|
||||
@Column({ type: 'uuid' })
|
||||
template_id!: string;
|
||||
|
||||
@Column({ type: 'varchar' })
|
||||
project_hash!: string;
|
||||
|
||||
@Column({ type: 'enum', enum: ProcessInstanceStatus, default: ProcessInstanceStatus.RUNNING })
|
||||
status!: ProcessInstanceStatus;
|
||||
|
||||
@Column({ type: 'varchar', length: 50 })
|
||||
started_by!: string;
|
||||
|
||||
@Column({ type: 'int', default: 1 })
|
||||
cycle!: number;
|
||||
|
||||
@Column({ type: 'jsonb', default: '[]' })
|
||||
step_states!: ProcessStepState[];
|
||||
|
||||
@CreateDateColumn()
|
||||
started_at!: Date;
|
||||
|
||||
@Column({ type: 'timestamp', nullable: true })
|
||||
completed_at?: Date;
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
|
||||
import { ProcessTemplateStatus } from '../../domain/enums/process-status.enum';
|
||||
import type { ProcessStepTemplate, ProcessEdge } from '../../domain/entities/process-template.entity';
|
||||
|
||||
@Entity('capital_process_templates')
|
||||
export class ProcessTemplateTypeormEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 12 })
|
||||
coopname!: string;
|
||||
|
||||
@Column({ type: 'varchar' })
|
||||
project_hash!: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 255 })
|
||||
title!: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
description?: string;
|
||||
|
||||
@Column({ type: 'enum', enum: ProcessTemplateStatus, default: ProcessTemplateStatus.DRAFT })
|
||||
status!: ProcessTemplateStatus;
|
||||
|
||||
@Column({ type: 'varchar', length: 50 })
|
||||
created_by!: string;
|
||||
|
||||
@Column({ type: 'jsonb', default: '[]' })
|
||||
steps!: ProcessStepTemplate[];
|
||||
|
||||
@Column({ type: 'jsonb', default: '[]' })
|
||||
edges!: ProcessEdge[];
|
||||
|
||||
@CreateDateColumn()
|
||||
created_at!: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updated_at!: Date;
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { ProcessInstanceTypeormEntity } from '../entities/process-instance.entity';
|
||||
import type { ProcessInstanceRepository } from '../../domain/repositories/process.repository';
|
||||
import type { ProcessInstanceDomainEntity } from '../../domain/entities/process-instance.entity';
|
||||
import { CAPITAL_DATABASE_CONNECTION } from '../database/capital-database.module';
|
||||
|
||||
@Injectable()
|
||||
export class ProcessInstanceTypeormRepository implements ProcessInstanceRepository {
|
||||
constructor(
|
||||
@InjectRepository(ProcessInstanceTypeormEntity, CAPITAL_DATABASE_CONNECTION)
|
||||
private readonly repo: Repository<ProcessInstanceTypeormEntity>,
|
||||
) {}
|
||||
|
||||
async create(data: Partial<ProcessInstanceDomainEntity>): Promise<ProcessInstanceDomainEntity> {
|
||||
const entity = this.repo.create(data as any);
|
||||
const saved = await this.repo.save(entity);
|
||||
return saved as unknown as ProcessInstanceDomainEntity;
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<ProcessInstanceDomainEntity | null> {
|
||||
const entity = await this.repo.findOneBy({ id });
|
||||
return entity as unknown as ProcessInstanceDomainEntity | null;
|
||||
}
|
||||
|
||||
async findByTemplateId(templateId: string): Promise<ProcessInstanceDomainEntity[]> {
|
||||
const entities = await this.repo.find({ where: { template_id: templateId }, order: { started_at: 'DESC' } });
|
||||
return entities as unknown as ProcessInstanceDomainEntity[];
|
||||
}
|
||||
|
||||
async findByProjectHash(projectHash: string): Promise<ProcessInstanceDomainEntity[]> {
|
||||
const entities = await this.repo.find({ where: { project_hash: projectHash }, order: { started_at: 'DESC' } });
|
||||
return entities as unknown as ProcessInstanceDomainEntity[];
|
||||
}
|
||||
|
||||
async update(id: string, data: Partial<ProcessInstanceDomainEntity>): Promise<ProcessInstanceDomainEntity> {
|
||||
await this.repo.update(id, data as any);
|
||||
return (await this.findById(id))!;
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { ProcessTemplateTypeormEntity } from '../entities/process-template.entity';
|
||||
import type { ProcessTemplateRepository } from '../../domain/repositories/process.repository';
|
||||
import type { ProcessTemplateDomainEntity } from '../../domain/entities/process-template.entity';
|
||||
import { CAPITAL_DATABASE_CONNECTION } from '../database/capital-database.module';
|
||||
|
||||
@Injectable()
|
||||
export class ProcessTemplateTypeormRepository implements ProcessTemplateRepository {
|
||||
constructor(
|
||||
@InjectRepository(ProcessTemplateTypeormEntity, CAPITAL_DATABASE_CONNECTION)
|
||||
private readonly repo: Repository<ProcessTemplateTypeormEntity>,
|
||||
) {}
|
||||
|
||||
async create(data: Partial<ProcessTemplateDomainEntity>): Promise<ProcessTemplateDomainEntity> {
|
||||
const entity = this.repo.create(data as any);
|
||||
const saved = await this.repo.save(entity);
|
||||
return saved as unknown as ProcessTemplateDomainEntity;
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<ProcessTemplateDomainEntity | null> {
|
||||
const entity = await this.repo.findOneBy({ id });
|
||||
return entity as unknown as ProcessTemplateDomainEntity | null;
|
||||
}
|
||||
|
||||
async findByProjectHash(projectHash: string): Promise<ProcessTemplateDomainEntity[]> {
|
||||
const entities = await this.repo.find({ where: { project_hash: projectHash } });
|
||||
return entities as unknown as ProcessTemplateDomainEntity[];
|
||||
}
|
||||
|
||||
async findByCoopname(coopname: string): Promise<ProcessTemplateDomainEntity[]> {
|
||||
const entities = await this.repo.find({ where: { coopname }, order: { created_at: 'DESC' } });
|
||||
return entities as unknown as ProcessTemplateDomainEntity[];
|
||||
}
|
||||
|
||||
async update(id: string, data: Partial<ProcessTemplateDomainEntity>): Promise<ProcessTemplateDomainEntity> {
|
||||
await this.repo.update(id, data as any);
|
||||
return (await this.findById(id))!;
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
await this.repo.delete(id);
|
||||
}
|
||||
}
|
||||
+267
@@ -0,0 +1,267 @@
|
||||
import { Injectable, Inject, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
|
||||
import cron from 'node-cron';
|
||||
import { WinstonLoggerService } from '~/application/logger/logger-app.service';
|
||||
import { NovuWorkflowAdapter } from '~/infrastructure/novu/novu-workflow.adapter';
|
||||
import { NOVU_WORKFLOW_PORT } from '~/domain/notification/interfaces/novu-workflow.port';
|
||||
import { ACCOUNT_DATA_PORT, AccountDataPort } from '~/domain/account/ports/account-data.port';
|
||||
import { VARS_REPOSITORY, VarsRepository } from '~/domain/common/repositories/vars.repository';
|
||||
import {
|
||||
EXTENSION_REPOSITORY,
|
||||
type ExtensionDomainRepository,
|
||||
} from '~/domain/extension/repositories/extension-domain.repository';
|
||||
import { LOG_EXTENSION_REPOSITORY, LogExtensionDomainRepository } from '~/domain/extension/repositories/log-extension-domain.repository';
|
||||
import { SovietBlockchainPort, SOVIET_BLOCKCHAIN_PORT } from '~/domain/common/ports/soviet-blockchain.port';
|
||||
import { SovietContract } from 'cooptypes';
|
||||
import config from '~/config/config';
|
||||
import type { WorkflowTriggerDomainInterface } from '~/domain/notification/interfaces/workflow-trigger-domain.interface';
|
||||
import type { ExtensionDomainEntity } from '~/domain/extension/entities/extension-domain.entity';
|
||||
import { Workflows } from '@coopenomics/notifications';
|
||||
|
||||
/**
|
||||
* Сервис для отправки уведомлений об отмене решений по истечению срока
|
||||
*/
|
||||
@Injectable()
|
||||
export class DecisionExpiredNotificationService implements OnModuleInit, OnModuleDestroy {
|
||||
private cronJob: cron.ScheduledTask | null = null;
|
||||
constructor(
|
||||
@Inject(NOVU_WORKFLOW_PORT)
|
||||
private readonly novuWorkflowAdapter: NovuWorkflowAdapter,
|
||||
@Inject(ACCOUNT_DATA_PORT)
|
||||
private readonly accountPort: AccountDataPort,
|
||||
@Inject(VARS_REPOSITORY)
|
||||
private readonly varsRepository: VarsRepository,
|
||||
@Inject(EXTENSION_REPOSITORY)
|
||||
private readonly extensionRepository: ExtensionDomainRepository,
|
||||
@Inject(LOG_EXTENSION_REPOSITORY)
|
||||
private readonly logExtensionRepository: LogExtensionDomainRepository,
|
||||
@Inject(SOVIET_BLOCKCHAIN_PORT)
|
||||
private readonly sovietBlockchainPort: SovietBlockchainPort,
|
||||
private readonly logger: WinstonLoggerService
|
||||
) {
|
||||
this.logger.setContext(DecisionExpiredNotificationService.name);
|
||||
}
|
||||
|
||||
async onModuleInit() {
|
||||
this.logger.log('DecisionExpiredNotificationService инициализирован');
|
||||
}
|
||||
|
||||
onModuleDestroy() {
|
||||
if (this.cronJob) {
|
||||
this.cronJob.stop();
|
||||
this.cronJob = null;
|
||||
this.logger.info('node-cron задача проверки истекших решений остановлена');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Инициализирует cron-задачу для проверки истекших решений
|
||||
*/
|
||||
async initialize(plugin: ExtensionDomainEntity): Promise<void> {
|
||||
|
||||
// Регистрация cron-задачи для проверки истекших решений
|
||||
const cronExpression = `*/${plugin.config.checkInterval || 5} * * * *`; // каждые N минут, значение по умолчанию 5
|
||||
this.cronJob = cron.schedule(cronExpression, async () => {
|
||||
this.logger.info('Запуск задачи проверки истекших решений');
|
||||
try {
|
||||
await this.checkExpiredDecisions(plugin);
|
||||
} catch (error) {
|
||||
const errorObj = error as Error;
|
||||
this.logger.error(
|
||||
'Ошибка при выполнении задачи проверки истекших решений:',
|
||||
errorObj.message || 'Неизвестная ошибка'
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Записывает событие в лог расширения
|
||||
*/
|
||||
private async log(data: { type: string; coopname: string; decision_id?: string; result?: string }) {
|
||||
await this.logExtensionRepository.push('chairman', {
|
||||
type: data.type,
|
||||
coopname: data.coopname,
|
||||
decision_id: data.decision_id,
|
||||
result: data.result,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Отправляет уведомление пайщику об отмене решения по истечению срока
|
||||
*/
|
||||
async sendDecisionExpiredNotification(
|
||||
decisionId: string,
|
||||
decisionTitle: string,
|
||||
username: string,
|
||||
coopname: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
this.logger.debug(`Отправка уведомления об отмене решения ID: ${decisionId} пайщику ${username}`);
|
||||
|
||||
// Получаем аккаунт пайщика
|
||||
const userAccount = await this.accountPort.getAccount(username);
|
||||
const userEmail = userAccount.provider_account?.email;
|
||||
|
||||
if (!userEmail) {
|
||||
this.logger.warn(`Email пайщика ${username} не найден`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Получаем отображаемое имя пайщика
|
||||
const userName = await this.accountPort.getDisplayName(username);
|
||||
|
||||
// Получаем данные кооператива из Vars
|
||||
const vars = await this.varsRepository.get();
|
||||
if (!vars) {
|
||||
this.logger.warn(`Vars для кооператива ${coopname} не найдены`);
|
||||
return;
|
||||
}
|
||||
|
||||
const short_abbr = vars.short_abbr;
|
||||
const name = vars.name;
|
||||
|
||||
// Формируем данные для workflow
|
||||
const payload: Workflows.DecisionExpired.IPayload = {
|
||||
userName,
|
||||
decisionTitle,
|
||||
coopname,
|
||||
decision_id: decisionId,
|
||||
short_abbr,
|
||||
name,
|
||||
};
|
||||
|
||||
// Отправляем уведомление пайщику
|
||||
const triggerData: WorkflowTriggerDomainInterface = {
|
||||
name: Workflows.DecisionExpired.id,
|
||||
to: {
|
||||
subscriberId: username,
|
||||
email: userEmail,
|
||||
},
|
||||
payload,
|
||||
};
|
||||
|
||||
await this.novuWorkflowAdapter.triggerWorkflow(triggerData);
|
||||
this.logger.log(
|
||||
`Уведомление об отмене решения ${decisionId} отправлено пайщику ${username}`
|
||||
);
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Ошибка при отправке уведомления об отмене решения: ${error.message}`, error.stack);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет и отменяет истекшие решения
|
||||
*/
|
||||
async checkExpiredDecisions(plugin: ExtensionDomainEntity): Promise<void> {
|
||||
try {
|
||||
// Получаем coopname из конфигурации
|
||||
const coopname = config.coopname;
|
||||
|
||||
this.logger.info(`Проверка решений для кооператива ${coopname}`);
|
||||
|
||||
// Запись о проверке в лог
|
||||
await this.log({
|
||||
type: 'check',
|
||||
coopname,
|
||||
});
|
||||
|
||||
// Получаем все решения для кооператива
|
||||
const decisions = await this.sovietBlockchainPort.getDecisions(coopname);
|
||||
|
||||
// Текущая дата для сравнения
|
||||
const now = new Date();
|
||||
// Находим истекшие решения
|
||||
const expiredDecisions = decisions.filter((decision) => {
|
||||
// TODO: убрать после первого деплоя т.к. все старые решения отменятся.
|
||||
// Если поле expired_at не существует, добавляем
|
||||
if (!decision.expired_at) return true;
|
||||
|
||||
// Если решение уже принято и не требуется отменять принятые решения, пропускаем
|
||||
if (decision.approved && !plugin.config.cancelApprovedDecisions) return false;
|
||||
|
||||
// Конвертируем дату из формата блокчейна в JavaScript Date
|
||||
const expiredDate = new Date(decision.expired_at);
|
||||
|
||||
// Проверяем, истек ли срок
|
||||
return expiredDate <= now;
|
||||
});
|
||||
|
||||
this.logger.info(`Найдено ${expiredDecisions.length} истекших решений для кооператива ${coopname}`);
|
||||
|
||||
// Отменяем каждое истекшее решение
|
||||
for (const decision of expiredDecisions) {
|
||||
try {
|
||||
|
||||
this.logger.info(`Отмена истекшего решения ID: ${decision.id} для кооператива ${coopname}`);
|
||||
|
||||
// Создаем объект данных для транзакции отмены
|
||||
const cancelData: SovietContract.Actions.Decisions.Cancelexprd.ICancelExpired = {
|
||||
coopname,
|
||||
decision_id: decision.id,
|
||||
};
|
||||
|
||||
// Вызываем метод отмены из порта блокчейна
|
||||
const result = await this.sovietBlockchainPort.cancelExpiredDecision(cancelData);
|
||||
|
||||
// Запись об успешной отмене в лог
|
||||
await this.log({
|
||||
type: 'cancel',
|
||||
coopname,
|
||||
decision_id: decision.id.toString(),
|
||||
result: 'success',
|
||||
});
|
||||
|
||||
// Безопасно обращаемся к результату транзакции
|
||||
const txId = result.resolved?.transaction?.id ? result.resolved.transaction.id : 'неизвестно';
|
||||
this.logger.info(`Решение ID: ${decision.id} успешно отменено`, { transactionId: txId });
|
||||
|
||||
// Отправляем уведомление пайщику об отмене решения
|
||||
try {
|
||||
const decisionTitle = (() => {
|
||||
try {
|
||||
const parsedMeta = JSON.parse(decision.meta);
|
||||
return parsedMeta.title || '';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
})();
|
||||
await this.sendDecisionExpiredNotification(
|
||||
decision.id.toString(),
|
||||
decisionTitle,
|
||||
decision.username,
|
||||
decision.coopname
|
||||
);
|
||||
} catch (notificationError: any) {
|
||||
this.logger.error(`Ошибка при отправке уведомления об отмене решения ID: ${decision.id}`, notificationError.message);
|
||||
}
|
||||
} catch (error) {
|
||||
// Безопасно обрабатываем ошибку
|
||||
const errorObj = error as Error;
|
||||
this.logger.error(`Ошибка при отмене решения ID: ${decision.id}`, errorObj.message || 'Неизвестная ошибка');
|
||||
|
||||
// Запись об ошибке в лог
|
||||
await this.log({
|
||||
type: 'cancel',
|
||||
coopname,
|
||||
decision_id: decision.id.toString(),
|
||||
result: `error: ${errorObj.message || 'Неизвестная ошибка'}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Обновляем дату последней проверки (только это поле, чтобы не перезаписать другие параметры конфига)
|
||||
const updatedConfig = {
|
||||
...plugin.config,
|
||||
lastCheckDate: new Date().toISOString(),
|
||||
};
|
||||
await this.extensionRepository.update({ ...plugin, config: updatedConfig });
|
||||
|
||||
this.logger.info('Проверка истекших решений завершена');
|
||||
} catch (error) {
|
||||
// Безопасно обрабатываем ошибку
|
||||
const errorObj = error as Error;
|
||||
this.logger.error('Ошибка при проверке истекших решений:', errorObj.message || 'Неизвестная ошибка');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
import cron from 'node-cron';
|
||||
import { Inject, Module, OnModuleDestroy } from '@nestjs/common';
|
||||
import { Inject, Module } from '@nestjs/common';
|
||||
import { BaseExtModule } from '../base.extension.module';
|
||||
import config from '~/config/config';
|
||||
import {
|
||||
EXTENSION_REPOSITORY,
|
||||
type ExtensionDomainRepository,
|
||||
@@ -15,7 +13,6 @@ import {
|
||||
import { z } from 'zod';
|
||||
import type { DeserializedDescriptionOfExtension } from '~/types/shared';
|
||||
import { SOVIET_BLOCKCHAIN_PORT, SovietBlockchainPort } from '~/domain/common/ports/soviet-blockchain.port';
|
||||
import { SovietContract } from 'cooptypes';
|
||||
import { merge } from 'lodash';
|
||||
import { AccountInfrastructureModule } from '~/infrastructure/account/account-infrastructure.module';
|
||||
import { MeetInfrastructureModule } from '~/infrastructure/meet/meet-infrastructure.module';
|
||||
@@ -23,6 +20,7 @@ import { SystemInfrastructureModule } from '~/infrastructure/system/system-infra
|
||||
import { DocumentDomainModule } from '~/domain/document/document.module';
|
||||
import { FreeDecisionDomainModule } from '~/domain/free-decision/free-decision.module';
|
||||
import { VaultDomainModule } from '~/domain/vault/vault-domain.module';
|
||||
import { SystemDomainModule } from '~/domain/system/system-domain.module';
|
||||
|
||||
// Chairman Database and Infrastructure
|
||||
import { ChairmanDatabaseModule } from './infrastructure/database/chairman-database.module';
|
||||
@@ -38,6 +36,7 @@ import { ApprovalSyncService } from './infrastructure/blockchain/services/approv
|
||||
import { ApprovalService } from './application/services/approval.service';
|
||||
import { ApprovalNotificationService } from './application/services/approval-notification.service';
|
||||
import { ApprovalResponseNotificationService } from './application/services/approval-response-notification.service';
|
||||
import { DecisionExpiredNotificationService } from './application/services/decision-expired-notification.service';
|
||||
import { ChairmanOnboardingService } from './application/services/onboarding.service';
|
||||
import { ChairmanOnboardingEventsService } from './application/services/onboarding-events.service';
|
||||
import { ChairmanBlockchainAdapter } from './infrastructure/blockchain/adapters/chairman-blockchain.adapter';
|
||||
@@ -92,8 +91,8 @@ export const Schema = z.object({
|
||||
.describe(
|
||||
describeField({
|
||||
label: 'Интервал проверки истекших решений (в минутах)',
|
||||
note: 'Минимум: 5 минут',
|
||||
rules: ['val >= 5'],
|
||||
note: 'Минимум: 1 минута',
|
||||
rules: ['val >= 1'],
|
||||
prepend: 'Каждые',
|
||||
append: 'минут',
|
||||
})
|
||||
@@ -189,13 +188,13 @@ export interface ILog {
|
||||
timestamp?: string; // Делаем опциональным, так как будет добавляться внутри метода log
|
||||
}
|
||||
|
||||
export class ChairmanPlugin extends BaseExtModule implements OnModuleDestroy {
|
||||
private cronJob: cron.ScheduledTask | null = null;
|
||||
export class ChairmanPlugin extends BaseExtModule {
|
||||
|
||||
constructor(
|
||||
@Inject(EXTENSION_REPOSITORY) private readonly extensionRepository: ExtensionDomainRepository<IConfig>,
|
||||
@Inject(LOG_EXTENSION_REPOSITORY) private readonly logExtensionRepository: LogExtensionDomainRepository<ILog>,
|
||||
@Inject(SOVIET_BLOCKCHAIN_PORT) private readonly sovietBlockchainPort: SovietBlockchainPort,
|
||||
private readonly decisionExpiredNotificationService: DecisionExpiredNotificationService,
|
||||
private readonly logger: WinstonLoggerService
|
||||
) {
|
||||
super();
|
||||
@@ -238,150 +237,8 @@ export class ChairmanPlugin extends BaseExtModule implements OnModuleDestroy {
|
||||
|
||||
this.logger.info(`Инициализация ${this.name} с конфигурацией`, this.plugin.config);
|
||||
|
||||
// Проверяем, была ли проверка решений в последнее время
|
||||
const lastDate = this.plugin.config.lastCheckDate ? new Date(this.plugin.config.lastCheckDate) : null;
|
||||
|
||||
const now = new Date();
|
||||
|
||||
if (lastDate) {
|
||||
const diffInMinutes = Math.abs(now.getTime() - lastDate.getTime()) / 60000; // Разница во времени в минутах
|
||||
if (diffInMinutes < this.plugin.config.checkInterval) {
|
||||
this.logger.info(
|
||||
`Проверка решений уже выполнялась в последние ${this.plugin.config.checkInterval} минут. Повторная проверка не требуется.`
|
||||
);
|
||||
} else {
|
||||
this.logger.info(
|
||||
`Проверка решений не выполнялась в последние ${this.plugin.config.checkInterval} минут. Проверка пропущена.`
|
||||
);
|
||||
// await this.checkExpiredDecisions();
|
||||
}
|
||||
} else {
|
||||
this.logger.info('Дата последней проверки отсутствует. Проверка пропущена.');
|
||||
// await this.checkExpiredDecisions();
|
||||
}
|
||||
|
||||
// Регистрация cron-задачи для проверки истекших решений
|
||||
// const cronExpression = `*/${this.plugin.config.checkInterval || 5} * * * *`; // каждые N минут, значение по умолчанию 5
|
||||
// this.cronJob = cron.schedule(cronExpression, async () => {
|
||||
// this.logger.info('Запуск задачи проверки истекших решений');
|
||||
// try {
|
||||
// await this.checkExpiredDecisions();
|
||||
// } catch (error) {
|
||||
// const errorObj = error as Error;
|
||||
// this.logger.error(
|
||||
// 'Ошибка при выполнении задачи проверки истекших решений:',
|
||||
// errorObj.message || 'Неизвестная ошибка'
|
||||
// );
|
||||
// }
|
||||
// });
|
||||
}
|
||||
|
||||
onModuleDestroy() {
|
||||
if (this.cronJob) {
|
||||
this.cronJob.stop();
|
||||
this.cronJob = null;
|
||||
this.logger.info('node-cron задача проверки истекших решений остановлена');
|
||||
}
|
||||
}
|
||||
|
||||
// Логирование действий
|
||||
private async log(action: ILog) {
|
||||
await this.logExtensionRepository.push(this.name, {
|
||||
...action,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
// Основная функция проверки и отмены истекших решений
|
||||
private async checkExpiredDecisions() {
|
||||
try {
|
||||
// Получаем coopname из конфигурации
|
||||
const coopname = config.coopname;
|
||||
|
||||
this.logger.info(`Проверка решений для кооператива ${coopname}`);
|
||||
|
||||
// Запись о проверке в лог
|
||||
await this.log({
|
||||
type: 'check',
|
||||
coopname,
|
||||
});
|
||||
|
||||
// Получаем все решения для кооператива
|
||||
const decisions = await this.sovietBlockchainPort.getDecisions(coopname);
|
||||
|
||||
// Текущая дата для сравнения
|
||||
const now = new Date();
|
||||
// Находим истекшие решения
|
||||
const expiredDecisions = decisions.filter((decision) => {
|
||||
// TODO: убрать после первого деплоя т.к. все старые решения отменятся.
|
||||
// Если поле expired_at не существует, добавляем
|
||||
if (!decision.expired_at) return true;
|
||||
|
||||
// Если решение уже принято и не требуется отменять принятые решения, пропускаем
|
||||
if (decision.approved && !this.plugin.config.cancelApprovedDecisions) return false;
|
||||
|
||||
// Конвертируем дату из формата блокчейна в JavaScript Date
|
||||
const expiredDate = new Date(decision.expired_at);
|
||||
|
||||
// Проверяем, истек ли срок
|
||||
return expiredDate <= now;
|
||||
});
|
||||
|
||||
this.logger.info(`Найдено ${expiredDecisions.length} истекших решений для кооператива ${coopname}`);
|
||||
|
||||
// Отменяем каждое истекшее решение
|
||||
for (const decision of expiredDecisions) {
|
||||
try {
|
||||
this.logger.info(`Отмена истекшего решения ID: ${decision.id} для кооператива ${coopname}`);
|
||||
|
||||
// Создаем объект данных для транзакции отмены
|
||||
const cancelData: SovietContract.Actions.Decisions.Cancelexprd.ICancelExpired = {
|
||||
coopname,
|
||||
decision_id: decision.id,
|
||||
};
|
||||
|
||||
// Вызываем метод отмены из порта блокчейна
|
||||
const result = await this.sovietBlockchainPort.cancelExpiredDecision(cancelData);
|
||||
|
||||
// Запись об успешной отмене в лог
|
||||
await this.log({
|
||||
type: 'cancel',
|
||||
coopname,
|
||||
decision_id: decision.id.toString(),
|
||||
result: 'success',
|
||||
});
|
||||
|
||||
// Безопасно обращаемся к результату транзакции
|
||||
const txId = result.resolved?.transaction?.id ? result.resolved.transaction.id : 'неизвестно';
|
||||
this.logger.info(`Решение ID: ${decision.id} успешно отменено`, { transactionId: txId });
|
||||
} catch (error) {
|
||||
// Безопасно обрабатываем ошибку
|
||||
const errorObj = error as Error;
|
||||
this.logger.error(`Ошибка при отмене решения ID: ${decision.id}`, errorObj.message || 'Неизвестная ошибка');
|
||||
|
||||
// Запись об ошибке в лог
|
||||
await this.log({
|
||||
type: 'cancel',
|
||||
coopname,
|
||||
decision_id: decision.id.toString(),
|
||||
result: `error: ${errorObj.message || 'Неизвестная ошибка'}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Обновляем дату последней проверки (только это поле, чтобы не перезаписать другие параметры конфига)
|
||||
const updatedConfig = {
|
||||
...this.plugin.config,
|
||||
lastCheckDate: new Date().toISOString(),
|
||||
};
|
||||
await this.extensionRepository.update({ ...this.plugin, config: updatedConfig });
|
||||
|
||||
this.logger.info('Проверка истекших решений завершена');
|
||||
} catch (error) {
|
||||
// Безопасно обрабатываем ошибку
|
||||
const errorObj = error as Error;
|
||||
this.logger.error('Ошибка при проверке истекших решений:', errorObj.message || 'Неизвестная ошибка');
|
||||
}
|
||||
// Инициализация сервиса проверки истекших решений
|
||||
await this.decisionExpiredNotificationService.initialize(this.plugin);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -396,6 +253,7 @@ export class ChairmanPlugin extends BaseExtModule implements OnModuleDestroy {
|
||||
VaultDomainModule,
|
||||
FreeDecisionInfrastructureModule,
|
||||
DecisionTrackingInfrastructureModule,
|
||||
SystemDomainModule,
|
||||
],
|
||||
providers: [
|
||||
ChairmanPlugin,
|
||||
@@ -415,6 +273,7 @@ export class ChairmanPlugin extends BaseExtModule implements OnModuleDestroy {
|
||||
ApprovalService,
|
||||
ApprovalNotificationService,
|
||||
ApprovalResponseNotificationService,
|
||||
DecisionExpiredNotificationService,
|
||||
ChairmanOnboardingService,
|
||||
ChairmanOnboardingEventsService,
|
||||
{
|
||||
|
||||
+74
-22
@@ -260,31 +260,47 @@ export class ChatCoopApplicationService {
|
||||
return;
|
||||
}
|
||||
|
||||
// Определяем уровень прав для пользователя
|
||||
let powerLevel = 0;
|
||||
// Определяем требуемый уровень прав для пользователя
|
||||
let requiredPowerLevel = 0;
|
||||
|
||||
if (roomType === 'members') {
|
||||
// В комнате пайщиков:
|
||||
// - chairman и member (члены совета) получают уровень 50 — могут создавать звонки (m.call.invite = 50)
|
||||
// - остальные пайщики остаются на уровне 0 — могут участвовать в звонках (m.call.answer = 0)
|
||||
if (userRole === 'chairman' || userRole === 'member') {
|
||||
powerLevel = 50;
|
||||
requiredPowerLevel = 50;
|
||||
}
|
||||
} else if (roomType === 'council') {
|
||||
// В комнате совета:
|
||||
// - chairman получает уровень 50 — модератор
|
||||
// - member остается на уровне 0 — обычные права, но может создавать звонки (m.call.invite = 0)
|
||||
if (userRole === 'chairman') {
|
||||
powerLevel = 50;
|
||||
requiredPowerLevel = 50;
|
||||
}
|
||||
}
|
||||
|
||||
// Проверяем текущие права пользователя
|
||||
const currentUserPowerLevel = currentPowerLevels.users?.[matrixUserId] ?? 0;
|
||||
|
||||
// Если права уже установлены правильно, пропускаем обновление
|
||||
if (currentUserPowerLevel === requiredPowerLevel) {
|
||||
this.logger.log(`Права пользователя ${matrixUserId} в комнате ${roomType} уже установлены корректно (уровень ${requiredPowerLevel})`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем, можем ли мы изменить права пользователя
|
||||
// Если пользователь имеет уровень прав >= 100 (админ), мы не можем их изменить
|
||||
if (currentUserPowerLevel >= 100) {
|
||||
this.logger.warn(`Пользователь ${matrixUserId} имеет уровень прав ${currentUserPowerLevel}, который нельзя изменить. Пропускаем обновление.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Обновляем права пользователя
|
||||
const updatedPowerLevels = { ...currentPowerLevels };
|
||||
if (!updatedPowerLevels.users) {
|
||||
updatedPowerLevels.users = {};
|
||||
}
|
||||
updatedPowerLevels.users[matrixUserId] = powerLevel;
|
||||
updatedPowerLevels.users[matrixUserId] = requiredPowerLevel;
|
||||
|
||||
// Если есть пользователь с правами 50+, понижаем invite до 50 (чтобы могли приглашать только модераторы)
|
||||
const hasHighPrivilegedUser = Object.values(updatedPowerLevels.users).some((level: any) => level >= 50);
|
||||
@@ -293,7 +309,7 @@ export class ChatCoopApplicationService {
|
||||
}
|
||||
|
||||
await this.matrixApiService.updateRoomPowerLevels(roomId, updatedPowerLevels);
|
||||
this.logger.log(`Права пользователя ${matrixUserId} в комнате ${roomType} обновлены до уровня ${powerLevel}`);
|
||||
this.logger.log(`Права пользователя ${matrixUserId} в комнате ${roomType} обновлены с ${currentUserPowerLevel} до ${requiredPowerLevel}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Не удалось обновить права пользователя ${matrixUserId} в комнате ${roomId}: ${error}`);
|
||||
}
|
||||
@@ -373,29 +389,55 @@ export class ChatCoopApplicationService {
|
||||
|
||||
// Все пользователи присоединяются к пространству
|
||||
if (spaceId) {
|
||||
await this.matrixApiService.joinRoom(matrixUserId, spaceId);
|
||||
this.logger.log(`Пользователь ${matrixUserId} присоединился к пространству ${spaceId}`);
|
||||
try {
|
||||
// Проверяем, является ли пользователь уже членом пространства
|
||||
const isMember = await this.matrixApiService.isUserInRoom(matrixUserId, spaceId);
|
||||
if (!isMember) {
|
||||
await this.matrixApiService.joinRoom(matrixUserId, spaceId);
|
||||
this.logger.log(`Пользователь ${matrixUserId} присоединился к пространству ${spaceId}`);
|
||||
} else {
|
||||
this.logger.log(`Пользователь ${matrixUserId} уже является членом пространства ${spaceId}`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.warn(`Не удалось проверить/добавить пользователя ${matrixUserId} в пространство ${spaceId}: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Все пайщики присоединяются к комнате пайщиков
|
||||
if (membersRoomId) {
|
||||
await this.matrixApiService.joinRoom(matrixUserId, membersRoomId);
|
||||
this.logger.log(`Пользователь ${matrixUserId} присоединился к комнате пайщиков ${membersRoomId}`);
|
||||
try {
|
||||
// Проверяем, является ли пользователь уже членом комнаты пайщиков
|
||||
const isMember = await this.matrixApiService.isUserInRoom(matrixUserId, membersRoomId);
|
||||
if (!isMember) {
|
||||
await this.matrixApiService.joinRoom(matrixUserId, membersRoomId);
|
||||
this.logger.log(`Пользователь ${matrixUserId} присоединился к комнате пайщиков ${membersRoomId}`);
|
||||
} else {
|
||||
this.logger.log(`Пользователь ${matrixUserId} уже является членом комнаты пайщиков ${membersRoomId}`);
|
||||
}
|
||||
|
||||
// Обновляем права пользователя в комнате пайщиков
|
||||
await this.updateUserPowerLevel(matrixUserId, membersRoomId, userRole, 'members');
|
||||
// Обновляем права пользователя в комнате пайщиков
|
||||
await this.updateUserPowerLevel(matrixUserId, membersRoomId, userRole, 'members');
|
||||
} catch (error) {
|
||||
this.logger.warn(`Не удалось добавить пользователя ${matrixUserId} в комнату пайщиков ${membersRoomId}: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Все пайщики присоединяются к общей комнате (если указана)
|
||||
if (extendedConfig.matrix.common_room_id) {
|
||||
try {
|
||||
await this.matrixApiService.joinRoom(matrixUserId, extendedConfig.matrix.common_room_id);
|
||||
this.logger.log(
|
||||
`Пользователь ${matrixUserId} присоединился к общей комнате ${extendedConfig.matrix.common_room_id}`
|
||||
);
|
||||
// Проверяем, является ли пользователь уже членом общей комнаты
|
||||
const isMember = await this.matrixApiService.isUserInRoom(matrixUserId, extendedConfig.matrix.common_room_id);
|
||||
if (!isMember) {
|
||||
await this.matrixApiService.joinRoom(matrixUserId, extendedConfig.matrix.common_room_id);
|
||||
this.logger.log(
|
||||
`Пользователь ${matrixUserId} присоединился к общей комнате ${extendedConfig.matrix.common_room_id}`
|
||||
);
|
||||
} else {
|
||||
this.logger.log(`Пользователь ${matrixUserId} уже является членом общей комнаты ${extendedConfig.matrix.common_room_id}`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
`Не удалось добавить пользователя ${matrixUserId} в общую комнату ${extendedConfig.matrix.common_room_id}: ${error}`
|
||||
`Не удалось проверить/добавить пользователя ${matrixUserId} в общую комнату ${extendedConfig.matrix.common_room_id}: ${error}`
|
||||
);
|
||||
// Не прерываем выполнение, продолжаем с другими комнатами
|
||||
}
|
||||
@@ -406,12 +448,22 @@ export class ChatCoopApplicationService {
|
||||
`Проверка добавления в комнату совета: isCouncilMember=${isCouncilMember}, councilRoomId=${councilRoomId}`
|
||||
);
|
||||
if (isCouncilMember && councilRoomId) {
|
||||
this.logger.log(`Добавляем члена совета ${matrixUserId} в комнату совета ${councilRoomId}`);
|
||||
await this.matrixApiService.joinRoom(matrixUserId, councilRoomId);
|
||||
this.logger.log(`Член совета ${matrixUserId} присоединился к комнате совета ${councilRoomId}`);
|
||||
try {
|
||||
// Проверяем, является ли пользователь уже членом комнаты совета
|
||||
const isMember = await this.matrixApiService.isUserInRoom(matrixUserId, councilRoomId);
|
||||
if (!isMember) {
|
||||
this.logger.log(`Добавляем члена совета ${matrixUserId} в комнату совета ${councilRoomId}`);
|
||||
await this.matrixApiService.joinRoom(matrixUserId, councilRoomId);
|
||||
this.logger.log(`Член совета ${matrixUserId} присоединился к комнате совета ${councilRoomId}`);
|
||||
} else {
|
||||
this.logger.log(`Член совета ${matrixUserId} уже является членом комнаты совета ${councilRoomId}`);
|
||||
}
|
||||
|
||||
// Обновляем права пользователя в комнате совета
|
||||
await this.updateUserPowerLevel(matrixUserId, councilRoomId, userRole, 'council');
|
||||
// Обновляем права пользователя в комнате совета
|
||||
await this.updateUserPowerLevel(matrixUserId, councilRoomId, userRole, 'council');
|
||||
} catch (error) {
|
||||
this.logger.warn(`Не удалось добавить пользователя ${matrixUserId} в комнату совета ${councilRoomId}: ${error}`);
|
||||
}
|
||||
} else {
|
||||
this.logger.log(`Пользователь ${matrixUserId} НЕ добавлен в комнату совета`);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user