* [598-6][@ant] feat: backend whitelist + дефолтная витрина — Story 3.1 Story 3.1 «Управление whitelist поставщиков + дефолтная витрина»: — Domain entities `MarketplaceVitrineDomainEntity`, `MarketplaceWhitelistEntryDomainEntity` (role: 'auto-coop' | 'manual') и domain-репозитории `MarketplaceVitrineDomainRepository`, `MarketplaceWhitelistDomainRepository` (DIP — реализация в infra). — TypeORM-entities `marketplace_vitrine` (PK=id, MVP всегда 'default'), `marketplace_whitelist` (UUID, UNIQUE(coop, member)). Подключение `marketplace` уже работает с `synchronize:true` — DDL автоматический. — Mappers + adapters; идемпотентные `ensureDefault` / `add(role)`. — Bootstrap-миграция v3 `marketplaceBootstrapV3Migration`: в afterMigrate создаёт `{id:'default', display_name:'Стол заказов'}` + auto-coop whitelist entry (FR5 — перепоставка остатков самим коопом). — `MarketplaceWhitelistService.isOfferer` — источник `context.isOfferer` для `mapCoreRolesToMarketplaceRoles`: «открытая витрина» (только auto-coop) → true для всех User; «по whitelist» → true только для manual-записей. TTL-кеш 60s, инвалидируется при add/remove. — `MarketplaceVitrineService.getDefault/list` — read-only API конфигурации витрины (конструктор кастомных витрин Out-of-MVP). — GraphQL endpoints: `marketplaceListWhitelist` / `marketplaceAddToWhitelist` / `marketplaceRemoveFromWhitelist` под `@RequireMarketplaceAccess( 'Whitelist','manage')` (admin); `marketplaceDefaultVitrine` под `'Vitrine','read'` (всем marketplace-ролям). auto-coop неудаляем. — Access-matrix: добавлен `Vitrine:['read']` для offerer/operator, расширены `admin: Offer:['moderate','read']`, `Vitrine:['manage','read']`. — `MarketplaceMembershipGuard` теперь async, дёргает `whitelistService.isOfferer(coopname, username)` для генерации marketplace-роли `offerer` на каждый GraphQL-запрос (через cache). — Регистрация миграции v3 в `extension-domain.module.ts`, новых entities/adapters/mappers/repositories в `marketplace-infrastructure .module.ts`, сервисов/резолверов в `marketplace-application.module.ts`. Тесты: 21 unit-кейс (`tests/unit/marketplace/`): — marketplace-whitelist-service.test.ts (11): list, add, remove manual/auto-coop/missing, isOfferer семантика 3-х режимов + TTL-кеш + инвалидация на add/remove; — marketplace-vitrine-service.test.ts (3): getDefault / null / list; — marketplace-membership-guard.test.ts (7, обновлён под async + DI): open vitrine → +offerer, whitelist → no offerer, member/chairman роли, status/auth/server-secret bypass. pnpm tsc проходит чисто на изменённых файлах (предсуществующие ошибки в `file-storage/` не относятся к Story 3.1). targeted jest зелёный. Зависимости: Эпик 1 (whitelist использует marketplace-roles.mapper, access-matrix, MembershipGuard). Не блокирует и не блокируется Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * [598-6][@ant] feat: backend Offer CRUD + 10 baseline-категорий — Story 3.2 Story 3.2 «Поставщик публикует и управляет жизненным циклом Offer'а»: — Domain: `MarketplaceOfferDomainEntity` (полный набор полей под Stories 3.2/3.3/3.4 — quantity_available/blocked/consumed, cycle_type/days/target_volume/max_wait_days/min_threshold, status PENDING_MODERATION→ACTIVE→REJECTED/WITHDRAWN, approved_by/at, rejected_by/at, reject_reason); `MarketplaceCategoryDomainEntity` (справочник 10 baseline); типы `MarketplaceOfferStatus`/`CycleType`/`UnitOfMeasure`. — TypeORM-entities `marketplace_offer` (UUID, 3 индекса под Stories 3.2/3.5) и `marketplace_category` (PK=integer, seed-таблица). — Repositories: `MarketplaceOfferDomainRepository` (findById/list/countByCategory/countRecentCreatedBy/create/applyUpdate) + `MarketplaceCategoryDomainRepository` (listBaseline/findById/upsertBaseline). — Adapters TypeORM-based с QueryBuilder (фильтры по supplier/status/ category/available_only, сортировки по created_at/price); countByCategory считает только ACTIVE + (unlimited OR available>0). — `MarketplaceOfferService` инкапсулирует AC Story 3.2: • create: PENDING_MODERATION + валидация (product_name 1..200, description ≤2000, baseline category 1..10, numeric price 0-4 знака, cycle/unit enums, quantity invariants); • rate-limit: 10 Offer'ов/час на supplier (`countRecentCreatedBy`); • update: ownership + reset в PENDING_MODERATION; REJECTED/WITHDRAWN → 403; • withdraw: ownership + status WITHDRAWN; stub `hasActiveOrders`=false (точка интеграции с Story 4.x order-репозиторием после merge #375); • unlimited_flag=true атомарно обнуляет quantity_available. — `MarketplaceCategoryService.listBaseline` — read для UI (форма создания Offer'а + фильтр-чипы Story 3.5). — GraphQL resolver: `marketplaceCreateOffer` / `marketplaceUpdateOffer` / `marketplaceWithdrawOffer` / `marketplaceListMyOffers` — `@RequireMarketplaceAccess('Offer','create:own'|'update:own'|'delete:own')`; `marketplaceListCategories` — `'Offer','read'`. — DTO: MarketplaceOfferDTO + MarketplaceOfferPageDTO + MarketplaceCategoryDTO, Create/Update/Withdraw/ListMy Input DTOs (class-validator: Min/Max/Matches/ IsIn для enum-полей). — Bootstrap-миграция v4 (`marketplace-bootstrap-v4`): идемпотентный upsert 10 категорий через `categoryRepo.upsertBaseline()`. DDL — synchronize:true. — Регистрация: новые entities/adapters/mappers/repositories в infrastructure.module, services/resolver в application.module, миграция v4 в extension-domain.module. Тесты: 24 unit-кейса в `marketplace-offer-service.test.ts`: — create (11): happy path + rate-limit + категория вне baseline + отсутствующая в БД + product_name пустой/>200 + quantity invariants + unlimited_flag → 0 + неверные cycle/unit/price; — update (6): reset в PENDING_MODERATION + ownership + WITHDRAWN/REJECTED 403 + 404 + unlimited→0 + invalid category; — withdraw (5): happy + ownership + уже WITHDRAWN + 404 + sentinel под active-orders; — listMine + getById (2). `pnpm tsc --noEmit` чисто; jest зелёный. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * [598-6][@ant] feat: backend модерация Offer'ов админом — Story 3.3 Story 3.3 «Модерация Offer'ов админом (approve/reject + комментарий)»: — Domain: `MarketplaceModerationLogDomainEntity` (offer_id, action, by_account, reason, created_at; append-only) + repository интерфейс `MarketplaceModerationLogDomainRepository.append/listByOffer`. — TypeORM entity `marketplace_moderation_log` (UUID PK, индексы по offer_id+created_at и by_account); adapter + mapper. — `MarketplaceModerationService`: • listPending — `repo.list({status:PENDING_MODERATION}, paging)`; • approve(offer_id, admin): PENDING → ACTIVE, approved_by/approved_at, очистка rejected_*; append log с action='approve'; EventEmitter2.emit `marketplace.offer.approved` (порядок save→emit соблюдён, INV-12 controller/CLAUDE.md); • reject(offer_id, admin, reason): обязательное и trim'нутое reason 1..1000 char; PENDING → REJECTED + rejected_by/at + reject_reason; log с action='reject'; emit `marketplace.offer.rejected`; • listLog — append-only история по offer'у; • Не-PENDING статус → 409 Conflict; missing → 404; reason invalid → 400. — GraphQL resolver `marketplace-moderation.resolver.ts`: `marketplaceListPendingOffers` / `marketplaceApproveOffer` / `marketplaceRejectOffer` / `marketplaceListModerationLog` — все под `@RequireMarketplaceAccess('Offer','moderate')` (только admin). — DTO: Approve/Reject/ListPending Input + ModerationLogEntry Output. — Подключение в infrastructure + application модули; EventEmitter2 уже в core-ApplicationModule (через `@nestjs/event-emitter`). Тесты: 10 unit-кейсов в `marketplace-moderation-service.test.ts`: — approve (3): happy + 409 на не-PENDING + 404; — reject (5): happy + empty reason 400 + >1000 chars 400 + 409 на уже-rejected + trim применяется к reason; — listPending+listLog (2): корректная делегация с filter/paging. `pnpm tsc --noEmit` чисто; jest зелёный. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * [598-6][@ant] feat: backend counters available/blocked/consumed — Story 3.4 Story 3.4 «Backend ведёт available/blocked count Offer'ов»: — Domain контракт `MarketplaceOfferDomainRepository` расширен 3-мя атомарными дельтами: `applyBlockDelta` / `applyUnblockDelta` / `applyConsumeDelta`. Возвращают `OfferCountersDeltaResult` — discriminated `{ok, reason?, offer?}` с reason'ами `insufficient_*`, `offer_not_active`, `offer_not_found`. — Adapter реализован одним SQL UPDATE с WHERE-CAS-условием и RETURNING: • block: `quantity_blocked+=K`, `available -= K` (если не unlimited); WHERE status=ACTIVE AND (unlimited OR available>=K); • unblock: `quantity_blocked-=K`, `available += K` (если не unlimited); WHERE blocked>=K; • consume: `quantity_blocked-=K`, `quantity_consumed+=K`; WHERE blocked>=K. 0 affected rows → fallback findOne + классификация причины. Атомарность обеспечивается одним SQL-statement'ом, гонки между параллельными Order-блокировками не разрушают инвариант. — `MarketplaceOfferCountersService` — точка интеграции с Эпиком 4 (`o.mkt.block/unblock/consume` canonical из PR #375): order-side syncer вызывает `onOrderBlocked/Unblocked/Consumed/Adjusted` внутри `dispatch` после `save` Order'а до `emit pubsub` (INV-12, см. controller/CLAUDE.md). Валидация qty > 0 integer; перевод reason → NestJS exception (404/400). EventEmitter2 emit `marketplace.offer.counters.changed` после успешной операции — Story 3.5 каталог и offerer-«Активность» подписываются (Phase 2 GraphQL Sub). — `onOrderAdjusted(qty_diff)` (FR23 «факт меньше заказа») семантически делегирует в `onOrderUnblocked`. Инвариант (для не-unlimited): `available + blocked + consumed = lifetime_published` поддерживается дельтами (изменение суммы 0 на каждом методе). Подключение в `marketplace-application.module.ts`. Существующие моки в тестах 3.2 / 3.3 расширены под новый интерфейс репозитория. Тесты: 11 unit-кейсов в `marketplace-offer-counters-service.test.ts`: — happy paths (4: block / unblock / consume / adjusted-delegate); — qty validation (1: 0 / negative / non-integer); — классификация reason → exception (4: insufficient_available/blocked, not_active, not_found); — emit skip on error (1); — math invariant (1): block 10 → unblock 3 → consume 7, net 0. Атомарность SQL CAS-update — интеграционный уровень (testcontainers PG, after merge Эпика 4). Точка интеграции зафиксирована интерфейсом. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * [598-6][@ant] feat: каталог Offer'ов на orderer-столе — Story 3.5 Story 3.5 «Каталог Offer'ов с фильтром-чипами по 10 категориям»: Backend: — `marketplaceListCatalog(input?: ListCatalogInput)`: только ACTIVE + (unlimited OR available>0), фильтр `category_id`, paging limit/offset (default 24/0), sort `created_at_desc` (default) | `price_asc` | `price_desc`. Доступ `@RequireMarketplaceAccess('Offer','read')` — всем marketplace-ролям. — `marketplaceCategoryOfferCounts`: счётчики активных Offer'ов per category для фильтр-чипов. Гарантированно возвращает все 10 baseline-категорий (даже count=0) — UX-DR10 рисует полный набор чипов. — DTO: `MarketplaceListCatalogInput` (class-validator: category_id 1..10, sort IsIn, paging Min/Max), `MarketplaceCategoryOfferCount`. — Резолвер `MarketplaceCatalogResolver` подключён в application-module. Frontend (FSD slim — отдельный pages/Marketplace/MarketplaceCatalog): — `types.ts` — ручная типизация MarketplaceOfferView / CategoryOfferCount / CatalogSort / CatalogFilter (техдолг: после cooptypes:gen-zeus переписать на `Queries.Marketplace.*` из SDK, паттерн из PR #381). — `api/index.ts` — raw GraphQL через `sendPOST('/v1/graphql')` (`fetchCatalog` / `fetchCategories` / `fetchCategoryOfferCounts`), обработка `body.errors` как throw. — `ui/CatalogOfferCard.vue` (UX-DR10 — карточка): product_name (≤2 строки ellipsis), supplier, цена-за-единицу с правильным unit-label (шт/кг/л/упак), quantity_label («Без ограничений» или «Доступно: N ед»), cycle-label per cycle_type с правильными формулировками AC, warranty-чип, кнопка «Заказать» (disabled при отсутствии остатка); aria-label, ellipsis-2-lines. — `ui/MarketplaceCatalogPage.vue`: • горизонтальная полоса фильтр-чипов «Все» + 10 категорий с counter-badge per чип (`scroll-x` под mobile); • сортировка через q-select (3 опции); • responsive grid (`col-12 col-sm-6 col-md-4 col-lg-3`), pagination 24/страницу через `q-infinite-scroll`; • EmptyState компонент при пустом результате (фильтр / каталог); • aria-label на регион/чипы/карточки; • Order-форма (Эпик 4 Story 4.1) — пока Notify-stub с указанием точки интеграции. `install.ts`: — `defaultRoute: 'marketplace-catalog'` (новая страница); — route `/:coopname/market/catalog` → `MarketplaceCatalogPage`; — legacy `marketplace-showcase` помечен `hidden:true` (donor-страница на старой Marketplace архитектуре, см. project_stol_zakazov_mvp решение 2026-05-12 «donor переписывается без переходников»; переписывается в Phase 2 / последующем PR Stories 3.2 frontend). vue-tsc: на новых файлах ошибок нет (`MapIterator` итерация заменена на `Array.from(...).reduce`). Известные baseline-ошибки `Cannot find module 'src/...'` во всех `extensions/*/install.ts` (включая `branch`, `capital`, `chairman` — pre-existing). Тесты: backend-резолвер — thin forward в `offerRepo.list` / `countByCategory`, оба метода покрыты на adapter-уровне (тесты catalog filtering — интеграционный testcontainers PG); карточная логика label-форматтеров — компонентный тест Phase 2. Limitations: — Order-форма stub (Эпик 4); — Zeus типы в SDK — техдолг (как в PR #381 marketplace KU details); — WCAG 2.1 AA полный audit — Phase 2 (Эпик 10 Story 10.2); — Donor-страницы Marketplace/Showcase, CreateParentOffer, UserParentOffers, Moderation не переписаны под новые GraphQL мутации Stories 3.2/3.3 — это работа отдельного UI-PR (текущий backend mutations работают, donor-UI закрыт legacy путём). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * [598-6][@ant] chore: BC-sync integration seam — onOrderRolledBack + OrderSync scaffolding Закрытие разрыва «где живёт blockchain-synchronization для Эпика 3», явный seam в коде вместо разбросанных JSDoc-комментариев. Контекст: ревью пользователя 2026-05-15 — Эпик 3 был доставлен как backend-only, и Story 3.4 counters стояли как stub-callback без явной sync-точки. Слабая фиксация интеграции (только JSDoc) — исправляется. Source-of-truth паттерна (ARCH-документы blago): — `13-platforma-tsifrovogo-kooperativa/components/14-versiya-3/ requirements/c3-arch-sinkhronizatsiya-uzla-s-blokcheynom-v1.md` (ADR-001 .. ADR-012); — `4f-arch-integratsiya-kontrollera-s-parser2-v1.md` (DEC-T01..T12 ParserClient). Спецификация интеграции (для Эпика 4): — `_bmad-output/implementation-artifacts/spec-3-4-bc-integration.md` (проект `1-prilozhenie-stol-zakazov`, компонент `3-minimalnyy-produkt`) — фиксирует contract Эпик 3 ↔ Эпик 4: какие методы counters-сервиса дёргаются на какие canonical actions (#375 PR), какой subscriptionId / consumerName / startFromBlock для ParserClient, какой ForkRegistry handler, чек-лист what Эпик 4 должен реализовать (composite-entity Order, mapper, repository, sync-service, subscription, side-effect injection, fork handler, write-mutation pool, GraphQL Subscription). Изменения в коде: 1. `MarketplaceOfferDomainRepository.applyRollbackDelta(offer_id, qty)` — новый contract-метод для ADR-005 ForkRegistry handler'а. Реализован в TypeORM-adapter'е через SQL UPDATE **без** CAS-проверки `blocked>=qty` (rollback может приходить когда счётчик уже в consumed-состоянии — это ожидаемо при катастрофе fork-вне-Rollback- Horizon; manual reconciliation FR12 ARCH-sync). 2. `MarketplaceOfferCountersService.onOrderRolledBack(offer_id, qty)` — публичный метод target-сервиса. Дёргается из `MarketplaceOrderSyncService.handleFork` для каждого откатываемого Order'а в block-состоянии. Emit `marketplace.offer.counters.changed` с op='rollback' (Story 3.5 каталог-subscription и offerer-«Активность» получают update). 3. `extensions/marketplace/sync/marketplace-order-sync.service.ts` — **scaffolding** `MarketplaceOrderSyncService`. Skeleton-класс с тремя методами (`start`, `dispatch`, `handleFork`) — все throw `'NOT IMPLEMENTED — Эпик 4'`. В JSDoc — полный контракт: — `@DomainKey({primary:'id', sync:'order_id'})`, — `@SyncBehaviour({forkPolicy:'rollback-via-versions', dlq:true})`, — `@Versioned({strategy:'entity_versions'})`, — subscription `controller-${coopname}` `primary` `last_known`, — мапинг `p.mkt.supply.createorder` → `onOrderBlocked`, `cancelorder`/`expireorder`/`declineorder` → `onOrderUnblocked`, `consume`+`consume2` → `onOrderConsumed`, FR23 → `onOrderAdjusted`, — fork handler → `onOrderRolledBack` per Order; — ссылка на spec-3-4-bc-integration.md. Это фиксирует **место в коде** для Эпика 4 — а не «JSDoc в любом из 5 файлов»; чек-лист реализации виден один в одном файле. 4. Тесты: — `applyRollbackDelta` добавлен в моки `MarketplaceOfferDomainRepository` во всех 3-х existing test-файлах (offer-service, moderation-service, offer-counters-service); — новый кейс в `marketplace-offer-counters-service.test.ts`: `onOrderRolledBack (ADR-005 ForkRegistry handler) → applyRollbackDelta + emit op:rollback` — 12-й тест в этом файле, 67-й в Эпике 3. `pnpm tsc` чисто; `pnpm jest tests/unit/marketplace/marketplace-offer- counters-service.test.ts` — 12/12 passed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * [598-6][@ant] refactor: review feedback PR #382 — coopname, общая пагинация, RU-сообщения, продовольственные категории — coopname вместо cooperative_id во всём marketplace (поле, колонки, индексы, UNIQUE constraint, DTO, resolvers, миграции, spec); blockchain-aligned именование как в capital/reports. — общий PaginationInputDTO/createPaginationResult/PaginationResultDomainInterface как в payment-methods и documents; MarketplaceOfferPaginationResultDTO заменил MarketplaceOfferPageDTO; List*InputDTO наследуют PaginationInputDTO с page/limit/sortBy/sortOrder. — сообщения ошибок на пользовательский русский без терминов available/blocked/offer/withdraw; пользователь видит понятное «Недостаточно свободного количества», «Предложение неактивно», «Можно изменять только свои предложения». — baseline-категории: 8 продовольственных (овощи/фрукты, молочные, мясо/птица, рыба/морепродукты, хлеб/выпечка, бакалея, напитки, готовая еда) + «Прочее»; удалены услуги ремонта/доставки, хозяйственные, стройматериалы и прочие непродовольственные (вне MVP по PRD 3.2.7). --------- Co-authored-by: coopops <coopos@coopenomics.world> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Цифровой Кооператив
Платформа «Цифровой Кооператив» — комплексное программное обеспечение для управления кооперативными организациями на основе блокчейна EOSIO. Система обеспечивает полный цикл управления кооперативом: от регистрации пайщиков и электронного документооборота до проведения собраний и финансового учёта. Построена на принципах прозрачности, децентрализации и простой электронной подписи.
Проект является частью экосистемы Кооперативная Экономика.
Архитектура
| Компонент | Пакет | Описание |
|---|---|---|
| boot | @coopenomics/boot |
CLI для инициализации и управления блокчейн-инфраструктурой |
| cleos | @coopenomics/cleos |
Утилита командной строки для работы с блокчейн-кошельком |
| contracts | @coopenomics/contracts |
Смарт-контракты EOSIO на C++ |
| controller | @coopenomics/controller |
GraphQL API сервер (NestJS) |
| cooptypes | cooptypes |
Общие типы и интерфейсы блокчейн-контрактов |
| desktop | @coopenomics/desktop |
Рабочий стол кооператива (Vue 3 + Quasar) |
| factory | @coopenomics/factory |
Генератор юридических документов |
| migrator | migrator |
Утилита миграции данных |
| notifications | @coopenomics/notifications |
Библиотека уведомлений на основе Novu |
| parser | @coopenomics/parser |
Индексатор блокчейна через State History Plugin |
| sdk | @coopenomics/sdk |
TypeScript SDK для GraphQL API |
| setup | @coopenomics/setup |
Мастер первоначальной настройки |
Быстрый старт
Предварительные требования
- Node.js >= 20
- pnpm 9
- Docker и Docker Compose
- WeasyPrint (для генерации PDF)
Установка
pnpm install
Конфигурация
pnpm run setup
Интерактивный мастер создаст необходимые .env файлы для всех компонентов.
Запуск инфраструктуры
docker compose up -d
pnpm run reboot
Разработка
Бэкенд (controller + parser)
pnpm run dev:backend
Фронтенд (desktop)
pnpm run dev:desktop
Библиотеки (factory + cooptypes)
pnpm run dev:lib
Все сервисы одновременно
pnpm run dev:all
Примечание: установка пакетов производится только через фильтр:
pnpm add <пакет> --filter <компонент>
Тестирование
# Все тесты
pnpm run test
# Юнит-тесты (cooptypes, parser, notifications)
pnpm run test:unit
# Компонентные тесты (factory)
pnpm run test:component
# Интеграционные тесты (boot + blockchain)
pnpm run test:integration
Сборка
# Библиотеки (cooptypes, factory)
pnpm run build:lib
# Смарт-контракты
pnpm run build:contracts:all
# Desktop (SSR)
pnpm --filter @coopenomics/desktop run build
Лицензия
Продукт Потребительского Кооператива «ВОСХОД» распространяется по лицензии BY-NC-SA 4.0.
Разрешено делиться, копировать и распространять материал, адаптировать и создавать производные произведения при условии указания авторства и сохранения той же лицензии. Коммерческое использование запрещено.