b67581ce80
extensions.config is a single jsonb blob shared by every flag/hash the L1 onboarding wizards (capital, chairman) write to. Every write site did the classic read-modify-write: findByName() the whole row, spread+mutate one key in app memory, then update() the whole config back. Two DecisionTrackedEvent handlers firing concurrently (two council decisions tracked near-simultaneously) each read the same stale snapshot and each write their own flag back — whichever write lands last wins and silently reverts the other one's flag to its old value. Symptom hit live: onboarding_generator_program_template_done reverted to false (with its hash still present, proving the decision really was tracked) after onboarding_generation_contract_template_done raced it to true. Added ExtensionDomainRepository.patchConfig(name, patch), implemented as a single `UPDATE extensions SET config = config || patch::jsonb ... RETURNING *` — no app-side read before the write, Postgres serializes concurrent UPDATEs on the row so two different-key patches can no longer clobber each other. Replaced every config read-modify-write in capital's and chairman's onboarding services (the events handler's flag write, loadPlugin's init/expire timestamps, the hash writes in completeStep/completeGeneralMeet, saveProgramDocDataHash) with this atomic patch. update() is untouched for other callers. Live data recovered manually: onboarding_generator_program_template_done was patched back to true for voskhod/capital (hash was already present, confirming the decision had in fact been tracked before the race reverted the flag).
🎛️ @coopenomics/controller
Основной бэкенд-сервис платформы «Цифровой Кооператив». GraphQL API на NestJS с чистой архитектурой — обрабатывает все запросы от рабочего стола, управляет кооперативными процессами, генерирует документы и взаимодействует с блокчейном EOSIO.
Основные возможности
- GraphQL API для всех операций платформы
- Чистая архитектура: domain → infrastructure → modules
- Управление пайщиками, кошельками и финансовыми операциями
- Электронный документооборот с ЭП через блокчейн
- Система расширений (extensions) для модульного подключения функциональности
- Платёжный шлюз с поддержкой нескольких провайдеров
- JWT-аутентификация и авторизация
- Миграции данных через TypeORM
- Интеграция с Novu для уведомлений и LiveKit для видеоконференций
Установка
Компонент является частью монорепозитория. Установка зависимостей из корня проекта:
pnpm install
Или только для этого компонента:
pnpm install --filter @coopenomics/controller
Скрипты
| Скрипт | Команда | Описание |
|---|---|---|
dev |
pnpm run dev |
Запуск в режиме разработки (nodemon + ts-node) |
start |
pnpm run start |
Запуск в production-режиме |
lint |
pnpm run lint |
Проверка кода (ESLint) |
lint:fix |
pnpm run lint:fix |
Автоисправление lint-ошибок |
typecheck |
pnpm run typecheck |
Проверка типов TypeScript |
migration:generate |
pnpm run migration:generate |
Генерация новой миграции |
migration:run |
pnpm run migration:run |
Применение всех миграций |
migration:status |
pnpm run migration:status |
Текущий статус миграций |
migration:rollback |
pnpm run migration:rollback |
Откат миграций |
migration:list |
pnpm run migration:list |
Список всех миграций |
docs |
pnpm run docs |
Генерация документации API (SpectaQL) |
Конфигурация
Скопируйте .env-example в .env и настройте переменные окружения:
- Строки подключения к MongoDB, реляционной БД, Redis
- Endpoint блокчейн-ноды и
CHAIN_ID VAPID_PUBLIC_KEY/VAPID_PRIVATE_KEY— ключи для push-уведомлений- Настройки JWT-аутентификации
- Конфигурация платёжных провайдеров
- Настройки Sentry для мониторинга ошибок
Подробное описание переменных — в файле .env-example.
Тестирование
Интеграционные тесты выполняются через компонент @coopenomics/boot, который разворачивает полную инфраструктуру:
pnpm --filter @coopenomics/boot run test
Архитектура
Проект следует принципам чистой архитектуры с направлением зависимостей внутрь (к домену):
src/
├── domain/ # Доменный слой (бизнес-логика)
│ ├── account/ # Аккаунты пользователей
│ ├── agreement/ # Соглашения
│ ├── auth/ # Аутентификация
│ ├── blockchain/ # Интерфейсы блокчейна
│ ├── branch/ # Кооперативные участки
│ ├── capital/ # Паевые взносы
│ ├── cooplace/ # Маркетплейс
│ ├── document/ # Документооборот
│ ├── extension/ # Расширения
│ ├── gateway/ # Платёжный шлюз
│ ├── ledger/ # Бухгалтерский учёт
│ ├── meet/ # Собрания
│ ├── participant/ # Участники кооператива
│ ├── registration/ # Регистрация
│ ├── wallet/ # Кошельки
│ └── common/ # Общие порты и репозитории
├── infrastructure/ # Инфраструктурный слой (адаптеры)
│ ├── blockchain/ # Адаптер блокчейна EOSIO
│ ├── database/ # Адаптеры БД (Mongoose, TypeORM)
│ └── external/ # Внешние сервисы
├── modules/ # Прикладной слой (NestJS-модули)
│ ├── auth/ # Модуль аутентификации
│ ├── cooperative/ # Модуль кооператива
│ └── ... # Остальные модули
└── extensions/ # Система подключаемых расширений
├── extensions.registry.ts
└── base.extension.module.ts
migrations/ # Миграции данных (TypeORM)
Поток данных
- Резолвер принимает входные данные и передаёт в сервис
- Сервис вызывает интерактор домена, передавая DTO (реализующие доменный интерфейс)
- Интерактор выполняет бизнес-логику и взаимодействует с портами
- Адаптеры (инфраструктура) преобразуют доменные объекты в формат внешних систем
- Результаты возвращаются обратно по цепочке
Ключевые зависимости
- @nestjs/* — фреймворк бэкенда
- graphql / @nestjs/graphql — GraphQL API
- mongoose — MongoDB ODM
- typeorm — реляционный ORM с миграциями
- eosjs — взаимодействие с блокчейном EOSIO
- ioredis — Redis-клиент
- @novu/api — сервис уведомлений
- livekit-server-sdk — видеоконференции
- winston — логирование