Compare commits
62 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 |
@@ -76,6 +76,19 @@ pnpm run reboot
|
||||
```
|
||||
- **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`.
|
||||
|
||||
+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,46 +1,181 @@
|
||||
# TASKS.md — Прогресс выполнения задач
|
||||
|
||||
## Завершённые задачи
|
||||
|
||||
### 1-6. Предыдущие задачи (см. git history)
|
||||
- ✅ Dev-окружение, Security, Тесты, README, AGENTS.md, Setup
|
||||
- ✅ Поисковая система документов (OpenSearch)
|
||||
- ✅ Процессы (Capital extension)
|
||||
- ✅ 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
|
||||
|
||||
---
|
||||
|
||||
## Текущая задача: Генерация отчётов ФНС (расширение reports)
|
||||
## Активные задачи
|
||||
|
||||
### Документы ФНС для генерации:
|
||||
1. **6-НДФЛ** — ежеквартально (XSD: NO_NDFL6.2)
|
||||
2. **4-ФСС (ЕФС-1)** — ежеквартально
|
||||
3. **РСВ** — ежеквартально (XSD: NO_RASCHSV)
|
||||
4. **ПСВ** — ежемесячно (XSD: NO_PERSSVFL)
|
||||
5. **Бухгалтерский баланс** — ежегодно (XSD: NO_BUHOTCH) — КЛЮЧЕВОЙ
|
||||
6. **ДУСН** — декларация УСН ежегодно (XSD: NO_USN)
|
||||
7. **Уведомление о страховых взносах** — ежемесячно с 2026 (XSD: UT_UVISCHSUMNAL)
|
||||
8. **УУСН** — уведомление УСН
|
||||
### 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 для передачи данных организации
|
||||
|
||||
### Архитектура:
|
||||
- Расширение `reports` в `components/controller/src/extensions/`
|
||||
- Фабрика XML отчётов: на вход данные за период → на выходе XML
|
||||
- Валидация по XSD схемам
|
||||
- Desktop UI: магазин приложений → установка → рабочий стол отчётов
|
||||
### 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)
|
||||
|
||||
- [x] **8.1 Исследование**: Все XSD разобраны, format.nalog.ru изучен
|
||||
- [x] **8.2 Инфраструктура**: Расширение reports, ReportRegistryService (фабрика)
|
||||
- [x] **8.3 XML генератор**: Фабричный подход — IReportGenerator interface
|
||||
- [x] **8.4 Бухбаланс**: BuhotchGenerator — счета 51, 80, 86 из ledger
|
||||
- [x] **8.5 6-НДФЛ**: Ndfl6Generator — нулевая
|
||||
- [x] **8.6 4-ФСС**: Zero generator — нулевая
|
||||
- [x] **8.7 РСВ**: Zero generator — нулевая
|
||||
- [x] **8.8 ПСВ**: Zero generator — нулевая
|
||||
- [x] **8.9 ДУСН**: Zero generator — нулевая
|
||||
- [x] **8.10 Уведомление о взносах**: Zero generator — нулевая
|
||||
- [x] **8.11 УУСН**: Zero generator — нулевая
|
||||
- [x] **8.12 GraphQL API**: getAvailableReports + generateReport
|
||||
- [ ] **8.13 XSD валидация + тесты**: Проверка по схемам
|
||||
- [ ] **8.14 Desktop UI**: Страница отчётов в магазине приложений
|
||||
- [ ] **8.15 Ledger интеграция**: Реальные данные из ledger_operations
|
||||
#### 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,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))
|
||||
})
|
||||
})
|
||||
@@ -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("")
|
||||
);
|
||||
};
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -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",
|
||||
@@ -126,6 +127,7 @@
|
||||
"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",
|
||||
|
||||
@@ -16,6 +16,13 @@ input AcceptChildOrderInput {
|
||||
username: String!
|
||||
}
|
||||
|
||||
input AcceptStockInput {
|
||||
convert_in: SignedDigitalDocumentInput!
|
||||
request_hash: String!
|
||||
return_statement: SignedDigitalDocumentInput!
|
||||
username: String!
|
||||
}
|
||||
|
||||
type Account {
|
||||
"""
|
||||
объект аккаунта в блокчейне содержит системную информацию, такую как публичные ключи доступа, доступные вычислительные ресурсы, информация об установленном смарт-контракте, и т.д. и т.п. Это системный уровень обслуживания, где у каждого пайщика есть аккаунт, но не каждый аккаунт может быть пайщиком в каком-либо кооперативе. Все смарт-контракты устанавливаются и исполняются на этом уровне.
|
||||
@@ -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 {
|
||||
"""Дата создания записи"""
|
||||
@@ -1989,6 +2020,12 @@ input CapitalCycleFilter {
|
||||
status: CycleStatus
|
||||
}
|
||||
|
||||
type CapitalDataChange {
|
||||
action: String!
|
||||
entity: String!
|
||||
id: String
|
||||
}
|
||||
|
||||
"""Долг в системе CAPITAL"""
|
||||
type CapitalDebt {
|
||||
"""Дата создания записи"""
|
||||
@@ -4079,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
|
||||
@@ -4110,6 +4158,12 @@ input CreateAnnualGeneralMeetInput {
|
||||
secretary: String!
|
||||
}
|
||||
|
||||
input CreateApiKeyInput {
|
||||
allowedOperations: [String!] = ["*"]
|
||||
expiresInDays: Int
|
||||
name: String!
|
||||
}
|
||||
|
||||
input CreateBranchInput {
|
||||
"""
|
||||
Документ, на основании которого действует Уполномоченный (решение совета №СС-.. от ..)
|
||||
@@ -4581,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!
|
||||
@@ -5014,6 +5087,11 @@ type DesktopWorkspace {
|
||||
title: String!
|
||||
}
|
||||
|
||||
input DestroyRequestInput {
|
||||
destruction_act: SignedDigitalDocumentInput!
|
||||
request_hash: String!
|
||||
}
|
||||
|
||||
input DisputeOnRequestInput {
|
||||
"""Имя аккаунта кооператива"""
|
||||
coopname: String!
|
||||
@@ -6350,6 +6428,12 @@ type KeyWeight {
|
||||
weight: Int!
|
||||
}
|
||||
|
||||
enum LeadRequestPolicy {
|
||||
BOTH
|
||||
OFFERS_ONLY
|
||||
ORDERS_ONLY
|
||||
}
|
||||
|
||||
type LedgerHistoryResponse {
|
||||
"""Текущая страница"""
|
||||
currentPage: Int!
|
||||
@@ -6498,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
|
||||
@@ -6787,6 +6887,9 @@ type Mutation {
|
||||
"""Подтвердить поставку имущества на заявку"""
|
||||
acceptChildOrder(data: AcceptChildOrderInput!): Transaction!
|
||||
|
||||
"""Принять предложение из запасов кооператива"""
|
||||
acceptStock(data: AcceptStockInput!): Transaction!
|
||||
|
||||
"""
|
||||
Добавить активного пайщика, который вступил в кооператив, не используя платформу (заполнив заявление собственноручно, оплатив вступительный и минимальный паевый взносы, и получив протокол решения совета)
|
||||
"""
|
||||
@@ -6795,6 +6898,9 @@ type Mutation {
|
||||
"""Добавить метод оплаты (банковский счёт или СБП)"""
|
||||
addPaymentMethod(data: AddPaymentMethodInput!): PaymentMethod!
|
||||
|
||||
"""Добавить пайщика в белый список публикации"""
|
||||
addToPublishWhitelist(username: String!): Boolean!
|
||||
|
||||
"""Добавить доверенное лицо кооперативного участка"""
|
||||
addTrustedAccount(data: AddTrustedAccountInput!): Branch!
|
||||
|
||||
@@ -7061,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!
|
||||
|
||||
@@ -7090,6 +7204,12 @@ type Mutation {
|
||||
"""
|
||||
createProjectOfFreeDecision(data: CreateProjectFreeDecisionInput!): CreatedProjectFreeDecision!
|
||||
|
||||
"""Создать ссылку доступа к странице"""
|
||||
createShareLink(data: CreateShareLinkInput!): ShareLink!
|
||||
|
||||
"""Создать перевозку (КУ отправителя)"""
|
||||
createShipment(data: CreateShipmentInput!): Transaction!
|
||||
|
||||
"""Создать веб-пуш подписку для пользователя"""
|
||||
createWebPushSubscription(data: CreateSubscriptionInput!): CreateSubscriptionResponse!
|
||||
|
||||
@@ -7117,6 +7237,9 @@ type Mutation {
|
||||
"""Подтвердить доставку имущества Заказчику по заявке"""
|
||||
deliverOnRequest(data: DeliverOnRequestInput!): Transaction!
|
||||
|
||||
"""Уничтожить просроченное имущество"""
|
||||
destroyRequest(data: DestroyRequestInput!): Transaction!
|
||||
|
||||
"""Открыть спор по заявке"""
|
||||
disputeOnRequest(data: DisputeOnRequestInput!): Transaction!
|
||||
|
||||
@@ -7175,7 +7298,7 @@ type Mutation {
|
||||
generateRegistrationDocuments(data: GenerateRegistrationDocumentsInput!): GenerateRegistrationDocumentsOutput!
|
||||
|
||||
"""Генерация отчёта для ФНС/ФСС"""
|
||||
generateReport(data: GenerateReportInput!): GeneratedReport!
|
||||
generateReport(data: GenerateReportInput!, organization: OrganizationDataInput!): GeneratedReport!
|
||||
|
||||
"""Сгенерировать документ акта возврата имущества."""
|
||||
generateReturnByAssetAct(data: ReturnByAssetActGenerateDocumentInput!, options: GenerateDocumentOptionsInput): GeneratedDocument!
|
||||
@@ -7259,6 +7382,9 @@ type Mutation {
|
||||
"""
|
||||
receiveOnRequest(data: ReceiveOnRequestInput!): Transaction!
|
||||
|
||||
"""Приём перевозки на складе КУ получателя"""
|
||||
receiveShipment(data: SignShipmentInput!): Transaction!
|
||||
|
||||
"""Обновить токен доступа аккаунта"""
|
||||
refresh(data: RefreshInput!): RegisteredAccount!
|
||||
|
||||
@@ -7270,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!
|
||||
|
||||
@@ -7290,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!
|
||||
|
||||
"""
|
||||
Начать процесс установки кооператива, установить ключ и получить код установки
|
||||
"""
|
||||
@@ -7333,6 +7480,9 @@ type Mutation {
|
||||
"""Обновить расширение"""
|
||||
updateExtension(data: ExtensionInput!): Extension!
|
||||
|
||||
"""Обновить настройки маркетплейса (только chairman)"""
|
||||
updateMarketplaceSettings(data: UpdateMarketplaceSettingsInput!): MarketplaceSettings!
|
||||
|
||||
"""Обновить заявку"""
|
||||
updateRequest(data: UpdateRequestInput!): Transaction!
|
||||
|
||||
@@ -7461,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!
|
||||
@@ -8653,6 +8820,12 @@ type PublicChairman {
|
||||
middle_name: String!
|
||||
}
|
||||
|
||||
enum PublishAccessPolicy {
|
||||
ALL_MEMBERS
|
||||
COUNCIL_ONLY
|
||||
WHITELIST
|
||||
}
|
||||
|
||||
input PublishProjectFreeDecisionInput {
|
||||
"""Имя аккаунта кооператива"""
|
||||
coopname: String!
|
||||
@@ -8837,6 +9010,9 @@ type Query {
|
||||
"""Получить список вопросов совета кооператива для голосования"""
|
||||
getAgenda: [AgendaWithDocuments!]!
|
||||
|
||||
"""Получить список API ключей кооператива"""
|
||||
getApiKeys: [ApiKeyInfo!]!
|
||||
|
||||
"""Получить список доступных типов отчётов"""
|
||||
getAvailableReports: [AvailableReport!]!
|
||||
|
||||
@@ -8891,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!
|
||||
|
||||
@@ -8920,6 +9102,9 @@ type Query {
|
||||
"""Получить конфигурацию программ регистрации для кооператива"""
|
||||
getRegistrationConfig(account_type: AccountType!, coopname: String!): RegistrationConfig!
|
||||
|
||||
"""Получить страницы, к которым мне предоставлен доступ"""
|
||||
getSharedWithMe: [ShareLink!]!
|
||||
|
||||
"""Получить сводную публичную информацию о системе"""
|
||||
getSystemInfo: SystemInfo!
|
||||
|
||||
@@ -9180,6 +9365,13 @@ type RegistrationProgram {
|
||||
title: String!
|
||||
}
|
||||
|
||||
input ReofferRequestInput {
|
||||
new_hash: String!
|
||||
new_meta: String!
|
||||
new_unit_cost: String!
|
||||
request_hash: String!
|
||||
}
|
||||
|
||||
enum ReportType {
|
||||
BUHOTCH
|
||||
DUSN
|
||||
@@ -9230,6 +9422,12 @@ input RepresentedByInput {
|
||||
position: String!
|
||||
}
|
||||
|
||||
input ReqReturnInput {
|
||||
request_hash: String!
|
||||
return_statement: SignedDigitalDocumentInput!
|
||||
username: String!
|
||||
}
|
||||
|
||||
input ResetKeyInput {
|
||||
"""Публичный ключ для замены"""
|
||||
public_key: String!
|
||||
@@ -10028,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!
|
||||
@@ -10080,6 +10297,11 @@ input SignBySecretaryOnAnnualGeneralMeetInput {
|
||||
username: String!
|
||||
}
|
||||
|
||||
input SignShipmentInput {
|
||||
document: SignedDigitalDocumentInput!
|
||||
hash: String!
|
||||
}
|
||||
|
||||
type SignatureInfo {
|
||||
id: Float!
|
||||
is_valid: Boolean
|
||||
@@ -10166,6 +10388,11 @@ input SignedDigitalDocumentInput {
|
||||
version: String!
|
||||
}
|
||||
|
||||
type SovietDataChange {
|
||||
action: String!
|
||||
entity: String!
|
||||
}
|
||||
|
||||
input SovietMemberInput {
|
||||
individual_data: CreateSovietIndividualDataInput!
|
||||
role: String!
|
||||
@@ -10244,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!
|
||||
@@ -10345,6 +10597,11 @@ enum SystemStatus {
|
||||
maintenance
|
||||
}
|
||||
|
||||
type SystemStatusChange {
|
||||
message: String
|
||||
status: String!
|
||||
}
|
||||
|
||||
type Token {
|
||||
"""Дата истечения токена доступа"""
|
||||
expires: DateTime!
|
||||
@@ -10580,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!
|
||||
|
||||
@@ -16,6 +16,12 @@ import { EventsInfrastructureModule } from './infrastructure/events/events.modul
|
||||
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';
|
||||
@@ -73,6 +79,7 @@ import { SettingsApplicationModule } from './application/settings/settings.modul
|
||||
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({
|
||||
@@ -95,6 +102,10 @@ import { MutationLoggingInterceptor } from './application/common/interceptors/mu
|
||||
RedisModule,
|
||||
NovuModule,
|
||||
SearchInfrastructureModule,
|
||||
CaslModule,
|
||||
PubSubModule,
|
||||
ShareModule,
|
||||
ApiKeyModule,
|
||||
EventsInfrastructureModule,
|
||||
FreeDecisionInfrastructureModule,
|
||||
DecisionTrackingInfrastructureModule,
|
||||
@@ -152,7 +163,10 @@ import { MutationLoggingInterceptor } from './application/common/interceptors/mu
|
||||
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,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 {}
|
||||
+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');
|
||||
|
||||
+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);
|
||||
}
|
||||
}
|
||||
+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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+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';
|
||||
@@ -218,6 +219,7 @@ import { ProcessInstanceTypeormRepository } from './infrastructure/repositories/
|
||||
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';
|
||||
@@ -520,6 +522,7 @@ export class CapitalPlugin extends BaseExtModule {
|
||||
forwardRef(() => RegistrationModule),
|
||||
RegistrationInfrastructureModule,
|
||||
EventEmitterModule.forRoot(),
|
||||
PubSubModule,
|
||||
],
|
||||
providers: [
|
||||
// Plugin
|
||||
@@ -673,6 +676,7 @@ IssueIdGenerationService,
|
||||
},
|
||||
ProcessService,
|
||||
ProcessResolver,
|
||||
CapitalSubscriptionResolver,
|
||||
{
|
||||
provide: COMMENT_REPOSITORY,
|
||||
useClass: CommentTypeormRepository,
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { MarketplaceDomainModule } from '../domain/marketplace-domain.module';
|
||||
import { CategoryTreeResolver } from './resolvers/category-tree.resolver';
|
||||
import { AttributeResolver } from './resolvers/attribute.resolver';
|
||||
import { AvailableCategoryAdminResolver } from './resolvers/available-category-admin.resolver';
|
||||
import { RequestResolver } from './resolvers/request.resolver';
|
||||
import { CategoryTreeService, CATEGORY_TREE_SERVICE } from './services/category-tree.service';
|
||||
|
||||
/**
|
||||
* Модуль приложения marketplace
|
||||
* Содержит GraphQL резолверы и сервисы приложения
|
||||
*/
|
||||
@Module({
|
||||
imports: [MarketplaceDomainModule],
|
||||
providers: [
|
||||
// GraphQL резолверы
|
||||
CategoryTreeResolver,
|
||||
AttributeResolver,
|
||||
AvailableCategoryAdminResolver,
|
||||
RequestResolver,
|
||||
|
||||
// Сервисы приложения
|
||||
{
|
||||
provide: CATEGORY_TREE_SERVICE,
|
||||
useClass: CategoryTreeService,
|
||||
},
|
||||
CategoryTreeService,
|
||||
],
|
||||
exports: [
|
||||
// Экспортируем сервисы для использования в других модулях
|
||||
CATEGORY_TREE_SERVICE,
|
||||
|
||||
// Экспортируем резолверы для регистрации в GraphQL
|
||||
CategoryTreeResolver,
|
||||
AttributeResolver,
|
||||
AvailableCategoryAdminResolver,
|
||||
RequestResolver,
|
||||
],
|
||||
})
|
||||
export class MarketplaceApplicationModule {}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { InputType, Field, Int } from '@nestjs/graphql';
|
||||
|
||||
/**
|
||||
* Input для добавления категорий
|
||||
*/
|
||||
@InputType()
|
||||
export class AddAvailableCategoriesInput {
|
||||
@Field(() => [Int], { description: 'ID категорий для добавления (целые категории)' })
|
||||
categoryIds!: number[];
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import { InputType, Field } from '@nestjs/graphql';
|
||||
import { CategoryTypeInput } from './category-type-input.dto';
|
||||
|
||||
/**
|
||||
* Input для добавления типов товаров
|
||||
*/
|
||||
@InputType()
|
||||
export class AddAvailableCategoryTypesInput {
|
||||
@Field(() => [CategoryTypeInput], { description: 'Типы товаров для добавления' })
|
||||
categoryTypes!: CategoryTypeInput[];
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
import { ObjectType, Field, Int, registerEnumType } from '@nestjs/graphql';
|
||||
import type { AttributeDomainEntity } from '../../domain/entities/attribute-domain.entity';
|
||||
import type { DictionaryDomainEntity } from '../../domain/entities/dictionary-domain.entity';
|
||||
import type { DictionaryValueDomainEntity } from '../../domain/entities/dictionary-value-domain.entity';
|
||||
|
||||
/**
|
||||
* Enum для типов атрибутов
|
||||
*/
|
||||
export enum AttributeType {
|
||||
STRING = 'string',
|
||||
NUMBER = 'number',
|
||||
BOOLEAN = 'boolean',
|
||||
DATE = 'date',
|
||||
URL = 'url',
|
||||
}
|
||||
|
||||
registerEnumType(AttributeType, {
|
||||
name: 'MarketplaceAttributeType',
|
||||
description: 'Тип атрибута товара',
|
||||
});
|
||||
|
||||
/**
|
||||
* GraphQL DTO для значения словаря
|
||||
*/
|
||||
@ObjectType('MarketplaceDictionaryValue')
|
||||
export class DictionaryValueDTO {
|
||||
@Field(() => Int, { description: 'ID значения словаря' })
|
||||
dictionaryValueId!: number;
|
||||
|
||||
@Field({ description: 'Значение' })
|
||||
value!: string;
|
||||
|
||||
@Field({ description: 'Дополнительная информация', nullable: true })
|
||||
info?: string;
|
||||
|
||||
@Field({ description: 'URL изображения', nullable: true })
|
||||
picture?: string;
|
||||
|
||||
@Field(() => Int, { description: 'ID словаря' })
|
||||
dictionaryId!: number;
|
||||
|
||||
@Field({ description: 'Есть ли изображение' })
|
||||
hasPicture!: boolean;
|
||||
|
||||
@Field({ description: 'Есть ли дополнительная информация' })
|
||||
hasInfo!: boolean;
|
||||
|
||||
@Field({ description: 'Полное описание значения' })
|
||||
fullDescription!: string;
|
||||
|
||||
constructor(data: {
|
||||
dictionaryValueId: number;
|
||||
value: string;
|
||||
info?: string;
|
||||
picture?: string;
|
||||
dictionaryId: number;
|
||||
hasPicture: boolean;
|
||||
hasInfo: boolean;
|
||||
fullDescription: string;
|
||||
}) {
|
||||
this.dictionaryValueId = data.dictionaryValueId;
|
||||
this.value = data.value;
|
||||
this.info = data.info;
|
||||
this.picture = data.picture;
|
||||
this.dictionaryId = data.dictionaryId;
|
||||
this.hasPicture = data.hasPicture;
|
||||
this.hasInfo = data.hasInfo;
|
||||
this.fullDescription = data.fullDescription;
|
||||
}
|
||||
|
||||
static fromDomain(entity: DictionaryValueDomainEntity): DictionaryValueDTO {
|
||||
return new DictionaryValueDTO({
|
||||
dictionaryValueId: entity.dictionaryValueId,
|
||||
value: entity.value,
|
||||
info: entity.info,
|
||||
picture: entity.picture,
|
||||
dictionaryId: entity.dictionaryId,
|
||||
hasPicture: entity.hasPicture(),
|
||||
hasInfo: entity.hasInfo(),
|
||||
fullDescription: entity.getFullDescription(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GraphQL DTO для словаря
|
||||
*/
|
||||
@ObjectType('MarketplaceDictionary')
|
||||
export class DictionaryDTO {
|
||||
@Field(() => Int, { description: 'ID словаря' })
|
||||
dictionaryId!: number;
|
||||
|
||||
@Field({ description: 'Название словаря', nullable: true })
|
||||
name?: string;
|
||||
|
||||
@Field({ description: 'Описание словаря', nullable: true })
|
||||
description?: string;
|
||||
|
||||
@Field(() => [DictionaryValueDTO], { description: 'Значения словаря' })
|
||||
values!: DictionaryValueDTO[];
|
||||
|
||||
@Field(() => Int, { description: 'Количество значений' })
|
||||
valuesCount!: number;
|
||||
|
||||
@Field({ description: 'Есть ли значения с изображениями' })
|
||||
hasValuesWithPictures!: boolean;
|
||||
|
||||
constructor(data: {
|
||||
dictionaryId: number;
|
||||
name?: string;
|
||||
description?: string;
|
||||
values: DictionaryValueDTO[];
|
||||
valuesCount: number;
|
||||
hasValuesWithPictures: boolean;
|
||||
}) {
|
||||
this.dictionaryId = data.dictionaryId;
|
||||
this.name = data.name;
|
||||
this.description = data.description;
|
||||
this.values = data.values;
|
||||
this.valuesCount = data.valuesCount;
|
||||
this.hasValuesWithPictures = data.hasValuesWithPictures;
|
||||
}
|
||||
|
||||
static fromDomain(entity: DictionaryDomainEntity, includeValues = true): DictionaryDTO {
|
||||
return new DictionaryDTO({
|
||||
dictionaryId: entity.dictionaryId,
|
||||
name: entity.name,
|
||||
description: entity.description,
|
||||
values: includeValues ? entity.values.map((value) => DictionaryValueDTO.fromDomain(value)) : [],
|
||||
valuesCount: entity.getValuesCount(),
|
||||
hasValuesWithPictures: entity.hasValuesWithPictures(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GraphQL DTO для группы атрибутов
|
||||
*/
|
||||
@ObjectType('MarketplaceAttributeGroup')
|
||||
export class AttributeGroupDTO {
|
||||
@Field(() => Int, { description: 'ID группы', nullable: true })
|
||||
groupId?: number;
|
||||
|
||||
@Field({ description: 'Название группы' })
|
||||
groupName!: string;
|
||||
|
||||
@Field(() => [AttributeDTO], { description: 'Атрибуты в группе' })
|
||||
attributes!: AttributeDTO[];
|
||||
|
||||
@Field(() => Int, { description: 'Количество атрибутов в группе' })
|
||||
attributesCount!: number;
|
||||
|
||||
constructor(data: { groupId?: number; groupName: string; attributes: AttributeDTO[] }) {
|
||||
this.groupId = data.groupId;
|
||||
this.groupName = data.groupName;
|
||||
this.attributes = data.attributes;
|
||||
this.attributesCount = data.attributes.length;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GraphQL DTO для атрибута товара
|
||||
*/
|
||||
@ObjectType('MarketplaceAttribute')
|
||||
export class AttributeDTO {
|
||||
@Field(() => Int, { description: 'ID атрибута' })
|
||||
attributeId!: number;
|
||||
|
||||
@Field({ description: 'Название атрибута' })
|
||||
name!: string;
|
||||
|
||||
@Field({ description: 'Описание атрибута', nullable: true })
|
||||
description?: string;
|
||||
|
||||
@Field(() => AttributeType, { description: 'Тип атрибута' })
|
||||
type!: AttributeType;
|
||||
|
||||
@Field({ description: 'Является ли атрибут коллекцией' })
|
||||
isCollection!: boolean;
|
||||
|
||||
@Field({ description: 'Является ли атрибут обязательным' })
|
||||
isRequired!: boolean;
|
||||
|
||||
@Field({ description: 'Является ли атрибут аспектным' })
|
||||
isAspect!: boolean;
|
||||
|
||||
@Field(() => Int, { description: 'Максимальное количество значений' })
|
||||
maxValueCount!: number;
|
||||
|
||||
@Field({ description: 'Название группы', nullable: true })
|
||||
groupName?: string;
|
||||
|
||||
@Field(() => Int, { description: 'ID группы', nullable: true })
|
||||
groupId?: number;
|
||||
|
||||
@Field(() => Int, { description: 'ID словаря', nullable: true })
|
||||
dictionaryId?: number;
|
||||
|
||||
@Field({ description: 'Зависит ли от категории' })
|
||||
categoryDependent!: boolean;
|
||||
|
||||
@Field({ description: 'Комплексная коллекция' })
|
||||
complexIsCollection!: boolean;
|
||||
|
||||
@Field(() => Int, { description: 'ID комплексного атрибута' })
|
||||
attributeComplexId!: number;
|
||||
|
||||
@Field(() => DictionaryDTO, { description: 'Словарь значений', nullable: true })
|
||||
dictionary?: DictionaryDTO;
|
||||
|
||||
@Field({ description: 'Имеет ли словарь' })
|
||||
hasDictionary!: boolean;
|
||||
|
||||
@Field({ description: 'Можно ли изменить после создания товара' })
|
||||
canBeModifiedAfterCreation!: boolean;
|
||||
|
||||
@Field(() => Int, { description: 'Максимальное количество значений (вычисленное)' })
|
||||
maxValues!: number;
|
||||
|
||||
@Field({ description: 'Является ли комплексным атрибутом' })
|
||||
isComplexAttribute!: boolean;
|
||||
|
||||
constructor(data: {
|
||||
attributeId: number;
|
||||
name: string;
|
||||
description?: string;
|
||||
type: AttributeType;
|
||||
isCollection: boolean;
|
||||
isRequired: boolean;
|
||||
isAspect: boolean;
|
||||
maxValueCount: number;
|
||||
groupName?: string;
|
||||
groupId?: number;
|
||||
dictionaryId?: number;
|
||||
categoryDependent: boolean;
|
||||
complexIsCollection: boolean;
|
||||
attributeComplexId: number;
|
||||
dictionary?: DictionaryDTO;
|
||||
hasDictionary: boolean;
|
||||
canBeModifiedAfterCreation: boolean;
|
||||
maxValues: number;
|
||||
isComplexAttribute: boolean;
|
||||
}) {
|
||||
this.attributeId = data.attributeId;
|
||||
this.name = data.name;
|
||||
this.description = data.description;
|
||||
this.type = data.type;
|
||||
this.isCollection = data.isCollection;
|
||||
this.isRequired = data.isRequired;
|
||||
this.isAspect = data.isAspect;
|
||||
this.maxValueCount = data.maxValueCount;
|
||||
this.groupName = data.groupName;
|
||||
this.groupId = data.groupId;
|
||||
this.dictionaryId = data.dictionaryId;
|
||||
this.categoryDependent = data.categoryDependent;
|
||||
this.complexIsCollection = data.complexIsCollection;
|
||||
this.attributeComplexId = data.attributeComplexId;
|
||||
this.dictionary = data.dictionary;
|
||||
this.hasDictionary = data.hasDictionary;
|
||||
this.canBeModifiedAfterCreation = data.canBeModifiedAfterCreation;
|
||||
this.maxValues = data.maxValues;
|
||||
this.isComplexAttribute = data.isComplexAttribute;
|
||||
}
|
||||
|
||||
static fromDomain(entity: AttributeDomainEntity, includeDictionary = true): AttributeDTO {
|
||||
return new AttributeDTO({
|
||||
attributeId: entity.attributeId,
|
||||
name: entity.name,
|
||||
description: entity.description,
|
||||
type: entity.type as AttributeType,
|
||||
isCollection: entity.isCollection,
|
||||
isRequired: entity.isRequired,
|
||||
isAspect: entity.isAspect,
|
||||
maxValueCount: entity.maxValueCount,
|
||||
groupName: entity.groupName,
|
||||
groupId: entity.groupId,
|
||||
dictionaryId: entity.dictionaryId,
|
||||
categoryDependent: entity.categoryDependent,
|
||||
complexIsCollection: entity.complexIsCollection,
|
||||
attributeComplexId: entity.attributeComplexId,
|
||||
dictionary: includeDictionary && entity.dictionary ? DictionaryDTO.fromDomain(entity.dictionary) : undefined,
|
||||
hasDictionary: entity.hasDictionary(),
|
||||
canBeModifiedAfterCreation: entity.canBeModifiedAfterCreation(),
|
||||
maxValues: entity.getMaxValues(),
|
||||
isComplexAttribute: entity.isComplexAttribute(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GraphQL DTO для статистики атрибутов
|
||||
*/
|
||||
@ObjectType('MarketplaceAttributeStats')
|
||||
export class AttributeStatsDTO {
|
||||
@Field(() => Int, { description: 'Общее количество атрибутов' })
|
||||
totalAttributes!: number;
|
||||
|
||||
@Field(() => Int, { description: 'Количество обязательных атрибутов' })
|
||||
requiredAttributes!: number;
|
||||
|
||||
@Field(() => Int, { description: 'Количество аспектных атрибутов' })
|
||||
aspectAttributes!: number;
|
||||
|
||||
@Field(() => Int, { description: 'Количество словарных атрибутов' })
|
||||
dictionaryAttributes!: number;
|
||||
|
||||
@Field(() => Int, { description: 'Общее количество словарей' })
|
||||
totalDictionaries!: number;
|
||||
|
||||
@Field(() => Int, { description: 'Общее количество значений словарей' })
|
||||
totalDictionaryValues!: number;
|
||||
|
||||
constructor(data: {
|
||||
totalAttributes: number;
|
||||
requiredAttributes: number;
|
||||
aspectAttributes: number;
|
||||
dictionaryAttributes: number;
|
||||
totalDictionaries: number;
|
||||
totalDictionaryValues: number;
|
||||
}) {
|
||||
this.totalAttributes = data.totalAttributes;
|
||||
this.requiredAttributes = data.requiredAttributes;
|
||||
this.aspectAttributes = data.aspectAttributes;
|
||||
this.dictionaryAttributes = data.dictionaryAttributes;
|
||||
this.totalDictionaries = data.totalDictionaries;
|
||||
this.totalDictionaryValues = data.totalDictionaryValues;
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { ObjectType, Field, Int } from '@nestjs/graphql';
|
||||
|
||||
/**
|
||||
* DTO для статистики доступности
|
||||
*/
|
||||
@ObjectType('MarketplaceAvailabilityStats')
|
||||
export class AvailabilityStatsDTO {
|
||||
@Field(() => Int, { description: 'Общее количество доступных элементов' })
|
||||
totalAvailable!: number;
|
||||
|
||||
@Field(() => Int, { description: 'Количество доступных категорий (целых)' })
|
||||
categoriesCount!: number;
|
||||
|
||||
@Field(() => Int, { description: 'Количество доступных типов товаров' })
|
||||
typesCount!: number;
|
||||
|
||||
@Field({ description: 'Есть ли ограничения по категориям' })
|
||||
hasRestrictions!: boolean;
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import { ObjectType, Field, Int } from '@nestjs/graphql';
|
||||
|
||||
/**
|
||||
* DTO для доступной категории/типа
|
||||
*/
|
||||
@ObjectType('MarketplaceAvailableCategory')
|
||||
export class AvailableCategoryDTO {
|
||||
@Field(() => Int, { description: 'ID записи' })
|
||||
id!: number;
|
||||
|
||||
@Field({ description: 'Название кооператива' })
|
||||
coopname!: string;
|
||||
|
||||
@Field(() => Int, { description: 'ID категории' })
|
||||
categoryId!: number;
|
||||
|
||||
@Field(() => Int, { description: 'ID типа товара (null = вся категория)', nullable: true })
|
||||
typeId?: number;
|
||||
|
||||
@Field({ description: 'Активна ли категория/тип' })
|
||||
isActive!: boolean;
|
||||
|
||||
@Field({ description: 'Кто добавил категорию/тип' })
|
||||
addedBy!: string;
|
||||
|
||||
@Field({ description: 'Применяется к всей категории' })
|
||||
isForEntireCategory!: boolean;
|
||||
|
||||
@Field({ description: 'Применяется к конкретному типу' })
|
||||
isForSpecificType!: boolean;
|
||||
|
||||
@Field({ description: 'Дата создания' })
|
||||
createdAt!: Date;
|
||||
|
||||
@Field({ description: 'Дата обновления' })
|
||||
updatedAt!: Date;
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
import { ObjectType, Field, Int } from '@nestjs/graphql';
|
||||
import type { CategoryDomainEntity } from '../../domain/entities/category-domain.entity';
|
||||
import type { TypeDomainEntity } from '../../domain/entities/type-domain.entity';
|
||||
|
||||
/**
|
||||
* GraphQL DTO для типа товара
|
||||
*/
|
||||
@ObjectType('MarketplaceProductType')
|
||||
export class ProductTypeDTO {
|
||||
@Field(() => Int, { description: 'ID типа товара' })
|
||||
typeId!: number;
|
||||
|
||||
@Field({ description: 'Название типа товара' })
|
||||
typeName!: string;
|
||||
|
||||
@Field({ description: 'Признак отключения типа' })
|
||||
disabled!: boolean;
|
||||
|
||||
@Field(() => Int, { description: 'ID категории' })
|
||||
descriptionCategoryId!: number;
|
||||
|
||||
@Field({ description: 'Полное название с категорией', nullable: true })
|
||||
fullName?: string;
|
||||
|
||||
@Field({ description: 'Доступен ли тип для создания товаров' })
|
||||
isAvailable!: boolean;
|
||||
|
||||
constructor(data: {
|
||||
typeId: number;
|
||||
typeName: string;
|
||||
disabled: boolean;
|
||||
descriptionCategoryId: number;
|
||||
fullName?: string;
|
||||
isAvailable: boolean;
|
||||
}) {
|
||||
this.typeId = data.typeId;
|
||||
this.typeName = data.typeName;
|
||||
this.disabled = data.disabled;
|
||||
this.descriptionCategoryId = data.descriptionCategoryId;
|
||||
this.fullName = data.fullName;
|
||||
this.isAvailable = data.isAvailable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Создать DTO из доменной сущности
|
||||
*/
|
||||
static fromDomain(entity: TypeDomainEntity): ProductTypeDTO {
|
||||
return new ProductTypeDTO({
|
||||
typeId: entity.typeId,
|
||||
typeName: entity.typeName,
|
||||
disabled: entity.disabled,
|
||||
descriptionCategoryId: entity.descriptionCategoryId,
|
||||
fullName: entity.getFullName(),
|
||||
isAvailable: entity.isAvailable(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GraphQL DTO для категории товаров
|
||||
*/
|
||||
@ObjectType('MarketplaceCategory')
|
||||
export class CategoryDTO {
|
||||
@Field(() => Int, { description: 'ID категории' })
|
||||
descriptionCategoryId!: number;
|
||||
|
||||
@Field({ description: 'Название категории' })
|
||||
categoryName!: string;
|
||||
|
||||
@Field({ description: 'Признак отключения категории' })
|
||||
disabled!: boolean;
|
||||
|
||||
@Field(() => Int, { description: 'ID родительской категории', nullable: true })
|
||||
parentId?: number;
|
||||
|
||||
@Field(() => [CategoryDTO], { description: 'Дочерние категории' })
|
||||
children!: CategoryDTO[];
|
||||
|
||||
@Field(() => [ProductTypeDTO], { description: 'Типы товаров в категории' })
|
||||
types!: ProductTypeDTO[];
|
||||
|
||||
@Field({ description: 'Полный путь к категории', nullable: true })
|
||||
fullPath?: string;
|
||||
|
||||
@Field({ description: 'Является ли категория листовой (можно создавать товары)' })
|
||||
isLeafCategory!: boolean;
|
||||
|
||||
@Field(() => Int, { description: 'Количество дочерних категорий' })
|
||||
childrenCount!: number;
|
||||
|
||||
@Field(() => Int, { description: 'Количество типов товаров' })
|
||||
typesCount!: number;
|
||||
|
||||
constructor(data: {
|
||||
descriptionCategoryId: number;
|
||||
categoryName: string;
|
||||
disabled: boolean;
|
||||
parentId?: number;
|
||||
children: CategoryDTO[];
|
||||
types: ProductTypeDTO[];
|
||||
fullPath?: string;
|
||||
isLeafCategory: boolean;
|
||||
}) {
|
||||
this.descriptionCategoryId = data.descriptionCategoryId;
|
||||
this.categoryName = data.categoryName;
|
||||
this.disabled = data.disabled;
|
||||
this.parentId = data.parentId;
|
||||
this.children = data.children;
|
||||
this.types = data.types;
|
||||
this.fullPath = data.fullPath;
|
||||
this.isLeafCategory = data.isLeafCategory;
|
||||
this.childrenCount = data.children.length;
|
||||
this.typesCount = data.types.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Создать DTO из доменной сущности
|
||||
*/
|
||||
static fromDomain(entity: CategoryDomainEntity): CategoryDTO {
|
||||
return new CategoryDTO({
|
||||
descriptionCategoryId: entity.descriptionCategoryId,
|
||||
categoryName: entity.categoryName,
|
||||
disabled: entity.disabled,
|
||||
parentId: entity.parentId,
|
||||
children: entity.children.map((child) => CategoryDTO.fromDomain(child)),
|
||||
types: entity.types.map((type) => ProductTypeDTO.fromDomain(type)),
|
||||
fullPath: entity.getFullPath(),
|
||||
isLeafCategory: entity.isLeafCategory(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GraphQL DTO для статистики дерева категорий
|
||||
*/
|
||||
@ObjectType('MarketplaceCategoryTreeStats')
|
||||
export class CategoryTreeStatsDTO {
|
||||
@Field(() => Int, { description: 'Общее количество категорий' })
|
||||
totalCategories!: number;
|
||||
|
||||
@Field(() => Int, { description: 'Количество корневых категорий' })
|
||||
rootCategories!: number;
|
||||
|
||||
@Field(() => Int, { description: 'Количество листовых категорий' })
|
||||
leafCategories!: number;
|
||||
|
||||
@Field(() => Int, { description: 'Количество отключенных категорий' })
|
||||
disabledCategories!: number;
|
||||
|
||||
@Field(() => Int, { description: 'Общее количество типов товаров' })
|
||||
totalTypes!: number;
|
||||
|
||||
@Field(() => Int, { description: 'Количество доступных типов товаров' })
|
||||
availableTypes!: number;
|
||||
|
||||
constructor(data: {
|
||||
totalCategories: number;
|
||||
rootCategories: number;
|
||||
leafCategories: number;
|
||||
disabledCategories: number;
|
||||
totalTypes: number;
|
||||
availableTypes: number;
|
||||
}) {
|
||||
this.totalCategories = data.totalCategories;
|
||||
this.rootCategories = data.rootCategories;
|
||||
this.leafCategories = data.leafCategories;
|
||||
this.disabledCategories = data.disabledCategories;
|
||||
this.totalTypes = data.totalTypes;
|
||||
this.availableTypes = data.availableTypes;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GraphQL DTO для результатов поиска по дереву
|
||||
*/
|
||||
@ObjectType('MarketplaceCategoryTreeSearchResult')
|
||||
export class CategoryTreeSearchResultDTO {
|
||||
@Field(() => [CategoryDTO], { description: 'Найденные категории' })
|
||||
matchedCategories!: CategoryDTO[];
|
||||
|
||||
@Field(() => [ProductTypeDTO], { description: 'Найденные типы товаров' })
|
||||
matchedTypes!: ProductTypeDTO[];
|
||||
|
||||
@Field(() => [CategoryDTO], { description: 'Результирующее дерево категорий' })
|
||||
resultTree!: CategoryDTO[];
|
||||
|
||||
@Field(() => Int, { description: 'Общее количество найденных элементов' })
|
||||
totalMatches!: number;
|
||||
|
||||
constructor(data: { matchedCategories: CategoryDTO[]; matchedTypes: ProductTypeDTO[]; resultTree: CategoryDTO[] }) {
|
||||
this.matchedCategories = data.matchedCategories;
|
||||
this.matchedTypes = data.matchedTypes;
|
||||
this.resultTree = data.resultTree;
|
||||
this.totalMatches = data.matchedCategories.length + data.matchedTypes.length;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GraphQL DTO для товара с путем к категории
|
||||
*/
|
||||
@ObjectType('MarketplaceProductWithPath')
|
||||
export class ProductWithPathDTO {
|
||||
@Field(() => ProductTypeDTO, { description: 'Тип товара' })
|
||||
type!: ProductTypeDTO;
|
||||
|
||||
@Field(() => [CategoryDTO], { description: 'Путь к категории' })
|
||||
categoryPath!: CategoryDTO[];
|
||||
|
||||
@Field({ description: 'Полный путь в виде строки' })
|
||||
fullPath!: string;
|
||||
|
||||
constructor(data: { type: ProductTypeDTO; categoryPath: CategoryDTO[]; fullPath: string }) {
|
||||
this.type = data.type;
|
||||
this.categoryPath = data.categoryPath;
|
||||
this.fullPath = data.fullPath;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GraphQL DTO для категории с типами
|
||||
*/
|
||||
@ObjectType('MarketplaceCategoryWithTypes')
|
||||
export class CategoryWithTypesDTO {
|
||||
@Field(() => CategoryDTO, { description: 'Категория' })
|
||||
category!: CategoryDTO;
|
||||
|
||||
@Field(() => [ProductTypeDTO], { description: 'Найденные типы товаров в категории' })
|
||||
matchedTypes!: ProductTypeDTO[];
|
||||
|
||||
@Field(() => [ProductTypeDTO], { description: 'Все типы товаров в категории' })
|
||||
allTypes!: ProductTypeDTO[];
|
||||
|
||||
@Field(() => [CategoryDTO], { description: 'Путь к категории' })
|
||||
categoryPath!: CategoryDTO[];
|
||||
|
||||
@Field(() => Int, { description: 'Количество найденных типов' })
|
||||
matchedTypesCount!: number;
|
||||
|
||||
@Field(() => Int, { description: 'Общее количество типов' })
|
||||
totalTypesCount!: number;
|
||||
|
||||
constructor(data: {
|
||||
category: CategoryDTO;
|
||||
matchedTypes: ProductTypeDTO[];
|
||||
allTypes: ProductTypeDTO[];
|
||||
categoryPath: CategoryDTO[];
|
||||
}) {
|
||||
this.category = data.category;
|
||||
this.matchedTypes = data.matchedTypes;
|
||||
this.allTypes = data.allTypes;
|
||||
this.categoryPath = data.categoryPath;
|
||||
this.matchedTypesCount = data.matchedTypes.length;
|
||||
this.totalTypesCount = data.allTypes.length;
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { InputType, Field, Int } from '@nestjs/graphql';
|
||||
|
||||
/**
|
||||
* Input для типа категории товара
|
||||
*/
|
||||
@InputType()
|
||||
export class CategoryTypeInput {
|
||||
@Field(() => Int, { description: 'ID категории' })
|
||||
categoryId!: number;
|
||||
|
||||
@Field(() => Int, { description: 'ID типа товара' })
|
||||
typeId!: number;
|
||||
}
|
||||
+242
@@ -0,0 +1,242 @@
|
||||
import { InputType, Field, Int, registerEnumType } from '@nestjs/graphql';
|
||||
import { IsOptional, IsString, IsNumber, IsArray, IsEnum, IsNotEmpty, Min, IsUrl, ValidateNested } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
// Регистрируем enum для GraphQL
|
||||
export enum RequestTypeInput {
|
||||
OFFER = 'offer',
|
||||
ORDER = 'order',
|
||||
}
|
||||
|
||||
export enum RequestImageTypeInput {
|
||||
REGULAR = 'regular',
|
||||
PRIMARY = 'primary',
|
||||
COLOR_SAMPLE = 'color_sample',
|
||||
IMAGE_360 = 'image_360',
|
||||
}
|
||||
|
||||
registerEnumType(RequestTypeInput, {
|
||||
name: 'RequestTypeInput',
|
||||
description: 'Тип заявки: offer - предложение, order - заказ',
|
||||
});
|
||||
|
||||
registerEnumType(RequestImageTypeInput, {
|
||||
name: 'RequestImageTypeInput',
|
||||
description: 'Тип изображения заявки',
|
||||
});
|
||||
|
||||
/**
|
||||
* Input для атрибута заявки
|
||||
*/
|
||||
@InputType()
|
||||
export class RequestAttributeInput {
|
||||
@Field(() => Int, { description: 'ID атрибута' })
|
||||
@IsNumber()
|
||||
attributeId!: number;
|
||||
|
||||
@Field({ description: 'Значение атрибута' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
value!: string;
|
||||
|
||||
@Field(() => Int, { description: 'ID комплексного атрибута', nullable: true, defaultValue: 0 })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
complexId?: number;
|
||||
|
||||
@Field(() => Int, { description: 'ID значения из словаря', nullable: true })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
dictionaryValueId?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Input для изображения заявки
|
||||
*/
|
||||
@InputType()
|
||||
export class RequestImageInput {
|
||||
@Field({ description: 'URL изображения' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@IsUrl({}, { message: 'Некорректный URL изображения' })
|
||||
imageUrl!: string;
|
||||
|
||||
@Field(() => RequestImageTypeInput, { description: 'Тип изображения', defaultValue: RequestImageTypeInput.REGULAR })
|
||||
@IsEnum(RequestImageTypeInput)
|
||||
imageType!: RequestImageTypeInput;
|
||||
|
||||
@Field(() => Int, { description: 'Порядок сортировки', defaultValue: 0 })
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
sortOrder!: number;
|
||||
|
||||
@Field({ description: 'Описание изображения', nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Input для создания заявки
|
||||
*/
|
||||
@InputType()
|
||||
export class CreateRequestInput {
|
||||
@Field({ description: 'Имя аккаунта кооператива' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
coopname!: string;
|
||||
|
||||
@Field(() => RequestTypeInput, { description: 'Тип заявки: offer - предложение, order - заказ' })
|
||||
@IsEnum(RequestTypeInput)
|
||||
type!: RequestTypeInput;
|
||||
|
||||
// Основная информация о товаре
|
||||
@Field({ description: 'Название товара (до 500 символов)' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name!: string;
|
||||
|
||||
@Field({ description: 'Артикул товара (до 50 символов)' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
articleNumber!: string;
|
||||
|
||||
@Field({ description: 'Штрихкод товара', nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
barcode?: string;
|
||||
|
||||
// Категория и тип
|
||||
@Field(() => Int, { description: 'ID категории' })
|
||||
@IsNumber()
|
||||
descriptionCategoryId!: number;
|
||||
|
||||
@Field(() => Int, { description: 'ID типа товара' })
|
||||
@IsNumber()
|
||||
typeId!: number;
|
||||
|
||||
// Цены
|
||||
@Field(() => Number, { description: 'Цена товара' })
|
||||
@IsNumber()
|
||||
@Min(0.01)
|
||||
price!: number;
|
||||
|
||||
@Field(() => Number, { description: 'Цена до скидки', nullable: true })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0.01)
|
||||
oldPrice?: number;
|
||||
|
||||
@Field({ description: 'Валюта', defaultValue: 'RUB' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
currencyCode!: string;
|
||||
|
||||
@Field({ description: 'Ставка НДС (0, 0.05, 0.07, 0.1, 0.2)' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
vat!: string;
|
||||
|
||||
// Габариты и вес
|
||||
@Field(() => Int, { description: 'Ширина упаковки', nullable: true })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
width?: number;
|
||||
|
||||
@Field(() => Int, { description: 'Высота упаковки', nullable: true })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
height?: number;
|
||||
|
||||
@Field(() => Int, { description: 'Глубина упаковки', nullable: true })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
depth?: number;
|
||||
|
||||
@Field({ description: 'Единица измерения габаритов', nullable: true, defaultValue: 'mm' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
dimensionUnit?: string;
|
||||
|
||||
@Field(() => Int, { description: 'Вес товара', nullable: true })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
weight?: number;
|
||||
|
||||
@Field({ description: 'Единица измерения веса', nullable: true, defaultValue: 'g' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
weightUnit?: string;
|
||||
|
||||
// Количества
|
||||
@Field(() => Int, { description: 'Количество единиц товара' })
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
units!: number;
|
||||
|
||||
// Время жизни и гарантии
|
||||
@Field(() => Int, { description: 'Время жизни продукта в секундах', nullable: true })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
productLifecycleSecs?: number;
|
||||
|
||||
@Field(() => Int, { description: 'Гарантийный срок в днях', nullable: true })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
warrantyDays?: number;
|
||||
|
||||
// Дополнительные данные
|
||||
@Field({ description: 'Дополнительные данные JSON', nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
data?: string;
|
||||
|
||||
@Field({ description: 'Метаданные JSON', nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
meta?: string;
|
||||
|
||||
// Атрибуты и изображения
|
||||
@Field(() => [RequestAttributeInput], { description: 'Атрибуты товара', defaultValue: [] })
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => RequestAttributeInput)
|
||||
attributes!: RequestAttributeInput[];
|
||||
|
||||
@Field(() => [RequestImageInput], { description: 'Изображения товара', defaultValue: [] })
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => RequestImageInput)
|
||||
images!: RequestImageInput[];
|
||||
|
||||
// URL изображений
|
||||
@Field({ description: 'URL главного изображения', nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsUrl({}, { message: 'Некорректный URL главного изображения' })
|
||||
primaryImageUrl?: string;
|
||||
|
||||
@Field({ description: 'URL образца цвета', nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsUrl({}, { message: 'Некорректный URL образца цвета' })
|
||||
colorImageUrl?: string;
|
||||
|
||||
// Геоограничения
|
||||
@Field(() => [String], { description: 'Геоограничения', defaultValue: [] })
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
geoNames!: string[];
|
||||
|
||||
// Родительская заявка
|
||||
@Field({ description: 'Хэш родительской заявки', nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
parentHash?: string;
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { InputType, Field, Int } from '@nestjs/graphql';
|
||||
import { IsNumber } from 'class-validator';
|
||||
|
||||
/**
|
||||
* Input для поиска потенциальных совпадений
|
||||
*/
|
||||
@InputType()
|
||||
export class FindPotentialMatchesInput {
|
||||
@Field(() => Int, { description: 'ID заявки для поиска совпадений' })
|
||||
@IsNumber()
|
||||
requestId!: number;
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import { InputType, Field, Int } from '@nestjs/graphql';
|
||||
import { IsOptional, IsNumber } from 'class-validator';
|
||||
|
||||
/**
|
||||
* Input для получения атрибутов категории и типа
|
||||
*/
|
||||
@InputType()
|
||||
export class GetCategoryAttributesInput {
|
||||
@Field(() => Int, { description: 'ID категории' })
|
||||
@IsNumber()
|
||||
categoryId!: number;
|
||||
|
||||
@Field(() => Int, { description: 'ID типа товара' })
|
||||
@IsNumber()
|
||||
typeId!: number;
|
||||
|
||||
@Field({ description: 'Включать значения словарей', nullable: true, defaultValue: true })
|
||||
@IsOptional()
|
||||
includeDictionaryValues?: boolean;
|
||||
|
||||
@Field({ description: 'Только обязательные атрибуты', nullable: true, defaultValue: false })
|
||||
@IsOptional()
|
||||
onlyRequired?: boolean;
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { InputType, Field, Int } from '@nestjs/graphql';
|
||||
import { IsNumber } from 'class-validator';
|
||||
|
||||
/**
|
||||
* Input для получения категории по ID
|
||||
*/
|
||||
@InputType()
|
||||
export class GetCategoryByIdInput {
|
||||
@Field(() => Int, { description: 'ID категории' })
|
||||
@IsNumber()
|
||||
categoryId!: number;
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import { InputType, Field, Int } from '@nestjs/graphql';
|
||||
import { IsOptional, IsNumber, Min, Max } from 'class-validator';
|
||||
|
||||
/**
|
||||
* Input для получения дерева категорий
|
||||
*/
|
||||
@InputType()
|
||||
export class GetCategoryTreeInput {
|
||||
@Field(() => Int, { description: 'ID корневой категории', nullable: true })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
rootCategoryId?: number;
|
||||
|
||||
@Field({ description: 'Включать только доступные категории', nullable: true, defaultValue: false })
|
||||
@IsOptional()
|
||||
onlyAvailable?: boolean;
|
||||
|
||||
@Field({ description: 'Включать типы товаров', nullable: true, defaultValue: true })
|
||||
@IsOptional()
|
||||
includeTypes?: boolean;
|
||||
|
||||
@Field(() => Int, { description: 'Максимальная глубина дерева', nullable: true })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
@Max(10)
|
||||
maxDepth?: number;
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import { InputType, Field, Int } from '@nestjs/graphql';
|
||||
import { IsOptional, IsString, IsNumber, IsEnum, Min } from 'class-validator';
|
||||
import { RequestTypeInput } from './create-request-input.dto';
|
||||
|
||||
/**
|
||||
* Input для получения заявок кооператива
|
||||
*/
|
||||
@InputType()
|
||||
export class GetCoopRequestsInput {
|
||||
@Field({ description: 'Название кооператива' })
|
||||
@IsString()
|
||||
coopname!: string;
|
||||
|
||||
@Field(() => RequestTypeInput, { description: 'Тип заявки', nullable: true })
|
||||
@IsOptional()
|
||||
@IsEnum(RequestTypeInput)
|
||||
type?: RequestTypeInput;
|
||||
|
||||
@Field({ description: 'Статус заявки', nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
status?: string;
|
||||
|
||||
@Field(() => Int, { description: 'Максимальное количество результатов', nullable: true, defaultValue: 50 })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
limit?: number;
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { InputType, Field, Int } from '@nestjs/graphql';
|
||||
import { IsNumber } from 'class-validator';
|
||||
|
||||
/**
|
||||
* Input для получения типа товара по ID
|
||||
*/
|
||||
@InputType()
|
||||
export class GetProductTypeByIdInput {
|
||||
@Field(() => Int, { description: 'ID типа товара' })
|
||||
@IsNumber()
|
||||
typeId!: number;
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { InputType, Field } from '@nestjs/graphql';
|
||||
import { IsString } from 'class-validator';
|
||||
|
||||
/**
|
||||
* Input для получения заявки по хэшу
|
||||
*/
|
||||
@InputType()
|
||||
export class GetRequestByHashInput {
|
||||
@Field({ description: 'Хэш заявки' })
|
||||
@IsString()
|
||||
hash!: string;
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { InputType, Field, Int } from '@nestjs/graphql';
|
||||
import { IsNumber } from 'class-validator';
|
||||
|
||||
/**
|
||||
* Input для получения заявки по ID
|
||||
*/
|
||||
@InputType()
|
||||
export class GetRequestInput {
|
||||
@Field(() => Int, { description: 'ID заявки' })
|
||||
@IsNumber()
|
||||
id!: number;
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { InputType, Field } from '@nestjs/graphql';
|
||||
import { IsString, IsNotEmpty } from 'class-validator';
|
||||
|
||||
/**
|
||||
* Input для получения статистики заявок
|
||||
*/
|
||||
@InputType()
|
||||
export class GetRequestStatisticsInput {
|
||||
@Field({ description: 'Название кооператива' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
coopname!: string;
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import { InputType, Field, Int } from '@nestjs/graphql';
|
||||
import { IsNumber } from 'class-validator';
|
||||
|
||||
/**
|
||||
* Input для получения обязательных атрибутов
|
||||
*/
|
||||
@InputType()
|
||||
export class GetRequiredAttributesInput {
|
||||
@Field(() => Int, { description: 'ID категории' })
|
||||
@IsNumber()
|
||||
categoryId!: number;
|
||||
|
||||
@Field(() => Int, { description: 'ID типа товара' })
|
||||
@IsNumber()
|
||||
typeId!: number;
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { InputType, Field, Int } from '@nestjs/graphql';
|
||||
import { IsOptional, IsNumber, Min } from 'class-validator';
|
||||
|
||||
/**
|
||||
* Input для получения заявок пользователя
|
||||
*/
|
||||
@InputType()
|
||||
export class GetUserRequestsInput {
|
||||
@Field(() => Int, { description: 'Максимальное количество результатов', nullable: true, defaultValue: 20 })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
limit?: number;
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
import { ObjectType, Field, InputType, Int, registerEnumType } from '@nestjs/graphql';
|
||||
import { IsString, IsOptional, IsBoolean, IsInt, IsEnum, IsArray } from 'class-validator';
|
||||
import { LeadRequestPolicy, PublishAccessPolicy } from '../../domain/entities/marketplace-settings.entity';
|
||||
|
||||
registerEnumType(LeadRequestPolicy, { name: 'LeadRequestPolicy' });
|
||||
registerEnumType(PublishAccessPolicy, { name: 'PublishAccessPolicy' });
|
||||
|
||||
@InputType('UpdateMarketplaceSettingsInput')
|
||||
export class UpdateMarketplaceSettingsInputDTO {
|
||||
@Field(() => LeadRequestPolicy, { nullable: true })
|
||||
@IsOptional()
|
||||
@IsEnum(LeadRequestPolicy)
|
||||
lead_request_policy?: LeadRequestPolicy;
|
||||
|
||||
@Field(() => PublishAccessPolicy, { nullable: true })
|
||||
@IsOptional()
|
||||
@IsEnum(PublishAccessPolicy)
|
||||
publish_access_policy?: PublishAccessPolicy;
|
||||
|
||||
@Field(() => [String], { nullable: true })
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
publish_whitelist?: string[];
|
||||
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
moderation_required?: boolean;
|
||||
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
cycles_enabled?: boolean;
|
||||
|
||||
@Field(() => Int, { nullable: true })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
max_cycle_days?: number;
|
||||
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
external_delivery_enabled?: boolean;
|
||||
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
internal_delivery_enabled?: boolean;
|
||||
|
||||
@Field(() => [String], { nullable: true })
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
allowed_category_ids?: string[];
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
min_unit_cost?: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
max_unit_cost?: string;
|
||||
}
|
||||
|
||||
@ObjectType('MarketplaceSettings')
|
||||
export class MarketplaceSettingsDTO {
|
||||
@Field(() => String)
|
||||
id!: string;
|
||||
|
||||
@Field(() => String)
|
||||
coopname!: string;
|
||||
|
||||
@Field(() => LeadRequestPolicy)
|
||||
lead_request_policy!: LeadRequestPolicy;
|
||||
|
||||
@Field(() => PublishAccessPolicy)
|
||||
publish_access_policy!: PublishAccessPolicy;
|
||||
|
||||
@Field(() => [String])
|
||||
publish_whitelist!: string[];
|
||||
|
||||
@Field(() => Boolean)
|
||||
moderation_required!: boolean;
|
||||
|
||||
@Field(() => Boolean)
|
||||
cycles_enabled!: boolean;
|
||||
|
||||
@Field(() => Int)
|
||||
max_cycle_days!: number;
|
||||
|
||||
@Field(() => Boolean)
|
||||
external_delivery_enabled!: boolean;
|
||||
|
||||
@Field(() => Boolean)
|
||||
internal_delivery_enabled!: boolean;
|
||||
|
||||
@Field(() => [String])
|
||||
allowed_category_ids!: string[];
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
min_unit_cost?: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
max_unit_cost?: string;
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { InputType, Field, Int } from '@nestjs/graphql';
|
||||
import { IsNumber } from 'class-validator';
|
||||
|
||||
/**
|
||||
* Input для публикации заявки
|
||||
*/
|
||||
@InputType()
|
||||
export class PublishRequestInput {
|
||||
@Field(() => Int, { description: 'ID заявки для публикации' })
|
||||
@IsNumber()
|
||||
id!: number;
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { InputType, Field, Int } from '@nestjs/graphql';
|
||||
|
||||
/**
|
||||
* Input для удаления категорий
|
||||
*/
|
||||
@InputType()
|
||||
export class RemoveAvailableCategoriesInput {
|
||||
@Field(() => [Int], { description: 'ID категорий для удаления' })
|
||||
categoryIds!: number[];
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import { InputType, Field } from '@nestjs/graphql';
|
||||
import { CategoryTypeInput } from './category-type-input.dto';
|
||||
|
||||
/**
|
||||
* Input для удаления типов товаров
|
||||
*/
|
||||
@InputType()
|
||||
export class RemoveAvailableCategoryTypesInput {
|
||||
@Field(() => [CategoryTypeInput], { description: 'Типы товаров для удаления' })
|
||||
categoryTypes!: CategoryTypeInput[];
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { InputType, Field, Int } from '@nestjs/graphql';
|
||||
import { CategoryTypeInput } from './category-type-input.dto';
|
||||
|
||||
/**
|
||||
* Input для замены всех доступных элементов
|
||||
*/
|
||||
@InputType()
|
||||
export class ReplaceAvailableItemsInput {
|
||||
@Field(() => [Int], { description: 'ID категорий (целые категории)', defaultValue: [] })
|
||||
categoryIds: number[] = [];
|
||||
|
||||
@Field(() => [CategoryTypeInput], { description: 'Типы товаров', defaultValue: [] })
|
||||
categoryTypes: CategoryTypeInput[] = [];
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
import { ObjectType, Field, Int } from '@nestjs/graphql';
|
||||
|
||||
/**
|
||||
* DTO для подсчета заявок по категории
|
||||
*/
|
||||
@ObjectType('MarketplaceCategoryRequestCount')
|
||||
export class CategoryRequestCountDTO {
|
||||
@Field(() => Int, { description: 'ID категории' })
|
||||
categoryId!: number;
|
||||
|
||||
@Field({ description: 'Название категории' })
|
||||
categoryName!: string;
|
||||
|
||||
@Field(() => Int, { description: 'Количество заявок' })
|
||||
count!: number;
|
||||
|
||||
constructor(data: { categoryId: number; categoryName: string; count: number }) {
|
||||
this.categoryId = data.categoryId;
|
||||
this.categoryName = data.categoryName;
|
||||
this.count = data.count;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DTO для статистики заявок
|
||||
*/
|
||||
@ObjectType('MarketplaceRequestStatistics')
|
||||
export class RequestStatisticsDTO {
|
||||
@Field(() => Int, { description: 'Общее количество заявок' })
|
||||
totalRequests!: number;
|
||||
|
||||
@Field(() => Int, { description: 'Активные предложения' })
|
||||
activeOffers!: number;
|
||||
|
||||
@Field(() => Int, { description: 'Активные заказы' })
|
||||
activeOrders!: number;
|
||||
|
||||
@Field(() => Int, { description: 'Завершенные сделки' })
|
||||
completedDeals!: number;
|
||||
|
||||
@Field(() => [CategoryRequestCountDTO], { description: 'Заявки по категориям' })
|
||||
requestsByCategory!: CategoryRequestCountDTO[];
|
||||
|
||||
constructor(data: {
|
||||
totalRequests: number;
|
||||
activeOffers: number;
|
||||
activeOrders: number;
|
||||
completedDeals: number;
|
||||
requestsByCategory: Array<{
|
||||
categoryId: number;
|
||||
categoryName: string;
|
||||
count: number;
|
||||
}>;
|
||||
}) {
|
||||
this.totalRequests = data.totalRequests;
|
||||
this.activeOffers = data.activeOffers;
|
||||
this.activeOrders = data.activeOrders;
|
||||
this.completedDeals = data.completedDeals;
|
||||
this.requestsByCategory = data.requestsByCategory.map((item) => new CategoryRequestCountDTO(item));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,500 @@
|
||||
import { ObjectType, Field, Int, registerEnumType } from '@nestjs/graphql';
|
||||
import { RequestDomainEntity, RequestType, RequestStatus } from '../../domain/entities/request-domain.entity';
|
||||
import { RequestAttributeValueDomainEntity } from '../../domain/entities/request-attribute-value-domain.entity';
|
||||
import { RequestImageDomainEntity, RequestImageType } from '../../domain/entities/request-image-domain.entity';
|
||||
import { CategoryDTO } from './category-tree.dto';
|
||||
import { ProductTypeDTO } from './category-tree.dto';
|
||||
import { AttributeDTO } from './attribute.dto';
|
||||
|
||||
// Регистрируем enums для GraphQL
|
||||
registerEnumType(RequestType, {
|
||||
name: 'RequestType',
|
||||
description: 'Тип заявки',
|
||||
});
|
||||
|
||||
registerEnumType(RequestStatus, {
|
||||
name: 'RequestStatus',
|
||||
description: 'Статус заявки',
|
||||
});
|
||||
|
||||
registerEnumType(RequestImageType, {
|
||||
name: 'RequestImageType',
|
||||
description: 'Тип изображения заявки',
|
||||
});
|
||||
|
||||
/**
|
||||
* DTO для значения атрибута заявки
|
||||
*/
|
||||
@ObjectType('MarketplaceRequestAttributeValue')
|
||||
export class RequestAttributeValueDTO {
|
||||
@Field(() => Int, { description: 'ID значения атрибута' })
|
||||
id!: number;
|
||||
|
||||
@Field(() => Int, { description: 'ID атрибута' })
|
||||
attributeId!: number;
|
||||
|
||||
@Field(() => AttributeDTO, { description: 'Информация об атрибуте' })
|
||||
attribute!: AttributeDTO;
|
||||
|
||||
@Field(() => Int, { description: 'ID комплексного атрибута' })
|
||||
complexId!: number;
|
||||
|
||||
@Field({ description: 'Значение атрибута' })
|
||||
value!: string;
|
||||
|
||||
@Field(() => Int, { description: 'ID значения из словаря', nullable: true })
|
||||
dictionaryValueId?: number;
|
||||
|
||||
@Field({ description: 'Является ли атрибут обязательным' })
|
||||
isRequired!: boolean;
|
||||
|
||||
@Field({ description: 'Является ли атрибут аспектным' })
|
||||
isAspect!: boolean;
|
||||
|
||||
@Field({ description: 'Тип атрибута' })
|
||||
attributeType!: string;
|
||||
|
||||
@Field({ description: 'Группа атрибута', nullable: true })
|
||||
attributeGroup?: string;
|
||||
|
||||
@Field({ description: 'Дата создания' })
|
||||
createdAt!: Date;
|
||||
|
||||
constructor(data: {
|
||||
id: number;
|
||||
attributeId: number;
|
||||
attribute: AttributeDTO;
|
||||
complexId: number;
|
||||
value: string;
|
||||
dictionaryValueId?: number;
|
||||
isRequired: boolean;
|
||||
isAspect: boolean;
|
||||
attributeType: string;
|
||||
attributeGroup?: string;
|
||||
createdAt: Date;
|
||||
}) {
|
||||
this.id = data.id;
|
||||
this.attributeId = data.attributeId;
|
||||
this.attribute = data.attribute;
|
||||
this.complexId = data.complexId;
|
||||
this.value = data.value;
|
||||
this.dictionaryValueId = data.dictionaryValueId;
|
||||
this.isRequired = data.isRequired;
|
||||
this.isAspect = data.isAspect;
|
||||
this.attributeType = data.attributeType;
|
||||
this.attributeGroup = data.attributeGroup;
|
||||
this.createdAt = data.createdAt;
|
||||
}
|
||||
|
||||
static fromDomain(entity: RequestAttributeValueDomainEntity): RequestAttributeValueDTO {
|
||||
return new RequestAttributeValueDTO({
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
id: entity.id!,
|
||||
attributeId: entity.attributeId,
|
||||
attribute: AttributeDTO.fromDomain(entity.attribute),
|
||||
complexId: entity.complexId,
|
||||
value: entity.value,
|
||||
dictionaryValueId: entity.dictionaryValueId,
|
||||
isRequired: entity.isRequired(),
|
||||
isAspect: entity.isAspect(),
|
||||
attributeType: entity.getAttributeType(),
|
||||
attributeGroup: entity.getAttributeGroup(),
|
||||
createdAt: entity.createdAt,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DTO для изображения заявки
|
||||
*/
|
||||
@ObjectType('MarketplaceRequestImage')
|
||||
export class RequestImageDTO {
|
||||
@Field(() => Int, { description: 'ID изображения' })
|
||||
id!: number;
|
||||
|
||||
@Field({ description: 'URL изображения' })
|
||||
imageUrl!: string;
|
||||
|
||||
@Field(() => RequestImageType, { description: 'Тип изображения' })
|
||||
imageType!: RequestImageType;
|
||||
|
||||
@Field(() => Int, { description: 'Порядок сортировки' })
|
||||
sortOrder!: number;
|
||||
|
||||
@Field({ description: 'Описание изображения', nullable: true })
|
||||
description?: string;
|
||||
|
||||
@Field({ description: 'Является ли главным изображением' })
|
||||
isPrimary!: boolean;
|
||||
|
||||
@Field({ description: 'Является ли образцом цвета' })
|
||||
isColorSample!: boolean;
|
||||
|
||||
@Field({ description: 'Является ли изображением 360°' })
|
||||
is360Image!: boolean;
|
||||
|
||||
@Field({ description: 'Описание типа изображения' })
|
||||
typeDescription!: string;
|
||||
|
||||
@Field({ description: 'Имя файла' })
|
||||
fileName!: string;
|
||||
|
||||
@Field({ description: 'Дата создания' })
|
||||
createdAt!: Date;
|
||||
|
||||
constructor(data: {
|
||||
id: number;
|
||||
imageUrl: string;
|
||||
imageType: RequestImageType;
|
||||
sortOrder: number;
|
||||
description?: string;
|
||||
isPrimary: boolean;
|
||||
isColorSample: boolean;
|
||||
is360Image: boolean;
|
||||
typeDescription: string;
|
||||
fileName: string;
|
||||
createdAt: Date;
|
||||
}) {
|
||||
this.id = data.id;
|
||||
this.imageUrl = data.imageUrl;
|
||||
this.imageType = data.imageType;
|
||||
this.sortOrder = data.sortOrder;
|
||||
this.description = data.description;
|
||||
this.isPrimary = data.isPrimary;
|
||||
this.isColorSample = data.isColorSample;
|
||||
this.is360Image = data.is360Image;
|
||||
this.typeDescription = data.typeDescription;
|
||||
this.fileName = data.fileName;
|
||||
this.createdAt = data.createdAt;
|
||||
}
|
||||
|
||||
static fromDomain(entity: RequestImageDomainEntity): RequestImageDTO {
|
||||
return new RequestImageDTO({
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
id: entity.id!,
|
||||
imageUrl: entity.imageUrl,
|
||||
imageType: entity.imageType,
|
||||
sortOrder: entity.sortOrder,
|
||||
description: entity.description,
|
||||
isPrimary: entity.isPrimary(),
|
||||
isColorSample: entity.isColorSample(),
|
||||
is360Image: entity.is360Image(),
|
||||
typeDescription: entity.getTypeDescription(),
|
||||
fileName: entity.getFileName(),
|
||||
createdAt: entity.createdAt,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DTO для заявки marketplace
|
||||
*/
|
||||
@ObjectType('MarketplaceRequest')
|
||||
export class RequestDTO {
|
||||
@Field(() => Int, { description: 'ID заявки' })
|
||||
id!: number;
|
||||
|
||||
@Field({ description: 'Уникальный хэш заявки' })
|
||||
hash!: string;
|
||||
|
||||
@Field({ description: 'Хэш родительской заявки', nullable: true })
|
||||
parentHash?: string;
|
||||
|
||||
@Field({ description: 'Название кооператива' })
|
||||
coopname!: string;
|
||||
|
||||
@Field({ description: 'Имя пользователя' })
|
||||
username!: string;
|
||||
|
||||
@Field(() => RequestType, { description: 'Тип заявки' })
|
||||
type!: RequestType;
|
||||
|
||||
@Field(() => RequestStatus, { description: 'Статус заявки' })
|
||||
status!: RequestStatus;
|
||||
|
||||
// Основная информация о товаре
|
||||
@Field({ description: 'Название товара' })
|
||||
name!: string;
|
||||
|
||||
@Field({ description: 'Артикул товара' })
|
||||
articleNumber!: string;
|
||||
|
||||
@Field({ description: 'Штрихкод товара', nullable: true })
|
||||
barcode?: string;
|
||||
|
||||
// Категория и тип
|
||||
@Field(() => Int, { description: 'ID категории' })
|
||||
descriptionCategoryId!: number;
|
||||
|
||||
@Field(() => Int, { description: 'ID типа товара' })
|
||||
typeId!: number;
|
||||
|
||||
@Field(() => CategoryDTO, { description: 'Информация о категории' })
|
||||
category!: CategoryDTO;
|
||||
|
||||
@Field(() => ProductTypeDTO, { description: 'Информация о типе товара' })
|
||||
productType!: ProductTypeDTO;
|
||||
|
||||
// Цены
|
||||
@Field(() => Number, { description: 'Цена товара' })
|
||||
price!: number;
|
||||
|
||||
@Field(() => Number, { description: 'Цена до скидки', nullable: true })
|
||||
oldPrice?: number;
|
||||
|
||||
@Field({ description: 'Валюта' })
|
||||
currencyCode!: string;
|
||||
|
||||
@Field({ description: 'Ставка НДС' })
|
||||
vat!: string;
|
||||
|
||||
// Вычисляемые поля для цен
|
||||
@Field(() => Number, { description: 'Цена как число' })
|
||||
priceAsNumber!: number;
|
||||
|
||||
@Field(() => Number, { description: 'Старая цена как число', nullable: true })
|
||||
oldPriceAsNumber?: number;
|
||||
|
||||
@Field({ description: 'Есть ли скидка' })
|
||||
hasDiscount!: boolean;
|
||||
|
||||
@Field(() => Number, { description: 'Процент скидки' })
|
||||
discountPercentage!: number;
|
||||
|
||||
// Габариты и вес
|
||||
@Field(() => Int, { description: 'Ширина упаковки', nullable: true })
|
||||
width?: number;
|
||||
|
||||
@Field(() => Int, { description: 'Высота упаковки', nullable: true })
|
||||
height?: number;
|
||||
|
||||
@Field(() => Int, { description: 'Глубина упаковки', nullable: true })
|
||||
depth?: number;
|
||||
|
||||
@Field({ description: 'Единица измерения габаритов', nullable: true })
|
||||
dimensionUnit?: string;
|
||||
|
||||
@Field(() => Int, { description: 'Вес товара', nullable: true })
|
||||
weight?: number;
|
||||
|
||||
@Field({ description: 'Единица измерения веса', nullable: true })
|
||||
weightUnit?: string;
|
||||
|
||||
// Количества
|
||||
@Field(() => Int, { description: 'Общее количество единиц' })
|
||||
units!: number;
|
||||
|
||||
@Field(() => Int, { description: 'Доступное количество единиц' })
|
||||
availableUnits!: number;
|
||||
|
||||
@Field(() => Int, { description: 'Количество проданных единиц' })
|
||||
settledUnits!: number;
|
||||
|
||||
// Время жизни и гарантии
|
||||
@Field(() => Int, { description: 'Время жизни продукта в секундах', nullable: true })
|
||||
productLifecycleSecs?: number;
|
||||
|
||||
@Field(() => Int, { description: 'Гарантийный срок в днях', nullable: true })
|
||||
warrantyDays?: number;
|
||||
|
||||
// Дополнительные данные
|
||||
@Field({ description: 'Дополнительные данные', nullable: true })
|
||||
data?: string;
|
||||
|
||||
@Field({ description: 'Метаданные', nullable: true })
|
||||
meta?: string;
|
||||
|
||||
// Связанные сущности
|
||||
@Field(() => [RequestAttributeValueDTO], { description: 'Атрибуты заявки' })
|
||||
attributes!: RequestAttributeValueDTO[];
|
||||
|
||||
@Field(() => [RequestImageDTO], { description: 'Изображения заявки' })
|
||||
images!: RequestImageDTO[];
|
||||
|
||||
@Field({ description: 'URL главного изображения', nullable: true })
|
||||
primaryImageUrl?: string;
|
||||
|
||||
@Field({ description: 'URL образца цвета', nullable: true })
|
||||
colorImageUrl?: string;
|
||||
|
||||
// Геоограничения
|
||||
@Field(() => [String], { description: 'Геоограничения' })
|
||||
geoNames!: string[];
|
||||
|
||||
// Статусы и проверки
|
||||
@Field({ description: 'Является ли заявка активной' })
|
||||
isActive!: boolean;
|
||||
|
||||
@Field({ description: 'Можно ли редактировать заявку' })
|
||||
canBeEdited!: boolean;
|
||||
|
||||
@Field({ description: 'Является ли предложением' })
|
||||
isOffer!: boolean;
|
||||
|
||||
@Field({ description: 'Является ли заказом' })
|
||||
isOrder!: boolean;
|
||||
|
||||
@Field({ description: 'Заполнены ли все обязательные атрибуты' })
|
||||
hasAllRequiredAttributes!: boolean;
|
||||
|
||||
// Временные метки
|
||||
@Field({ description: 'Дата создания' })
|
||||
createdAt!: Date;
|
||||
|
||||
@Field({ description: 'Дата обновления' })
|
||||
updatedAt!: Date;
|
||||
|
||||
constructor(data: {
|
||||
id: number;
|
||||
hash: string;
|
||||
parentHash?: string;
|
||||
coopname: string;
|
||||
username: string;
|
||||
type: RequestType;
|
||||
status: RequestStatus;
|
||||
name: string;
|
||||
articleNumber: string;
|
||||
barcode?: string;
|
||||
descriptionCategoryId: number;
|
||||
typeId: number;
|
||||
category: CategoryDTO;
|
||||
productType: ProductTypeDTO;
|
||||
price: number;
|
||||
oldPrice?: number;
|
||||
currencyCode: string;
|
||||
vat: string;
|
||||
priceAsNumber: number;
|
||||
oldPriceAsNumber?: number;
|
||||
hasDiscount: boolean;
|
||||
discountPercentage: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
depth?: number;
|
||||
dimensionUnit?: string;
|
||||
weight?: number;
|
||||
weightUnit?: string;
|
||||
units: number;
|
||||
availableUnits: number;
|
||||
settledUnits: number;
|
||||
productLifecycleSecs?: number;
|
||||
warrantyDays?: number;
|
||||
data?: string;
|
||||
meta?: string;
|
||||
attributes: RequestAttributeValueDTO[];
|
||||
images: RequestImageDTO[];
|
||||
primaryImageUrl?: string;
|
||||
colorImageUrl?: string;
|
||||
geoNames: string[];
|
||||
isActive: boolean;
|
||||
canBeEdited: boolean;
|
||||
isOffer: boolean;
|
||||
isOrder: boolean;
|
||||
hasAllRequiredAttributes: boolean;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}) {
|
||||
this.id = data.id;
|
||||
this.hash = data.hash;
|
||||
this.parentHash = data.parentHash;
|
||||
this.coopname = data.coopname;
|
||||
this.username = data.username;
|
||||
this.type = data.type;
|
||||
this.status = data.status;
|
||||
this.name = data.name;
|
||||
this.articleNumber = data.articleNumber;
|
||||
this.barcode = data.barcode;
|
||||
this.descriptionCategoryId = data.descriptionCategoryId;
|
||||
this.typeId = data.typeId;
|
||||
this.category = data.category;
|
||||
this.productType = data.productType;
|
||||
this.price = data.price;
|
||||
this.oldPrice = data.oldPrice;
|
||||
this.currencyCode = data.currencyCode;
|
||||
this.vat = data.vat;
|
||||
this.priceAsNumber = data.priceAsNumber;
|
||||
this.oldPriceAsNumber = data.oldPriceAsNumber;
|
||||
this.hasDiscount = data.hasDiscount;
|
||||
this.discountPercentage = data.discountPercentage;
|
||||
this.width = data.width;
|
||||
this.height = data.height;
|
||||
this.depth = data.depth;
|
||||
this.dimensionUnit = data.dimensionUnit;
|
||||
this.weight = data.weight;
|
||||
this.weightUnit = data.weightUnit;
|
||||
this.units = data.units;
|
||||
this.availableUnits = data.availableUnits;
|
||||
this.settledUnits = data.settledUnits;
|
||||
this.productLifecycleSecs = data.productLifecycleSecs;
|
||||
this.warrantyDays = data.warrantyDays;
|
||||
this.data = data.data;
|
||||
this.meta = data.meta;
|
||||
this.attributes = data.attributes;
|
||||
this.images = data.images;
|
||||
this.primaryImageUrl = data.primaryImageUrl;
|
||||
this.colorImageUrl = data.colorImageUrl;
|
||||
this.geoNames = data.geoNames;
|
||||
this.isActive = data.isActive;
|
||||
this.canBeEdited = data.canBeEdited;
|
||||
this.isOffer = data.isOffer;
|
||||
this.isOrder = data.isOrder;
|
||||
this.hasAllRequiredAttributes = data.hasAllRequiredAttributes;
|
||||
this.createdAt = data.createdAt;
|
||||
this.updatedAt = data.updatedAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Создать DTO из доменной сущности
|
||||
*/
|
||||
static fromDomain(entity: RequestDomainEntity): RequestDTO {
|
||||
return new RequestDTO({
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
id: entity.id!,
|
||||
hash: entity.hash,
|
||||
parentHash: entity.parentHash,
|
||||
coopname: entity.coopname,
|
||||
username: entity.username,
|
||||
type: entity.type,
|
||||
status: entity.status,
|
||||
name: entity.name,
|
||||
articleNumber: entity.articleNumber,
|
||||
barcode: entity.barcode,
|
||||
descriptionCategoryId: entity.descriptionCategoryId,
|
||||
typeId: entity.typeId,
|
||||
category: CategoryDTO.fromDomain(entity.category),
|
||||
productType: ProductTypeDTO.fromDomain(entity.productType),
|
||||
price: entity.price,
|
||||
oldPrice: entity.oldPrice,
|
||||
currencyCode: entity.currencyCode,
|
||||
vat: entity.vat,
|
||||
priceAsNumber: entity.getPriceAsNumber(),
|
||||
oldPriceAsNumber: entity.getOldPriceAsNumber() || undefined,
|
||||
hasDiscount: entity.hasDiscount(),
|
||||
discountPercentage: entity.getDiscountPercentage(),
|
||||
width: entity.width,
|
||||
height: entity.height,
|
||||
depth: entity.depth,
|
||||
dimensionUnit: entity.dimensionUnit,
|
||||
weight: entity.weight,
|
||||
weightUnit: entity.weightUnit,
|
||||
units: entity.units,
|
||||
availableUnits: entity.availableUnits,
|
||||
settledUnits: entity.settledUnits,
|
||||
productLifecycleSecs: entity.productLifecycleSecs,
|
||||
warrantyDays: entity.warrantyDays,
|
||||
data: entity.data,
|
||||
meta: entity.meta,
|
||||
attributes: entity.attributes.map((attr) => RequestAttributeValueDTO.fromDomain(attr)),
|
||||
images: entity.images.map((img) => RequestImageDTO.fromDomain(img)),
|
||||
primaryImageUrl: entity.primaryImageUrl,
|
||||
colorImageUrl: entity.colorImageUrl,
|
||||
geoNames: entity.geoNames,
|
||||
isActive: entity.isActive(),
|
||||
canBeEdited: entity.canBeEdited(),
|
||||
isOffer: entity.isOffer(),
|
||||
isOrder: entity.isOrder(),
|
||||
hasAllRequiredAttributes: entity.hasAllRequiredAttributes(),
|
||||
createdAt: entity.createdAt,
|
||||
updatedAt: entity.updatedAt,
|
||||
});
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { InputType, Field, Int } from '@nestjs/graphql';
|
||||
import { IsOptional, IsNumber, IsString, Min, Max } from 'class-validator';
|
||||
|
||||
/**
|
||||
* Input для поиска атрибутов
|
||||
*/
|
||||
@InputType()
|
||||
export class SearchAttributesInput {
|
||||
@Field({ description: 'Текст для поиска' })
|
||||
@IsString()
|
||||
searchTerm!: string;
|
||||
|
||||
@Field(() => Int, { description: 'ID категории для фильтрации', nullable: true })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
categoryId?: number;
|
||||
|
||||
@Field(() => Int, { description: 'ID типа товара для фильтрации', nullable: true })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
typeId?: number;
|
||||
|
||||
@Field({ description: 'Только обязательные', nullable: true, defaultValue: false })
|
||||
@IsOptional()
|
||||
onlyRequired?: boolean;
|
||||
|
||||
@Field({ description: 'Только аспектные', nullable: true, defaultValue: false })
|
||||
@IsOptional()
|
||||
onlyAspect?: boolean;
|
||||
|
||||
@Field({ description: 'Только со словарями', nullable: true, defaultValue: false })
|
||||
@IsOptional()
|
||||
onlyWithDictionary?: boolean;
|
||||
|
||||
@Field(() => Int, { description: 'Максимальное количество результатов', nullable: true, defaultValue: 50 })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
@Max(100)
|
||||
limit?: number;
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { InputType, Field } from '@nestjs/graphql';
|
||||
import { IsString } from 'class-validator';
|
||||
|
||||
/**
|
||||
* Input для поиска категорий
|
||||
*/
|
||||
@InputType()
|
||||
export class SearchCategoriesInput {
|
||||
@Field({ description: 'Текст для поиска по категориям и типам товаров' })
|
||||
@IsString()
|
||||
searchTerm!: string;
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import { InputType, Field, Int } from '@nestjs/graphql';
|
||||
import { IsOptional, IsNumber, IsString, Min, Max } from 'class-validator';
|
||||
|
||||
/**
|
||||
* Input для поиска значений словаря
|
||||
*/
|
||||
@InputType()
|
||||
export class SearchDictionaryValuesInput {
|
||||
@Field(() => Int, { description: 'ID словаря' })
|
||||
@IsNumber()
|
||||
dictionaryId!: number;
|
||||
|
||||
@Field({ description: 'Текст для поиска' })
|
||||
@IsString()
|
||||
searchTerm!: string;
|
||||
|
||||
@Field(() => Int, { description: 'Максимальное количество результатов', nullable: true, defaultValue: 50 })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
@Max(100)
|
||||
limit?: number;
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { InputType, Field, Int } from '@nestjs/graphql';
|
||||
import { IsOptional, IsString, IsNumber, Min } from 'class-validator';
|
||||
|
||||
/**
|
||||
* Input для поиска заявок
|
||||
*/
|
||||
@InputType()
|
||||
export class SearchRequestsInput {
|
||||
@Field({ description: 'Текст для поиска по названию товара' })
|
||||
@IsString()
|
||||
searchTerm!: string;
|
||||
|
||||
@Field(() => Int, { description: 'Максимальное количество результатов', nullable: true, defaultValue: 20 })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
limit?: number;
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { InputType, Field, Int } from '@nestjs/graphql';
|
||||
import { IsNumber } from 'class-validator';
|
||||
|
||||
/**
|
||||
* Input для валидации значений атрибута
|
||||
*/
|
||||
@InputType()
|
||||
export class ValidateAttributeValuesInput {
|
||||
@Field(() => Int, { description: 'ID атрибута' })
|
||||
@IsNumber()
|
||||
attributeId!: number;
|
||||
|
||||
@Field(() => [String], { description: 'Значения для валидации' })
|
||||
values!: string[];
|
||||
}
|
||||
+212
@@ -0,0 +1,212 @@
|
||||
import { Resolver, Query, Args } from '@nestjs/graphql';
|
||||
import { Injectable, UseGuards } from '@nestjs/common';
|
||||
import { GqlJwtAuthGuard } from '~/modules/auth/guards/graphql-jwt-auth.guard';
|
||||
import { RolesGuard } from '~/modules/auth/guards/roles.guard';
|
||||
import { AuthRoles } from '~/modules/auth/decorators/auth.decorator';
|
||||
import { AttributeDomainService } from '../../domain/services/attribute-domain.service';
|
||||
import { AttributeDTO, AttributeGroupDTO, AttributeStatsDTO, DictionaryValueDTO } from '../dto/attribute.dto';
|
||||
import { SearchAttributesInput } from '../dto/search-attributes-input.dto';
|
||||
import { GetCategoryAttributesInput } from '../dto/get-category-attributes-input.dto';
|
||||
import { SearchDictionaryValuesInput } from '../dto/search-dictionary-values-input.dto';
|
||||
import { ValidateAttributeValuesInput } from '../dto/validate-attribute-values-input.dto';
|
||||
import { GetRequiredAttributesInput } from '../dto/get-required-attributes-input.dto';
|
||||
|
||||
/**
|
||||
* Результат валидации атрибута
|
||||
*/
|
||||
import { ObjectType, Field } from '@nestjs/graphql';
|
||||
|
||||
@ObjectType('MarketplaceAttributeValidation')
|
||||
export class AttributeValidationResult {
|
||||
@Field({ description: 'Результат валидации' })
|
||||
isValid!: boolean;
|
||||
|
||||
@Field(() => [String], { description: 'Список ошибок' })
|
||||
errors!: string[];
|
||||
|
||||
constructor(data: { isValid: boolean; errors: string[] }) {
|
||||
this.isValid = data.isValid;
|
||||
this.errors = data.errors;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GraphQL резолвер для атрибутов marketplace
|
||||
*/
|
||||
@Resolver(() => AttributeDTO)
|
||||
@Injectable()
|
||||
export class AttributeResolver {
|
||||
constructor(private readonly attributeService: AttributeDomainService) {}
|
||||
|
||||
/**
|
||||
* Получить атрибуты для категории и типа товара
|
||||
*/
|
||||
@Query(() => [AttributeDTO], {
|
||||
name: 'marketplaceCategoryAttributes',
|
||||
description: 'Получить атрибуты для конкретной категории и типа товара marketplace',
|
||||
})
|
||||
async getCategoryAttributes(
|
||||
@Args('input', { type: () => GetCategoryAttributesInput })
|
||||
input: GetCategoryAttributesInput
|
||||
): Promise<AttributeDTO[]> {
|
||||
const result = await this.attributeService.getAttributesForCategoryTypeWithFilters({
|
||||
categoryId: input.categoryId,
|
||||
typeId: input.typeId,
|
||||
includeDictionaryValues: input.includeDictionaryValues,
|
||||
onlyRequired: input.onlyRequired,
|
||||
});
|
||||
|
||||
return result.attributes.map((attr) => AttributeDTO.fromDomain(attr, input.includeDictionaryValues));
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить группированные атрибуты для категории и типа товара
|
||||
*/
|
||||
@Query(() => [AttributeGroupDTO], {
|
||||
name: 'marketplaceCategoryAttributesGrouped',
|
||||
description: 'Получить группированные атрибуты для категории и типа товара marketplace',
|
||||
})
|
||||
async getCategoryAttributesGrouped(
|
||||
@Args('input', { type: () => GetCategoryAttributesInput })
|
||||
input: GetCategoryAttributesInput
|
||||
): Promise<AttributeGroupDTO[]> {
|
||||
// Получаем отфильтрованные атрибуты через доменный сервис
|
||||
const result = await this.attributeService.getAttributesForCategoryTypeWithFilters({
|
||||
categoryId: input.categoryId,
|
||||
typeId: input.typeId,
|
||||
includeDictionaryValues: input.includeDictionaryValues,
|
||||
onlyRequired: input.onlyRequired,
|
||||
});
|
||||
|
||||
// Группируем атрибуты
|
||||
const groupedAttributes = new Map<string, typeof result.attributes>();
|
||||
for (const attribute of result.attributes) {
|
||||
const groupName = attribute.groupName || 'Без группы';
|
||||
if (!groupedAttributes.has(groupName)) {
|
||||
groupedAttributes.set(groupName, []);
|
||||
}
|
||||
groupedAttributes.get(groupName)?.push(attribute);
|
||||
}
|
||||
|
||||
// Преобразуем в DTO
|
||||
const resultGroups: AttributeGroupDTO[] = [];
|
||||
for (const [groupName, attributes] of groupedAttributes.entries()) {
|
||||
if (attributes.length > 0) {
|
||||
const groupId = attributes[0].groupId;
|
||||
resultGroups.push(
|
||||
new AttributeGroupDTO({
|
||||
groupId,
|
||||
groupName,
|
||||
attributes: attributes.map((attr) => AttributeDTO.fromDomain(attr, input.includeDictionaryValues)),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return resultGroups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить обязательные атрибуты для категории и типа
|
||||
*/
|
||||
@Query(() => [AttributeDTO], {
|
||||
name: 'marketplaceRequiredAttributes',
|
||||
description: 'Получить обязательные атрибуты для категории и типа товара marketplace',
|
||||
})
|
||||
async getRequiredAttributes(
|
||||
@Args('data', { type: () => GetRequiredAttributesInput })
|
||||
data: GetRequiredAttributesInput
|
||||
): Promise<AttributeDTO[]> {
|
||||
const attributes = await this.attributeService.getRequiredAttributes(data.categoryId, data.typeId);
|
||||
return attributes.map((attr) => AttributeDTO.fromDomain(attr));
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить аспектные атрибуты для категории и типа
|
||||
*/
|
||||
@Query(() => [AttributeDTO], {
|
||||
name: 'marketplaceAspectAttributes',
|
||||
description: 'Получить аспектные атрибуты для категории и типа товара marketplace',
|
||||
})
|
||||
async getAspectAttributes(
|
||||
@Args('data', { type: () => GetRequiredAttributesInput })
|
||||
data: GetRequiredAttributesInput
|
||||
): Promise<AttributeDTO[]> {
|
||||
const attributes = await this.attributeService.getAspectAttributes(data.categoryId, data.typeId);
|
||||
return attributes.map((attr) => AttributeDTO.fromDomain(attr));
|
||||
}
|
||||
|
||||
/**
|
||||
* Поиск атрибутов
|
||||
*/
|
||||
@Query(() => [AttributeDTO], {
|
||||
name: 'marketplaceSearchAttributes',
|
||||
description: 'Поиск атрибутов marketplace по названию',
|
||||
})
|
||||
async searchAttributes(
|
||||
@Args('input', { type: () => SearchAttributesInput })
|
||||
input: SearchAttributesInput
|
||||
): Promise<AttributeDTO[]> {
|
||||
const attributes = await this.attributeService.searchAttributesWithFilters({
|
||||
searchTerm: input.searchTerm,
|
||||
categoryId: input.categoryId,
|
||||
typeId: input.typeId,
|
||||
onlyRequired: input.onlyRequired,
|
||||
onlyAspect: input.onlyAspect,
|
||||
onlyWithDictionary: input.onlyWithDictionary,
|
||||
limit: input.limit,
|
||||
});
|
||||
|
||||
return attributes.map((attr) => AttributeDTO.fromDomain(attr));
|
||||
}
|
||||
|
||||
/**
|
||||
* Поиск значений словаря
|
||||
*/
|
||||
@Query(() => [DictionaryValueDTO], {
|
||||
name: 'marketplaceSearchDictionaryValues',
|
||||
description: 'Поиск значений словаря marketplace',
|
||||
})
|
||||
async searchDictionaryValues(
|
||||
@Args('input', { type: () => SearchDictionaryValuesInput })
|
||||
input: SearchDictionaryValuesInput
|
||||
): Promise<DictionaryValueDTO[]> {
|
||||
const values = await this.attributeService.searchDictionaryValues(
|
||||
input.dictionaryId,
|
||||
input.searchTerm,
|
||||
input.limit || 50
|
||||
);
|
||||
|
||||
return values.map((value) => DictionaryValueDTO.fromDomain(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Валидация значений атрибута
|
||||
*/
|
||||
@Query(() => AttributeValidationResult, {
|
||||
name: 'marketplaceValidateAttributeValues',
|
||||
description: 'Валидация значений атрибута marketplace',
|
||||
})
|
||||
async validateAttributeValues(
|
||||
@Args('input', { type: () => ValidateAttributeValuesInput })
|
||||
input: ValidateAttributeValuesInput
|
||||
): Promise<AttributeValidationResult> {
|
||||
const result = await this.attributeService.validateAttributeValues(input.attributeId, input.values);
|
||||
|
||||
return new AttributeValidationResult(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить статистику по атрибутам
|
||||
*/
|
||||
@Query(() => AttributeStatsDTO, {
|
||||
name: 'marketplaceAttributeStats',
|
||||
description: 'Получить статистику по атрибутам marketplace',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['chairman', 'member'])
|
||||
async getAttributeStats(): Promise<AttributeStatsDTO> {
|
||||
const stats = await this.attributeService.getAttributeStats();
|
||||
return new AttributeStatsDTO(stats);
|
||||
}
|
||||
}
|
||||
+244
@@ -0,0 +1,244 @@
|
||||
import { Resolver, Query, Mutation, Args, Int } from '@nestjs/graphql';
|
||||
import { Injectable, Inject, UseGuards } from '@nestjs/common';
|
||||
import {
|
||||
AvailableCategoryDomainService,
|
||||
AVAILABLE_CATEGORY_DOMAIN_SERVICE,
|
||||
} from '../../domain/services/available-category-domain.service';
|
||||
import { CategoryTreeDomainService, CATEGORY_TREE_DOMAIN_SERVICE } from '../../domain/services/category-tree-domain.service';
|
||||
import { CategoryDTO } from '../dto/category-tree.dto';
|
||||
import { AvailableCategoryDTO } from '../dto/available-category.dto';
|
||||
import { AvailabilityStatsDTO } from '../dto/availability-stats.dto';
|
||||
import { AddAvailableCategoriesInput } from '../dto/add-available-categories-input.dto';
|
||||
import { AddAvailableCategoryTypesInput } from '../dto/add-available-category-types-input.dto';
|
||||
import { RemoveAvailableCategoriesInput } from '../dto/remove-available-categories-input.dto';
|
||||
import { RemoveAvailableCategoryTypesInput } from '../dto/remove-available-category-types-input.dto';
|
||||
import { ReplaceAvailableItemsInput } from '../dto/replace-available-items-input.dto';
|
||||
import { GqlJwtAuthGuard } from '~/modules/auth/guards/graphql-jwt-auth.guard';
|
||||
import { RolesGuard } from '~/modules/auth/guards/roles.guard';
|
||||
import { AuthRoles } from '~/modules/auth/decorators/auth.decorator';
|
||||
import { CurrentUser } from '~/modules/auth/decorators/current-user.decorator';
|
||||
import type { MonoAccountDomainInterface } from '~/domain/account/interfaces/mono-account-domain.interface';
|
||||
import config from '~/config/config';
|
||||
|
||||
/**
|
||||
* GraphQL резолвер для администрирования доступных категорий и типов товаров marketplace
|
||||
*/
|
||||
@Resolver(() => AvailableCategoryDTO)
|
||||
@Injectable()
|
||||
export class AvailableCategoryAdminResolver {
|
||||
constructor(
|
||||
@Inject(AVAILABLE_CATEGORY_DOMAIN_SERVICE)
|
||||
private readonly availableCategoryService: AvailableCategoryDomainService,
|
||||
@Inject(CATEGORY_TREE_DOMAIN_SERVICE)
|
||||
private readonly categoryTreeService: CategoryTreeDomainService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Получить все доступные категории и типы для текущего кооператива
|
||||
*/
|
||||
@Query(() => [AvailableCategoryDTO], {
|
||||
name: 'marketplaceGetAvailableCategories',
|
||||
description: 'Получить все доступные категории и типы для кооператива',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['chairman'])
|
||||
async getAvailableCategories(): Promise<AvailableCategoryDTO[]> {
|
||||
const availableCategories = await this.availableCategoryService.getAvailableCategories(config.coopname);
|
||||
|
||||
return availableCategories.map((cat) => ({
|
||||
id: cat.id,
|
||||
coopname: cat.coopname,
|
||||
categoryId: cat.categoryId,
|
||||
typeId: cat.typeId,
|
||||
isActive: cat.isActive,
|
||||
addedBy: cat.addedBy,
|
||||
isForEntireCategory: cat.isForEntireCategory(),
|
||||
isForSpecificType: cat.isForSpecificType(),
|
||||
createdAt: cat.createdAt,
|
||||
updatedAt: cat.updatedAt,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить дерево доступных категорий и типов
|
||||
*/
|
||||
@Query(() => [CategoryDTO], {
|
||||
name: 'marketplaceGetAvailableCategoryTree',
|
||||
description: 'Получить дерево доступных категорий и типов для кооператива',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['chairman'])
|
||||
async getAvailableCategoryTree(): Promise<CategoryDTO[]> {
|
||||
const availableTree = await this.categoryTreeService.buildAvailableCategoryTree(config.coopname);
|
||||
return availableTree.map((category) => CategoryDTO.fromDomain(category));
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить статистику по доступности категорий
|
||||
*/
|
||||
@Query(() => AvailabilityStatsDTO, {
|
||||
name: 'marketplaceGetAvailabilityStats',
|
||||
description: 'Получить статистику по доступности категорий в кооперативе',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['chairman'])
|
||||
async getAvailabilityStats(): Promise<AvailabilityStatsDTO> {
|
||||
const stats = await this.availableCategoryService.getAvailabilityStats(config.coopname);
|
||||
return {
|
||||
totalAvailable: stats.totalAvailable,
|
||||
categoriesCount: stats.categoriesCount,
|
||||
typesCount: stats.typesCount,
|
||||
hasRestrictions: stats.hasRestrictions,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Добавить категории в доступные (целые категории)
|
||||
*/
|
||||
@Mutation(() => [AvailableCategoryDTO], {
|
||||
name: 'marketplaceAddAvailableCategories',
|
||||
description: 'Добавить категории в доступные для кооператива (целые категории)',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['chairman'])
|
||||
async addAvailableCategories(
|
||||
@Args('input', { type: () => AddAvailableCategoriesInput })
|
||||
input: AddAvailableCategoriesInput,
|
||||
@CurrentUser() currentUser: MonoAccountDomainInterface
|
||||
): Promise<AvailableCategoryDTO[]> {
|
||||
const availableCategories = await this.availableCategoryService.addMultipleCategories(
|
||||
config.coopname,
|
||||
input.categoryIds,
|
||||
currentUser?.username ?? 'system'
|
||||
);
|
||||
|
||||
return this.mapToDTO(availableCategories);
|
||||
}
|
||||
|
||||
/**
|
||||
* Добавить типы товаров в доступные
|
||||
*/
|
||||
@Mutation(() => [AvailableCategoryDTO], {
|
||||
name: 'marketplaceAddAvailableCategoryTypes',
|
||||
description: 'Добавить конкретные типы товаров в доступные для кооператива',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['chairman'])
|
||||
async addAvailableCategoryTypes(
|
||||
@Args('input', { type: () => AddAvailableCategoryTypesInput })
|
||||
input: AddAvailableCategoryTypesInput,
|
||||
@CurrentUser() currentUser: MonoAccountDomainInterface
|
||||
): Promise<AvailableCategoryDTO[]> {
|
||||
const availableCategories = await this.availableCategoryService.addMultipleCategoryTypes(
|
||||
config.coopname,
|
||||
input.categoryTypes,
|
||||
currentUser?.username ?? 'system'
|
||||
);
|
||||
|
||||
return this.mapToDTO(availableCategories);
|
||||
}
|
||||
|
||||
/**
|
||||
* Удалить категории из доступных
|
||||
*/
|
||||
@Mutation(() => Boolean, {
|
||||
name: 'marketplaceRemoveAvailableCategories',
|
||||
description: 'Удалить категории из доступных для кооператива (включая все их типы)',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['chairman'])
|
||||
async removeAvailableCategories(
|
||||
@Args('input', { type: () => RemoveAvailableCategoriesInput })
|
||||
input: RemoveAvailableCategoriesInput
|
||||
): Promise<boolean> {
|
||||
await this.availableCategoryService.removeMultipleCategories(config.coopname, input.categoryIds);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Удалить типы товаров из доступных
|
||||
*/
|
||||
@Mutation(() => Boolean, {
|
||||
name: 'marketplaceRemoveAvailableCategoryTypes',
|
||||
description: 'Удалить конкретные типы товаров из доступных для кооператива',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['chairman'])
|
||||
async removeAvailableCategoryTypes(
|
||||
@Args('input', { type: () => RemoveAvailableCategoryTypesInput })
|
||||
input: RemoveAvailableCategoryTypesInput
|
||||
): Promise<boolean> {
|
||||
await this.availableCategoryService.removeMultipleCategoryTypes(config.coopname, input.categoryTypes);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Заменить все доступные категории и типы новым списком
|
||||
*/
|
||||
@Mutation(() => [AvailableCategoryDTO], {
|
||||
name: 'marketplaceReplaceAvailableItems',
|
||||
description: 'Заменить все доступные категории и типы новым списком',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['chairman'])
|
||||
async replaceAvailableItems(
|
||||
@Args('input', { type: () => ReplaceAvailableItemsInput })
|
||||
input: ReplaceAvailableItemsInput,
|
||||
@CurrentUser() currentUser: MonoAccountDomainInterface
|
||||
): Promise<AvailableCategoryDTO[]> {
|
||||
const availableCategories = await this.availableCategoryService.replaceAvailableItems(
|
||||
config.coopname,
|
||||
input.categoryIds,
|
||||
input.categoryTypes,
|
||||
currentUser?.username ?? 'system'
|
||||
);
|
||||
|
||||
return this.mapToDTO(availableCategories);
|
||||
}
|
||||
|
||||
/**
|
||||
* Очистить все доступные категории (сделать доступными все)
|
||||
*/
|
||||
@Mutation(() => Boolean, {
|
||||
name: 'marketplaceClearAvailableCategories',
|
||||
description: 'Очистить все доступные категории (сделать доступными все)',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['chairman'])
|
||||
async clearAvailableCategories(@CurrentUser() currentUser: MonoAccountDomainInterface): Promise<boolean> {
|
||||
await this.availableCategoryService.replaceAvailableItems(config.coopname, [], [], currentUser?.username ?? 'system');
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить доступные правила для конкретной категории
|
||||
*/
|
||||
@Query(() => [AvailableCategoryDTO], {
|
||||
name: 'marketplaceGetCategoryRules',
|
||||
description: 'Получить все доступные правила для конкретной категории',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['chairman'])
|
||||
async getCategoryRules(@Args('categoryId', { type: () => Int }) categoryId: number): Promise<AvailableCategoryDTO[]> {
|
||||
const rules = await this.availableCategoryService.getCategoryRules(config.coopname, categoryId);
|
||||
return this.mapToDTO(rules);
|
||||
}
|
||||
|
||||
/**
|
||||
* Вспомогательный метод для маппинга в DTO
|
||||
*/
|
||||
private mapToDTO(availableCategories: any[]): AvailableCategoryDTO[] {
|
||||
return availableCategories.map((cat) => ({
|
||||
id: cat.id,
|
||||
coopname: cat.coopname,
|
||||
categoryId: cat.categoryId,
|
||||
typeId: cat.typeId,
|
||||
isActive: cat.isActive,
|
||||
addedBy: cat.addedBy,
|
||||
isForEntireCategory: cat.isForEntireCategory(),
|
||||
isForSpecificType: cat.isForSpecificType(),
|
||||
createdAt: cat.createdAt,
|
||||
updatedAt: cat.updatedAt,
|
||||
}));
|
||||
}
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
import { Resolver, Query, Args, Int } from '@nestjs/graphql';
|
||||
import { Inject, UseGuards } from '@nestjs/common';
|
||||
import { GqlJwtAuthGuard } from '~/modules/auth/guards/graphql-jwt-auth.guard';
|
||||
import { RolesGuard } from '~/modules/auth/guards/roles.guard';
|
||||
import { AuthRoles } from '~/modules/auth/decorators/auth.decorator';
|
||||
import { CategoryTreeService, CATEGORY_TREE_SERVICE } from '../services/category-tree.service';
|
||||
import { CategoryTreeDomainService, CATEGORY_TREE_DOMAIN_SERVICE } from '../../domain/services/category-tree-domain.service';
|
||||
import { CategoryDTO, ProductTypeDTO } from '../dto/category-tree.dto';
|
||||
import { CategoryTreeStatsDTO } from '../dto/category-tree.dto';
|
||||
import { GetCategoryTreeInput } from '../dto/get-category-tree-input.dto';
|
||||
import { GetCategoryByIdInput } from '../dto/get-category-by-id-input.dto';
|
||||
import { GetProductTypeByIdInput } from '../dto/get-product-type-by-id-input.dto';
|
||||
import { SearchCategoriesInput } from '../dto/search-categories-input.dto';
|
||||
|
||||
/**
|
||||
* GraphQL резолвер для работы с деревом категорий marketplace
|
||||
*/
|
||||
@Resolver(() => CategoryDTO)
|
||||
export class CategoryTreeResolver {
|
||||
constructor(
|
||||
@Inject(CATEGORY_TREE_SERVICE)
|
||||
private readonly categoryTreeService: CategoryTreeService,
|
||||
@Inject(CATEGORY_TREE_DOMAIN_SERVICE)
|
||||
private readonly categoryTreeDomainService: CategoryTreeDomainService
|
||||
) {}
|
||||
|
||||
@Query(() => [CategoryDTO], {
|
||||
name: 'marketplaceGetCategoryTree',
|
||||
description: 'Получить полное дерево категорий marketplace с типами товаров',
|
||||
})
|
||||
async getCategoryTree(
|
||||
@Args('input', { type: () => GetCategoryTreeInput, nullable: true })
|
||||
input?: GetCategoryTreeInput
|
||||
): Promise<CategoryDTO[]> {
|
||||
return this.categoryTreeService.getCategoryTree(input);
|
||||
}
|
||||
|
||||
@Query(() => [CategoryDTO], {
|
||||
name: 'marketplaceGetRootCategories',
|
||||
description: 'Получить все корневые категории marketplace',
|
||||
})
|
||||
async getRootCategories(): Promise<CategoryDTO[]> {
|
||||
return this.categoryTreeService.getRootCategories();
|
||||
}
|
||||
|
||||
@Query(() => CategoryDTO, {
|
||||
name: 'marketplaceGetCategoryById',
|
||||
description: 'Получить категорию marketplace по ID',
|
||||
nullable: true,
|
||||
})
|
||||
async getCategoryById(
|
||||
@Args('data', { type: () => GetCategoryByIdInput })
|
||||
data: GetCategoryByIdInput
|
||||
): Promise<CategoryDTO | null> {
|
||||
return this.categoryTreeService.getCategoryById(data.categoryId);
|
||||
}
|
||||
|
||||
@Query(() => ProductTypeDTO, {
|
||||
name: 'marketplaceGetProductTypeById',
|
||||
description: 'Получить тип товара marketplace по ID',
|
||||
nullable: true,
|
||||
})
|
||||
async getProductTypeById(
|
||||
@Args('data', { type: () => GetProductTypeByIdInput })
|
||||
data: GetProductTypeByIdInput
|
||||
): Promise<ProductTypeDTO | null> {
|
||||
return this.categoryTreeService.getProductTypeById(data.typeId);
|
||||
}
|
||||
|
||||
@Query(() => [CategoryDTO], {
|
||||
name: 'marketplaceGetSearchCategories',
|
||||
description: 'Универсальный поиск по категориям и типам товаров',
|
||||
})
|
||||
async searchCategories(
|
||||
@Args('data', { type: () => SearchCategoriesInput })
|
||||
data: SearchCategoriesInput
|
||||
): Promise<CategoryDTO[]> {
|
||||
return this.categoryTreeService.search(data.searchTerm);
|
||||
}
|
||||
|
||||
@Query(() => CategoryTreeStatsDTO, {
|
||||
name: 'marketplaceGetCategoryTreeStats',
|
||||
description: 'Получить статистику по дереву категорий',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['chairman', 'member'])
|
||||
async getCategoryTreeStats(): Promise<CategoryTreeStatsDTO> {
|
||||
return this.categoryTreeService.getCategoryTreeStats();
|
||||
}
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
import { Resolver, Query, Mutation, Args } from '@nestjs/graphql';
|
||||
import { UseGuards, Inject } 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 { config } from '~/config';
|
||||
import { UpdateMarketplaceSettingsInputDTO, MarketplaceSettingsDTO } from '../dto/marketplace-settings.dto';
|
||||
import { MARKETPLACE_SETTINGS_REPOSITORY, type MarketplaceSettingsRepository } from '../../domain/repositories/marketplace-settings.repository';
|
||||
|
||||
@Resolver(() => MarketplaceSettingsDTO)
|
||||
export class MarketplaceSettingsResolver {
|
||||
constructor(
|
||||
@Inject(MARKETPLACE_SETTINGS_REPOSITORY)
|
||||
private readonly settingsRepo: MarketplaceSettingsRepository,
|
||||
) {}
|
||||
|
||||
@Query(() => MarketplaceSettingsDTO, {
|
||||
name: 'getMarketplaceSettings',
|
||||
description: 'Получить настройки маркетплейса',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard)
|
||||
async getMarketplaceSettings(): Promise<MarketplaceSettingsDTO> {
|
||||
const settings = await this.settingsRepo.findByCoopname(config.coopname);
|
||||
if (settings) return settings as unknown as MarketplaceSettingsDTO;
|
||||
|
||||
return (await this.settingsRepo.upsert({
|
||||
coopname: config.coopname,
|
||||
lead_request_policy: 'both' as any,
|
||||
publish_access_policy: 'all_members' as any,
|
||||
publish_whitelist: [],
|
||||
moderation_required: true,
|
||||
cycles_enabled: true,
|
||||
max_cycle_days: 30,
|
||||
external_delivery_enabled: true,
|
||||
internal_delivery_enabled: true,
|
||||
allowed_category_ids: [],
|
||||
})) as unknown as MarketplaceSettingsDTO;
|
||||
}
|
||||
|
||||
@Mutation(() => MarketplaceSettingsDTO, {
|
||||
name: 'updateMarketplaceSettings',
|
||||
description: 'Обновить настройки маркетплейса (только chairman)',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['chairman'])
|
||||
async updateMarketplaceSettings(
|
||||
@Args('data') data: UpdateMarketplaceSettingsInputDTO,
|
||||
): Promise<MarketplaceSettingsDTO> {
|
||||
return (await this.settingsRepo.upsert({
|
||||
coopname: config.coopname,
|
||||
...data,
|
||||
})) as unknown as MarketplaceSettingsDTO;
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean, {
|
||||
name: 'addToPublishWhitelist',
|
||||
description: 'Добавить пайщика в белый список публикации',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['chairman'])
|
||||
async addToPublishWhitelist(
|
||||
@Args('username', { type: () => String }) username: string,
|
||||
): Promise<boolean> {
|
||||
const settings = await this.settingsRepo.findByCoopname(config.coopname);
|
||||
if (!settings) return false;
|
||||
if (settings.publish_whitelist.includes(username)) return true;
|
||||
await this.settingsRepo.upsert({
|
||||
coopname: config.coopname,
|
||||
publish_whitelist: [...settings.publish_whitelist, username],
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean, {
|
||||
name: 'removeFromPublishWhitelist',
|
||||
description: 'Удалить пайщика из белого списка публикации',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['chairman'])
|
||||
async removeFromPublishWhitelist(
|
||||
@Args('username', { type: () => String }) username: string,
|
||||
): Promise<boolean> {
|
||||
const settings = await this.settingsRepo.findByCoopname(config.coopname);
|
||||
if (!settings) return false;
|
||||
await this.settingsRepo.upsert({
|
||||
coopname: config.coopname,
|
||||
publish_whitelist: settings.publish_whitelist.filter(u => u !== username),
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+280
@@ -0,0 +1,280 @@
|
||||
import { Resolver, Mutation, Query, Args, Int } from '@nestjs/graphql';
|
||||
import { Injectable, Inject, UseGuards } from '@nestjs/common';
|
||||
import { RequestDomainService, REQUEST_DOMAIN_SERVICE } from '../../domain/services/request-domain.service';
|
||||
import { RolesGuard } from '~/modules/auth/guards/roles.guard';
|
||||
import { AuthRoles } from '~/modules/auth/decorators/auth.decorator';
|
||||
import { RequestDTO } from '../dto/request.dto';
|
||||
import { CreateRequestInput, RequestTypeInput, RequestImageTypeInput } from '../dto/create-request-input.dto';
|
||||
import { GetCoopRequestsInput } from '../dto/get-coop-requests-input.dto';
|
||||
import { GetRequestStatisticsInput } from '../dto/get-request-statistics-input.dto';
|
||||
import { RequestStatisticsDTO } from '../dto/request-statistics.dto';
|
||||
import { SearchRequestsInput } from '../dto/search-requests-input.dto';
|
||||
import { GetUserRequestsInput } from '../dto/get-user-requests-input.dto';
|
||||
import { FindPotentialMatchesInput } from '../dto/find-potential-matches-input.dto';
|
||||
import { PublishRequestInput } from '../dto/publish-request-input.dto';
|
||||
import { GetRequestInput } from '../dto/get-request-input.dto';
|
||||
import { GetRequestByHashInput } from '../dto/get-request-by-hash-input.dto';
|
||||
import { RequestType, RequestStatus } from '../../domain/entities/request-domain.entity';
|
||||
import { RequestImageType } from '../../domain/entities/request-image-domain.entity';
|
||||
import { GqlJwtAuthGuard } from '~/modules/auth/guards/graphql-jwt-auth.guard';
|
||||
import { CurrentUser } from '~/modules/auth/decorators/current-user.decorator';
|
||||
import type { MonoAccountDomainInterface } from '~/domain/account/interfaces/mono-account-domain.interface';
|
||||
|
||||
/**
|
||||
* GraphQL resolver для работы с заявками marketplace
|
||||
*/
|
||||
@Resolver(() => RequestDTO)
|
||||
@Injectable()
|
||||
export class RequestResolver {
|
||||
constructor(
|
||||
@Inject(REQUEST_DOMAIN_SERVICE)
|
||||
private readonly requestService: RequestDomainService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Создать новую заявку
|
||||
*/
|
||||
@Mutation(() => RequestDTO, {
|
||||
name: 'marketplaceCreateRequest',
|
||||
description: 'Создать новую заявку на поставку или заказ товара',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['member', 'chairman'])
|
||||
async createRequest(
|
||||
@Args('data', { type: () => CreateRequestInput })
|
||||
data: CreateRequestInput,
|
||||
@CurrentUser() user: MonoAccountDomainInterface
|
||||
): Promise<RequestDTO> {
|
||||
// Преобразуем входные данные в параметры для доменного сервиса
|
||||
const requestParams = {
|
||||
coopname: data.coopname,
|
||||
username: user.username,
|
||||
type: this.convertRequestType(data.type),
|
||||
name: data.name,
|
||||
articleNumber: data.articleNumber,
|
||||
descriptionCategoryId: data.descriptionCategoryId,
|
||||
typeId: data.typeId,
|
||||
price: data.price,
|
||||
currencyCode: data.currencyCode || 'RUB',
|
||||
vat: data.vat,
|
||||
units: data.units,
|
||||
barcode: data.barcode,
|
||||
oldPrice: data.oldPrice,
|
||||
width: data.width,
|
||||
height: data.height,
|
||||
depth: data.depth,
|
||||
dimensionUnit: data.dimensionUnit || 'mm',
|
||||
weight: data.weight,
|
||||
weightUnit: data.weightUnit || 'g',
|
||||
productLifecycleSecs: data.productLifecycleSecs,
|
||||
warrantyDays: data.warrantyDays,
|
||||
data: data.data,
|
||||
meta: data.meta,
|
||||
attributes: data.attributes.map((attr) => ({
|
||||
attributeId: attr.attributeId,
|
||||
value: attr.value,
|
||||
complexId: attr.complexId || 0,
|
||||
dictionaryValueId: attr.dictionaryValueId,
|
||||
})),
|
||||
images: data.images.map((img) => ({
|
||||
imageUrl: img.imageUrl,
|
||||
imageType: this.convertImageType(img.imageType),
|
||||
sortOrder: img.sortOrder,
|
||||
description: img.description,
|
||||
})),
|
||||
primaryImageUrl: data.primaryImageUrl,
|
||||
colorImageUrl: data.colorImageUrl,
|
||||
geoNames: data.geoNames || [],
|
||||
parentHash: data.parentHash,
|
||||
};
|
||||
|
||||
const request = await this.requestService.createRequest(requestParams);
|
||||
return RequestDTO.fromDomain(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить заявку по ID
|
||||
*/
|
||||
@Query(() => RequestDTO, {
|
||||
name: 'marketplaceGetRequest',
|
||||
description: 'Получить заявку по ID',
|
||||
nullable: true,
|
||||
})
|
||||
async getRequest(
|
||||
@Args('data', { type: () => GetRequestInput })
|
||||
data: GetRequestInput
|
||||
): Promise<RequestDTO | null> {
|
||||
const request = await this.requestService.findById(data.id);
|
||||
return request ? RequestDTO.fromDomain(request) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить заявку по хэшу
|
||||
*/
|
||||
@Query(() => RequestDTO, {
|
||||
name: 'marketplaceGetRequestByHash',
|
||||
description: 'Получить заявку по хэшу',
|
||||
nullable: true,
|
||||
})
|
||||
async getRequestByHash(
|
||||
@Args('data', { type: () => GetRequestByHashInput })
|
||||
data: GetRequestByHashInput
|
||||
): Promise<RequestDTO | null> {
|
||||
const request = await this.requestService.findByHash(data.hash);
|
||||
return request ? RequestDTO.fromDomain(request) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить заявки пользователя
|
||||
*/
|
||||
@Query(() => [RequestDTO], {
|
||||
name: 'marketplaceGetUserRequests',
|
||||
description: 'Получить заявки текущего пользователя',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['member', 'chairman'])
|
||||
async getUserRequests(
|
||||
@CurrentUser() user: MonoAccountDomainInterface,
|
||||
@Args('data', { type: () => GetUserRequestsInput, nullable: true })
|
||||
data?: GetUserRequestsInput
|
||||
): Promise<RequestDTO[]> {
|
||||
const requests = await this.requestService.findRecentByUser(user.username, data?.limit);
|
||||
return requests.map((req) => RequestDTO.fromDomain(req));
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить заявки кооператива
|
||||
*/
|
||||
@Query(() => [RequestDTO], {
|
||||
name: 'marketplaceGetCoopRequests',
|
||||
description: 'Получить заявки кооператива',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['member', 'chairman'])
|
||||
async getCoopRequests(
|
||||
@Args('data', { type: () => GetCoopRequestsInput }) data: GetCoopRequestsInput
|
||||
): Promise<RequestDTO[]> {
|
||||
const filters = {
|
||||
coopname: data.coopname,
|
||||
...(data.type && { type: this.convertRequestType(data.type) }),
|
||||
...(data.status && { status: data.status as RequestStatus }),
|
||||
limit: data.limit,
|
||||
};
|
||||
|
||||
const requests = await this.requestService.findWithFilters(filters);
|
||||
return requests.map((req) => RequestDTO.fromDomain(req));
|
||||
}
|
||||
|
||||
/**
|
||||
* Поиск заявок по названию товара
|
||||
*/
|
||||
@Query(() => [RequestDTO], {
|
||||
name: 'marketplaceSearchRequests',
|
||||
description: 'Поиск заявок по названию товара',
|
||||
})
|
||||
async searchRequests(
|
||||
@Args('data', { type: () => SearchRequestsInput })
|
||||
data: SearchRequestsInput
|
||||
): Promise<RequestDTO[]> {
|
||||
const requests = await this.requestService.searchByProductName(data.searchTerm, data.limit);
|
||||
return requests.map((req) => RequestDTO.fromDomain(req));
|
||||
}
|
||||
|
||||
/**
|
||||
* Найти потенциальные совпадения для заявки
|
||||
*/
|
||||
@Query(() => [RequestDTO], {
|
||||
name: 'marketplaceFindPotentialMatches',
|
||||
description: 'Найти потенциальные совпадения для заявки',
|
||||
})
|
||||
async findPotentialMatches(
|
||||
@Args('data', { type: () => FindPotentialMatchesInput })
|
||||
data: FindPotentialMatchesInput
|
||||
): Promise<RequestDTO[]> {
|
||||
const matches = await this.requestService.findPotentialMatches(data.requestId);
|
||||
return matches.map((req) => RequestDTO.fromDomain(req));
|
||||
}
|
||||
|
||||
// /**
|
||||
// * Опубликовать заявку
|
||||
// */
|
||||
// @Mutation(() => RequestDTO, {
|
||||
// name: 'marketplacePublishRequest',
|
||||
// description: 'Опубликовать заявку для поиска совпадений',
|
||||
// })
|
||||
// @UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
// @AuthRoles(['member', 'chairman'])
|
||||
// async publishRequest(
|
||||
// @Args('data', { type: () => PublishRequestInput })
|
||||
// data: PublishRequestInput,
|
||||
// @CurrentUser() user: MonoAccountDomainInterface
|
||||
// ): Promise<RequestDTO> {
|
||||
// // Проверяем, что заявка принадлежит пользователю
|
||||
// const request = await this.requestService.findById(data.id);
|
||||
// if (!request) {
|
||||
// throw new Error('Заявка не найдена');
|
||||
// }
|
||||
|
||||
// if (request.username !== user.username) {
|
||||
// throw new Error('Нет прав для публикации этой заявки');
|
||||
// }
|
||||
|
||||
// const publishedRequest = await this.requestService.publishRequest(data.id);
|
||||
// return RequestDTO.fromDomain(publishedRequest);
|
||||
// }
|
||||
|
||||
/**
|
||||
* Получить статистику заявок
|
||||
*/
|
||||
@Query(() => RequestStatisticsDTO, {
|
||||
name: 'marketplaceGetRequestStatistics',
|
||||
description: 'Получить статистику заявок кооператива',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['chairman', 'member'])
|
||||
async getRequestStatistics(
|
||||
@Args('data', { type: () => GetRequestStatisticsInput }) data: GetRequestStatisticsInput
|
||||
): Promise<RequestStatisticsDTO> {
|
||||
const stats = await this.requestService.getRequestStatistics(data.coopname);
|
||||
|
||||
return new RequestStatisticsDTO({
|
||||
totalRequests: stats.totalRequests,
|
||||
activeOffers: stats.activeOffers,
|
||||
activeOrders: stats.activeOrders,
|
||||
completedDeals: stats.completedDeals,
|
||||
requestsByCategory: stats.requestsByCategory,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Преобразовать тип заявки из GraphQL в доменный
|
||||
*/
|
||||
private convertRequestType(type: RequestTypeInput): RequestType {
|
||||
switch (type) {
|
||||
case RequestTypeInput.OFFER:
|
||||
return RequestType.OFFER;
|
||||
case RequestTypeInput.ORDER:
|
||||
return RequestType.ORDER;
|
||||
default:
|
||||
throw new Error(`Неизвестный тип заявки: ${type}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Преобразовать тип изображения из GraphQL в доменный
|
||||
*/
|
||||
private convertImageType(type: RequestImageTypeInput): RequestImageType {
|
||||
switch (type) {
|
||||
case RequestImageTypeInput.REGULAR:
|
||||
return RequestImageType.REGULAR;
|
||||
case RequestImageTypeInput.PRIMARY:
|
||||
return RequestImageType.PRIMARY;
|
||||
case RequestImageTypeInput.COLOR_SAMPLE:
|
||||
return RequestImageType.COLOR_SAMPLE;
|
||||
case RequestImageTypeInput.IMAGE_360:
|
||||
return RequestImageType.IMAGE_360;
|
||||
default:
|
||||
throw new Error(`Неизвестный тип изображения: ${type}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
import { Resolver, Mutation, Args, ObjectType, Field, InputType, Int } from '@nestjs/graphql';
|
||||
import { UseGuards, Inject } 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 { TransactionDTO } from '~/application/common/dto/transaction-result-response.dto';
|
||||
import { IsString, IsArray } from 'class-validator';
|
||||
import { SignedDigitalDocumentInputDTO } from '~/application/document/dto/signed-digital-document-input.dto';
|
||||
import { COOPLACE_BLOCKCHAIN_PORT, type CooplaceBlockchainPort } from '~/domain/cooplace/interfaces/cooplace-blockchain.port';
|
||||
import { config } from '~/config';
|
||||
|
||||
@InputType('CreateShipmentInput')
|
||||
export class CreateShipmentInputDTO {
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
hash!: string;
|
||||
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
driver_username!: string;
|
||||
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
source_braname!: string;
|
||||
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
destination_braname!: string;
|
||||
|
||||
@Field(() => [String])
|
||||
@IsArray()
|
||||
request_hashes!: string[];
|
||||
|
||||
@Field(() => SignedDigitalDocumentInputDTO)
|
||||
transport_act!: SignedDigitalDocumentInputDTO;
|
||||
}
|
||||
|
||||
@InputType('SignShipmentInput')
|
||||
export class SignShipmentInputDTO {
|
||||
@Field(() => String)
|
||||
@IsString()
|
||||
hash!: string;
|
||||
|
||||
@Field(() => SignedDigitalDocumentInputDTO)
|
||||
document!: SignedDigitalDocumentInputDTO;
|
||||
}
|
||||
|
||||
@Resolver()
|
||||
export class ShipmentResolver {
|
||||
constructor(
|
||||
@Inject(COOPLACE_BLOCKCHAIN_PORT)
|
||||
private readonly blockchainPort: CooplaceBlockchainPort,
|
||||
) {}
|
||||
|
||||
@Mutation(() => TransactionDTO, {
|
||||
name: 'createShipment',
|
||||
description: 'Создать перевозку (КУ отправителя)',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['chairman', 'member'])
|
||||
async createShipment(
|
||||
@Args('data') data: CreateShipmentInputDTO,
|
||||
): Promise<TransactionDTO> {
|
||||
const doc: Record<string, unknown> = { ...(data.transport_act as any) };
|
||||
doc.meta = JSON.stringify(doc.meta);
|
||||
return (this.blockchainPort as any).createShipment?.({
|
||||
coopname: config.coopname,
|
||||
...data,
|
||||
transport_act_sender: doc,
|
||||
}) as any;
|
||||
}
|
||||
|
||||
@Mutation(() => TransactionDTO, {
|
||||
name: 'signShipmentByDriver',
|
||||
description: 'Подпись водителя на перевозке',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard)
|
||||
async signShipmentByDriver(
|
||||
@Args('data') data: SignShipmentInputDTO,
|
||||
): Promise<TransactionDTO> {
|
||||
const doc: Record<string, unknown> = { ...(data.document as any) };
|
||||
doc.meta = JSON.stringify(doc.meta);
|
||||
return (this.blockchainPort as any).signByDriver?.({
|
||||
coopname: config.coopname,
|
||||
hash: data.hash,
|
||||
transport_act_driver: doc,
|
||||
}) as any;
|
||||
}
|
||||
|
||||
@Mutation(() => TransactionDTO, {
|
||||
name: 'shipmentArrived',
|
||||
description: 'Водитель отмечает прибытие',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard)
|
||||
async shipmentArrived(
|
||||
@Args('data') data: SignShipmentInputDTO,
|
||||
): Promise<TransactionDTO> {
|
||||
const doc: Record<string, unknown> = { ...(data.document as any) };
|
||||
doc.meta = JSON.stringify(doc.meta);
|
||||
return (this.blockchainPort as any).arrived?.({
|
||||
coopname: config.coopname,
|
||||
hash: data.hash,
|
||||
transport_act_delivery: doc,
|
||||
}) as any;
|
||||
}
|
||||
|
||||
@Mutation(() => TransactionDTO, {
|
||||
name: 'receiveShipment',
|
||||
description: 'Приём перевозки на складе КУ получателя',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, RolesGuard)
|
||||
@AuthRoles(['chairman', 'member'])
|
||||
async receiveShipment(
|
||||
@Args('data') data: SignShipmentInputDTO,
|
||||
): Promise<TransactionDTO> {
|
||||
const doc: Record<string, unknown> = { ...(data.document as any) };
|
||||
doc.meta = JSON.stringify(doc.meta);
|
||||
return (this.blockchainPort as any).receiveShipment?.({
|
||||
coopname: config.coopname,
|
||||
hash: data.hash,
|
||||
warehouse_receipt_act: doc,
|
||||
}) as any;
|
||||
}
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { CategoryTreeDomainService, CATEGORY_TREE_DOMAIN_SERVICE } from '../../domain/services/category-tree-domain.service';
|
||||
import { CategoryDTO, ProductTypeDTO } from '../dto/category-tree.dto';
|
||||
import { CategoryTreeStatsDTO } from '../dto/category-tree.dto';
|
||||
import { GetCategoryTreeInput } from '../dto/get-category-tree-input.dto';
|
||||
|
||||
/**
|
||||
* Сервис приложения для работы с деревом категорий
|
||||
*/
|
||||
@Injectable()
|
||||
export class CategoryTreeService {
|
||||
constructor(
|
||||
@Inject(CATEGORY_TREE_DOMAIN_SERVICE)
|
||||
private readonly categoryTreeDomainService: CategoryTreeDomainService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Получить полное дерево категорий с типами товаров
|
||||
*/
|
||||
async getCategoryTree(input?: GetCategoryTreeInput): Promise<CategoryDTO[]> {
|
||||
const domainTree = await this.categoryTreeDomainService.buildCategoryTreeWithOptions({
|
||||
rootCategoryId: input?.rootCategoryId,
|
||||
onlyAvailable: input?.onlyAvailable,
|
||||
includeTypes: input?.includeTypes,
|
||||
maxDepth: input?.maxDepth,
|
||||
});
|
||||
|
||||
return domainTree.map((category) => CategoryDTO.fromDomain(category));
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить дерево категорий с фильтрацией по доступности
|
||||
*/
|
||||
async getAvailableCategoryTree(coopname?: string): Promise<CategoryDTO[]> {
|
||||
const domainTree = await this.categoryTreeDomainService.buildAvailableCategoryTree(coopname);
|
||||
return domainTree.map((category) => CategoryDTO.fromDomain(category));
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить все root-категории
|
||||
*/
|
||||
async getRootCategories(): Promise<CategoryDTO[]> {
|
||||
const rootCategories = await this.categoryTreeDomainService.getRootCategories();
|
||||
return rootCategories.map((category) => CategoryDTO.fromDomain(category));
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить категорию по ID
|
||||
*/
|
||||
async getCategoryById(categoryId: number): Promise<CategoryDTO | null> {
|
||||
const category = await this.categoryTreeDomainService.getCategoryById(categoryId);
|
||||
if (!category) return null;
|
||||
|
||||
return CategoryDTO.fromDomain(category);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить тип товара по ID
|
||||
*/
|
||||
async getProductTypeById(typeId: number): Promise<ProductTypeDTO | null> {
|
||||
const type = await this.categoryTreeDomainService.getTypeById(typeId);
|
||||
if (!type) return null;
|
||||
|
||||
return ProductTypeDTO.fromDomain(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Универсальный поиск по дереву категорий и типов товаров
|
||||
* Возвращает дерево категорий на основе поискового запроса
|
||||
*/
|
||||
async search(searchTerm: string): Promise<CategoryDTO[]> {
|
||||
const resultTree = await this.categoryTreeDomainService.search(searchTerm);
|
||||
return resultTree.map((category) => CategoryDTO.fromDomain(category));
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить статистику по дереву категорий
|
||||
*/
|
||||
async getCategoryTreeStats(): Promise<CategoryTreeStatsDTO> {
|
||||
const stats = await this.categoryTreeDomainService.getCategoryTreeStats();
|
||||
return new CategoryTreeStatsDTO(stats);
|
||||
}
|
||||
}
|
||||
|
||||
export const CATEGORY_TREE_SERVICE = Symbol('CATEGORY_TREE_SERVICE');
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { WinstonLoggerService } from '~/application/logger/logger-app.service';
|
||||
|
||||
/**
|
||||
* Обработчик событий маркетплейса от парсера блокчейна.
|
||||
* Синхронизирует состояние заявок в БД с блокчейном.
|
||||
*/
|
||||
@Injectable()
|
||||
export class MarketplaceEventService {
|
||||
constructor(private readonly logger: WinstonLoggerService) {
|
||||
this.logger.setContext(MarketplaceEventService.name);
|
||||
}
|
||||
|
||||
@OnEvent('action::marketplace::orderoffer')
|
||||
async handleOrderOffer(event: any): Promise<void> {
|
||||
this.logger.info(`Новая заявка orderoffer: ${event?.data?.hash || 'unknown'}`);
|
||||
}
|
||||
|
||||
@OnEvent('action::marketplace::accept')
|
||||
async handleAccept(event: any): Promise<void> {
|
||||
this.logger.info(`Заявка принята: ${event?.data?.request_hash || 'unknown'}`);
|
||||
}
|
||||
|
||||
@OnEvent('action::marketplace::authcontrib')
|
||||
async handleAuthContrib(event: any): Promise<void> {
|
||||
this.logger.info(`Взнос авторизован: ${event?.data?.request_hash || 'unknown'}`);
|
||||
}
|
||||
|
||||
@OnEvent('action::marketplace::supply')
|
||||
async handleSupply(event: any): Promise<void> {
|
||||
this.logger.info(`Поставка: ${event?.data?.request_hash || 'unknown'}`);
|
||||
}
|
||||
|
||||
@OnEvent('action::marketplace::supplcnf')
|
||||
async handleSupplyConfirm(event: any): Promise<void> {
|
||||
this.logger.info(`Поставка подтверждена: ${event?.data?.request_hash || 'unknown'}`);
|
||||
}
|
||||
|
||||
@OnEvent('action::marketplace::delivered')
|
||||
async handleDelivered(event: any): Promise<void> {
|
||||
this.logger.info(`Доставлено: ${event?.data?.request_hash || 'unknown'}`);
|
||||
}
|
||||
|
||||
@OnEvent('action::marketplace::reqreturn')
|
||||
async handleReqReturn(event: any): Promise<void> {
|
||||
this.logger.info(`Запрос возврата: ${event?.data?.request_hash || 'unknown'}`);
|
||||
}
|
||||
|
||||
@OnEvent('action::marketplace::receive')
|
||||
async handleReceive(event: any): Promise<void> {
|
||||
this.logger.info(`Получение: ${event?.data?.request_hash || 'unknown'}`);
|
||||
}
|
||||
|
||||
@OnEvent('action::marketplace::receivecnf')
|
||||
async handleReceiveConfirm(event: any): Promise<void> {
|
||||
this.logger.info(`Получение подтверждено: ${event?.data?.request_hash || 'unknown'}`);
|
||||
}
|
||||
|
||||
@OnEvent('action::marketplace::complete')
|
||||
async handleComplete(event: any): Promise<void> {
|
||||
this.logger.info(`Поставка завершена: ${event?.data?.request_hash || 'unknown'}`);
|
||||
}
|
||||
|
||||
@OnEvent('action::marketplace::cancel')
|
||||
async handleCancel(event: any): Promise<void> {
|
||||
this.logger.info(`Заявка отменена: ${event?.data?.request_hash || 'unknown'}`);
|
||||
}
|
||||
|
||||
@OnEvent('action::marketplace::destroy')
|
||||
async handleDestroy(event: any): Promise<void> {
|
||||
this.logger.info(`Имущество уничтожено: ${event?.data?.request_hash || 'unknown'}`);
|
||||
}
|
||||
|
||||
@OnEvent('action::marketplace::dispute')
|
||||
async handleDispute(event: any): Promise<void> {
|
||||
this.logger.info(`Претензия: ${event?.data?.request_hash || 'unknown'}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { MarketplaceSettingsResolver } from './application/resolvers/marketplace-settings.resolver';
|
||||
import { ShipmentResolver } from './application/resolvers/shipment.resolver';
|
||||
import { MarketplaceEventService } from './application/services/marketplace-event.service';
|
||||
import { MatchService } from './domain/services/match.service';
|
||||
import { CycleService } from './domain/services/cycle.service';
|
||||
import { MARKETPLACE_SETTINGS_REPOSITORY } from './domain/repositories/marketplace-settings.repository';
|
||||
import { MarketplaceSettingsTypeormEntity } from './infrastructure/entities/marketplace-settings.typeorm-entity';
|
||||
import { MarketplaceSettingsTypeormRepository } from './infrastructure/adapters/marketplace-settings.typeorm-repository';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
MarketplaceSettingsTypeormEntity,
|
||||
]),
|
||||
],
|
||||
providers: [
|
||||
MarketplaceSettingsResolver,
|
||||
ShipmentResolver,
|
||||
MarketplaceEventService,
|
||||
MatchService,
|
||||
CycleService,
|
||||
{ provide: MARKETPLACE_SETTINGS_REPOSITORY, useClass: MarketplaceSettingsTypeormRepository },
|
||||
],
|
||||
exports: [MatchService, CycleService],
|
||||
})
|
||||
export class CooplaceExtensionModule {}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
import type { DictionaryDomainEntity } from './dictionary-domain.entity';
|
||||
import type { CategoryTypeAttributeDomainEntity } from './category-type-attribute-domain.entity';
|
||||
|
||||
/**
|
||||
* Доменная сущность атрибута товара для marketplace расширения
|
||||
* Представляет характеристики товаров для конкретных категорий и типов товаров
|
||||
*/
|
||||
export class AttributeDomainEntity {
|
||||
public readonly attributeId: number;
|
||||
public readonly name: string;
|
||||
public readonly description?: string;
|
||||
public readonly type: string;
|
||||
public readonly isCollection: boolean;
|
||||
public readonly isRequired: boolean;
|
||||
public readonly isAspect: boolean;
|
||||
public readonly maxValueCount: number;
|
||||
public readonly groupName?: string;
|
||||
public readonly groupId?: number;
|
||||
public readonly dictionaryId?: number;
|
||||
public readonly categoryDependent: boolean;
|
||||
public readonly complexIsCollection: boolean;
|
||||
public readonly attributeComplexId: number;
|
||||
public readonly dictionary?: DictionaryDomainEntity;
|
||||
public readonly categoryTypeAttributes: CategoryTypeAttributeDomainEntity[];
|
||||
public readonly createdAt: Date;
|
||||
public readonly updatedAt: Date;
|
||||
|
||||
constructor(data: {
|
||||
attributeId: number;
|
||||
name: string;
|
||||
description?: string;
|
||||
type: string;
|
||||
isCollection: boolean;
|
||||
isRequired: boolean;
|
||||
isAspect: boolean;
|
||||
maxValueCount: number;
|
||||
groupName?: string;
|
||||
groupId?: number;
|
||||
dictionaryId?: number;
|
||||
categoryDependent: boolean;
|
||||
complexIsCollection: boolean;
|
||||
attributeComplexId: number;
|
||||
dictionary?: DictionaryDomainEntity;
|
||||
categoryTypeAttributes?: CategoryTypeAttributeDomainEntity[];
|
||||
createdAt?: Date;
|
||||
updatedAt?: Date;
|
||||
}) {
|
||||
this.attributeId = data.attributeId;
|
||||
this.name = data.name;
|
||||
this.description = data.description;
|
||||
this.type = data.type;
|
||||
this.isCollection = data.isCollection;
|
||||
this.isRequired = data.isRequired;
|
||||
this.isAspect = data.isAspect;
|
||||
this.maxValueCount = data.maxValueCount;
|
||||
this.groupName = data.groupName;
|
||||
this.groupId = data.groupId;
|
||||
this.dictionaryId = data.dictionaryId;
|
||||
this.categoryDependent = data.categoryDependent;
|
||||
this.complexIsCollection = data.complexIsCollection;
|
||||
this.attributeComplexId = data.attributeComplexId;
|
||||
this.dictionary = data.dictionary;
|
||||
this.categoryTypeAttributes = data.categoryTypeAttributes || [];
|
||||
this.createdAt = data.createdAt || new Date();
|
||||
this.updatedAt = data.updatedAt || new Date();
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, является ли атрибут словарным
|
||||
*/
|
||||
hasDictionary(): boolean {
|
||||
return this.dictionaryId !== undefined && this.dictionaryId > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, можно ли изменить атрибут после создания товара
|
||||
*/
|
||||
canBeModifiedAfterCreation(): boolean {
|
||||
return !this.isAspect;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает максимальное количество значений
|
||||
*/
|
||||
getMaxValues(): number {
|
||||
if (this.isCollection) {
|
||||
return this.maxValueCount > 0 ? this.maxValueCount : Number.MAX_SAFE_INTEGER;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает группу атрибута
|
||||
*/
|
||||
getGroup(): { id?: number; name?: string } {
|
||||
return {
|
||||
id: this.groupId,
|
||||
name: this.groupName,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, является ли атрибут комплексным
|
||||
*/
|
||||
isComplexAttribute(): boolean {
|
||||
return this.attributeComplexId > 0;
|
||||
}
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Доменная сущность для управления доступными категориями и типами товаров в кооперативе
|
||||
* Определяет какие категории и типы товаров маркетплейса доступны в конкретном кооперативе
|
||||
*/
|
||||
export class AvailableCategoryDomainEntity {
|
||||
public readonly id: number;
|
||||
public readonly coopname: string;
|
||||
public readonly categoryId: number;
|
||||
public readonly typeId?: number; // null = доступна вся категория, число = только конкретный тип
|
||||
public readonly isActive: boolean;
|
||||
public readonly addedBy: string;
|
||||
public readonly createdAt: Date;
|
||||
public readonly updatedAt: Date;
|
||||
|
||||
constructor(data: {
|
||||
id?: number;
|
||||
coopname: string;
|
||||
categoryId: number;
|
||||
typeId?: number;
|
||||
isActive?: boolean;
|
||||
addedBy: string;
|
||||
createdAt?: Date;
|
||||
updatedAt?: Date;
|
||||
}) {
|
||||
this.id = data.id || 0;
|
||||
this.coopname = data.coopname;
|
||||
this.categoryId = data.categoryId;
|
||||
this.typeId = data.typeId;
|
||||
this.isActive = data.isActive ?? true;
|
||||
this.addedBy = data.addedBy;
|
||||
this.createdAt = data.createdAt || new Date();
|
||||
this.updatedAt = data.updatedAt || new Date();
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, активна ли категория/тип
|
||||
*/
|
||||
isActiveCategory(): boolean {
|
||||
return this.isActive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, применяется ли к конкретному типу товара
|
||||
*/
|
||||
isForSpecificType(): boolean {
|
||||
return this.typeId !== undefined && this.typeId !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, применяется ли к всей категории
|
||||
*/
|
||||
isForEntireCategory(): boolean {
|
||||
return this.typeId === undefined || this.typeId === null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, доступен ли указанный тип товара
|
||||
*/
|
||||
isTypeAvailable(typeId: number): boolean {
|
||||
if (!this.isActive) return false;
|
||||
|
||||
// Если правило для всей категории - любой тип доступен
|
||||
if (this.isForEntireCategory()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Если правило для конкретного типа - проверяем соответствие
|
||||
return this.typeId === typeId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Деактивирует категорию/тип
|
||||
*/
|
||||
deactivate(): AvailableCategoryDomainEntity {
|
||||
return new AvailableCategoryDomainEntity({
|
||||
...this,
|
||||
isActive: false,
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Активирует категорию/тип
|
||||
*/
|
||||
activate(): AvailableCategoryDomainEntity {
|
||||
return new AvailableCategoryDomainEntity({
|
||||
...this,
|
||||
isActive: true,
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
}
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
import type { TypeDomainEntity } from './type-domain.entity';
|
||||
|
||||
/**
|
||||
* Доменная сущность категории для marketplace расширения
|
||||
* Представляет иерархическую структуру категорий из Ozon API
|
||||
*/
|
||||
export class CategoryDomainEntity {
|
||||
public readonly descriptionCategoryId: number;
|
||||
public readonly categoryName: string;
|
||||
public readonly disabled: boolean;
|
||||
public readonly parentId?: number;
|
||||
public readonly parent?: CategoryDomainEntity;
|
||||
public readonly children: CategoryDomainEntity[];
|
||||
public readonly types: TypeDomainEntity[];
|
||||
public readonly createdAt: Date;
|
||||
public readonly updatedAt: Date;
|
||||
|
||||
constructor(data: {
|
||||
descriptionCategoryId: number;
|
||||
categoryName: string;
|
||||
disabled: boolean;
|
||||
parentId?: number;
|
||||
parent?: CategoryDomainEntity;
|
||||
children?: CategoryDomainEntity[];
|
||||
types?: TypeDomainEntity[];
|
||||
createdAt?: Date;
|
||||
updatedAt?: Date;
|
||||
}) {
|
||||
this.descriptionCategoryId = data.descriptionCategoryId;
|
||||
this.categoryName = data.categoryName;
|
||||
this.disabled = data.disabled;
|
||||
this.parentId = data.parentId;
|
||||
this.parent = data.parent;
|
||||
this.children = data.children || [];
|
||||
this.types = data.types || [];
|
||||
this.createdAt = data.createdAt || new Date();
|
||||
this.updatedAt = data.updatedAt || new Date();
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, является ли категория листовой (может содержать товары)
|
||||
*/
|
||||
isLeafCategory(): boolean {
|
||||
return this.children.length === 0 && !this.disabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает полный путь к категории
|
||||
*/
|
||||
getFullPath(): string {
|
||||
if (!this.parent) {
|
||||
return this.categoryName;
|
||||
}
|
||||
return `${this.parent.getFullPath()} / ${this.categoryName}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает все дочерние категории рекурсивно
|
||||
*/
|
||||
getAllDescendants(): CategoryDomainEntity[] {
|
||||
const descendants: CategoryDomainEntity[] = [];
|
||||
|
||||
for (const child of this.children) {
|
||||
descendants.push(child);
|
||||
descendants.push(...child.getAllDescendants());
|
||||
}
|
||||
|
||||
return descendants;
|
||||
}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
import type { CategoryDomainEntity } from './category-domain.entity';
|
||||
import type { TypeDomainEntity } from './type-domain.entity';
|
||||
import type { AttributeDomainEntity } from './attribute-domain.entity';
|
||||
|
||||
/**
|
||||
* Доменная сущность связи между категориями, типами товаров и их характеристиками
|
||||
* Представляет отношение "многие ко многим" между категориями, типами и атрибутами из Ozon API
|
||||
*/
|
||||
export class CategoryTypeAttributeDomainEntity {
|
||||
public readonly descriptionCategoryId: number;
|
||||
public readonly typeId: number;
|
||||
public readonly attributeId: number;
|
||||
public readonly category: CategoryDomainEntity;
|
||||
public readonly type: TypeDomainEntity;
|
||||
public readonly attribute: AttributeDomainEntity;
|
||||
public readonly categoryName: string;
|
||||
public readonly typeName: string;
|
||||
public readonly isFetched: boolean;
|
||||
public readonly createdAt: Date;
|
||||
public readonly updatedAt: Date;
|
||||
|
||||
constructor(data: {
|
||||
descriptionCategoryId: number;
|
||||
typeId: number;
|
||||
attributeId: number;
|
||||
category: CategoryDomainEntity;
|
||||
type: TypeDomainEntity;
|
||||
attribute: AttributeDomainEntity;
|
||||
categoryName: string;
|
||||
typeName: string;
|
||||
isFetched: boolean;
|
||||
createdAt?: Date;
|
||||
updatedAt?: Date;
|
||||
}) {
|
||||
this.descriptionCategoryId = data.descriptionCategoryId;
|
||||
this.typeId = data.typeId;
|
||||
this.attributeId = data.attributeId;
|
||||
this.category = data.category;
|
||||
this.type = data.type;
|
||||
this.attribute = data.attribute;
|
||||
this.categoryName = data.categoryName;
|
||||
this.typeName = data.typeName;
|
||||
this.isFetched = data.isFetched;
|
||||
this.createdAt = data.createdAt || new Date();
|
||||
this.updatedAt = data.updatedAt || new Date();
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает уникальный идентификатор связи
|
||||
*/
|
||||
getCompositeKey(): string {
|
||||
return `${this.descriptionCategoryId}-${this.typeId}-${this.attributeId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, является ли атрибут обязательным для данной категории и типа
|
||||
*/
|
||||
isRequired(): boolean {
|
||||
return this.attribute.isRequired;
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, является ли атрибут аспектным
|
||||
*/
|
||||
isAspect(): boolean {
|
||||
return this.attribute.isAspect;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает полное описание связи
|
||||
*/
|
||||
getDescription(): string {
|
||||
return `${this.categoryName} / ${this.typeName} -> ${this.attribute.name}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, загружены ли данные атрибута
|
||||
*/
|
||||
isDataFetched(): boolean {
|
||||
return this.isFetched;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает группу атрибута
|
||||
*/
|
||||
getAttributeGroup(): { id?: number; name?: string } {
|
||||
return this.attribute.getGroup();
|
||||
}
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
import { RequestStatus, type RequestDomainEntity } from './request-domain.entity';
|
||||
|
||||
/**
|
||||
* Статусы цепочки поставки
|
||||
*/
|
||||
export enum DeliveryChainStatus {
|
||||
PLANNING = 'planning', // Планируется маршрут
|
||||
ACTIVE = 'active', // Активная цепочка
|
||||
COMPLETED = 'completed', // Завершена успешно
|
||||
FAILED = 'failed', // Провалена
|
||||
CANCELLED = 'cancelled', // Отменена
|
||||
DISPUTED = 'disputed', // Есть споры в сегментах
|
||||
}
|
||||
|
||||
/**
|
||||
* Связанная заявка в цепочке
|
||||
*/
|
||||
export class LinkedRequest {
|
||||
public readonly requestHash: string; // Хэш заявки
|
||||
public readonly blockchainId: string; // ID блокчейна где находится заявка
|
||||
|
||||
constructor(data: { requestHash: string; blockchainId: string }) {
|
||||
this.requestHash = data.requestHash;
|
||||
this.blockchainId = data.blockchainId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Цепочка поставки - связывает заявки для выполнения поставки
|
||||
*/
|
||||
export class DeliveryChainDomainEntity {
|
||||
public readonly id?: number;
|
||||
|
||||
// Связанные заявки (обычно 2 - поставка и заказ)
|
||||
public readonly linkedRequests: RequestDomainEntity[]; // Все заявки в цепочке
|
||||
|
||||
// Временные рамки
|
||||
public readonly createdAt: Date;
|
||||
|
||||
constructor(data: { id?: number; status: DeliveryChainStatus; linkedRequests: RequestDomainEntity[]; createdAt?: Date }) {
|
||||
this.id = data.id;
|
||||
this.linkedRequests = data.linkedRequests;
|
||||
this.createdAt = data.createdAt || new Date();
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверить, завершена ли цепочка
|
||||
*/
|
||||
isCompleted(): boolean {
|
||||
return this.linkedRequests.filter((r) => r.status === RequestStatus.COMPLETED).length === this.linkedRequests.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверить, активна ли цепочка
|
||||
*/
|
||||
isActive(): boolean {
|
||||
return this.linkedRequests.filter((r) => r.status === RequestStatus.ACTIVE).length === this.linkedRequests.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверить, есть ли споры в цепочке
|
||||
*/
|
||||
hasDisputes(): boolean {
|
||||
return this.linkedRequests.filter((r) => r.status === RequestStatus.DISPUTED).length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить все хэши заявок
|
||||
*/
|
||||
getAllRequestHashes(): string[] {
|
||||
return this.linkedRequests.map((lr) => lr.hash);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user