Compare commits

...

845 Commits

Author SHA1 Message Date
ant a291ef06b4 On E16-korzina-zakaz-agregat: WIP backup before E16 cleanup: desktop+docs prior-session changes 2026-06-02 10:33:43 +00:00
ant 0a79d5330c index on E16-korzina-zakaz-agregat: c00f1920c4 [598-33][@ant] feat: SDK-обёртки корзины и checkout (селекторы, queries, mutations) — дать desktop типизированный клиент для работы с корзиной и оформлением заказа 2026-06-02 10:33:43 +00:00
ant c00f1920c4 [598-33][@ant] feat: SDK-обёртки корзины и checkout (селекторы, queries, mutations) — дать desktop типизированный клиент для работы с корзиной и оформлением заказа 2026-06-02 09:15:30 +00:00
ant 7f36d0d70b [598-33][@ant] feat: codegen ZEUS-клиента — добавить корзину/checkout/checkout_id Эпика 16 в клиент и заодно завести ожидавшую в дереве схему привязки штрих-кода склада (inventory-barcode) 2026-06-02 09:11:25 +00:00
ant e2b42f4b0b [598-33][@ant] feat: checkout-сервис оформления из корзины — построчное создание заказов с общим checkout_id, предвалидация баланса и сохранение непрошедшего остатка для повтора 2026-06-02 08:52:50 +00:00
ant 16e304fb57 [598-33][@ant] feat: backend корзины + checkout_id заказа-агрегата + КУ-фильтр каталога — перевести заказы на оформление через корзину по выбранному пункту выдачи 2026-06-02 08:46:32 +00:00
ant 5ba06a1295 fix(document): версионировать черновики по hash+block_num, не затирать
Двухподписный акт падал у второго подписанта: "Хэш метаданных не
совпадает с существующим подписанным документом". Причина — в storage
фабрики, не в генерации (генерация версий в разное время корректна).

saveDraft ключевал черновик только по hash (= sha256 PDF-тела). Тело
впечатывает created_at лишь до минуты, а meta.block_num тянется
getCurrentBlock() на каждой генерации. Две генерации одного акта в одной
минуте → один hash, разный block_num → upsert $set затирал предыдущую
версию meta. Второй подписант через buildDocumentAggregate читал черновик
по doc_hash и получал чужую (позднюю) meta → meta_hash не сходился с
подписью первого → SDK signDocument отбивал подпись.

Фикс (storage-only, генерацию не трогаем):
- MongoDBConnector.saveDraft — компаунд-ключ (hash + meta.block_num):
  все версии черновика сосуществуют, ничего не теряется.
- DocumentRepository.findByHash(hash, block_num?) — тянет точную версию
  по block_num, фолбэк на hash-only для легаси-черновиков.
- DocumentAggregator.buildDocumentAggregate — отдаёт второму подписанту
  ровно ту версию, которую подписал первый (по signedDoc.meta.block_num).

Общий фикс класса багов для всех двухподписных документов (capital/meet/
agreement), не только marketplace-АПП.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 18:31:51 +00:00
ant aa46cf8db8 feat(marketplace): выдача сгруппирована по заказчику вместо плоской таблицы
Стол выдачи был q-table на сотни строк — оператор не видел «кому что
отдать». Переделано в карточки по заказчику: одна карточка = получатель
(ФИО + аккаунт), внутри список единиц, разнесённый по стадии:
«К выдаче» (ACCEPTED_TO_COOP, открыть подписью председателя) и
«Ждут получения» (READY_TO_RECEIVE, ждём заказчика). Заказчики с тем,
что можно выдать сейчас, — наверх; итог по сумме и числу позиций.

Backend без изменений: два существующих статуса ленты и есть стадии
выдано/не-выдано с точки зрения оператора. QR-скан и шаг открытия —
прежние.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 18:01:35 +00:00
ant 12ee1aa19a fix(marketplace): ФИО поставщика в ожидаемых поставках вместо username
marketplaceListSupplierPickupOrders обогащал заказы без
withParticipantNames — supplier_name/orderer_name приходили null, и
фронт показывал account («ant») вместо ФИО. Резолвер уже ограничен
ролью Receiving и членством в КУ, поэтому резолв имён здесь допустим
(как в приёмке/выдаче). Схема не изменилась — codegen не нужен.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 17:58:01 +00:00
ant d9aa45e699 fix(marketplace): меню «⋮» на карточке склада не открывалось
BaseButton рендерит default-слот только при !iconOnly, а q-menu был
вложен именно в него — для icon-only кнопки меню не монтировалось.
Перенёс q-menu в слот #icon-left (рендерится всегда), меню привязано
к кнопке. Поправил вложенность q-list/q-item.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 17:52:31 +00:00
ant 4ead387131 feat(marketplace): склад участка переосмыслен как доска полок
Плоский список «позиция + поле Полка + кнопки» был непонятен: не было
ощущения, ГДЕ что лежит и зачем раскладка. Переделано в доску:

- колонка «Поступило» — принятое, ещё не разложенное;
- колонки-полки — что физически лежит на каждой полке;
- раскладка = перетащить карточку на полку (нативный DnD) или меню «⋮»;
- «+ Полка» создаёт новую полку; снять с полки = вернуть в «Поступило»;
- этикетка и раскладка по количеству — в меню карточки;
- штрих-код показан в карточке; отдельный лист — только для печати.

Цель страницы теперь читается: разложить так, чтобы при выдаче
заказчику сразу найти, на какой полке заказ. Backend без изменений
(assignInventoryShelf/splitInventory/generateInventoryLabel), без новых
зависимостей (DnD на HTML5). Канон: токены --p-*, убран хардкод #b3261e.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 17:29:45 +00:00
ant c89ca2784b fix(marketplace): перераскладка склада обратима + штрих-код в строке
Раскладка по полкам теперь перераспределяет ВЕСЬ непромаркированный
RECEIVED-пул заказа, а не один кусок: оператор может собрать имущество
с полок обратно (merge), разложить иначе (re-split) или перенести —
в одном действии «Разложить». Промаркированные единицы (штрих-код)
фиксированы и в пул не входят.

Backend: splitInventory собирает пул по order_id (непромаркированные
RECEIVED), сверяет сумму долей с суммой пула, схлопывает лишние куски
(новый repo.deleteById). Сигнатура GraphQL не изменилась — codegen не нужен.

Desktop: штрих-код показывается в той же строке, что имущество и полка
(нижний печатный лист скрыт на экране, разворачивается только при печати);
диалог раскладки предзаполняется текущим раскладом заказа по полкам.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 17:14:02 +00:00
ant fe9bb86328 feat(marketplace): подпись приёмки сводной поставкой (группировка актов)
Поставщик и председатель КУ видят и подписывают доставку ЦЕЛИКОМ одной
кнопкой, а не каждую единицу имущества по отдельности. Под капотом по каждому
акту приёма-передачи — отдельный документ и отдельная транзакция (блокчейн не
проведёт всё одной tx); подпись группы = последовательный цикл по receptions с
прогрессом «подписано N из M».

- shared/lib/marketplace/groupAplReceptions: группировка актов в сводную
  поставку (ПВЗ: поставщик+КУ+способ+статус; поставщик: КУ+способ+статус),
  агрегация строк по товару + сумма
- стол ПВЗ «Требуют подписи» и стол поставщика «Подпись приёмки» → сводные
  карточки, одна кнопка на группу
- оба диалога подписи принимают группу и подписывают циклом, показывая прогресс
  и частичные ошибки

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 16:50:44 +00:00
ant 691a00117b fix(marketplace): стол ПВЗ — агрегация поставок по поставщику + фикс повторной сдачи
- ожидаемые поставки агрегируются по поставщику (одна карточка = один
  поставщик на КУ): ФИО, состав по товарам (одинаковый товар суммируется),
  способы доставки, общая сумма
- диалог приёмки группирует единицы по товару (строки по заказчикам внутри),
  факт/цена правятся per-Order — бухгалтерия не меняется
- предлагаем к приёмке только заказы, чья партия ещё SUPPLY_PREPARED: заказы
  партий в RECEPTION_IN_PROGRESS (ждут подписи) повторно не показываем — иначе
  create падал «ожидался SUPPLY_PREPARED» при повторном приезде поставщика

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 16:21:50 +00:00
ant 33e27ef2db fix(marketplace): стол ПВЗ — ФИО поставщика + сумма в конец карточки
Карточка показывала аккаунт вместо имени — ФИО берём из состава (supplier_name
заказа), аккаунт остаётся в AccountBadge. Сумма перенесена из верхней строки в
итоговую «Сумма поставки» под составом (рубли — это итог, а не «привезёт по
факту», что относится к составу).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 16:01:39 +00:00
ant c5d91fc60e style(marketplace): стол ПВЗ — ожидаемые поставки в канон-карточках
Список ожидаемых поставок перевёрстан на тот же BaseCard-grid, что и акты на
подпись (Avatar + имя + AccountBadge, бейдж способа доставки, сумма mono, состав
в surface-2 блоке с получателем под товаром). Унифицированы классы __card* —
один паттерн карточки для поставок и актов (DRY).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 15:58:07 +00:00
ant fda8cd8aa8 fix(marketplace): стол ПВЗ — единый список ожидаемых поставок
Самовывоз по факту больше не отдельной таблицей: для ПВЗ нет разницы,
сформировал поставщик партию заранее или привезёт самовывозом — и то и другое
дойдёт до участка. Слиты в один computed expectedDeliveries; самовывоз показан
способом доставки «Самовывоз» без даты формирования.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 15:26:42 +00:00
ant 2c33b935ac style(marketplace): стол ПВЗ — карточки актов на подпись по канону + бизнес-хинт
«Требуют подписи» перевёрстаны на BaseCard-grid: Avatar + имя поставщика +
AccountBadge в шапке, статус-бейдж справа, состав в surface-2 блоке, действие
подписи в футере на hairline-разделителе. Хинт переписан человеческим языком.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 15:20:27 +00:00
ant e5ddf7c13e fix(marketplace): стол ПВЗ — убрать таблицу принятых актов, сжать хинт
Принятые кооперативом акты (ACCEPTED_TO_COOP) уже на складе — на столе приёмки
не нужны; таблица АПП заменена карточками только требующих подписи актов
(PENDING_*), показывается лишь когда есть что подписать. Убран дублирующий
заголовок «Ожидают приёмки» (раздел назван в шапке), хинт сжат до двух строк,
пустое состояние — в каноне через EmptyState.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 15:09:51 +00:00
ant 8fd251a84d feat(marketplace): стол ПВЗ — слить «Ожидаемые поставки» + «Приёмка» в одну страницу
Оператор не прыгает между «жду» и «принимаю»: единый стол показывает что/когда/кому
везут (дата формирования партии + получатель в карточках состава), запуск приёмки —
только скан QR/ввод кода, тут же статусы и подпись актов. Удалён избыточный
KPI-дашборд incoming-shipments и сама страница (api оставлен). defaultRoute → reception.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 15:03:45 +00:00
ant 12f645b3b4 feat(marketplace): стол ПВЗ — склад/приёмка/раскладка под приёмочную модель
Десктоп к приёмочной модели склада (Путь A):

- Приёмка партии: убраны кнопки-лаунчеры («Принять партию», «Принять
  самовывоз»). Приёмка запускается ТОЛЬКО сканом QR/вводом кода (идентификация
  обязательна). Ожидающие партии и самовывоз показаны карточками со составом
  («что везут» — товар + кол-во) без проваливания.
- «Маркировка имущества» → «Раскладка и маркировка»: список принятых позиций
  склада; на строке — полка (свободный input), «Разложить» (split одной позиции
  по нескольким полкам), «Этикетка» (опциональный штрих-код). Снесены выбор
  партии и стратегии PER_ORDER/UNIT/PACKAGE. Печать этикеток сохранена.
- Склад моего КУ: показывает всё принятое (статус RECEIVED «Принято»), колонка
  «Полка», штрих-код опционален (—), сортировка/возраст по received_at.

SDK-обёртки inventory перегенерированы под новые мутации.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 11:55:11 +00:00
ant b95a40d232 feat(marketplace): склад КУ — приёмочная модель (Путь A) + полки + раскладка
Корень: склад показывал только промаркированное (inventory рождался лишь на
маркировке). Теперь позиция склада рождается на приёмке кооперативом по акту
(ACCEPTED_TO_COOP), статус RECEIVED — склад показывает ВСЁ принятое, штрих-код
и полка опциональны.

Бэкенд (controller, DB-only composite, synchronize + миграция v9):
- inventory: barcode_value/format/labeled_* → nullable; ADD shelf, received_at,
  received_by_operator_account; новый статус RECEIVED (стартовый).
- apl-reception.signAsChairman материализует склад: на каждый принятый Order
  (fact_quantity>0) позиция RECEIVED (qty=факт, expiry=received_at+warranty_secs),
  идемпотентно по countByOrder.
- inventory-label сервис переписан под раскладку/маркировку: assignShelf,
  splitInventory (разложить позицию по нескольким полкам, RECEIVED-only),
  generateLabel (наклеить штрих-код RECEIVED→LABELED). Снесены стратегии
  PER_ORDER/UNIT/PACKAGE и пакетная маркировка партии.
- мутации: marketplaceAssignInventoryShelf / marketplaceSplitInventory /
  marketplaceGenerateInventoryLabel (взамен labelInventory/labelShipmentInventory).
- крон списания скоропорта сканирует RECEIVED+LABELED.

Codegen: schema → zeus-клиент → sdk build. tsc controller зелёный.

Десктоп (стол ПВЗ) — следующим коммитом в этой ветке.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 11:49:20 +00:00
ant dae50db188 feat(marketplace): ПВЗ — «Сканировать QR» в шапку приёмки
Кнопка сканирования кода передачи переехала из строки «Ожидают приёмки»
в шапку стола (канон Teleport, как «Мой код для ПВЗ» в подготовке
отгрузки) — единое предсказуемое место сверху.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 11:17:13 +00:00
ant ff6835826e feat(marketplace): стол поставщика — страница «Отгрузить партию» (Pickup-QR)
- Новый пункт меню «Отгрузить партию» после «Подготовки отгрузки»: один
  account-bound Pickup-QR во всю страницу (зеркало «Получить заказ» заказчика).
- Обобщил виджет ReceiveCode → HandoffCode (kind=Receive|Pickup, тексты в copy.ts):
  один контент/диалог на оба стола. Подключил у заказчика (страница + диалоги
  «Мои заказы»/деталь) и у поставщика (страница + диалог из шапки «Подготовки
  отгрузки» вместо inline-QR).
- Снёс мёртвый виджет CreateOrderDialog (дубль MarketplaceCatalog/OrderCreateDialog).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 10:56:32 +00:00
ant ec1ca903a5 fix(marketplace): код получения — диалог из шапки + единый виджет карточки партии
- «Получить заказ» вернул из переадресации в диалог (ReceiveCodeDialog),
  кнопка в шапке и на «Моих заказах», и на детали заказа — в одном месте;
  отдельная страница меню остаётся. Общий ReceiveCodeContent (DRY).
- OfferGallery: точки-навигация выключены по умолчанию (убраны со всех экранов).
- Вынес SupplyPartyCard — единая карточка партии для заказчика
  («Коллективный заказ») и поставщика («Входящие заказы»), вместо дублей.
- Подчистил мёртвые стили карусели/плейсхолдера в CatalogOfferCard и
  странице оферты.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 10:48:37 +00:00
ant b01652f8ae fix(marketplace): входящие заказы поставщика — единая карточка партии на всех стадиях
Убрана разнобойная вёрстка: накопитель показывался прогрессом, а принятая
партия — метрик-сеткой с сырым UUID партии + вложенными OrderCard'ами (две
разные сущности на одинаковые по смыслу поставки). Теперь все партии рисуются
ОДНОЙ карточкой: шапка с бейджем этапа, прогресс-бар всегда, состав компактными
строками (поставщику важен общий объём, не кто заказал — имена справочно, без
дробления на отдельные карточки), действия «Принять/Отклонить» — только пока
партия копится. Удалены метрик-сетка, UUID партии и вложенные OrderCard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 10:35:45 +00:00
ant 42cbc38a3d fix(marketplace): общий виджет OfferGallery (слайдер), деталь заказа просторнее, кнопка «Получить заказ» везде
- Вынесен переиспользуемый OfferGallery (карусель изображений) вместо inline-
  каруселей: каталог, деталь оферты и деталь заказа теперь используют один
  виджет (DRY). На детали заказа вместо одиночного q-img — полноценный слайдер.
- Деталь заказа: больше воздуха (увеличены gap'ы шапки/фактов, обложка 144px).
- Кнопка «Получить заказ» (ведёт на страницу QR) продублирована в шапке
  «Моих заказов» и в действиях детали заказа — код выдачи под рукой везде,
  не только на отдельной странице.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 10:32:33 +00:00
ant 597a9594f5 fix(marketplace): деталь заказа по канону (обложка + ActivityTimeline), правка подписи QR
- Детальная страница заказа: добавлена обложка товара (догруз оферты по
  offer_id), хронология переведена на канон-ленту ActivityTimeline вместо
  плоских строк, добавлен воздух над кнопкой «К моим заказам».
- Подпись на странице «Получить заказ»: убрано непонятное «показать заранее»
  → «показать с экрана телефона или с распечатки».

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 10:21:57 +00:00
ant 84a5f7a050 fix(marketplace): код получения — отдельный пункт меню «Получить заказ»
Убрал кнопку «Мой код получения» из шапки «Моих заказов» и вынес в отдельный
пункт меню «Получить заказ» (OrdererReceiveCode) после «Коллективного заказа»:
одна страница с account-bound QR-кодом на всю ширину и пояснением, больше
ничего. Так код очевидно findable — пайщику не нужно объяснять, где искать
его на пункте выдачи; пункт всегда виден в меню стола.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 10:09:18 +00:00
ant bc798b74f0 fix(marketplace): убрать стол «Готово к получению», свернуть в «Мои заказы» + детальная карточка
Сократили навигацию заказчика: получение перестало быть отдельной страницей.

- Удалён стол «Готово к получению» (OrdererReadyToReceive) из навигации.
- «Мой код получения» (account-bound QR) вынесен в шапку «Моих заказов».
- «Подписать и получить» (финальная подпись signiss2) — действие прямо в
  карточке заказа со статусом READY_TO_RECEIVE; диалог финализации переехал
  в MyOrders. READY_TO_RECEIVE стал обычным этапом общего списка.
- Клик по карточке открывает детальную страницу заказа (новый скрытый роут
  marketplace-order-detail, OrdererOrderDetail) на marketplaceGetOrder: состав,
  ПВЗ, хронология этапов, факт выдачи, те же действия.
- OrderCard: проп openable (клик по телу → emit open), действие receive.

Чистый фронт: marketplaceGetOrder/ListMyReadyToReceive/finalizeIssuance уже
есть, codegen не нужен. MarketplaceOrderView самодостаточен для диалога
(issuance_fact в общем orderSelector).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 10:04:28 +00:00
ant e2976c3a1c fix(marketplace): коллективный заказ — грамотные формулировки в UI
Убрал разговорные обороты («исчезает отсюда», «копится») из пустого
состояния и подсказки страницы; текст переписан нейтрально и грамотно.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 09:53:26 +00:00
ant b2efdda415 fix(marketplace): коллективный заказ — только партии до начала выдачи
Страница-трекер сбора показывает лишь партии на этапе накопления и
подготовки поставки (ACTIVE…ACCEPTED_TO_COOP). Открытые на выдачу
(READY_TO_RECEIVE и далее), полученные, возвращённые и отменённые заказы
уходят со страницы — её цель наблюдать накопление, а не историю.
Получение/история — на «Моих заказах». Пустое состояние переформулировано.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 09:52:14 +00:00
ant a55a20eb43 fix(marketplace): коллективный заказ — единая карточка партии на всех стадиях
Переделал страницу: все партии (на сборе и уже принятые) рендерятся ОДНИМ
шаблоном — шапка (товар + КУ + бейдж этапа), прогресс-бар сбора, свои заказы
строками. Убраны разные по вёрстке виды (метрик-сетка + большие OrderCard для
принятых), убран сырой UUID партии. Отличие стадий — только заполнение бара и
подпись этапа, а не разные сущности.

Бэкенд: group_accumulated_quantity/group_min_volume теперь заполняются на любой
стадии — для принятых партий коллективный объём берётся суммой по cycle_id
(новый агрегат sumByCycleIds), для собирающихся — суммой активного пула пары
оферта×КУ. Так прогресс-бар осмыслен на каждой карточке.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 09:40:31 +00:00
ant 56e41418c4 fix(marketplace): коллективный заказ — накопитель компактными строками, без дубль-карточек
В партии-накопителе выводил полные OrderCard, из-за чего название товара/КУ
повторялось в шапке партии и внутри — выглядело как забытая старая карточка.
Привёл к канону стола поставщика: накопитель показывает компактные строки
своих заказов (без больших карточек), большие OrderCard остаются только в
сформированной (принятой) партии. Накопитель и сформированная партия теперь
визуально разной формы, не читаются как близнецы.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 09:30:52 +00:00
ant 2706a16c46 feat(marketplace): коллективный заказ заказчика — прогресс сбора как у поставщика
Стол заказчика «Коллективный заказ» (бывш. «Сводный заказ») теперь зеркалит
карточку поставщика: партии-накопители показывают коллективный прогресс сбора
(накоплено всеми пайщиками / целевой минимум КУ), без ФИО других заказчиков.
КУ выводится человекочитаемым именем (delivery_point_name) вместо служебного
braname.

Бэкенд: Order-DTO обогащается group_accumulated_quantity (сумма ACTIVE по паре
оферта×КУ всех пайщиков) и group_min_volume (целевой минимум КУ) — новый
батч-агрегат sumActiveByOfferBranch + флаг withGroupProgress в display-сервисе.
Заодно ленты заказов (my/supplier) обогащаются именами участников
(withParticipantNames): на столе поставщика «кто заказал» теперь реально
показывает ФИО, а не аккаунт.

Codegen: generate-schema/client + zeus-клиент sdk и controller.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 08:45:04 +00:00
ant b8551e198a fix(marketplace): онбординг заказчика — один лоадер вместо двух
Во время подписания+перехода (redirecting) одновременно крутились два
спиннера: полностраничный q-inner-loading по центру и спиннер кнопки
«Подписать оферту» внутри gate. Прячем gate на время redirect — остаётся
один полностраничный лоадер.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 08:08:31 +00:00
ant 50eee97942 fix(marketplace): способ поставки «Самовывоз» → «Самоввоз»
В подготовке поставки поставщик сам привозит товар на ПВЗ — это «самоввоз»,
не «самовывоз» (самовывоз — когда забирает получатель).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 08:06:42 +00:00
ant 39a03df486 feat(marketplace): заказчик в лентах заказов — сокращённое ФИО вместо username
Колонка «Кто заказал» (входящие заказы поставщика) и «Заказчик» (выдача
оператора) показывали полное имя или, при пустом, username. Теперь —
сокращённое «Фамилия И.О.» через новый строковый хелпер formatShortFio
(заказчик всегда физлицо, поэтому сокращение безопасно). Полное ФИО
с бэкенда (orderer_name) остаётся доступным для документов.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 08:06:42 +00:00
ant 632643b788 feat(marketplace): модерация — переход на полную карточку предложения на столе администратора
Карточка в «Модерации» теперь кликабельна (курсор-pointer + hover) и
ведёт на полную страницу предложения в рамках стола администратора
(market-admin, hidden-маршрут marketplace-admin-offer-detail, Order:read:all)
— без перехода на стол заказчика. Модератор видит полное описание,
участки поставки с объёмами и гарантию.

Страница MarketplaceOfferDetailPage читает meta.readonly: в режиме модератора
прячет кнопку «Заказать» и диалог заказа, «назад» ведёт в «Модерацию».
Карточка модерации показывает категорию + ФИО поставщика (как в каталоге);
избыточный inline-блок деталей убран.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 08:01:59 +00:00
ant 3779571aba fix(marketplace): остаток «Моих предложений» — убрать двойное вычитание quantity_blocked
quantity_available уже исключает заблокированное количество (инвариант
counters: available + blocked + consumed = опубликовано; при заказе
available уменьшается сразу). Повторное вычитание quantity_blocked на
карточке давало двойное списание: опубликовал 100, заказали 1 → показывало
98 вместо 99.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 08:01:49 +00:00
ant abbcfb64d3 refactor(marketplace): шаг «Условия поставки» — короткий бизнес-текст
Убрано объяснение про партию/группировку/порог. Осталось: «Отметьте
кооперативные участки, на которые готовы обеспечить доставку, и укажите объём
поставки на каждое.» Подзаголовок шага — «Участки и объём поставки».

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 07:50:15 +00:00
ant c1b319ed4b fix(marketplace): ПВЗ-страница — value-импорт enum статусов (фикс зависания)
GeocodeStatus/KuDetailsStatus импортировались как `import type`, но используются
в рантайме (GeocodeStatus.OK и т.п.) — тип стирается при компиляции, отсюда
ReferenceError в hasCoords и краш рендера всей страницы после сохранения ПВЗ
(лоадер «крутился вечно», UI замерзал). Импорт переведён в value.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 07:38:49 +00:00
ant 0e68a46310 fix(marketplace): диалог ПВЗ — поля времени без резерва нижней полосы, по центру строки
hide-bottom-space на time-input'ах убирает зарезервированную пустую полосу под
сообщения — видимое поле центрируется по строке дня (align-items:center), а не
висит в верхней части высокой ячейки.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 07:30:14 +00:00
ant 4b518330e0 fix(marketplace): диалог ПВЗ — постоянная высота строки дня (без скачка)
Поля времени рендерятся всегда; при выключенном дне они disabled (серые), а не
скрыты по v-if. Высота строки одинакова для включённого и выключенного дня —
переключение больше не «скачет» по высоте.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 07:26:02 +00:00
ant 1751035016 fix(marketplace): диалог ПВЗ — имя участка в шапке + время в одной строке с днём
Заголовок диалога показывает человекочитаемое имя участка (branchName), а не
служебный braname. Режим работы: каждый день — одна строка (переключатель слева,
поля «Открытие/Закрытие» справа на той же линии); включение дня больше не
добавляет вторую строку и список не удлиняется.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 07:18:05 +00:00
ant 7afb20af73 fix(marketplace): онбординг ЦПП — единый список 1-2-3-4 + лоадер внутри карточки
Шаги 3–4 (участки, ПВЗ) больше не вынесены во вторую карточку «Дальнейшая
настройка» — они идут тем же сквозным списком после шагов совета (extraSteps
в CouncilOnboardingCard), нумерация продолжается. Доп.шаги видны всегда, в том
числе после утверждения документов.

Лоадер вынесен из оверлея на всё окно (WindowLoader, .absolute-full) внутрь
карточки (q-inner-loading с резервом высоты) — больше не «болтается» поверх уже
отрисованной страницы.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 06:33:23 +00:00
ant a97dc61258 feat(marketplace): страница деталей предложения + слим-карточка каталога
Карточка каталога несёт только категорию, поставщика (ФИО) и краткое
описание (≤200 символов); полное описание, участки поставки с минимальным
объёмом, гарантия и галерея раскрываются на отдельной странице предложения
по клику. Быстрый заказ остаётся на кнопке «Заказать».

- backend: query marketplaceGetOffer(id) (enrichment через field-резолверы)
- sdk: Queries.Marketplace.GetOffer + регенерация zeus-клиента
- desktop: новая страница MarketplaceOfferDetail + скрытый маршрут
  marketplace-offer-detail (offer/:offerId); CatalogOfferCard — категория +
  поставщик + отсечка описания; каталог открывает деталь по клику

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 06:17:19 +00:00
ant a8ce8de69a feat(marketplace): единицы измерения в объёмах поставки + имена вместо braname/account
«от 1» без единицы и сырой braname/аккаунт в карточках были непонятны.

- CreateMarketplaceOffer: поле «Мин. объём» получило суффикс выбранной единицы;
  превью участков поставки — «<КУ> (от N <ед.>)»; исправлен leftover-баг после
  SSOT — превью брало несуществующее `.label` вместо `.name` КУ.
- ChairmanModeration: «Участки поставки» теперь «<наименование КУ> (от N <ед.>)»
  вместо braname без единицы; «Поставщик» — ФИО (supplier_name) вместо аккаунта.

Имена/единицы берутся из backend-полей оферты (supplier_name, delivery_points.name)
и общего хелпера marketplaceUnitShort — без сторонних дозапросов.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 06:02:22 +00:00
ant d79cefa2c7 feat(marketplace): онбординг ЦПП — шаги 3–4 (создать КУ + назначить ПВЗ) со ссылками
После утверждения Советом двух документов председателю было неочевидно, что
дальше нужно добавить кооперативные участки и отметить их как ПВЗ. Добавил на
страницу онбординга блок «Дальнейшая настройка» с двумя шагами-подсказками:
- шаг 3 → ссылка на «Кооперативные участки» (route `branches`);
- шаг 4 → ссылка на «Пункты выдачи» (route `marketplace-issuance-points`).

Без дублирования функционала — только текст + кнопки-переходы на профильные
столы. Видны сразу, чтобы был понятен полный путь подключения.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 05:58:43 +00:00
ant 6ab07ae2ff refactor(marketplace): статусы КУ/геокода/оферты — enum через registerEnumType + Zeus
Строжайшее правило: статусы (status/geocodeStatus) — всегда enum, никогда
строковый литерал; объявляются на backend, прокидываются в Zeus и используются
на фронте. Раньше KU/geocode/offer статусы были `@Field(() => String)` +
сравнения по строкам — нет единого источника значений, легко опечататься/
забыть набор.

Backend:
- KuDetailsStatuses / GeocodeStatuses / MarketplaceOfferStatuses → registerEnumType
  (GraphQL enum MarketplaceKUStatus / MarketplaceGeocodeStatus / MarketplaceOfferStatus);
  `@Field(() => Enum)` вместо String.
- Все строковые литералы статусов в сервисах/адаптерах/мапперах KU/offer/order
  заменены на enum-члены (ku-details, moderation, order-create, order-cancel,
  catalog-resolver, offer/order repo-адаптеры, order-delta mapper).
- SetKUStatusInput → @Field(() => MarketplaceKUStatus) + @IsEnum.
- Geocoder-port union (OK/FAILED) и TypeORM @Column enum-DDL оставлены — это
  отдельный port-контракт и DDL-объявление, не surfaced-статусы.

Frontend:
- entities/MarketplaceKUDetails: KuDetailsStatus/GeocodeStatus = Zeus.MarketplaceKUStatus
  / Zeus.MarketplaceGeocodeStatus (единые значения с backend).
- AdminIssuancePoints / CreateMarketplaceOffer: сравнения geocode/статуса — на
  enum-члены (script + pug-шаблон).

Codegen: schema + zeus + sdk build. Unit КУ 12/12, tsc controller clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 05:52:06 +00:00
ant 299835e06a feat(marketplace): единый источник правды реквизитов КУ — реквизиты ПВЗ из организации участка
Корень рассинхрона: адрес/контакты участка хранились копией в ku_details и
правились отдельно на столе ПВЗ, расходясь с правкой председателя в
«Кооперативные участки». Теперь детализация ПВЗ их не хранит — наименование,
адрес и контакты резолвятся живьём из организации участка (единый источник
правды) через field-резолвер MarketplaceKUDetails (как supplier_name/точки
поставки у оферты).

Backend:
- ku_details: drop contact_phone/contact_email; address_full → geocoded_address
  (nullable) как внутренний кэш-ключ геокода (bootstrap-v8, synchronize:true).
- MarketplaceDetailKUInput больше не принимает адрес/контакты — только режим
  работы/описание; KuDetailsService берёт адрес из организации, ленивый
  reconcile геокода на чтении списка при дрейфе адреса.
- MarketplaceOrderDisplayService: контакты/адрес веток из организации,
  resolveBranchContacts; enrich заказов больше не читает локальный адрес КУ.
- Фикс DI: MarketplaceOrderDisplayService добавлен plain-class провайдером —
  field-резолверы оферты/КУ инжектят его по классу (латентный баг Chunk 1).

Frontend:
- Диалог ПВЗ: адрес/контакты read-only (правятся на «Кооперативные участки»),
  редактируются только режим работы/описание.
- CreateMarketplaceOffer: список КУ берёт name/address прямо из KU-details,
  без отдельного join branches.
- KU-details типы: name + nullable реквизиты; AdminIssuancePoints fallback.

Codegen: schema (83 резолвера) + zeus client + sdk build. Unit КУ 12/12.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 05:14:22 +00:00
ant 907ecf025a feat(marketplace): системное обогащение оферты именами на backend
Оферта теперь отдаёт отображаемые реквизиты живьём с бэкенда, чтобы фронт
не дозапрашивал их сторонними запросами:
- supplier_name — ФИО физлица/ИП или наименование организации поставщика;
- по каждому delivery_point: name (наименование участка) + address — живьём
  из организации кооперативного участка (единый источник правды, правится
  председателем в «Кооперативные участки»), lat/lng — из геокода КУ.

Реализовано field-резолверами на типах MarketplaceOffer и
MarketplaceOfferDeliveryPoint (как ленивый images-резолвер) — применяется
ко всем запросам, где возвращается оферта (каталог, мои предложения,
модерация), без дублирования. Резолв реквизитов участка переиспользует
MarketplaceOrderDisplayService (live-резолв, дедуп, best-effort); промис
кешируется на инстансе точки, чтобы 4 поля не дёргали БД повторно.

Codegen: schema + Zeus-клиент + sdk build; selector оферты расширен.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 04:47:04 +00:00
ant 964ec39a4c feat(desktop): наименование КУ + кнопка карты на шаге поставки оферты
В списке кооперативных участков при создании оферты вместо служебного кода
участка (braname, напр. iondlofolmqy) показываем человеческое наименование
филиала (short_name/full_name) и адрес. Рядом — маленькая кнопка карты
(icon-only), открывающая всплывашку MapView с точкой по координатам
геокодера — тот же паттерн, что в админке ПВЗ (AdminIssuancePoints).

Наименование берём из branchStore.loadBranches (join по braname), адрес и
координаты — из kuStore.details (геокодер). Кнопка карты только при
geocodeStatus OK и наличии lat/lng.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 04:24:23 +00:00
ant 6c82bb5068 refactor(desktop): убрать прямой GraphQL-запрос из ChairmanModerationPage
Страница модерации дёргала client.Query(ListCategories) прямо из .vue.
Перевёл loadCategories на типизированную обёртку fetchCategories из
MarketplaceCatalog/api (reuse, без дубля). Импорты client/Queries из vue убраны.

Канон: никаких client.Query/Mutation в .vue — только функции api/ со строгой
типизацией из @coopenomics/sdk IInput/IOutput.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 04:19:14 +00:00
ant eadddcb5e0 fix(desktop): передавать coopname в ListKUDetails на странице создания оферты
Страница создания предложения поставщиком грузила список КУ запросом
marketplaceListKUDetails с пустым data: {}, из-за чего GraphQL отклонял
запрос «Field "coopname" of required type "String!" was not provided» и
кооперативные участки не загружались.

Переведён на entity-store MarketplaceKUDetails (как AdminIssuancePoints):
coopname берётся из systemStore.info, onlyActive: true. Инлайн client.Query
с пустым data убран (DRY: используем готовую обёртку entity).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 04:17:00 +00:00
ant 005c03fccd chore(controller): убрать schema.gql из трекинга (он уже в .gitignore)
schema.gql — генерируемый артефакт (NestJS autoSchemaFile + generate-schema),
числился в .gitignore (строка 26), но был случайно затрекан → постоянный шум
в diff при регенерации. SDK build (typecheck+unbuild) его НЕ читает; читает только
скрипт generate-index-comments (docs/update:comments), которому нужна свежая
локальная генерация, а не закоммиченная копия.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 03:44:41 +00:00
ant 30cd8d4a54 feat(marketplace): Эпик 15 стол поставщика — партии-накопители с прогрессом сбора
OffererIncomingOrders переписан на партии вместо плоского списка заявок:
- ACTIVE-заказы группируются по «оферта × КУ» в партию-накопитель с прогресс-баром
  к min_supply_volume (цель сбора, не порог), краткий состав партии вместо россыпи карточек
- «Принять партию (N)»/«Отклонить» массивом, доступны всегда (принять можно < min)
- min ≤ 1 → поштучный режим без прогресс-бара
- принятые (cycle_id) → сформированная партия (метрики + readonly-карточки)
- фикс: вкладка «Ждут акцепта» → фильтр ['ACTIVE'] (раньше leftover ACCEPTED_PENDING_SUPPLIER*)
- min-карта из marketplaceListMyOffers.delivery_points (хелпер fetchSupplierMinVolumeMap)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 19:18:09 +00:00
ant 664dfe2441 refactor(marketplace): Эпик 15 desktop — КУ-чекбоксы + min-объём, batch-accept
- Форма оффера: вместо селектора типа поставки + target_volume — список КУ
  кооператива (BaseCheckbox + min-объём на каждый, через marketplaceListKUDetails);
  убраны cycleTypeOptions/isCollective/onSelectCycle/триггер-кнопка.
- Стол поставщика: accept/decline через единый batch-API (acceptOrdersBatch/
  declineOrdersBatch) по order_id[]; убрана cycle_id-маршрутизация.
- Сводный заказ заказчика, модерация: убраны cycle-лейблы → сводка по КУ.
- Удалены cycle-type-view алиасы, marketplaceCycleLabel; ESLint зелёный.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 18:45:15 +00:00
ant 9fa0bd6d16 refactor(marketplace): Эпик 15 — поставка по min-объёму на КУ (backend+contract+sdk)
Упразднён тип поставки (cycle_type/target_volume) как сущность. Управление
поставкой сведено к delivery_points оффера: список КУ + min_supply_volume на
каждом (мягкий ориентир группировки, не порог). individual/collective больше
нет нигде.

- Контракт: убран cycle_type из createorder (param + поле orders + namespace
  CycleType + валидация). Требует ребут dev-чейна + regen ABI.
- cooptypes: ICreateOrder/IOrder без cycle_type.
- Controller: offer.delivery_points (jsonb) вместо cycle_type/target_volume;
  order без cycle_type-снимка; единый batch-accept/decline по order_id[]
  (свёрнуты acceptIndividual/acceptConsolidated/declineConsolidated/
  triggerCollectiveSupply); удалены cycle-aggregator + consolidated-accept-decline
  сервисы; consolidated_request без cycle_type (создаётся в момент batch-accept).
- SDK: AcceptOrdersBatch/DeclineOrdersBatch мутации; delivery_points в
  offerSelector; schema.gql + zeus regen.
- Тесты контроллера обновлены, tsc --noEmit зелёный.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 18:34:01 +00:00
ant ce44625b25 Merge branch 'feat/marketplace-collective-supply' into marketplace2 2026-05-31 17:22:36 +00:00
ant 77fb19055a Merge branch 'dev' into marketplace2
Конфликты разрешены:
- app.module.ts: оставлены оба набора модулей (SignedDocumentsModule из dev
  + Marketplace* из marketplace2)
- workspace.ts (IWorkspaceRouteMeta): icon? опционален (dev, для hidden
  deep-link роутов) + grants-канон requires?/roles? опц. (marketplace2 —
  grants заменяют roles)

cooptypes пересобран (dev принёс SovietContract Registry.NewDeclined в
src, dist был stale). Контроллер tsc --noEmit зелёный.
2026-05-31 16:55:19 +00:00
ant 0b659c4b5a refactor(marketplace): rename DTO-файл trigger-open-subscription → trigger-collective-supply
Имя файла отставало от уже переименованного класса
MarketplaceTriggerCollectiveSupplyInputDTO; правлю импорт в
marketplace-cycle.resolver.ts.
2026-05-31 16:48:26 +00:00
ant 71dfa08e04 refactor(marketplace): frontend+sdk → 2 способа поставки (individual | collective)
SDK:
- mutation triggerOpenSubscription → triggerCollectiveSupply
  (MarketplaceTriggerCollectiveSupplyInput); barrel-export обновлён
- offerSelector: убраны удалённые поля cycle_days/max_wait_days/min_threshold

Desktop:
- конструктор оферты: 4 radio-карточки → 2 (individual | collective),
  target_volume опционален для collective (авто-старт по объёму либо
  ручной запуск); удалены 3 поля из формы/payload/prefill
- OrdererConsolidated: CYCLE_TYPE_LABEL/HINT → COLLECTIVE/INDIVIDUAL,
  STAGE_RANK без EXPIRED_NO_THRESHOLD/EXPIRED_NO_VOLUME
- OrderCard/MyOrders/OffererIncomingOrders/ProcessSupplyInfoWidget:
  выпилены отменённые статусы EXPIRED_NO_* и старые подписи циклов
- marketplace-units: подписи cycle → individual | collective

schema.gql не трогаю (gitignore в dev, войдёт на мердже).
2026-05-31 16:45:33 +00:00
ant e6c2aadc52 Merge pull request 'feat: Эпик 14 (14.1+14.5) — единый явный путь отгрузки (самовывоз/экспедитор+ТТН)' (#50) from feat/E14-explicit-shipment into marketplace2
Reviewed-on: #50
Reviewed-by: Алексей Муравьев <chairman.voskhod@gmail.com>
2026-05-31 16:18:59 +00:00
ant be429b9335 refactor(marketplace): способы поставки → 2 режима (individual | collective)
Свёл cycle_type с 4 значений (time_based/volume_based/open_subscription/
individual) к двум: individual и collective (коллективная закупка).

Коллективная закупка:
- target_volume опционален: задан → авто-старт партии при наборе объёма;
  не задан → только ручной запуск поставщиком.
- ручной «Запустить поставку» доступен любой collective-оферте
  (снят гейт open_subscription).
- жёсткого таймера нет: заказчик выходит сам в любой момент до акцепта.

Удалено: поля cycle_days/min_threshold/max_wait_days; cron-агрегаторы
aggregateTimeBased + aggregateVolumeBasedExpired; статусы партии/ордера
EXPIRED_NO_THRESHOLD/EXPIRED_NO_VOLUME; репо-методы listAllActiveTimeBased/
listAllActiveVolumeBased/findExpiredTimeBased.

Переименовано: GraphQL-мутация marketplaceTriggerOpenSubscription →
marketplaceTriggerCollectiveSupply; aggregator evaluate/trigger-методы.
Сохранён EXPIRED_NO_RESPONSE (истечение окна акцепта) + cron
expireUnacceptedPending.

tsc/eslint чисто; unit-тесты обновлены и зелёные. Контракт p.mkt.supply
(2-значный CycleType) уже в базовой ветке. schema.gql + zeus-клиент +
фронт — отдельным шагом (codegen).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 15:58:23 +00:00
ant 8c18e1c0de refactor(marketplace-contract): cycle_type → individual | collective
Способы поставки сведены к двум: INDIVIDUAL (заказ обслуживается отдельно)
и COLLECTIVE (коллективная закупка — копится партия, старт по объёму или
ручному запуску). Убраны timebased/volumebased/opensubscr. Дефолт order.
cycle_type = INDIVIDUAL, валидация createorder сужена до двух значений.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 15:31:01 +00:00
ant 20bba44e76 fix(marketplace): меню-страницы — убрать верхний отступ над PageTabs
Меню-вкладки (каталог, мои заказы, входящие заказы) — первый блок
страницы; верхний padding q-page давал лишний зазор над саб-навигацией.
Гасим padding-top: меню прижимается к топбару, контент ниже разводит
flex-gap. PageHint при dismiss не рендерится — пустого слота нет.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 14:33:47 +00:00
ant e82c789500 fix(marketplace): OperatorBranchBar — адрес в hint селектора, без двойного зазора
При выборе КУ адрес шёл отдельной строкой под BaseSelect, а поле уже
резервирует место под hint (reserve-hint-space) → двойной зазор и
рассинхрон с центрированными иконкой/бейджем. Адрес кладём в :hint поля,
бейдж прижимаем margin-left:auto в обоих режимах.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 12:11:10 +00:00
ant 74850036f0 fix(marketplace): ttn_number varchar(32)→64 + UX подготовки отгрузки
- ttn_number в shipment/apl_reception: 32→64. Номер ТТН
  COOPNAME-TTN-<cycle8>-<ku6>-<suffix8> ≈ 36–42 символов переполнял
  varchar(32) → «value too long» при формировании партии экспедитора.
  Требует рестарта контроллера (synchronize применит ALTER, увеличение
  длины безопасно).
- CreateShipmentDialog: maximized для удобной работы с dual-list.
- OperatorBranchBar: при выборе из нескольких участков убран дублирующий
  крупный заголовок (название КУ повторялось в селекте «Участок-Участок»);
  селектор стал основным идентификатором, адрес — под ним. Один участок —
  по-прежнему текстом.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 11:42:53 +00:00
ant 9f7173fc26 feat(marketplace): S3 — редизайн «Подготовки отгрузки» (глобальная кнопка + dual-list)
Стол подготовки отгрузки переведён на модель брифа E14:
- Глобальная кнопка «Сформировать партию» в шапке (Teleport #header-actions-host)
  вместо per-cycle кнопок. Лист сформированных партий — основной вид стола.
- Новый диалог формирования: способ доставки (самовывоз / экспедитор по ТТН) →
  один кооперативный участок → dual-list заказов («Переместить всё» + откат
  отдельных строк назад). Грузим всё, что справа; невыбранное остаётся ACCEPTED.
  Гранулярность целая — количество в заказе не дробим.
- Submit группирует выбранные заказы по заявке (cycle_id) и шлёт createShipment
  по каждой заявке с order_ids-подмножеством (backend S1). Для экспедитора —
  поля ТТН (на каждую партию печатается QR приёмки, S2).
- groupAcceptedByKu(): группировка акцептованных заказов по КУ через все заявки.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 10:36:10 +00:00
ant a700422dd1 feat(marketplace): S2 — приёмка партии экспедитора по QR из ТТН
Экспедитор не пайщик (нет аккаунта/кабинета) — приёмку открываем строго по
партии из накладной, а не по account-коду поставщика.

- HandoffTokenKind.Shipment + формат blago:shipment:<coopname>:<shipment_id>
  (shipment-bound, не account-bound). decode/encode поддерживают оба вида.
- TTNPrintPreview: QR-код приёмки партии на печатном листе ТТН (PNG data-URL,
  встраивается в печать и в скачанный самодостаточный HTML).
- buildTtnData: состав ТТН по order.shipment_id (fallback на (cycle,КУ) для
  старых партий) + qrValue = shipment-bound токен.
- OperatorReception: ветка скана QR ТТН → openPickupForShipment(id) грузит
  СТРОГО состав одной партии, без добора по акцепту («ничего больше»).
  Приёмка/план/маппинг переведены с группировки по cycle_id на shipment_id
  (при нескольких частичных партиях на одном КУ cycle_id брал не ту партию).
- MarketplaceOrder GraphQL DTO + orderSelector: поле shipment_id (нужно фронту
  для фильтрации состава партии). Схема + zeus перегенерированы.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 10:31:24 +00:00
ant 07f94e4be3 feat(marketplace): S1 — частичное формирование партий (подмножество заказов)
Эпик E14: «отделить акцептованное от реально погруженного». Поставщик
формирует партию из ПОДМНОЖЕСТВА акцептованных заказов КУ, остальное
остаётся ACCEPTED для следующей партии.

Модель: партия = (КУ, вариант доставки, подмножество заказов); на одном
(cycle, КУ) допустимо несколько частичных партий (self+expeditor split,
догрузка остатка). Невключённые заказы остаются ACCEPTED.

Изменения (аддитивно — старый фронт совместим):
- order.shipment_id: связь заказ→партия. Приёмка резолвит состав партии
  по shipment_id (findByShipmentId), а не инференцией по (cycle, КУ) —
  обязательно при нескольких партиях на одном КУ. Fallback на (cycle,КУ)
  для партий, созданных до появления связи.
- MarketplaceShipmentGroupInput.order_ids?: подмножество заказов (пусто →
  все ACCEPTED заказы КУ, как раньше).
- createShipment: снят 1:1-инвариант покрытия всех КУ и cycle-level
  idempotency (ConflictException). Повторное формирование разрешено;
  guard `shipment_id IS NULL` в assignToShipment не даёт включить заказ
  в две партии. total_amount по подмножеству.
- assignToShipment(): bulk ACCEPTED→SUPPLY_PREPARED + привязка к партии.
- createExpress: привязка через assignToShipment (консистентно).
- schema.gql + zeus-клиент перегенерированы.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 10:18:58 +00:00
ant f6594415cd fix(desktop): убрать нерабочую кнопку «Отгрузить» с карточки заказа поставщика
Действие ship в OrderCard (offerer/paid) не обрабатывалось ни одним
родителем — кнопка ничего не делала. Отгрузка идёт через формирование
партии на странице «Подготовка отгрузки», не с карточки заказа.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 09:37:41 +00:00
coopops ed9f620e2c fix(desktop): открывать первую страницу стола при выборе из command palette
Предыдущий фикс снимал лоадер, но onSelectWorkspace пушил на mainRoute.name —
родительский layout-роут без компонента → серый экран. WorkspaceSwitcher
работает потому что зовёт goToDefaultPage(), который через getDefaultPageRoute()
резолвит реальную лист-страницу (authorized_default_route / defaultRoute /
первый child) И сбрасывает isWorkspaceChanging. Используем его же.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 09:29:36 +00:00
coopops e810644386 fix(desktop): открывать первую страницу стола при выборе из command palette
Предыдущий фикс снимал лоадер, но onSelectWorkspace пушил на mainRoute.name —
родительский layout-роут без компонента → серый экран. WorkspaceSwitcher
работает потому что зовёт goToDefaultPage(), который через getDefaultPageRoute()
резолвит реальную лист-страницу (authorized_default_route / defaultRoute /
первый child) И сбрасывает isWorkspaceChanging. Используем его же.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 09:29:15 +00:00
ant 132444af81 Merge remote-tracking branch 'origin/marketplace2' into feat/E14-explicit-shipment 2026-05-31 09:23:01 +00:00
coopops a71af4ca29 fix(desktop): снимать full-page лоадер при переключении стола из command palette
selectWorkspace() ставит isWorkspaceChanging=true (full-page WindowLoader
поверх router-view), а сбрасывал флаг только goToDefaultPage(). Обычное меню
(WorkspaceSwitcher) зовёт goToDefaultPage и лоадер снимается; command palette
(onSelectWorkspace/onSelectPage) делал прямой router.push без сброса флага —
лоадер висел вечно. Тот же leak в ConnectionAgreementStepper.goToQuickClient.

Снимаем флаг через setWorkspaceChanging(false) в .finally() после навигации.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 09:19:45 +00:00
coopops 264d592033 fix(desktop): снимать full-page лоадер при переключении стола из command palette
selectWorkspace() ставит isWorkspaceChanging=true (full-page WindowLoader
поверх router-view), а сбрасывал флаг только goToDefaultPage(). Обычное меню
(WorkspaceSwitcher) зовёт goToDefaultPage и лоадер снимается; command palette
(onSelectWorkspace/onSelectPage) делал прямой router.push без сброса флага —
лоадер висел вечно. Тот же leak в ConnectionAgreementStepper.goToQuickClient.

Снимаем флаг через setWorkspaceChanging(false) в .finally() после навигации.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 09:14:41 +00:00
ant a5441a02ab fix(desktop): marketplace — центрировать EmptyState в пустых таблицах
Тот же фикс, что для «Ожидаемых поставок»: слот #no-data у q-table
выравнивает контент влево, а .empty — блок по ширине контента.
Оборачиваю EmptyState в .<block>__nodata (full-width flex center) на
страницах Приёмка / Выдача / Склад участка / Выплаты совета / Готово
к получению.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 09:14:08 +00:00
ant c171664077 fix(desktop): ожидаемые поставки — центрировать EmptyState в пустой таблице
Слот #no-data q-table выравнивает контент влево, из-за чего EmptyState
(«Ожидаемых поставок нет») прижимался к левому краю. Обёрнут в full-width
flex-контейнер с justify-content:center.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 09:09:13 +00:00
ant 10f96ee26c chore(desktop): удалить осиротевший OfferDetailsDialog + clickable у карточки
- Виджет OfferDetailsDialog больше нигде не используется (модерация перешла на
  inline-карточку) — удалён.
- CatalogOfferCard: prop clickable (default true) гейтит mp-card--interactive
  (cursor-pointer + hover-zoom) и emit click. Каталог и «Мои предложения» клик
  используют — у них без изменений; модерация передаёт clickable=false, чтобы
  карточка не выглядела кликабельной (по ней ничего не открывается).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 08:52:34 +00:00
ant af82016744 fix(desktop): модерация — данные предложения в карточке, без диалога
Страница модерации открывала по клику OfferDetailsDialog с полным описанием и
кнопками одобрить/отклонить — лишний клик. Вдобавок диалог показывал
плейсхолдер вместо картинок: selectedDetail не маппил preview/images, а диалог
рендерит offer.preview (карточка же тянет carousel через marketplaceOfferImageUrls).

Теперь решение принимается прямо в карточке:
- CatalogOfferCard получил слот #details (после описания) — родитель кладёт туда
  доп. данные, не открывая отдельный диалог.
- Модерация заполняет #details мета-строками (категория, тип отсечки, гарантия,
  поставщик) и выносит обе кнопки (Отклонить + Одобрить) в #actions карточки.
- Клик-открытие диалога, selectedDetail, openDetails и сам OfferDetailsDialog с
  страницы убраны; подсказка «нажмите на карточку» тоже.
- Подписи cycle_type вынесены в общий marketplaceCycleLabel (shared/lib/consts) —
  карта дублировалась в OfferDetailsDialog/CreateMarketplaceOffer/OrdererConsolidated.

Виджет OfferDetailsDialog больше нигде не используется (осиротел) — оставлен на
случай detail-view в каталоге; удалять отдельным решением.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 08:43:16 +00:00
ant f411f658a2 fix(desktop): L3-онбординг Стола заказов — подпись сразу ведёт на стол без экрана-поздравления
На столе заказчика после подписи оферты ЦПП requires_gate становился false →
gate скрывался → на время loadDesktop + переустановки маршрутов + router.push
мелькал баннер «Вы уже подключены к Столу заказов». Пользователь не хочет
промежуточный экран — подписал, сразу на стол.

Добавлен флаг redirecting: выставляется в начале onAccept, прячет баннер
alreadyDone и держит q-inner-loading спиннер до самой навигации. Убран
SuccessAlert-поздравление — редирект на стол и есть подтверждение. Баннер
«уже подключены» остаётся для легитимного захода на онбординг-URL уже
подключённым пайщиком (redirecting=false).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 07:48:51 +00:00
ant eb3751bad2 fix(desktop): онбординг ЦПП — авто-обновление столов по завершении
После утверждения Советом обоих документов ЦПП «Стол заказов» расширение
подключается и grants в getDesktop меняются, но фронт перечитывал desktop
workspace только на init и при install/enable расширения — для онбординга
триггера не было. Итог: рабочие столы и страницы Стола заказов появлялись лишь
после ручной перезагрузки страницы.

Добавлен watcher на isCompleted в useMarketplaceOnboarding: при переходе
false→true (оба шага completed) вызывается desktopStore.loadDesktop(), который
перечитывает getDesktop с новыми grants. Меню/столы появляются сразу.
loadDesktop идемпотентен (мерджит маршруты); watch не срабатывает на initial
mount, только на реальном завершении онбординга в течение сессии.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 07:31:50 +00:00
ant da9e125a21 fix(desktop): онбординг ЦПП — лоадер на отправке решения + карточка во всю ширину
Страница «Подключение ЦПП Стол заказов»:
- Лоадер на «Объявить»: карточка использовала локальный submitting и
  закрывала диалог сразу после синхронного emit('step-submit'), пока
  реальная async-отправка проекта решения шла в composable (handleStepSubmit)
  с отдельным submitting, не пробрасываемым в карточку. Итог — диалог
  схлопывался мгновенно, лоадера не было нигде. Теперь submitting проброшен
  из composable в CouncilOnboardingCard: кнопка «Объявить» крутит лоадер,
  диалог держится открытым до завершения транзакции и закрывается по watcher'у
  (submitting true→false).
- Ширина: убран max-width 860px у карточки онбординга — растягивается на всю
  ширину страницы.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 07:12:08 +00:00
coopops 3521881fd2 fix(desktop): выделение текста — solid accent вместо болотного teal
Глобальный ::selection был --p-primary-soft (teal 10% alpha) — болотный,
почти невидим. Меняем на solid --p-accent + белый текст: видно на любом фоне.
Тост-специфичные ::selection-override (.q-notification/.toast) убраны —
accent читаем и на тёмной поверхности тоста, выделение единое везде.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 07:07:07 +00:00
coopops 7d43ef9668 fix(desktop): выделение текста — solid accent вместо болотного teal
Глобальный ::selection был --p-primary-soft (teal 10% alpha) — болотный,
почти невидим. Меняем на solid --p-accent + белый текст: видно на любом фоне.
Тост-специфичные ::selection-override (.q-notification/.toast) убраны —
accent читаем и на тёмной поверхности тоста, выделение единое везде.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 07:06:02 +00:00
ant bc989f47d1 Merge pull request '[C28-21] Реестр подписанных документов в Postgres: getDocuments+searchDocuments на PG, newdeclined, авто-backfill' (#47) from feat/C28-21-pg-signed-documents into dev
Reviewed-on: #47
2026-05-31 06:21:18 +00:00
ant cec2f0219a fix(ledger2): синхронизация cooptypes operations.ts с canonical marketplace-операциями
operations.ts — рукопашное зеркало contracts/.../operations.hpp OPERATION_REGISTRY
(генератора нет: gen:from-cpp покрывает только wallets.generated.ts). Зеркало
устарело — держало legacy клиринговые o.mkt.supply/o.mkt.recv (p.mkt.reqst),
которых нет в контракте. Из-за этого MARKETPLACE_OP_CODES (фильтр по
contract==='marketplace') не знал o.mkt.purch/return/... → инвариант I3
(счёт 10 Материалы) считал их немаркетплейсными и падал.

- operations.ts: заменены 2 legacy-записи на 9 canonical marketplace-операций
  (lock/conv/lockm/unlock/purch/payout/consum/return/wroff), включая новые из
  R#1 PR #50: o.mkt.conv (паевой→членский «Стола заказов») и o.mkt.lockm
  (добор резерва с членского). unlock/return → w.mkt.member (членский программы),
  синхронно с operations.hpp.
- process-hash-locator.ts: удалён мёртвый locator p.mkt.reqst (больше не
  референсится из реестра).
- marketplace-process-trace-coverage.spec.ts: EXPECTED-списки приведены к
  9 операциям; walletTo unlock/return w.wal.member→w.mkt.member.

Тесты: marketplace-ledger2-invariants.spec.ts + marketplace-process-trace-coverage.spec.ts
= 73 passed (ранее 2 I3-теста были красные на baseline). Сигнатуры actions/ABI
не менялись (операции ledger2 внутренние) — regen schema/sdk не требуется.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 05:51:20 +00:00
ant 51c313019b fix(marketplace): инвариант I5 ledger2 — резерв учитывает новые потоки членского «Стола заказов»
После переориентации денежных потоков на w.mkt.member (ревью PR #50 #1):
- I5 reserve-consistency: добавлен o.mkt.lockm (w.mkt.member→w.mkt.order) как
  приход резерва; o.mkt.unlock теперь сверяется по walletTo=w.mkt.member.
- spec-фикстуры unlock/return переведены на w.mkt.member.

27 тестов проходят (как на baseline). 2 теста I3 (счёт 10) остаются красными —
предсуществующий разрыв: cooptypes/ledger2/operations.ts не содержит канонических
marketplace-операций (только legacy o.mkt.supply/recv), из-за чего MARKETPLACE_OP_CODES
их не распознаёт. Полный синк operations.ts с operations.hpp (+ o.mkt.conv/lockm) —
отдельный фоллоуап (затрагивает process-hash-locator/ledger2.service).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 05:40:21 +00:00
coopops 6e388e3196 [C28-21][@ant] fix: icon в IWorkspaceRouteMeta опционален для hidden-роутов
Typecheck / desktop (pull_request) Successful in 13m52s
Typecheck / controller (pull_request) Successful in 13m12s
Страницы deep-link (карточка документа, name=document-details/
user-document-details) помечены meta.hidden=true и в навигацию не
попадают — иконка им не нужна. Drawer рендерит q-icon под v-if='item.icon',
undefined безопасен. Убирает TS2741 'Property icon is missing'.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 05:37:11 +00:00
ant 9284cdb564 feat(marketplace): членский кошелёк «Стола заказов» — конвертация паевой→членский на доплате, возвраты в программу (ревью PR #50 #1)
Денежные потоки заказа деноминируются на членском «Стола заказов»
(w.mkt.member), а не на универсальном членском, и доплата по факту НЕ
списывается с паевого напрямую:

- ledger2: новый кошелёк w.mkt.member (USER_SHARED, program 2) + операции
  o.mkt.conv (паевой→членский «Стола заказов», Дт 80/Кт 86) и o.mkt.lockm
  (членский→резерв заказа).
- signiss2 при факт>план: o.mkt.conv(diff)+o.mkt.lockm(diff) вместо прямого
  списания с паевого; при факт<план остаток → w.mkt.member.
- o.mkt.unlock (отмена/decline/expire/недовыдача) и o.mkt.return (гарантийный
  возврат) переориентированы с w.wal.member на w.mkt.member.
- стандарты p.mkt.supply/p.mkt.return: кошельки, операции, проводки и проза.
- cooptypes wallets.generated.ts регенерирован (17 кошельков, 8 mapping).

Контракт собирается (constexpr-валидаторы реестра ledger2 зелёные); деплой
wasm/abi — отдельным шагом пайплайна. Подпись signiss2 не менялась — conv/lockm
суть ledger2-операции, не контракт-экшены: cooptypes-actions/codegen не затронуты.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 05:34:34 +00:00
ant a12986a079 fix(marketplace): ревью PR #50 — строгая типизация API из SDK, префикс blago, убран глобальный shims
- desktop API (OperatorReception/OperatorIssuance): все обёртки client.Query/
  Mutation принимают `data: IInput['data']` из SDK и передают `{ data }` целиком,
  без позиционных аргументов и разворачивания полей (канон desktop). Call-sites
  обновлены (приёмка/выдача/подпись/председатель КУ).
- handoff-token: префикс кода передачи `mp1` → `blago`.
- удалён глобальный src/shims-marketplace-qr.d.ts: типы `qrcode` через @types/qrcode
  (dev-dep), `BarcodeDetector` — узким локальным типом в QrScanner (нет в lib.dom).

Фронт-only, codegen не нужен.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 16:19:23 +00:00
coopops 42d3fc46e0 [C28-21][@ant] fix: открытие документа из карточки по hash (подписанный), а не doc_hash
Typecheck / desktop (pull_request) Failing after 8m20s
Typecheck / controller (pull_request) Successful in 12m31s
Карточка реестра вела на страницу документа, передавая rawDocument.hash — а это
doc_hash (хеш самого документа, без подписи). Бэкенд же ищет документ по колонке
hash (подписанный документ = содержимое + подпись): UPPER(d.hash)=UPPER(:hash).
Значения разные → getDocuments возвращал 0 → «Документ не найден».

Из поиска работало, потому что searchDocuments отдаёт именно hash (колонку), а не
doc_hash. Карточка теперь берёт тот же document.hash (= колонка hash в реестре).

Проверено по БД: фильтр по doc_hash → 0 строк, по document.hash → 1; у всех
записей document.hash == колонка hash.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 16:17:30 +00:00
ant 923532bed7 fix(marketplace): приёмка партии из списка — через единый диалог коррекции (ревью 2026-05-30)
Кнопка «Создать акт приёмки» на ожидающей партии звала createAplReception
напрямую, без формы сверки — оператор принимал по заказанным кол-вам и
исходным ценам (риск «приняли как 100, хотя привезли 80»). Перенаправлена в
openPickupForSupplier («Принять партию») — тот же диалог коррекции кол-ва/цены,
что и QR-скан и «самовывоз по факту». Коррекция доступна на ВСЕХ путях приёмки
без исключений. createReceptionForShipment удалена. Фронт-only, codegen не нужен.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 16:05:32 +00:00
coopops 716eb4fe5a [C28-21][@ant] feat: реестр документов на канон-карточки + липкая кнопка «назад»
Typecheck / desktop (pull_request) Failing after 8m31s
Typecheck / controller (pull_request) Successful in 12m49s
Реестр документов переведён с таблицы на канон плоские строки (DocumentRow):
строка кликабельна и ведёт на отдельную страницу документа. Таблица с
инлайн-раскрытием оставлена только для Union-страницы (expand=true).

- DocumentCardsList: список DocumentRow (заголовок, дата, ФИО подписантов,
  скачивание в actions), скелетоны при загрузке, «Загрузить ещё». Статус-чип
  не показываем — в реестре всё подписано (канон stop-signal).
- ListOfDocumentsWidget: проп expand — карточки по умолчанию (совет/пайщик),
  таблица для Union (передаёт expand=true) — поведение Union не меняется.
- useDocumentNavigation перенесён из фичи поиска в entity Document
  (переиспользуют и поиск, и карточный список — чистые слои).
- DocumentDetailsPage: кнопка «назад» теперь в липком баре (sticky,
  top=--p-topbar-h), видна всегда при прокрутке документа.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 16:04:31 +00:00
ant d50e0f85a2 feat(marketplace): ФИО/наименование участников (B3) + сверка-таблица в диалогах подписи приёмки (B4)
B3 — backend-обогащение display-сервиса именами участников (ФИО физлица/ИП
или short_name организации) через UserCertificateInteractor. Резолв только в
уже авторизованных резолверах приёмки/выдачи (оператор/председатель КУ или сам
поставщик), приватные имена не утекают в общие ленты заказов.
- MarketplaceOrder: orderer_name + supplier_name (заполняются в лентах выдачи).
- MarketplaceAplReception: offerer_name; позиции fact_quantity_per_order несут
  product_name + unit_of_measure (для таблицы сверки).
- order-repo: findByIds (батч для обогащения позиций приёмки).
- desktop: выдача — колонка «Заказчик» = ФИО; приёмка — заголовок = наименование
  поставщика.

B4 — общий read-only виджет ReceptionLinesTable (позиции/кол-во/цена/сумма) +
кнопка «Показать акт» (рендер html подписываемого документа) в диалогах подписи
поставщика и председателя. У председателя сверка строго на чтение.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 15:53:11 +00:00
coopops 7f65478dc5 [C28-21][@ant] fix: раздел документа выводить из имени роута, не из workspace
Typecheck / desktop (pull_request) Failing after 8m32s
Typecheck / controller (pull_request) Successful in 12m33s
activeWorkspaceName инициализируется асинхронно — при холодном переходе по
прямой ссылке на документ совета скоуп мог упасть в скоуп пайщика и документ
«не находился». Имя роута (document-details / user-document-details) доступно
сразу и детерминированно.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 15:41:23 +00:00
coopops 0cbc76b018 [C28-21][@ant] feat: отдельная canon-страница документа вместо фильтра реестра
Typecheck / desktop (pull_request) Failing after 8m24s
Typecheck / controller (pull_request) Successful in 12m42s
Прошлый подход (фильтр реестра по ?document=<hash> + баннер) отклонён —
нужна полноценная отдельная страница документа с переходом назад и deep-link.

- DocumentDetailsPage: canon-страница по эталону карточки собрания (back-link
  под шапкой, имя документа в заголовок через setPageTitleOverride, скелетон,
  EmptyState; контент — ComplexDocument). Грузит документ сама по hash из роута,
  поэтому открывается и прямым переходом по ссылке.
- Роуты documents/:hash в обоих расширениях: document-details (совет) и
  user-document-details (пайщик), hidden — по аналогии с meet-details.
- DocumentModel.loadDocument(username, hash): точечная загрузка одного документа
  (статус-агностичный фильтр newsubmitted), не трогает состояние списка.
- Клик в поиске ведёт на страницу документа (useDocumentNavigation выбирает роут
  по рабочему столу), а не фильтрует реестр.
- Откат фильтр-подхода: удалены SearchedDocumentBanner и useDocumentRouteFilter,
  страницы реестра/виджет/таблица возвращены к baseline.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 15:39:48 +00:00
ant 449e960e43 feat(marketplace): правка цены и количества на express-приёмке самовывоза
Коррекция фактического количества и цены теперь доступна оператору на ВСЕХ
путях приёмки без исключений, включая express-самовывоз (добор по акцепту и
"самовывоз по факту" без партии). Раньше express принимался по цене и
количеству заказа без возможности правки.

- createExpressReception проводит fact_quantity_per_order сквозь create →
  signsupp/signchair (контракт не меняется — переиспользуется B2).
- desktop: addon-юниты в диалоге приёмки редактируемы (кол-во+цена); кнопка
  "Принять самовывоз" открывает ту же форму коррекции, что и QR-скан.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 15:34:09 +00:00
ant 544ff598e2 feat(marketplace): корректировка цены на приёмке и выдаче (B2)
Оператор правит при открытии не только количество, но и цену за единицу
(привезли хуже/замена → принимаю со скидкой; цену меняю на месте).
Канон-имя параметра — actual_unit_price (asset), достраивает пару к
actual_quantity.

Контракт:
- signiss2: +actual_unit_price; fact_cost = actual_quantity × actual_unit_price
  (ветки возврата/доплаты по стоимости — без изменений).
- signchair: +actual_quantity +actual_unit_price; кооператив книжит
  поставщику o.mkt.purch на fact_cost (итоговая стоимость к получению),
  вместо o.total_cost. ABI собран и проверен.

cooptypes: ISignIss2 +actual_unit_price; ISignChair +actual_quantity
+actual_unit_price.

controller:
- issuance: open захватывает actual_unit_price → issuance_fact.fact_unit_price;
  finalize шлёт его в signiss2 (заказчик факт не редактирует).
- reception: fact-entry несёт fact_unit_price; signchair получает кол-во+цену
  из снапшота; total_amount/проводка от факт-цены.
- diff_state выдачи теперь по СТОИМОСТИ (цена могла измениться, не только кол-во).
- DTO + GraphQL ObjectType + selectors + полный codegen (schema/client/sdk).

desktop:
- CorrectionTable: редактируемая колонка цены (предзаполнена), эмит factPrice.
- IssueActOpenDialog: правка цены при открытии выдачи.
- OperatorReceptionPage: поле цены per-позиция при открытии приёмки.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 15:18:16 +00:00
coopops f64a430bff [C28-21][@ant] feat: открытие документа из поиска через фильтр реестра
Typecheck / desktop (pull_request) Successful in 12m55s
Typecheck / controller (pull_request) Successful in 12m31s
Клик по результату поиска вёл в никуда (openDocument был пуст) — найденный
документ нельзя было посмотреть. Теперь клик кладёт hash в ?document=<hash>
текущего роута; страница реестра читает его и сводит список к одному документу,
авто-раскрывая его. Переиспользуем готовый backend-фильтр filter.document.hash
(тот же путь, что у Union-страницы) — без изменений API и codegen.

- useDocumentRouteFilter/useDocumentNavigation: чтение и установка ?document.
- SearchedDocumentBanner: баннер «показан один документ» с возвратом к списку.
- Обе страницы реестра (совет + пайщик) фильтруют виджет по выбранному документу.
- Виджет реагирует на смену filter и форсит статус-агностичный newsubmitted при
  hash-фильтре (иначе на вкладке «только утверждённые» неутверждённый документ
  из поиска не нашёлся бы).
- DocumentsTable авто-раскрывает строку по expandHash (регистронезависимо).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 15:06:38 +00:00
coopops 9d183f3b64 [C28-21][@ant] copy: срок рассмотрения заявления — до 30 дней вместо 24 часов
Typecheck / desktop (pull_request) Successful in 12m54s
Typecheck / controller (pull_request) Successful in 12m32s
В экране ожидания регистрации (WaitingRegistration) «до 24 часов» — неактуально:
реальный регламентный срок рассмотрения советом — до 30 дней (обычно решение
принимается за день-два). Поправили текст под фактический срок.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 14:34:54 +00:00
ant f2b0e8fd0b fix(marketplace): выдача — финальная подпись в кабинете заказчика, факт фиксируется оператором при открытии (B1)
Кардинальный дефект: финальную подпись заказчика держал стол оператора
(2-й скан штрих-кода + ввод приватного ключа ЗАКАЗЧИКА на устройстве
оператора). Перенесено корректно, без изменения контракта:

- signiss1 не берёт actual_quantity (только акт), signiss2 берёт →
  relocation чисто backend+frontend.
- openIssuance принимает actual_quantity (правка оператора): зашивается в
  акт председателя и сохраняется снапшотом issuance_fact на открытии.
- finalizeIssuance больше не принимает actual_quantity/delivery_signer —
  backend берёт их из заказа (issuance_fact, chairman_account); заказчик
  факт не редактирует.
- Полный codegen: schema.gql + zeus (controller+sdk).
- Frontend оператора: IssueActOpenDialog получил таблицу сверки кол-ва +
  «Показать акт»; IssueActFinalizeDialog удалён, «Завершить выдачу»/2-й
  скан убраны, READY_TO_RECEIVE на столе оператора = «Ждём подпись
  заказчика».
- Frontend заказчика: новый OrdererFinalizeIssuanceDialog (read-only
  сверка + «Показать акт» + «Подписать и получить» своим ключом) на столе
  «Готово к получению».

Правка ЦЕНЫ (не только кол-ва) — отдельным шагом B2 (затрагивает контракт).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 14:27:16 +00:00
ant 42b669eb45 fix(marketplace): корректировки приёмки/выдачи после очного прогона — Класс A
Косметика/канон/тексты без backend (дефекты предыдущих итераций):
- A1: суммы через formatAsset2Digits (срез нулей + ru-группировка) вместо
  сырого "750.0000 ₽" — оба диалога подписи приёмки, диалог открытия
  выдачи, столы выдачи оператора и заказчика, форма приёмки.
- A2: убран технический жаргон из инфо-виджетов (registry_id, signiss,
  o.mkt.*, "ключом текущей сессии", "backend отправит на цепь") —
  заменён одной бизнес-строкой.
- A3: SignAplReceptionChairmanDialog переведён с сырого q-dialog/q-btn на
  BaseDialog/BaseButton (Правило №1); формы приёмки/подписи развёрнуты
  на весь экран (maximized).

Класс B (выдача-кошмар: финальная подпись на стороне заказчика, цена,
ФИО вместо username, сверка-таблицы) — следующими шагами, см. epics.md
блок "КОРРЕКЦИИ ПОСЛЕ ОЧНОГО ПРОГОНА".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 13:59:47 +00:00
ant 9a799f43b1 feat(marketplace): агрегирующая приёмка по account-bound коду (R4/R5/R7/R7a)
Уточнённая модель приёмки на ПВЗ (Story 14.3): «акцепт ≠ привезли»,
потолок приёмки = акцепт, партия/ТТН — разметка, не лимит.

R4: единый базис. Новый read-query marketplaceListSupplierPickupOrders
(braname, offerer_account) отдаёт Order'ы поставщика на КУ в статусах
SUPPLY_PREPARED (по партии/ТТН) и ACCEPTED (добор по акцепту) через
MarketplaceOrderDisplayService. Суммирование лент партии+express убрано —
лента одна, партия — разметка через статус.

R7a: диалог приёмки = плоский список единиц имущества с разделителем
«задекларировано в партии (ТТН) / добор по акцепту», без партийной группировки.

R7: per-unit выбор «принять»; партия без выбранных единиц не создаётся и
ждёт (кейс экспедитора). Добор — отдельный тумблер.

R5: правка факта per-Order на приёмке (поле «факт», потолок = заказано).
Инвариант-guard в buildFactQuantity: fact > order.quantity → отказ
(«сверх акцепта не принимаем»).

Остаётся: per-unit факт/выбор для добора (express), отмена акцепта
поставщиком R6 (контракт).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 13:21:31 +00:00
coopops e877bd3eac [C28-21][@ant] style: убрать верхний отступ в выдаче поиска документов
Typecheck / desktop (pull_request) Successful in 13m3s
Typecheck / controller (pull_request) Successful in 12m36s
Большая пустая полоса между строкой поиска и первым результатом мешала —
снят верхний padding секции результатов (q-pt-none), список начинается ближе.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 12:52:52 +00:00
coopops 9427c204ab [C28-21][@ant] feat: выдача поиска документов — ФИО подписанта и чистый заголовок
Typecheck / desktop (pull_request) Successful in 12m57s
Typecheck / controller (pull_request) Successful in 12m36s
Поиск показывал username («ant») и сырой full_title с хвостом ` - - дата.pdf`.
Приводим к виду реестра на главной:
- SearchResult.signer — ФИО подписанта-субъекта («Иванов Иван Иванович»), берётся
  из signer_certificate готового агрегата (подпись с signer===username, иначе первая;
  организация → short_name). Тот же источник, что у чипов подписей в реестре.
- full_title в выдаче поиска теперь = meta.title (чистое наименование), как в реестре;
  fallback — прежний full_title.
- Фронт DocumentSearchDialog: заголовок + чип ФИО (BaseBadge) + дата вместо строки
  «username · дата»; убран пустой highlights-блок.

Схема + zeus-клиент перегенерированы (generate-schema/generate-client + sdk build).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 12:30:54 +00:00
coopops cdb9cc8344 [C28-21][@ant] fix: парсинг meta.created_at формата DD.MM.YYYY HH:mm + устойчивый backfill
Typecheck / desktop (pull_request) Successful in 13m13s
Typecheck / controller (pull_request) Successful in 12m41s
Документы хранят дату в локализованном формате `DD.MM.YYYY HH:mm` + отдельное
meta.timezone (так пишет фабрика @coopenomics/factory через moment.tz). Прежний
`new Date(meta.created_at)` этот формат не парсил → Invalid Date → колонка
timestamptz падала с "invalid input syntax ... 0NaN-NaN-NaN", и throw в одной
записи ронял весь прогон backfill. Парсим тем же moment.tz, что и фабрика
(timezone из меты, иначе config.timezone), невалидное → null. Плюс per-record
try/catch в backfill: одна битая запись логируется и пропускается, а не валит прогон.

Проверено вживую на voskhod: backfill завершён scanned=8 created=4 updated=4
skipped=0, document_created_at = 2026-05-28 07:53:00+00 (валидные даты, не null).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 12:07:29 +00:00
ant a435a4909f refactor(marketplace): убрать legacy per-партия/per-заказ QR (Story 14.3/14.4)
Обратной совместимости в разработке нет — мусор не оставляем. Account-bound код
становится единственным путём передачи на ПВЗ.

- Оператор приёмки/выдачи: убран fallback-декод по id; нераспознанный скан
  отвергается с понятной ошибкой. Только account-bound токен.
- Поставщик: убран per-партия QR (кнопка/диалог/колонка «Передача» в таблице
  сформированных партий) — остаётся единый «Мой код для ПВЗ».
- Заказчик: убран per-заказ QR (кнопка/диалог/колонка «Получение») — остаётся
  единый «Мой код получения».
- handoff-token: doc-комментарий decode без упоминания legacy-пути.

Десктоп-путь без камеры сохранён: выбор партии «Создать акт приёмки» из списка
ожидающих и кнопки выдачи в таблице (это канон, не QR-fallback).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 12:01:45 +00:00
coopops c5272c7088 [C28-21][@ant] fix: @Index по имени свойства packageHash, не db-колонки package
Typecheck / desktop (pull_request) Successful in 12m56s
Typecheck / controller (pull_request) Successful in 12m36s
TypeORM @Index(['...']) принимает имена СВОЙСТВ сущности, а не db-колонок.
Колонка объявлена @Column({ name: 'package' }) packageHash, поэтому индекс
['coopname','package'] падал на старте: "Index contains column that is missing
in the entity (SignedDocumentEntity): package". Индекс по db-колонке `package`
строится корректно и через имя свойства packageHash.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 11:52:32 +00:00
ant 4d02d0047e feat(marketplace): account-bound QR-код передачи на ПВЗ (Story 14.3/14.4)
Код привязан к аккаунту человека, а не к партии/заказу («как в Ozon»):
один показ = оператор резолвит аккаунт против ленты своего КУ и принимает/
выдаёт разом все ожидаемые единицы этого человека.

- shared/lib/marketplace/handoff-token: encode/decode токена
  `mp1:<kind>:<coopname>:<account>` (pickup|receive). Детерминирован от
  личности+намерения → генерируется заранее/оффлайн, backend не минтит.
  Привязка к КУ — на стороне оператора (резолв против своей ленты); coopname
  отсекает чужой кооператив.
- Поставщик: «Мой код для ПВЗ» (pickup-токен) на «Подготовке отгрузки».
- Заказчик: «Мой код получения» (receive-токен) на «Готово к получению».
- Оператор приёмки: скан pickup-кода → диалог «Принять всё» (сформированные
  партии SUPPLY_PREPARED + самовывоз по факту разом).
- Оператор выдачи: скан receive-кода → панель всех заказов заказчика на выдачу;
  пошаговое открытие/завершение (каждый шаг — своя двойная подпись).
- Legacy-QR (сам shipment_id/order_id) продолжает работать — decode вернёт null
  и код падает на прежний путь по id (обратная совместимость).

Подписанный/одноразовый токен-сервис с TTL остаётся доработкой (mp2): аккаунт-
биндинг уже даёт Ozon-UX, криптоподпись пользователь явно отложил.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 11:49:28 +00:00
ant d1da62d979 [E14][@ant] feat: 14.2 — express-приёмка самовывоза без предварительной партии
Оператор принимает самовывоз поставщика, не сформировавшего партию заранее.
Backend: marketplaceCreateExpressReception синтезирует SELF-партию из ACCEPTED-
заказов поставщика на этом КУ (per-cycle) и открывает приёмку через
существующий create(); marketplaceListExpressPickupsByBraname — лента
поставщиков, ожидающих самовывоза на КУ (агрегат). Добавлен фильтр
delivery_braname в order-repo. Жёсткий 1:1-акцепт всех КУ цикла здесь намеренно
не применяется — принимается только привезённое на этот КУ.

Desktop: на «Приёмке» раздел «Самовывоз по факту» со списком поставщиков и
кнопкой «Принять самовывоз». Двухподписный акт и переход в ACCEPTED_TO_COOP —
без изменений.

Codegen: schema.gql + zeus regenerated, SDK-обёртки CreateExpressReception /
ListExpressPickupsByBraname.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 11:34:53 +00:00
ant 923859855b [E14][@ant] feat: 14.5 — печать/скачивание ТТН (TTNPrintPreview) в «Подготовке отгрузки»
Виджет TTNPrintPreview подключён в стол поставщика: для партий Варианта Б
(экспедитор) в «Сформированных партиях» — кнопка «ТТН», открывающая накладную
с составом (позиции из заказов SUPPLY_PREPARED партии). Печать через изолированный
iframe (печатается только лист А5, без хрома приложения) + скачивание
самодостаточного HTML (в т.ч. «Сохранить как PDF» из браузера). Без новых
зависимостей. Виджет приведён к канону: BaseButton вместо q-btn, токены --p-*.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 11:20:58 +00:00
ant ba82eecc21 [E14][@ant] fix: «Приёмка» — выбор партии из списка вместо ручного ввода id
Убран ручной ввод shipment_id + кнопка «Создать акт» (ломались: оператор
копировал обрезанный для показа id → invalid input syntax for type uuid).
Вместо этого — список ожидающих приёмки партий КУ (статус SUPPLY_PREPARED из
listShipmentsByBraname) с кнопкой «Создать акт приёмки» на каждой; QR-сканер
остаётся для приёмки с телефона. Оба пути зовут createAplReception({shipment_id}).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 11:01:02 +00:00
ant 54c27599dc [E14][@ant] refactor: канон-тосты по всему marketplace — Notify → SuccessAlert/FailAlert/NotifyAlert
Системно заменён прямой Notify.create на канонические алерты (правый нижний
угол, тёмный фон, единый визуал) во всех 10 страницах marketplace:
positive→SuccessAlert, negative→FailAlert(e) (сам извлекает GraphQL-ошибку),
info/warning→NotifyAlert. Notify убран из quasar-импортов.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 11:01:02 +00:00
ant ee26bb9d59 [E14][@ant] feat: QR-передача заказа заказчик→оператор выдачи (Story 14.4)
Зеркальный сценарий выдачи на ПВЗ, переиспользует HandoffQr/QrScanner (DRY):
- заказчик (OrdererReadyToReceive): колонка «Получение» с кнопкой QR →
  диалог с QR заказа (value = order id);
- оператор выдачи (OperatorIssuance): кнопка «Сканировать QR заказа» →
  сканер → onQrScanned находит заказ в ленте КУ и запускает нужный шаг
  (ACCEPTED_TO_COOP → открыть выдачу, READY_TO_RECEIVE → завершить);
  заказ не на этом КУ / не в статусе выдачи → FailAlert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 10:44:51 +00:00
ant 9fcb0d27ae [E14][@ant] feat: QR-передача партии поставщик→оператор (Story 14.3)
Основной сценарий ПВЗ: поставщик показывает QR сформированной партии,
оператор сканирует — идентификатор подставляется и акт приёмки открывается
без ручного ввода shipment_id.

Два переиспользуемых канон-виджета (DRY, под Story 14.4 заказчик→выдача):
- HandoffQr — генерация QR из идентификатора (qrcode, toDataURL) + копируемый
  код как запасной путь;
- QrScanner — нативный BarcodeDetector (Chromium) + камера, с ручным вводом
  кода если камера/API недоступны; зависимостей не добавляем.

Поставщик (OffererSupplyPreparation): колонка «Передача» с кнопкой QR на
партии в статусе SUPPLY_PREPARED/RECEPTION_IN_PROGRESS → диалог с QR.
Оператор (OperatorReception): кнопка «Сканировать QR» → диалог сканера →
onQrScanned подставляет код и создаёт акт приёмки.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 10:41:48 +00:00
ant b1a3e1721c [E14][@ant] fix: формирование партии — enum-имена SELF/EXPEDITOR + канон-алерты
GraphQL-enum MarketplaceShipmentDeliveryVariant принимает значения по ИМЕНИ
(SELF/EXPEDITOR), а не по внутреннему коду 'A'/'B' — самовывоз падал с
"Value 'A' does not exist in enum". Берём значения из Zeus-enum SDK.
Notify.create заменён на канонические SuccessAlert/FailAlert (тост справа внизу).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 10:34:27 +00:00
ant 94ca46331f feat(desktop): Story 14.5 — явное формирование партии с выбором варианта + ТТН
Страница «Подготовка отгрузки» получила раздел «К формированию»: принятые
(ACCEPTED) заказы группируются по заявке→КУ; по кнопке «Сформировать партию»
открывается диалог с выбором способа доставки ПО КАЖДОМУ КУ — самовывоз
(Вариант А) или экспедитор+ТТН (Вариант Б, форма из 7 полей) — и вызовом
marketplaceCreateShipment. Раздел «Сформированные партии» — прежняя таблица с
колонкой «Следующий шаг». Refresh унифицирован на общий RefreshButton.

Логика группировки и типы вынесены в lib/shipmentFormation.ts (переиспользуются
диалогом и страницей). Закрывает разрыв Story 5.1: единый явный путь отгрузки
для индивидуальных и пакетных заказов (вместе со Story 14.1 на бэкенде).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 10:20:05 +00:00
ant 05b47100b1 feat(controller): Story 14.1 — индивидуальный accept без навязанного Варианта А
Раньше acceptIndividual авто-формировал партию Варианта А (самовывоз, без ТТН)
через synthesizeIndividualShipment — поставщик был лишён выбора доставки. Теперь
синтезируется только заявка (cycle) в статусе ACCEPTED, а партию поставщик
формирует явно (marketplaceCreateShipment) с выбором варианта — единый путь с
пакетными заказами. Order после accept остаётся ACCEPTED до явного формирования.

Убраны неиспользуемые зависимости shipmentCreate + импорт варианта; spec и
комментарий forward-guard приведены в соответствие. tsc зелёный.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 10:14:38 +00:00
ant 16104f194d feat(desktop): «Подготовка к отгрузке» — колонка «Следующий шаг» (Шаг 1)
Страница была read-only и выглядела «мёртвой»: ни кнопок, ни подсказки, что
делать. Добавлена колонка-подсказка «Следующий шаг» по статусу+варианту партии
(самовывоз → привезти на КУ; экспедитор → передать по ТТН; идёт приёмка;
принято кооперативом) и честный баннер. Активная подготовка (выбор варианта,
ТТН, QR-передача) — доработки Эпика 14, здесь пока не реализованы.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 10:03:39 +00:00
ant 60e6a24b74 fix(desktop): статус SUPPLY_PREPARED — «Поставка готова» вместо «готовится»
Бейдж заказа в SUPPLY_PREPARED показывал «Поставка готовится» (warn), хотя
доменно это завершённое состояние: партия уже сформирована (вариант доставки
выбран, для экспедитора выпущена ТТН) и готова к отгрузке/приёмке. Лейбл
расходился со страницей партий («Готова к отгрузке») и вкладкой «Поставка
готова» у поставщика. Приведено к «Поставка готова» (info). Вкладка заказчика,
объединяющая принят/готова/у кооператива, переименована «Готовятся» → «В работе»
во избежание противоречия с бейджем.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 09:51:38 +00:00
ant 59db799051 fix(desktop): каталог и фильтры заказов по канону + общий RefreshButton
- Каталог: чип-фильтр категорий + плавающая сортировка → канон PageTabs
  (категории как вкладки со счётчиком) + BaseSelect в слоте #actions.
- «Мои заказы»: вкладки покрывают ВСЕ статусы заказа (был баг — заказ в
  SUPPLY_PREPARED не попадал ни в одну вкладку, кроме «Все»). Добавлена
  вкладка «Готовятся» (ACCEPTED/SUPPLY_PREPARED/ACCEPTED_TO_COOP), «Отменены»
  расширена истёкшими циклами и возвратами.
- «Входящие заказы» поставщика: аналогичное исчерпывающее покрытие статусов.
- Кнопка «Обновить» вынесена в общий виджет widgets/Marketplace/RefreshButton
  (дублировалась ~15× по столам, по-разному свёрстана) и поставлена в шапку:
  на «Сводный заказ» и «Готово к получению» — через канон Teleport в
  #header-actions-host, у поставщика — в слот вкладок PageTabs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 09:22:18 +00:00
ant 46ce429d3b fix(desktop): карточки заказов по канону + единый статус + фикс вкладки «Ждут акцепта»
OrderCard переверстан: заголовок и бейдж в одной строке, №·дата — отдельной
строкой на всю ширину (раньше «висла» вокруг бейджа); Сумма крупным числом-
героем + Кол-во; ПВЗ отдельным блоком (наименование КУ + адрес). Убрано
задвоение статуса — раньше показывались и грубый card-status («Размещён»), и
доменный («Ждёт поставщика»).

Единый orderStatusDisplay (подпись + вариант бейджа) в виджете OrderCard —
устранены три дубля STATUS_LABEL (MyOrders / OperatorIssuance /
OrdererConsolidated). Доменный статус и cancellable прокинуты в модель карточки;
действия заказчика — отмена по cancellable, без нерабочих «Открыть»; добавлен
readonly для обзорного сводного заказа.

Фильтры столов заказчика и поставщика переведены на канон PageTabs.

Фикс бага: вкладка «Ждут акцепта» (поставщик) и «Ждут поставщика» (заказчик)
теперь покрывают оба статуса ожидания акцепта — ACCEPTED_PENDING_SUPPLIER и
ACCEPTED_PENDING_SUPPLIER_INDIVIDUAL. Раньше индивидуальный заказ с кнопкой
«Принять» в эти вкладки не попадал (отдельная вкладка «Индивидуальные» путала).

OrdererConsolidated переведён на общий toOrderCardModel — раньше показывал сырой
offer_id как заголовок и braname вместо наименования+адреса ПВЗ.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 09:03:37 +00:00
ant 92eed2622a feat(desktop): наименование+адрес ПВЗ в карточках и лентах выдачи
- OrderCard: ПВЗ двумя строками — наименование КУ (основная) + адрес
  (приглушённая), запасной вариант braname если оба пусты.
- toOrderCardModel вынесен в виджет OrderCard (DRY): «Мои заказы» и
  «Входящие заказы» больше не дублируют STATUS_TO_CARD и маппинг.
- Таблицы выдачи (оператор / готово к получению): колонка «Товар»,
  единица измерения у количества, ПВЗ через наименование·адрес.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 08:21:24 +00:00
ant 89d325ea4b feat(marketplace): наименование ПВЗ + обогащение списков выдачи
- delivery_point_name на заказе: live-резолв наименования кооперативного
  участка (org.short_name) по braname через core ORGANIZATION_REPOSITORY,
  без денормализации (копия отставала бы при переименовании КУ).
- buildDisplayFields вынесен из order-резолвера в общий
  MarketplaceOrderDisplayService (DRY): теперь обогащает и оба стола, и
  ленты выдачи (listIssuancesByBraname, listMyReadyToReceive) — товар,
  единица, наименование и адрес ПВЗ. Best-effort, без N+1.
- schema/zeus/sdk-селектор регенерированы под новое поле.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 08:21:10 +00:00
ant f8e1a7e567 feat(desktop): канон-вёрстка карточки заказа + понятные товар и ПВЗ
OrderCard переведён на канон MONO Platform v2: BaseCard/BaseBadge/BaseButton,
токены --p-*, моноширинный номер, статус-пилюля с точкой; убраны --mp-*-токены,
прямой q-card, тени и кастомный .mp-status-chip.

Столы заказчика («Мои заказы») и поставщика («Входящие заказы») показывают
название товара (product_name) вместо UUID предложения, единицу измерения
через общий marketplaceUnitShort и адрес ПВЗ (delivery_point_address) вместо
служебного идентификатора — с запасным значением, если реквизит недоступен.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 07:46:55 +00:00
ant e5aec26665 feat(marketplace): обогащение заказов реквизитами товара и ПВЗ
Заказ ссылается на предложение и ПВЗ по id и не нёс отображаемых реквизитов —
карточки столов показывали сырой offer_id и служебный braname. Резолверы
listMyOrders/listSupplierOrders/getOrder теперь обогащают DTO заказа:
product_name и unit_of_measure из предложения (батч offerRepo.findByIds без
N+1), delivery_point_address из детализации ПВЗ (kuRepo.findByCoopname).
Поля nullable, best-effort: нет предложения/ПВЗ — остаются пустыми, клиент
показывает запасной вид.

Схема и zeus-клиент перегенерированы, orderSelector дополнен; offer-репо
получил findByIds (+ моки в unit-тестах).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 07:46:43 +00:00
ant 140310df49 chore: реконсиляция pnpm-lock — убрать vue3-openlayers, вернуть dev-тулинг
Переинсталл (--no-frozen-lockfile) после удаления vue3-openlayers из
desktop/package.json в прошлой сессии: lockfile синхронизирован, node_modules
восстановлен (ts-node/eslint), что разблокировало канон-цикл
generate-schema/generate-client.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 07:46:29 +00:00
coopops 49631dda6a fix gitgnore 2026-05-30 07:29:40 +00:00
coopops b1c2f3d558 fix gitgnore
Build bootstrap container / build (push) Successful in 1m21s
2026-05-30 07:22:36 +00:00
ant 68ad39bf0d chore(desktop): убрать упоминание OpenLayers из комментария quasar.config
Зависимость удалена — чистим устаревший пример тяжёлой библиотеки в
комментарии про отключение vue-tsc в dev. Только комментарий, рестарт
dev-сервера не требуется.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 07:07:11 +00:00
ant cf6ad7c76d fix(desktop): карта ПВЗ на Яндекс.Картах вместо нерабочего OpenLayers
На странице пунктов выдачи (стол администратора) диалог карты падал с
«Failed to resolve component: ol-map»: boot/map.ts с регистрацией
vue3-openlayers никогда не был подключён в quasar.config (массив boot),
поэтому ol-* не резолвились и карта не рисовалась.

OpenLayers в проекте больше нигде не используется — переводим shared/ui/Map
на Yandex Maps JS API (как рабочий виджет KUMapWithList) и сносим OpenLayers:

- shared/ui/Map/Map.vue: single-point карта на ymaps (метка — pin.svg),
  тот же публичный API long/lat; деградированный баннер без ключа.
- shared/lib/yandexMaps: загрузчик SDK вынесен из widgets/KUMapWithList в
  shared (DRY) — общий для виджета списка ПВЗ и single-point карты.
- удалён boot/map.ts (мёртвый, не в boot) и зависимость vue3-openlayers.

Lockfile подровняется при следующей установке (vue3-openlayers пока остаётся
orphan-записью).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 07:06:15 +00:00
coopops 6f53923303 chore(release): publish
Build bootstrap container / build (push) Successful in 3m7s
Release / release (push) Successful in 24m25s
Release / publish-packages (push) Has been skipped
Release / trigger-coopenomics-docs (push) Has been skipped
Release / trigger-mono-docs (push) Has been skipped
2026-05-30 06:58:26 +00:00
ant 34e515f48c Merge origin/dev into marketplace2
Свежие desktop-фиксы из dev: форматирование сумм в тостах ошибок (formatAssetsInText),
SSR-фиксы Sentry/темы, гонка Teleport-host шапки + обновление грантов после регистрации,
плейсхолдеры форм ИП/организации, фикс выбора BaseRadioCard при наведении.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 06:49:32 +00:00
coopops 33b7923773 chore(release): publish
Build bootstrap container / build (push) Successful in 3m37s
Release / release (push) Successful in 25m15s
Release / publish-packages (push) Has been skipped
Release / trigger-coopenomics-docs (push) Has been skipped
Release / trigger-mono-docs (push) Has been skipped
2026-05-30 06:48:30 +00:00
coopops 10d792613f Merge branch 'dev' into testnet 2026-05-30 06:48:13 +00:00
coopops 58bdb8a995 fix(desktop): форматировать суммы в тостах ошибок до 2 знаков
Ошибки цепи возвращают суммы с сырой precision=4 ("100.0000 RUB"). Добавлен formatAssetsInText() рядом с formatAsset2Digits и применён в FailAlert — единая точка для всех тостов ошибок. Суммы вида <целое>.<>=3 знаков> <ТИКЕР> приводятся к 2 знакам с группировкой; уже отформатированные не трогаются (идемпотентно).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 06:47:32 +00:00
coopops 067fd8d562 fix(desktop): не вешать watch темы на SSR-сервере (утечка подписчиков на Dark)
theme boot подписывал watch на глобальный Quasar-реактив Dark. В SSR boot-файлы
выполняются на каждый рендер-запрос, а watch без owning-scope не утилизируется —
подписчики копились на синглтоне Dark безгранично (та же природа, что у sentry.ts,
только дешевле за итерацию). Тема (data-theme на documentElement) нужна только на
клиенте; гейтим boot через process.env.SERVER.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 06:47:32 +00:00
coopops ca2b97f18f fix(desktop): не инициализировать Sentry на SSR-сервере (спам логов + утечка RAM)
Boot-файл sentry.ts использует браузерный @sentry/vue с browserTracingIntegration.
В SSR boot-файлы выполняются на каждый рендер-запрос, поэтому Sentry.init плодил
новый клиент/интеграции на каждом запросе (утечка памяти → «пила» RAM, заметная
на нодах с малым объёмом RAM) и спамил серверный лог строкой про инициализацию
(~80/мин). Гейтим boot через process.env.SERVER (как в chatwoot.ts) — Sentry
остаётся только на клиенте, где ему и место.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 06:47:32 +00:00
coopops 708607f83a fix(desktop): цвет выделения текста в тостах
Глобальный ::selection красит текст в var(--p-ink) (тёмный на свет-теме)
поверх полупрозрачного фона — на тёмной поверхности тоста выделенный текст
пропадал. Scoped-override для .q-notification/.toast: полупрозрачная светлая
подложка + светлый текст, читаемо на всех типах тостов.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 06:47:32 +00:00
coopops 5a21d017e0 fix(desktop): убрать гонку Teleport-host шапки и обновлять гранты/столы после регистрации
Баг A: предупреждение "Failed to locate Teleport target '#header-actions-host'"
возникало, потому что host был под v-if='loggedIn'. Сразу после регистрации
навигация на стол происходит раньше, чем loggedIn (зависит от async
session.isAuth / isRegistrationComplete) станет true → цель Teleport
отсутствует в DOM → даже defer-Teleport источников страниц её не находит.
Рендерим #header-actions-host безусловно — видимость блока действий уже
регулируется CSS (.topbar__actions:has(> .header-actions-host:only-child:empty)).

Баг B: после регистрации grant-gated кнопки («Совершить взнос», «Получить
возврат») не появлялись до F5. В потоке завершения регистрации (watch на
participant_account) не вызывался desktops.loadDesktop() (свежие столы/гранты),
а кошелёк/agreements не грузились: на момент первого run() статус ещё не
'active' → isFullyActive=false. После того как статус обновился, до-вызываем
run(true) и loadDesktop() по образцу init-app / EnableButton. Условие
isFullyActive в init-wallet не ослаблялось.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 06:47:32 +00:00
coopops a2bfa69c2b feat(desktop): placeholder для наименования банка в форме ИП
Только неочевидное поле — наименование банка получило подсказку ПАО "Сбербанк".
Остальные поля ИП самоописательны (формат даты, кол-во цифр, «как в паспорте»).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 06:47:32 +00:00
coopops 804a178234 feat(desktop): добавить placeholder-примеры в форму организации при вступлении
Поля краткого/полного наименования, должности представителя и наименования
банка получили placeholder-подсказки (ПК "Ромашка", Председатель совета,
ПАО "Сбербанк") — показываются при фокусе пустого поля.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 06:47:32 +00:00
coopops 0eb1224390 fix(desktop): не терять выбор в BaseRadioCard при наведении
Hover-правило .base-radio-card:hover (специфичность 0,2,0) перекрывало
.base-radio-card--selected (0,1,0), поэтому при наведении на выбранную
карточку программы primary-фон/граница подменялись на surface/soft и
выбор визуально пропадал. Ограничил hover невыбранными карточками через
:not(.base-radio-card--selected).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 06:47:32 +00:00
coopops b465529f8a [design-wave1][@dark] fix: палитра команд — рамка курсора на любом столе, не только активном
Заголовок workspace-row получал outline только когда стол активный
(.is-active .row.is-selected). На неактивных столах при навигации курсор
«терялся»: только лёгкая смена фона на surface-2, без рамки — в отличие
от page'ей, которые подсвечиваются outline'ом всегда. Выровнял правило:
outline на любом .workspace-row.is-selected.
2026-05-30 06:47:31 +00:00
coopops 6e4ee9303a [design-wave1][@dark] fix: палитра команд — clamp навигации стрелками вместо wrap-around
На верхней/нижней позиции стрелка дальше прыгала в противоположный
конец списка из-за (idx+delta+len)%len. Заменено на clamp в [0..len-1]:
курсор остаётся на месте, как в нативном listbox/меню. Пустое выделение
+ ↓ ставит на первый, ↑ — на последний.
2026-05-30 06:47:31 +00:00
coopops c3cc6bffb6 [design-wave1][@dark] fix: палитра команд — снят sticky-баннер активного стола
Активный workspace был position:sticky top:0 — при навигации стрелкой
вверх курсор-выделение «исчезал» за sticky-overlay'ем, потому что
scrollIntoView({block:'nearest'}) считал элемент видимым в normal flow,
не учитывая sticky-перекрытие. Активный стол и так идёт первым по
сортировке + есть badge «Активный», sticky не нужен.
2026-05-30 06:47:31 +00:00
coopops 39be17dfbc [@dark] fix(desktop): CommandPalette — primary вместо accent + meta.conditions/hidden + drawer close на мобильниках
- CommandPalette.vue: все --p-accent / --p-accent-soft / --p-ink-on-accent
  заменены на --p-primary (бирюзовая палитра канона). Откатывается
  регрессия после fix-коммитов design-wave1 (sticky-banner / clamp /
  рамка курсора), которые случайно вернули accent поверх primary
  (см. b82b778b81 — изначально palette был в primary).

- default.vue::paletteWorkspaces: возвращена фильтрация meta.conditions
  (participant/chairman/soviet install.ts) + meta.hidden (capital
  install.ts) — та же логика что в LeftDrawerMenu.filteredRoutes,
  чтобы CommandPalette не показывал страницы, которые не отображаются
  в rail-меню. onSelectWorkspace/onSelectPage закрывают левый дровер
  на мобильниках через desktop.closeLeftDrawerOnMobile().
2026-05-30 06:47:31 +00:00
coopops ab8f1186e8 [@dark] refactor(desktop): canon CommandPalette + снос осиротевшего меню — фикс утерянного ⌘K после design-wave1
- layouts/default.vue: вместо widgets/Desktop/CmdkMenu mount'им
  shared/ui/domain/CommandPalette (canon из design-wave1) с адаптером
  workspaceMenus → CommandPaletteWorkspace[] и глобальным ⌘K / Ctrl+K.
- entities/CommandPalette: минимальный store (isOpen / open / close /
  toggle) — заменяет тяжёлый useCmdkMenuStore.
- LeftDrawerMenu.onCmdk → palette.open() (триггер «Найти» в AppDrawer).
- Удалены целиком осиротевшие после design-wave1 директории:
  entities/CmdkMenu, widgets/Desktop/CmdkMenu, widgets/Desktop/CmdkTrigger,
  widgets/Desktop/SecondLevelMenuList (никем не использовался),
  widgets/Desktop/WorkspaceMenu (заменён на WorkspaceSwitcher),
  widgets/Wallet/MicroWallet (заменён на RailUserCard).
- Поправлены barrels widgets/Desktop и widgets/Wallet.
2026-05-30 06:47:31 +00:00
coopops 7b39ed34be fix(desktop): читаемость тостов на тёмной теме
Фон и текст тоста (.q-notification + .toast) теперь фиксированные тёмная
поверхность + светлый текст в обеих темах, а не инвертируемые
var(--p-ink)/var(--p-canvas). На dark-теме текст уходил в тёмный на тёмном
deep-tint фоне (#052e16 и т.п.) — нечитаемо. Цветные иконки по типу
остаются на токенах.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 06:47:31 +00:00
ant e68475e1d8 fix(desktop): отступ документа от рамки в диалоге подписи соглашения
q-pa-lg на flat-карточке SignAgreementDialog — документ больше не прижат
к границе на странице кошелька пайщика.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 06:47:31 +00:00
coopops a076962058 [blago-cli-add-filter][@ant] fix(blago-cli): фильтр non-entity .md в add + понятные ошибки push — чтобы blago add . не валил push «type: undefined»
— format/peekBlagoEntityType: не бросает; возвращает project|issue|story|undefined.
— sync/add: классификация unreadable/non-entity/clean/dirty; README/notes/CLAUDE/AGENTS не попадают в staging, отдельный счётчик skippedNonEntity.
— sync/push: на старте чистит staging от non-entity (warn); ошибки parseBlagoMarkdown / validateParsedForPush префиксуются путём файла.
— cmd/run-cli: вывод add показывает skippedNonEntity.
— bump 2026.5.28-1.
2026-05-30 06:47:31 +00:00
coopops 7e98a3adfb [mobile-drawer][@ant] fix: левый дровер — автозакрытие на мобиле при смене маршрута 2026-05-30 06:46:00 +00:00
coopops eb3e388b78 [fix-vue-tsc][@dark] fix: IPayment override → локальный IPaymentRow в виджете
Глобальный Omit<IPayment,'id'>&{id:string} ломал store.ts/mergePayments
и features/Payment/SetStatus/api (туда приходит сырой Zeus-вывод с
id:unknown). Откатываю override в entities/Payment/model/types.ts,
сужаю id до string локально в ListOfPaymentsWidget через IPaymentRow —
точка сужения только в UI, контракт стора/API не трогается.

vue-tsc desktop локально: PASS.
2026-05-30 06:46:00 +00:00
coopops d65643065e [fix-vue-tsc][@dark] fix: vue-tsc desktop — IAgenda в QuestionsTable + IPayment.id:string
QuestionsTable принимал Cooperative.Document.IComplexAgenda[] (cooptypes,
table:IDecision без certificates, id:IUint64=number|string), а сверху
ListOfAgendaQuestions передаёт IAgenda[] из desktop/entities/Agenda
(GraphQL-вывод getAgenda, table:BlockchainDecision с certificates,
id:number). Перевожу QuestionsTable на IAgenda — это реальная форма
данных из стора; уходит расхождение IComplexAgenda↔IAgenda для
:agenda='row' в QuestionCard (он тоже IAgenda).

IPayment.id был ModelTypes['ID']=unknown (Zeus не имеет scalar-resolver
для ID), из-за чего шаблон ListOfPaymentsWidget падал на expanded.get/.set
и :id-биндингах. Переопределяю id как string через Omit+&.
2026-05-30 06:46:00 +00:00
ant 0c343f0d50 Merge pull request 'fix(desktop): форматировать суммы в тостах ошибок до 2 знаков' (#48) from fix/desktop-format-asset-errors into dev
Reviewed-on: #48
2026-05-30 06:43:25 +00:00
coopops 91f74c76b4 fix(desktop): форматировать суммы в тостах ошибок до 2 знаков
Typecheck / desktop (pull_request) Successful in 14m11s
Typecheck / controller (pull_request) Successful in 13m38s
Ошибки цепи возвращают суммы с сырой precision=4 ("100.0000 RUB"). Добавлен formatAssetsInText() рядом с formatAsset2Digits и применён в FailAlert — единая точка для всех тостов ошибок. Суммы вида <целое>.<>=3 знаков> <ТИКЕР> приводятся к 2 знакам с группировкой; уже отформатированные не трогаются (идемпотентно).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 06:38:57 +00:00
ant e794bbefe1 docs(desktop): демо PageHint на витнес-странице /_dev/ui
В секцию «Баннеры» добавлен пример закрывающейся подсказки PageHint.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 06:28:41 +00:00
ant 2ec07a0a5c feat(marketplace): стол администратора в канон MONO Platform v2 + PageHint
8 страниц стола администратора (market-admin) переведены с legacy-вёрстки
(mp-role-admin, токены --mp-*, FontAwesome, raw q-btn/q-card/q-input/q-select,
text-h5-заголовки) на канон MONO Platform v2:

- внутристраничный дубль-заголовок → закрывающаяся подсказка PageHint;
- --mp-* → --p-*; FontAwesome → Material q-icon;
- q-btn → BaseButton, q-card → BaseCard, q-input → BaseInput, диалог → BaseDialog;
- q-chip-статусы → BaseBadge; KPI-карточки → прототип .kpi;
- мультистатус-фильтры → канон .chip-тогглы; пустые экраны → EmptyState.

Вся бизнес-логика (модерация, whitelist, проекты списания, повестка совета,
выплаты, онбординг ЦПП) сохранена без изменений. Множественный q-select в
whitelist оставлен raw (BaseSelect — single-value), канонизируется quasar-canon.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 06:27:34 +00:00
coopops 3691d95777 fix(desktop): не вешать watch темы на SSR-сервере (утечка подписчиков на Dark)
theme boot подписывал watch на глобальный Quasar-реактив Dark. В SSR boot-файлы
выполняются на каждый рендер-запрос, а watch без owning-scope не утилизируется —
подписчики копились на синглтоне Dark безгранично (та же природа, что у sentry.ts,
только дешевле за итерацию). Тема (data-theme на documentElement) нужна только на
клиенте; гейтим boot через process.env.SERVER.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 06:25:10 +00:00
coopops f264aa85a3 fix(desktop): не инициализировать Sentry на SSR-сервере (спам логов + утечка RAM)
Boot-файл sentry.ts использует браузерный @sentry/vue с browserTracingIntegration.
В SSR boot-файлы выполняются на каждый рендер-запрос, поэтому Sentry.init плодил
новый клиент/интеграции на каждом запросе (утечка памяти → «пила» RAM, заметная
на нодах с малым объёмом RAM) и спамил серверный лог строкой про инициализацию
(~80/мин). Гейтим boot через process.env.SERVER (как в chatwoot.ts) — Sentry
остаётся только на клиенте, где ему и место.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 06:19:11 +00:00
ant 85ac54a13f feat(marketplace): PageHint на столе ПВЗ — убраны дубль-заголовки
8 страниц стола ПВЗ (market-pvz): внутристраничный .__head с .t-h2 (дублировал
заголовок навигации) заменён на закрывающуюся подсказку PageHint с прежним
пояснительным текстом. На «Маркировке» сохранён .no-print. На «Доверенных лицах»
постоянный инфо-баннер стал закрывающимся (условный state-плейсхолдер оставлен).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 06:15:57 +00:00
ant 82715fdb51 feat(marketplace): PageHint — закрывающаяся подсказка стола вместо дубль-заголовков
Вынесен domain-компонент shared/ui/domain/PageHint: инкапсулирует хук
useDismissibleBanner (скрытие через LocalStorage без мигания) и канон-разметку
.banner.banner--info + крестик. Текст подсказки — через слот, ключ хранения —
prop storage-key. Устраняет ~28-кратное повторение inline-баннера по столам (DRY).

Стол заказчика (5 страниц): убран внутристраничный дубль-заголовок «Каталог /
Каталог» (.__head с .t-h2 дублировал заголовок навигации) — вместо него
PageHint с прежним пояснительным текстом и крестиком.

Стол поставщика (5 Offerer* + AdminIssuancePoints): inline-баннер + локальный
хук/SCSS заменены на PageHint без изменения визуала.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 06:12:01 +00:00
ant f9faa45844 fix(marketplace): убрать as never/as number из Стола администратора
Аудит кастов по Столу оператора (ПВЗ) и Столу администратора:
- template-кастов 0, `as any` 0 — опасных/рантайм-фатальных нет;
- два реальных хака в admin-страницах убраны без каста:
  - BoardAgendaWriteoff: `(... ) as never` — локальный строковый WriteoffStatus
    не стыковался с SDK-входом statuses (enum MarketplaceWriteoffProposalStatus).
    Добавлена явная таблица STATUS_TO_ENUM (5 видимых совету статусов → enum),
    statuses строится map'ом. Тип доказан образцом соседней AdminWriteoffsPage,
    которая так же кормит listWriteoffProposals enum-массивом.
  - OnboardingCoopAcceptCpp/composable: `stepToRegistryId[meta.id] as number` →
    typeof-guard (робастно к noUncheckedIndexedAccess).

НЕ тронуты api-слойные `return result as <ViewType>` (OperatorIssuance/Reception/
InventoryLabeling/ReturnClaims, AdminWriteoffs): это НЕСУЩЕЕ сужение Zeus-скаляров
(DateTime/ID типизированы Zeus как unknown; view-тип сужает их до string/
NonNullable через Omit&{…}). Убрать = сломать типы (unknown не присваивается
string) или добавить бессмысленный рантайм-.map(). Направление каста безопасное
(unknown→точный тип), у каждого инлайн-обоснование. Это не `as any`.

ESLint 0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 05:46:08 +00:00
coopops 49834fb58b fix(desktop): цвет выделения текста в тостах
Глобальный ::selection красит текст в var(--p-ink) (тёмный на свет-теме)
поверх полупрозрачного фона — на тёмной поверхности тоста выделенный текст
пропадал. Scoped-override для .q-notification/.toast: полупрозрачная светлая
подложка + светлый текст, читаемо на всех типах тостов.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 05:40:01 +00:00
coopops 3a59be5101 fix(desktop): цвет выделения текста в тостах
Глобальный ::selection красит текст в var(--p-ink) (тёмный на свет-теме)
поверх полупрозрачного фона — на тёмной поверхности тоста выделенный текст
пропадал. Scoped-override для .q-notification/.toast: полупрозрачная светлая
подложка + светлый текст, читаемо на всех типах тостов.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 05:39:47 +00:00
ant af08bad471 fix(marketplace): убрать TS-касты из шаблонов и скриптов (boot SyntaxError 'as')
В MyOrdersPage в template-выражении был TS-каст
`(order as any).domainStatus as MarketplaceOrderStatusView` (тащился из
оригинала). Vue компилирует template-выражения в обычный JS, поэтому `as`
валит render-функцию в рантайме: `[Quasar] boot error: SyntaxError:
Unexpected identifier 'as'`. Так как install.ts импортирует MyOrdersPage
на старте (markRaw), падал весь boot приложения.

- MyOrders: доменный статус берём из items по id в script-хелперах
  (rowStatusLabel/isOrderCancellable) — типобезопасно, без `as` и без `any`.
- MarketplaceCatalog: убран избыточный `cc as ...[]` (api уже типизирован),
  `value as CatalogSort` → type-guard isCatalogSort по sortOptions.
- OperatorInventoryLabeling: `v as BarcodeStrategy/BarcodeFormat` в сеттерах
  BaseSelect → type-guard'ы isStrategy/isFormat по спискам значений enum.

Во всех мной написанных страницах/диалогах marketplace type-assertion-кастов
больше нет (import-алиасы `type X as Y` — не касты). ESLint 0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 05:33:52 +00:00
ant d5ff25f61d feat(marketplace): визуальный реканон Стола заказчика — MONO Platform v2
Все рабочие страницы и локальные диалоги Стола заказчика приведены к канону
MONO Platform v2: сняты mp-role-*/mp-status-chip/mp-filter-chip, токены --mp-*,
FontAwesome и raw rgba/hex → токены --p-*, Material q-icon, базовые обёртки и
канон-классы (.t-h2/.t-muted/.chip/.kpi-подобные плитки/EmptyState).

Страницы (полная перезапись по канон-скелету):
- MarketplaceCatalog: фильтр категорий → тоггл-чипы .chip, сортировка →
  BaseSelect, «Заказать» → BaseButton, пустой экран → EmptyState.
- MyOrders: фильтр статусов → тоггл-чипы, действия → BaseButton, EmptyState.
- OrdererConsolidated: группы-партии → hairline-карточки на --p-*, счётчик →
  .chip--accent + Material-иконка, refresh → BaseButton.
- OrdererReadyToReceive: канон-таблица + #no-data EmptyState, refresh → BaseButton.
- OrdererReturnClaims: статусы → BaseBadge, подача → BaseSelect+BaseButton,
  пустые секции → hairline-панели.
- OrderCreateDialog: полностью на BaseDialog/BaseInput/BaseSelect/BaseButton.

Диалоги подписания (точечно, как в Столе ПВЗ): SubmitReturnClaimDialog —
FA step-иконки → Material (edit/image/draw), --mp-→--p-; ReturnClaimDetailsDialog
— --mp-→--p-, rgba-бордеры → var(--p-line).

Осознанные исключения (зеркало реканона ПВЗ):
- q-table/q-stepper/q-list/q-item/q-timeline/q-infinite-scroll оставлены прямыми
  Quasar — обёртки функционально недостаточны, а quasar-canon.css их уже канонит.
- Общие доменные виджеты CatalogOfferCard/OrderCard/TakeoverDialog НЕ тронуты:
  они кросс-стольные (их рендерят также столы поставщика и админа), их реканон —
  отдельная задача «реканон виджетов Marketplace», поднимающая сразу 4 стола.

ESLint 0 на всех 8 файлах; sweep легаси по зоне стола — чисто; Rule 1 соблюдён.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 05:23:57 +00:00
ant 0335ed39cc feat(marketplace): визуальный реканон Стола ПВЗ — MONO Platform v2
7 операторских страниц + 4 диалога переведены на единый канон:
- сняты mp-role-operator/mp-card/mp-status-chip и токены --mp-*/rgba()
- FontAwesome (fa-solid) → Material q-icon
- статусы → BaseBadge; мультиселект-фильтры → тоггл-чипы (.chip);
  сводки → канон-плитки .kpi
- тулбары и действия → BaseButton/BaseInput/BaseSelect; панели → BaseCard;
  пустые состояния → EmptyState
- на каждой странице guard «вы не оператор кооперативного участка»
  (минимальный вид для не-оператора)
- q-table/q-tabs/q-stepper оставлены прямыми (канонятся quasar-canon.css;
  обёртки функционально недостаточны: сортировка/пагинация/мультиселект)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 20:34:37 +00:00
coopops 43d9363800 [C28-21][@ant] docs: backfill идемпотентен по (coopname, doc_hash), не package
Typecheck / desktop (pull_request) Successful in 13m31s
Typecheck / controller (pull_request) Successful in 13m7s
Устаревшая формулировка в docstring backfill-сервиса: после перехода на ключ
doc_hash идемпотентность/уникальность реестра — по (coopname, doc_hash), а НЕ по
(coopname, package). package сознательно не уникален (один процесс = несколько
разных документов с разными подписантами), поэтому ключом записи он быть не может.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 20:15:16 +00:00
coopops 33437c6b35 [C28-21][@ant] fix: block_num реестра = блок on-chain действия, не генерации
Typecheck / desktop (pull_request) Successful in 13m36s
Typecheck / controller (pull_request) Successful in 13m5s
Ось упорядочивания версий подписи одного doc_hash должна мерить одну величину
на обеих сторонах guard'ов ingestAction. Раньше block_num хранился через
resolveBlockNum(meta.block_num, action.block_num) с приоритетом meta.block_num
(блок ГЕНЕРАЦИИ документа фабрикой), а incomingBlock в guard'ах — action.block_num
(блок ДЕЙСТВИЯ в цепи). Так как документ всегда генерируется до подачи в цепь
(meta.block_num <= action.block_num), защита от отката на старую версию подписи
(incomingBlock < existing.blockNum) и идемпотентность по блоку фактически не
срабатывали. Теперь приоритет — action.block_num, meta.block_num лишь fallback.

Заодно выправлены устаревшие комментарии интерфейса репозитория (ключ — doc_hash,
а не hash).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 20:07:47 +00:00
ant 0f2f288ede feat(marketplace): Стол ПВЗ — рабочее место оператора конкретного КУ
Стол ПВЗ переориентирован с «списка всех ПВЗ кооператива» на рабочее место
оператора ЕГО кооперативного участка (председатель КУ или доверенное лицо).

- entities/OperatorBranch: стор активного КУ оператора на marketplaceWhoAmI
  (+ детали ПВЗ + best-effort core getBranches) и шапка OperatorBranchBar
  (название/адрес/статус ПВЗ, без кодов; селектор при нескольких КУ).
- Операторские страницы (поставки/приёмка/маркировка/выдача/возвраты/склад/
  сводный) переведены на активный КУ из стора — убраны ручные поля «Код КУ»
  и чипы-коды, автозагрузка по активному участку.
- Новая страница «Доверенные лица»: председатель КУ добавляет/снимает
  доверенных (features/Branch/ManageTrusted → addTrustedAccount/
  deleteTrustedAccount); правки гейтятся chairman.
- Страница «ПВЗ кооператива» (PvzList) удалена со Стола ПВЗ — управление
  сетью ПВЗ живёт на столе администратора.
- Админ-страница ПВЗ: кнопка «Карта» (OSM, без ключа) для точки с координатами.

Бэкенд-фоллоуапы (вне scope): обогащение whoAmI именем/доверенными для
оператора-только-доверенного; auth addtrusted для trustee КУ (сейчас chairman
кооператива); грант-нюанс — admin видит Стол ПВЗ через :all.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 19:54:58 +00:00
ant 0980c11db5 feat(marketplace): admin-стол «Пункты выдачи заказов» — подключение КУ как ПВЗ
Новая страница market-admin: соединяет список КУ (core getBranches) с их
ПВЗ-детализациями (marketplaceListKUDetails), председатель подключает участок
как пункт выдачи (адрес/контакты/режим работы → marketplaceDetailKU) и
управляет статусом/геокодингом. Диалог детализации переведён на канон
(BaseDialog/BaseButton + raw q-form с :rules), предзаполнение из карточки КУ,
правила времени/email вынесены в script (pug съедает backslash в inline-regex).
Статус-бейдж без dot (dot скрывал подпись), тултип с ошибкой геокода.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 19:23:40 +00:00
coopops 423648d342 [C28-21][@ant] fix(controller): ключ реестра — doc_hash (схлопывание версий подписи) + поиск пайщику по своим документам
Typecheck / desktop (pull_request) Successful in 13m57s
Typecheck / controller (pull_request) Successful in 13m34s
Два уточнения по замечаниям:

1. Ключ реестра переведён с hash на doc_hash. doc_hash = идентичность документа; hash включает
   наложенные подписи, поэтому у ОДНОГО документа бывает несколько hash (версии по мере накопления
   подписей — напр. акт приёма-передачи: 1-я подпись → один hash, 2-я → другой, doc_hash тот же).
   При ключе по hash документ задваивался в списке (одна подпись / две подписи). Теперь:
   - уникальный индекс (coopname, doc_hash); hash и package — неуникальные индексы;
   - ingestAction ключуется по data.document.doc_hash и держит КРАЙНЮЮ версию: guard'ы «не понижать
     статус» и «не откатывать на меньший block_num» (getState отдаёт status+blockNum); крайняя версия
     перетирает предыдущую (тот же документ, больше подписей);
   - hash хранится как версия последней подписи (для фильтра Union по document.hash и отображения).
   package по-прежнему группирует разные документы процесса (обмен = разные doc_hash = разные строки).

2. searchDocuments вернул доступ пайщику, но со скоупом: член совета (chairman/member) ищет по всему
   кооперативу, обычный пайщик — ТОЛЬКО по своим документам (search scope username=user.username).
   Снял role-гард, добавил @CurrentUser; SearchResolver решает скоуп по роли. Заодно закрывает прежнюю
   утечку (старый поиск был coop-wide для любого авторизованного).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 18:38:11 +00:00
coopops b9be36d203 [C28-21][@ant] fix(controller): ключ реестра — hash документа, а не package (package = процесс с N заявлений)
Typecheck / desktop (pull_request) Successful in 13m53s
Typecheck / controller (pull_request) Successful in 14m23s
Самоаудит по замечанию: package — это идентификатор ПРОЦЕССА, в который может входить несколько
заявлений с разными подписантами. Доказано в контракте: marketplace/change.cpp шлёт ДВА newsubmitted
с одним package, но разными document и username (обмен: contribute+return). Прежний уникальный индекс
(coopname, package) + upsert-по-package затирал второе заявление первым → ПОТЕРЯ ДОКУМЕНТА и склейка
username. Старый explorer-путь (список по newsubmitted) показывал оба.

- Сущность: уникальный индекс теперь (coopname, hash) — одна строка = один документ-заявление;
  (coopname, package) стал НЕуникальным группирующим индексом.
- ingestAction ключуется по data.document.hash; смена статуса (newsubmitted/newresolved/newdeclined,
  все несут то же заявление — подтверждено createagenda/declinedoc/make_complete_document) матчит
  строку по hash.
- newdecision/newact/newlink несут package (не заявление) → пересобирают агрегаты ВСЕХ заявлений
  пакета (repository.findByPackage), сохраняя статус каждого.
- Репозиторий: upsert/getStatus по hash; findByPackage вместо getSourceActionData; убраны
  неиспользуемые setStatus/exists.

Также: searchDocuments закрыт гардом RolesGuard + @AuthRoles(['chairman','member']) — поиск по
документообороту кооператива только для совета (SearchDocumentsInput не содержит username, поэтому
правило «свои ресурсы» не открывает доступ обычному пайщику). Поиск пайщиком по своим документам
пока не предусмотрен.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 18:20:38 +00:00
ant d117d87a96 feat(marketplace): канон стола поставщика — входящие/отгрузка/подпись/выплаты
Переверстка остальных страниц стола поставщика на MONO Platform v2 (как
«Мои предложения»):

- Входящие заказы: инфо-баннер с persist-скрытием, канон-`.tabbar` с deep-link
  ?status=, скелетоны вместо q-inner-loading, EmptyState, акцепт/отказ на карточке.
- Подготовка отгрузки, Подпись приёмки, История выплат: canon-таблица .table-wrap
  + TableSkeleton (каркас не дёргается), статусы через BaseBadge (точка+цвет из
  токенов), EmptyState с иконкой, ghost-refresh, инфо-баннер.
- Диалог подписи акта: BaseDialog + BaseButton + .banner вместо q-dialog/q-banner
  bg-primary.

Убраны дубли заголовков (заголовок в топбаре), легаси --mp-*/mp-role-*,
FontAwesome → Material q-icon. Скрываемый баннер вынесен в общий composable
useDismissibleBanner (DRY, 5 страниц).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 14:15:37 +00:00
coopops 3943802931 [C28-21][@ant] fix(controller): парность getDocuments — «Все входящие» = все статусы + скоуп backfill по кооперативу
Typecheck / desktop (pull_request) Successful in 14m40s
Typecheck / controller (pull_request) Successful in 13m16s
Самоаудит видимости документов выявил две регрессии относительно прежнего explorer-пути:

1. Вкладка «Все входящие» (type=newsubmitted) теряла решённые документы. Прежний фильтр был по
   ИМЕНИ действия: у каждого пакета есть newsubmitted-действие → «Все входящие» = все документы
   (решённые показывались и тут, и во «Только утверждённые»). Партиция по статусу (newsubmitted→
   status=Submitted) скрывала решённые из дефолтной вкладки User/DocumentsPage. Фикс: newsubmitted →
   без фильтра статуса (все), newresolved → status=Resolved.

2. Backfill сканировал explorer с query={} — без скоупа. Explorer (cooparser) хранит трейсы по
   получателям; без receiver вернулись бы все трейсы каждого действия (soviet/пайщик) и, возможно,
   чужие кооперативы. Фикс: query={receiver: coopname} — ровно как прежний chairman-путь getDocuments
   (один трейс на документ, только наш кооператив).

Утечки не было и нет: RolesGuard разрешает обычному пайщику только data.username==его_имя, а фильтр
findAggregates ограничивает username; coopname-скоуп (весь кооператив) доступен лишь совету/председателю.
findAggregates всегда фильтрует по config.coopname.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 14:06:54 +00:00
ant af5c8e33ee feat(marketplace): канон оферт — редактирование изображений, republish, sticky-форма
«Мои предложения»: info-карточка .banner с persist-скрытием, скелетоны вместо
лоудеров, канон-вкладки с deep-link ?status=, кнопка «Создать» в правом верхнем
углу через teleport, republish для WITHDRAWN. Форма создания/редактирования:
единый заголовок (без дублей), один .banner, sticky нижний бар «Отменить/Далее/
Сохранить» фикс-высоты, форматирование цены при префилле. REJECTED правится и
переотправляется через форму с показом причины отклонения.

Изображения: единая галерея — добавление (не замена), удаление и смена обложки
у любой картинки с самого начала. Backend отдаёт/принимает bucket_key (сохранить
существующее) либо base64 (новое); чужой ключ отклоняется. Signed-URL чтения
стабилизирован в окне 300с — не мигает при polling.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 14:06:35 +00:00
coopops 33801798ed [C28-21][@ant] refactor(controller): полное удаление OpenSearch — поиск и список документов целиком на Postgres
Typecheck / desktop (pull_request) Successful in 14m26s
Typecheck / controller (pull_request) Successful in 13m48s
Завершение перехода одним PR: searchDocuments и getDocuments уже читают PG-реестр подписанных
документов, OpenSearch стал мёртвым кодом — убираем его полностью.

- Удалены infrastructure/search/* (opensearch.service, search-registry.service,
  search-infrastructure.module) и application/search/services/search-event.service
  (старый индексатор по newsubmitted — заменён SignedDocumentIngestionService + backfill).
- app.module: снят SearchInfrastructureModule. search.module: только SearchResolver (читает
  SIGNED_DOCUMENT_REPOSITORY из глобального TypeOrmModule).
- package.json: убрана зависимость @opensearch-project/opensearch; pnpm-lock.yaml пересчитан.
- docker-compose: удалён сервис opensearch + volume opensearch_data.
- .env-example: убраны OPENSEARCH_* (поиск больше не конфигурируется через env).
- system.interactor: features.search = true (поиск на PG доступен всегда, не зависит от OPENSEARCH_ENABLED).

Файлы удалены только из текущего состояния (git rm) — в истории остаются.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 11:36:55 +00:00
coopops aeeb9de380 [C28-21][@ant] fix(controller): getDocuments — точная эквивалентность read-path (receiver-скоуп + фильтр по hash)
Typecheck / desktop (pull_request) Successful in 13m35s
Typecheck / controller (pull_request) Successful in 13m6s
Старый getDocuments через DocumentService подмешивал в explorer-фильтр `receiver: username`
(on-chain require_recipient): receiver=coopname отдавал все документы кооператива, receiver=<пайщик>
— его документы; Union-страница добавляла filter.document.hash. PG read-path эти ключи игнорировал
(искал несуществующий data.username) — личная страница пайщика показала бы все документы кооператива.

- interactor: extractReceiverScope (receiver===coopname → весь кооператив; иначе фильтр username=receiver,
  т.к. колонка username = пайщик-субъект заявления) + extractHashFilter (filter.document.hash).
- repository: новый параметр hash в SignedDocumentListParams; фильтр UPPER(d.hash)=UPPER(:hash)
  (фронт шлёт хэш в upper-case).

Набор документов getDocuments теперь совпадает со старым explorer-путём для всех трёх потребителей
(Cooperative/ListOfDocuments, User/DocumentsPage, Union/ListOfCooperatives).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 11:30:02 +00:00
ant ce2117af25 fix(storage): отдавать Cross-Origin-Resource-Policy: cross-origin для файлов
Картинки оферт SPA грузит как <img> с другого origin (в dev — другой
порт, чем у бэкенда :3028). Глобальный helmet ставит CORP same-origin,
браузер режет загрузку (ERR_BLOCKED_BY_RESPONSE.NotSameOrigin) — CORS-
обход на это не влияет, это другой механизм. Переопределяем CORP на
cross-origin на успешном ответе ручки /api/storage: доступ к объекту
защищён HMAC-подписью с TTL в URL, а не origin'ом, поэтому ослабления
безопасности нет. Работает и в dev, и в prod независимо от раскладки
origin'ов.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 09:26:48 +00:00
coopops 4d1b3b87c1 [C28-21][@ant] feat(controller): денормализация ФИО подписантов (signers_text) + поиск по подписанту
Typecheck / desktop (pull_request) Successful in 13m28s
Typecheck / controller (pull_request) Successful in 13m12s
Поиск по документам — в основном по фамилии подписанта, а не username. content_text (html заявления)
этого не покрывает: соподписанты (председатель/совет) подписывают решение/акты, а не тело заявления,
плюс зависимость от текста шаблона. ФИО уже присутствуют в агрегате — в signer_certificate каждой подписи
(DocumentAggregator резолвит их из учётных данных подписанта).

- Новая колонка signed_documents.signers_text — ФИО/наименования всех подписантов пакета
  (last/first/middle_name физлиц-ИП и short_name организаций из signer_certificate всех частей
  агрегата: заявление/решение/акты/связанные) + их username.
- Заполняется на ingestion и backfill из готового агрегата (collectSignersText).
- searchDocuments ILIKE теперь покрывает signers_text (приоритетно), full_title, content_text, username.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 09:24:14 +00:00
ant e32cea69a6 fix(marketplace): после создания оферты — на «Мои предложения»; карточка с каруселью, изображениями и русскими единицами
- Поставщика после публикации возвращаем на стол «Мои предложения»
  (там сразу видна оферта «На модерации»), а не в каталог заказчика.
- CatalogOfferCard: медиа-область теперь q-carousel (свайп/стрелки,
  без точек, изображение во всю ширину) вместо одиночного <img>;
  проп images[] (обратно совместимо с preview).
- Прокинули изображения оферты в карточку на всех столах (мои
  предложения, модерация, каталог) — раньше всегда была заглушка.
- Единицы измерения по-русски: вынес общий справочник
  shared/lib/consts/marketplace-units (опции + короткая подпись) и
  helper shared/lib/utils/marketplaceOfferImages (обложка первой);
  убрал 4 дубля карты единиц и 2 дубля сортировки картинок (DRY).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 09:18:21 +00:00
ant 94aac6168e fix(marketplace): человекочитаемое сообщение после публикации оферты
Убрал из SuccessAlert технический id и английскую константу статуса
(result.status) — пользователю это не нужно. Сам SuccessAlert не трогаю
(правится в dev-ветке).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 09:08:00 +00:00
ant c2cee3b42d fix(marketplace): карусель во всю ширину без точек + персист изображений черновика
- Карусель предпросмотра: убран дефолтный padding слайда q-carousel
  (изображение во всю ширину), убраны точки-навигация — только свайп и стрелки.
- Черновик формы теперь сохраняет и изображения (base64 + data-URL превью
  после reload), если суммарный размер укладывается в бюджет (~2M base64
  символов); сверх бюджета или при QuotaExceeded — поля сохраняются без
  картинок. Сохранение дебаунсится (400мс), чтобы не сериализовать base64
  на каждое нажатие; coverIndex тоже в черновике.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 09:02:58 +00:00
coopops d34303a5c4 [C28-21][@ant] feat: getDocuments+searchDocuments на Postgres, newdeclined в cooptypes, авто-backfill агрегатов
Typecheck / desktop (pull_request) Successful in 13m43s
Typecheck / controller (pull_request) Successful in 13m14s
Вторая итерация C28-21 — сервис функционирует по-новому из PG:

- cooptypes: заведено registry-действие soviet `newdeclined` (+ экспорт в Registry); отслеживается ingestion'ом.
- Реестр хранит ГОТОВЫЙ агрегат пакета (document_aggregate jsonb) + денормализованную проекцию
  (full_title/content_text/action/registry_id/username/block_num/document_created_at) для поиска и фильтра.
- Ingestion собирает агрегат через DocumentPackageAggregator на 6 событиях soviet:
  newsubmitted/newresolved/newdeclined (статус) + newdecision/newact/newlink (досборка по source_action_data).
- getDocuments — read-path из PG: отдаём готовые агрегаты, фильтр по статусу(type)/действию/пайщику/блокам,
  без обращений к explorer/Mongo на лету.
- searchDocuments — SQL ILIKE по PG-реестру (контракт SearchResultDTO сохранён).
- Backfill запускается автоматически РАЗОВО при пустом реестре (onApplicationBootstrap, детачем) или
  принудительно по SIGNED_DOCS_BACKFILL_ON_BOOT=true; идемпотентен по (coopname, package).

OpenSearch-инфра пока оставлена (снос — отдельный финальный шаг). cooptypes/dist gitignored — пересобирается в CI.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 08:52:48 +00:00
ant 5bc67d877d feat(marketplace): черновик формы в LocalStorage + slide-карусель + канон карточки
- persistentState: при создании предложения форма (поля + текущий шаг)
  автосохраняется в LocalStorage и восстанавливается после перезагрузки;
  очищается при успешной публикации. Изображения не сохраняются (object-URL
  невалиден после reload, base64 не влезает в LocalStorage).
- Карусель предпросмотра: slide-right/slide-left транзишн — изображения
  сматываются вбок неразрывно, а не исчезают/появляются.
- Карточка-предпросмотр переверстана по канону: тень --p-shadow-card,
  название + категория в шапке, цена крупно слева и наличие чипом справа
  (pos/neg), блок «Характеристики» с разделителем и аккуратными строками.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 08:51:27 +00:00
ant 61bfdb69d7 fix(marketplace): символ валюты из system info, карусель предпросмотра, мелочи UX
- Цена: символ валюты из useSystemStore().governSymbol (блокчейн-параметр
  root_govern_symbol), а не хардкод «₽» — при смене символа цепи фронт не правим.
- Количество и чекбокс «без ограничения» в одной строке (поле уже не во всю
  ширину); чекбокс выровнен, строка не прыгает.
- Уменьшен вертикальный gap между полями (p-4 → p-3) — ближе к канону.
- Убраны дубли заголовков шагов «Фотографии товара»/«Как набираются заказы»
  (их рисует VerticalStepper из step.description).
- Предпросмотр: q-carousel со свайпом и стрелками вместо нижних миниатюр
  (место под миниатюрами зарезервировано под будущие варианты товара).
- BaseRadioCard: hover по выбранной карточке больше не перекрашивает её
  в белый (selected:hover сохраняет выбранный фон).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 08:37:03 +00:00
coopops f5ff2a7b10 [C28-21][@ant] feat(controller): реестр подписанных документов в Postgres — ingestion + backfill
Typecheck / desktop (pull_request) Successful in 14m18s
Typecheck / controller (pull_request) Successful in 13m30s
Фундамент C28-21: PG-проекция подписанных документов (таблица signed_documents),
наполняемая ловлей blockchain-событий soviet с внутренней шины и разовым backfill'ом.
Аддитивно — OpenSearch и резолверы (searchDocuments/getDocuments) пока не трогаются.

- SignedDocumentEntity (signed_documents): package/hash/coopname/username/status,
  full_title/content_text/html/pdf(bytea), document_aggregate(jsonb), meta, block_num.
- SignedDocumentRepository (+SIGNED_DOCUMENT_REPOSITORY) + TypeORM-реализация (upsert/setStatus/search ILIKE).
- SignedDocumentIngestionService: @OnEvent(action::soviet::{newsubmitted,newresolved,newdeclined})
  -> идемпотентный upsert с защитой от понижения статуса; контент из factory-Mongo.
- SignedDocumentBackfillService: разовый идемпотентный скан действий (env-gate SIGNED_DOCS_BACKFILL_ON_BOOT).
- Регистрация в TypeOrmModule + app.module.

newdeclined пока не экспортирован как registry-action в cooptypes — подписка по литералу на будущее.
Следующая итерация: перевод searchDocuments/getDocuments на PG + снос OpenSearch + тесты.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 08:31:49 +00:00
ant 1d6e2ff36c fix(marketplace): полировка мастера создания предложения + лимит тела запроса
- Поля формы плотнее (dense + no-error-icon) по канону MONO.
- Блок количества не «прыгает»: чекбокс «без ограничения» сверху,
  поле количества всегда отрисовано (disable при безлимите), не v-if.
- Цена — два знака после запятой (рубли/копейки): regex 2 знака,
  ввод через точку или запятую, отображение через formatAsset2Digits.
- Изображения: выбор обложки кликом, непрозрачный бейдж «Обложка»,
  заметная кнопка удаления; обложка уходит в payload первой.
- Шаг «Проверка» — карточка-предпросмотр (главное фото + слайдер
  миниатюр + название/категория/цена/наличие/характеристики).
- Ошибки/успех через канон FailAlert/SuccessAlert (правый нижний угол,
  разворот GraphQL-ошибки вместо [object Object]); убран полноэкранный
  Loading — лоадер на кнопке.
- app.ts: express.json/urlencoded limit 2mb → 160mb (base64-загрузка
  изображений в GraphQL); зеркально в playbooks nginx.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 08:12:45 +00:00
coopops e96e157b91 fix(desktop): убрать гонку Teleport-host шапки и обновлять гранты/столы после регистрации
Баг A: предупреждение "Failed to locate Teleport target '#header-actions-host'"
возникало, потому что host был под v-if='loggedIn'. Сразу после регистрации
навигация на стол происходит раньше, чем loggedIn (зависит от async
session.isAuth / isRegistrationComplete) станет true → цель Teleport
отсутствует в DOM → даже defer-Teleport источников страниц её не находит.
Рендерим #header-actions-host безусловно — видимость блока действий уже
регулируется CSS (.topbar__actions:has(> .header-actions-host:only-child:empty)).

Баг B: после регистрации grant-gated кнопки («Совершить взнос», «Получить
возврат») не появлялись до F5. В потоке завершения регистрации (watch на
participant_account) не вызывался desktops.loadDesktop() (свежие столы/гранты),
а кошелёк/agreements не грузились: на момент первого run() статус ещё не
'active' → isFullyActive=false. После того как статус обновился, до-вызываем
run(true) и loadDesktop() по образцу init-app / EnableButton. Условие
isFullyActive в init-wallet не ослаблялось.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 07:39:59 +00:00
coopops bbb0ae5e41 feat(desktop): placeholder для наименования банка в форме ИП
Только неочевидное поле — наименование банка получило подсказку ПАО "Сбербанк".
Остальные поля ИП самоописательны (формат даты, кол-во цифр, «как в паспорте»).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 07:26:02 +00:00
coopops 2eeb64c188 feat(desktop): добавить placeholder-примеры в форму организации при вступлении
Поля краткого/полного наименования, должности представителя и наименования
банка получили placeholder-подсказки (ПК "Ромашка", Председатель совета,
ПАО "Сбербанк") — показываются при фокусе пустого поля.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 07:25:37 +00:00
ant f533171329 feat(marketplace): загрузка изображений offer + многошаговый мастер создания предложения
Форма «Создать предложение» переверстана по канону MONO Design System как
VerticalStepper-мастер (Товар → Цена и наличие → Условия поставки → Изображения
→ Проверка; эталон CreateMeetForm): бизнес-язык вместо time_based/volume_based,
BaseRadioCard под отсечку, reserve-hint-space против layout shift.

Изображения товара (закрыт gap parent-UX, где MVP-epics уронили offer-фото):
- jsonb-колонка marketplace_offer.images ({bucket_key,content_hash,mime_type}[],
  индекс 0 = обложка); миграция v7 (DDL через synchronize);
- MarketplaceOfferImagesService на @UseBucket('stol-zakazov:images', AR31) —
  зеркало return-claim-канона; ключ content-addressed offers/<coop>/<owner>/<sha>;
- base64-инпут images в create/update DTO (≤8, ≤10МБ, jpeg/png/webp), декод+put
  в offer.service + cleanup сирот при провале записи;
- @ResolveField images → HMAC-signed URL (ленивый резолв, не в каждом list);
- codegen: schema.gql + zeus (controller+sdk) + offerSelector.

DRY: fileToBase64 вынесен в shared/lib/utils, два return-claim-дубля переключены.

Research вариантов продукта (off-chain PG-only, контракт не трогается; счётчики
3.4 на variant-уровень — самая инвазивная часть; FR18a снимается, не нарушается)
зафиксирован в плане проекта (Эпик 13, research-only).

Тесты: offer-service 43/43 (вкл. 3 новых на images), moderation+counters зелёные;
tsc контроллера 0, ESLint desktop 0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 07:18:58 +00:00
coopops 33d4230873 fix(desktop): не терять выбор в BaseRadioCard при наведении
Hover-правило .base-radio-card:hover (специфичность 0,2,0) перекрывало
.base-radio-card--selected (0,1,0), поэтому при наведении на выбранную
карточку программы primary-фон/граница подменялись на surface/soft и
выбор визуально пропадал. Ограничил hover невыбранными карточками через
:not(.base-radio-card--selected).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 07:07:29 +00:00
ant 3919fe7fa8 fix(marketplace): гейт стола заказчика для председателя + подпись оферты долетает
Два бага L3-онбординга заказчика:

1) Председатель (и совет) видели рабочие страницы стола заказчика
   (Каталог/Мои заказы/Сводный) до подписи оферты: страницы требовали
   Offer:read / Order:read:own, а admin/board дают голый Offer:read и
   Order:read:all, который разворачивается в :own — гранты «протекали»
   на стол заказчика. Фикс: рабочие страницы требуют orderer-эксклюзивный
   Order:create (нет у admin/board, не покрывается :all-разворотом) —
   стол заказчика гейтится персональной подписью для любой роли.

2) Подпись оферты не долетала: getOnboardingState читал AgreementRepository
   (soviet::agreements3), а ЦПП «Стол заказов» — ПРОГРАММА (program_id=2),
   и wallet::signagree пишет подпись в wallet::users.programs[]. Источник
   истины — UserAgreementRepository.findProgram(program_id) (канон
   AgreementService.fetchProgrammatic). Из-за чтения не той таблицы
   requires_gate оставался true навсегда. Фикс: читаем программную подпись.

3) Авто-переключение после подписи: фронт коротко поллит getOnboardingState
   пока подпись синхронизируется в PG (wallet::signagree уже подтверждён
   цепочкой), затем loadDesktop+навигация — без ручного refresh.

Тесты: onboarding-service переписан под программный источник (6), grants (8) — 14/14.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 06:21:12 +00:00
ant 71f629b084 fix(marketplace): L3-гейт онбординга заказчика через гранты
Стол заказчика до подписи персональной оферты ЦПП показывал все
страницы (Каталог и т.д.) — провайдер грантов гейтил только на уровне
кооператива (coopAcceptance), а L3-подпись конкретного пайщика в гранты
не заходила. Страница подключения требовала Offer:read — тот же грант,
что и Каталог, поэтому не отличалась от рабочих страниц.

- backend resolveGrants: для orderer-роли спрашиваем
  MarketplaceOnboardingService.requires_gate; пока не подписал —
  вместо orderer-прав выдаём маркер Onboarding:orderer (симметрия
  L1-гейту председателя Extension:configure). Прочие роли не зависят.
- install.ts: страница подключения первой в столе, requires
  Onboarding:orderer; после подписи авто-скрывается (grant-driven).
- страница: один подписываемый документ (оферта), убран дубль-заголовок,
  refresh грантов после подписи (loadDesktop+loadExtensionRoutes), без
  поллинга и без loadDesktop на каждом переходе.
- unit-тест провайдера: L1/L3-гейты, 8/8.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 05:56:38 +00:00
ant 96f143f1b4 fix(desktop): онбординг ЦПП и видимость столов — вёрстка + grants
- WorkspaceSwitcher: видимость столов через канон desktop.isWorkspaceVisible
  (grants) вместо локального meta.roles — marketplace-столы (Заказчик/Поставщик/
  ПВЗ) больше не видны до приёма ЦПП Советом. Был рассинхрон с CmdkMenu: при
  merge grants-канона переключатель в шапке забыли перевести на канон.
- OnboardingCoopAcceptCpp: убрана дублирующая шапка страницы; «Не подключено»
  и таймер сведены в один ряд в CouncilOnboardingCard (новый слот #status +
  проп subtitle); список шагов разрежён, чип «Ожидаем решение совета» больше
  не на всю ширину; экран «подключено» переведён на канон EmptyState с
  success-акцентом иконки.
- Стол заказчика: иконка «Готово к получению» fa-box-check → fa-box-open
  (первой нет в FA6 free — не рисовалась).
- Экосистема: meta.hidden=true (скрыта из меню/поиска; маршрут и код оставлены
  до публикации раздела).
- Дизайн-система: страница-витрина удалена полностью + вычищены ссылки на неё
  в extensions/market/CLAUDE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 05:27:59 +00:00
ant 3406b63072 [@dark] feat(desktop/marketplace2): фильтрация меню через canon-grants
LeftDrawerMenu.filteredRoutes и default.vue::paletteWorkspaces переключены
с legacy meta.roles.includes(userRole) на DesktopStore.isPageVisible /
isWorkspaceVisible. Для grant-стола (marketplace, etc) видимость пунктов
сверяется с meta.requires против выданных бэкендом прав; для legacy-стола
canon-функции сами fall-back'аются на meta.roles по core-роли — поведение
по умолчанию сохранено.

meta.hidden и meta.conditions остаются дополнительными фильтрами
(participant/chairman/soviet используют conditions; capital — hidden).

Marketplace2-only: canon-grants (isPageVisible/isWorkspaceVisible)
существуют только в marketplace2; в dev этих функций в DesktopStore нет —
там фильтр остаётся legacy meta.roles.
2026-05-28 19:18:39 +00:00
ant 1d2bf29953 [merge-dev-into-marketplace2][@dark] chore: CommandPalette — primary + поведение стрелок/обводки/sticky из design-wave1 2026-05-28 19:17:15 +00:00
coopops 829f8f823b [design-wave1][@dark] fix: палитра команд — рамка курсора на любом столе, не только активном
Заголовок workspace-row получал outline только когда стол активный
(.is-active .row.is-selected). На неактивных столах при навигации курсор
«терялся»: только лёгкая смена фона на surface-2, без рамки — в отличие
от page'ей, которые подсвечиваются outline'ом всегда. Выровнял правило:
outline на любом .workspace-row.is-selected.
2026-05-28 19:17:02 +00:00
coopops 9f92e4bb52 [design-wave1][@dark] fix: палитра команд — clamp навигации стрелками вместо wrap-around
На верхней/нижней позиции стрелка дальше прыгала в противоположный
конец списка из-за (idx+delta+len)%len. Заменено на clamp в [0..len-1]:
курсор остаётся на месте, как в нативном listbox/меню. Пустое выделение
+ ↓ ставит на первый, ↑ — на последний.
2026-05-28 19:17:02 +00:00
coopops 8d895c5fa9 [design-wave1][@dark] fix: палитра команд — снят sticky-баннер активного стола
Активный workspace был position:sticky top:0 — при навигации стрелкой
вверх курсор-выделение «исчезал» за sticky-overlay'ем, потому что
scrollIntoView({block:'nearest'}) считал элемент видимым в normal flow,
не учитывая sticky-перекрытие. Активный стол и так идёт первым по
сортировке + есть badge «Активный», sticky не нужен.
2026-05-28 19:17:02 +00:00
coopops 8bfd4e916c [@dark] fix(desktop): CommandPalette — primary вместо accent + meta.conditions/hidden + drawer close на мобильниках
- CommandPalette.vue: все --p-accent / --p-accent-soft / --p-ink-on-accent
  заменены на --p-primary (бирюзовая палитра канона). Откатывается
  регрессия после fix-коммитов design-wave1 (sticky-banner / clamp /
  рамка курсора), которые случайно вернули accent поверх primary
  (см. b82b778b81 — изначально palette был в primary).

- default.vue::paletteWorkspaces: возвращена фильтрация meta.conditions
  (participant/chairman/soviet install.ts) + meta.hidden (capital
  install.ts) — та же логика что в LeftDrawerMenu.filteredRoutes,
  чтобы CommandPalette не показывал страницы, которые не отображаются
  в rail-меню. onSelectWorkspace/onSelectPage закрывают левый дровер
  на мобильниках через desktop.closeLeftDrawerOnMobile().
2026-05-28 19:15:47 +00:00
ant e14e3346b2 [merge-dev-into-marketplace2][@dark] chore: canon CommandPalette + снос осиротевшего Cmdk-стека из dev
Конфликты modify/delete разрешены через delete (canon-направление):
- entities/CmdkMenu/model/store.ts — заменён на entities/CommandPalette
  (минимальный store isOpen/open/close/toggle).
- widgets/Desktop/SecondLevelMenuList — снесён, AppDrawer.rail заменяет.
- widgets/Desktop/WorkspaceMenu — снесён, WorkspaceSwitcher заменяет.
- widgets/Wallet/MicroWallet — снесён, RailUserCard заменяет.

NB: фильтрация пунктов меню в LeftDrawerMenu.filteredRoutes и
default.vue::paletteWorkspaces в dev — legacy meta.roles. В marketplace2
есть canon-grants (DesktopStore.isPageVisible/isWorkspaceVisible) —
переключение на canon потребует отдельного коммита поверх merge'а.
2026-05-28 19:04:44 +00:00
coopops df26d5ac9c [@dark] refactor(desktop): canon CommandPalette + снос осиротевшего меню — фикс утерянного ⌘K после design-wave1
- layouts/default.vue: вместо widgets/Desktop/CmdkMenu mount'им
  shared/ui/domain/CommandPalette (canon из design-wave1) с адаптером
  workspaceMenus → CommandPaletteWorkspace[] и глобальным ⌘K / Ctrl+K.
- entities/CommandPalette: минимальный store (isOpen / open / close /
  toggle) — заменяет тяжёлый useCmdkMenuStore.
- LeftDrawerMenu.onCmdk → palette.open() (триггер «Найти» в AppDrawer).
- Удалены целиком осиротевшие после design-wave1 директории:
  entities/CmdkMenu, widgets/Desktop/CmdkMenu, widgets/Desktop/CmdkTrigger,
  widgets/Desktop/SecondLevelMenuList (никем не использовался),
  widgets/Desktop/WorkspaceMenu (заменён на WorkspaceSwitcher),
  widgets/Wallet/MicroWallet (заменён на RailUserCard).
- Поправлены barrels widgets/Desktop и widgets/Wallet.
2026-05-28 19:02:24 +00:00
coopops c8f93cfbae fix(desktop): читаемость тостов на тёмной теме
Фон и текст тоста (.q-notification + .toast) теперь фиксированные тёмная
поверхность + светлый текст в обеих темах, а не инвертируемые
var(--p-ink)/var(--p-canvas). На dark-теме текст уходил в тёмный на тёмном
deep-tint фоне (#052e16 и т.п.) — нечитаемо. Цветные иконки по типу
остаются на токенах.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 17:58:30 +00:00
coopops 2136252933 fix(desktop): читаемость тостов на тёмной теме
Фон и текст тоста (.q-notification + .toast) теперь фиксированные тёмная
поверхность + светлый текст в обеих темах, а не инвертируемые
var(--p-ink)/var(--p-canvas). На dark-теме текст уходил в тёмный на тёмном
deep-tint фоне (#052e16 и т.п.) — нечитаемо. Цветные иконки по типу
остаются на токенах.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 17:58:18 +00:00
ant e5e97d9074 fix(desktop): отступ документа от рамки в диалоге подписи соглашения
q-pa-lg на flat-карточке SignAgreementDialog — документ больше не прижат
к границе на странице кошелька пайщика.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 17:51:58 +00:00
ant 1ff7bd0db5 fix(desktop): отступ документа от рамки в диалоге подписи соглашения
q-pa-lg на flat-карточке SignAgreementDialog — документ больше не прижат
к границе на странице кошелька пайщика.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 17:51:33 +00:00
coopops 7d2f49468d [blago-cli-add-filter][@ant] fix(blago-cli): фильтр non-entity .md в add + понятные ошибки push — чтобы blago add . не валил push «type: undefined»
— format/peekBlagoEntityType: не бросает; возвращает project|issue|story|undefined.
— sync/add: классификация unreadable/non-entity/clean/dirty; README/notes/CLAUDE/AGENTS не попадают в staging, отдельный счётчик skippedNonEntity.
— sync/push: на старте чистит staging от non-entity (warn); ошибки parseBlagoMarkdown / validateParsedForPush префиксуются путём файла.
— cmd/run-cli: вывод add показывает skippedNonEntity.
— bump 2026.5.28-1.
2026-05-28 17:17:08 +00:00
ant 4444302ce5 [merge-dev-into-marketplace2][@dark.sun] fix(marketplace): зарегистрировать MarketplaceBranchOwnershipService в DI
Восстановленный после merge dev сервис не был добавлен провайдером
в MarketplaceExtensionApplicationModule — NestJS падал при boot:
"Nest can't resolve dependencies of the MarketplaceReturnClaimResolver
... Symbol(MarketplaceBranchOwnershipService) at index [2]".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 17:12:14 +00:00
ant 7f01908a84 [merge-dev-into-marketplace2][@dark.sun] chore: merge origin/dev в marketplace2
Резолв конфликтов:
- ledger2/operations.hpp: оставлен CONVERT_TO_MEMBER (marketplace2) +
  принята новая логика withdraw из dev (WITHDRAW_PENDING вместо BLOCK).
- ledger2/wallets.hpp: реестр расширен до 16 кошельков
  (USER_SHARED=6 с MARKETPLACE_ORDER_LOCK + COOPERATIVE=10 с WITHDRAW_PENDING).
- WalletProgramWidget.vue / WalletWidget.vue: убраны неиспользуемые helper'ы из marketplace2,
  принят рефактор dev.
- schema.gql + zeus/* (controller + sdk): полная регенерация после merge.

Сопутствующие правки:
- Восстановлен marketplace-branch-ownership.service.ts (отсутствовал в origin/marketplace2,
  но был импортирован из marketplace-return-claim.resolver.ts — взят из ветки
  chore/trustees-ownership 46f5269b89).
- Исправлена опечатка MARKETPLACE_KU_CHAIRMEN_SERVICE → MARKETPLACE_KU_CHAIRMAN_SERVICE
  в marketplace-desktop-grants.provider.ts.
- orderSelector.ts: blocked_amount → locked_amount, удалены did_convert/did_assign
  (привести selector к новой схеме MarketplaceOrderCreateTxSnapshot).
- Пересобраны cooptypes и inter (stale dist после merge).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 17:03:20 +00:00
coopops 9d3be7259f [mobile-drawer][@ant] fix: левый дровер — автозакрытие на мобиле при смене маршрута 2026-05-28 16:43:15 +00:00
ant 4ffb32ff9e Merge pull request 'fix(desktop): vue-tsc — IAgenda для QuestionsTable + IPayment.id:string' (#45) from fix/desktop-vue-tsc-errors into dev
Reviewed-on: #45
2026-05-28 16:42:01 +00:00
ant 06b80527c7 Merge pull request '[598][@ant] feat(marketplace): documentation pass + grants canon + onboarding via council + payout/IDOR/own-KU fixes' (#46) from feat/marketplace-docs into marketplace2
Reviewed-on: #46
2026-05-28 16:41:24 +00:00
Alex Ant 2747ac34a4 chore(release): publish
Build bootstrap container / build (push) Successful in 3m27s
Release / release (push) Successful in 26m48s
Release / publish-packages (push) Has been skipped
Release / trigger-coopenomics-docs (push) Has been skipped
Release / trigger-mono-docs (push) Has been skipped
2026-05-28 21:17:47 +05:00
ant d67d40b04d [merge-marketplace2-drop-blocked][@ant] chore: регенерация Zeus-клиента после merge PR #41 marketplace2 (drop blocked → w.mkt.order, переименование chairmen→chairman) 2026-05-28 16:03:51 +00:00
ant 25b6fed50c Merge remote-tracking branch 'origin/marketplace2' into feat/marketplace-docs
# Conflicts:
#	components/controller/schema.gql
#	components/controller/src/extensions/marketplace/application/resolvers/marketplace-apl-reception.resolver.ts
#	components/controller/src/extensions/marketplace/application/resolvers/marketplace-inventory.resolver.ts
#	components/controller/src/extensions/marketplace/application/resolvers/marketplace-issuance.resolver.ts
#	components/controller/src/extensions/marketplace/application/resolvers/marketplace-return-claim.resolver.ts
#	components/desktop/extensions/market/install.ts
#	components/desktop/src/pages/Marketplace/OffererMyOffers/ui/OffererMyOffersPage.vue
2026-05-28 16:02:18 +00:00
coopops 745df7f6cc [fix-vue-tsc][@dark] fix: IPayment override → локальный IPaymentRow в виджете
Typecheck / desktop (pull_request) Successful in 13m36s
Typecheck / controller (pull_request) Successful in 13m14s
Глобальный Omit<IPayment,'id'>&{id:string} ломал store.ts/mergePayments
и features/Payment/SetStatus/api (туда приходит сырой Zeus-вывод с
id:unknown). Откатываю override в entities/Payment/model/types.ts,
сужаю id до string локально в ListOfPaymentsWidget через IPaymentRow —
точка сужения только в UI, контракт стора/API не трогается.

vue-tsc desktop локально: PASS.
2026-05-28 15:32:24 +00:00
ant d2c0f605d0 [merge-marketplace2][@ant] chore: регенерация schema.gql и zeus-клиента после merge marketplace2 — подхватили обновлённое описание marketplaceListInventoryLabels (admin/совет видят весь склад, оператор — только свой участок) и интеграцию MarketplaceBranchOwnershipService в return-claim resolver 2026-05-28 15:16:21 +00:00
ant 2734d9dc63 Merge remote-tracking branch 'origin/marketplace2' into feat/marketplace-docs
# Conflicts:
#	components/controller/src/extensions/marketplace/application/resolvers/marketplace-apl-reception.resolver.ts
#	components/controller/src/extensions/marketplace/application/resolvers/marketplace-inventory.resolver.ts
#	components/controller/src/extensions/marketplace/application/resolvers/marketplace-issuance.resolver.ts
#	components/controller/src/extensions/marketplace/application/resolvers/marketplace-return-claim.resolver.ts
2026-05-28 15:10:22 +00:00
coopops 61283a7276 [fix-vue-tsc][@dark] fix: vue-tsc desktop — IAgenda в QuestionsTable + IPayment.id:string
Typecheck / desktop (pull_request) Failing after 8m13s
Typecheck / controller (pull_request) Successful in 13m20s
QuestionsTable принимал Cooperative.Document.IComplexAgenda[] (cooptypes,
table:IDecision без certificates, id:IUint64=number|string), а сверху
ListOfAgendaQuestions передаёт IAgenda[] из desktop/entities/Agenda
(GraphQL-вывод getAgenda, table:BlockchainDecision с certificates,
id:number). Перевожу QuestionsTable на IAgenda — это реальная форма
данных из стора; уходит расхождение IComplexAgenda↔IAgenda для
:agenda='row' в QuestionCard (он тоже IAgenda).

IPayment.id был ModelTypes['ID']=unknown (Zeus не имеет scalar-resolver
для ID), из-за чего шаблон ListOfPaymentsWidget падал на expanded.get/.set
и :id-биндингах. Переопределяю id как string через Omit+&.
2026-05-28 15:08:37 +00:00
ant 14bee19c87 Merge pull request 'Минимум членов совета на этапе Install Coop: 5 → 3' (#44) from fix/min-soviet-members-3 into dev
Reviewed-on: #44
2026-05-28 15:01:04 +00:00
coopops 5ee3a60b72 [soviet][@dark] fix: MIN_SOVIET_MEMBERS_COUNT 5 → 3 для прод
Typecheck / desktop (pull_request) Failing after 8m42s
Typecheck / controller (pull_request) Has been cancelled
Снижает минимальный порог состава совета кооператива на этапе
Install Coop с 5 до 3 человек. Единственный ограничитель —
контракт soviet::createboard (см. consts.hpp + createboard.cpp:58).
Backend (install.interactor) и desktop (SetSovietForm) собственных
числовых проверок не имеют — фронт требует только «хотя бы одного»,
бэкэнд передаёт массив как есть в blockchainPort.createBoard.
2026-05-28 14:55:00 +00:00
ant 5041abe851 Merge pull request 'design-wave1: миграция desktop на canon — Form/ModalBase/Styles shims снос' (#42) from feat/design-wave1-cleanup-dialogs-forms into dev
Reviewed-on: #42
2026-05-28 14:10:13 +00:00
coopops 04e7503e7d [design-wave1][@dark] feat: миграция extensions на канон — снос ModalBase/TitleStyles/CardStyles/AutoAvatar shim'ов
Typecheck / desktop (pull_request) Failing after 8m58s
Typecheck / controller (pull_request) Successful in 13m16s
ModalBase → BaseDialog (18 call-sites):
* extensions/capital — 14 features + 1 widget (ImportContributorsButton).
* extensions/chairman — 2 approval features + 1 page (AgendaPresetsPage).
* extensions/powerup — 1 widget (ResourceInfoWidget, через barrel).
Паттерн: q-dialog внутри q-btn → BaseDialog параллельно q-btn в общей div-обёртке.
persistent=true → :close-on-backdrop='false' :close-on-escape='false'.
@hide → @update:model-value='(v) => !v && callback()'. maximized → :maximized='true'.
ProjectInvestFabAction: два дублирующих q-dialog объединены в один BaseDialog.

TitleStyles + CardStyles → src/css/legacy-stylers.scss (глобально через quasar.config.cjs).
Все 8 side-effect import'ов в extensions/capital удалены.
Долг: переписать .title-container/.info-card/.card-* классы на canon-токены.

AutoAvatar: 1 импорт в CapitalProfilePage переключён на shared/ui/domain/AutoAvatar.
Shim shared/ui/AutoAvatar снесён.

Удалены: shared/ui/{ModalBase,TitleStyles,CardStyles,AutoAvatar} + reexport ModalBase
из shared/ui/index.ts. Design-wave1 закрыт полностью.
2026-05-28 13:42:39 +00:00
coopops 8d2962b786 [design-wave1][@dark] fix: вернуть re-export ModalBase в shared/ui/index.ts
ResourceInfoWidget в extensions/powerup импортирует ModalBase через
barrel: import { ModalBase } from 'src/shared/ui'. После сноса
re-export'а vite валился SyntaxError: does not provide an export.
Возвращаю строку обратно — папка ModalBase уже восстановлена
предыдущим коммитом.
2026-05-28 13:10:30 +00:00
coopops 15465e805e [design-wave1][@dark] fix: вернуть ModalBase/TitleStyles/CardStyles/AutoAvatar для extensions
В прошлой волне снёс shared/ui/{ModalBase,TitleStyles,CardStyles,AutoAvatar},
но смотрел только src/. В extensions/{capital,chairman}/ осталось:
* ModalBase — 17 файлов
* TitleStyles (side-effect SCSS) — 6 страниц capital
* CardStyles (side-effect SCSS) — 1 виджет capital
* AutoAvatar — 1 страница capital
Из-за этого vite валился на 404 TitleStyles → "Failed to fetch dynamically
imported module src/boot/init.ts" и фронт не грузился.

Восстановлены оригинальные файлы. AutoAvatar в shared/ui/AutoAvatar
оставлен как re-export нового канон-расположения shared/ui/domain/AutoAvatar.
Полная миграция extensions на BaseDialog и канон-токены — отдельная волна.
2026-05-28 13:08:03 +00:00
coopops b69920e263 [design-wave1][@dark] fix: cooperativeAgreements — не падать на пустом coopname
При bootstrap'е фронта RequireAgreements/SignUp успевают смонтироваться
ДО завершения system.loadSystemInfo(), и info.coopname=undefined.
Zeus сериализует variables в {} → сервер кидает
"Variable $coopname of required type String! was not provided".

Решение по слою:
- api/index.ts: гард — если coopname пустой, не отправляем запрос.
- RequireAgreements/SignUp: watch(() => info.coopname) — load
  отрабатывает, как только coopname прорастёт, без блокировки UI.
2026-05-28 13:03:44 +00:00
coopops 298e1919e8 [design-wave1][@dark] feat: BaseDialog :maximized — закрываем последние 5 maximized + сносим ModalBase
В BaseDialog добавлен prop :maximized (boolean) — q-dialog получает maximized, q-card растягивается на весь экран, body становится flex-scrollable. При :maximized размер size игнорируется.

Это покрывает 5 last maximized call-sites:
* Agreementer.SignAgreementDialog — fullscreen для подписи договора (hideCloseButton)
* Agreementer.ReadAgreementDialog — fullscreen для просмотра договора
* Agreementer.StaticPrivacyDialog — fullscreen для статичной privacy policy
* Branch.SelectBranch.SelectBranchOverlay — fullscreen onboarding flow с шагами 1/2
* Decision.CreateProject.CreateProjectButton — fullscreen для предложения повестки

Все 5 переведены на BaseDialog :maximized='true' :close-on-backdrop='false' :close-on-escape='false'.

После миграции ModalBase больше не используется — удалена папка shared/ui/ModalBase и реэкспорт из shared/ui/index.ts. Design-wave1 закрыт полностью.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 12:49:56 +00:00
coopops 7735c40d06 [design-wave1][@dark] refactor: ModalBase → BaseDialog в 14 call-sites — основная часть миграции
Простые модальные диалоги переведены с q-dialog+ModalBase на canon BaseDialog (внутри уже q-dialog), API v-model:modelValue:

* features: DeleteBranch / CreateBranch / DepositToWallet (двух-стейтовый dialog) / WithdrawFromWallet / AddPaymentMethod / DeletePaymentMethod / Decision.CreateProjectFreeDecision (lg) / FreeDecision.CreateProject (md) / Payment.SetStatus.SetOrderPaid/Completed/Refunded
* widgets: ConnectionDashboard.AxonWallet / Desktop.WorkspaceMenu (size=lg вместо 700px-фикса)
* shared: CreateDialog (общий wrapper) / CouncilOnboarding.CouncilOnboardingCard (кнопки в slot footer)

Persistent → :close-on-backdrop='false' + :close-on-escape='false'. @hide='clear' → @update:model-value с проверкой v===false. style='width: NNNpx' переведён на canon size sm/md/lg.

Что НЕ мигрировалось (5 файлов остались с ModalBase):
SelectBranchOverlay, SignAgreementDialog, ReadAgreementDialog, StaticPrivacyDialog, Decision.CreateProject.CreateProjectButton — все используют :maximized='true'. BaseDialog в canon не имеет fullscreen-режима, расширять canon без согласования — нарушение правила "Канон-нейминг согласовывать ДО реализации". Эти 5 кейсов оставлены legacy.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 12:48:41 +00:00
coopops 41d0af6706 [design-wave1][@dark] refactor: Form → shim над BaseForm/BaseButton — 20 call-sites визуально canon
Form.vue переписан как тонкий wrapper над canon BaseForm + BaseButton с сохранением старого API (handlerSubmit/isSubmitting/showCancel/showSubmit/buttonSubmitTxt/buttonCancelTxt/disabled/size).

Что изменилось визуально:
* Кнопки cancel/submit вынесены в slot #footer (canon-layout с gap=14px между body и footer).
* Cancel — variant='ghost' (плоская), Submit — variant='primary' (canon-primary), оба BaseButton с no-caps/без ripple.
* Кнопки выровнены justify-content:flex-end (canon-pattern для форм), вместо старого .flex без выравнивания.

API call-sites не трогаем — 20 form-call-sites (Branch/Wallet/PaymentMethod/Agreementer/Decision/Union/FreeDecision/Payment/Request/AxonWallet) получают canon-визуал сразу.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 12:47:15 +00:00
coopops 8356b507f8 [design-wave1][@dark] chore: снос CardStyles — заинлайнил единственный live-класс info-card
* Глобальный CardStyles из 430 строк CSS реально использовался только через .info-card в RestartMeetForm.vue.
* Все остальные классы (card-container, section-header, card-label, card-value) определены локально в scoped CSS соответствующих компонентов — глобальный импорт лишний.
* Стили .info-card (+ dark-override) заинлайнены в scoped стиль RestartMeetForm под нативным именем .meet-form-agenda-item.
* Удалены: глобальный импорт в App.vue, @import в RestartMeetForm, папка shared/ui/CardStyles.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 12:47:15 +00:00
coopops c306789ed2 [design-wave1][@dark] chore: снос мёртвых InputStyles + TitleStyles — волна 5
* InputStyles — весь файл состоял из закомментированного кода.
* TitleStyles — .title-container не использовался ни в одном .vue.
* Удалены 3 импорта InputStyles в Editable*Card и реэкспорт TitleStyles из shared/ui/index.ts.

CardStyles оставлен: используется в 4 call-sites (RestartMeetForm, UnionMembershipStep, TariffCard, MeetInfoCard) — миграция отдельным шагом.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 12:47:15 +00:00
coopops 11b0d6f123 [design-wave1][@dark] refactor: ToogleDarkLight → ThemeToggle + AutoAvatar в domain — добиваем волну 5
* ThemeToggle получает asButton/showText/isMobile, поглощая роль ToogleDarkLight (исправлена опечатка в имени).
* AutoAvatar перенесён в shared/ui/domain/AutoAvatar — он DiceBear-процедурный, а не base-примитив (canon Avatar делает инициалы/image и не заменяет процедурную генерацию).
* Снесены legacy папки shared/ui/ToogleDarkLight и shared/ui/AutoAvatar.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 12:47:15 +00:00
ant 8bdc3dd0cd Merge pull request 'Канон тостов и шапка-мобильник: bottom-right + accent-полоска + icon-only CTA' (#38) from feat/notif-mobile-canon into dev
Reviewed-on: #38
2026-05-28 11:57:00 +00:00
ant 4f70d8eac8 Merge pull request 'feat(marketplace): drop blocked-механику в ledger2 — резерв под Order через отдельный кошелёк w.mkt.order' (#41) from feat/marketplace2-drop-blocked into marketplace2
Reviewed-on: #41
2026-05-28 11:56:03 +00:00
claude aa2e8f6ba3 docs+code(marketplace): полный rewrite YAML standards под новую модель + удаление deprecated полей
YAML стандарты переписаны под текущую модель (5/1/1 операций), удалены
блоки удалённых операций и упоминания старой схемы:

p.mkt.supply.standard.yaml — 5 операций:
  o.mkt.lock   — TRANSFER w.wal.share → w.mkt.order, Дт 80 / Кт 86
  o.mkt.unlock — TRANSFER w.mkt.order → w.wal.member, без проводки
  o.mkt.purch  — NONE, Дт 10 / Кт 86
  o.mkt.payout — ISSUE → w.mkt.payout, Дт 86 / Кт 51
  o.mkt.consum — BURN w.mkt.order, Дт 86 / Кт 10
  Удалены: o.wal.conv, o.mkt.assign, o.mkt.recall, o.mkt.consum2

p.mkt.return.standard.yaml — 1 операция:
  o.mkt.return — ISSUE → w.wal.member, Дт 10 / Кт 86
  Удалена: o.mkt.return2

p.mkt.wroff.standard.yaml — 1 операция:
  o.mkt.wroff — NONE, Дт 86 / Кт 10
  Удалена: o.mkt.wroff2

Полностью удалены deprecated поля из TS controller (не оставлены даже
как deprecated пометки):
- MarketplaceOrderCreateTxSnapshotDTO.did_convert / did_assign
- MarketplaceOrderCreateTxSnapshot.did_convert / did_assign
- Поле blocked_amount переименовано в locked_amount (везде: DTO, entity,
  service, spec, infrastructure entity)

Зачистка doc-комментариев от удалённых операций в C++ + TS:
- processes.hpp, lib/core/marketplace/marketplace.hpp,
  marketplace/marketplace.hpp, table_marketplace_writeoff_proposals.hpp,
  wallets.hpp (description w.mkt.order под новый flow)
- marketplace-canonical-blockchain.port.ts, marketplace-order-cancel.service.ts,
  marketplace-issuance.service.ts, marketplace-offer.entity.ts,
  marketplace-order.entity.ts, marketplace-return-claim.types.ts
- marketplace-ledger2-invariants.ts: I2 формула, I3 формула, I4 пояснение
- marketplace-process-trace-coverage.spec.ts: EXPECTED_MARKETPLACE_OP_CODES
  и EXPECTED_REGISTRY переписаны под 7 операций (5 supply + 1 return + 1 wroff)
  с актуальными wallet_op / debit / credit

Bulk переименование o.mkt.unblk → o.mkt.unlock, o.mkt.block → o.mkt.lock
в комментариях C++ контракта и TS controller.

Проверки:
- C++ contract пересобран (dicoop/blockchain_v5.1.1:dev arm64): 0 errors,
  marketplace.wasm + .abi созданы
- jest invariants + resolver: 31/33 passed (2 fail — те же pre-existing
  baseline Story 11.1 cooptypes/operations.ts)
- cooptypes regen + schema/zeus/sdk cascade прошли успешно

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 16:54:24 +05:00
claude 521618a5b2 refactor(marketplace): lock сразу w.wal.share→w.mkt.order + чистка комментариев (Review #41)
Замечания ревью @ant Review 31:

(1) o.mkt.lock теперь идёт **прямо** w.wal.share → w.mkt.order
    с проводкой Дт 80 / Кт 86 — без промежуточного w.wal.member и без
    conditional o.wal.conv. Один шаг ledger2 в createorder.
    - createorder: убран каскад conv+lock; lock сам делает работу
    - signiss2 actual>ordered: убран conv, остался только lock(diff)
    - Достаточность средств: только w.wal.share.available

(2) Убраны из комментариев и YAML фразы «без транзита 91» / «через транзит 91»
    как нерелевантный шум (никого не интересует прошлое):
    - cancelorder.cpp, accretrn.cpp, execwroff.cpp
    - processes.hpp: пересчитан состав операций под новую схему
    - controller TS: marketplace-return-claim.{types,service}.ts,
      marketplace-canonical-blockchain.port.ts, marketplace-order.types.ts,
      marketplace-application.module.ts

(3) Убраны упоминания «может быть выведено пайщиком на паевой» —
    с w.wal.member на w.wal.share вывод НЕ предусмотрен в этой схеме.

Инварианты I5 переписаны под новый источник lock (w.wal.share вместо
w.wal.member). spec.ts фикстуры обновлены: happyPathOrderFlow теперь
4 операции (lock + purch + payout + consum) без conv. 31/33 теста зелёные,
2 fail — pre-existing baseline (Story 11.1 cooptypes).

C++ контракт пересобран через ARM-native CDT (dicoop/blockchain_v5.1.1:dev) —
0 errors, marketplace.wasm + .abi созданы успешно.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 15:23:33 +05:00
claude b266181a05 docs(CLAUDE.md): для сборки контрактов на ARM использовать dicoop/blockchain_v5.1.1:dev
build-all.sh / build.sh хардкодят dicoop/blockchain:latest (amd64-only) —
на ARM падают «exec format error». Multi-arch образ dicoop/blockchain_v5.1.1:dev
имеет нативный arm64 manifest; команда для marketplace в одну строку
зафиксирована в новой секции «Сборка контрактов на ARM-машине».

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 15:02:34 +05:00
coopops 0229b639b2 [design-wave2][@ant] fix: навигатор — canon clean-up + сохраняем primary
Typecheck / desktop (pull_request) Failing after 8m23s
Typecheck / controller (pull_request) Failing after 1m48s
- default.vue: убрал bordered у левого q-drawer (двойная линия с .rail
  border-right из канона) и hardcoded #00800038 (legacy зелёное cooperative)
  → var(--p-line); удалил мёртвый .drawer-close-btn.
- components.css: комментарий .rail__usercard поправлен — там primary,
  а не accent (намеренно: оранжевый в шапочке навигатора отвлекал бы).
- _dev/ui: текст секции AppDrawer привёл к фактическому поведению
  (soft-primary заливка + 2-px deep-teal rail).

Цветовая схема навигатора:
- активный пункт — primary (deep teal #0f766e), 2px rail слева;
- brand-иконка, avatar, balance-блок — primary-soft (не accent);
- никакого accent (оранжевого) в навигаторе.
2026-05-28 09:44:55 +00:00
coopops 64fa0d2f97 [design-wave2][@ant] docs: строгое указание использовать дизайн-канон в desktop
Typecheck / desktop (pull_request) Failing after 8m16s
Typecheck / controller (pull_request) Successful in 13m28s
В CLAUDE.md новый раздел «ДИЗАЙН-КАНОН desktop — ОБЯЗАТЕЛЕН»:
1) HTML-канон ~/blago/production/shared/MONO Design System.html;
2) Живая реализация components/desktop/src/pages/_dev/ui/index.vue.

После компакта забыл, что shared/MONO Design System.html — основной SoT,
и нашёл «канон» в auth-prototype, который НЕ канон — пришлось переверстать
тосты. Фиксирую правило, чтобы не повторять.
2026-05-28 09:33:57 +00:00
coopops 82478155ce [design-wave2][@ant] fix: capital header-кнопки в canon micro-режим
Typecheck / desktop (pull_request) Failing after 8m19s
Typecheck / controller (pull_request) Successful in 13m6s
Завершаю аудит — добавил capital по запросу:
- RouteMenuButton получил опциональный icon-проп; на мобильном
  превращается в round-иконку + tooltip, на десктопе — flat + label.
- В ProjectPage и ComponentPage прописал material-иконки для каждого
  раздела (Описание/Артефакты/Компоненты/План/Участники/История/
  Задачи/Голосование/Результаты).
- ImportContributorButton, ImportContributorsButton, FilterDialogWithButton
  переведены в canon-micro (isMobile → flat+dense+sm+accent + tooltip).
  Заодно вынес q-dialog из q-btn-вложенности.

RouteMenuButton по сути — раздельные таб-кнопки навигации; полноценное
UX-решение для capital (вынести в SecondLevelTabs под шапкой) — отдельная
задача, тут только мобильная читаемость шапки.
2026-05-28 09:03:45 +00:00
coopops ecd895e745 [design-wave2][@ant] fix: остальные header-кнопки в canon micro-режим
Typecheck / desktop (pull_request) Failing after 8m21s
Typecheck / controller (pull_request) Successful in 13m9s
Аудит точек установки в шапку — два механизма:
1. registerAction(useHeaderActions) — Members/Branches/Documents/Agenda
   /Participants/CoopWallets/powerup.Settings;
2. Teleport to=#header-actions-host — PaymentMethods/Documents/Wallet/Meets.

Уже в каноне: SearchHeaderAction, CreateMeetButton, DepositButton,
WithdrawButton, AddPaymentButton, AddMemberButton, CreateBranchButton.

Доделал остальные:
- CreateProjectButton (Предложить повестку);
- AddUserButton (Добавить пайщика);
- ImportParticipantsButton (Импорт);
- TransferWalletsButton (Перевести между кошельками);
- powerup SettingsPage save-кнопка (через computed + watch на isMobile).

Капитал-расширение по правилу проекта не трогаю.
2026-05-28 08:44:25 +00:00
ant e2f467b78f Merge pull request 'fix(desktop): четыре vue-tsc ошибки — типизация props и string-fallback'и' (#39) from fix/vue-tsc-desktop into dev
Reviewed-on: #39
2026-05-28 08:43:33 +00:00
claude a550e06802 refactor(marketplace): drop blocked + simplify per review (rename block→lock, no w.mkt.member, no 91-transit)
Переделка PR #41 по двум замечаниям ревью @ant 2026-05-28:

(1) Согласованность имён operation_id с кошельком w.mkt.order:
  o.mkt.block / BLOCK_FOR_ORDER   → o.mkt.lock   / LOCK_ORDER
  o.mkt.unblk / UNBLOCK_ON_CANCEL → o.mkt.unlock / UNLOCK_ORDER
  CONSUME_BY_MEMBER оставлено (consume — это про BURN, не про lock).

(2) Упрощение архитектуры — убрать каскады и счёт 91:
  - Удалён кошелёк w.mkt.member (MARKETPLACE_MEMBER) — промежуточного
    программного членского нет; источник и приёмник средств — w.wal.member.
  - Удалены операции o.mkt.assign / o.mkt.recall (каскад createorder
    схлопнут с 3 шагов до 2: conv condi + lock).
  - Удалены парные *2-операции o.mkt.consum2 / o.mkt.return2 / o.mkt.wroff2 —
    счёт 91 (транзит) в marketplace больше не используется.
  - Все 3 «выбытие/возврат имущества» операции — одним вызовом через 86:
    o.mkt.consum: BURN w.mkt.order, Дт 86 / Кт 10 (было: Дт 91 / Кт 10 + 86 / 91)
    o.mkt.return: ISSUE → w.wal.member, Дт 10 / Кт 86 (было: Дт 91 / Кт 86 + 10 / 91)
    o.mkt.wroff:  NONE, Дт 86 / Кт 10 (было: Дт 91 / Кт 10 + 86 / 91)
  - Возврат при cancel/decline/expire — на w.wal.member (универсальный
    членский), а не на бывший w.mkt.member.

Итого marketplace ledger2-операций: было 13 → стало 8.
LEDGER2_WALLET_REGISTRY: 16 → 15. USER_SHARED program-mapping: 8 → 7.

Затронуто:
- C++ ledger2: wallets.hpp, operations.hpp
- C++ marketplace .cpp: createorder, cancelorder, declineorder, expireorder,
  signiss2 (включая special cases недо/переплаты), accretrn, execwroff
- TS controller: marketplace-member-wallet.{dto,resolver,test}.ts — 3 кошелька
  в выдаче вместо 4 (w.mkt.member выпал)
- TS invariants: I5/I6 переписаны под o.mkt.lock/unlock/consum, I4 для
  marketplace тривиально пройден (91 не задевается); 27/29 тестов passed,
  2 fail остаются pre-existing baseline (cooptypes/operations.ts Story 11.1)
- Cooptypes: wallets.generated.ts regen (wallets=15, mapping=7)
- Schema/zeus/sdk: regen, только description-only diff
- YAML p.mkt.supply.standard.yaml: обновлён header под reserve-flow + 6 ops

NOT в scope этого фикса (TODO follow-up):
- Полный rewrite секций операций в p.mkt.{supply,return,wroff}.standard.yaml
  (детальные блоки операций remain с упоминаниями o.mkt.block/unblk/w.mkt.member —
  бизнес-документация для бухгалтера, не влияет на компиляцию контракта)
- Memo-функции в marketplace.hpp для удалённых операций (assign_memo,
  consum_transit_close_memo и т.д.) — мёртвый код, в C++ unused functions OK

Проверки (tsc запрещён локально — см. memory mono-no-local-tsc):
- jest marketplace-member-wallet-resolver: 4/4 passed
- jest marketplace-ledger2-invariants: 27/29 passed (2 pre-existing baseline)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 13:43:18 +05:00
coopops c29c3720d2 [design-wave2][@ant] fix: AddMember/CreateBranch — canon micro в шапке на мобиле
Typecheck / desktop (pull_request) Failing after 8m14s
Typecheck / controller (pull_request) Successful in 12m59s
CSS-хак `.topbar__actions .base-btn__label { display: none }` не покрывал
эти две кнопки: они рисуют q-btn напрямую через registerAction (не через
BaseButton под .topbar__actions). В итоге на мобильном «Добавить члена»
и «добавить участок» оставались с лейблами и распирали шапку.

Перевёл обе в canon-паттерн как CreateMeetButton/DepositButton/WithdrawButton:
isMobile → flat+dense+sm+accent, иконка + q-tooltip; без обёртки
.header-action, чтобы кнопка не растягивалась на всю высоту шапки.
2026-05-28 08:33:56 +00:00
coopops 45ef337201 [design-wave2][@ant] fix: канон тостов под shared/MONO Design System.html + dev/ui демо
Typecheck / desktop (pull_request) Failing after 8m14s
Typecheck / controller (pull_request) Successful in 13m2s
- quasar-canon.css: переписан override .q-notification под реальный
  канон (shared/MONO Design System.html → .toast): тёмный фон
  (var(--p-ink) для нейтрального; #052e16/#4c0a0a/#3d2400/#0c1e3f
  для positive/negative/warning/info), светлый текст, ЦВЕТНАЯ иконка
  по типу (--p-pos/--p-neg/--p-warn/--p-info). Раньше ошибочно был
  сделан нейтральный фон + left-accent-полоска — «откуда попало»,
  не из канона.
- alerts.ts: убран color: 'grey-7'/'positive' у actions — они
  наследуют светлый тон от тёмного фона через override.
- _dev/ui секция 39 «Тосты»: пять кнопок для триггера всех типов
  тостов (успех, успех с CTA, ошибка, in-app push, push с avatar).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 07:48:40 +00:00
coopops e0e846c24b [design-wave2][@ant] fix: канон тостов и шапка-мобильник — bottom-right + accent-полоска, icon-only CTA
Typecheck / desktop (pull_request) Failing after 8m14s
Typecheck / controller (pull_request) Successful in 13m1s
- SuccessAlert/FailAlert/NotifyAlert (shared/api/alerts.ts): единый
  визуал через override .q-notification в quasar-canon.css. Нейтральная
  поверхность + 3px accent-полоска по типу (positive/negative/warning
  /info), без «цветной заливки карточки» (stop-signal §19.1).
- Позиция тостов bottom-right — было top-right, что противоречило
  канону mono-design-system v2 (toast-host: fixed; bottom: 24px;
  right: 24px). На мобильнике — full-width минус 16px полей.
- SuccessAlert: type:'positive' вместо color:'primary' — раньше успех
  красился брендовым teal, а не позитивным зелёным.
- NotifyAlert: дефолтная иконка notifications, если нет avatar — чтобы
  in-app push от Novu визуально совпадал с Success/Fail.
- Topbar @media (max-width:600px): .topbar__actions .base-btn__label
  скрыт, остаются только иконки CTA-кнопок страницы. Бренд-название
  с ellipsis (max-width: 50vw). Раньше кнопки переносились на вторую
  строку на узких экранах.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 06:59:37 +00:00
coopops 81db8355e0 [fix-vue-tsc-desktop][@ant] fix: четыре vue-tsc ошибки в desktop — типизация props и string-fallback'и
Typecheck / desktop (pull_request) Failing after 8m22s
Typecheck / controller (pull_request) Successful in 13m12s
CI vue-tsc --noEmit падал на четырёх файлах:

- QuestionsTable.vue: props.decisions типизирован через
  PropType<Cooperative.Document.IComplexAgenda[]>. Раньше тип был
  плоский Array → row в v-for становился unknown, и template
  ругался на row.table.id, isProcessing(row.table.id) и т.п.
- ListOfPaymentsWidget.vue: явный computed items: IPayment[] через
  cast (as unknown as IPayment[]) — zeus резолвил items слабее, чем
  нужно шаблону, и поля row.id / username / quantity / etc. оставались
  unknown.
- ExtensionPage.vue и ExtensionCard.vue: для AutoAvatar :username
  добавлен fallback || '' — extension.name / extension.title могут
  быть string | null | undefined, AutoAvatar требует string.

ESLint по четырём файлам — EXIT=0; tsc-прогон делегирован CI
(точечный vue-tsc по факту тащит весь проект и вешает машину, см.
memory feedback_no_full_vue_tsc_desktop).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 06:50:12 +00:00
claude fd458a4e35 docs(marketplace): update p.mkt.supply.standard.yaml — reserve wallet model
Обновлены описания операций o.mkt.block / o.mkt.unblk / o.mkt.consum
в стандарте процесса под reserve-wallet модель (введён 2026-05-28):

- Заголовок раздела ledger2 (комментарий перед секциями): добавлен новый
  USER_SHARED-кошелёк w.mkt.order, обновлено описание w.mkt.member.
- o.mkt.block:  BLOCK → TRANSFER w.mkt.member → w.mkt.order; L3 теперь
  пара движений на двух кошельках вместо split available/blocked на одном.
- o.mkt.unblk:  UNBLOCK → TRANSFER w.mkt.order → w.mkt.member; зеркало 12c.
- o.mkt.consum: REVOKE → BURN с w.mkt.order; резерв сжигается без получателя
  при подписании АПП выдачи (signiss2).
- Список ledger2-кошельков в Секции 6 — упомянут w.mkt.order.

Бизнес-словарь: «блокировка» → «резерв», «разблокировка» → «снятие резерва»,
«заблокированная сумма списывается» → «резерв сжигается». Сами op_code
(o.mkt.block / o.mkt.unblk / o.mkt.consum) и amount_ref / triggered_by /
human_name (последний обновлён для consistency) — сохранены, чтобы
бухгалтер мог пройти по тем же шагам процесса в журнале операций.

p.mkt.return.standard.yaml и p.mkt.wroff.standard.yaml не требуют правок —
эти flow используют ISSUE/NONE, без BLOCK/UNBLOCK/REVOKE.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 11:35:41 +05:00
claude 1f02f8d7cf refactor(marketplace): rewrite I5/I6 invariants for w.mkt.order reserve model
Инвариант I5 переименован: `BlockedConsistency` → `ReserveConsistency`.
Считает резерв через пары TRANSFER + BURN в новой модели:

  Σ TRANSFER(w.mkt.member → w.mkt.order)    // o.mkt.block  — резерв входит
  − Σ TRANSFER(w.mkt.order → w.mkt.member)  // o.mkt.unblk  — резерв снят
  − Σ BURN(w.mkt.order)                     // o.mkt.consum — резерв сожжён
    = Σ balance(w.mkt.order у пайщиков)

Раньше I5 сверялся с suma blocked у w.mkt.member-кошельков (поле `.blocked`).
Поле `.blocked` для marketplace-кошельков теперь deprecated (всегда 0) —
резерв выражается через `.available` отдельного кошелька w.mkt.order.

Инвариант I6 переименован: `NoOrphanedBlocks` → `NoOrphanedReserves`.
Логика парности по process_hash (consum без block, unblk без block,
block+unblk+consum = двойное закрытие) не меняется — op_code'ы
o.mkt.block/unblk/consum сохранены.

Фикстуры в spec.ts переписаны под новую модель: walletFrom/walletTo
для block теперь w.mkt.member → w.mkt.order, для unblk наоборот,
для consum — walletFrom=w.mkt.order (BURN).

ВНИМАНИЕ — pre-existing baseline:
`marketplace-process-trace-coverage.spec.ts` и 4 теста в `*-invariants.spec.ts`,
которые опираются на `MARKETPLACE_OPERATION_CODES` (фильтр через
`Ledger2.LEDGER2_OPERATION_REGISTRY.contract === 'marketplace'`), падают
с pre-existing проблемой: `cooptypes/ledger2/operations.ts` содержит только
2 legacy marketplace-операции (`o.mkt.supply`/`o.mkt.recv`) вместо 13 новых.
Это известный блокер Эпика 11 Story 11.1 (синхронизация TS-стороны cooptypes
с C++ operations.hpp) — выходит за scope этого PR.

Проверено: 32/36 тестов в marketplace-ledger2-invariants.spec.ts passed
(те же 4 теста были сломаны на pre-state baseline).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 11:30:09 +05:00
claude fb94c7ff11 feat(marketplace): expose w.mkt.order as 4th wallet in marketplaceMemberWallet DTO + regen
DTO `MarketplaceMemberWallet.wallets` теперь возвращает массив из 4 USER_SHARED-кошельков
пайщика для Стола заказов:

  1. w.wal.share  — Паевой   | Цифровой Кошелёк
  2. w.wal.member — Членский | Цифровой Кошелёк
  3. w.mkt.member — Членский | Стол Заказов (свободные средства программы)
  4. w.mkt.order  — Резерв   | Стол Заказов (под конкретный Order; новый, 2026-05-28)

Поле `blocked` остаётся в DTO для wallet/withdraw flow и legacy данных, но для
marketplace-кошельков (w.mkt.member, w.mkt.order) всегда `0` после миграции
с BLOCK/UNBLOCK на пары TRANSFER — резерв выражается через `.available`
отдельного кошелька w.mkt.order. Обновлены description'ы @Field и @Query на
бизнес-словаре (без story-ссылок).

Regen:
- cooptypes/ledger2/wallets.generated.ts: wallets=16, mapping=8 (из C++).
- controller/schema.gql, controller/zeus/index.ts, sdk/zeus/index.ts:
  только description-only diff в MarketplaceMemberWallet/Entry, никаких
  breaking changes в типах GraphQL/zeus.

Unit-тесты marketplace-member-wallet-resolver.test.ts обновлены под 4-й
кошелёк (4/4 passed, runInBand).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 11:22:52 +05:00
claude 07584c8e78 docs(marketplace-cpp): update p.mkt.supply action comments for reserve-wallet flow
Семантика in-code не меняется: Ledger2::apply(coopname, op, amount, ...)
вызывается с теми же operation_id (o.mkt.block / o.mkt.unblk / o.mkt.consum),
изменения только в doc-блоках и inline-комментариях:

- createorder.cpp: "BLOCK на w.mkt.member" → "TRANSFER w.mkt.member → w.mkt.order"
- cancelorder.cpp: "сумма остаётся на .available" → уточнено, что снятие резерва
- declineorder.cpp: "резерв возвращается заказчику" → уточнено через TRANSFER
- expireorder.cpp: тот же flow
- signiss2.cpp: actual<ordered → снятие части резерва (UNBLOCK_ON_CANCEL),
                actual>ordered → доп. резерв (BLOCK_FOR_ORDER),
                CONSUME_BY_MEMBER теперь BURN с w.mkt.order

Поведенческих изменений в C++ нет — вся семантика выражена через изменения
в OPERATION_REGISTRY (предыдущий коммит). Этот коммит — только документация
точки вызовов, чтоб не путать читателя.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 11:09:12 +05:00
claude c4a42f7243 feat(ledger2): add w.mkt.order reserve wallet, replace marketplace BLOCK/UNBLOCK with TRANSFER
Вводит USER_SHARED-кошелёк w.mkt.order (MARKETPLACE_ORDER_LOCK) — резерв
средств пайщика под конкретный Order. Заменяет механику BLOCK/UNBLOCK
на отдельных balance-полях кошелька w.mkt.member на пару кошельков:

- o.mkt.block   (BLOCK_FOR_ORDER):   TRANSFER w.mkt.member → w.mkt.order
- o.mkt.unblk   (UNBLOCK_ON_CANCEL): TRANSFER w.mkt.order → w.mkt.member
- o.mkt.consum  (CONSUME_BY_MEMBER): BURN с w.mkt.order  (вместо BURN_BLOCKED w.mkt.member)

Operation_id'ы (o.mkt.block / o.mkt.unblk / o.mkt.consum) сохранены —
изменилась только реализация в OPERATION_REGISTRY, не имена; внешние
op_code в blockchain_actions читаются без сдвига смысла.

Параметр Ledger2::apply(coopname, op, amount, ...) не меняется — сигнатура
вызовов в .cpp идентична, отдельная правка реализаций следующим коммитом
ограничивается обновлением doc-комментариев.

LEDGER2_WALLET_REGISTRY: 15 → 16 (USER_SHARED 6 → 7).
LEDGER2_USER_SHARED_PROGRAM_MAPPING: 7 → 8 (w.mkt.order → program_id=2).

Это marketplace-сторона миграции, повторяющая подход dev для wallet/withdraw
(WITHDRAW_PENDING кошелёк-резерв; ADR — общий принцип «убрать blocked,
заменить на пары TRANSFER в выделенные кошельки»). Enum WalletOp в этом PR
НЕ модифицируется — значения BLOCK/UNBLOCK/BURN_BLOCKED остаются для legacy
wallet/withdraw flow, который мигрирует следующим merge dev → marketplace2.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 11:08:59 +05:00
ant 7fff48af92 Merge pull request 'docs(marketplace): Стол заказов MVP — 30 страниц прозы по столам + harness/seed scaffold' (#17) from worktree-marketplace-docs-impl into marketplace2
Reviewed-on: #17
2026-05-28 05:05:31 +00:00
claude de1791955d fix(marketplace): правки по ревью PR #17 (комментарии @ant)
- 416 rename Chairmen→Chairman; 419/421/422/423 убраны as unknown (строгие
  Zeus-типы, скаляры через Omit+override); 420 статусы через Zeus enum;
  424/425 SuccessAlert/FailAlert в OrderCreateDialog; 417/418 дедуп
  normalizeTxHash/rethrowChainError в shared/chain-tx.util; 415 единый filter
  через InputType DTO (+ regen schema/zeus/SDK); 426/427 @OnEvent из cooptypes
  WalletContract (добавлены wallet-действия authwthd/declinewthd).

Controller tsc зелёный; desktop vue-tsc — 2 pre-existing ошибки (q-tab :name=null).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 01:41:31 +05:00
claude 370a7e2900 Merge branch 'marketplace2' into feat/marketplace-docs (разрешение конфликтов)
Конфликты в 4 marketplace-резолверах (apl-reception, inventory, issuance,
return-claim) разрешены в пользу этой ветки: ownership через
MarketplaceKuChairmanService (trustee+trusted), а не branchOwnership из
marketplace2 (PR #16). Осиротевший marketplace-branch-ownership.service.ts
и его проводка удалены — заменены kuChairmanService.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 01:41:31 +05:00
Alex Ant c3c49cc297 Merge branch 'main' into dev
Build bootstrap container / build (push) Successful in 2m26s
2026-05-27 23:52:26 +05:00
Alex Ant f4a5a8ce2d chore(release): publish
Build bootstrap container / build (push) Successful in 4m7s
Release / release (push) Successful in 42m38s
Release / publish-packages (push) Successful in 14m54s
Release / trigger-coopenomics-docs (push) Successful in 1s
Release / trigger-mono-docs (push) Successful in 2s
2026-05-27 23:52:22 +05:00
Alex Ant 2bbeecfa4f Merge branch 'testnet' 2026-05-27 23:36:46 +05:00
Alex Ant a32ec9532b Merge branch 'dev' of ssh://git.coopenomics.world:222/C9S/mono into dev 2026-05-27 23:36:09 +05:00
ant 67b808db04 Merge pull request 'fix(security): не отдавать matrixRoomId на фронт — комнаты секретаря и транскрипции по opaque id' (#37) from feat/chatcoop-hide-matrix-room-id into dev
Reviewed-on: #37
Reviewed-by: Алексей Муравьев <chairman.voskhod@gmail.com>
2026-05-27 09:22:52 +00:00
coopops 0bc05de392 chore(release): publish
Build bootstrap container / build (push) Successful in 3m4s
Release / release (push) Successful in 26m12s
Release / publish-packages (push) Has been skipped
Release / trigger-coopenomics-docs (push) Has been skipped
Release / trigger-mono-docs (push) Has been skipped
2026-05-27 09:22:05 +00:00
coopops 9f1058c997 Merge branch 'dev' into testnet
Build bootstrap container / build (push) Successful in 3m22s
2026-05-27 09:19:48 +00:00
ant 3c2a03df6f [nodemon-restart-storm][@ant] fix: debounce nodemon + SIGTERM в controller/nodemon.json — устранить restart-storm, вешавший coopback на bind-mount
Без delay nodemon реагировал на каждое fs-событие пачки (правки/сборка/git на хосте) десятками рестартов в секунду; pstree.remy без `ps` в образе виснет на обходе /proc (накапливались зомби-сканеры), холодный старт ts-node прерывался, порт 2998 не занимался — бэкенд молча лежал. delay:2000 схлопывает пачку в один рестарт, signal:SIGTERM даёт чистое завершение дочернего процесса.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:19:30 +00:00
ant 8209482556 [fix-ext-config-lost-update][@ant] fix: убрать lost-update конфигов расширений в периодических задачах — cron-планировщики перезаписывали config устаревшим in-memory снимком с boot и стирали онбординг-флаги/хэши председателя
extensionRepository.update заменяет config JSONB целиком; chairman/powerup/meet-tracker писали захваченный при initialize снимок поверх свежих данных. Теперь read-modify-write по актуальному config из БД с наложением только владомых полей.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:19:30 +00:00
coopops cd323add27 [design-wave1][@dark] feat: тактильная отдача (вибрация) при перезагрузке страницы
Boot-файл haptics: при reload страницы на устройстве с Vibration API (мобильный
браузер / PWA на Android) коротко вибрирует (15 мс). Reload определяется через
Navigation Timing (с фолбэком на legacy API), чтобы не срабатывать на первой
загрузке/навигации. Полностью безопасно для десктопа и обычных сайтов: где
Vibration API нет — вызов не делается, любая ошибка проглатывается, на загрузку
приложения не влияет. Только клиент (гард для SSR).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:19:30 +00:00
coopops 3d4e7fae0f [design-wave1][@dark] fix: убрать паразитный горизонтальный скролл левого дровера
В левом дровере был маленький горизонтальный скроллбар, и трекпадом его можно
было «оттянуть», обнажая правую границу — дровер выглядел скроллируемым, хотя не
должен. Причина: у .rail собственный border-right (1px), который при content-box
даёт ~1px overflow внутри контента дровера. Фикс в .app-left-drawer: overflow-x
hidden + overscroll-behavior-x contain на .q-drawer__content, .rail приведён к
box-sizing border-box и width 100%.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:19:30 +00:00
coopops a829e9452b [design-wave1][@dark] fix: повестка — держать лоадер на «Утвердить» 3с после голоса
Если проголосовать и сразу нажать «Утвердить», утверждение падает с ошибкой —
бэкенд ещё не учёл голос из блокчейна. После успешного голоса держим состояние
загрузки пункта (а значит и кнопку «Утвердить» в :loading) ещё VOTE_SETTLE_MS
(3с), за это время голос успевает обработаться. На ошибке загрузка снимается
сразу. Заодно ручное мутирование processingDecisions + setTimeout-хак заменены
аккуратным реактивным хелпером setProcessing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:19:30 +00:00
coopops 64809ab37c [design-wave1][@dark] fix: повестка — скрывать пункт только при утверждении, не при голосовании
Повестка совета показывает все НЕутверждённые вопросы. Голос «за»/«против» не
должен убирать пункт — он остаётся неутверждённым, лишь помечается отметкой
голоса. Убрал ошибочное actedDecisionIds.add из onVoteFor/onVoteAgainst
(оставлен только тихий рефетч). Скрытие через actedDecisionIds оставлено только
в onAuthorizeDecision — там пункт исполняется и должен уйти из повестки без
возврата от отстающего поллинга.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:19:30 +00:00
coopops 8586aab85a [design-wave1][@dark] fix: повестка совета — без моргания и без «исчез/вернулся» при голосовании
Две проблемы при голосовании/утверждении в повестке совета:

1. Моргание всей страницы: QuestionsTable подменял список тремя
   скелетонами при любом loading=true, а пост-экшен loadDecisions вызывался
   без hidden → loading=true → список «моргал» в скелетоны и обратно.
   Скелетоны теперь показываются только на первой загрузке (loading &&
   !decisions.length), а пост-экшен рефетчи переведены в тихий режим (hidden).

2. «Исчез → вернулся → исчез»: повестка на бэкенде уже не отдаёт пункт, по
   которому проголосовал/утвердил, но данные из блокчейна доходят с задержкой,
   и поллинг успевал вернуть отработанный пункт. Добавлен локальный набор
   actedDecisionIds — проголосованный/утверждённый пункт прячется сразу и не
   возвращается, что бы ни подтянул отстающий рефетч. Просто, без оптимистичных
   merge-оверлеев.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:19:30 +00:00
coopops d7ddfeb0f2 [design-wave1][@dark] fix: push-подписка не должна вешать UI и шуметь красной ошибкой
Две независимые проблемы при выдаче разрешения на уведомления:

1. Фриз приложения: shouldShowDialog был computed с побочным эффектом —
   внутри геттера вызывался updateSupport(), присваивавший store.support
   новый объект на каждое чтение. После выдачи разрешения это давало каскад
   инвалидаций/ре-рендеров, забивавший главный поток (роутер менял URL, DOM
   «застывал»). Геттер сделан чистым, updateSupport() вынесен в showDialog().

2. Красная ошибка «Service Worker не готов в течение 5 секунд»: в dev без PWA
   SW намеренно не регистрируется, поэтому navigator.serviceWorker.ready
   никогда не резолвится. Теперь сначала проверяем getRegistrations() — при
   отсутствии SW выходим сразу и тихо (warn вместо error), а subscribe()
   возвращает false без FailAlert. Push при недоступности SW полностью
   изолирован и не влияет на работу приложения.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:19:29 +00:00
coopops e41fbf4570 [design-wave1][@dark] feat: скрывать «Совершить взнос»/«Получить возврат» до подписи соглашения кошелька
Кнопки взноса и возврата на столе пайщика недоступны, пока пайщик не
подписал главное соглашение цифрового кошелька (type='wallet') — как и
сама карточка кошелька, которая не появляется без подписанного соглашения.

Геттер isWalletAgreementSigned в сторе Wallet читает тот же список
соглашений пайщика, что и RequireAgreements.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:19:29 +00:00
coopops 0f648d0bd6 [design-wave1][@dark] copy: явная опциональность шага «Импорт пайщиков» (онбординг председателя)
«(опционально)» в заголовок + пояснение в описании: если есть пайщики —
импортируйте CSV, если нет — двигайтесь дальше. Чтобы шаг не воспринимался
обязательным.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:19:29 +00:00
coopops 7af7a64091 [design-wave1][@dark] fix: ключ в Invite — выделенная панель вместо readonly-инпута
readonly outlined q-input рендерится с dashed-рамкой (дефолт Quasar) и
обрезал значение (…UJR4) — выпадало из канона и выглядело плоско.
Сделал ключ surface-2 панелью: eyebrow-лейбл + copy-иконка в шапке,
mono-значение целиком (word-break) ниже. Даёт вес и убирает «ёлочку».

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:19:29 +00:00
coopops 81b066e697 [design-wave1][@dark] fix: канон-композиция карточки «Сохраните ключ» (Invite)
Прошлый вариант разъезжался: «Скопировать» висел сиротой справа, провалы
между блоками, узкая потерянная кнопка, карточка 560px — разрежено.
Переделал по канону:
- копирование ключа — иконкой content_copy в append самого поля (+tooltip)
- кнопка «Установить ключ» — full-width (block), как «Войти» в LoginForm
- ширина карточки 480 как у остальных auth-карточек
- чекбокс слева, плотный ритм без лишних margin (reserve-hint-space)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:19:29 +00:00
coopops eac08d3438 [design-wave1][@dark] copy: полные примеры ОПФ+ в placeholder (с «Социального Комплекса»)
Голый «Потребительский Кооператив» в примере провоцировал вводить просто
ОПФ без расширения. Дал полный пример «...Социального Комплекса» во всех
трёх падежах — чтобы вводили расширенную ОПФ+, а не базовую форму.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:19:29 +00:00
coopops 933b0698be [design-wave1][@dark] copy: шаг «Настройка фабрики документов» вместо «Переменные документов»
Это не переменные, а постоянные параметры, которыми настраивается фабрика
документов кооператива. Переименовал label и intro шага.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:19:29 +00:00
coopops 6811bfa6fa [design-wave1][@dark] revert: убрать глобальный margin-bottom у .q-field--with-bottom
Микроотступ стал лишним и снова раздвигал поля слишком широко — откатываю.
Разделение полей обеспечивает сам reserve-hint-space, дополнительный
глобальный отступ не нужен.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:19:29 +00:00
coopops c2bbe68e2c [design-wave1][@dark] fix: примеры-подсказки в placeholder вместо hint (SetVariablesForm)
Постоянный hint под полем занимал место и отвлекал. Перенёс примеры
наименований ОПФ+ в placeholder (видны только в пустом поле), убрал hint.
Добавил reserve-hint-space всем q-input формы — строка под ошибку
резервируется без текста, валидация не вызывает layout shift.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:19:29 +00:00
coopops 17bfd2b5cd [design-wave1][@dark] fix: системный микроотступ под полями с hint/error
Без зазора текст hint/error верхнего поля почти касался границы нижнего.
Вернул маленький отступ, но системно и один раз — глобально в quasar-canon:
margin-bottom: var(--p-1) только полям .q-field--with-bottom (те, что
резервируют нижнюю строку). Работает везде (BaseForm, формы установки,
регистрация), в стеках не складывается в дыру, инлайновые поля не трогает.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:19:29 +00:00
claude ebf4929a66 booter fix 2026-05-27 09:19:29 +00:00
coopops 1961345a54 [design-wave1][@dark] fix: убрать двойной зазор между полями форм установки
reserve-hint-space у q-input/BaseInput уже даёт ~24px снизу под error/hint;
дополнительный gap в стеках полей складывался с ним → избыточное расстояние.
Убрал gap (канон BaseForm__body):
- CreateOrganizationDataForm / IndividualDataForm (.user-data-stack)
- SetVariablesForm (.vars-section__fields)
- RequestKeyForm (.request-key)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:52 +00:00
coopops 23918d6aea [design-wave1][@dark] fix: dense-инпуты в формах установки кооператива
Канон-плотность полей (dense) была только в BaseInput; общие формы
CreateOrganizationDataForm и IndividualDataForm + поля шагов установки
рендерили крупные не-dense q-input. Добавил dense:
- CreateOrganizationDataForm (22 поля) — данные организации
- IndividualDataForm (6 полей) — члены совета
- email-инпуты SetInitForm/SetSovietForm и поля SetVariablesForm

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:52 +00:00
coopops 1e774573fa [design-wave1][@dark] feat: страница установки кооператива в канон
- InstallCooperativePage: VerticalStepper вместо q-stepper, canon-панель
  с accent-стрипом, экран завершения в каноне (без градиентов/shimmer)
- RequestKeyForm: BaseInput (mono) + BaseButton
- SetInitForm: canon info-нотки, кнопки Назад/Далее на BaseButton
- SetSovietForm: canon-карточки членов совета вместо q-card/q-badge
- SetVariablesForm: canon-секции, q-input outlined с правилами, BaseButton
- Invite: осмысленное тело AuthCard при отсутствии токена приглашения

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:52 +00:00
coopops 30d5cf6156 [design-wave1][@dark] fix: убрать двойной отступ сверху на странице расширения
.extension-page уже внутри .catalog-shell__content с padding var(--p-6);
собственный padding давал двойной зазор от шапки до кнопки «Назад».

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:52 +00:00
coopops 303c169fea [design-wave1][@dark] style: панель вокруг контента расширения + «Отменить» в настройках
- Весь контент страницы расширения обёрнут в канон-панель (surface+border),
  больше не лежит на голом фоне
- В режиме настроек добавлена кнопка «Отменить» (CancelButton) рядом с
  «Сохранить» — явный выход без сохранения; кнопка «Назад» страницы тоже работает

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:52 +00:00
coopops 30a9b5c33e [design-wave1][@dark] style: страница расширения — назад на уровень страницы, центровка лого/столов, вкл+удалить в строку
- Кнопка «Назад» убрана из шапки (useBackButton снят), добавлена канон-кнопка
  под шапкой на самой странице
- Логотип AutoAvatar центрирован в колонке
- DesktopsList: центрированный inset-блок (выделяется), а не плоский список
- ext-actions toggle: «включено» и «удалить» снова в одной строке

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:52 +00:00
coopops f03f8f3bed [design-wave1][@dark] feat: страница расширения и его настройки в канон
- ExtensionPage: двухколоночный канон-layout с padding; q-img заменён на
  генеративный логотип AutoAvatar (animated), консистентно с каталогом
- ExtensionInfo: канон-заголовок + статус-бейдж, теги убраны
- ExtensionActions: компоновка кнопок в столбец на токенах, teal→primary
- ExtensionInstall: BaseCard вместо .info-card/text-h5
- DesktopsList: text-grey/subtitle → канон-токены
- SaveButton/SettingsButton/InstallButton: teal→primary, no-caps

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:52 +00:00
coopops a41861f356 [design-wave1][@dark] style: анимация логотипа по наведению на всю карточку
Триггер ринг-анимации перенесён с hover самого SVG на hover карточки
(.app-card:hover :deep(.ring-seg)).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:52 +00:00
coopops 754dbe67a0 [design-wave1][@dark] feat: анимация логотипа приложения по наведению
AutoAvatar получил режим animated: инлайн-SVG вместо <img>, каждый
сегмент-дуга обёрнут в .ring-seg. По hover логотипа сегменты на ~1.3с
расходятся — прямые и зеркальные (matrix-flip) от одного поворота идут
в разные стороны, со стаггером по индексу. CoopCard не затронут (img/blob).
Уважает prefers-reduced-motion.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:51 +00:00
coopops f6503e291b [design-wave1][@dark] style: убрать фиксированный резерв высоты заголовка карточки
Статус идёт сразу под заголовком (динамично) — без дыры под однострочными
названиями. Обрезку в 2 строки оставили, чтобы длинный заголовок не ломал сетку.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:51 +00:00
coopops 3461343710 [design-wave1][@dark] style: выровнять карточки приложений при переносе заголовка
Заголовок резервирует 2 строки (clamp+min-height), аватар по верху —
статус и описание на всех карточках начинаются на одной линии, не прыгают.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:51 +00:00
coopops 2d1f869774 [design-wave1][@dark] style: компоновка карточки приложения — заголовок и статус рядом с логотипом
Верхний ряд: логотип + (заголовок, под ним статус); описание во всю
ширину ниже; футер «Подробнее» прижат к низу для выравнивания карточек.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:51 +00:00
coopops a4498e1a45 [design-wave1][@dark] style: приглушить генеративный логотип каталога
Палитра колец → мягкие приглушённые тона + CSS saturate(.5)/opacity .85,
чтобы знак читался как тихая текстура, а не неоновое пятно.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:51 +00:00
coopops 49836cd139 [design-wave1][@dark] feat: генеративный логотип (DiceBear rings) в карточке приложения
- AutoAvatar параметризован: props size/radius/background/ringColor
  с обратно-совместимыми дефолтами (CoopCard не затронут)
- ExtensionCard: буква-монограмма заменена на уникальный rings-логотип,
  seed = имя расширения; цвет кольца детерминирован по seed из палитры

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:51 +00:00
coopops 226943a52a [design-wave1][@dark] feat: каталог приложений в канон — табы на 2-й уровень, карточки без изображений
- «Магазин приложений» → «Каталог приложений» (drawer + breadcrumb)
- Витрина/Установленные перенесены из топбара в SecondLevelTabs
- ExtensionCard переверстан: убраны изображение и теги, добавлены
  монограмма-плитка, статус-бейдж, обрезка описания, футер «Подробнее»
- Quasar .row/.col-md-3 → канон auto-fill грид

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:51 +00:00
coopops 55983ed3f3 [design-wave1][@dark] fix: вёрстка формы провайдера платежей + текст контактов
- PaymentProviderForm: убраны вложенные q-gutter (кривой левый отступ
  селекта), standout=bg-teal → outlined, text-grey-7 → токен; чистый
  канон-столбец (селект max-width 480, hint ink-2, действия в ряд, no-caps).
- ChangeContacts: текст уточнён — контакты видны всем (раздел «Контакты
  кооператива» пайщикам + подвал сайта незарегистрированным), не только
  незарегистрированным.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:51 +00:00
coopops 974fcb305a [design-wave1][@dark] feat: канон для остальных страниц стола председателя
Кооперативные участки, Регистрационные взносы, Ключ кооператива,
Провайдер платежей, Контакты кооператива:
- убран дублирующий заголовок страницы (есть крошка в шапке);
- описание перенесено с голого фона на канон-поверхность .banner;
- page-shell/hero/surface-card с хардкод-px → канон-отступы и токены;
- q-input/q-select standout=bg-teal → outlined color=primary.
Также удалён лишний хвост текста на Стартовых страницах.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:51 +00:00
coopops a9fa74fc8c [design-wave1][@dark] fix: убрать дубль заголовка на Стартовых страницах и Членах совета
Заголовок страницы уже показан крошкой в шапке — повтор h2 убран.
Пояснительный текст вынесен с голого фона на канон-поверхность (.banner
с инфо-иконкой). ApprovalsPage не трогаем — там заголовка страницы нет.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:51 +00:00
coopops a41550d14d [design-wave1][@dark] feat: канон для Запросов одобрений, Стартовых страниц, Членов совета
- ApprovalsPage: канон-отступы страницы, фильтр-селект teal standout → outlined.
- SystemSettingsPage: hero-card/surface-card с хардкод-px → канон page-head
  (h2 + sub на токенах) + q-card(flat); канон-отступы.
- MembersPage: тот же канон-паттерн вместо hero-card с хардкодами.
- DefaultPagesForm: 4× q-select standout=bg-teal → outlined color=primary.
Виджеты таблиц/состава совета без экзотики — канон-тема через quasar-canon.css.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:51 +00:00
coopops 6d50758cc8 [design-wave1][@dark] fix: убрать Vue-warn о class в WorkspaceSwitcher
Шаблон двухкорневой (кнопка + Teleport затемнения), class из родителя
(LeftDrawerMenu) не наследовался автоматически. inheritAttrs:false +
v-bind=$attrs на корневую кнопку — атрибуты направлены явно.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:51 +00:00
coopops 26b94c367c [design-wave1][@dark] style: согласовать типографику диалога повестки
Подписи секций «Вопрос на повестке»/«Проект решения» переведены в
надстрочные метки (uppercase, трекинг, ink-3) — отдельный ярус ниже
заголовка окна, чтобы не казаться конкурирующим заголовком рядом с
крупным заголовком самого документа. Текст вопроса — читаемый body.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:51 +00:00
coopops 60d647c470 [design-wave1][@dark] fix: канон-отступы и чистка онбординга председателя
- ConnectPage: добавлены канон-отступы страницы (var(--p-6)/var(--p-4)) —
  класс .padding в проекте не определён, контент липнул к краям.
- OnboardingStepsCard: убран q-card (двойной паддинг), текст-интро
  растянут на полную ширину (снят max-width 70ch), вокруг кнопки шага
  убрана лишняя рамка (.step__content → .step__action, только отступ).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:51 +00:00
coopops d55b3e2fd9 [design-wave1][@dark] feat: онбординг председателя в канон (вертикальный степпер + текст + диалог повестки)
- OnboardingStepsCard: под заголовком «Адаптируйте кооператив…» добавлен
  поясняющий текст (что происходит дальше: решения совета в электронной
  форме, вступление в «Восход», импорт пайщиков, общее собрание) + плашка
  срока адаптации с отсчётом.
- Чек-лист шагов переведён на канон-вертикальный степпер (.stepper--v):
  пройденные — галочка/заливка primary, текущий — подсветка номера,
  действия — канон BaseButton; agenda/import/meet типы сохранены.
- Диалог «Предложение повестки» (объявить собрание совета) переведён
  с ModalBase на канон BaseDialog + BaseButton.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:51 +00:00
coopops e0a9d559a9 [design-wave1][@dark] feat: хлебные крошки в шапке + скрыта кнопка «Перевести»
- CommonHeader: крошка строится из route.matched (путь внутри стола),
  напр. «Отчётность › Календарь»; корень стола не выводится; плоские
  страницы дают одну крошку как прежде; pageTitleOverride сохранён.
- CoopWalletsPage: переводы временно скрыты под флагом transferEnabled=false
  (открывались с L3-кошельков пайщиков; вернём для кооп-кошельков позже).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:50 +00:00
coopops b5cfe40661 [design-wave1][@dark] style: канон-токены в редакторе отчёта (ReportEditorDialog)
- editor-container/action-panel/backdrop/validation-badge/mark-hint: hex и rgba → --p-* токены
- text-grey-8 → t-muted; box-shadow/overlay через токены с fallback

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:50 +00:00
coopops 180e22d07a [design-wave1][@dark] style: канон-токены на списочных страницах бухстола + cells
- Operations/Postings/Accounts: канон page-padding, убран двойной q-mt-md, text-grey-* → t-faint/t-muted, hex-границы → токены
- Settings: канон-padding, save-bar на токены (--p-surface/--p-line/--p-pos), text-grey-7 → t-muted
- AccountIdCell/WalletIdCell: text-grey-6 → t-faint; DirectionCell: move-иконка на --p-ink-3
- PROCESS_COLORS доменного цвет-кодирования операций сохранены (это фича)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:50 +00:00
coopops 34ac74f6a1 [design-wave1][@dark] style: канон-токены на столе бухгалтера — кошельки, календарь, отчётность
- CoopWalletsPage/ParticipantWalletsPage: rgba/var(--q-*) → канон-токены (--p-pos/--p-warn/--p-ink-*)
- ReportsCalendar/CalendarCell: статусы и грид на канон-палитру, убраны body--dark дубли
- Documents подстраницы: убран двойной padding, q-chip→BaseBadge, таблицы в канон-карточку

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:50 +00:00
coopops e23196d492 [design-wave1][@dark] feat: канон-меню второго уровня (SecondLevelTabs) на столе бухгалтера
- SecondLevelTabs на глобальных .tabbar/.tab — sub-навигация между топбаром и контентом
- WalletsPage/DocumentsPage shell-страницы: вкладки в .tabbar вместо RouteMenuButton в топбаре
- TransferWalletsButton: канон header-кнопка (q-btn primary)
- WalletTransferDialog: на BaseDialog + BaseButton, токены вместо rgba

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:33 +00:00
coopops 8206c9f358 [design-wave1][@dark] fix: канон-поля ZodForm и убран дубль заголовка «Настройки»
ZodForm: убран standout="bg-teal text-white" с динамического поля —
QInput/QSelect теперь канон (outlined+dense+color=primary+reserve-hint-space),
как BaseInput. SCSS на канон-токены (вместо неопределённого --q-primary-rgb).
ExtensionSettings: снят заголовок «Настройки» внутри карточки — он дублировал
«Настройки аренды» в шапке.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:33 +00:00
coopops 5e01c25ff8 [design-wave1][@dark] fix: фильтры лога на BaseInput вместо сырого q-input
Канон-инпут вместо q-input: BaseInput расширен поддержкой type="date"
и clearable (+ emit clear) — это нужно для фильтра по датам. Фильтры
в ExtensionLogsList переведены на BaseInput.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:32 +00:00
coopops 957711e3bc [design-wave1][@dark] feat: настройки и лог расширения в канон
ExtensionSettings: EmptyState + BaseCard вокруг ZodForm вместо самописных
header/empty-state. ExtensionLogsList (используется только powerup):
q-table → список канон-карточек с фильтром по датам, скелетоном,
«Загрузить ещё» и EmptyState; слот #log-item и API сохранены.
Канон-падинги страниц настроек/лога аренды.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:32 +00:00
coopops 2ee53bc7dd [design-wave1][@dark] feat: Стол вычислительных ресурсов в канон — раскладка, виджеты, лог
Монитор ресурсов: баннер-предупреждение на всю ширину сверху, ряд
«Как это работает» (2/3) + кошелёк AXON (1/3), ряд CPU/NET/RAM.
Убран 3D-флип карт (он же «прыгал») — детали раскрываются inline.
Все виджеты/страницы/лог сведены к канон-токенам surface/line/ink.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:32 +00:00
coopops 2f0aad0015 [design-wave1][@dark] fix: отступы действий платежей и ширина даты в документах
- Реестр платежей: кнопки действий больше не block (не заполняют ячейку
  от левого края, не липнут к бейджу статуса). Стопка inline-flex,
  равная ширина кнопок (align-items:stretch), прижата к правому краю
  колонки (col-action 168px, text-align:right) — слева остаётся воздух.
- Реестр документов: col-date 116→156px — дата «26.05.2026 08:25» больше
  не налезает на заголовок документа; min-width 720→780.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:32 +00:00
coopops e986984dcc [design-wave1][@dark] fix: реестры платежей/документов и форма взноса — ширины и кнопки
- Bank.vue: панель платежа max-width 380px по центру — длинное «Назначение»
  больше не раздувает диалог «Совершите взнос» до max-content; значение
  переносится по строкам (overflow-wrap: anywhere).
- Кнопки статуса платежа SetOrderPaid/RefundedStatusButton → BaseButton
  (primary/danger, block), подписи «Подтвердить»/«Отклонить»; диалог
  вынесен из-под кнопки.
- Реестр платежей: кнопки действий друг под другом (.cell-actions),
  колонка действий 156px, дата без переноса (.col-date 132px), min-width
  таблицы уменьшен.
- Реестр документов: сужены колонки подписей (220→150), действий
  (110→56), id/дата; min-width 880→720 — освобождено место под
  наименование, таблица не вылазит за страницу.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:32 +00:00
coopops 976b98dd3f [design-wave1][@dark] fix: реестры документов и платежей — канон-поля страницы
Таблицы документов и платежей уже на канон-токенах и самообрамлены
(.table-wrap). Не хватало полей страницы как в реестре пайщиков:
- PaymentsPage рендерил виджет голым, впритык к краям → q-page + 24/16px.
- ListOfDocumentsPage: q-page.padding (16px) → канон-24px.
- Убрана лишняя .row.justify-center обёртка в ListOfDocumentsWidget
  (.col-12 и так во всю ширину).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:32 +00:00
coopops 7bd98a2a79 [design-wave1][@dark] fix: диалоги «Добавить» и «Импорт» пайщиков в канон
AddUserDialog перестроен в канон-визард по образцу CreateMeetForm:
шапка-bar (surface+line) вместо bg-gradient-dark, VerticalStepper из трёх
шагов — Электронная почта → Тип и данные → Вступительный взнос, BaseButton
в подвале, outlined-инпуты. UserDataForm переиспользован целиком (общий,
не тронут). Опция начисления взноса — канон-блок вместо q-item/q-card.

ParticipantsImportDialog: добавлено описание-интро (раньше отсутствовало),
ModalBase/bg-gradient-dark заменён на канон-bar, выбор типа аккаунтов через
BaseRadioCard, dropzone на токенах вместо хардкод-цветов, секции и действия
в канон-обёртках, BaseButton вместо цветных q-btn. Логика парсинга/импорта
и таблицы превью/результатов сохранены.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:32 +00:00
coopops 52ca724508 [design-wave1][@dark] fix: реестр пайщиков — канон-поля и дыхание вместо встык-вёрстки
Страница получила канон-отступ (--p-6 / --p-4 на мобилке), таблица обёрнута
в обрамлённую surface-карточку (--p-line, --p-r-lg) вместо edge-to-edge.
Развёрнутые данные пайщика — спокойная вложенная панель на --p-surface-2
с отступами --p-5/--p-6, читаемой шириной формы (640px) и вертикальным
ритмом между полями. Снят full-height у таблицы (мешал внутри карточки).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:32 +00:00
coopops 5c0a08bce8 [design-wave1][@dark] refactor: реестр пайщиков в канон + убран переключатель Данные/Документы
Развёрнутая строка пайщика теперь показывает только его данные;
документы пайщика живут в отдельном «Реестре документов» и здесь не дублируются.
ParticipantDetails упрощён (без q-tabs/ListOfDocumentsWidget), tab-логика
вычищена из таблицы и страницы. Мобильная ParticipantCard переведена на канон-токены
(убраны хардкод-цвета, CardStyles и q-badge статуса → disabled-checkbox).
Таблица сохранена, сортировка по дате вступления (DESC) без изменений.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:32 +00:00
coopops 9d27acb45a [design-wave1][@dark] fix: повестка — номер вопроса на зелёной плашке вместо иконки, клик копирует
Убран EntityIdBadge. Зелёная плашка-аватар слева теперь несёт сам номер вопроса
(он же Decision ID): клик по плашке копирует id (тултип «Скопировать №»), hover —
инверсия в сплошной teal. Заголовок снова просто текст вопроса.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:32 +00:00
coopops e643020980 [design-wave1][@dark] fix: повестка — бейдж ID в строку заголовка перед текстом вопроса
EntityIdBadge перенесён из отдельной строки под ФИО в начало строки заголовка:
[#id] → сразу текст вопроса. Inline, vertical-align middle. Раньше висел снизу
и пусто растягивал верхнюю строку.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:32 +00:00
coopops 2221552c32 [design-wave1][@dark] feat: повестка — копируемый бейдж Decision ID под ФИО
EntityIdBadge с id вопроса под заголовком/ФИО: отображает #<id>, по клику копирует
чистый id (copy-on-click). Канон-компонент, клик не раскрывает карточку.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:32 +00:00
coopops 3ef7bd1f92 [design-wave1][@dark] fix: повестка — срок «Истекает …» в нижнюю полоску рядом с «Утвердить»
«Истекает через месяц» перенесён из строки под заголовком в постоянную нижнюю
полоску (footer): срок слева, «Утвердить» справа (только председателю). У обычного
пайщика — только срок, узкая панелька.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:32 +00:00
coopops 8beab9ab0a [design-wave1][@dark] fix: повестка — «Истекает …» и убран избыточный бейдж статуса
Срок теперь подписан «Истекает через месяц» (было просто «через месяц» — неясно).
Удалён статус-чип «Вы за/против» — направление голоса уже видно по подсветке кнопок,
бейдж был избыточным. Убраны связанные computeds и стили.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:32 +00:00
coopops 16884c9b1c [design-wave1][@dark] fix: повестка — переосмысленная компоновка карточки вопроса
Кнопки голосования прижаты к правому краю строки. «Утвердить» вынесена вниз
отдельной строкой-footer (справа, hairline сверху) — больше не теснит данные.
Срок «через месяц» и статус-чип «Вы за» перенесены влево под заголовок/ФИО.
Клик по строке раскрывает документ, по кнопкам голосования и footer — нет.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:32 +00:00
coopops b0a200a191 [design-wave1][@dark] fix: повестка — органы управления в одну строку с инфо, не остров по центру
Карточка вопроса перестроена в единую горизонтальную строку: иконка + вопрос/ФИО
слева (flex), компактный блок органов управления (голосование + «Утвердить»),
срок + статус-чип и шеврон — справа, друг рядом с другом. Убрана центрированная
vote-зона с пустотой по бокам. Клик по строке раскрывает документ, по органам
управления (@click.stop) — нет. Подсказка «Утвердить» возвращена в tooltip.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:31 +00:00
coopops e1f2a0cb7d [design-wave1][@dark] fix: повестка — раскрытие документа по клику на шапку, не на органы управления
Шапка карточки (вопрос/ФИО/срок/статус) кликабельна целиком и раскрывает документ,
справа шеврон-индикатор. Органы управления голосованием — отдельная зона ниже шапки
(сиблинг, не аккордеон): клик по ним голосует/утверждает и документ не раскрывает.
Убрана отдельная кнопка «Документ» — раскрытие теперь по клику на шапку.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:31 +00:00
coopops 23811fa3e7 [design-wave1][@dark] fix: голосование повестки на верхнем уровне + header-CTA совета
QuestionCard: VotingButtons + «Утвердить» вынесены на верхний уровень карточки —
голосовать можно без раскрытия; раскрытие («Документ») открывает только содержимое
документа. Истечение срока остаётся в шапке.
VotingButtons: вернул чек-индикатор «принято советом» (закрашивается при принятии)
вместо иконки verified.
Header-CTA совета (Предложить/Добавить/Импорт): убран push (q-btn--push не попадал
под канон-правило заливки primary → белый шрифт на нетиловом фоне), подписи с заглавной.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:31 +00:00
coopops 702d395f6c [design-wave1][@dark] feat: повестка совета в канон — единый карточный список
QuestionsTable: q-table → карточный список (скелетоны + EmptyState).
QuestionCard: канон-поверхность, BaseButton «Утвердить», токен-чип статуса
вместо q-badge, локальное состояние раскрытия, без @import CardStyles и хардкод-hex.
VotingButtons: токены pos/neg вместо .text-red/.text-green/#666.
Страница: канон-padding вместо q-card flat.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:31 +00:00
coopops 1f74367a5a [design-wave1][@dark] fix: единый регистр названий столов в переключателе
Названия столов в реестре workspace'ов заведены вразнобой («Стол
благороста», «Стол вычислительных ресурсов» vs «Стол Совета»).
Добавил text-transform: capitalize на подписи в WorkspaceSwitcher
(заголовок текущего стола + пункты выпадающего меню) и WorkspaceMenu
(карусель + диалог выбора) — первая буква каждого слова заглавная,
одинаково для всех и для будущих столов, без правки строк-источников.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:31 +00:00
coopops 70a6d9010d [design-wave1][@dark] fix: «Контакты» — единая сетка полей вместо сегментов
По фидбеку переработана композиция:
- убраны заголовки секций «Контакты»/«Руководство» и per-строчные
  иконки телефона/почты/адреса (из-за них значения «прыгали» вправо
  относительно ИНН/ОГРН);
- ИНН и ОГРН теперь в одну строку (адаптивная сетка полей);
- председатель поднят наверх к реквизитам, поле названо просто
  «Председатель совета»;
- контакты — те же поля, телефон/email как ссылки (tel/mailto), всё на
  единой левой кромке; одна тонкая линия делит реквизиты и контакты.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:31 +00:00
coopops fbaca1ba9f [design-wave1][@dark] fix: «Контакты» — единая раскладка строк во всех секциях
Реквизиты/Руководство больше не разносят подпись и значение к
противоположным краям (на полной ширине это давало пустой провал и
оторванные значения). Все секции теперь как ContactSheet: подпись
сверху, значение под ней, слева, hairline между строками — один ритм
по всей карточке.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:31 +00:00
coopops 5ddc78b535 [design-wave1][@dark] feat: страница «Контакты» в канон + удалён мёртвый MeetQuorumIndicator
- ContactsPage: три цветные ColorCard (orange/indigo/teal/blue) заменены
  на единую спокойную canon-поверхность с секциями через hairline
  (Реквизиты / Контакты / Руководство). Контакты — через canon
  ContactSheet (копирование, mailto/tel). Заголовок и строки на токенах
  (--p-ink/-2/-3, --p-surface/--p-line/--p-r-lg), полная ширина (padding
  24/16px). Убраны rgba-фоны и .q-dark-хаки.
- Удалён неиспользуемый виджет MeetQuorumIndicator (нет импортов;
  явка/кворум живут в MeetInfoCard).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:31 +00:00
coopops 837d529ddb [design-wave1][@dark] feat: тело деталей собрания в канон — повестка/голосование/результаты
Убрал токсичный teal из state-gated виджетов деталей собрания:
- MeetDetailsAgenda/Voting/Results переписаны на canon-поверхности
  (--p-surface/--p-line/--p-r-lg), section-heading с иконкой вместо
  центрированной градиентной линии; color-mix/--q-primary/.q-dark удалены.
- Голосование: q-btn color=primary → BaseButton variant=primary;
  опции голоса на токенах pos/neg/neutral-soft, баннер «уже голосовали»
  на --p-pos-soft.
- Результаты: итог решения — токен-чип (pos/neg/unknown), счётчики
  голосов на pos/neg/surface-2-soft.
- AgendaNumberAvatar: teal q-avatar → нейтральный квадрат surface-2/ink
  (общий для трёх виджетов — убирает teal сразу везде).
- MeetCardsList: отступ между карточками 16→20px.
- MeetDetailsPage: кастомный empty-state → canon EmptyState, снят
  @import CardStyles.

MeetQuorumIndicator оставлен как есть — мёртвый код (нет использований).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:31 +00:00
coopops ba6464c397 [E14][@dark-sun] feat(собрания): полная ширина страниц; тело деталей (MeetInfoCard) на canon
- ListOfMeetsPage / MeetDetailsPage: убран max-width:960px center — контент
  во всю ширину с canon-паддингом (24px/16px), как документы/платежи.
- MeetInfoCard: переписан с токсичного teal на canon — единая calm-поверхность
  с тремя секциями через hairline (Даты / Ведущие / Явка и кворум) вместо
  трёх вложенных цветных карточек; заголовки ink, проценты ink (не teal);
  дубль заголовка «Общее собрание № N» убран (он в шапке). Убраны
  var(--q-primary), color-mix, .body--dark, хардкод-rgba.
- MeetDetailsInfo: контейнер с .card-container → canon.
- MeetDetailsActions: q-btn color=primary → canon BaseButton.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:31 +00:00
coopops 225b4abc0e [E14][@dark-sun] feat(собрания): повестка на шаге проверки канон; карточка списка канон; шапка деталей — back-link вниз, имя в заголовок
- CreateMeetForm: повестка на шаге «Проверка» — аккуратные canon-карточки
  (Вопрос / Проект решения / Приложения) вместо скомканного списка.
- MeetCompactCard: переписана на canon — нейтральный hover (без
  токсичного teal-свечения), ink-заголовок, canon-плитки/иконка;
  убраны @extend .card-container, var(--q-primary), .body--dark, color-mix.
- MeetStatusBanner: нейтральный canon-контейнер (surface-2 + line),
  цвет статуса несёт иконка; убраны хардкод-rgba и .body--dark.
- MeetCardsList: empty-state на canon EmptyState, skeleton на .skel.
- Детали собрания: кнопка «Назад» убрана из топбара (снят useBackButton),
  добавлен canon back-link под шапкой слева; название собрания выводится
  в заголовок шапки через новый desktopStore.pageTitleOverride
  (приоритет над route.meta.title; транзиентно, чистится при уходе).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:31 +00:00
coopops ddc66d8529 [E14][@dark-sun] feat(собрания): форма созыва на canon-степпер; кнопка в шапке к канону; скрыта «Сверить»
- BaseDocument: кнопка «Сверить» временно скрыта через :hide-verify
  (обработчик @verify остаётся, вернуть = убрать флаг).
- CreateMeetButton: приведена к одобренному canon-паттерну (primary
  solid на десктопе, micro flat accent + tooltip на мобильном),
  убран legacy push/teal с белым текстом.
- ListOfMeetsPage: инжект кнопки переведён с useHeaderActions на
  Teleport #header-actions-host.
- CreateMeetForm: полностью переписана с перегруженной тёмной модалки
  на canon-визард — maximized-диалог + VerticalStepper (3 шага:
  Параметры → Повестка → Проверка). Canon outlined-поля, повестка
  аккуратными карточками, шаг проверки со сводкой. Убраны
  bg-gradient-dark / standout bg-teal / .body--dark / хардкод rgba.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:31 +00:00
coopops 5d007b987d [E14][@dark-sun] feat(стол пайщика): скелетоны вместо спиннера в таблицах документов и платежей
Добавлен canon-компонент TableSkeleton (shared/ui/base) — повторяет
структуру .table-wrap/.table с реальными заголовками и мерцающими
плейсхолдерами (.skel) в ячейках. Каркас не дёргается при подгрузке
данных (calm-data, UX-DR2/UX-DR23). Заменил перекрывающий q-spinner
в DocumentsTable и ListOfPaymentsWidget.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:31 +00:00
coopops c807eb0ef8 [E14][@dark-sun] feat(стол пайщика): документы — ID копируемым бейджем, отдельные колонки Дата/Документ, подписи бейджами; поиск минимальный на мобильном
- DocumentsTable: наименование берём из meta.title (чистое, без даты/.pdf
  суффикса full_title); отдельная колонка Дата (meta.created_at) с сортировкой
  по block_num, по умолчанию свежие сверху; колонка Документ — только заголовок.
- ID — копируемый EntityIdBadge (показывает короткий хеш, копирует полный
  doc_hash по клику + иконка-affordance).
- Подписи — отдельные BaseBadge на подписанта (новый helper
  getSignersListFromDocumentPackage возвращает массив).
- EntityIdBadge канонизирован: токены вместо rgba/--q-accent/.q-dark; добавлен
  copyValue (показать одно — скопировать другое).
- SearchHeaderAction: на мобильном — минимальная round-dense иконка без подписи
  «Поиск» (label только на десктопе).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:31 +00:00
coopops 37d6a761db [E14][@dark-sun] feat(стол пайщика): документы и платежи — канон-таблица .table-wrap вместо q-table
Переделка после ревью: q-table давал не-канон вид, дёрганье при раскрытии
(virtual-scroll пересчитывал размеры + colspan не совпадал с числом колонок)
и уродливые мобильные карточки в grid-режиме.

- Статическая канон-таблица .table-wrap/.table (бордер+радиус+surface как
  карточка), table-layout:fixed → колонки не разъезжаются при раскрытии.
- Раскрытие строки — CSS-only (expand-row td colspan по числу колонок),
  без virtual-scroll → нет дёрганья шапки/колонок.
- Пагинация load-more (.table-foot + BaseButton «Загрузить ещё» + «1–N из M»)
  вместо infinite virtual-scroll.
- Мобильный: горизонтальный скролл таблицы (.table-scroll) вместо grid-карточек
  PaymentCard/DocumentCard — карточки удалены.
- Статусы платежей — BaseBadge (pos/warn/neg/info/neutral); направление —
  иконка + цвет --p-pos/--p-neg; хеш документа — mono.
- Действия платежей (SetOrderPaid/Refunded) скрыты на столе пайщика
  (hideActions=true) — они в реестре платежей; download документов сохранён.
- EmptyState + спиннер первой загрузки.

Виджеты общие с админ-контуром: load-more и горизонтальный скролл там тоже
уместнее jittery virtual-scroll.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:31 +00:00
coopops e5bf464a4d [E14][@dark-sun] feat(стол пайщика): документы и платежи в канон — статус-бейджи, токены, header-action на Teleport
- ListOfPaymentsWidget + PaymentCard: статус платежа через BaseBadge
  (canon-варианты pos/warn/neg/info/neutral) вместо хардкод q-color
  teal/orange/red/blue; убраны битый inline-стиль $toolbar-min-height,
  inline max-width, глобальный .q-list style-leak; цвета/тени/радиусы —
  на токены (--p-line/--p-ink-3/--p-shadow-card/--p-surface-2).
- DocumentsTable + DocumentCard: убраны битый inline-height, inline
  max-width, .q-dark-селекторы, --q-gray, хардкод rgba; тени/линии — токены;
  глобальный q-table__top style → scoped :deep; CardStyles import убран.
- ListOfDocumentsWidget: brand-leak toggle-color teal → primary.
- DocumentsPage: SearchHeaderAction со старого useHeaderActions store на
  canon Teleport (#header-actions-host), кнопка сама гейтится по features.search.
- DocumentsPage/PaymentsPage: канон-паддинг страницы.

q-table-движок (virtual-scroll/infinite-load/раскрытие строк/mobile-grid)
сохранён — виджеты общие с админ/председательскими контурами, регресс недопустим.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:31 +00:00
coopops 94d09d482d [E14][@dark-sun] fix(шапка): правая группа прилипала влево на страницах без действий — вернул margin-left:auto
Когда страница не телепортирует действия, .topbar__actions скрывается
display:none через :has(пустой host), но adjacent-селектор
.topbar__actions + .topbar__right всё равно матчился и обнулял margin-left
правой группы — она уезжала влево к крошке. Возвращаем auto в :has-правиле
(его специфичность выше). Также убрал лид-надпись на странице реквизитов.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:30 +00:00
coopops e125fb3d56 [E14][@dark-sun] feat(стол пайщика): удостоверение и реквизиты в канон — IdentityPanel/DataRow/BaseCard, header-action на Teleport
- ProfilePage (Удостоверение): IdentityPanel-шапка + BaseCard-секции (Учётная
  запись/Личные данные/Документы и реквизиты) на DataRow; убран CardStyles,
  хардкод rgba и .q-dark.
- PaymentMethods widget (Реквизиты): BaseCard на метод + DataRow + EmptyState
  вместо .info-label/.info-value и хардкод-цветов.
- PaymentMethodsPage: кнопка добавления реквизитов переведена с useHeaderActions
  store на canon Teleport (#header-actions-host), micro на мобильном.
- AddPaymentButton: триггер под canon micro-паттерн (как Deposit/WithdrawButton).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:30 +00:00
claude 0acd4aa872 fix: remove stray node_modules symlink committed into branch
Симлинк node_modules -> /home/admin/mono-ai-2/node_modules был случайно
закоммичен и ломал pnpm install (ENOTDIR) на любой машине без этого пути.
Правило .gitignore 'node_modules/' (со слешем) ловит только каталог, не
симлинк-файл — добавлено правило 'node_modules' без слеша.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 09:18:30 +00:00
coopops 5d490913b2 [E14][@dark-sun] feat(шапка): крошка-название страницы слева + фикс переноса заголовка ModalBase
Крошка: в #crumb рядом с BackButton выводится route.meta.title текущей
страницы (тот же, что подсвечен в меню) — именно для неё действия и
сдвинуты вправо. Длинное название обрезается ellipsis в .topbar__crumb b.

ModalBase: q-bar получил авто-высоту, заголовок переносится (white-space
normal + overflow-wrap), крестик прижат к верху — на узком экране титул
больше не обрезается сверху.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:30 +00:00
coopops a3942d696c [E14][@dark-sun] fix(WalletPage): micro-кнопки взноса/возврата в шапке на мобильном
Why: полноразмерные DepositButton/WithdrawButton в узкой мобильной шапке
раздувались — текст переносился в 2 строки, кнопки вылезали за высоту
topbar. micro-вариант (иконка + tooltip, flat/dense) и предназначен
для слота шапки.

What: Teleport-кнопки получают :micro='isMobile' (useWindowSize, <768px).
Мобильный — компактные иконки; десктоп — полные кнопки с подписью.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:30 +00:00
coopops 5784c20aa7 [E14][@dark-sun] fix(WithdrawButton): «получить возврат» → «Получить возврат» — заглавная
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:30 +00:00
coopops ceac5aee31 [E14][@dark-sun] feat(стол пайщика): кошельки списком во всю ширину + действия шапки через Teleport
Кошельки (по фидбэку — переносы выглядят плохо):
- .wallet-programs из grid (repeat auto-fill minmax) → flex-column: карточки
  идут списком во всю ширину страницы.
- canon .wallet__title/__sub: возвращён nowrap + ellipsis (откат wrap-фикса);
  на широкой строке текст почти всегда влезает, иначе — ellipsis.
- WalletCard + минимум-карточка: нативный tooltip `title` — при наведении
  виден полный текст. Убран .wallet--row reset (базовый снова nowrap).

Действия шапки (кнопки взноса/возврата пропали; нужна новая механика):
- Новый canon-механизм: страница телепортирует свои действия в шапку через
  <Teleport to="#header-actions-host">. Host — постоянный span с
  display:contents в #actions слоте CommonHeader (при loggedIn).
- :has()-правило прячет .topbar__actions, когда внутри только пустой host
  (нет ни store-кнопок, ни телепорта) — чтобы не было пустого разделителя.
- WalletPage переведён на Teleport (DepositButton/WithdrawButton),
  useHeaderActions store-механизм убран со страницы.
- Старый useHeaderActions оставлен для прочих 10 страниц — мигрируем
  и удалим отдельно. DepositButton сам скрыт, пока пайщик не принят
  (status !== 'active') — это защита, не баг.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:30 +00:00
coopops 028eb31c03 [E11][@dark-sun] fix(CommonHeader): бренд-логотип inline-SVG в зелёном квадрате, как в личном кабинете
Why: на странице без логина логотип был <img :src='logo.svg'> — img рендерит
SVG в изоляции, не наследует currentColor, поэтому показывался чёрно-белым
и не реагировал на смену темы. В личном кабинете (WorkspaceSwitcher)
тот же logo.svg рендерится inline через v-html в зелёном квадрате
и наследует color (logo.svg на fill:currentColor) — «зелёненький
на зелёном фоне», одинаковый в обеих темах.

What: CommonHeader brand-slot переведён на тот же приём —
`logo.svg?raw` + v-html внутри .app-q-header__logo
(background var(--p-primary-soft), color var(--p-primary), 28px квадрат,
16px svg). Переключение темы больше не требуется — зелёный константен.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:30 +00:00
coopops fc60c58f51 [E14][@dark-sun] fix(canon .wallet): просто wrap по пробелам, без overflow-wrap: anywhere
Why: предыдущий фикс `overflow-wrap: anywhere; word-break: break-word`
ломал слова в любом месте даже когда колонка достаточно широкая —
«Минимальный неснижаемый остаток» рендерился по одному слову на строку,
несмотря на ~480px доступной ширины. Реально нужен только wrap по пробелам;
agressive break-word оправдан только для URL-подобных нерасчленяемых строк.

What: убраны `overflow-wrap` и `word-break` из .wallet__title/__sub —
браузер делает естественный wrap по пробелам, длинные заголовки переносятся
только когда не помещаются. `.wallet--row` reset для compact-варианта
оставлен — там по-прежнему single-line+ellipsis.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:30 +00:00
coopops 6a2a56dcfa [E14][@dark-sun] fix(canon .wallet): разрешить перенос длинных заголовков на full-варианте + порядок карточек в WalletProgramWidget
Why: canon-стиль .wallet__title и .wallet__sub был с
`white-space: nowrap; overflow: hidden; text-overflow: ellipsis;` — на full-
варианте это резало длинные подписи («Минимальны…», «Возвращается п…»)
даже на просторных экранах, потому что grid-колонка `.wallet__main` сжимается
ради `.wallet__amount` справа. Эта обрезка будет всплывать на любой
длинной строке (метки программ, статусы пайщика, длинные subtitle).
Compact-вариант `.wallet--row` (слот шапки) должен остаться одной строкой.

What:
- .wallet__title/__sub: убран nowrap/ellipsis; добавлено
  `overflow-wrap: anywhere; word-break: break-word; hyphens: auto` (для title)
  и `overflow-wrap: anywhere; word-break: break-word` (для sub) — длинные
  заголовки переносятся на 2+ строки.
- .wallet--row: явно возвращает nowrap+ellipsis (с reset word-break/
  overflow-wrap), чтобы compact-ряды в шапке оставались строго одной строкой.
- WalletProgramWidget: карточка минимального неснижаемого остатка
  перемещена в начало grid'а (была после программ) — это базовая защита
  средств пайщика, логичнее видеть её первой.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:30 +00:00
coopops a8013c7414 [E14][@dark-sun] refactor(WalletCard,WalletProgramWidget): «Главный кошелёк» канон-default + минимальный остаток отдельной карточкой
Why:
- Заголовок «Кошелёк» для program='wallet' путал — в столе пайщика этот
  кошелёк семантически главный, и в любых других местах canon (MicroWallet,
  WalletCardMini, _dev/ui) он тоже должен называться полно — «Главный
  кошелёк». Лучше один canon-default, чем локальный override в каждом
  потребителе. Согласовано — переименование canon DEFAULT_TITLES.
- Минимальный неснижаемый остаток — НЕ баланс кошелька, а самостоятельная
  сущность пайщика (паевой взнос, возвращается при выходе). Пристегивать
  его DataRow-строкой под grid'ом — нелогично, как было и раньше в legacy.
  Правильнее — отдельной карточкой в той же сетке кошельков.

What:
- WalletCard.vue: DEFAULT_TITLES.wallet 'Кошелёк' → 'Главный кошелёк'.
  Глобально для всех потребителей canon-компонента.
- WalletProgramWidget.vue: убран TITLE_OVERRIDE и DataRow-блок минимального
  остатка. Карточка остатка теперь рендерится в общем .wallet-programs
  grid'е canon-разметкой `.wallet` с иконкой `savings`, заголовком
  «Минимальный неснижаемый остаток», подзаголовком «Возвращается при выходе
  из кооператива» и зарезервированной суммой. Нейтральная подсветка иконки
  через `.wallet--minimum { --prog-bg, --prog-fg }`, чтобы визуально
  отличалась от программ, но встала в общую сетку.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:30 +00:00
coopops 0f475e7dbd [E14][@dark-sun] feat(WalletProgramWidget): «Главный кошелёк» + минимальный неснижаемый остаток
Why: на старом WalletWidget пайщик видел свой минимальный неснижаемый остаток
(паевой взнос, возвращается при выходе из кооператива) — самостоятельная
сущность пайщика, не баланс кошелька. При переходе на canon я её упустил.
Канон-default WalletCard 'wallet' = «Кошелёк» — для стола пайщика этот
кошелёк семантически является главным (свободный остаток ЦК), поэтому
локально перекрываем заголовок на «Главный кошелёк».

What:
- TITLE_OVERRIDE['wallet'] = 'Главный кошелёк' — локальное перекрытие
  заголовка только в этом widget'е, canon DEFAULT_TITLES не трогаем
  (другие потребители WalletCard могут использовать общий «Кошелёк»).
- DataRow «Минимальный неснижаемый остаток» под grid'ом программ
  (только когда session.participantAccount.minimum_amount > 0)
  с hint «Возвращается пайщику при выходе из кооператива».
- Источник остатка — session.participantAccount?.minimum_amount,
  как и в legacy WalletWidget.

Note: locked-line уже рендерится самим canon WalletCard, когда
locked-balance > 0 (logic: hasBlocked ? blocked.amount : undefined
в WalletProgramWidget). Отдельная разметка под «Заблокировано» не нужна.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:30 +00:00
coopops c824b2147e [E14][@dark-sun] fix(WalletProgramWidget): vue-tsc — flatMap вместо filter+predicate
Why: type predicate `e is CanonProgramEntry` не сходился — optional `locked?: string`
в interface vs required `locked: string | undefined` в литерале map'а. TS считает
эти типы несовместимыми для predicate, хотя они эквивалентны при присваивании.

What: переход на `flatMap<CanonProgramEntry>` с возвратом `[]` для исключаемых
программ. Predicate не нужен, generic flatMap даёт точный тип результата.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:29 +00:00
coopops df13f30788 [E14][@dark-sun] refactor(participant/wallet): WalletPage + WalletProgramWidget в канон MONO Platform v2
Why: первая страница стола пайщика (default route 'wallet') использовала legacy
обвязку — CardStyles import, scoped SCSS с .body--dark и hardcoded rgba цветами,
ColorCard с произвольным цветом по индексу. Канон уже знает программы платформы
(blagorost/wallet/generator) через WalletCard + токены --prog-*; на нём и строим.

What:
- WalletPage.vue: убран import 'src/shared/ui/CardStyles', scoped SCSS на
  токенах --p-6/--p-4; вырезана легаси-карточка «Минимальный остаток»
  (она не отображалась — не было разметки в template). useHeaderActions
  для Deposit/Withdraw оставлен — канон поддерживает actions через #actions slot.
- WalletProgramWidget.vue: переписан на canon WalletCard + EmptyState.
  Фильтр на канон-набор программ через ZEUS_TO_CANON
  (MAIN→wallet, BLAGOROST→blagorost, GENERATOR→generator);
  MARKETPLACE и прочие исключены — это не из основной тройки платформы
  и должны рендериться отдельным виджетом.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:18:29 +00:00
coopops 077e716407 [E11][@dark-sun] feat(AppHeader): brand slot — лого + название кооператива на странице без логина
Why: на главную без логина нужно показывать бренд кооператива (логотип + имя)
рядом с глобальными действиями шапки; раньше выводилось только текстовое
название через title-проп без логотипа.

What:
- AppHeader.vue: опциональный slot #brand перед .topbar__crumb;
  hasBrand computed по slots.brand. Когда slot заполнен — крошка не рендерится.
- components.css: стили .topbar__brand (desktop + .topbar--mobile вариант)
  с canon-токенами (--p-fs-body, --p-ink, --p-fs-meta).
- CommonHeader.vue: на !loggedIn заполняет #brand src/assets/logo.svg
  + <b>{coopTitle}</b>; передача title-пропа убрана.
- default.vue: .fixed-top-right { top: 51px } → top: var(--p-topbar-h)
  (canon 56px) — выравнивание FAB под точную высоту шапки.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:17:08 +00:00
coopops 5b24a78237 [E11][@dark-sun] refactor(CommandPalette): иерархия рабочих столов и страниц вместо generic pages/actions/recent
Существующая модель cmdk в проекте (entities/CmdkMenu/model/store.ts) —
иерархия рабочих столов и их страниц. Активный стол sticky сверху с
бейджем «Активный», без запроса показывается иерархия (стол + indented
страницы), с запросом — плоский список со столом-префиксом у каждой
страницы; стол отдельной строкой появляется только если запрос явно
начинается с его имени или содержит «стол»/«workspace».

Переписал canon CommandPalette под эту модель:
- Props: `workspaces: CommandPaletteWorkspace[]` вместо
  `commands: CommandItem[]`. Каждый workspace = `{ name, title, icon,
  isActive?, pages: CommandPalettePage[] }`. Page = `{ name, title,
  icon?, shortcut? }`.
- Emits: `select-workspace(name)`, `select-page(workspaceName, pageName)`
  — props-only, навигация и filtering по ролям/conditions остаются
  заботой connected-обёртки (миграция legacy CmdkMenu — отдельная story).
- Sticky-баннер активного стола, plus accent-soft фон + outline-обводка
  на selected, ↑↓ работает плоско поверх иерархии (стол → страницы → стол
  → страницы).

Mock-data в /_dev/ui/index.vue обновлён: три стола (Председатель/Пайщик/
Отчётность) со своими страницами вместо плоского списка команд.

Старый widgets/Desktop/CmdkMenu пока живёт параллельно — переключим в
ходе миграции Wave 3.
2026-05-27 09:17:08 +00:00
coopops 459888fea7 [E11][@dark-sun] feat: NotificationCenter, CommandPalette, DetailsDrawer — эпик «Навигация и нотификации»
Реализованы три props-only доменных компонента из E11:
- NotificationCenter — panel-content для popover в шапке: группировка
  notifications по category (system/financial/voting/message), unread-bullet
  через BaseBadge, кнопка «Прочитать все», EmptyState и «Показать все».
  Relative-date форматирование с русским склонением.
- CommandPalette — ⌘K/Ctrl+K с fuzzy-поиском, секции recent/pages/actions,
  ↑↓ навигация, Enter/Esc обработка. localStorage недавних — в connected
  обёртке, компонент props-only.
- DetailsDrawer — side-sheet справа 480px (override через :width), slots
  default/actions/footer, Esc и backdrop close, на xs — fullscreen.

Все три зарегистрированы в boot/ui.ts и локально импортированы в /_dev/ui
с mock-data в секциях 36-38.

E11.4 RailUserCard отложен из-за конфликта имён — существующий компонент
имеет другую роль; нужно согласовать канон-нейминг.
2026-05-27 09:17:08 +00:00
coopops 4c6a21a965 [E10][@dark-sun] fix(BaseDocument): центрировать spinner-лоадер по обеим осям
Заменил `.full-width.text-center` + flex без выравнивания на flex-column
со `min-height: 360px` — лоадер больше не прижат к левому верху карточки.
Заодно поправил опечатку «подговка» → «Формируем документ».
2026-05-27 09:17:08 +00:00
coopops 039ad58055 [E10][@dark-sun] fix(AmountInput): нативный :suffix='symbol' вместо #append slot
Корень: я положил символ валюты в #append slot своим <span>, в обход
встроенного prop suffix. Quasar имеет правила позиционирования именно
для родного .q-field__suffix (см. revert d0a8dc0080 от 2026-05-19,
где было решено оставить Quasar дефолт — «нас устраивает»). Мой span
в append-slot не подчинялся этим правилам и сидел в произвольной
позиции относительно цифр.

Фикс:
- :suffix='symbol' — символ валюты идёт нативным механизмом Quasar.
- Удалил .amount-input__symbol класс и template #append вообще.
- Удалил override align-items: center на q-field__control/__append/
  __after (тоже мешал, как было показано в d0a8dc0080).
- font-weight 500 на цифрах оставил — нормальный вес поля.

Совет на будущее: использовать встроенные q-input props (suffix,
prefix), а не #append/#prepend slots, если можно — Quasar для них
держит готовое выравнивание, проверенное пользователем.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:17:08 +00:00
coopops dac113f9f0 [E10][@dark-sun] fix(AmountInput): выровнять цифры и RUB-суффикс по центру
Корень: font-weight: 600 + tabular-nums + Quasar dense нативный input
имеют чуть смещённую baseline относительно append-слота, где сидит
RUB. Визуально цифры лежали ниже суффикса.

Фикс:
- font-weight 600 → 500 (нормальный вес поля ввода, без bold-акцента
  на цифрах).
- Явный font-size + line-height на нативном input.
- align-items: center на q-field__control / __append / __after, чтобы
  суффикс и any after-слот (кнопка «макс») центрировались по высоте
  входной полосы.
- align-self: center на самом __symbol — на случай если q-field__append
  кто-то переопределит как stretch.

TODO на следующий подход: BaseDocument loader («Формируем документ…»)
выравнивать по центру рамки документа (сейчас стоит сверху).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:17:08 +00:00
coopops 7425c66e2a [E10][@dark-sun] fix(OtpInput): убрать TypeScript-касты as из pug-шаблона
[Quasar] boot error: SyntaxError: Unexpected identifier 'as' — runtime-парсер
обрабатывает выражения в pug-template как чистый JS, без TypeScript. Касты
вида `el as HTMLInputElement | null`, `e as InputEvent`, `e as KeyboardEvent`
прямо в атрибутах `:ref` / `@input` / `@keydown` — синтаксическая ошибка во
время бутстрапа Vue, из-за которой /_dev/ui целиком не грузился.

Фикс:
- :ref='(el) => setRef(idx, el)' + функция setRef(idx, el: Element |
  ComponentPublicInstance | null) с кастом в TS-скрипте.
- @input='(e) => onInput(idx, e)' + сигнатура onInput(idx, event: Event)
  с внутренним кастом target.
- @keydown оставил передавать event «как есть» — KeyboardEvent — никакой
  cast не нужен.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:17:08 +00:00
coopops 88a8f54806 [E10][@dark-sun] fix(_dev/ui): локальный импорт E10-компонентов — boot/ui.ts не HMR'ится
Quasar boot-файлы подхватываются только при рестарте dev-сервера. После
коммита 1af0f9c06f глобальные регистрации AmountInput/OtpInput/FilterBar/
FileUploader/VerticalStepper не подтянулись на лету — теги рендерились как
unknown components (пусто внутри секций 31–35 на /_dev/ui).

Фикс: добавил локальные импорты прямо в script setup _dev/ui/index.vue —
тот же паттерн, что у WalletCard, RailUserCard, AuthCard. Глобальная
регистрация в boot/ui.ts остаётся для боевого использования из других
страниц (после следующего рестарта dev'а).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:17:08 +00:00
coopops 378aa664a4 [E10][@dark-sun] feat: AmountInput, OtpInput, FilterBar, FileUploader, VerticalStepper — эпик «Формы и ввод»
Пять props-only доменных компонентов из shared/ui/domain/:
- AmountInput — денежный ввод с символом валюты, форматированием тысячных,
  precision из marketplace asset config, кнопкой «макс» по balance, опциональной
  подписью баланса. Tabular-nums, right-align, font-weight 600.
- OtpInput — 6 ячеек с автопереходом фокуса, Backspace откатывает на
  предыдущую, paste 6-значного кода распределяется по ячейкам.
  Регулярка /^\d$/, состояния error/disabled.
- FilterBar — search (debounce 300мс) + dropdown-фильтры + chip'ы активных
  значений с remove + «сбросить всё». v-model для values, v-model:search для
  поиска. Активные chip'ы рендерятся под рядом фильтров.
- FileUploader — drag&drop + клик по зоне; валидация accept/maxSize/maxFiles
  → emit error; список загруженных с иконкой типа, именем, размером,
  кнопкой ×; слот progress для connected-обёртки.
- VerticalStepper — состояния pending/current/completed/error; completed
  кликабельны для возврата назад (опционально); опциональные/disabled шаги;
  слот active под телом текущего шага.

Все компоненты — pug, canon-токены --p-*, без store/router/api. Демо-секции
31–35 на /_dev/ui (наш стенд-витрина).

Зарегистрированы в boot/ui.ts и shared/ui/domain/index.ts. ESLint точечно
прошёл, vue-tsc не запускался полностью на desktop (запрет).

Wave 2 / E10 закрыт.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:17:08 +00:00
coopops 211c17f47c fix(DocumentPreview): убрать txt/image — наши документы это HTML/PDF
Удалены типы 'txt' и 'image' из DocumentPreviewType + соответствующие
ветки рендера (pre.document-preview__txt и img.document-preview__image)
и стили. В реальных потоках платформы документ кооператива всегда
HTML (рендерится в ShadowHtml внутри BaseDocument) или PDF — plain-
text фрагмент в моноширинном pre не используется и выглядит как
техническая ерунда. Аналогично image.

_dev: удалён previewTxtDemo (мусорный EOSIO chain id из debugging
notes → перешитый в фрагмент протокола → теперь полностью убран
по требованию пользователя). Секция 30 показывает только HTML +
loading + error состояния.
2026-05-27 09:17:08 +00:00
coopops 8c5a7bd466 fix(BaseDocument,_dev): vue-tsc, мусорный txt-demo, ширина документа
vue-tsc:
- _dev: убрал txId/explorerUrl из signatureSignedDemo — оба поля
  удалены из Signature ранее (общего эксплорера нет).
- BaseDocument: canonSignatures.map — нормализую is_valid через
  '?? undefined' (бэкенд может вернуть null, canon-компонент ждёт
  boolean | undefined).

DocumentPreview txt demo: вместо мусорного 'EOSIO chain id …
dirty window' (мой случайный кусок из debugging notes) — фрагмент
протокола собрания пайщиков ПК «Восход».

ComplexDocument: .col-md-7 → .col-md-10. Это контейнер, в котором
лежит BaseDocument в реальных страницах документов. 7/12 = 58%
ширины было визуально 'приплюснуто'.
2026-05-27 09:17:08 +00:00
coopops db80cf0362 feat(ui): DocumentSignatures + перевод BaseDocument на canon
Реальный формат подписей кооперативного документа — это не «pending/
signed/rejected» из абстрактного SignatureCard, а IDocumentAggregate с
полями doc_hash + signatures[] (signer_certificate, public_key,
signature, is_valid). Каждая подпись разворачивается в детали.

Что сделано:
- Новый canon-компонент DocumentSignatures (story 9.5) в shared/ui/
  domain — props-only, принимает уже резолвнутые signerName и hash-
  совпадение, эмитит download/verify. Поверх Quasar — собственный
  expand на ref<Set<number>>, чтобы стиль был полностью canon.
- BaseDocument теперь рендерит DocumentSignatures вместо своего
  q-card.verify-card + q-list + q-expansion-item на teal/red badges.
  Адаптер canonSignatures маппит signer_certificate → ФИО через
  getNameFromCertificate, чтобы canon-компонент не знал про сертификаты.
- SignatureCard: удалены поля txId/explorerUrl и ссылка «Открыть в
  explorer» — общего эксплорера в платформе нет.

Demo: секция 29 → DocumentSignatures (валидный + с битой подписью),
секция 30 → DocumentPreview (HTML заголовок сокращён, чтобы не
обрывался при узкой колонке).
2026-05-27 09:17:08 +00:00
coopops 7524fd6941 feat(ui): волна 2 / E9 — документы и подписи
4 props-only canon-компонента в src/shared/ui/domain/, регистрация
в boot/ui.ts, demo-секции 26-29 в _dev/ui:

- DocumentRow (story 9.1) — строка документа в списке: иконка типа
  с tint'ом (pdf neg-soft, docx info-soft, html primary-soft), title,
  status через BaseBadge, дата/автор/описание, slot actions, emit open.
- SignatureCard (story 9.3) — подписавший (Avatar+ФИО+AccountBadge),
  статус (BaseChip pending/signed/rejected), для signed — хеш в моно-
  блоке + ссылка на explorer, для rejected — BaseBanner с причиной.
- ActivityTimeline (story 9.4) — вертикальный таймлайн событий с
  цветными иконками по типу (sign/reject/create/update/comment/
  transfer), groupByDate группирует по «Сегодня/Вчера/конкретная
  дата».
- DocumentPreview (story 9.2) — html (через DOMPurify), pdf через
  iframe, image через img, txt через pre. Стейты loading/error.

Все props-only, темо-зависимые значения только через --p-* токены.
2026-05-27 09:17:08 +00:00
coopops 29cd2cd5e5 fix(_dev/ui): «Член кооператива» → «Пайщик» в PersonCard demo
Канон-терминология blago/ЦК: пайщик, не член. Прецедент закреплён
в auto-memory feedback_kooperatsiya_pai.
2026-05-27 09:17:08 +00:00
coopops cfec8afe70 fix(IdentityPanel): compact тоже в карточке — padding+border+bg
Раньше compact-вариант рендерился без обрамления — avatar упирался
в левый край контейнера, визуально «проваливался» из общего стека
full-карточек (Screenshot_2026-05-21_21-51-09).

Compact теперь: padding 8/12px (тоньше чем full 16px), тот же border
+ surface + radius. В одну строку avatar + ФИО + AccountBadge + status,
но визуально это всё-таки карточка, не голая строка.
2026-05-27 09:17:08 +00:00
coopops 9f24394398 fix(ContactSheet): label и значение в отдельных строках
Body раньше был обычным block-контейнером, а .head и .value-row —
оба display: inline-flex. Без явного block-форматирования inline-flex
элементы становятся inline и рендерились в одну строку. Визуально:
Emailivanov@example.ru, Телефон+7 (903)..., Telegram@ivanov — слипшиеся.

Body теперь display: flex + flex-direction: column, gap canon — 8px
в comfortable, 4px в compact. Head/value-row переведены на обычный
display: flex (вместо inline-flex), gap внутри сохранён.

Прецедент 2026-05-21 — Screenshot_2026-05-21_21-42-16, 21-42-31.
2026-05-27 09:17:08 +00:00
coopops 824fd3341e chore: убрать закоммиченный symlink node_modules 2026-05-27 09:17:07 +00:00
coopops cc05cd4ba3 fix(domain): E8 — copy-кнопки, отступы и compact-layout
- AccountBadge: copy-кнопка не сжимается (flex-shrink: 0), размер 20×20 + icon 14px — раньше 18×18 + 12px тонула рядом с текстом badge.
- DataRow: column-gap var(--p-3, 12px) → var(--p-5, 20px) + min-label-width 140 → 160px + padding-right на label. Между label и value был визуально слипшийся стык.
- ContactSheet: comfortable margin-top между label и value 2px → 8px (compact 4px). Раньше label «налипал» на значение.
- IdentityPanel compact: переделан в flat flex-row (avatar + ФИО + AccountBadge + status badge) — раньше grid с .body загонял имя и AccountBadge в две строки и avatar «уезжал» от имени. Spec 8.1 требует «только avatar + ФИО + EntityIdBadge в одну строку».
2026-05-27 09:17:07 +00:00
coopops 61e8bdf083 feat(ui): волна 2 / E8 — идентификация и контакты
5 props-only canon-компонентов в src/shared/ui/domain/, регистрация
в boot/ui.ts, demo-секции 21-25 в _dev/ui:

- AccountBadge (story 8.5) — on-chain account name в mono-шрифте,
  copyable + опциональный explorer link. Назван AccountBadge вместо
  EntityIdBadge: имя EntityIdBadge занято под numeric ID пайщика
  в capital. Decision зафиксирован в epics.md и planning-artifacts.
- DataRow (story 8.3) — `label: value` пара для реестровых карточек,
  mono-режим, copyable, slot value-override, hint.
- ContactSheet (story 8.2) — email/phone/address/tg/web с автоиконками,
  кликабельными mailto:/tel:/t.me ссылками, copy и verified-чек.
- IdentityPanel (story 8.1) — Avatar + ФИО + AccountBadge + статус
  (active/blocked/pending) + role + actions-slot; compact/full.
- PersonCard (story 8.4) — Avatar + ФИО + role + AccountBadge +
  ContactSheet + slot meta; compact/comfortable.

Все props-only: useStore/useRouter/useApi внутри запрещены. Темо-зависимые
значения только через --p-* токены, никаких body--dark селекторов.
2026-05-27 09:17:07 +00:00
coopops 98e82eeced fix(BaseCheckbox): приглушить заливку галочки через --p-primary-press
q-checkbox по умолчанию красит .q-checkbox__bg в --q-primary, который в
dark теме равен #2dd4bf — на маленьком квадрате 18px этот яркий бирюзовый
смотрится ядовито. Переопределяем заливку filled-состояния на
--p-primary-press (#134e4a light / #14b8a6 dark) — глубже и спокойнее,
чек-символ остаётся белым.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:17:07 +00:00
coopops c247eb169f fix(tokens): приглушить --p-accent + унифицировать .agreement-link
Старое значение --p-accent (#D84315 light / #FF7043 dark) — слишком
насыщенное, выглядит ядовито рядом с тёплой палитрой ink/canvas. Меняем
на тёплую терракоту: #B85C38 (light) / #E89472 (dark) — тот же warm-tone,
но без перевозбуждения красного канала.

.agreement-link (псевдо-ссылки на просмотр документа в согласиях) была
hardcoded в #1c64f2 (синий) light / #ff9f43 (жёлто-оранжевый) dark и не
совпадала со ссылкой «Устав кооператива». Унифицируем: все ссылки внутри
согласий теперь var(--p-accent), визиt-цвет тот же — без фиолета.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:17:07 +00:00
coopops 991ea1d299 fix(ReadStatement): чекбоксы согласий — выравнивание + gap + accent ссылка
BaseCheckbox block-вариант: убран собственный display:flex + align-items:
flex-start, который перебивал внутренний flex Quasar и сдвигал label вверх
относительно inner. Теперь Quasar сам центрирует inner на первой строке.
Добавлен padding-left: 8px на .q-checkbox__label — больше воздуха между
галочкой и текстом согласия.

ReadStatement: ссылка «Устав кооператива» — --p-primary → --p-accent
(тёплый оранжевый #D84315 / #FF7043 dark) как акцентный цвет палитры.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:17:07 +00:00
coopops 3e01d8c915 feat(UserDataForm): canon-стиль полей + тип аккаунта на BaseRadioCard
UserDataForm/UserDataForm.vue:
- Выбор типа аккаунта: три q-btn(glossy, height:75px) → три BaseRadioCard
  с title/description; больше нет teal-«кнопок-плиток»
- Возврат к выбору типа: flat q-btn → BaseButton(ghost) с arrow_back

Sub-forms (Individual / Entrepreneur / Organization + Create*):
- q-input/q-select: standout="bg-teal text-white" → outlined color='primary'
  (canon-tone, единый primary-акцент вместо устаревшего teal)
- .q-gutter-sm.q-mt-md → .user-data-stack (flex column, gap токены)
- OrganizationDataForm: кнопка «совпадает» — color='teal' → flat primary

Валидация q-form.validate() и :rules сохранены — это всё ещё q-input
с rules-массивом, только tone и контейнер переехали на canon-токены.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:17:07 +00:00
coopops 4348cd0ae7 feat(ui): BaseCheckbox + BaseRadioCard и миграция SignUp-шагов
- BaseCheckbox: обёртка q-checkbox с canon-стилями, block-вариант для длинных
  согласий (чекбокс прижат к верху, label многострочный)
- BaseRadioCard: карточка-опция с радио-индикатором справа, props title /
  description / meta + slot fallback'и; используется для выбора программы

Миграции SignUp:
- ReadStatement: q-checkbox → BaseCheckbox(block); согласия в .agreements-стек
- SelectProgram: q-list+q-radio → список BaseRadioCard
- GenerateAccount: q-input → BaseInput(readonly, mono) с copy в #append;
  q-checkbox → BaseCheckbox; автоселект через querySelector('input').select()
  вместо ref.select() (BaseInput не пробрасывает внутренний API q-input)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:17:07 +00:00
coopops 58530d9db8 fix(desktop): .ws-switcher__title padding-top 2px
Воздух над заголовком стола внутри WorkspaceSwitcher — заголовок не упирается
в верхнюю границу caption «ПК «ВОСХОД»».

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:17:07 +00:00
coopops 65b55d7e4b fix(desktop): .rail__top padding 16px 8px + убрать disabled-стиль у WorkspaceSwitcher
- .rail__top: padding 18px 20px → 16px 8px (унификация боковых отступов)
- WorkspaceSwitcher: убраны :disabled-атрибут и hover:not(:disabled) — у пайщика, которому доступен только один стол, кнопка выглядит как обычно (без not-allowed-cursor, без приглушённого цвета). Меню q-menu просто не открывается через v-if='workspaces.length > 1'. Тихое игнорирование клика вместо явного запрета.
2026-05-27 09:17:07 +00:00
coopops 7578d1cfd3 fix(desktop): WorkspaceSwitcher — фильтр столов по ролям + 3-строчный title
WorkspaceSwitcher показывал ВСЕ workspaceMenus, включая столы выше роли пользователя (например, «Стол председателя» видел рядовой пайщик), что приводило к ошибке access-denied при попытке переключения.

- Добавлена фильтрация по иерархии: chairman ⊇ member ⊇ user. Председатель видит столы любых ролей; член совета — user+member; пайщик — только user. Workspace без meta.roles (или с пустым массивом) — публичный для всех (та же логика, что в legacy WorkspaceMenu, плюс корректное наследование прав).
- Title допускает перенос на 3 строки (line-clamp 3) — «Стол вычислительных ресурсов» теперь умещается полностью
- Расширена кнопка switcher'а (margin сторон 8→4 в LeftDrawerMenu), даёт больше места под текст
2026-05-27 09:17:07 +00:00
coopops 7f2d809821 fix(desktop,signup): WorkspaceSwitcher polish + Bank toggle QR↔реквизиты
WorkspaceSwitcher:
- Brand-строка теперь собирается из vars.short_abbr + «vars.name» (получается «ПК «ВОСХОД»»). Раньше показывал только short_abbr — название кооператива было «потеряно»
- Title допускает перенос на 2 строки (line-clamp 2) — длинные названия типа «Стол вычислительных ресурсов» больше не обрезаются ellipsis'ом с одного слова
- Chevron приклеен к верху строки (align-self start) — корректно при двухстрочном title
- Меню workspace: canon padding (8/12) вокруг q-item, отступ между avatar-section и текстом — иконка и название больше не упираются в края
- Затемнение фона при открытом меню (rgba(9,9,11,0.32) overlay через Teleport в body) — меню больше не сливается с контентом за drawer'ом

Bank.vue (карточка платежа):
- Убрали q-expansion-item (раскрытие вниз внутри карточки выглядело перегружено)
- Кнопка-toggle: «Показать реквизиты» рядом с «Скачать QR». Клик переключает зону под сводкой между QR и списком 9 BaseInput'ов с copy-кнопками
- В режиме реквизитов: кнопки меняются на «Скопировать всё» (primary) + «Показать QR» (secondary)
- QR-canvas использует v-show чтобы сохранять состояние при toggle назад; реквизиты — v-if чтобы не держать BaseInput'ы в DOM
2026-05-27 09:17:07 +00:00
coopops 144cc901ff feat(desktop): WorkspaceSwitcher + фикс ReadStatement стилей + плоский Bank.vue
ReadStatement: backend-HTML генерирует с inline-стилями и Quasar text-h* классами, которые перебивали :deep правила. Усилили все ключевые селекторы !important + добавили охват .text-h1/h2/h3/h4/h5/h6 и .text-right (под мета-блок «УТВЕРЖДЕНО…»).

Bank.vue (карточка платежа):
- убрали отдельные surface-карточки вокруг сводки и QR (получалось «карточка-в-карточке-в-карточке»)
- сводка → плоский <dl> со строкой-разделителем снизу (border-bottom var(--p-line))
- QR → просто центрированный canvas без обёртки/фона/рамки; белый цвет внутри canvas — функциональное требование контрастного сканирования, не дизайн-выбор
- detail-expansion → без своего фона, только border-top/bottom

WorkspaceSwitcher (новый widget src/widgets/Desktop/WorkspaceSwitcher/):
- Карточка-кнопка в шапке drawer: иконка кооп + caption «ПК «ВОСХОД»» + bold «Стол совета» (текущий) + chevron
- Клик → q-menu со списком всех workspaceMenus (Стол пайщика, Стол совета, Стол председателя и т.д.); выбор → selectWorkspace + goToDefaultPage
- Подключён через #brand slot AppDrawer'а в LeftDrawerMenu, заменяет дефолтный brand-row
2026-05-27 09:17:07 +00:00
coopops 40492a40cd feat(signup): canon-типографика заявления + рефакторинг карточки платежа
ReadStatement (этап 6.1):
- Backend генерирует HTML документа — локально нормализуем через :deep
- h1 уменьшен с монструозного дефолта до canon h3 (20px), центрирован
- h2/h3, p, strong, ul/ol, table, hr — типографика по var(--p-*)
- мета-блок .approved/.meta-right (УТВЕРЖДЕНО…) — ink-2, мельче, плотнее
- Чекбоксы соглашений снизу не трогали (как просил пользователь)

Bank (этап 6.2 — карточка платежа PayWithProvider):
- QR-код вынесен в фокус: сначала сводка (получатель/сумма/назначение) → QR → действия → реквизиты для ручного перевода скрыты под q-expansion-item
- Полные 9 полей (ИНН/БИК/КПП/корр-счёт/...) переведены на BaseInput(readonly, mono) с copy-кнопкой в #append; раскрываются только при необходимости
- Кнопки «Скачать QR» (primary) и «Скопировать реквизиты» (secondary) — BaseButton
- Удалены: q-btn(push), case-mixed «скопировать реквизиты», стиль #qr через global селектор
2026-05-27 09:17:07 +00:00
coopops fd0609de19 feat(invite): этап 5 — миграция Invite на canon AuthCard + BaseInput/BaseButton
- InvitePage: row/col-обёртка → .invite-page flex-center + canon padding (как у SignInPage)
- Invite widget: AuthCard со штатными title/subtitle (вместо CAPS-заголовка с background-clip градиент-текста)
- BaseInput(mono, readonly) для приватного ключа — canon-оформление key/hash полей
- BaseButton(ghost) для копирования + BaseButton(primary) для «Установить ключ»
- Удалены: градиент-текст, semi-transparent blue/green tint карточки вокруг инструкции и чекбокса, hover translateY + box-shadow glow, custom border-radius/box-shadow для q-input и кнопок, мобильные @media-блоки (canon-токены и AuthCard сами адаптивны)
2026-05-27 09:17:07 +00:00
coopops 791c7e1707 fix(signup): canon-рамка для контейнера подписи вместо неонового glow
- .signature-container: 3px primary border + 3-слойный box-shadow glow → 1px dashed по var(--p-line-2), surface-2 фон, без свечения; на hover лишь окрашивается рамка
- .signature-hint: переехала в центр контейнера, canon ink-2 цвет, убран frosted-chip с blur/shadow — теперь спокойная метка «Оставьте собственноручную подпись в рамке», pointer-events: none пропускает клики на canvas
- min-height снижен 300→220, padding на canon-токены
2026-05-27 09:17:07 +00:00
coopops 71331854f0 fix(signup): отступ сверху, canon фон q-stepper, ширина email-поля
- SignUp.vue: добавлена обёртка .signup-page (padding + center + min-height) — как у SignInPage; раньше страница была прижата к шапке
- AuthCard maxWidth: 1000 → 720 — историческое 1000 для трёх-колоночной UserDataForm; для остальных шагов выглядит абсурдно широко
- q-stepper: убраны собственный фон/тень/padding (не «карточка в карточке»), линии-разделители переведены на var(--p-line)
- EmailInput: max-width поля 360px — email не должен растягиваться во всю ширину карточки
2026-05-27 09:17:07 +00:00
coopops 4a6af072ac fix(authcard): убрать оборванный barrel-экспорт после удаления старого AuthCard
vue-tsc ругался: shared/ui/index.ts:21 — Cannot find module './AuthCard'.
- shared/ui/index.ts: убран `export * from './AuthCard'`
- LoginRedirectPage.vue: импорт переключён на canon `shared/ui/domain/AuthCard`, CAPS-заголовок заменён на canon `title` prop
- Invite.vue: импорт переключён на canon (полная миграция Invite — этап 5)
2026-05-27 09:17:06 +00:00
coopops c418a544ac feat(signup): этап 4 — миграция SignUp на canon AuthCard + BaseButton/BaseInput
- canon `shared/ui/domain/AuthCard` переписан на pug
- SignUp.vue переключён со старого «глянцевого» AuthCard на canon (title prop вместо CAPS-заголовка)
- старый `shared/ui/AuthCard` удалён (использовался только в SignUp)
- 7 подэкранов SignUp (EmailInput, SetUserData, SelectProgram, GenerateAccount, SelectBranch, ReadStatement, SignStatement): q-btn → BaseButton (ghost для «назад», primary для «Продолжить»)
- EmailInput: q-input → BaseInput, валидация rules переведена на computed :error
- q-input в GenerateAccount оставлен из-за зависимости от ref.select()
2026-05-27 09:17:06 +00:00
coopops 5480c1cd04 content(brand): og-image из poster-logo-horizontal + новый длинный дескриптор
- og-image.png — взят brand-постер ~/blago/production/shared/poster-logo-horizontal.png,
  отресайзен до 1000px по ширине и центрирован на полотне 1200×630
  с белыми краями по вертикали. Шрифт/композиция родные бренда.
- description / og:description / twitter:description: «Цифровой Кооператив —
  система управления хозяйством. Регистрация пайщиков, заказы на поставку
  и приобретение имущества, собрания совета, общие собрания пайщиков,
  взаимные расчёты на блокчейне, автоматический документооборот и бухбаланс
  на основе простой электронной подписи.»
2026-05-27 09:17:06 +00:00
coopops fea2e32b59 content(brand): обновить превью и мета — система управления хозяйством на платформе «Благо»
- og-image.png перерисован: «Цифровой Кооператив» / «Система управления
  хозяйством» / «на платформе БЛАГО — кооперативной экономики для жизни» /
  «Регистрация пайщиков · заказы на поставку и приобретение имущества ·
  расчёты на блокчейне».
- description / og:description / twitter:description выровнены под этот
  же копирайт. Слово «взаимоотношения с пайщиками» убрано как слишком
  узкое — управление хозяйством включает в себя ещё заказы, реестры
  и документы.
2026-05-27 09:17:06 +00:00
coopops 265e2783af feat(brand): системный логотип ЦК + чистка ассетов + OG-превью
- src/assets/logo.svg, public/favicon.svg, public/logo.svg — единая SVG
  логотипа кооператива (path с «ц» внутри). Скачано с
  https://цифровой-кооператив.рф из шапки сайта.
- src/assets/logo.svg: fill -> currentColor чтобы лого внутри
  canon .rail__brand наследовал --p-primary (для тёмной темы тоже).
- LeftDrawerMenu подключает лого через slot #brand-icon AppDrawer
  (импорт ?raw + v-html). Внутри зелёного квадрата шапки рейла теперь
  наш системный знак, а не дефолтный q-icon "dashboard".
- index.html: favicon → /favicon.svg, apple-touch-icon тоже на SVG.
  Удалены неработающие ссылки на /favicon.ico и /apple-touch-icon-*.png.
- index.html: добавлены description + расширенные og:* и twitter:*
  с конкретным описанием и og-image (1200×630), чтобы ссылки в чатах
  выглядели прилично.
- public/og-image.png — превью 1200×630, ImageMagick-сгенерированный
  композит из логотипа + текста.
- Удалены неиспользуемые ассеты:
  - public/icons/, public/pwa/ — старые favicon/PWA-PNG'и всех размеров;
  - src/assets/* — 40 легаси SVG/PNG (anime, blockchain, dacom*, flow*,
    header-logo, quasar-logo-vertical, system*, club*, welcome.jpeg и т.д.).
  Оставлены только src/assets/pin.svg (используется в Map.vue) и
  src/assets/logo.svg (новый).
2026-05-27 09:17:06 +00:00
coopops 41dcd40098 fix(layout): убрать TS-каст из pug-шаблона AppDrawer
Pug-template передаёт выражения в Vue compiler как строки; конструкции
вида (entry as RailItem).route не превращаются в JS и падают
SyntaxError в браузере: «Unexpected identifier 'as'».

Касты унесены в <script>: добавлены computed flatItems и
normalizedGroups, шаблон работает с уже типизированными массивами без
inline-TS.
2026-05-27 09:17:06 +00:00
coopops c3649ca972 fix(layout): этап 3.3 — все canon-компоненты на pug + фикс импорта cmdk
- LeftDrawerMenu: импорт useCmdkStore → useCmdkMenuStore (правильное имя).
  Без этого ломался ESM build всего drawer'а: «no exported member
  useCmdkStore» — RailUserCard не монтировался, поэтому и chevron свёртки
  «не работал», и Найти/Пополнить не реагировали.
- AppDrawer.vue, AppHeader.vue, RailUserCard.vue переведены на pug
  (lang="pug"). В проекте везде pug — никакого исторического HTML в
  canon-компонентах быть не должно.
2026-05-27 09:17:06 +00:00
coopops abd634beca fix(layout): этап 3.2 — pug + v-model:collapsed + рабочие триггеры
- q-drawer width 240 → 248px (совпадает с canon --p-rail-w, кошелёк больше
  не уплывает справа из-за обрезки на 8px)
- LeftDrawerMenu: убран весь :deep override на .rail__usercard/.rail__signout,
  возвращаем canon margin/padding как есть
- LeftDrawerMenu: добавлен v-model:collapsed на RailUserCard с сохранением
  в localStorage — chevron «свернуть/развернуть» снова работает
- LeftDrawerMenu: кнопка Найти теперь дёргает cmdkStore.openDialog() напрямую
  (фейковый KeyboardEvent не доходил до глобального обработчика)
- LeftDrawerMenu: «Пополнить» → useDepositDialog().open(); DepositButton +
  WithdrawButton рендерятся скрыто как держатели q-dialog (q-portal в body)
- Header.vue и LeftDrawerMenu.vue переписаны на pug (как остальной проект)
2026-05-27 09:17:06 +00:00
coopops 26a25e1131 fix(layout): этап 3.1 — RailUserCard в footer + canvas-фон + отступы рейла
- AppDrawer: плоский items оборачивается в .rail__nav, появляется gap 4-8px до cmdk
- cmdk margin 16→8px, чтоб выровнять с rail__nav (8px от стенки)
- LeftDrawerMenu footer: WalletCardMini+LogoutButton → один canon RailUserCard
  (avatar+balance в primary-soft + Пополнить + встроенный signout)
- :deep override на .rail__usercard margin 16→12/8 и .rail__signout padding 12/20→12/12
- quasar-canon: body--light/dark + q-layout/q-page-container/q-page красятся
  через --p-canvas, чтобы фон страницы соответствовал шапке и рейлу
2026-05-27 09:17:06 +00:00
coopops e7a352af22 feat(layout): этап 3 — WalletCardMini + canon LogoutButton в footer AppDrawer
LeftDrawerMenu:
- legacy `MicroWallet` (ColorCard teal с большой карточкой пайщика + ИП-badge
  + Deposit/Withdraw micro-кнопки) → canon `WalletCardMini` из
  widgets/wallet-card-mini (тонкая карточка из shared/ui/domain/WalletCard
  compact-variant, читает walletStore сама).
- Убран toggle «свернуть/развернуть нижнюю секцию» — нижняя секция теперь
  всегда видна (короткий компактный блок: WalletCardMini + Выйти).
- Удалены неиспользуемые `onMounted`/`ref` импорты, slide-анимация и CSS
  toggle-кнопки.

LogoutButton:
- pug + q-item с красной полупрозрачной плашкой → плоский ghost-button в
  стиле rail-пункта AppDrawer: прозрачный фон в покое, на hover —
  `--p-neg-soft` фон + `--p-neg` текст/иконка.
- Из uppercase «ВЫЙТИ» в title-case «Выйти» (canon — никаких uppercase).

Визуально левый drawer теперь полностью в canon-семье: единый rail с
пунктами + поиском + компактным wallet-балансом + ghost-кнопкой выхода.

Закрывает этап 3 из 5 в волне «приземление DS на реальный layout».
Дальше — SignUp (этап 4) и Invite (этап 5).
2026-05-27 09:17:06 +00:00
coopops b524374b23 feat(layout): этап 2 — AppHeader в реальном layout (заменяет MainHeader)
widgets/Header/CommonHeader/Header.vue теперь использует canon AppHeader
из shared/ui/layout. Высота шапки 56px (--p-topbar-h), border-bottom
hairline, без shadow и фоновых градиентов.

Слоты:
- #crumb: BackButton (для авторизованных), либо title с названием
  кооператива из system.info.vars для гостей/install.
- #actions: headerActions injection через useHeaderActionsReader (всё что
  страницы инжектят — Deposit/Withdraw, SettingsDropdown и пр. — рендерим
  как было).
- #notifications: NotificationCenter (только loggedIn + isClient).
- #theme: ToogleDarkLight — пока legacy, сохраняет storage + PWA-цвет;
  наш canon ThemeToggle их не делает (привяжу позже отдельным эпиком).
- #profile: BaseButton primary для гостей — Регистрация/Вход (в зависимости
  от текущего route).

Удалены:
- MainHeader.vue (вся логика scroll arrows + carousel actions group ушла —
  canon `.topbar__actions { gap: 8px }` достаточно; overflow-scroll
  будет вернут отдельно если потребуется на узких экранах).
- HeaderStyles.scss (стили scroll-arrow и q-toolbar overrides больше
  не нужны — canon-разметка через `<header class="topbar">`).

q-header Quasar остаётся wrapper'ом (sticky-poзиционирование в q-layout),
внутри — AppHeader даёт canon-разметку.
2026-05-27 09:17:06 +00:00
coopops 8280cb938e feat(layout): этап 1 — AppDrawer в реальном layout (заменяет LeftDrawerMenu)
Переписан widgets/Desktop/LeftDrawerMenu/LeftDrawerMenu.vue: внутри теперь
canon `AppDrawer` из shared/ui/layout с rail-видом MONO v2 (логотип ПК +
короткое имя кооператива в шапке рейла, ⌘K-поиск, пункты-router-link'и
с иконкой/бэйджем, sticky-footer).

Адаптер store→canon:
- items: desktop.activeSecondLevelRoutes → RailItem[], с прежней
  фильтрацией по roles + meta.conditions + meta.hidden (один-к-одному
  логика из SecondLevelMenuList).
- activeKey: вычисляется из router.currentRoute с поддержкой группового
  паттерна project-* → projects-list.
- @select: router.push({name, params:{coopname}}) или
  actionsStore.executeAction(meta.action), закрытие drawer на mobile.
- @cmdk: эмиттим keyboard event ⌘K → CmdkMenu (он смонтирован глобально
  в default.vue) подхватывает.

Footer-слот: пока сохранён legacy MicroWallet + LogoutButton + toggle
свернуть/развернуть (заменим на WalletCardMini в этапе 3).

default.vue: q-drawer width 200 → 240 (canon-ширина rail'а).

Что НЕ затронуто этим коммитом: верхняя шапка (legacy MainHeader),
правый drawer для actions, q-footer для гостей. Их меняем отдельными
этапами 2–3.
2026-05-27 09:17:06 +00:00
coopops b327f1b103 fix(ui): убрать конфликт с Quasar — .row → .u-row
Корневая причина «оторванной» галочки в ResetKeyForm: я ранее задал в
mono-platform/components.css глобальное `.row { display: flex;
align-items: center; gap: 12px; }`. Quasar внутри `q-checkbox` (а также
q-radio, q-toggle, q-btn-toggle и др.) сам добавляет class="row" на
корень компонента и использует его как inline-flex — мой `gap: 12px`
раздвигал `.q-checkbox__inner` и `.q-checkbox__label` на 12px по всему
приложению.

Решение:
- Переименовать утилиту `.row` → `.u-row` (та же семантика, тот же набор
  модификаторов --wrap, --gap-2/4/6). У Quasar теперь свободный .row.
- Обновить все 6 использований в /_dev/ui.
- Откатить предыдущий :deep(.q-checkbox__label) padding-left:4px workaround
  в ResetKeyForm — лечил симптом, причина устранена.

Затрагивает только нашу dev-витрину и canon-utility; legacy pug-виджеты
с `.row.justify-center` продолжают работать через стандартный Quasar
flex-grid класс.
2026-05-27 09:17:06 +00:00
coopops 69e71871e8 fix(ui): ResetKeyForm — сжать зазор между галочкой и текстом «Я сохранил ключ»
Quasar по умолчанию даёт ~12–16px padding на .q-checkbox__label, в
compact-форме сохранения ключа это смотрелось «оторвано». Уменьшаем до 4px
через :deep override на классе .rk-form__confirm.
2026-05-27 09:17:05 +00:00
coopops 05c9fdf072 feat(dev): секция 20 — ResetKey двухэтапный flow в /_dev/ui
Три кадра в одной секции: LostKey (ввод email), ResetKeyForm check-mail
(уведомление «письмо отправлено»), ResetKeyForm save-key (демонстрация
сохранения ключа с моковым сгенерированным аккаунтом). Submit на третьем
кадре регенерирует мок-ключ для повторной проверки.

Заменяет ошибочную секцию ChangeKey из предыдущей итерации.
2026-05-27 09:17:05 +00:00
coopops 1d8268b41e fix(ui): откатить ChangeKey + мигрировать настоящий ResetKey flow на base-компоненты
ChangeKey-фича (формы current/new/confirm WIF + успешный диалог) была
ошибочной интерпретацией E7. Реальный flow «сменить ключ» в продукте —
это **двухэтапный ResetKey**:

  1. LostKey: пайщик вводит email (он потерял ключ — текущего WIF нет),
     бэк присылает письмо со ссылкой `?token=...`.
  2. ResetKey: при заходе по ссылке клиент сам генерирует новый ключ
     прямо в браузере (через `useCreateUser().generateAccount()`),
     показывает приватный ключ readonly с «Скопировать», требует
     подтверждения «Я сохранил ключ» и вызывает on-chain
     `resetKey({token, public_key})`.

Текущий ключ ввести нельзя (его нет). Новый ключ ввести нельзя (только
генерируется). Никакого подтверждения нового ключа — он один и тот же.

Что сделано:
- Удалена ошибочная `features/User/ChangeKey/`.
- Старый `widgets/Registrator/ResetKey/ui/ResetKey.vue` (pug + сырые
  q-input/q-checkbox + кастомные градиентные стили) разбит на:
  * `ResetKeyForm.vue` — presentation (props-driven, режимы
    `check-mail` / `save-key`), без useRouter/useApi — пригоден для
    /_dev/ui витрины.
  * `ResetKey.vue` — connected обёртка, читает `route.query.token`,
    генерит account через `useCreateUser`, диспатчит `resetKey` через
    `useResetKey`, на успех → router.push на signin.
- Новая UI собрана на BaseForm + BaseInput readonly mono + BaseButton +
  BaseBanner + AuthCard.

/_dev/ui секция 20 теперь показывает три кадра подряд: LostKey (email
шаг), ResetKey check-mail, ResetKey save-key с моковым сгенерированным
ключом.

FR11, UX-DR9, UX-DR15.
2026-05-27 09:17:05 +00:00
coopops 2e7a6547fb fix(ui): BaseInput/BaseSelect — явный проброс слотов вместо v-for in $slots
Generic-проброс `<template v-for="(_, slotName) in $slots" #[slotName]>`
рушится в Quasar 2.19 рендере: при наличии scoped-слота #append вылетает
`Cannot read properties of null (reading 'key')` в renderSlot + Vue warns
по `_isVue`/`constructor` свойствам. Заменяем на явный список слотов
(prepend / append / before / after / hint у q-input; +option, selected-item
у q-select) — синтаксис, который Quasar держит корректно.

Кейс всплыл на ChangeKeyForm: #append с q-icon (toggle visibility) ронял
страницу /_dev/ui целиком.
2026-05-27 09:17:05 +00:00
coopops aa24f3d9b2 feat(dev): секция 20 — ChangeKey flow в /_dev/ui (визуальная проверка E7)
Подключение ChangeKeyForm + ChangeKeySuccessDialog к dev-витрине.
Submit запускает заглушку useChangeKey().changeKey() (≈600 мс), по успеху
открывает диалог с новым WIF. Generate генерирует валидный K1 ключ через
SDK и заполняет оба новых поля. Confirmed сбрасывает форму.
2026-05-27 09:17:05 +00:00
coopops a444648db1 feat(ui): E7.2 ChangeKeySuccessDialog — подтверждение через BaseDialog без backdrop-exit
Диалог успеха смены ключа: новый WIF в моноширинном блоке с copy-кнопкой,
BaseDialog с closeOnBackdrop=false, closeOnEscape=false, hideCloseButton=true.
Закрытие только через primary-кнопку «Я сохранил ключ»; до копирования
кнопка показывает guard-баннер вместо немедленного закрытия. По confirmed
эмитим событие — реальный flow подключит router.push на дашборд.

FR11, UX-DR11, UX-DR15. Story 7.2 из epics.md.
2026-05-27 09:17:05 +00:00
coopops 39a03c48eb feat(ui): E7.1 ChangeKeyForm — смена ключа на BaseForm + BaseInput + BaseBanner
Форма смены ключа пайщика на новых компонентах дизайн-системы.
BaseBanner severity=warn с предупреждением о потере, checklist последствий,
поля current/new/confirm WIF моноширинные с toggle visibility, ghost-кнопка
«Сгенерировать» через PrivateKey.generate из @wharfkit/session.

Бизнес-логика — заглушка с TODO на привязку on-chain change-key action
(pattern см. features/User/ResetKey/api) и TODO на OtpInput шаг подтверждения
(приходит в Эпике 10).

FR11, UX-DR9, UX-DR15. Story 7.1 из epics.md.
2026-05-27 09:17:05 +00:00
coopops d70ecb3878 fix(ui): Avatar tone — единая семантика neutral/primary/accent, убрать ink
Bug после rename --p-accent → --p-primary: класс .avatar--accent смотрел
на --p-primary-soft (canon teal), а должен был — на --p-accent-soft
(warm orange по новой семантике). И вариант ink (белый bg + dark text)
визуально выпадал из палитры — крайний xl-аватар на демо-витрине казался
«не пришей к звезде рукав».

Новая семантика AvatarTone:
- neutral (default) — серый surface-3 + ink-1 (нейтральный)
- primary — canon-teal soft + canon-teal text (основной выделение)
- accent — warm-orange soft + warm-orange text (редкое выделение,
  например подсветка пайщика по batch_id)

Удалён вариант ink — не использовался в реальном коде.
В dev-витрине xl-аватар: tone="ink" → tone="primary".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:17:05 +00:00
coopops ed93765fb0 revert(ui): убрать override центрирования suffix/prefix — оставить Quasar дефолт
Override (padding-top:0 + align-self:center на dense) не отрабатывал —
Quasar в dense держит prefix/suffix по baseline вводимого значения,
и пользователю это устраивает. Убираем неработающий override, чтобы
не мешался в дальнейших правках.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:17:05 +00:00
coopops a252fe00ec fix(ui): BaseDialog — отступ через margin-top второго ребёнка
Inline `style="margin:0"` на <p>-описании перебивал мой margin-bottom
через CSS (специфичность inline > selector). Переключаюсь на
`> :first-child + *` margin-top — отступ крепится ко второму ребёнку,
не зависит от inline-стилей первого.

Работает универсально: <p> + <BaseInput>, <p> + <BaseForm>, любая
другая пара «описание + контент».

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:17:05 +00:00
coopops 0fbf764437 feat(ui): --p-accent (warm orange) + suffix-центр + точная вёрстка gap
1. --p-accent (canon token): #D84315 (light) / #FF7043 (dark).
   Совпадает с legacy Quasar $accent (warm orange, Material Deep Orange).
   Проброшен в --q-accent через CSS var — legacy color="accent",
   var(--q-accent), text-accent продолжают работать без изменений,
   но теперь следуют за canon-темой (light/dark). Резерв под редкое
   выделение: ссылки batch_id и т.п.
2. quasar-canon.css: .q-field--dense .q-field__suffix/prefix —
   padding-top:0 + align-self:center. Quasar по умолчанию ставит
   padding-top: 24px под floating-label, отчего «RUB» визуально
   опускался к низу control'а. Теперь в одну линию со значением.
3. BaseForm: убран gap 12px из .base-form__body — reserve-hint-space
   у q-input даёт ~24px снизу под error/hint, дополнительный gap
   делал расстояние между инпутами неприятно большим.
4. BaseDialog: убран универсальный gap из body, оставлен margin-bottom
   только на первом <p>/.intro — между описанием и первым инпутом
   воздух нужен, между остальными детьми reserve-hint-space хватает.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:17:05 +00:00
coopops 5ed4b25634 fix: убрать случайный symlink node_modules из индекса
Промахнулся `git add -A` — захватил worktree-symlink node_modules
(он pnpm-shared с основным репо). .gitignore покрывает node_modules/
но симлинк-файл — отдельный путь.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:17:05 +00:00
coopops fa423c1caa refactor(ui): --p-accent → --p-primary — устранить конфликт с Quasar primary
Контекст: после проброса --q-primary = var(--p-primary) (4fddfe2343)
наш токен --p-accent и Quasar primary стали обозначать ОДИН и тот же
canon-teal — два имени для одного цвета это когнитивный шум, и при
росте платформы будет путать. Имя `accent` оставляем свободным под
будущий ВТОРОЙ цвет (warm-orange, для редкого выделения вроде ссылок
по batch_id) — это отдельная задача когда понадобится.

Переименовано везде (109 ссылок в 6 файлах):
- --p-accent          → --p-primary
- --p-accent-hover    → --p-primary-hover
- --p-accent-press    → --p-primary-press
- --p-accent-soft     → --p-primary-soft
- --p-accent-line     → --p-primary-line
- --p-accent-strong   → --p-primary-strong
- --p-ink-on-accent   → --p-ink-on-primary

Файлы: tokens.css, quasar-canon.css, components.css, RailUserCard,
AuthCard, _dev/ui (включая user-facing labels палитры «Accent → Primary»
и заголовок секции «Акцент → Основной цвет»).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:17:05 +00:00
coopops 23389a8a42 feat(ui): canon-цвет через --q-primary CSS var; вернуть dense + rounded
- tokens.css: проброс canon-палитры в Quasar brand-переменные
  (--q-primary/negative/positive/warning/info = соответствующие
  --p-* токены). Резолвится lazy → автоматически следует за темой.
  Теперь Quasar САМ красит focus q-field, q-btn color="primary",
  bg-primary utility-классы в canon-цвета без CSS-overrides;
- quasar-canon.css: убраны border-color overrides на ::before
  (default/focus/error) — Quasar управляет цветом через --q-primary
  и собственный механизм --focused/--highlighted. Оставлены только
  background, text-color и rounded углы (border-radius var(--p-r-sm));
- BaseInput/Select: вернули `dense` (компактный 40px) и явный
  `color="primary"` (canon teal через --q-primary).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:16:37 +00:00
coopops 07cfff6341 fix(ui): BaseDialog — уменьшить отступ между body и footer-кнопками
Между нижним инпутом и footer-кнопками был ~32px (q-card-section
padding-bottom 20 + q-card-actions padding-top 12) — выглядело
растянуто. Сводим к 16px (8+8) — кнопки сидят ближе к контенту.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:16:37 +00:00
coopops d58cecc55d fix(ui): q-field overrides — оставить только цвет, всё остальное дефолт Quasar
Регрессия мерцания границы и приплюснутости — следствие борьбы наших
overrides с встроенной Quasar анимацией. Возвращаем дефолт: Quasar
сам решает геометрию (height/padding/border-radius/font), анимации,
opacity ::before и focus-индикацию. Наша задача — только покрасить
в canon-цвета.

Удалено из .q-field--outlined:
- height/padding/border-radius/font-size/font-family overrides;
- transition border-color (Quasar сам делает свой transition);
- opacity:1 !important на ::before (был костыль от мерцания);
- box-shadow:none на --focused/--highlighted;
- display:none на ::after underline;
- override .q-field--dense;
- override .q-field--float .q-field__label (Quasar анимирует label сам);
- focus-shadow / outline reset на native input.

Оставлено (только цвет):
- background: var(--p-surface);
- border-color: line-1 / accent (focus) / neg (error);
- color текста, placeholder, label, prefix/suffix, hint/error.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:16:37 +00:00
coopops d32e63643c fix(ui): мерцание границы q-input на focus + unused import в BaseTable
- Quasar при focus анимирует opacity ::before (прячет старую границу
  перед показом ::after underline). Так как ::after мы спрятали,
  получался видимый gap ~200-400 ms «граница исчезла → появилась
  с подсветкой». Фикс: opacity:1 !important на ::before,
  transition только по border-color.
- BaseTable.vue: убран неиспользуемый импорт BaseTableColumn.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:16:37 +00:00
coopops a9e6487ee7 fix(ui): убрать dense из BaseInput/Select; gap в Dialog/Form body; wallet 17px
- BaseInput/BaseSelect: убран `dense` — возвращаемся к дефолтному
  outlined Quasar (control ~56px). Floating-label получает достаточно
  воздуха, текст значения визуально центрируется без сжатия;
- BaseDialog: `.base-dialog__body` теперь flex-column с gap 16px —
  описание/инпуты/таблицы внутри диалога получают единый воздух
  без правки потребителя (пример: «Паевой взнос: укажите сумму…» →
  отступ до инпута появляется автоматически);
- BaseForm: `.base-form__body` gap 4px → 12px — те же требования
  внутри формы;
- WalletCard: `.wallet__metric-val` 19→17px — итоговое значение
  для аккуратного баланс-блока FULL.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:16:37 +00:00
coopops 5462e59b47 fix(ui): убрать stack-label, двойную focus-подсветку и mini-wallet из шапки
1. Stack-label: BaseInput/BaseSelect → только `dense`. Quasar
   стандартно сам корректно держит floating-label в dense+outlined,
   без CSS-override метка работает по документации Quasar.
2. Двойная focus-подсветка: убран наш box-shadow var(--p-focus-ring)
   поверх Quasar внутренней focus-индикации (виден был как «белый
   ринг» на тёмной + наш teal). Теперь только border-color меняется
   через ::before (canon-минимализм). Дополнительно `box-shadow: none
   !important` на --focused/--highlighted control и `display:none` на
   ::after underline (на случай dense Quasar variants).
3. Mini-wallet в правом верхнем углу AppHeader: слот #wallet удалён
   из API (баланс уже отображается в RailUserCard в drawer'е,
   дубль в шапке избыточен). Убран из dev-витрины каркаса.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:16:37 +00:00
coopops a566b96609 fix(ui): WalletCard и mini-wallet — снизить визуальный вес цифр
Регрессия после Quasar wrapper-pivot: метрики кошельков выглядели
крупно по сравнению с заголовком/sub-label, а padding'и были
избыточны для compact-варианта.

- .wallet (FULL): padding 18→14px по верт, иконка 44→40px,
  metric-val 22→19px, ccy 13→12px;
- .wallet--row (compact): padding 14→10px, иконка 36→32px,
  metric-val 18→16px — выглядит как row в списке, а не как карточка;
- .rail__balance-val (mini-wallet в drawer): 22→18px — теперь не
  доминирует над identity-блоком пайщика.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:16:36 +00:00
coopops 80d1fe9e8f fix(ui): canon q-field/q-btn пропорции — убрать раздавливание floating-label
Корень регрессии: жёсткий height: 40px в .q-field--outlined раздавил
вертикальную разметку q-input (Quasar держит padding-top:24px под
floating-label слот; при сжатии текст значения сдвигается). Те же
эффекты для select.

- BaseInput/BaseSelect → dense + stack-label по умолчанию;
  Quasar сам корректно держит 40px в dense, метка статически стоит
  над инпутом без floating-механики;
- .q-field--outlined: убран принудительный height; .q-field--dense
  получает только padding-x, высоту контролирует Quasar;
- .q-btn: font-weight 500→450, min-height 40→36px, padding 16→14px —
  кнопки больше не выглядят «жирно/тяжело»;
- .q-btn--outline: hairline-граница через ::before (Quasar pattern),
  без двойной рамки;
- размеры sm/lg/dense пропорционально уменьшены;
- .q-chip: высота 24→26px, padding 10→12px, font-weight 500→450;
  + .q-chip__icon размер 14px (для точечных чипов).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:16:36 +00:00
coopops 7118328ba9 refactor(desktop/base): остальные base/ и AuthCard → обёртки Quasar
BaseSelect → q-select (outlined, reserve-hint-space, map-options/emit-value
для совместимости с {value,label} options API).

BaseCard → q-card flat с q-card-section head/body. Variants flat/inset/quiet
override-ят через scoped style.

BaseChip → q-chip с color mapping (variant → primary/positive/negative/
warning/info). Square для canon-look.

BaseBadge → q-badge с тем же color mapping. Dot-вариант через scoped style
(8×8 круг).

BaseBanner → q-banner rounded dense с border-left по variant.

BaseDialog → q-dialog + q-card с size→maxWidth mapping. closeOnBackdrop/
closeOnEscape пробрасываются через :persistent / :no-*-dismiss.

BaseTable → q-table с трансформацией columns в QTableProps['columns'].
Hide-pagination, binary-state-sort. Cell-slots работают через body-cell-{key}.

AuthCard (domain) — был на native canon `.card` div. Переведён на q-card flat
для консистентности. Усиленный shadow + accent-stripe сверху сохранены.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:16:36 +00:00
coopops 65db1b6660 refactor(desktop/base): BaseInput/Button/Form → обёртки Quasar
BaseInput теперь рендерит <q-input outlined reserve-hint-space no-error-icon>
с проброшенными slots. reserve-hint-space от Quasar резервирует место
под error/hint встроенно — раньше делали вручную через NBSP. Все Quasar
features (rules через v-bind \$attrs, mask, ref API, native slots) работают
прозрачно. Props API совместим (modelValue, label, hint, error, prefix,
suffix, mono, readonly, disabled, autocomplete, name, id, type).

BaseButton — q-btn с variant→Quasar mapping:
  primary → :unelevated color="primary" (заливка accent)
  secondary → :outline (border + surface)
  ghost → :flat (transparent)
  danger → :flat color="negative" (transparent + neg-cвет)
:no-caps :ripple=false для canon-look. Props API совместим.

BaseForm — q-form-обёртка с сводным error-баннером (q-banner) и slot footer.
banner—neg styling прилетает из quasar-canon.css через .bg-negative-soft.

Глобальная стилизация Quasar в quasar-canon.css даёт canon-look всем
этим компонентам и существующим q-input/q-btn/q-card по всему проекту.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:16:36 +00:00
coopops aba3fc53f0 feat(desktop/css): canon-стилизация Quasar поверх (quasar-canon.css)
Архитектурный pivot: вместо native HTML5-компонентов под canon-классами
стилизуем Quasar поверх. Причины:
- Сохраняется весь Quasar API из коробки (rules/mask/dense/slots/refs)
- Существующий код (сотни q-input, q-btn, q-card, q-select) автоматом
  получает canon-look без правок
- Меньше регрессий — Quasar отвечает за accessibility/keyboard/focus

Файл src/css/mono-platform/quasar-canon.css содержит overrides под canon
для: q-field--outlined (q-input/q-select), q-btn (unelevated/outline/flat),
q-card, q-chip, q-badge, q-banner, q-dialog/q-menu, q-item, q-table,
q-checkbox/q-toggle.

Подключен в quasar.config.cjs css[] после components.css. tokens.css
без изменений — все цвета/радиусы/тени тянутся как переменные.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:16:36 +00:00
coopops 096f4c4ecc fix(desktop): удалить legacy глобальный .card { transform: scale(1.03) }
Истинный источник «прыгалки» AuthCard на hover: глобальное правило
`.card:hover { transform: scale(1.03) }` в src/app/styles/style.css
(и его дубль в pages/Marketplace/MainPage/ui/MainPage.vue, тоже не scoped)
бьёт по любому элементу с классом card. AuthCard имеет корневой
<div class="card auth-card"> — каждый hover дёргает её на 3%.

Поиск показал: класс `card` без модификаторов в templates никто кроме
новых canon-компонентов (BaseCard, AuthCard) не использует. Этот глобал —
мёртвый legacy, удаляем безопасно.

NBSP-фикс layout-shift из предыдущего коммита (2a2d9a786a) остаётся
актуальным — он лечил другую (на уровне поля) проблему.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:16:36 +00:00
coopops 02fa115365 fix(desktop/ui): добить layout-shift, ужать gap полей, accent-стрип AuthCard, затемнить canvas
1) BaseInput — layout shift жив: text-interpolation `{{ error || hint || ' ' }}`
   рендерила обычный пробел, браузер схлопывает text-node с одним whitespace
   в 0-width content → div сжимается → форма прыгает. Заменили на
   `<span>&nbsp;</span>` (NBSP в DOM, не схлопывается).

2) BaseInput — canon `.field { margin-bottom: 16px }` создавал слишком
   большой разрыв между полями (16px margin + 10px form gap + 17px message =
   ~43px). Override `.field { margin-bottom: 0 }` в scoped — расстояние
   управляется gap'ом BaseForm.

3) AuthCard — статичный accent-стрип 3px сверху (`::before`). Визуальный
   якорь без анимации — даёт бренд-маркер «здесь главное».

4) tokens.css light — canvas #fafafa → #f4f4f5 (cool-gray), canvas-2
   #f4f4f5 → #ebebed. Surface остаётся #ffffff, теперь карточка чётче
   проступает на фоне.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:16:36 +00:00
coopops d8b5afe7e4 fix(desktop/ui): убрать layout shift в BaseInput и оживить AuthCard на светлой
BaseInput: контейнер field__message теперь рендерится всегда с min-height
calc(--p-fs-meta * 1.4) — раньше div появлялся/исчезал на каждом keystroke
при валидации (см. LostKey emailError), форма прыгала вверх-вниз.
Когда нет ни ошибки, ни хинта — рендерится NBSP, высота сохраняется.

AuthCard: добавлен box-shadow поверх canon-минималистского .card —
canvas #fafafa и surface #ffffff на светлой почти неразличимы, без тени
карточка выглядит плоской. На тёмной отдельный shadow усилен (поверх
canon `--p-shadow-card`, который оптимизирован под data-cards, не hero).
border-color подкручен с --p-line на --p-line-1 для большей чёткости.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:16:36 +00:00
coopops 324d4fdab5 feat(desktop/LostKey): E6 Story 6.3 — миграция «Перевыпуск ключа» на canon
Scope-adjustment: plan'овая Story 6.3 предполагала миграцию SignUp как
второго auth-flow, но Register — multi-step wizard (10+ файлов в
pages/Registrator/SignUp/), который требует VerticalStepper из Эпика 10
и не помещается в Волну 1. Вместо этого мигрируем LostKey — простую
одно-полевую auth-страницу, того же auth-домена.

LostKey (widget): pug → html, старый AuthCard (q-card с shimmer) →
canon AuthCard. q-input → BaseInput с inline email-валидацией;
q-btn → BaseButton; всё обёрнуто в BaseForm с loading/error.
Бизнес-логика (useLostKey.startResetKey + router redirect на resetkey)
сохранена 1:1.

LostKeyPage: footer-кнопка «Назад» через ghost BaseButton в footer-slot
AuthCard; центровка через flexbox.

Не мигрировано в Волне 1 (известный gap):
- SignUp wizard (требует VerticalStepper из E10)
- ResetKey / Invite (на старом AuthCard, мигрируются в Волне 2)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:16:36 +00:00
coopops d5dfc3d645 feat(desktop/SignIn): E6 Story 6.2 — миграция Login на canon-компоненты
LoginForm (feature): q-input → BaseInput, q-btn → BaseButton, обёрнут в
BaseForm с управлением loading/error. Бизнес-логика (useLoginUser, redirect,
OpenReplay tracking, NotificationPermissionDialog) сохранена 1:1.
Ошибки входа теперь показываются через сводный error BaseForm
(плюс legacy FailAlert остаётся как fallback).

SignIn (widget): импорт AuthCard переключён на новый canon-композит
(src/shared/ui/domain/AuthCard), убран pug-template и градиентные стили.
Добавлен footer-slot — пробрасывается в AuthCard.

SignInPage: переписан с pug → html, footer-кнопки «Потеряли ключ?» /
«Нет аккаунта?» вынесены в footer-slot AuthCard через BaseButton ghost-sm.
Центровка через flexbox вместо QRow grid.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:16:36 +00:00
coopops 8591edb942 feat(desktop/AuthCard): E6 Story 6.1 — AuthCard композит + AuthLayout
AuthCard (src/shared/ui/domain/AuthCard) — canon .card + center-aligned
head (title/subtitle/slot head) + body-slot + опциональный footer-slot.
Старый src/shared/ui/AuthCard (q-card с shimmer-градиентом) остаётся
для не-мигрированных страниц (SignUp wizard, ResetKey, Invite).

AuthLayout (src/app/layouts/AuthLayout.vue) — wrapper-обёртка для full-page
auth-страниц: фон var(--p-canvas), центрирование, тогл темы в правом верхнем.

Витрина: dev-секция 19 с примером login-формы (BaseForm + BaseInput ×2 +
BaseButton + footer-ссылки).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:16:36 +00:00
coopops 294480ee5a feat(desktop/widgets/wallet-card-mini): E5 Story 5.2 — connected-обёртка WalletCard
WalletCardMini подключает WalletCard к Pinia (useWalletStore.program_wallets) +
System store (info.symbols.root_govern_symbol) + useRouter (клик →
name: 'wallet'). Программный mapping: wallet→MAIN, blagorost→BLAGOROST,
generator→GENERATOR (Zeus.ProgramType). Loading определяется автоматически
по наличию данных в store (или пробрасывается через prop).

Подключено в слот wallet AppHeader на dev-витрине (секция 16 — каркас) +
выделенная секция 18 с тремя программами. На dev-странице покажет
skeleton-loading т.к. loadUserWallet не вызывался; на реальных страницах
после init процесса — настоящий баланс.

Со-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:16:36 +00:00
coopops e4af71873e fix(desktop/_dev/ui): корректные подписи программ Благорост/Генератор в витрине WalletCard
Благорост — капитализация РИД (не «накопления на жильё»), Генератор —
генерация РИД (не «доходы от программы»). Кошелёк остаётся «свободный
остаток». Черновые ярлыки риском попадают в production через копипасту.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:16:36 +00:00
coopops f76e2c72f5 feat(desktop/WalletCard): E5 Story 5.1 — props-only компонент с программными акцентами
Канонический .wallet с компактным (--row) и full-вариантами, программные
акценты через --prog-blagorost / --prog-wallet / --prog-generator (UX-DR20).
Витрина на /_dev/ui секция 17: три программы × оба варианта + loading +
empty + locked-line.

Compact (.wallet--row): 36px icon, 18px sum, 14px padding — для слота шапки.
Full (.wallet): 44px icon, 22px sum, 18px padding — для дашборда.

Connected-обёртка widgets/wallet-card-mini (Story 5.2) делается отдельным
коммитом, требует интеграции с реальным wallet-store и MARKETPLACE_ASSET_CONFIG.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:16:36 +00:00
coopops 816212cee0 revert(desktop/RailUserCard): убрать transition свёртки — baseline-skipping
Анимация max-height + opacity при свёртке давала видимый прыжок крупной
суммы баланса (`font-size: 22px` + `display: flex; align-items: baseline`):
во время промежуточных кадров baseline переcчитывался относительно
схлопывающегося контейнера → текст «прыгал в середину» и обратно.

Возвращаемся к canon `display: none`. Resize моментальный, без skipping.
Поведение rail (top опускается, bottom-chevron остаётся через flex-spacer)
работает и без анимации — пользователь это и подтвердил в фидбеке.

Chevron rotation через :deep(.q-icon) и balance-route не трогаем —
они независимы.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:16:35 +00:00
coopops 44f121a043 feat(desktop/_dev/ui): TokenPalette + chevron full-width fix
1. **Палитра системы.** Новая секция «00» в шапке dev-страницы со всеми
   основными --p-* токенами:
   - Акцент (accent / hover / press)
   - Поверхности (canvas / canvas-2 / surface / 2 / 3)
   - Текст (ink / ink-on-accent)
   - Линии (line / 1 / 2) — текстовый input для rgba
   - Статусы (pos / neg / warn / info)
   - Программные тинты (blagorost / wallet / generator)

   Color-picker для hex-токенов, текстовый ввод для rgba. Live-override
   через `document.documentElement.style.setProperty()` — inline-style на
   :root перебивает CSS-правила, перекрашивание мгновенное. Подсветка
   переопределённых токенов + счётчик в шапке + кнопка «Сбросить» (через
   removeProperty). Watch на Quasar Dark — при смене темы пикеры
   обновляются на новые resolved-значения.

2. **Chevron в свёрнутом виде — full-width.** Из-за того что для плавной
   анимации primary остаётся в DOM (max-height:0 вместо display:none),
   у него сохранялся `flex: 1` и он съедал ширину карточки → chevron не
   мог растянуться через canon-правило `width: 100%`. В `.is-collapsed`
   добавили `flex: 0 0 0` + `width: 0` для primary — chevron теперь
   корректно занимает всю ширину.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:16:35 +00:00
coopops fb43d9c2d0 fix(desktop/RailUserCard): chevron rotation, плавная свёртка, balance-route
По фидбеку:

1. **Chevron не переворачивается.** Canon-правило
   `.rail__usercard__collapse svg { transform: rotate(180deg) }` рассчитано
   на `<svg>`, а Quasar `<q-icon>` рендерит `<i class="q-icon">`. Прокидываем
   transition + rotate через scoped `:deep(.q-icon)` — chevron теперь крутится
   при свёртке/развёртке.

2. **«Карточка улетает»** — резкий jump при `display:none` создаёт визуальный
   прыжок. Подменяем display:none на `max-height: 0` + `opacity: 0` с
   transition'ом. Логика canon-селектора `.is-collapsed` не нарушается,
   просто добавлена плавность. Если структурное поведение rail (top
   опускается, bottom-chevron остаётся на месте) всё равно неудобно —
   перевёрстаем во вторую итерацию.

3. **Клик по балансу → маршрут кошелька.** Новый prop `balanceRoute` —
   если задан, блок `.rail__balance` оборачивается в `<router-link :to>`.
   Параллельно эмитим `balance-click` — для аналитики или функционального
   handler'а. На витрине привязал к `/_dev/ui#wallet` чтобы проверить.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:16:35 +00:00
coopops 0e60b5444a feat(desktop/domain): RailUserCard + фикс RouteMeta для dev-роута
1. `RouteMeta.icon` объявлен обязательным в `src/env.d.ts` — добавили
   `icon: 'fa-solid fa-flask'` для dev-роута, иначе vue-tsc валится.

2. `RailUserCard` (`shared/ui/domain/RailUserCard`) — доменный
   общеплатформенный компонент мини-кошелька в нижней части drawer'а.
   Это будущий E11 (Волна 2), но логично вытащить раньше, чтобы основной
   layout можно было заменить целиком уже сейчас.

   Реализован по canon строго: rail__usercard с usertop+balance+actions,
   collapse-chevron справа с поворотом arrow при свёртке (через
   `.is-collapsed` модификатор + canon CSS), отдельный `rail__signout`
   блок (выходит вне `.rail__usercard` — рендерится сразу после, в том
   же `aside.rail` через footer-slot).

   Контракт строго dumb по архитектуре: только props/emits/slots.
   - props: name, role, avatarSrc, balance, symbol, balanceLabel,
     lockedBalance, lockedLabel, primaryActionLabel, collapsed,
     showSignout, signoutLabel
   - emits: primary-action, update:collapsed (v-model), signout
   - slots: usertop-extra, actions

   Coнnected-обёртки (с инжектом store/auth) — на уровне страницы,
   не здесь.

3. Витрина `/_dev/ui`:
   - Секция 12 (AppDrawer): заменили временный огрызок rail-demo__user
     на полноценный `<RailUserCard show-signout>` через footer-slot.
   - Секция 16 (Каркас приложения): аналогично — теперь у rail полноценный
     footer.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:16:35 +00:00
coopops 9ebed71283 feat(desktop/layout): E4 — AppDrawer + AppHeader + PageHead + PageTabs
E4 «Layout-каркас» Волны 1: 4 структурные обёртки в `src/shared/ui/layout/`.
По архитектуре — только props/emits/slots, без store/router/api, без знания
о данных. Cтраницы передают menu-items, breadcrumbs, табы и активные ключи
сверху.

Состав:
- `AppDrawer` — canon `.rail`: solid surface, активный пункт soft-accent
  пилюлей + 2-px рейл слева. Поддерживает плоский список + секции
  (RailSection с eyebrow-меткой), opt-in ⌘K-кнопку, slot `footer` для
  user-card / logout.
- `AppHeader` — canon `.topbar`: бургер · crumb · `topbar__actions` ·
  `topbar__right`. Поддерживает простой title и multi-level breadcrumb.
  Глобальные действия через слоты `notifications`/`theme`/`wallet`/`profile`
  или общий `right`.
- `PageHead` — canon `.page-head`: eyebrow + title + subtitle + slot
  `actions`. Используется когда страница БЕЗ подстраниц.
- `PageTabs` — canon `.tabbar` + `.tab`: вкладки подстраниц с tab__count,
  опциональный hairline-разделитель и `tabbar__actions` справа.

Также в составе коммита:
- Убрали AuthCard из base — он композит (cap + body + head + footer),
  переедет в E6 Auth flow рядом с SignUp/ResetKey/Invite.
- BaseForm: gap полей 16→10px (тесные формы как в каноне).
- Витрина `/_dev/ui`: 4 новые секции (12-15) на каждый layout-компонент +
  секция 16 «Каркас приложения» — AppDrawer + AppHeader + PageTabs в живой
  связке без legacy QLayout/QDrawer.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:16:35 +00:00
coopops 1148eac2ed fix(desktop/mono-platform): убрать HTML-теги <style> из canon CSS
Скрипт извлечения из bundled HTML оставил `  <style>...` в начале и
`</style>` в конце обоих файлов. CSS-парсер ловил `<` как ошибочный
селектор и пропускал блок до ближайшей `}` — съедая весь первый блок
правил.

В tokens.css первым блоком был `:root, [data-theme="light"]` со светлой
палитрой — она целиком игнорировалась. `[data-theme="dark"]` ниже
парсился нормально, поэтому тёмная тема работала, а светлая — нет:
кнопки primary/ghost/danger оставались с transparent background и
сливались с canvas.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:16:35 +00:00
coopops e741b5646a feat(desktop): MONO Platform v2 — базовые компоненты дизайн-системы
Заносим эталонную дизайн-систему в проект — только базовые элементы для
дальнейшей компоновки. Source — MONO Design System.html.

Архитектура (по `_bmad-output/planning-artifacts/architecture.md`):
- `src/css/mono-platform/{tokens.css,components.css}` — canon токены и стили
  компонентов 1:1 из эталона; импортируются первыми в quasar.config css[].
- `src/shared/ui/base/<Имя>/{Vue,types,index}` — 14 базовых обёрток (Vue 3 +
  TS, native HTML над canon BEM, no Quasar-deps кроме Dark API в ThemeToggle).
  FSD-граница: только props/emits/slots, никаких store/router/api.
- `src/boot/ui.ts` — глобальная регистрация 14 базовых компонентов.
- `src/boot/theme.ts` — sync `html[data-theme]` ↔ Quasar Dark.
- Inter + JetBrains Mono в index.html.

14 базовых:
BaseButton, BaseInput, BaseSelect, BaseCard, BaseTable, BaseChip, BaseBadge,
BaseDialog, BaseBanner, BaseForm, EmptyState, Avatar, ThemeToggle, AuthCard.

Витрина `/_dev/ui` (dev-only) — каждая обёртка отдельной секцией над чистым
canon-канвасом, без layout-обёрток. Маршрут смонтирован на корневом уровне,
ВНЕ DynamicLayoutWrapper, чтобы legacy default.vue не перебивал стили.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:16:35 +00:00
ant 937d4e3fb6 Merge pull request 'Дизайн-волны 1–2: канон UI, формы, документы, навигация, шапка, кошелёк' (#25) from worktree-design-wave1 into dev
Build bootstrap container / build (push) Successful in 4m8s
Reviewed-on: #25
2026-05-27 09:15:38 +00:00
coopops bcb76b4cd9 merge: разрешены конфликты с dev — boot-скрипт под dev, кошельки под canon
Typecheck / desktop (pull_request) Failing after 8m35s
Typecheck / controller (pull_request) Successful in 13m22s
- clean_reboot.sh: версия origin/dev (унификация трёх reboot-скриптов)
- WalletProgramWidget.vue: canon-версия (WalletCard + EmptyState)
- ParticipantWalletsPage.vue: canon-токены --p-pos/--p-warn + value-blocked

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:14:37 +00:00
coopops a267a8a24b [design-wave1][@dark] feat: тактильная отдача (вибрация) при перезагрузке страницы
Boot-файл haptics: при reload страницы на устройстве с Vibration API (мобильный
браузер / PWA на Android) коротко вибрирует (15 мс). Reload определяется через
Navigation Timing (с фолбэком на legacy API), чтобы не срабатывать на первой
загрузке/навигации. Полностью безопасно для десктопа и обычных сайтов: где
Vibration API нет — вызов не делается, любая ошибка проглатывается, на загрузку
приложения не влияет. Только клиент (гард для SSR).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 09:05:34 +00:00
coopops 8135ea46a5 [design-wave1][@dark] fix: убрать паразитный горизонтальный скролл левого дровера
В левом дровере был маленький горизонтальный скроллбар, и трекпадом его можно
было «оттянуть», обнажая правую границу — дровер выглядел скроллируемым, хотя не
должен. Причина: у .rail собственный border-right (1px), который при content-box
даёт ~1px overflow внутри контента дровера. Фикс в .app-left-drawer: overflow-x
hidden + overscroll-behavior-x contain на .q-drawer__content, .rail приведён к
box-sizing border-box и width 100%.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 08:53:46 +00:00
coopops 50e60f5750 [design-wave1][@dark] fix: повестка — держать лоадер на «Утвердить» 3с после голоса
Если проголосовать и сразу нажать «Утвердить», утверждение падает с ошибкой —
бэкенд ещё не учёл голос из блокчейна. После успешного голоса держим состояние
загрузки пункта (а значит и кнопку «Утвердить» в :loading) ещё VOTE_SETTLE_MS
(3с), за это время голос успевает обработаться. На ошибке загрузка снимается
сразу. Заодно ручное мутирование processingDecisions + setTimeout-хак заменены
аккуратным реактивным хелпером setProcessing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 08:49:25 +00:00
coopops 92bd6919d8 [design-wave1][@dark] fix: повестка — скрывать пункт только при утверждении, не при голосовании
Повестка совета показывает все НЕутверждённые вопросы. Голос «за»/«против» не
должен убирать пункт — он остаётся неутверждённым, лишь помечается отметкой
голоса. Убрал ошибочное actedDecisionIds.add из onVoteFor/onVoteAgainst
(оставлен только тихий рефетч). Скрытие через actedDecisionIds оставлено только
в onAuthorizeDecision — там пункт исполняется и должен уйти из повестки без
возврата от отстающего поллинга.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 08:46:11 +00:00
coopops 63fdb74db5 [design-wave1][@dark] fix: повестка совета — без моргания и без «исчез/вернулся» при голосовании
Две проблемы при голосовании/утверждении в повестке совета:

1. Моргание всей страницы: QuestionsTable подменял список тремя
   скелетонами при любом loading=true, а пост-экшен loadDecisions вызывался
   без hidden → loading=true → список «моргал» в скелетоны и обратно.
   Скелетоны теперь показываются только на первой загрузке (loading &&
   !decisions.length), а пост-экшен рефетчи переведены в тихий режим (hidden).

2. «Исчез → вернулся → исчез»: повестка на бэкенде уже не отдаёт пункт, по
   которому проголосовал/утвердил, но данные из блокчейна доходят с задержкой,
   и поллинг успевал вернуть отработанный пункт. Добавлен локальный набор
   actedDecisionIds — проголосованный/утверждённый пункт прячется сразу и не
   возвращается, что бы ни подтянул отстающий рефетч. Просто, без оптимистичных
   merge-оверлеев.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 08:43:01 +00:00
coopops 5d837f2c20 [design-wave1][@dark] fix: push-подписка не должна вешать UI и шуметь красной ошибкой
Две независимые проблемы при выдаче разрешения на уведомления:

1. Фриз приложения: shouldShowDialog был computed с побочным эффектом —
   внутри геттера вызывался updateSupport(), присваивавший store.support
   новый объект на каждое чтение. После выдачи разрешения это давало каскад
   инвалидаций/ре-рендеров, забивавший главный поток (роутер менял URL, DOM
   «застывал»). Геттер сделан чистым, updateSupport() вынесен в showDialog().

2. Красная ошибка «Service Worker не готов в течение 5 секунд»: в dev без PWA
   SW намеренно не регистрируется, поэтому navigator.serviceWorker.ready
   никогда не резолвится. Теперь сначала проверяем getRegistrations() — при
   отсутствии SW выходим сразу и тихо (warn вместо error), а subscribe()
   возвращает false без FailAlert. Push при недоступности SW полностью
   изолирован и не влияет на работу приложения.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 08:34:59 +00:00
coopops 426a06a076 [design-wave1][@dark] feat: скрывать «Совершить взнос»/«Получить возврат» до подписи соглашения кошелька
Кнопки взноса и возврата на столе пайщика недоступны, пока пайщик не
подписал главное соглашение цифрового кошелька (type='wallet') — как и
сама карточка кошелька, которая не появляется без подписанного соглашения.

Геттер isWalletAgreementSigned в сторе Wallet читает тот же список
соглашений пайщика, что и RequireAgreements.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 08:22:42 +00:00
coopops c2bb718895 [design-wave1][@dark] copy: явная опциональность шага «Импорт пайщиков» (онбординг председателя)
«(опционально)» в заголовок + пояснение в описании: если есть пайщики —
импортируйте CSV, если нет — двигайтесь дальше. Чтобы шаг не воспринимался
обязательным.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 08:05:49 +00:00
coopops 04cf9dc3c4 [design-wave1][@dark] fix: ключ в Invite — выделенная панель вместо readonly-инпута
readonly outlined q-input рендерится с dashed-рамкой (дефолт Quasar) и
обрезал значение (…UJR4) — выпадало из канона и выглядело плоско.
Сделал ключ surface-2 панелью: eyebrow-лейбл + copy-иконка в шапке,
mono-значение целиком (word-break) ниже. Даёт вес и убирает «ёлочку».

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 08:01:41 +00:00
coopops 5dec964a83 [design-wave1][@dark] fix: канон-композиция карточки «Сохраните ключ» (Invite)
Прошлый вариант разъезжался: «Скопировать» висел сиротой справа, провалы
между блоками, узкая потерянная кнопка, карточка 560px — разрежено.
Переделал по канону:
- копирование ключа — иконкой content_copy в append самого поля (+tooltip)
- кнопка «Установить ключ» — full-width (block), как «Войти» в LoginForm
- ширина карточки 480 как у остальных auth-карточек
- чекбокс слева, плотный ритм без лишних margin (reserve-hint-space)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 07:57:58 +00:00
coopops dd2ebc2983 [design-wave1][@dark] copy: полные примеры ОПФ+ в placeholder (с «Социального Комплекса»)
Голый «Потребительский Кооператив» в примере провоцировал вводить просто
ОПФ без расширения. Дал полный пример «...Социального Комплекса» во всех
трёх падежах — чтобы вводили расширенную ОПФ+, а не базовую форму.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 07:51:44 +00:00
coopops 3f6c2538fc [design-wave1][@dark] copy: шаг «Настройка фабрики документов» вместо «Переменные документов»
Это не переменные, а постоянные параметры, которыми настраивается фабрика
документов кооператива. Переименовал label и intro шага.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 07:49:33 +00:00
coopops 81cb16eb61 [design-wave1][@dark] revert: убрать глобальный margin-bottom у .q-field--with-bottom
Микроотступ стал лишним и снова раздвигал поля слишком широко — откатываю.
Разделение полей обеспечивает сам reserve-hint-space, дополнительный
глобальный отступ не нужен.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 07:48:17 +00:00
coopops 5de5e07dae [design-wave1][@dark] fix: примеры-подсказки в placeholder вместо hint (SetVariablesForm)
Постоянный hint под полем занимал место и отвлекал. Перенёс примеры
наименований ОПФ+ в placeholder (видны только в пустом поле), убрал hint.
Добавил reserve-hint-space всем q-input формы — строка под ошибку
резервируется без текста, валидация не вызывает layout shift.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 07:42:45 +00:00
ant 66a323bc98 [desktop-grants-auth-order][@ant] fix: восстанавливать сессию до первого getDesktop в init-app — иначе grant-столы расширений (market) получали гостевой пустой набор grants и не появлялись до ручного вкл/выкл расширения
Корень: на холодной перезагрузке client пересоздаётся без JWT; desktops.loadDesktop() в init-app выполнялся ДО useInitWalletProcess().run()→session.init()→globalStore.init()→client.setToken(). Гостевой getDesktop → grants=[] у market-столов; повторного refetch после авторизации не было. Легаси-столы выживали (видимость реактивна от session.isChairman), grant-столы — нет. session.init идемпотентен, повторный вызов в init-wallet = no-op.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 07:29:37 +00:00
ant 72d7ef975e [nodemon-restart-storm][@ant] fix: debounce nodemon + SIGTERM в controller/nodemon.json — устранить restart-storm, вешавший coopback на bind-mount
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 07:29:37 +00:00
coopops 5470e9ad89 [design-wave1][@dark] fix: системный микроотступ под полями с hint/error
Без зазора текст hint/error верхнего поля почти касался границы нижнего.
Вернул маленький отступ, но системно и один раз — глобально в quasar-canon:
margin-bottom: var(--p-1) только полям .q-field--with-bottom (те, что
резервируют нижнюю строку). Работает везде (BaseForm, формы установки,
регистрация), в стеках не складывается в дыру, инлайновые поля не трогает.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 07:22:06 +00:00
claude 8ac3f06928 booter fix 2026-05-27 12:17:17 +05:00
coopops 6c1b6262b2 [design-wave1][@dark] fix: убрать двойной зазор между полями форм установки
reserve-hint-space у q-input/BaseInput уже даёт ~24px снизу под error/hint;
дополнительный gap в стеках полей складывался с ним → избыточное расстояние.
Убрал gap (канон BaseForm__body):
- CreateOrganizationDataForm / IndividualDataForm (.user-data-stack)
- SetVariablesForm (.vars-section__fields)
- RequestKeyForm (.request-key)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 07:13:35 +00:00
coopops 618349ce91 [design-wave1][@dark] fix: dense-инпуты в формах установки кооператива
Канон-плотность полей (dense) была только в BaseInput; общие формы
CreateOrganizationDataForm и IndividualDataForm + поля шагов установки
рендерили крупные не-dense q-input. Добавил dense:
- CreateOrganizationDataForm (22 поля) — данные организации
- IndividualDataForm (6 полей) — члены совета
- email-инпуты SetInitForm/SetSovietForm и поля SetVariablesForm

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 07:09:47 +00:00
coopops 63d95f429d [design-wave1][@dark] feat: страница установки кооператива в канон
- InstallCooperativePage: VerticalStepper вместо q-stepper, canon-панель
  с accent-стрипом, экран завершения в каноне (без градиентов/shimmer)
- RequestKeyForm: BaseInput (mono) + BaseButton
- SetInitForm: canon info-нотки, кнопки Назад/Далее на BaseButton
- SetSovietForm: canon-карточки членов совета вместо q-card/q-badge
- SetVariablesForm: canon-секции, q-input outlined с правилами, BaseButton
- Invite: осмысленное тело AuthCard при отсутствии токена приглашения

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 05:36:48 +00:00
coopops 39a9727a93 [design-wave1][@dark] fix: убрать двойной отступ сверху на странице расширения
.extension-page уже внутри .catalog-shell__content с padding var(--p-6);
собственный padding давал двойной зазор от шапки до кнопки «Назад».

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 19:23:54 +00:00
coopops 1841c3ebaa [design-wave1][@dark] style: панель вокруг контента расширения + «Отменить» в настройках
- Весь контент страницы расширения обёрнут в канон-панель (surface+border),
  больше не лежит на голом фоне
- В режиме настроек добавлена кнопка «Отменить» (CancelButton) рядом с
  «Сохранить» — явный выход без сохранения; кнопка «Назад» страницы тоже работает

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 19:13:32 +00:00
coopops 46617a1430 [design-wave1][@dark] style: страница расширения — назад на уровень страницы, центровка лого/столов, вкл+удалить в строку
- Кнопка «Назад» убрана из шапки (useBackButton снят), добавлена канон-кнопка
  под шапкой на самой странице
- Логотип AutoAvatar центрирован в колонке
- DesktopsList: центрированный inset-блок (выделяется), а не плоский список
- ext-actions toggle: «включено» и «удалить» снова в одной строке

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 19:07:41 +00:00
coopops a42210e5b8 [design-wave1][@dark] feat: страница расширения и его настройки в канон
- ExtensionPage: двухколоночный канон-layout с padding; q-img заменён на
  генеративный логотип AutoAvatar (animated), консистентно с каталогом
- ExtensionInfo: канон-заголовок + статус-бейдж, теги убраны
- ExtensionActions: компоновка кнопок в столбец на токенах, teal→primary
- ExtensionInstall: BaseCard вместо .info-card/text-h5
- DesktopsList: text-grey/subtitle → канон-токены
- SaveButton/SettingsButton/InstallButton: teal→primary, no-caps

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 19:00:52 +00:00
coopops 7e4cb15207 [design-wave1][@dark] style: анимация логотипа по наведению на всю карточку
Триггер ринг-анимации перенесён с hover самого SVG на hover карточки
(.app-card:hover :deep(.ring-seg)).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 18:54:11 +00:00
coopops 0759793fb0 [design-wave1][@dark] feat: анимация логотипа приложения по наведению
AutoAvatar получил режим animated: инлайн-SVG вместо <img>, каждый
сегмент-дуга обёрнут в .ring-seg. По hover логотипа сегменты на ~1.3с
расходятся — прямые и зеркальные (matrix-flip) от одного поворота идут
в разные стороны, со стаггером по индексу. CoopCard не затронут (img/blob).
Уважает prefers-reduced-motion.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 18:51:43 +00:00
coopops 1b9fbabac5 [design-wave1][@dark] style: убрать фиксированный резерв высоты заголовка карточки
Статус идёт сразу под заголовком (динамично) — без дыры под однострочными
названиями. Обрезку в 2 строки оставили, чтобы длинный заголовок не ломал сетку.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 18:46:22 +00:00
coopops 717eae8540 [design-wave1][@dark] style: выровнять карточки приложений при переносе заголовка
Заголовок резервирует 2 строки (clamp+min-height), аватар по верху —
статус и описание на всех карточках начинаются на одной линии, не прыгают.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 18:43:45 +00:00
coopops c456602763 [design-wave1][@dark] style: компоновка карточки приложения — заголовок и статус рядом с логотипом
Верхний ряд: логотип + (заголовок, под ним статус); описание во всю
ширину ниже; футер «Подробнее» прижат к низу для выравнивания карточек.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 18:40:13 +00:00
coopops a36f5a63a9 [design-wave1][@dark] style: приглушить генеративный логотип каталога
Палитра колец → мягкие приглушённые тона + CSS saturate(.5)/opacity .85,
чтобы знак читался как тихая текстура, а не неоновое пятно.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 18:35:26 +00:00
coopops 6eced283ec [design-wave1][@dark] feat: генеративный логотип (DiceBear rings) в карточке приложения
- AutoAvatar параметризован: props size/radius/background/ringColor
  с обратно-совместимыми дефолтами (CoopCard не затронут)
- ExtensionCard: буква-монограмма заменена на уникальный rings-логотип,
  seed = имя расширения; цвет кольца детерминирован по seed из палитры

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 18:29:36 +00:00
coopops 6890b13387 [design-wave1][@dark] feat: каталог приложений в канон — табы на 2-й уровень, карточки без изображений
- «Магазин приложений» → «Каталог приложений» (drawer + breadcrumb)
- Витрина/Установленные перенесены из топбара в SecondLevelTabs
- ExtensionCard переверстан: убраны изображение и теги, добавлены
  монограмма-плитка, статус-бейдж, обрезка описания, футер «Подробнее»
- Quasar .row/.col-md-3 → канон auto-fill грид

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 18:19:38 +00:00
ant 7e180e701b [598-23][@ant] feat: реальный L1-онбординг ЦПП «Стол заказов» через повестку совета вместо stub — два документа (Положение 1107 + оферта 1100) утверждаются Советом, состояние меняется по ончейн-решению как в Капитале
- cooptypes 1107.MarketplaceProgramTemplate (Положение ЦПП, placeholder) + factory-генератор
- marketplace подключён к платформенному онбордингу (register-marketplace-onboarding-steps),
  coopAcceptance.accepted выводится из all-steps-done в initialize() (auto-restart по ONBOARDING_COMPLETED)
- desktop: страница онбординга переведена на shared CouncilOnboardingCard + generic onboarding SDK
- дока ядра онбординга: components/context/notes/EXTENSION_ONBOARDING_CORE.md

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 17:28:47 +00:00
coopops 18f8e2a86c [design-wave1][@dark] fix: вёрстка формы провайдера платежей + текст контактов
- PaymentProviderForm: убраны вложенные q-gutter (кривой левый отступ
  селекта), standout=bg-teal → outlined, text-grey-7 → токен; чистый
  канон-столбец (селект max-width 480, hint ink-2, действия в ряд, no-caps).
- ChangeContacts: текст уточнён — контакты видны всем (раздел «Контакты
  кооператива» пайщикам + подвал сайта незарегистрированным), не только
  незарегистрированным.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 17:03:04 +00:00
coopops abaf0cbdab [design-wave1][@dark] feat: канон для остальных страниц стола председателя
Кооперативные участки, Регистрационные взносы, Ключ кооператива,
Провайдер платежей, Контакты кооператива:
- убран дублирующий заголовок страницы (есть крошка в шапке);
- описание перенесено с голого фона на канон-поверхность .banner;
- page-shell/hero/surface-card с хардкод-px → канон-отступы и токены;
- q-input/q-select standout=bg-teal → outlined color=primary.
Также удалён лишний хвост текста на Стартовых страницах.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 16:54:16 +00:00
ant 2004931305 [598-23][@ant] fix: трактовать grants=null как core-стол (legacy meta.roles), а не grant-режим — иначе после свежей цепочки все core-столы (председатель/пайщик/совет) пустели и не показывались онбординг-соглашения
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 16:49:54 +00:00
coopops 3560839ef8 [design-wave1][@dark] fix: убрать дубль заголовка на Стартовых страницах и Членах совета
Заголовок страницы уже показан крошкой в шапке — повтор h2 убран.
Пояснительный текст вынесен с голого фона на канон-поверхность (.banner
с инфо-иконкой). ApprovalsPage не трогаем — там заголовка страницы нет.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 16:48:07 +00:00
coopops 4a4024f3f4 [design-wave1][@dark] feat: канон для Запросов одобрений, Стартовых страниц, Членов совета
- ApprovalsPage: канон-отступы страницы, фильтр-селект teal standout → outlined.
- SystemSettingsPage: hero-card/surface-card с хардкод-px → канон page-head
  (h2 + sub на токенах) + q-card(flat); канон-отступы.
- MembersPage: тот же канон-паттерн вместо hero-card с хардкодами.
- DefaultPagesForm: 4× q-select standout=bg-teal → outlined color=primary.
Виджеты таблиц/состава совета без экзотики — канон-тема через quasar-canon.css.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 16:39:24 +00:00
coopops fee0a27b3f [design-wave1][@dark] fix: убрать Vue-warn о class в WorkspaceSwitcher
Шаблон двухкорневой (кнопка + Teleport затемнения), class из родителя
(LeftDrawerMenu) не наследовался автоматически. inheritAttrs:false +
v-bind=$attrs на корневую кнопку — атрибуты направлены явно.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 16:36:25 +00:00
coopops ac20b270eb [design-wave1][@dark] style: согласовать типографику диалога повестки
Подписи секций «Вопрос на повестке»/«Проект решения» переведены в
надстрочные метки (uppercase, трекинг, ink-3) — отдельный ярус ниже
заголовка окна, чтобы не казаться конкурирующим заголовком рядом с
крупным заголовком самого документа. Текст вопроса — читаемый body.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 16:28:55 +00:00
coopops 90c733120d [design-wave1][@dark] fix: канон-отступы и чистка онбординга председателя
- ConnectPage: добавлены канон-отступы страницы (var(--p-6)/var(--p-4)) —
  класс .padding в проекте не определён, контент липнул к краям.
- OnboardingStepsCard: убран q-card (двойной паддинг), текст-интро
  растянут на полную ширину (снят max-width 70ch), вокруг кнопки шага
  убрана лишняя рамка (.step__content → .step__action, только отступ).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 16:20:05 +00:00
coopops b6b1f06948 [design-wave1][@dark] feat: онбординг председателя в канон (вертикальный степпер + текст + диалог повестки)
- OnboardingStepsCard: под заголовком «Адаптируйте кооператив…» добавлен
  поясняющий текст (что происходит дальше: решения совета в электронной
  форме, вступление в «Восход», импорт пайщиков, общее собрание) + плашка
  срока адаптации с отсчётом.
- Чек-лист шагов переведён на канон-вертикальный степпер (.stepper--v):
  пройденные — галочка/заливка primary, текущий — подсветка номера,
  действия — канон BaseButton; agenda/import/meet типы сохранены.
- Диалог «Предложение повестки» (объявить собрание совета) переведён
  с ModalBase на канон BaseDialog + BaseButton.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 16:06:00 +00:00
ant e46efe4081 [@ant] chore: регенерация GraphQL-схемы и Zeus-клиента под DesktopWorkspace.grants
generate-schema + generate-client + сборка SDK после добавления поля grants.
schema.gql и components/sdk/src/zeus/* теперь содержат grants — фронт штатно
выбирает поле через selectors/desktop/workspaceSelector.ts (канон авторизации
столов активирован end-to-end). dist в .gitignore.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 15:14:13 +00:00
coopops 32afc40147 [design-wave1][@dark] feat: хлебные крошки в шапке + скрыта кнопка «Перевести»
- CommonHeader: крошка строится из route.matched (путь внутри стола),
  напр. «Отчётность › Календарь»; корень стола не выводится; плоские
  страницы дают одну крошку как прежде; pageTitleOverride сохранён.
- CoopWalletsPage: переводы временно скрыты под флагом transferEnabled=false
  (открывались с L3-кошельков пайщиков; вернём для кооп-кошельков позже).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 14:59:08 +00:00
ant 64e808b148 [@ant] feat: единый канон авторизации столов (grants) — устранён дубль meta.roles↔access-matrix
getDesktop отдаёт DesktopWorkspace.grants (права текущего пользователя), фронт
сверяет meta.requires маршрута с ними (DesktopStore.isPageVisible/isWorkspaceVisible/
hasRouteAccess). Авторизацию считает только backend, фронт отражает. Провайдер
грантов расширения само-регистрируется в @Global ExtensionGrantsRegistry (нет цикла
модулей); getDesktop под OptionalGqlJwtAuthGuard. Онбординг сведён к грантам: до ЦПП
у председателя только Extension:configure → виден лишь Стол админа со страницей
подключения. market переведён на meta.requires; убраны onboarding-поля реестра,
desktop-фильтр и фронтовые beforeEnter/онбординг-ветка. Зачем: убрать дублирующую
логику видимости столов в двух местах и подготовить per-page доступ из бэкенда.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 12:57:39 +00:00
coopops 93d2c6ce77 [design-wave1][@dark] style: канон-токены в редакторе отчёта (ReportEditorDialog)
- editor-container/action-panel/backdrop/validation-badge/mark-hint: hex и rgba → --p-* токены
- text-grey-8 → t-muted; box-shadow/overlay через токены с fallback

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 12:30:47 +00:00
coopops 099f7b5aa2 [design-wave1][@dark] style: канон-токены на списочных страницах бухстола + cells
- Operations/Postings/Accounts: канон page-padding, убран двойной q-mt-md, text-grey-* → t-faint/t-muted, hex-границы → токены
- Settings: канон-padding, save-bar на токены (--p-surface/--p-line/--p-pos), text-grey-7 → t-muted
- AccountIdCell/WalletIdCell: text-grey-6 → t-faint; DirectionCell: move-иконка на --p-ink-3
- PROCESS_COLORS доменного цвет-кодирования операций сохранены (это фича)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 12:28:50 +00:00
coopops 6e9d466172 [design-wave1][@dark] style: канон-токены на столе бухгалтера — кошельки, календарь, отчётность
- CoopWalletsPage/ParticipantWalletsPage: rgba/var(--q-*) → канон-токены (--p-pos/--p-warn/--p-ink-*)
- ReportsCalendar/CalendarCell: статусы и грид на канон-палитру, убраны body--dark дубли
- Documents подстраницы: убран двойной padding, q-chip→BaseBadge, таблицы в канон-карточку

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 12:24:33 +00:00
coopops 511e9ab2ad [design-wave1][@dark] feat: канон-меню второго уровня (SecondLevelTabs) на столе бухгалтера
- SecondLevelTabs на глобальных .tabbar/.tab — sub-навигация между топбаром и контентом
- WalletsPage/DocumentsPage shell-страницы: вкладки в .tabbar вместо RouteMenuButton в топбаре
- TransferWalletsButton: канон header-кнопка (q-btn primary)
- WalletTransferDialog: на BaseDialog + BaseButton, токены вместо rgba

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 12:19:33 +00:00
ant 52fb3e03f9 docs(context): задокументировать канон онбординга расширений (гейтинг столов + редирект)
Дописана секция в EXTENSIONS_SCHEMA_SYSTEM.md: контракт реестра
(onboarding_route/onboarding_desktop/isOnboarded), две согласованные половины
гейтинга (backend getDesktop скрывает столы, frontend install.ts ограничивает
стол ролью и одной страницей), редирект при включении, жизненный цикл и
ограничения (нужна перезагрузка после принятия ЦПП), опциональность.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 12:07:39 +00:00
ant 0fd24c812f feat(extensions): канон онбординга — гейтинг столов до принятия ЦПП + редирект при включении
Опциональный generic-канон для расширений с подключением на уровне кооператива:
- IRegistryExtension получает onboarding_route / onboarding_desktop / isOnboarded(config);
- getDesktop, пока isOnboarded(config)===false, отдаёт ТОЛЬКО onboarding_desktop —
  остальные столы скрыты из переключателя (для market: только Стол администратора);
- desktop install.ts зеркалит решение бэкенда и ограничивает админ-стол одной
  страницей подключения ЦПП и ролью chairman — до решения совета никто, кроме
  председателя, не видит ни столов, ни страниц;
- loadExtensionRoutes возвращает workspace-конфиги; кнопки включения/установки
  редиректят на defaultRoute первого стола — для расширений с онбордингом это
  страница подключения, чтобы администратор не терялся после включения.

Расширения без этих полей работают как раньше (опциональность). Источник
безопасности — backend-резолверы; гейтинг столов — UX-слой поверх них.
Для market: isOnboarded = config.coopAcceptance.accepted.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 12:06:44 +00:00
coopops 9373dfe847 [design-wave1][@dark] fix: канон-поля ZodForm и убран дубль заголовка «Настройки»
ZodForm: убран standout="bg-teal text-white" с динамического поля —
QInput/QSelect теперь канон (outlined+dense+color=primary+reserve-hint-space),
как BaseInput. SCSS на канон-токены (вместо неопределённого --q-primary-rgb).
ExtensionSettings: снят заголовок «Настройки» внутри карточки — он дублировал
«Настройки аренды» в шапке.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 12:01:36 +00:00
coopops e163bda08f [design-wave1][@dark] fix: фильтры лога на BaseInput вместо сырого q-input
Канон-инпут вместо q-input: BaseInput расширен поддержкой type="date"
и clearable (+ emit clear) — это нужно для фильтра по датам. Фильтры
в ExtensionLogsList переведены на BaseInput.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 11:55:44 +00:00
ant 6882ea23f5 fix(controller): списывать скоропорт с отступом после истечения срока (post_expiry_grace_days)
Возвращён регулируемый параметр списания, но с корректной семантикой: товар
попадает в проект списания, только пролежав N дней ПОСЛЕ истечения срока
(expiry_date <= now - grace), default 7. Свежепросроченное ещё может быть
забрано получателем — списываем лишь окончательно испорченное. Крон остаётся
ежемесячным (каждое 1-е число).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 11:53:36 +00:00
coopops 5257d5c6e6 [design-wave1][@dark] feat: настройки и лог расширения в канон
ExtensionSettings: EmptyState + BaseCard вокруг ZodForm вместо самописных
header/empty-state. ExtensionLogsList (используется только powerup):
q-table → список канон-карточек с фильтром по датам, скелетоном,
«Загрузить ещё» и EmptyState; слот #log-item и API сохранены.
Канон-падинги страниц настроек/лога аренды.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 11:52:04 +00:00
coopops 6184102022 [design-wave1][@dark] feat: Стол вычислительных ресурсов в канон — раскладка, виджеты, лог
Монитор ресурсов: баннер-предупреждение на всю ширину сверху, ряд
«Как это работает» (2/3) + кошелёк AXON (1/3), ряд CPU/NET/RAM.
Убран 3D-флип карт (он же «прыгал») — детали раскрываются inline.
Все виджеты/страницы/лог сведены к канон-токенам surface/line/ink.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 11:44:33 +00:00
ant 55d7289e0a fix(controller): списание скоропорта по факту истечения срока + автопроект включён по умолчанию
Семантика крона списания исправлена: в кандидаты попадают позиции с УЖЕ
истёкшим сроком годности (expiry_date <= now), а не за N дней до истечения —
списываем испорченное, а не заранее. Параметр expiry_grace_days удалён как
вводящий в заблуждение. auto_proposal_enabled теперь true по умолчанию, чтобы
проект списания формировался без ручной активации. Крон остаётся ежемесячным
(@Cron EVERY_1ST_DAY_OF_MONTH ≈ раз в 30 дней) — устойчив к рестартам, в
отличие от setInterval (лимит ~24.8 сут не покрывает 30-дневный интервал).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 11:39:42 +00:00
coopops ba1e17c991 [design-wave1][@dark] fix: отступы действий платежей и ширина даты в документах
- Реестр платежей: кнопки действий больше не block (не заполняют ячейку
  от левого края, не липнут к бейджу статуса). Стопка inline-flex,
  равная ширина кнопок (align-items:stretch), прижата к правому краю
  колонки (col-action 168px, text-align:right) — слева остаётся воздух.
- Реестр документов: col-date 116→156px — дата «26.05.2026 08:25» больше
  не налезает на заголовок документа; min-width 720→780.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 11:29:48 +00:00
coopops fdd38951b5 [design-wave1][@dark] fix: реестры платежей/документов и форма взноса — ширины и кнопки
- Bank.vue: панель платежа max-width 380px по центру — длинное «Назначение»
  больше не раздувает диалог «Совершите взнос» до max-content; значение
  переносится по строкам (overflow-wrap: anywhere).
- Кнопки статуса платежа SetOrderPaid/RefundedStatusButton → BaseButton
  (primary/danger, block), подписи «Подтвердить»/«Отклонить»; диалог
  вынесен из-под кнопки.
- Реестр платежей: кнопки действий друг под другом (.cell-actions),
  колонка действий 156px, дата без переноса (.col-date 132px), min-width
  таблицы уменьшен.
- Реестр документов: сужены колонки подписей (220→150), действий
  (110→56), id/дата; min-width 880→720 — освобождено место под
  наименование, таблица не вылазит за страницу.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 11:24:31 +00:00
ant 5c3b5328a3 config(controller): добавить человеческий заголовок группе настроек writeoff — чтобы в форме установки не светилось сырое имя поля
Родительскому объекту writeoff заданы label «Списание скоропорта» и note
через describeField; рендерер формы (ZodForm) показывает getLabel/getHint и
для объектов-групп, поэтому без описания отображалось техническое «writeoff».

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 11:24:19 +00:00
ant 2560990442 config(controller): минимизировать конфиг установки расширения marketplace — убрать технические поля, русифицировать настройки
Из формы установки расширения market убраны неиспользуемые в коде поля
enabled/lastSyncTimestamp/debug; coopAcceptance помечен visible:false как
системное состояние совета; настройки writeoff получили русские label/note
через describeField — по канону расширения capital. Чтобы при установке
приложения председатель видел минимальную понятную форму на русском, без
отладочных и внутренних параметров.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 11:21:12 +00:00
coopops fca4e7fc9c [design-wave1][@dark] fix: реестры документов и платежей — канон-поля страницы
Таблицы документов и платежей уже на канон-токенах и самообрамлены
(.table-wrap). Не хватало полей страницы как в реестре пайщиков:
- PaymentsPage рендерил виджет голым, впритык к краям → q-page + 24/16px.
- ListOfDocumentsPage: q-page.padding (16px) → канон-24px.
- Убрана лишняя .row.justify-center обёртка в ListOfDocumentsWidget
  (.col-12 и так во всю ширину).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 11:04:26 +00:00
coopops fa21066161 [design-wave1][@dark] fix: диалоги «Добавить» и «Импорт» пайщиков в канон
AddUserDialog перестроен в канон-визард по образцу CreateMeetForm:
шапка-bar (surface+line) вместо bg-gradient-dark, VerticalStepper из трёх
шагов — Электронная почта → Тип и данные → Вступительный взнос, BaseButton
в подвале, outlined-инпуты. UserDataForm переиспользован целиком (общий,
не тронут). Опция начисления взноса — канон-блок вместо q-item/q-card.

ParticipantsImportDialog: добавлено описание-интро (раньше отсутствовало),
ModalBase/bg-gradient-dark заменён на канон-bar, выбор типа аккаунтов через
BaseRadioCard, dropzone на токенах вместо хардкод-цветов, секции и действия
в канон-обёртках, BaseButton вместо цветных q-btn. Логика парсинга/импорта
и таблицы превью/результатов сохранены.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 10:53:06 +00:00
coopops 4b1bedd2e8 [design-wave1][@dark] fix: реестр пайщиков — канон-поля и дыхание вместо встык-вёрстки
Страница получила канон-отступ (--p-6 / --p-4 на мобилке), таблица обёрнута
в обрамлённую surface-карточку (--p-line, --p-r-lg) вместо edge-to-edge.
Развёрнутые данные пайщика — спокойная вложенная панель на --p-surface-2
с отступами --p-5/--p-6, читаемой шириной формы (640px) и вертикальным
ритмом между полями. Снят full-height у таблицы (мешал внутри карточки).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 10:37:15 +00:00
ant 26acb2bdfe feat(desktop): детальный просмотр и правка предложений marketplace
Карточки CatalogOfferCard на модерации и в «Мои предложения» нельзя было
открыть — описание обрезано, действий нет. Добавлен widget OfferDetailsDialog
(+ секция витрины дизайн-системы): клик по карточке открывает модалку с полным
описанием, метой (категория/тип отсечки/гарантия/поставщик), причиной отклонения
и slot actions под ролевые кнопки.

- Модерация (председатель): открыть → прочитать → Одобрить / Отклонить
  (добавлена мутация rejectOffer с обязательной причиной — раньше в UI её не было).
- Мои предложения (поставщик): открыть → Редактировать / Снять / Запустить поставку.
- Правка: скрытый роут marketplace-edit-offer переиспользует форму создания в
  режиме редактирования (префилл из listMyOffers, сабмит → marketplaceUpdateOffer).
  Бэкенд сбрасывает статус в PENDING_MODERATION — правка уходит на повторную
  модерацию в рамках того же предложения (баннер предупреждает об этом).

Проверено на живом стенде: диалоги открываются на обоих столах, Редактировать
ведёт на предзаполненную форму. ESLint чист, vue-tsc без ошибок в затронутых файлах.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 09:07:18 +00:00
coopops be7fc22928 [design-wave1][@dark] refactor: реестр пайщиков в канон + убран переключатель Данные/Документы
Развёрнутая строка пайщика теперь показывает только его данные;
документы пайщика живут в отдельном «Реестре документов» и здесь не дублируются.
ParticipantDetails упрощён (без q-tabs/ListOfDocumentsWidget), tab-логика
вычищена из таблицы и страницы. Мобильная ParticipantCard переведена на канон-токены
(убраны хардкод-цвета, CardStyles и q-badge статуса → disabled-checkbox).
Таблица сохранена, сортировка по дате вступления (DESC) без изменений.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 08:01:45 +00:00
ant 2c8d5466e2 feat(desktop): синхронизировать активный стол с текущим маршрутом
Активный стол (activeWorkspaceName) ставился только кликом по плитке,
редиректом с index и из localStorage. При прямом заходе по URL или переходе
кнопкой со стола А на маршрут стола Б активным оставался прежний стол — в
шапке и левом меню «висел» не тот стол (видно было на playwright-кадрах как
постоянный «Председатель»).

- DesktopStore.syncActiveWorkspaceFromRoute(matchedNames): находит стол по
  имени родительского маршрута (mainRoute.name) в цепочке to.matched и делает
  его активным без навигации и без спиннера.
- navigation-guard: router.afterEach зовёт синк после каждого перехода.
- init-app: однократный досинк после установки расширений (deep-link при
  холодной загрузке, когда afterEach сработал до прикрепления маршрутов).

Проверено на стенде: прямой заход на /market-supplier|market-admin|market/*
переключает активный стол и левое меню на нужный.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 07:57:44 +00:00
ant 13641a9027 feat(desktop,controller): разнести «Стол заказов» на 4 ролевых стола
Расширение market давало один смешанный стол из 21 страницы (заказчик +
поставщик + админ вперемешку) и отдельный Стол ПВЗ, причём market-pvz
вообще не объявлялся в backend AppRegistry → его маршруты молча терялись.

- backend extensions.registry.ts: AppRegistry['market'].desktops теперь
  объявляет 4 стола (market/market-supplier/market-pvz/market-admin) —
  фронт прикрепляет маршруты только к объявленным backend столам.
- desktop install.ts: возвращает 4 workspace, 29 страниц разнесены по
  ролям; Стол администратора виден только chairman+member (meta.roles),
  остальные — всем; имена маршрутов сохранены (навигация по имени).
- init-installed-extensions: убран немой catch{} (теперь console.error)
  + warn, когда install вернул workspace, отсутствующий на backend, —
  раньше сломанный/несинхронный стол выглядел как «пустой» без диагностики.

Проверено на стенде: getDesktop = 4 стола, Pinia-стор = children 6/6/8/9,
скриншот навигатора подтверждает рендер страниц по столам.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 07:53:56 +00:00
coopops 0a229df206 [design-wave1][@dark] fix: повестка — номер вопроса на зелёной плашке вместо иконки, клик копирует
Убран EntityIdBadge. Зелёная плашка-аватар слева теперь несёт сам номер вопроса
(он же Decision ID): клик по плашке копирует id (тултип «Скопировать №»), hover —
инверсия в сплошной teal. Заголовок снова просто текст вопроса.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 07:14:43 +00:00
coopops c67fb4703d [design-wave1][@dark] fix: повестка — бейдж ID в строку заголовка перед текстом вопроса
EntityIdBadge перенесён из отдельной строки под ФИО в начало строки заголовка:
[#id] → сразу текст вопроса. Inline, vertical-align middle. Раньше висел снизу
и пусто растягивал верхнюю строку.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 07:00:08 +00:00
ant db46533b06 fix(desktop): не блокировать загрузку приложения ожиданием SDK ChatWoot
Boot-файл ждал window.chatwootSDK через polling БЕЗ таймаута (await
waitForChatWootReady), поэтому недоступный support.coopenomics.world вешал
всю инициализацию Quasar → бесконечный спиннер. Вынес доинициализацию чата
в отделённую (неожидаемую) задачу + жёсткий 15s таймаут: boot завершается
сразу после app.use(chatwoot), недоступный support чат лишь отключает, но
загрузку приложения не блокирует.
2026-05-26 06:55:56 +00:00
coopops d213b1639d [design-wave1][@dark] feat: повестка — копируемый бейдж Decision ID под ФИО
EntityIdBadge с id вопроса под заголовком/ФИО: отображает #<id>, по клику копирует
чистый id (copy-on-click). Канон-компонент, клик не раскрывает карточку.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 06:39:47 +00:00
coopops fc8a060d71 [design-wave1][@dark] fix: повестка — срок «Истекает …» в нижнюю полоску рядом с «Утвердить»
«Истекает через месяц» перенесён из строки под заголовком в постоянную нижнюю
полоску (footer): срок слева, «Утвердить» справа (только председателю). У обычного
пайщика — только срок, узкая панелька.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 06:35:02 +00:00
coopops e8466143a6 [design-wave1][@dark] fix: повестка — «Истекает …» и убран избыточный бейдж статуса
Срок теперь подписан «Истекает через месяц» (было просто «через месяц» — неясно).
Удалён статус-чип «Вы за/против» — направление голоса уже видно по подсветке кнопок,
бейдж был избыточным. Убраны связанные computeds и стили.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 06:31:13 +00:00
coopops 009cc42afd [design-wave1][@dark] fix: повестка — переосмысленная компоновка карточки вопроса
Кнопки голосования прижаты к правому краю строки. «Утвердить» вынесена вниз
отдельной строкой-footer (справа, hairline сверху) — больше не теснит данные.
Срок «через месяц» и статус-чип «Вы за» перенесены влево под заголовок/ФИО.
Клик по строке раскрывает документ, по кнопкам голосования и footer — нет.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 06:24:50 +00:00
coopops cfe0835eea [design-wave1][@dark] fix: повестка — органы управления в одну строку с инфо, не остров по центру
Карточка вопроса перестроена в единую горизонтальную строку: иконка + вопрос/ФИО
слева (flex), компактный блок органов управления (голосование + «Утвердить»),
срок + статус-чип и шеврон — справа, друг рядом с другом. Убрана центрированная
vote-зона с пустотой по бокам. Клик по строке раскрывает документ, по органам
управления (@click.stop) — нет. Подсказка «Утвердить» возвращена в tooltip.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 06:18:00 +00:00
ant a889a28b10 [598-30][@ant] feat: подключить кнопку «Запустить поставку» на столе «Мои предложения» + актуализировать supply-types — чтобы поставщик запускал открытую подписку из UI (раньше только через API), как заявлено в документации 2026-05-26 05:51:01 +00:00
coopops a86711c111 [design-wave1][@dark] fix: повестка — раскрытие документа по клику на шапку, не на органы управления
Шапка карточки (вопрос/ФИО/срок/статус) кликабельна целиком и раскрывает документ,
справа шеврон-индикатор. Органы управления голосованием — отдельная зона ниже шапки
(сиблинг, не аккордеон): клик по ним голосует/утверждает и документ не раскрывает.
Убрана отдельная кнопка «Документ» — раскрытие теперь по клику на шапку.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 05:34:55 +00:00
coopops 9332bc63c8 [design-wave1][@dark] fix: голосование повестки на верхнем уровне + header-CTA совета
QuestionCard: VotingButtons + «Утвердить» вынесены на верхний уровень карточки —
голосовать можно без раскрытия; раскрытие («Документ») открывает только содержимое
документа. Истечение срока остаётся в шапке.
VotingButtons: вернул чек-индикатор «принято советом» (закрашивается при принятии)
вместо иконки verified.
Header-CTA совета (Предложить/Добавить/Импорт): убран push (q-btn--push не попадал
под канон-правило заливки primary → белый шрифт на нетиловом фоне), подписи с заглавной.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 04:39:25 +00:00
coopops 8217cd5299 [design-wave1][@dark] feat: повестка совета в канон — единый карточный список
QuestionsTable: q-table → карточный список (скелетоны + EmptyState).
QuestionCard: канон-поверхность, BaseButton «Утвердить», токен-чип статуса
вместо q-badge, локальное состояние раскрытия, без @import CardStyles и хардкод-hex.
VotingButtons: токены pos/neg вместо .text-red/.text-green/#666.
Страница: канон-padding вместо q-card flat.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 04:26:00 +00:00
ant 8e92b4e068 fix(marketplace): дата «Открыто к выдаче» на orderer-столе ready-to-receive
Колонка chairman_signed_at в QTable не отображала значение (нет format-функции,
сырой ISO не рендерился) — добавлен formatDate с ru-локалью (как в board/payouts),
теперь видна дата открытия выдачи. Обновлён скриншот документации.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 20:08:13 +00:00
ant 56454ca169 fix(marketplace-docs): ready-to-receive с реальными данными, board/payouts, удаление legacy-доков, ключ Яндекс-карт
Правки по голосовому ревью документации Marketplace:

- orderer-стол «Готово к получению»: заказ доведён по магистрали до
  READY_TO_RECEIVE (без финальной подписи), сценарий перестроен ОТ этой
  страницы (а не от вкладки «Получены»); проза уточнена — финальная подпись
  получения оформляется на столе оператора ПВЗ по штрих-коду.
- board/payouts: лента выплат показывает реальные данные (1 PENDING + 2
  COMPLETED) — данные засеяны магистралью; страница уже авто-грузится onMounted.
- удалены legacy-доки orderer/receive.md и branch-chairman/apl-reception-close.md
  + их nav-записи; висячие reference-ссылки перенаправлены на issuance-finalize /
  apl-reception-create / ready-to-receive (mkdocs --strict: по marketplace чисто).
- YANDEX_MAPS_API_KEY проброшен через EnvVars + generateConfig + fallback envCache
  (ключ только в gitignore'нутом .env; в config.default.js — пустой placeholder);
  карта Яндекса на странице ПВЗ рендерится, баннер «не задан ключ» убран; проза
  branches.md обновлена (карта есть всегда, точка — после геокодинга адреса).
- harness: устойчивая навигация ready-to-receive/pvz-list (ретрай + полный
  перезагруз против гонки core-guard/fetchRoles), ожидание DOM-рендера карты,
  dismiss онбординга на pvz-list.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 19:48:13 +00:00
coopops 44a0f7b132 [design-wave1][@dark] fix: единый регистр названий столов в переключателе
Названия столов в реестре workspace'ов заведены вразнобой («Стол
благороста», «Стол вычислительных ресурсов» vs «Стол Совета»).
Добавил text-transform: capitalize на подписи в WorkspaceSwitcher
(заголовок текущего стола + пункты выпадающего меню) и WorkspaceMenu
(карусель + диалог выбора) — первая буква каждого слова заглавная,
одинаково для всех и для будущих столов, без правки строк-источников.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 18:58:18 +00:00
coopops d120fff065 [design-wave1][@dark] fix: «Контакты» — единая сетка полей вместо сегментов
По фидбеку переработана композиция:
- убраны заголовки секций «Контакты»/«Руководство» и per-строчные
  иконки телефона/почты/адреса (из-за них значения «прыгали» вправо
  относительно ИНН/ОГРН);
- ИНН и ОГРН теперь в одну строку (адаптивная сетка полей);
- председатель поднят наверх к реквизитам, поле названо просто
  «Председатель совета»;
- контакты — те же поля, телефон/email как ссылки (tel/mailto), всё на
  единой левой кромке; одна тонкая линия делит реквизиты и контакты.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 18:45:45 +00:00
coopops 6c38298545 [design-wave1][@dark] fix: «Контакты» — единая раскладка строк во всех секциях
Реквизиты/Руководство больше не разносят подпись и значение к
противоположным краям (на полной ширине это давало пустой провал и
оторванные значения). Все секции теперь как ContactSheet: подпись
сверху, значение под ней, слева, hairline между строками — один ритм
по всей карточке.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 18:40:12 +00:00
coopops a8f9bf3c36 [design-wave1][@dark] feat: страница «Контакты» в канон + удалён мёртвый MeetQuorumIndicator
- ContactsPage: три цветные ColorCard (orange/indigo/teal/blue) заменены
  на единую спокойную canon-поверхность с секциями через hairline
  (Реквизиты / Контакты / Руководство). Контакты — через canon
  ContactSheet (копирование, mailto/tel). Заголовок и строки на токенах
  (--p-ink/-2/-3, --p-surface/--p-line/--p-r-lg), полная ширина (padding
  24/16px). Убраны rgba-фоны и .q-dark-хаки.
- Удалён неиспользуемый виджет MeetQuorumIndicator (нет импортов;
  явка/кворум живут в MeetInfoCard).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 18:36:09 +00:00
coopops c099373d00 [design-wave1][@dark] feat: тело деталей собрания в канон — повестка/голосование/результаты
Убрал токсичный teal из state-gated виджетов деталей собрания:
- MeetDetailsAgenda/Voting/Results переписаны на canon-поверхности
  (--p-surface/--p-line/--p-r-lg), section-heading с иконкой вместо
  центрированной градиентной линии; color-mix/--q-primary/.q-dark удалены.
- Голосование: q-btn color=primary → BaseButton variant=primary;
  опции голоса на токенах pos/neg/neutral-soft, баннер «уже голосовали»
  на --p-pos-soft.
- Результаты: итог решения — токен-чип (pos/neg/unknown), счётчики
  голосов на pos/neg/surface-2-soft.
- AgendaNumberAvatar: teal q-avatar → нейтральный квадрат surface-2/ink
  (общий для трёх виджетов — убирает teal сразу везде).
- MeetCardsList: отступ между карточками 16→20px.
- MeetDetailsPage: кастомный empty-state → canon EmptyState, снят
  @import CardStyles.

MeetQuorumIndicator оставлен как есть — мёртвый код (нет использований).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 18:28:50 +00:00
coopops 4d52302389 [E14][@dark-sun] feat(собрания): полная ширина страниц; тело деталей (MeetInfoCard) на canon
- ListOfMeetsPage / MeetDetailsPage: убран max-width:960px center — контент
  во всю ширину с canon-паддингом (24px/16px), как документы/платежи.
- MeetInfoCard: переписан с токсичного teal на canon — единая calm-поверхность
  с тремя секциями через hairline (Даты / Ведущие / Явка и кворум) вместо
  трёх вложенных цветных карточек; заголовки ink, проценты ink (не teal);
  дубль заголовка «Общее собрание № N» убран (он в шапке). Убраны
  var(--q-primary), color-mix, .body--dark, хардкод-rgba.
- MeetDetailsInfo: контейнер с .card-container → canon.
- MeetDetailsActions: q-btn color=primary → canon BaseButton.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 18:00:40 +00:00
coopops 093a31a9b5 [E14][@dark-sun] feat(собрания): повестка на шаге проверки канон; карточка списка канон; шапка деталей — back-link вниз, имя в заголовок
- CreateMeetForm: повестка на шаге «Проверка» — аккуратные canon-карточки
  (Вопрос / Проект решения / Приложения) вместо скомканного списка.
- MeetCompactCard: переписана на canon — нейтральный hover (без
  токсичного teal-свечения), ink-заголовок, canon-плитки/иконка;
  убраны @extend .card-container, var(--q-primary), .body--dark, color-mix.
- MeetStatusBanner: нейтральный canon-контейнер (surface-2 + line),
  цвет статуса несёт иконка; убраны хардкод-rgba и .body--dark.
- MeetCardsList: empty-state на canon EmptyState, skeleton на .skel.
- Детали собрания: кнопка «Назад» убрана из топбара (снят useBackButton),
  добавлен canon back-link под шапкой слева; название собрания выводится
  в заголовок шапки через новый desktopStore.pageTitleOverride
  (приоритет над route.meta.title; транзиентно, чистится при уходе).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 17:46:01 +00:00
coopops f560e7d2d0 [E14][@dark-sun] feat(собрания): форма созыва на canon-степпер; кнопка в шапке к канону; скрыта «Сверить»
- BaseDocument: кнопка «Сверить» временно скрыта через :hide-verify
  (обработчик @verify остаётся, вернуть = убрать флаг).
- CreateMeetButton: приведена к одобренному canon-паттерну (primary
  solid на десктопе, micro flat accent + tooltip на мобильном),
  убран legacy push/teal с белым текстом.
- ListOfMeetsPage: инжект кнопки переведён с useHeaderActions на
  Teleport #header-actions-host.
- CreateMeetForm: полностью переписана с перегруженной тёмной модалки
  на canon-визард — maximized-диалог + VerticalStepper (3 шага:
  Параметры → Повестка → Проверка). Canon outlined-поля, повестка
  аккуратными карточками, шаг проверки со сводкой. Убраны
  bg-gradient-dark / standout bg-teal / .body--dark / хардкод rgba.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 17:04:51 +00:00
ant ad9f0789cf fix(docs-harness): hash-routing + cold-Vite + onboarding-dialog; refresh marketplace docs with real magistral data
Систематический баг: 10 сценариев навигировали по URL без #/ (десктоп — hash-роутер),
из-за чего молча сохраняли спиннеры/404 вместо контента — корень «тонкого покрытия»
документации Marketplace. Плюс две устойчивости харнесса:
- loginAs/loginAsChairman: таймаут 60→150с (холодный Vite компилирует chunk роута
  signin дольше 60с на первом заходе);
- dismissOnboardingDialogs: распознавание онбординг-диалога на стадии «Формируем
  документ…» (кнопки «Подписать» ещё нет — ловим по фразе), иначе кадр ловит спиннер.
- operator/issuance: добавлен dismissOnboardingDialogs.

Доки обновлены реальными данными прогнанной e2e-магистрали Marketplace
(заказ → приёмка 2 подписи → ACCEPTED_TO_COOP → выплата COMPLETED → маркировка
EAN-13 → выдача signiss1/signiss2 → RECEIVED): каталог, мои заказы, сводный заказ,
предложения поставщика, входящие заказы, история выплат, ожидаемые поставки,
маркировка/склад КУ, лента выдач, экосистема, сеть ПВЗ.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 16:56:10 +00:00
coopops 32f1a89bc9 [E14][@dark-sun] feat(стол пайщика): скелетоны вместо спиннера в таблицах документов и платежей
Добавлен canon-компонент TableSkeleton (shared/ui/base) — повторяет
структуру .table-wrap/.table с реальными заголовками и мерцающими
плейсхолдерами (.skel) в ячейках. Каркас не дёргается при подгрузке
данных (calm-data, UX-DR2/UX-DR23). Заменил перекрывающий q-spinner
в DocumentsTable и ListOfPaymentsWidget.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 16:49:27 +00:00
coopops 24601722f7 [E14][@dark-sun] feat(стол пайщика): документы — ID копируемым бейджем, отдельные колонки Дата/Документ, подписи бейджами; поиск минимальный на мобильном
- DocumentsTable: наименование берём из meta.title (чистое, без даты/.pdf
  суффикса full_title); отдельная колонка Дата (meta.created_at) с сортировкой
  по block_num, по умолчанию свежие сверху; колонка Документ — только заголовок.
- ID — копируемый EntityIdBadge (показывает короткий хеш, копирует полный
  doc_hash по клику + иконка-affordance).
- Подписи — отдельные BaseBadge на подписанта (новый helper
  getSignersListFromDocumentPackage возвращает массив).
- EntityIdBadge канонизирован: токены вместо rgba/--q-accent/.q-dark; добавлен
  copyValue (показать одно — скопировать другое).
- SearchHeaderAction: на мобильном — минимальная round-dense иконка без подписи
  «Поиск» (label только на десктопе).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 16:18:33 +00:00
coopops fe0d23bd92 [E14][@dark-sun] feat(стол пайщика): документы и платежи — канон-таблица .table-wrap вместо q-table
Переделка после ревью: q-table давал не-канон вид, дёрганье при раскрытии
(virtual-scroll пересчитывал размеры + colspan не совпадал с числом колонок)
и уродливые мобильные карточки в grid-режиме.

- Статическая канон-таблица .table-wrap/.table (бордер+радиус+surface как
  карточка), table-layout:fixed → колонки не разъезжаются при раскрытии.
- Раскрытие строки — CSS-only (expand-row td colspan по числу колонок),
  без virtual-scroll → нет дёрганья шапки/колонок.
- Пагинация load-more (.table-foot + BaseButton «Загрузить ещё» + «1–N из M»)
  вместо infinite virtual-scroll.
- Мобильный: горизонтальный скролл таблицы (.table-scroll) вместо grid-карточек
  PaymentCard/DocumentCard — карточки удалены.
- Статусы платежей — BaseBadge (pos/warn/neg/info/neutral); направление —
  иконка + цвет --p-pos/--p-neg; хеш документа — mono.
- Действия платежей (SetOrderPaid/Refunded) скрыты на столе пайщика
  (hideActions=true) — они в реестре платежей; download документов сохранён.
- EmptyState + спиннер первой загрузки.

Виджеты общие с админ-контуром: load-more и горизонтальный скролл там тоже
уместнее jittery virtual-scroll.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 15:59:47 +00:00
coopops a23efb69a1 [E14][@dark-sun] feat(стол пайщика): документы и платежи в канон — статус-бейджи, токены, header-action на Teleport
- ListOfPaymentsWidget + PaymentCard: статус платежа через BaseBadge
  (canon-варианты pos/warn/neg/info/neutral) вместо хардкод q-color
  teal/orange/red/blue; убраны битый inline-стиль $toolbar-min-height,
  inline max-width, глобальный .q-list style-leak; цвета/тени/радиусы —
  на токены (--p-line/--p-ink-3/--p-shadow-card/--p-surface-2).
- DocumentsTable + DocumentCard: убраны битый inline-height, inline
  max-width, .q-dark-селекторы, --q-gray, хардкод rgba; тени/линии — токены;
  глобальный q-table__top style → scoped :deep; CardStyles import убран.
- ListOfDocumentsWidget: brand-leak toggle-color teal → primary.
- DocumentsPage: SearchHeaderAction со старого useHeaderActions store на
  canon Teleport (#header-actions-host), кнопка сама гейтится по features.search.
- DocumentsPage/PaymentsPage: канон-паддинг страницы.

q-table-движок (virtual-scroll/infinite-load/раскрытие строк/mobile-grid)
сохранён — виджеты общие с админ/председательскими контурами, регресс недопустим.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 15:31:56 +00:00
coopops 21be815e3b [E14][@dark-sun] fix(шапка): правая группа прилипала влево на страницах без действий — вернул margin-left:auto
Когда страница не телепортирует действия, .topbar__actions скрывается
display:none через :has(пустой host), но adjacent-селектор
.topbar__actions + .topbar__right всё равно матчился и обнулял margin-left
правой группы — она уезжала влево к крошке. Возвращаем auto в :has-правиле
(его специфичность выше). Также убрал лид-надпись на странице реквизитов.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 15:24:00 +00:00
coopops 29dab94a9a [E14][@dark-sun] feat(стол пайщика): удостоверение и реквизиты в канон — IdentityPanel/DataRow/BaseCard, header-action на Teleport
- ProfilePage (Удостоверение): IdentityPanel-шапка + BaseCard-секции (Учётная
  запись/Личные данные/Документы и реквизиты) на DataRow; убран CardStyles,
  хардкод rgba и .q-dark.
- PaymentMethods widget (Реквизиты): BaseCard на метод + DataRow + EmptyState
  вместо .info-label/.info-value и хардкод-цветов.
- PaymentMethodsPage: кнопка добавления реквизитов переведена с useHeaderActions
  store на canon Teleport (#header-actions-host), micro на мобильном.
- AddPaymentButton: триггер под canon micro-паттерн (как Deposit/WithdrawButton).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 12:13:32 +00:00
Alex Ant f20989e319 chore(release): publish
Build bootstrap container / build (push) Successful in 3m10s
Release / release (push) Successful in 25m29s
Release / trigger-coopenomics-docs (push) Successful in 2s
Release / trigger-mono-docs (push) Successful in 1s
Release / publish-packages (push) Successful in 14m27s
2026-05-25 16:55:38 +05:00
Alex Ant 2d3cd34e82 chore(release): publish
Build bootstrap container / build (push) Successful in 3m25s
Release / release (push) Successful in 25m24s
Release / publish-packages (push) Has been skipped
Release / trigger-coopenomics-docs (push) Has been skipped
Release / trigger-mono-docs (push) Has been skipped
2026-05-25 16:55:18 +05:00
claude 46cb511e43 fix: remove stray node_modules symlink committed into branch
Симлинк node_modules -> /home/admin/mono-ai-2/node_modules был случайно
закоммичен и ломал pnpm install (ENOTDIR) на любой машине без этого пути.
Правило .gitignore 'node_modules/' (со слешем) ловит только каталог, не
симлинк-файл — добавлено правило 'node_modules' без слеша.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 16:22:31 +05:00
ant 5a1d3342bf [598-34][@ant] docs: страница «Открытие выдачи (подпись председателя)» + сценарий harness — закрыть единственную дыру в документации полного пути поставки (выдача-открытие, signiss1), чтобы цепочка приёмка→выдача→RECEIVED была задокументирована end-to-end
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 11:14:19 +00:00
ant 4c1887105f [598-33][@ant] fix: довести кассирское подтверждение выплаты поставщику до COMPLETED — core payment_hash приравнен к сырому order_hash и outcome_hash нормализован в lowercase в payout-листенере — иначе gateway::outcomplete не находит on-chain объект выплаты (хэш=sha256 ≠ order_hash), а marketplace-projection не закрывается из-за рассинхрона регистра
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 11:13:36 +00:00
Alex Ant 1461ec466e Merge branch 'dev' of ssh://git.coopenomics.world:222/C9S/mono into dev 2026-05-25 15:52:33 +05:00
coopops 46e3686550 [E14][@dark-sun] feat(шапка): крошка-название страницы слева + фикс переноса заголовка ModalBase
Крошка: в #crumb рядом с BackButton выводится route.meta.title текущей
страницы (тот же, что подсвечен в меню) — именно для неё действия и
сдвинуты вправо. Длинное название обрезается ellipsis в .topbar__crumb b.

ModalBase: q-bar получил авто-высоту, заголовок переносится (white-space
normal + overflow-wrap), крестик прижат к верху — на узком экране титул
больше не обрезается сверху.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 09:49:19 +00:00
Alex Ant a625fc79e1 Merge branch 'dev' of ssh://git.coopenomics.world:222/C9S/mono into dev 2026-05-25 14:42:03 +05:00
coopops fb545b4122 [E14][@dark-sun] fix(WalletPage): micro-кнопки взноса/возврата в шапке на мобильном
Why: полноразмерные DepositButton/WithdrawButton в узкой мобильной шапке
раздувались — текст переносился в 2 строки, кнопки вылезали за высоту
topbar. micro-вариант (иконка + tooltip, flat/dense) и предназначен
для слота шапки.

What: Teleport-кнопки получают :micro='isMobile' (useWindowSize, <768px).
Мобильный — компактные иконки; десктоп — полные кнопки с подписью.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 09:41:18 +00:00
coopops e20ddb4a9c [E14][@dark-sun] fix(WithdrawButton): «получить возврат» → «Получить возврат» — заглавная
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 09:39:51 +00:00
ant 243298183c [598-32][@ant] fix: ownership-проверка превью акта выдачи (IDOR по order_id) — чтобы акт чужого заказа не утекал по подставленному id, доступ только заказчику или члену КУ выдачи 2026-05-25 09:37:39 +00:00
ant bc838dc471 [598-31][@ant] fix: логировать HTTP-статус и тело ошибки Novu вместо [object Object] — чтобы причина 422 была видна в логах при отладке провижининга 2026-05-25 09:37:39 +00:00
ant ee1feb014b [598-30][@ant] feat: батч-акцепт сводной заявки и триггер open_subscription (SDK + desktop + aggregator) — чтобы поставщик принимал накопленный объём одной кнопкой и запускал цикл подписки 2026-05-25 09:37:39 +00:00
ant 2599795678 [598-29][@ant] fix: привести STATUS_MAP к коротким on-chain именам (supplyprep/acceptcoop/readyrecv) — чтобы read-back дельты доходили до READY_TO_RECEIVE без ложного schema-drift 2026-05-25 09:37:39 +00:00
ant 786650cf2a [598-28][@ant] fix: дефолтная генерация EAN при выдаче этикеток партии — чтобы маркировка имущества работала без ручного указания штрих-кода 2026-05-25 09:37:39 +00:00
ant a74ba5e947 [598-27][@ant] docs: страница «Типы снабжения» для заказчика и поставщика + параметризация harness под cycle_type — чтобы пайщик понимал, что даёт каждый тип отсечки и как им управлять 2026-05-25 09:37:39 +00:00
ant 06bcb8b426 [598-26][@ant] fix: восстановить unit-тест supplier-action после расширения конструктора — вернуть controller-typecheck в зелёное (TS2554)
Конструктор MarketplaceOrderSupplierActionService оброс cycleRepo и
shipmentCreate (synthesizeIndividualShipment), тест остался на 4 аргументах
со времён Эпика 4. Добавлены моки cycleRepo.create/shipmentCreate.execute +
orderRepo.assignToCycle, инстанс собирается всеми 6 аргументами. tsc 0 ошибок,
jest 19/19.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 06:08:45 +00:00
ant dda4b480e0 [598-25][@ant] fix: авто-seed карты ПВЗ при reboot:extra и sentinel-username для анонимных мутаций — убрать ручной psql INSERT после ребута и устранить лог-шум NOT NULL в mutation_logs
- boot: seedMarketplaceKuDetails (idempotent ON CONFLICT) + CLI seed:marketplace-ku
  + шаг в extra_reboot.sh после coopback с ожиданием создания таблицы;
  на свежем стенде krg/odn/myt (ACTIVE) появляются автоматически.
- controller: MutationLoggingInterceptor пишет username='anonymous' вместо
  undefined для мутаций без JWT (login/register) — INSERT в mutation_logs
  больше не падает на NOT NULL, лог анонимной мутации сохраняется для аудита.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 05:58:36 +00:00
ant 9016a3b4ec [598-24][@ant] feat: read-only лента выплат поставщикам для совета (Payment:read:all) — закрыть placeholder-дыру на board/payouts-readonly реальным coop-wide обзором расчётов
- access-matrix: capability Payment:read:all для admin/board_readonly/board (член совета ведёт надзор за выплатами, подтверждает кассир)
- резолвер marketplaceListOutgoingPayments(supplier_account?, statuses?) под RequireMarketplaceAccess('Payment','read:all')
- repository.listAll(coopname, filter) + адаптер: coopname + опц. payee_account + In(statuses), created_at DESC
- SDK: Queries.Marketplace.ListOutgoingPayments + regen zeus (controller/zeus, sdk/src/zeus)
- desktop: BoardPayoutsReadonly с placeholder на реальную таблицу (фильтры поставщик/статус, сводка-счётчики, локализация статусов)
- harness-сценарий marketplace/board/payouts-readonly

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 05:23:23 +00:00
ant c7dadc5529 [598-23][@ant] feat: own-KU лента поставок ПВЗ, whoAmI.branches и выбор категорий по названию — устранить ручной ввод braname/ID и закрыть дыру оператора ПВЗ в Потоке IV 2026-05-25 05:02:53 +00:00
ant ef5467251b fix(sdk): remove duplicate marketplaceSupplierOrderActionResultSelector
The selector was declared twice with identical bodies in orderSelector.ts,
so `pnpm --filter @coopenomics/sdk build` failed with TS2451 (cannot
redeclare block-scoped variable). Latent because the SDK was not built
locally. Drop the second declaration.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 03:45:22 +00:00
ant e9667bce96 docs(marketplace): add returns/writeoff flow screenshots
Screenshots referenced by the updated return-approve, return-claim and
writeoff-propose pages.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 03:29:32 +00:00
ant 864871be5c feat(marketplace): returns/writeoff flow — backend fixes, UI, docs, harness
Поток III (возврат и списание): wrap signed return docs in
SignedDigitalDocumentInputDTO before toDocument() (flat-doc fix), rehydrate
jsonb-stored ISO dates to Date for the DateTime scalar in the return-claim
mapper. Frontend: orderer/operator return-claim pages + decision dialogs,
writeoff draft editor, branch-orders, issuance finalize, barcode scanner,
process info widgets. Update return-approve/return-claim/writeoff-propose
prose and screenshots.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 03:29:05 +00:00
ant 92e81d6a48 test(marketplace): repair stale onboarding/plugin unit tests
MarketplaceOnboardingService constructor grew to 4 args (soviet/wallet
blockchain ports) — the onboarding-service and l2-onboarding-flow specs
still called it with 2 and failed to compile. Pass stub ports (getOnboardingState
touches neither). plugin-initialize asserted the old warn-log about an
unmerged PR; production now logs info "File storage отключён конфигурацией".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 03:28:49 +00:00
ant 782d4d4668 fix(marketplace): own-KU ownership scoping in read/payload resolvers
Operators and КУ-chairmen could read inventory, issuances and acceptance
acts (АПП) of other branches: list resolvers returned coop-wide data and
signable-payload resolvers fetched documents by client-supplied id without
membership checks. Add own-KU scoping (isMemberOfBranch / orderer ownership)
across marketplaceListInventory, marketplaceListIssuancesByBraname,
issuance + apl-reception signable payloads, and tighten canAccess so that
an :all qualifier subsumes :own/:own-KU rather than requiring literal match.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 03:28:40 +00:00
ant 3edfa08090 fix(marketplace): добавить импорт formatAsset2Digits в ProcessWriteoffInfoWidget
Виджет использовал formatAsset2Digits в шаблоне без импорта — падал бы
с ReferenceError при открытии карточки процесса списания.
2026-05-24 19:07:31 +00:00
ant a744d74836 feat(marketplace): контрактный мост списания→повестка совета (Story 8.1) + восстановление ledger2-модели Стола заказов
- ledger2: восстановлены 11 операций o.mkt.* + wallet::CONVERT_TO_MEMBER +
  кошелёк w.mkt.member, утерянные кривым merge (operations.hpp/wallets.hpp
  откатились на legacy-сторону, тогда как accounts/processes остались на
  Эпике 11). CONSUME_BY_MEMBER → существующий WalletOp::BURN_BLOCKED (без
  дубля enum-слота). marketplace.wasm снова собирается.
- propwroff: action расширен (document2 statement, std::string meta);
  контракт сам ставит повестку inline-вызовом soviet::createagenda от
  permission_level{marketplace, active} (marketplace в contracts_whitelist).
- backend: propWroff несёт statement+meta (через SignedDigitalDocumentInputDTO
  .toDocument()); отдельный createWriteoffAgenda удалён из порта/адаптера/
  сервиса — кооператив не в whitelist, второй вызов создавал бы дубль повестки.
  cooptypes IPropWroff +statement+meta. spec 31/31.
- desktop: суммы списания форматируются через formatAsset2Digits на столе
  председателя, в ленте совета (фикс поля total_loss_amount→total_amount,
  показывалось «—»), в деталях проекта и виджете процесса.
- harness: writeoff-propose снова доводит проект до повестки совета
  (ENABLE_COUNCIL_SUBMIT=true после фикса моста).
2026-05-24 18:56:48 +00:00
coopops 710e0cda8c [E14][@dark-sun] feat(стол пайщика): кошельки списком во всю ширину + действия шапки через Teleport
Кошельки (по фидбэку — переносы выглядят плохо):
- .wallet-programs из grid (repeat auto-fill minmax) → flex-column: карточки
  идут списком во всю ширину страницы.
- canon .wallet__title/__sub: возвращён nowrap + ellipsis (откат wrap-фикса);
  на широкой строке текст почти всегда влезает, иначе — ellipsis.
- WalletCard + минимум-карточка: нативный tooltip `title` — при наведении
  виден полный текст. Убран .wallet--row reset (базовый снова nowrap).

Действия шапки (кнопки взноса/возврата пропали; нужна новая механика):
- Новый canon-механизм: страница телепортирует свои действия в шапку через
  <Teleport to="#header-actions-host">. Host — постоянный span с
  display:contents в #actions слоте CommonHeader (при loggedIn).
- :has()-правило прячет .topbar__actions, когда внутри только пустой host
  (нет ни store-кнопок, ни телепорта) — чтобы не было пустого разделителя.
- WalletPage переведён на Teleport (DepositButton/WithdrawButton),
  useHeaderActions store-механизм убран со страницы.
- Старый useHeaderActions оставлен для прочих 10 страниц — мигрируем
  и удалим отдельно. DepositButton сам скрыт, пока пайщик не принят
  (status !== 'active') — это защита, не баг.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 16:45:39 +00:00
ant dd1a908e81 docs(marketplace): локализованные скриншоты и проза приёмок на состоянии магистрали II
Пересняты на зелёной сборке с фиксом доступа председателя КУ: закрывающая
подпись приёмки, первая подпись поставщика, создание АПП — таблицы и диалоги
с русскими подписями статусов/вариантов. Проза приведена к подписям UI,
убран on-chain-жаргон (signchair/композитная транзакция). Обновлены
скриншоты выплат и выдачи.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 14:41:58 +00:00
ant 2e4f7f162e fix(marketplace): открыть приёмку председателям КУ + локализовать enum-подписи на столах
Убраны дубли роутов reception/labeling/branch-orders в extensions/market/install.ts:
сторонние entry с meta.roles:['chairman'] (core-роль главного председателя)
перекрывали canonical с requireMarketplaceOperator и отбивали председателей КУ
«Недостаточно прав» — регрессия открытия /market-pvz/* для KU-chairman.

Локализованы сырые enum (статус/вариант) в подписях столов: диалоги подписи
приёмки (председатель/поставщик), история выплат, подготовка поставки, выдача,
сводный стол КУ. Приёмки, ждущие подписи, всплывают наверх списка.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 14:41:45 +00:00
ant 7755acbc14 docs(marketplace): проза и свежие скриншоты 15 столов на состоянии магистрали II
Заказчик (мои заказы/каталог/сводный заказ/готово к получению), поставщик
(мои предложения/история выплат), оператор ПВЗ (склад/возвраты), председатель
(доступные категории/сводный склад), совет (повестка списаний/выплаты),
председатель КУ (сводный стол/список ПВЗ), подключение пайщика к программе.
Кадры на реальных данных стенда (заказы ekaterina, выплаты ivanpetrov,
наклейки КУ krg, партии), проза бизнес-языком без dev-жаргона.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 13:13:56 +00:00
ant df5defa4c0 chore(docs-harness): актуализировать фикстуры и подписи сценариев под состояние стенда
orders: список заказчика теперь непустой (фикс подписи + фильтр-таб «Получены»).
consolidated/payments: переключены на корректную перспективу (ekaterina-заказчик,
ivanpetrov-поставщик) — показывают реальные партии и платёжные запросы магистрали II.
warehouse: логин под chairkrg (read:own-KU) + подписание онбординга — таблица наклеек КУ krg.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 13:12:58 +00:00
ant 9a4ea9e001 fix(marketplace): убрать дубль const columns, ломавший prod-сборку SPA, + чистка жаргона на gate подключения
OffererPendingAplReceptionsPage: при сведении worktree→feat обе стороны конфликта
сохранили идентичный блок `const columns` — vue/compiler-sfc падал на «Identifier
already declared», ломая `quasar build` (но не controller tsc и не dev-Vite).
OnboardingMemberPickCpp: убран dev-жаргон из user-facing текста gate
(«Эпик 1 / L3 онбординг пайщика» → «Подключение пайщика к программе»;
«Шаблон документа № N в реестре кооператива» → бизнес-описание; «Marketplace» → «Стол заказов»).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 13:12:57 +00:00
ant 26cd309832 docs(marketplace): свежие скриншоты магистрали II на слитой ветке
Пересъёмка всех 12 сценариев магистрали II (offer-create → order-create →
incoming-orders → shipment-prep → приёмка АПП → подписи → маркировка →
выдача → финальная подпись) на merged-коде против текущего стенда.
Все 12/12 прошли e2e: createOffer/createOrder/approve sent=true status=200.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 12:32:33 +00:00
ant 215590f062 fix(marketplace): ревью PR #17 (batch 1) — оверлей, валюта из store, тексты на русском
- quasar.config.cjs: вернуть ESLint-оверлей (overlay:true) — нужен пользователю,
  vueTsc остаётся отключён (вешает машину); код eslint-чист, оверлей пуст на съёмке;
- OrderCreateDialog: символ валюты из system store (governSymbol), убран хардкод ₽;
- BoardAgendaWriteoff: «Сумма потерь» → «Сумма списания»;
- OffererMyOffers: «Offer'ы» → «предложения» (user-facing);
- OnSiteDecisionDialog: убрана англо-техспецификация из ярлыка («compensating forward атомарно»).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 12:19:09 +00:00
ant 3134acd4f0 merge(marketplace): свести breadth worktree-marketplace-docs-impl в depth feat/marketplace-docs
Сведение двух параллельных веток «Стол заказов» от общего предка 505a33f1db:
- depth (feat): магистраль II шаги 5-10 e2e, DocumentAggregate-канон двух подписей,
  forward-only guard статуса заказа (#151), свежий dev, очищенная проза магистрали II;
- breadth (worktree): доки Потоков III/IV (возврат/списание, день оператора ПВЗ),
  шаг 4 accept/decline individual, страницы по всем столам.

Приоритет разрешения 62 конфликтов:
- магистраль II сценарии/доки/backend-каноны → depth (ours);
- доки и сценарии Потоков III/IV → breadth (theirs);
- mkdocs.yml — объединённая навигация по столам + install-market/chairman-sign/issuance-finalize;
- order-create.service.ts → theirs (toChainCycleType вынесен в shared + надёжный tx_hash);
- infra.ts → ours (тонкий installExtraData + отдельный seed-marketplace-branches.ts).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 12:12:02 +00:00
ant 7927adb0fd docs(marketplace): «Стол заказов» — документация магистрали II + backend/e2e под неё
Документация (components/docs):
- 14 пользовательских страниц «Стол заказов» (onboarding/offerer/orderer/operator) + nav-секция в mkdocs.yml
- проза бизнес-языком по фактам со скриншотов; магистраль II (шаги 6–10) сняты e2e

Backend (controller):
- финальная подпись выдачи авторизуется по подписи заказчика в документе (устранён 403 на шаге 10)
- signable-payloads приёмки/выдачи отдают DocumentAggregate {hash, rawDocument, document} — канон второй подписи поверх документа первого подписанта без регенерации
- L3-mutation marketplaceSignOnboardingOffer + DTO; schema.gql + zeus regen
- DTO/мапперы приёмки (accept_braname/fact_quantity/reception_id/supplier_account/total_amount)

Desktop:
- OperatorInventoryLabeling/Issuance/Reception API + IssueActFinalizeDialog; KU-chairman доступ к /market-pvz/*

Harness (docs-harness):
- prod-SPA подход; toast/overlay-фикс в lib/harness.mjs; новые/обновлённые сценарии magistral II + diag-e2e-apl STOP_AFTER

Boot (installExtraData):
- ролевые фикстуры + 3 КУ, эмиссия L3-кошельков, активация КУ

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 11:40:31 +00:00
coopops 36694f648d [E11][@dark-sun] fix(CommonHeader): бренд-логотип inline-SVG в зелёном квадрате, как в личном кабинете
Why: на странице без логина логотип был <img :src='logo.svg'> — img рендерит
SVG в изоляции, не наследует currentColor, поэтому показывался чёрно-белым
и не реагировал на смену темы. В личном кабинете (WorkspaceSwitcher)
тот же logo.svg рендерится inline через v-html в зелёном квадрате
и наследует color (logo.svg на fill:currentColor) — «зелёненький
на зелёном фоне», одинаковый в обеих темах.

What: CommonHeader brand-slot переведён на тот же приём —
`logo.svg?raw` + v-html внутри .app-q-header__logo
(background var(--p-primary-soft), color var(--p-primary), 28px квадрат,
16px svg). Переключение темы больше не требуется — зелёный константен.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 06:39:59 +00:00
ant 4e2c91bd34 [598-4][@ant] feat(marketplace): фоллоуап Эпика 1 — L3 sign-mutation marketplaceSignOnboardingOffer + factory 1100/1101
Закрывает блокер magistral II документации (см. order-create.md / PLAN.md §9.13):
on-chain marketplace::createorder валил assertion «у пайщика не подписано
соглашение program_id=2 для w.mkt.member», потому что в фабрике документов
отсутствовали adapter'ы для шаблона/инстанса оферты ЦПП «Стол заказов», а
mutation подписи прямо со стола заказов была пропущена в Эпике 1 (помечена
фоллоуапом в onboarding/README.md и в blago issue 598-4).

Что сделано:

cooptypes:
- IVars.marketplace_program / marketplace_offer_template (optional). Юр.текст
  оферты остаётся «рыбой» — финальную редакцию готовит юр.отдел.
- UdataKey.MARKETPLACE_AGREEMENT_NUMBER / MARKETPLACE_AGREEMENT_CREATED_AT.
- 1101.MarketplaceOffer.Action расширён двумя обязательными полями: номер
  соглашения и дата (передаются явно из L3 service, без Udata-таблицы).

factory (Эпик 1 / Story 1.7 финализация):
- Actions/1100.MarketplaceOfferTemplate.ts + Templates/1100.
  MarketplaceOfferTemplate.ts (шаблон — только coop+vars, аналог
  998.BlagorostProgramTemplate).
- Actions/1101.MarketplaceOffer.ts + Templates/1101.MarketplaceOffer.ts
  (инстанс на конкретного пайщика — с common_user и номером соглашения).
- VarsSchema получил marketplace_program и marketplace_offer_template.
- index.ts и registry.ts factory регистрируют новые registry_id.

controller (Эпик 1 / Story 1.4 фоллоуап):
- DTO MarketplaceSignOnboardingOfferInputDTO — только signed document;
  coopname/username берутся из JWT context'а guard'а (защита от подмены).
- MarketplaceOnboardingService.signOnboardingOffer — лукап
  soviet::coagreements(coopname, type='marketplace'), вызов
  walletBlockchainPort.signProgramAgreement (program_id из coagreement,
  draft_id оттуда же, document — подписан фронтом).
- Mutation marketplaceSignOnboardingOffer в marketplace-onboarding.resolver.
  ts с возвратом свежего MarketplaceOnboardingState (UI не нужен второй
  запрос).

SDK:
- Mutations.Marketplace.MarketplaceSignOnboardingOffer wrapper (graphql-zeus
  regen для нового input MarketplaceSignOnboardingOfferInput).

desktop:
- OnboardingMemberPickCpp api: signOnboardingOffer() — генерация документа
  через DigitalDocument(registry_id=1101), подпись локальным WIF, вызов
  новой mutation.
- OnboardingMemberPickCppPage.vue: onAccept теперь подписывает прямо со
  стола (раньше редиректил в Registrator). По успеху — Notify «Оферта
  подписана» + redirect /market/catalog.
- OnboardingCPPGate widget: новый пропс busy для loading-state кнопки
  «Подписать оферту».

PLAN.md §9.14 — закрывающая запись по выбранному варианту A.

NB: schema.gql и SDK zeus/index.ts регенерированы автоматически — попадают
в коммит как часть SDK сборки. Текст оферты в cooptypes 1100/1101 остаётся
«рыбой» до утверждения юр.отделом; registry_id и подписи менять не
потребуется.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 10:20:33 +00:00
ant cd063c728d Merge remote-tracking branch 'origin/dev' into feat/marketplace-docs
# Conflicts:
#	.github/workflows/build-contracts-docs.yaml
#	.github/workflows/build-contracts.yaml
#	.github/workflows/publish-docs.yaml
#	.github/workflows/release.yaml
#	components/blago-cli/package.json
#	components/boot/package.json
#	components/cleos/package.json
#	components/contracts/cpp/ledger2/src/common/walletop.cpp
#	components/contracts/cpp/lib/core/ledger2/operations.hpp
#	components/contracts/cpp/lib/core/ledger2/wallets.hpp
#	components/contracts/cpp/wallet/p.wal.wthdrw.standard.yaml
#	components/contracts/package.json
#	components/controller/package-lock.json
#	components/controller/package.json
#	components/controller/schema.gql
#	components/cooptypes/package.json
#	components/cooptypes/src/ledger2/operations.ts
#	components/cooptypes/src/ledger2/wallets.generated.ts
#	components/desktop/package.json
#	components/desktop/quasar.config.cjs
#	components/docs-harness/lib/harness.mjs
#	components/docs-harness/package-lock.json
#	components/docs-harness/package.json
#	components/docs-harness/scenarios/onboarding/09-chairman-key-and-login.mjs
#	components/docs/package.json
#	components/factory/package.json
#	components/inter/package.json
#	components/migrator/package.json
#	components/notifications/package.json
#	components/parser/package-lock.json
#	components/parser/package.json
#	components/sdk/package.json
#	components/sdk/src/mutations/capital/generateGenerationConvertStatement.ts
#	components/setup/package.json
#	lerna.json
2026-05-23 05:56:58 +00:00
ant 172da20004 [153][@dark.sun] WIP: marketplace docs e2e magistral II — before dev merge
Накопленная работа по реализации e2e сценариев Стола заказов:
backend (MarketplaceKuChairmenService + spec, KU-chairman guard,
notification broadcast), frontend (market-session-store FSD-extension,
beforeEnter роутер-гарды, 11 новых Vue-страниц marketplace), SDK
wrappers (whoAmI, listPendingOffers/myOffers/supplierOrders, moderation
log, CPP queries), harness-сценарии и MD-черновики для magistral II
шагов 1-4.

Перед merge dev (101 коммит с perf-фиксами controller/desktop/mongo)
для дальнейшего прогона magistral II шага 5+.
2026-05-23 05:54:19 +00:00
coopops f20764adbd Merge branch 'dev' into worktree-design-wave1
Подтянуть hotfix controller TypeORM, SWC dev-builder и mongo standalone.
2026-05-23 05:33:20 +00:00
coopops 712080ff8f Merge branch 'dev' into worktree-design-wave1
Подтянуть свежие изменения dev в дизайн-ветку перед продолжением миграции стола пайщика.
2026-05-23 05:09:17 +00:00
coopops 76513b8526 [E14][@dark-sun] fix(canon .wallet): просто wrap по пробелам, без overflow-wrap: anywhere
Why: предыдущий фикс `overflow-wrap: anywhere; word-break: break-word`
ломал слова в любом месте даже когда колонка достаточно широкая —
«Минимальный неснижаемый остаток» рендерился по одному слову на строку,
несмотря на ~480px доступной ширины. Реально нужен только wrap по пробелам;
agressive break-word оправдан только для URL-подобных нерасчленяемых строк.

What: убраны `overflow-wrap` и `word-break` из .wallet__title/__sub —
браузер делает естественный wrap по пробелам, длинные заголовки переносятся
только когда не помещаются. `.wallet--row` reset для compact-варианта
оставлен — там по-прежнему single-line+ellipsis.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 04:50:01 +00:00
coopops 0872c4c093 [E14][@dark-sun] fix(canon .wallet): разрешить перенос длинных заголовков на full-варианте + порядок карточек в WalletProgramWidget
Why: canon-стиль .wallet__title и .wallet__sub был с
`white-space: nowrap; overflow: hidden; text-overflow: ellipsis;` — на full-
варианте это резало длинные подписи («Минимальны…», «Возвращается п…»)
даже на просторных экранах, потому что grid-колонка `.wallet__main` сжимается
ради `.wallet__amount` справа. Эта обрезка будет всплывать на любой
длинной строке (метки программ, статусы пайщика, длинные subtitle).
Compact-вариант `.wallet--row` (слот шапки) должен остаться одной строкой.

What:
- .wallet__title/__sub: убран nowrap/ellipsis; добавлено
  `overflow-wrap: anywhere; word-break: break-word; hyphens: auto` (для title)
  и `overflow-wrap: anywhere; word-break: break-word` (для sub) — длинные
  заголовки переносятся на 2+ строки.
- .wallet--row: явно возвращает nowrap+ellipsis (с reset word-break/
  overflow-wrap), чтобы compact-ряды в шапке оставались строго одной строкой.
- WalletProgramWidget: карточка минимального неснижаемого остатка
  перемещена в начало grid'а (была после программ) — это базовая защита
  средств пайщика, логичнее видеть её первой.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 04:45:30 +00:00
coopops c94cb3759a [E14][@dark-sun] refactor(WalletCard,WalletProgramWidget): «Главный кошелёк» канон-default + минимальный остаток отдельной карточкой
Why:
- Заголовок «Кошелёк» для program='wallet' путал — в столе пайщика этот
  кошелёк семантически главный, и в любых других местах canon (MicroWallet,
  WalletCardMini, _dev/ui) он тоже должен называться полно — «Главный
  кошелёк». Лучше один canon-default, чем локальный override в каждом
  потребителе. Согласовано — переименование canon DEFAULT_TITLES.
- Минимальный неснижаемый остаток — НЕ баланс кошелька, а самостоятельная
  сущность пайщика (паевой взнос, возвращается при выходе). Пристегивать
  его DataRow-строкой под grid'ом — нелогично, как было и раньше в legacy.
  Правильнее — отдельной карточкой в той же сетке кошельков.

What:
- WalletCard.vue: DEFAULT_TITLES.wallet 'Кошелёк' → 'Главный кошелёк'.
  Глобально для всех потребителей canon-компонента.
- WalletProgramWidget.vue: убран TITLE_OVERRIDE и DataRow-блок минимального
  остатка. Карточка остатка теперь рендерится в общем .wallet-programs
  grid'е canon-разметкой `.wallet` с иконкой `savings`, заголовком
  «Минимальный неснижаемый остаток», подзаголовком «Возвращается при выходе
  из кооператива» и зарезервированной суммой. Нейтральная подсветка иконки
  через `.wallet--minimum { --prog-bg, --prog-fg }`, чтобы визуально
  отличалась от программ, но встала в общую сетку.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 04:39:27 +00:00
coopops 40cbd3fef9 [E14][@dark-sun] feat(WalletProgramWidget): «Главный кошелёк» + минимальный неснижаемый остаток
Why: на старом WalletWidget пайщик видел свой минимальный неснижаемый остаток
(паевой взнос, возвращается при выходе из кооператива) — самостоятельная
сущность пайщика, не баланс кошелька. При переходе на canon я её упустил.
Канон-default WalletCard 'wallet' = «Кошелёк» — для стола пайщика этот
кошелёк семантически является главным (свободный остаток ЦК), поэтому
локально перекрываем заголовок на «Главный кошелёк».

What:
- TITLE_OVERRIDE['wallet'] = 'Главный кошелёк' — локальное перекрытие
  заголовка только в этом widget'е, canon DEFAULT_TITLES не трогаем
  (другие потребители WalletCard могут использовать общий «Кошелёк»).
- DataRow «Минимальный неснижаемый остаток» под grid'ом программ
  (только когда session.participantAccount.minimum_amount > 0)
  с hint «Возвращается пайщику при выходе из кооператива».
- Источник остатка — session.participantAccount?.minimum_amount,
  как и в legacy WalletWidget.

Note: locked-line уже рендерится самим canon WalletCard, когда
locked-balance > 0 (logic: hasBlocked ? blocked.amount : undefined
в WalletProgramWidget). Отдельная разметка под «Заблокировано» не нужна.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 04:30:00 +00:00
coopops cdb12efb62 [E14][@dark-sun] fix(WalletProgramWidget): vue-tsc — flatMap вместо filter+predicate
Why: type predicate `e is CanonProgramEntry` не сходился — optional `locked?: string`
в interface vs required `locked: string | undefined` в литерале map'а. TS считает
эти типы несовместимыми для predicate, хотя они эквивалентны при присваивании.

What: переход на `flatMap<CanonProgramEntry>` с возвратом `[]` для исключаемых
программ. Predicate не нужен, generic flatMap даёт точный тип результата.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 04:20:59 +00:00
coopops 7bad49522b [E14][@dark-sun] refactor(participant/wallet): WalletPage + WalletProgramWidget в канон MONO Platform v2
Why: первая страница стола пайщика (default route 'wallet') использовала legacy
обвязку — CardStyles import, scoped SCSS с .body--dark и hardcoded rgba цветами,
ColorCard с произвольным цветом по индексу. Канон уже знает программы платформы
(blagorost/wallet/generator) через WalletCard + токены --prog-*; на нём и строим.

What:
- WalletPage.vue: убран import 'src/shared/ui/CardStyles', scoped SCSS на
  токенах --p-6/--p-4; вырезана легаси-карточка «Минимальный остаток»
  (она не отображалась — не было разметки в template). useHeaderActions
  для Deposit/Withdraw оставлен — канон поддерживает actions через #actions slot.
- WalletProgramWidget.vue: переписан на canon WalletCard + EmptyState.
  Фильтр на канон-набор программ через ZEUS_TO_CANON
  (MAIN→wallet, BLAGOROST→blagorost, GENERATOR→generator);
  MARKETPLACE и прочие исключены — это не из основной тройки платформы
  и должны рендериться отдельным виджетом.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 04:14:41 +00:00
coopops 463801beed [E11][@dark-sun] feat(AppHeader): brand slot — лого + название кооператива на странице без логина
Why: на главную без логина нужно показывать бренд кооператива (логотип + имя)
рядом с глобальными действиями шапки; раньше выводилось только текстовое
название через title-проп без логотипа.

What:
- AppHeader.vue: опциональный slot #brand перед .topbar__crumb;
  hasBrand computed по slots.brand. Когда slot заполнен — крошка не рендерится.
- components.css: стили .topbar__brand (desktop + .topbar--mobile вариант)
  с canon-токенами (--p-fs-body, --p-ink, --p-fs-meta).
- CommonHeader.vue: на !loggedIn заполняет #brand src/assets/logo.svg
  + <b>{coopTitle}</b>; передача title-пропа убрана.
- default.vue: .fixed-top-right { top: 51px } → top: var(--p-topbar-h)
  (canon 56px) — выравнивание FAB под точную высоту шапки.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 04:14:22 +00:00
coopops f02df5a8d0 [E11][@dark-sun] refactor(CommandPalette): иерархия рабочих столов и страниц вместо generic pages/actions/recent
Существующая модель cmdk в проекте (entities/CmdkMenu/model/store.ts) —
иерархия рабочих столов и их страниц. Активный стол sticky сверху с
бейджем «Активный», без запроса показывается иерархия (стол + indented
страницы), с запросом — плоский список со столом-префиксом у каждой
страницы; стол отдельной строкой появляется только если запрос явно
начинается с его имени или содержит «стол»/«workspace».

Переписал canon CommandPalette под эту модель:
- Props: `workspaces: CommandPaletteWorkspace[]` вместо
  `commands: CommandItem[]`. Каждый workspace = `{ name, title, icon,
  isActive?, pages: CommandPalettePage[] }`. Page = `{ name, title,
  icon?, shortcut? }`.
- Emits: `select-workspace(name)`, `select-page(workspaceName, pageName)`
  — props-only, навигация и filtering по ролям/conditions остаются
  заботой connected-обёртки (миграция legacy CmdkMenu — отдельная story).
- Sticky-баннер активного стола, plus accent-soft фон + outline-обводка
  на selected, ↑↓ работает плоско поверх иерархии (стол → страницы → стол
  → страницы).

Mock-data в /_dev/ui/index.vue обновлён: три стола (Председатель/Пайщик/
Отчётность) со своими страницами вместо плоского списка команд.

Старый widgets/Desktop/CmdkMenu пока живёт параллельно — переключим в
ходе миграции Wave 3.
2026-05-22 18:41:46 +00:00
coopops e14f0b6a1d [E11][@dark-sun] feat: NotificationCenter, CommandPalette, DetailsDrawer — эпик «Навигация и нотификации»
Реализованы три props-only доменных компонента из E11:
- NotificationCenter — panel-content для popover в шапке: группировка
  notifications по category (system/financial/voting/message), unread-bullet
  через BaseBadge, кнопка «Прочитать все», EmptyState и «Показать все».
  Relative-date форматирование с русским склонением.
- CommandPalette — ⌘K/Ctrl+K с fuzzy-поиском, секции recent/pages/actions,
  ↑↓ навигация, Enter/Esc обработка. localStorage недавних — в connected
  обёртке, компонент props-only.
- DetailsDrawer — side-sheet справа 480px (override через :width), slots
  default/actions/footer, Esc и backdrop close, на xs — fullscreen.

Все три зарегистрированы в boot/ui.ts и локально импортированы в /_dev/ui
с mock-data в секциях 36-38.

E11.4 RailUserCard отложен из-за конфликта имён — существующий компонент
имеет другую роль; нужно согласовать канон-нейминг.
2026-05-22 18:24:55 +00:00
coopops bed7ba0a13 [E10][@dark-sun] fix(BaseDocument): центрировать spinner-лоадер по обеим осям
Заменил `.full-width.text-center` + flex без выравнивания на flex-column
со `min-height: 360px` — лоадер больше не прижат к левому верху карточки.
Заодно поправил опечатку «подговка» → «Формируем документ».
2026-05-22 18:13:07 +00:00
coopops 2de5f81997 [E10][@dark-sun] fix(AmountInput): нативный :suffix='symbol' вместо #append slot
Корень: я положил символ валюты в #append slot своим <span>, в обход
встроенного prop suffix. Quasar имеет правила позиционирования именно
для родного .q-field__suffix (см. revert d0a8dc0080 от 2026-05-19,
где было решено оставить Quasar дефолт — «нас устраивает»). Мой span
в append-slot не подчинялся этим правилам и сидел в произвольной
позиции относительно цифр.

Фикс:
- :suffix='symbol' — символ валюты идёт нативным механизмом Quasar.
- Удалил .amount-input__symbol класс и template #append вообще.
- Удалил override align-items: center на q-field__control/__append/
  __after (тоже мешал, как было показано в d0a8dc0080).
- font-weight 500 на цифрах оставил — нормальный вес поля.

Совет на будущее: использовать встроенные q-input props (suffix,
prefix), а не #append/#prepend slots, если можно — Quasar для них
держит готовое выравнивание, проверенное пользователем.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 18:04:21 +00:00
coopops 3c6fbba4ea [E10][@dark-sun] fix(AmountInput): выровнять цифры и RUB-суффикс по центру
Корень: font-weight: 600 + tabular-nums + Quasar dense нативный input
имеют чуть смещённую baseline относительно append-слота, где сидит
RUB. Визуально цифры лежали ниже суффикса.

Фикс:
- font-weight 600 → 500 (нормальный вес поля ввода, без bold-акцента
  на цифрах).
- Явный font-size + line-height на нативном input.
- align-items: center на q-field__control / __append / __after, чтобы
  суффикс и any after-слот (кнопка «макс») центрировались по высоте
  входной полосы.
- align-self: center на самом __symbol — на случай если q-field__append
  кто-то переопределит как stretch.

TODO на следующий подход: BaseDocument loader («Формируем документ…»)
выравнивать по центру рамки документа (сейчас стоит сверху).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 17:47:59 +00:00
ant b298810f87 [598-4][@ant] feat(marketplace): шаг 4 magistral II — accept/decline individual order через UI
OffererIncomingOrdersPage был read-only, кнопки «Принять/Отказать» в OrderCard'ах эмитили @action событие, которое родительский page не слушал. Подключил обработчик:

- SDK обёртки Mutations.Marketplace.AcceptIndividualOrder и DeclineIndividualOrder + selector MarketplaceSupplierOrderActionResult.
- api/index.ts: acceptIndividualOrder(order_id) и declineIndividualOrder(order_id, reason).
- Vue: onCardAction → onAccept или onDecline; decline открывает Quasar Dialog.prompt для reason (canon UX: причина показывается пайщику-заказчику в его заказе).
- harness: incoming-orders.mjs шаги 03-individual-pending и 04-after-accept, ждёт mp-order-card вместо waitForTimeout (исключает гонку с fetchSupplierOrders).

E2E прогон: ekaterina создаёт order c8a26513 (статус ACCEPTED_PENDING_SUPPLIER_INDIVIDUAL после фикса monotonic #151), ivanpetrov открывает /market/incoming-orders → таб «Индивидуальные ожидающие» → видит карточку № c8a26513 (2 ед., 240 ₽, ПВЗ krg) с кнопками «Принять/Отказать». Mutation marketplaceAcceptIndividualOrder отправляется на backend, backend дёргает chain action acceptOrder; цепь отвергает по max_transaction_cpu_usage=290000us (303363us занял chain — известный блокер #162, чинится отдельно через setparams).
2026-05-22 16:48:23 +00:00
ant c017568a12 [598-4][@ant] fix(marketplace): monotonic order status в updateFromBlockchain — backend cycle-hook не откатывается delta'й
backend `applyCycleTypeHook` ставит forward-статус (ACCEPTED_PENDING_SUPPLIER_INDIVIDUAL для cycle_type=individual) сразу после submit'а createorder, затем парсер через ~2с проигрывает delta с on-chain status=active и `MarketplaceOrderDomainEntity.updateFromBlockchain` затирает forward-статус. Это блокировало magistral II шаг 4: поставщик не видел заказ в табе «Ждут моего акцепта», статус оставался ACTIVE.

Фикс — monotonic check в updateFromBlockchain: incoming status применяется только если его ранг ≥ текущего ИЛИ статус терминальный (CANCELLED_*, EXPIRED_*, RETURNED, RECEIVED). on_chain_id / on_chain_block_num / on_chain_present обновляются всегда.

+5 unit-тестов в marketplace-order.entity.spec.ts: forward (ACTIVE → ACCEPTED), два backward-кейса игнорируются (ACCEPTED ← ACTIVE и INDIVIDUAL ← ACTIVE), терминальный (CANCELLED_BY_ORDERER поверх ACCEPTED), одинаковый ранг (PENDING_SUPPLIER ↔ INDIVIDUAL). Все 12 тестов в файле зелёные.
2026-05-22 15:18:04 +00:00
ant c59245f8ba [598-4][@ant] docs(plan): §9.16 — magistral II ЗАКРЫТА через UI, три корневые причины холодного Vite
Полный 4-шаговый прогон magistral II работает end-to-end:
offer-create (individual) → offer-moderation (approve) → order-create (ekaterina, 240 RUB)
→ incoming-orders (ivanpetrov видит обе карточки с «Принять/Отказать»).

Документируем три корневые причины Vite-залипания и оверлея, которые годами
мешали harness'у работать стабильно после reboot/restart:

1. cooptypes/dist/index.mjs может отсутствовать → Vite виснет на import резолве;
   лечится `pnpm --filter cooptypes build` + cp в host'овый
   /home/admin/mono-ai-4/components/cooptypes/dist/.
2. Vite optimizeDeps >20s на холодном route — `waitForTimeout(3000)` истекает
   до отрисовки; заменили на `waitFor(<якорь страницы>, timeout: 90000)`.
3. localStorage harness:noBranchOverlay читается в watch-branch-overlay только
   при mount'е первого auth-tick — setItem ПОСЛЕ loginAs опаздывает; через
   addInitScript ДО loginAs флаг гарантированно установлен.

Остаточный issue (не блокирует UI): backend cycle-hook ставит
ACCEPTED_PENDING_SUPPLIER_INDIVIDUAL, парсер перетирает на ACTIVE через 2с —
race с BlockchainConsumerService.
2026-05-22 14:17:56 +00:00
ant 5d6a56922d [598-4][@ant] fix(harness): magistral II разблокирована — waitFor + addInitScript для холодного Vite
Закрывает magistral II через UI: offer ivanpetrov individual ACTIVE → order ekaterina
status «Размещён» виден в offerer/incoming-orders с действиями «Принять/Отказать».

Три однотипных фикса под холодный Vite optimizeDeps в desktop dev:

— offer-moderation.mjs: ждём заголовок «Модерация предложений» — иначе кнопка
  «Одобрить» не находится в пустом Quasar-спиннере и approve не происходит.
— order-create.mjs: ждём mp-onboarding-gate / banner / q-page после goto
  member-cpp, и Заказать-кнопку / mp-catalog-empty после goto catalog. До этого
  схватывал пустой spinner и падал «В каталоге нет CatalogOfferCard».
— incoming-orders.mjs: harness:noBranchOverlay через addInitScript ДО loginAs.
  Реактивность watch-branch-overlay.ts читает localStorage только при mount'е
  первого auth-tick — поэтому setItem после loginAs опаздывает и оверлей
  «Выберите КУ» рендерится первым, перекрывая страницу incoming-orders.

Прогоны 4/4:
  ✓ offerer/offer-create   — offer ivanpetrov individual PENDING_MODERATION
  ✓ chairman/offer-moderation — approve, offer → ACTIVE
  ✓ orderer/order-create   — order ekaterina на individual offer, 240 RUB списано
  ✓ offerer/incoming-orders — обе карточки (15e70181, 09c9d6c8) показаны
2026-05-22 14:16:33 +00:00
ant 003ddb06c5 [598-4][@ant] fix(harness): offer-create — ждём label "Название" + правильный селектор radio individual
— Vite dev на холодном старте долго optimizeDeps'ит chunk'и страницы create-offer;
  waitForTimeout(3000) не успевал и шот ловил пустой Quasar-спиннер; теперь ждём
  конкретно label "Название" с большим timeout (90s) — надёжный сигнал что форма
  отрисовалась, а не SPA bootstrap.
— Quasar q-radio в q-option-group рендерится как <div class="q-radio">, не <label>;
  селектор `label.q-radio` молча промахивался и форма уходила с default cycle_type=
  time_based (cycle_days=7), magistral II опять упиралась в cron-aggregator.
  Сейчас .q-radio:has-text(...) + фолбэк по тексту — radio individual реально
  кликается, offer попадает в БД с cycle_type='individual'.

Прогон harness'а: 4/4 шота, новый offer ivanpetrov individual PENDING_MODERATION
в marketplace_offer.
2026-05-22 13:55:01 +00:00
ant 55d8c0a50d [598-4][@ant] feat(harness): offer-create выбирает cycle_type=individual для magistral II
Default time_based с cycle_days=7 заставляет заказ ekaterina висеть в status=ACTIVE
7 дней до перехода в ACCEPTED_PENDING_SUPPLIER (через @Cron aggregator). Для
harness/документации нужен синхронный переход.

cycle_type=individual обрабатывается синхронно в
marketplace-order-create.service.ts:248 — Order сразу попадает в
ACCEPTED_PENDING_SUPPLIER_INDIVIDUAL без cron-aggregator'а.

Добавлен click по radio «Индивидуально (individual)» в форме перед submit.

ПРИМЕЧАНИЕ: на текущем стенде Vite в desktop dev контейнере нестабилен после
нескольких прогонов harness — даёт пустой spinner. Альтернатива через
GraphQL-сидер (Task #122, seed-marketplace-flow.ts) надёжнее и не зависит от UI.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 12:45:53 +00:00
ant b9d107f953 [598-4][@ant] docs(plan): найден блокер cycle_days=7 в фикстуре offer'а для magistral II
Magistral II разблокирована тремя коммитами PR #24 — order-create зелёный, e2e
проходит до Notify «Заказ создан» и списания Main Wallet. При прогоне следующих
страниц (offerer/incoming-orders, orderer/orders, consolidated и т.д.)
зафиксированы пустые empty-state: заказ висит в status=ACTIVE.

Корневая причина: MarketplaceCycleAggregatorService @Cron каждые 5 минут
переводит ACTIVE → ACCEPTED_PENDING_SUPPLIER, но только когда offer.created_at +
cycle_days <= now(). Текущий offerer/offer-create.mjs использует default
cycle_days=7 — заказ ждёт 7 дней. Для документации нужен offer с
cycle_type=individual (синхронный переход в order-create.service.ts:248) или
cycle_days=0.

§9.15 в PLAN.md описывает варианты разблокировки: либо изменение
offerer/offer-create.mjs (создавать individual-offer параллельно), либо
TS-сидер seed-marketplace-flow.ts (Task #122).

Дополнительная находка: некритичный MutationLoggingInterceptor пишет
"null value in column username" — кандидат в отдельный фикс.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 11:10:32 +00:00
ant fa10ddec39 [598-4][@ant] fix(marketplace): normalizeTxResult — wharfkit @1.6.x кладёт transaction_id в tx.response
* MarketplaceOrderCreateService.normalizeTxResult бросал
  «цепь не вернула tx_hash» хотя on-chain createorder проходил
  и кошелёк списывался. Причина: код искал `tx?.transaction?.id`
  и `tx?.processed?.id`, но @wharfkit/session@1.6.1 возвращает
  TransactResult структуры `{ response: { transaction_id,
  processed: { id, block_num } }, transaction: ResolvedTransaction
  (без id-поля), resolved: ResolvedSigningRequest, ... }`. Поля
  `.processed` на верхнем уровне нет, `.transaction.id` либо
  undefined либо Checksum256 (не string).

* Правильно: `tx.response.transaction_id` (+ fallback'и на
  `resolved.transaction.id` и плоский `transaction.id` для
  совместимости с другими wharfkit-версиями — как в `extractTxHash`
  marketplace-apl-reception / return-claim / issuance / writeoff
  сервисов, где этот pattern уже работал).

E2E подтверждён: ekaterina подписывает ЦПП → marketplaceCreateOrder
проходит → Notify «Заказ создан» → кошелёк списан → offer
quantity_available уменьшился. Magistral II полностью разблокирована.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 10:35:02 +00:00
ant 1ae4d8959f [598-4][@ant] fix(marketplace,harness): variables wrapper для marketplaceSignOnboardingOffer + harness не дёргает host pnpm dev
* desktop OnboardingMemberPickCpp/api: client.Mutation(mut, { input })
  не передавал переменную в GraphQL — правильный pattern
  `{ variables: { input } }` (как в submitCreateOrder). Backend отвечал
  «Variable $input of required type MarketplaceSignOnboardingOfferInput!
  was not provided», подпись ЦПП не доходила до on-chain. После фикса
  `wallet::signagree` (voskhod/ekaterina program_id=2 draft_id=699)
  проходит, marketplace::createorder больше не падает на
  «walletop: не подписано program_id=2».

* docs-harness/bin/shoot.mjs: ветка `wasUp && distRebuilt` делала
  `pkill -f 'quasar.js dev'` на хосте + spawn хостовый `pnpm run dev`.
  Quasar dev живёт в docker-контейнере, pkill на хосте ничего не убивает,
  а параллельный хостовый pnpm dev писал в .q-cache под admin uid, пока
  контейнер пишет под node uid — EACCES, кэш разваливается, Vite
  перестаёт резолвить @coopenomics/sdk. Если порт отвечает — ничего
  не трогаем, vite сам подхватит dist через fs-watch.

* harness order-create.mjs: переписан под полный flow magistral II
  с шагом подписи L3 ЦПП через /market/onboarding/member-cpp +
  debug-снимок 00-marketplace-onboarding-page для будущей диагностики.

Magistral II по L3-подписи разблокирована. Остался отдельный блокер
в MarketplaceOrderCreateService.normalizeTxResult — backend не дождётся
tx_hash от цепи, хотя on-chain action createorder проходит
(out-of-band sync error). Это вне фоллоуапа Эпика 1.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 10:01:34 +00:00
coopops a67180a153 [E10][@dark-sun] fix(OtpInput): убрать TypeScript-касты as из pug-шаблона
[Quasar] boot error: SyntaxError: Unexpected identifier 'as' — runtime-парсер
обрабатывает выражения в pug-template как чистый JS, без TypeScript. Касты
вида `el as HTMLInputElement | null`, `e as InputEvent`, `e as KeyboardEvent`
прямо в атрибутах `:ref` / `@input` / `@keydown` — синтаксическая ошибка во
время бутстрапа Vue, из-за которой /_dev/ui целиком не грузился.

Фикс:
- :ref='(el) => setRef(idx, el)' + функция setRef(idx, el: Element |
  ComponentPublicInstance | null) с кастом в TS-скрипте.
- @input='(e) => onInput(idx, e)' + сигнатура onInput(idx, event: Event)
  с внутренним кастом target.
- @keydown оставил передавать event «как есть» — KeyboardEvent — никакой
  cast не нужен.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 09:38:34 +00:00
ant e7144e9f3d [598-4][@ant] feat(marketplace): фоллоуап Эпика 1 — L3 sign-mutation marketplaceSignOnboardingOffer + factory 1100/1101
Закрывает блокер magistral II документации (см. order-create.md / PLAN.md §9.13):
on-chain marketplace::createorder валил assertion «у пайщика не подписано
соглашение program_id=2 для w.mkt.member», потому что в фабрике документов
отсутствовали adapter'ы для шаблона/инстанса оферты ЦПП «Стол заказов», а
mutation подписи прямо со стола заказов была пропущена в Эпике 1 (помечена
фоллоуапом в onboarding/README.md и в blago issue 598-4).

Что сделано:

cooptypes:
- IVars.marketplace_program / marketplace_offer_template (optional). Юр.текст
  оферты остаётся «рыбой» — финальную редакцию готовит юр.отдел.
- UdataKey.MARKETPLACE_AGREEMENT_NUMBER / MARKETPLACE_AGREEMENT_CREATED_AT.
- 1101.MarketplaceOffer.Action расширён двумя обязательными полями: номер
  соглашения и дата (передаются явно из L3 service, без Udata-таблицы).

factory (Эпик 1 / Story 1.7 финализация):
- Actions/1100.MarketplaceOfferTemplate.ts + Templates/1100.
  MarketplaceOfferTemplate.ts (шаблон — только coop+vars, аналог
  998.BlagorostProgramTemplate).
- Actions/1101.MarketplaceOffer.ts + Templates/1101.MarketplaceOffer.ts
  (инстанс на конкретного пайщика — с common_user и номером соглашения).
- VarsSchema получил marketplace_program и marketplace_offer_template.
- index.ts и registry.ts factory регистрируют новые registry_id.

controller (Эпик 1 / Story 1.4 фоллоуап):
- DTO MarketplaceSignOnboardingOfferInputDTO — только signed document;
  coopname/username берутся из JWT context'а guard'а (защита от подмены).
- MarketplaceOnboardingService.signOnboardingOffer — лукап
  soviet::coagreements(coopname, type='marketplace'), вызов
  walletBlockchainPort.signProgramAgreement (program_id из coagreement,
  draft_id оттуда же, document — подписан фронтом).
- Mutation marketplaceSignOnboardingOffer в marketplace-onboarding.resolver.
  ts с возвратом свежего MarketplaceOnboardingState (UI не нужен второй
  запрос).

SDK:
- Mutations.Marketplace.MarketplaceSignOnboardingOffer wrapper (graphql-zeus
  regen для нового input MarketplaceSignOnboardingOfferInput).

desktop:
- OnboardingMemberPickCpp api: signOnboardingOffer() — генерация документа
  через DigitalDocument(registry_id=1101), подпись локальным WIF, вызов
  новой mutation.
- OnboardingMemberPickCppPage.vue: onAccept теперь подписывает прямо со
  стола (раньше редиректил в Registrator). По успеху — Notify «Оферта
  подписана» + redirect /market/catalog.
- OnboardingCPPGate widget: новый пропс busy для loading-state кнопки
  «Подписать оферту».

PLAN.md §9.14 — закрывающая запись по выбранному варианту A.

NB: schema.gql и SDK zeus/index.ts регенерированы автоматически — попадают
в коммит как часть SDK сборки. Текст оферты в cooptypes 1100/1101 остаётся
«рыбой» до утверждения юр.отделом; registry_id и подписи менять не
потребуется.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 09:14:53 +00:00
coopops 3ed84f5ffc [E10][@dark-sun] fix(_dev/ui): локальный импорт E10-компонентов — boot/ui.ts не HMR'ится
Quasar boot-файлы подхватываются только при рестарте dev-сервера. После
коммита 1af0f9c06f глобальные регистрации AmountInput/OtpInput/FilterBar/
FileUploader/VerticalStepper не подтянулись на лету — теги рендерились как
unknown components (пусто внутри секций 31–35 на /_dev/ui).

Фикс: добавил локальные импорты прямо в script setup _dev/ui/index.vue —
тот же паттерн, что у WalletCard, RailUserCard, AuthCard. Глобальная
регистрация в boot/ui.ts остаётся для боевого использования из других
страниц (после следующего рестарта dev'а).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 09:07:30 +00:00
coopops 1af0f9c06f [E10][@dark-sun] feat: AmountInput, OtpInput, FilterBar, FileUploader, VerticalStepper — эпик «Формы и ввод»
Пять props-only доменных компонентов из shared/ui/domain/:
- AmountInput — денежный ввод с символом валюты, форматированием тысячных,
  precision из marketplace asset config, кнопкой «макс» по balance, опциональной
  подписью баланса. Tabular-nums, right-align, font-weight 600.
- OtpInput — 6 ячеек с автопереходом фокуса, Backspace откатывает на
  предыдущую, paste 6-значного кода распределяется по ячейкам.
  Регулярка /^\d$/, состояния error/disabled.
- FilterBar — search (debounce 300мс) + dropdown-фильтры + chip'ы активных
  значений с remove + «сбросить всё». v-model для values, v-model:search для
  поиска. Активные chip'ы рендерятся под рядом фильтров.
- FileUploader — drag&drop + клик по зоне; валидация accept/maxSize/maxFiles
  → emit error; список загруженных с иконкой типа, именем, размером,
  кнопкой ×; слот progress для connected-обёртки.
- VerticalStepper — состояния pending/current/completed/error; completed
  кликабельны для возврата назад (опционально); опциональные/disabled шаги;
  слот active под телом текущего шага.

Все компоненты — pug, canon-токены --p-*, без store/router/api. Демо-секции
31–35 на /_dev/ui (наш стенд-витрина).

Зарегистрированы в boot/ui.ts и shared/ui/domain/index.ts. ESLint точечно
прошёл, vue-tsc не запускался полностью на desktop (запрет).

Wave 2 / E10 закрыт.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 08:29:49 +00:00
ant c63093eaa4 [598][@ant] docs(plan): §9.13 — magistral II заблокирован фоллоуапом Эпика 1 (factory 1100/1101 + L3 mutation marketplaceSignOnboardingOffer)
Без решения пользователя по развилке A/B/C дальнейшие итерации
по магистрали II невозможны — все oставшиеся shots требуют успешного
order-create, а тот упирается в отсутствие подписи program_id=2.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 08:21:02 +00:00
ant 28cf0552d3 [598][@ant] docs(marketplace): order-create — переписать блокер на «ЦПП program_id=2 не подписана»
Что обновлено:
- shot 03-order-create-no-funds → 03-order-create-no-agreement (новый имя+PNG со свежим Notify).
- order-create.md admonition заменена: реальный блокер не «нет средств», а отсутствие записи в `wallet::users.programs[]` с program_id=2 (marketplace, draft_id=699). Зафиксированы две root cause-причины (factory adapter 1100 + L3 mutation marketplaceSignOnboardingOffer), две решённые подблокировки (cycle_type mapper + marketplace-deposit-fund скрипт).
- orders/01-my-orders-empty.png — обновлён PNG (тот же empty-state).

Почему. ekaterina пополнена 10000 RUB через wallet::createdeposit + gateway::completeincome (CHAIN_URL=8918 — критично для скрипта marketplace-deposit-fund.ts), но createorder валит assertion на o.mkt.assign (TRANSFER из w.wal.member в w.mkt.member): пайщик не имеет программного соглашения marketplace. Реальный L2-flow (core signup + factory render 1100) не работает в этом стенде, потому что (1) factory adapter 1100/1101 отсутствует в components/factory/src/Actions/, (2) L3 mutation для прямой подписи со стола обозначена как фоллоуап Эпика 1 в onboarding/README.md. Документация теперь явно описывает, что именно требуется доделать.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 07:55:31 +00:00
ant 5503ba9967 [598][@ant] feat(boot,plan): one-shot deposit-fund скрипт — найден блокер joincoop membership
Создан components/boot/src/scripts/marketplace-deposit-fund.ts —
эмиссия в Main Wallet тестовых пайщиков через
wallet::createdeposit + gateway::completeincome (pattern из
seed-capital/08-investments.ts).

Запуск (ekaterina, sidorov) → fail:
«Пайщик не найден в кооперативе» (coops_access_helpers.hpp:57).

Root cause: текущий seed (add-plain-participant + ensureMarketplaceParticipant)
создаёт on-chain registrator account + mongo individual + pg user, но НЕ
делает joincoop через совет → запись в soviet::participants отсутствует →
createdeposit рекурсивно падает на membership check.

Архитектура кошельков (пояснил пользователь):
  Main Wallet → членских взносов → программы (Стол заказов и др.)
  Пополнение всегда в Main; конвертация автоматическая через
  o.wal.conv / o.mkt.assign / o.mkt.block.

PLAN §9.12 фиксирует развилку (Task #138):
  A) joincoop в installExtraData (быстрый seed)
  B) UI-сценарий onboarding/join-coop (документация UX)
  C) расширить add-plain-participant.ts (membership per default)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 07:15:10 +00:00
ant 2f69072fcc [598][@ant] fix(controller,docs,harness): mapper cycle_type→eosio::name — магистраль II разблокирована до o.wal.block
cycle_type Postgres-domain (snake_case) теперь конвертируется в eosio::name
(без `_`) на boundary chain submit:

  marketplace-offer.types.ts:
    + MARKETPLACE_CYCLE_TYPE_CHAIN_NAME registry
    + toChainCycleType(cycle) helper (соответствует enum-правилу CLAUDE.md)
  marketplace-order-create.service.ts:160:
    cycle_type: toChainCycleType(offer.cycle_type)

Mapping: time_based→timebased, volume_based→volumebased,
open_subscription→opensubscr, individual→individual
(см. components/contracts/cpp/lib/domain/table_marketplace_orders.hpp:69-71).

После фикса pipeline проходит cycle_type-валидацию и доходит до
`o.wal.block`, где упирается в новый блокер: пустой L3-кошелёк тестовых
пайщиков (0,00 RUB). UI корректно показывает Notify
«Недостаточно средств для заказа: требуется 240.0000 RUB, доступно 0.0000 RUB»
через extractErrorMessage().

Обновлены:
- order-create.md admonition (старый блокер resolved, новый описан)
- shot переименован 03-order-create-onchain-error → 03-order-create-no-funds
- PLAN §9.11 фиксирует решённый и новый блокер
- Task #137 заведён для пополнения L3-кошельков в installExtraData

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 06:56:06 +00:00
ant 2e49415831 [598][@ant] feat(desktop,harness,docs): orderer/order-create — реальный submit + найден on-chain блокер
- OrderCreateDialog.vue: extractErrorMessage() — Error/ApolloError/graphQLErrors/response.errors, иначе UI показывал [object Object]
- API: переключён на Queries.Marketplace.ListKUDetails (Branches.GetBranches требует chairman + NPE PaymentMethod в свежем worktree)
- scenario: 3 шота — пустой диалог / заполненная форма / Notify «Неизвестный тип цикла отсечки заявок»
- order-create.md: admonition «Известный блокер магистрали II» с указанием root-cause
- PLAN.md §9.10: backend хранит cycle_type='time_based' (snake_case), контракт marketplace::createorder ожидает eosio::name 'timebased' (без подчёркивания) — нужен mapper в backend
- assets: удалён stale 01-order-create-stub-notify.png, добавлены 3 актуальных

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 06:42:49 +00:00
coopops daae6170c7 fix(DocumentPreview): убрать txt/image — наши документы это HTML/PDF
Удалены типы 'txt' и 'image' из DocumentPreviewType + соответствующие
ветки рендера (pre.document-preview__txt и img.document-preview__image)
и стили. В реальных потоках платформы документ кооператива всегда
HTML (рендерится в ShadowHtml внутри BaseDocument) или PDF — plain-
text фрагмент в моноширинном pre не используется и выглядит как
техническая ерунда. Аналогично image.

_dev: удалён previewTxtDemo (мусорный EOSIO chain id из debugging
notes → перешитый в фрагмент протокола → теперь полностью убран
по требованию пользователя). Секция 30 показывает только HTML +
loading + error состояния.
2026-05-22 06:22:56 +00:00
ant 787878dae4 [598][@ant] feat(desktop,docs,harness): orderer/order-create — реальный submit через диалог в каталоге
- OrderCreateDialog.vue: q-input «Количество» + q-select «ПВЗ доставки» + расчёт итоговой суммы; submit вызывает Mutations.Marketplace.CreateOrder
- MarketplaceCatalogPage.vue: onSelectOffer открывает диалог вместо stub Notify, по @created — перезагрузка каталога
- API: fetchBranchOptions (Queries.Branches.GetBranches) + submitCreateOrder
- scenario orderer/order-create.mjs: 3 шота — пустой диалог / заполненная форма / Notify «Заказ создан»
- order-create.md: приведена к реальному MVP (убраны выдуманные «акт о паевом взносе», «комиссия», «программный кошелёк» — convention «не упоминать неиспользуемое»)
- PLAN.md §9.10: фиксация развилки §9.9 в пользу (A) UI

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 06:22:24 +00:00
coopops 46bae0a5de fix(BaseDocument,_dev): vue-tsc, мусорный txt-demo, ширина документа
vue-tsc:
- _dev: убрал txId/explorerUrl из signatureSignedDemo — оба поля
  удалены из Signature ранее (общего эксплорера нет).
- BaseDocument: canonSignatures.map — нормализую is_valid через
  '?? undefined' (бэкенд может вернуть null, canon-компонент ждёт
  boolean | undefined).

DocumentPreview txt demo: вместо мусорного 'EOSIO chain id …
dirty window' (мой случайный кусок из debugging notes) — фрагмент
протокола собрания пайщиков ПК «Восход».

ComplexDocument: .col-md-7 → .col-md-10. Это контейнер, в котором
лежит BaseDocument в реальных страницах документов. 7/12 = 58%
ширины было визуально 'приплюснуто'.
2026-05-22 06:01:55 +00:00
ant a8105eda21 [598][@ant] feat(harness,plan): order-create stub перешот + §9.9 со следующим блокером
orderer/order-create.mjs прогнан после магистрали II (Offer есть в каталоге),
но UI выводит stub Notify — реального сабмита marketplaceCreateOrder нет.
PLAN.md §9.9 фиксирует развилку: (A) доработать UI OrdererOrderCreate.vue
с реальной mutation + Membership flow или (B) TS-сидер через GraphQL.
(A) предпочтительно — даёт документируемую страницу.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 05:55:45 +00:00
ant 2db39b56bf [598][@ant] feat(harness,docs): магистраль II — Offer создан → одобрен → виден в каталоге
Прогон цепочки через harness:
1. offerer/offer-create — Сидоров публикует «Картофель Адретта 25 кг»
2. chairman/offer-moderation — председатель одобряет → ACTIVE
3. orderer/catalog — Петрова видит offer в каталоге
4. offerer/my-offers — Сидоров видит свой offer в статусе ACTIVE

Также фикс meta пути в scenarios/marketplace/chairman/offer-moderation.mjs:
docPath/assetsDir указывали в new/marketplace/board/, что создавало дубль при
каждом install — теперь канон-путь new/marketplace/chairman/offer-moderation.
PNG перенесены в правильный каталог assets/.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 05:53:42 +00:00
coopops a6c315ba32 feat(ui): DocumentSignatures + перевод BaseDocument на canon
Реальный формат подписей кооперативного документа — это не «pending/
signed/rejected» из абстрактного SignatureCard, а IDocumentAggregate с
полями doc_hash + signatures[] (signer_certificate, public_key,
signature, is_valid). Каждая подпись разворачивается в детали.

Что сделано:
- Новый canon-компонент DocumentSignatures (story 9.5) в shared/ui/
  domain — props-only, принимает уже резолвнутые signerName и hash-
  совпадение, эмитит download/verify. Поверх Quasar — собственный
  expand на ref<Set<number>>, чтобы стиль был полностью canon.
- BaseDocument теперь рендерит DocumentSignatures вместо своего
  q-card.verify-card + q-list + q-expansion-item на teal/red badges.
  Адаптер canonSignatures маппит signer_certificate → ФИО через
  getNameFromCertificate, чтобы canon-компонент не знал про сертификаты.
- SignatureCard: удалены поля txId/explorerUrl и ссылка «Открыть в
  explorer» — общего эксплорера в платформе нет.

Demo: секция 29 → DocumentSignatures (валидный + с битой подписью),
секция 30 → DocumentPreview (HTML заголовок сокращён, чтобы не
обрывался при узкой колонке).
2026-05-22 05:52:19 +00:00
ant 82db8eefcb [598][@ant] docs(marketplace): убрать «счёт 60 не используется» из payouts-readonly
В документации не нужно перечислять то, чего в системе нет — это шум.
Описывать только используемые счета и операции.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 05:41:58 +00:00
ant 336f45de5a [598][@ant] docs(marketplace): сверка с PRD MVP — счёт 91, двухэтапная L12, услуги вне MVP
Точечные правки на основе PRD «Стол заказов» MVP (~blago/production/1-prilozhenie-stol-zakazov):

- agenda-writeoff: операция o.mkt.wroff проходит через транзитный счёт 91
  «Прочие расходы» в две проводки (Dr 91 / Cr 10, затем Dr 86 / Cr 91), а не
  одной операцией списания с «накопителя». Транзит через 91 фиксирует
  безвозвратное исчезновение имущества, не передачу пайщику.

- payouts-readonly: двухэтапная модель L12 (FR57). На signchair срабатывает
  o.mkt.purch per-Order (фиксирует обязательство на счёте 86), создаётся
  pending-запись. После банковского перевода кассир подтверждает, backend
  дёргает o.mkt.payout (Дт 86 / Кт 51). Счёт 60 не используется.

- category-whitelist: категория «Услуги» помечена «вне MVP» — самостоятельная
  категория поставки в MVP не реализуется (требует отдельной правовой
  проработки). Заголовок раздела обновлён: «фиксированный набор категорий
  имущества» вместо «10 категорий».

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 05:21:03 +00:00
ant b9df0bfdef [598][@ant] docs(plan): §9 актуализация — admonition сняты, §9.8 план сидера Магистрали II
Магистраль I empty-state закрыта (9 страниц implemented, harness прогнан,
PNG installed, admonition «реализовано»). Добавлен §9.8 — конкретный план
сидера для Магистрали II: TS-скрипт seed-marketplace-flow.ts с GraphQL
chain из 6 mutations + on-chain APP-цепочка. Альтернатива — композитный
harness scenario через Playwright. Перечислены 13 шотов, которые нужно
переснять после прогона сидера.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 05:17:12 +00:00
ant ee06d40513 [598][@ant] docs(marketplace): admonition «Реализовано» — harness прогнан, ожидание снято
Harness уже отстрелял 8 страниц, PNG установлены, проза описана — слово «ожидается
прогон harness» в admonition больше не актуально. Чистый статус «Реализовано» без
лишнего шума о промежуточном состоянии разработки.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 05:13:22 +00:00
ant c58cbc416a [598][@ant] docs(marketplace): доставить PNG из 7 ранее снятых сценариев
Установлены недостающие PNG из существующих shots/ в docs/assets:
- operator/issuance → branch-chairman/issuance-open
- operator/returns → branch-chairman/return-approve
- operator/warehouse → operator/inventory-list
- chairman/branches: добавлены 3 недостающие PNG (pvz-network, dialog-empty,
  autocomplete-chairman, branch-details)
- branch-chairman/pvz-list → chairman/branches
- orderer/returns → orderer/return-claim
- 5 PNG обновлены поверх старых (свежее состояние UI)

MD-страницы не перезаписаны — admonition «Реализовано» и проза из PRD
сохранены. Дубль board/offer-moderation.md (auto-generated draft с
TODO) удалён — оригинал в chairman/offer-moderation.md остаётся.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 21:06:10 +00:00
ant 5611f66303 [598][@ant] docs(marketplace): подключить PNG-снимки harness в 9 MD-страниц
Все скриншоты прогона harness'а (cedde9f277) теперь видны в MD через
![alt](/assets/new/marketplace/.../.png) после admonition «Реализовано».
Существующая проза из PRD сохранена без изменений.

Страницы: orderer/consolidated, offerer/incoming-orders, offerer/offers,
chairman/category-whitelist, branch-chairman/branch-orders,
board/agenda-writeoff, board/payouts-readonly, onboarding/coop-accept-cpp,
onboarding/member-pick-cpp.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 20:02:22 +00:00
ant cedde9f277 [598][@ant] feat(docs-harness): fix 3 упавших сценариев + добавлены 6 PNG
Поправки:
- offerer/incoming-orders.mjs: фикстура sidorov → ivanpetrov +
  signAllAgreements (sidorov не подписал онбординг, диалоги перехватывали
  клики).
- offerer/my-offers.mjs: ivanpetrov + signAllAgreements +
  docPath/assetsDir выровнены на offers.md (Vue page = OffererMyOffers,
  но MD-страница уже создана под именем offers.md с прозой из PRD).
- branch-chairman/branch-orders.mjs: chairkrg не имеет marketplace_pvz
  membership на voskhod (роут /market-pvz/* требует roles: ['chairman']
  кооператива). Используем loginAsChairman (ant) — председатель сам
  открывает стол КУ для аудита; отдельный access-уровень «branch chairman»
  подключится в Story 6.x+1.

Установлены PNG: incoming-orders (2), offers (2), branch-orders (2).
Все 9 из 9 marketplace-сценариев успешно сняты harness'ом против
desktop dev :3029 (docker-mount worktree → /app в mono-ai-4-desktop-1).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 19:29:12 +00:00
ant df53a8f484 [598][@ant] docs(marketplace): install 6 PNG-снимков с прогона harness
Скриншоты:
- onboarding/coop-accept-cpp (2 шота: статус + диалог принятия)
- onboarding/member-pick-cpp (1 шот: gate или баннер)
- orderer/consolidated (1 шот: overview партий)
- chairman/category-whitelist (2 шота: overview + add-dialog)
- board/agenda-writeoff (2 шота: overview + on-agenda фильтр)
- board/payouts-readonly (1 шот: placeholder)

MD-прозы не перезаписаны (admonition «Реализовано» уже на месте, прозы
из PRD остаются валидны). Картинки подключатся в существующий текст MD
автоматически через relative paths.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 19:24:17 +00:00
ant 6a05e41942 [598][@ant] docs(marketplace): admonition «в разработке» → «реализовано» в 9 MD
Заменил warning admonition «Эпик X — в разработке» на info admonition
«Реализовано — ожидается прогон harness» с указанием коммита реализации
и команды harness'а для будущего прогона. Прозы сохранены — описания
из PRD остаются валидны как факт.

board/payouts-readonly остался в статусе «Placeholder — в подготовке»
(требует backend-расширения Payment/read:all).

Обновлены: orderer/consolidated, offerer/incoming-orders, offerer/offers,
onboarding/coop-accept-cpp, onboarding/member-pick-cpp,
chairman/category-whitelist, branch-chairman/branch-orders,
board/agenda-writeoff, board/payouts-readonly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 18:43:10 +00:00
ant 1e4c03de5e [598][@ant] feat(docs-harness): 6 оставшихся сценариев для marketplace-страниц
Завершает набор harness-сценариев для всех 9 реализованных страниц:
- onboarding/member-pick-cpp.mjs (ivanpetrov, L3 gate)
- offerer/incoming-orders.mjs (sidorov, входящие заказы)
- offerer/my-offers.mjs (sidorov, мои Offer'ы)
- branch-chairman/branch-orders.mjs (chairkrg, сводный стол КУ)
- board/agenda-writeoff.mjs (chairman ant, повестка совета)
- board/payouts-readonly.mjs (chairman ant, placeholder)

Создана папка scenarios/marketplace/board/ (раньше отсутствовала).

Прогон harness'ом по-прежнему ЗАБЛОКИРОВАН — shared desktop dev не
видит worktree pages. До решения по подхвату 9 Vue-страниц в shared
desktop сценарии существуют как готовая инфраструктура.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 18:39:18 +00:00
ant be02498815 [598][@ant] feat(docs-harness): 3 пилот-сценария для новых страниц marketplace
Сценарии для прогона harness'ом по реализованным страницам:
- onboarding/coop-accept-cpp.mjs (chairman ant, L1 ЦПП)
- orderer/consolidated.mjs (ivanpetrov, партии заказчика)
- chairman/category-whitelist.mjs (chairman ant, whitelist категорий)

Прогон самим harness'ом пока ЗАБЛОКИРОВАН — shared desktop dev :3029
не видит worktree-страниц (src vs node_modules: src в worktree локален,
а node_modules симлинкованы). Чтобы снять скриншоты:
вариант A — sync 9 pages в shared desktop;
вариант B — поднять отдельный worktree desktop dev (медленно).

Остальные 6 сценариев (offerer/incoming-orders, offerer/my-offers,
branch-chairman/branch-orders, board/agenda-writeoff, board/payouts-readonly,
onboarding/member-pick-cpp) — создаются после прогона пилот-тройки.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 18:06:02 +00:00
ant 02c3f68d71 [598][@ant] docs(plan): закрыть §9.6/§9.7 — 9 из 9 страниц marketplace реализованы
Обновил PLAN.md по факту:
- §9.6 теперь не «дыры UI», а карта реализованных страниц с коммитами.
- §9.7 пересмотрен: аудит был пессимистичен. Backend для 8 из 9 страниц
  уже был DONE — нужны были только SDK wrappers и Vue (8 страниц добавили
  только UI, board/payouts-readonly — placeholder).

Урок для будущих аудитов: перед оценкой «нужно backend» делать grep по
marketplace-*.resolver.ts:@Query / @Mutation — список из 70+ резолверов
покрывает почти все MVP-use-case'ы.

Технический долг после 9 страниц: backend для board/payouts-readonly
(Payment/read:all policy), tree-выбор для category-whitelist, auto-detect
braname для branch-chairman, прогон harness'а по 9 страницам.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 17:32:22 +00:00
ant 147951356a [598][@ant] feat(sdk,desktop): onboarding/member-pick-cpp — L3 gate ЦПП Marketplace
Эпик 1 / Story 1.4 + 1.11. Backend marketplaceOnboardingState уже DONE,
но SDK wrapper отсутствовал — добавлен Queries.Marketplace.MarketplaceOnboardingState.

Vue OnboardingMemberPickCpp: страница-точка-входа на подключение к Стол
заказов. Канон-виджет OnboardingCPPGate (UX-DR17) показывает пакет ЦПП
(оферта + Положение). Сама подпись делается через core Registrator-мастер
(wallet::signagree) — страница редиректит туда после accept.

Когда requires_gate=false — баннер «уже подключены» + переход к каталогу.
mp-role-orderer; роут /market/onboarding/member-cpp (все роли с auth).

Закрывает 9-ю из 9 нереализованных страниц Marketplace из PLAN.md §9.6.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 17:30:35 +00:00
ant 261be9a270 [598][@ant] feat(desktop): board/payouts-readonly — informational placeholder
Эпик 5 / Story 5.x. Совет нуждается в ленте всех выплат поставщикам;
текущий резолвер marketplaceListOutgoingPaymentsAsSupplier отдаёт только
выплаты текущего пайщика-supplier'а. Для совета нужна доп. policy
`Payment / read:all` в marketplace-access-matrix + новая query
`marketplaceListOutgoingPayments` с опц. supplier_account.

До этого — informational placeholder со ссылками на нужные файлы и
объяснением, как раздел заработает. До Phase 2 совет видит выплаты
через core cassir-стол (общий стол кассира кооператива).

mp-role-admin; роут /market/payouts (chairman + member совета).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 17:25:00 +00:00
ant 50538b45d4 [598][@ant] feat(sdk,desktop): chairman/category-whitelist — доступные категории
Эпик 3 / Story 3.x. Backend available-category-admin.resolver.ts уже DONE
(@AuthRoles chairman). Добавлены 4 SDK wrappers:
- Queries.Marketplace.GetAvailableCategories
- Queries.Marketplace.GetAvailabilityStats
- Mutations.Marketplace.AddAvailableCategories
- Mutations.Marketplace.RemoveAvailableCategories

Vue ChairmanCategoryWhitelist: список разрешённых категорий (entire +
specific types), 4 stat-карточки (всего/категорий/типов/whitelist активен),
кнопка «Добавить» с диалогом ручного ввода ID через запятую, удаление
с подтверждением. Tree-выбор через marketplaceGetCategoryTree — следующий
шаг Story 3.x.

mp-role-admin; роут /market/category-whitelist (chairman).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 17:22:52 +00:00
ant 2a41354c32 [598][@ant] feat(desktop): board/agenda-writeoff — read-only лента совета
Эпик 8 / Story 8.x. Совет видит проекты списания скоропорта в активных
статусах (ON_AGENDA / AUTHORIZED / EXECUTING / EXECUTED / REJECTED).
DRAFT-проекты скрыты — это рабочая зона администратора на /writeoffs.

Само голосование совета идёт через core soviet agenda (sov-flow);
здесь — обзор статусов и сумм потерь, фильтр по статусу, polling 30s.

Backend готов — reuse helper listWriteoffProposals из AdminWriteoffs/api
+ Queries.Marketplace.ListWriteoffProposals. Cast as never на statuses
из-за расхождения standalone vue-tsc и enum-merge (Zeus генерит enum
со values-как-литералами, vue-tsc вне -p не подхватывает merge).

mp-role-admin; роут /market/board-writeoff (chairman + member совета).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 17:17:20 +00:00
ant d1bebf6f64 [598][@ant] feat(desktop): branch-chairman/branch-orders — сводный стол КУ
Эпик 6 / Story 6.x. Председатель КУ видит 3 ленты процессов своего
участка в одном экране с табами и счётчиками:

- Приёмки партий (Queries.Marketplace.ListAplReceptionsByBraname, Эпик 5)
- Выдачи заказов (Queries.Marketplace.ListIssuancesByBraname, Эпик 6)
- Гарантийные возвраты (Queries.Marketplace.ListReturnClaimsByBraname, Эпик 7)

Backend готов — обновление аудита §9.7 второй раз: «нужна агрегационная
query» неверно, 3 существующих query достаточно через Promise.all.

Read-only обзор; действия выполняются на /reception, /issuance, /returns.
Поле braname — ручной ввод (auto-detect через whoami — следующий шаг).

Polling 20s; mp-role-operator; роут /market-pvz/branch-orders (chairman).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 17:13:26 +00:00
ant 69d3e79f36 [598][@ant] feat(sdk,desktop): offerer/incoming-orders + my-offers — стол поставщика
Эпик 3 / Story 3.4 + Эпик 4 / Story 4.5. Backend marketplaceListMyOffers
и marketplaceListSupplierOrders уже DONE — было пропущено в аудите §9.7.
Добавлены только SDK wrappers Queries.Marketplace.ListMyOffers / ListSupplierOrders
и две Vue-страницы.

OffererIncomingOrders (/market/incoming-orders): заказы где supplier = пайщик,
канон OrderCard, фильтр по статусу через q-tabs, polling 15s.

OffererMyOffers (/market/my-offers): свои Offer'ы во всех 4 статусах
(PENDING_MODERATION / ACTIVE / REJECTED / WITHDRAWN), канон CatalogOfferCard,
client-side фильтр + поиск, polling 30s. Для REJECTED показывается
reject_reason под карточкой.

mp-role-offerer + marketplace-tokens на обеих.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 17:08:45 +00:00
coopops ccfaf3592e feat(ui): волна 2 / E9 — документы и подписи
4 props-only canon-компонента в src/shared/ui/domain/, регистрация
в boot/ui.ts, demo-секции 26-29 в _dev/ui:

- DocumentRow (story 9.1) — строка документа в списке: иконка типа
  с tint'ом (pdf neg-soft, docx info-soft, html primary-soft), title,
  status через BaseBadge, дата/автор/описание, slot actions, emit open.
- SignatureCard (story 9.3) — подписавший (Avatar+ФИО+AccountBadge),
  статус (BaseChip pending/signed/rejected), для signed — хеш в моно-
  блоке + ссылка на explorer, для rejected — BaseBanner с причиной.
- ActivityTimeline (story 9.4) — вертикальный таймлайн событий с
  цветными иконками по типу (sign/reject/create/update/comment/
  transfer), groupByDate группирует по «Сегодня/Вчера/конкретная
  дата».
- DocumentPreview (story 9.2) — html (через DOMPurify), pdf через
  iframe, image через img, txt через pre. Стейты loading/error.

Все props-only, темо-зависимые значения только через --p-* токены.
2026-05-21 17:02:51 +00:00
ant e40b74fcf9 [598][@ant] feat(desktop): orderer/consolidated — сводный обзор партий заказчика
Эпик 4 / Story 4.4. Страница для пайщика-заказчика: его Order'ы группируются
по cycle_id (time_based/volume_based/open_subscription); индивидуальные —
отдельной группой. На каждой партии: этап (минимальный активный по STAGE_RANK),
суммарная стоимость, число заказов, ПВЗ. Внутри — канон-виджет OrderCard.

Источник данных — Queries.Marketplace.ListMyOrders (reuse MyOrders/api).
Polling 15s до Subscriptions Story 9.x. mp-role-orderer + marketplace-tokens.

Роут /market/consolidated (roles: chairman/member), иконка fa-layer-group.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 17:00:19 +00:00
coopops de245ee810 fix(_dev/ui): «Член кооператива» → «Пайщик» в PersonCard demo
Канон-терминология blago/ЦК: пайщик, не член. Прецедент закреплён
в auto-memory feedback_kooperatsiya_pai.
2026-05-21 16:57:50 +00:00
coopops e7dd0ce641 fix(IdentityPanel): compact тоже в карточке — padding+border+bg
Раньше compact-вариант рендерился без обрамления — avatar упирался
в левый край контейнера, визуально «проваливался» из общего стека
full-карточек (Screenshot_2026-05-21_21-51-09).

Compact теперь: padding 8/12px (тоньше чем full 16px), тот же border
+ surface + radius. В одну строку avatar + ФИО + AccountBadge + status,
но визуально это всё-таки карточка, не голая строка.
2026-05-21 16:52:41 +00:00
coopops e1113391cf fix(ContactSheet): label и значение в отдельных строках
Body раньше был обычным block-контейнером, а .head и .value-row —
оба display: inline-flex. Без явного block-форматирования inline-flex
элементы становятся inline и рендерились в одну строку. Визуально:
Emailivanov@example.ru, Телефон+7 (903)..., Telegram@ivanov — слипшиеся.

Body теперь display: flex + flex-direction: column, gap canon — 8px
в comfortable, 4px в compact. Head/value-row переведены на обычный
display: flex (вместо inline-flex), gap внутри сохранён.

Прецедент 2026-05-21 — Screenshot_2026-05-21_21-42-16, 21-42-31.
2026-05-21 16:44:40 +00:00
ant 510ccf545b [598][@ant] feat(sdk,desktop): L1 onboarding marketplace — страница приёма ЦПП кооперативом
Зачем: пользователь обнаружил что 9 страниц Marketplace из PLAN.md §9.6 физически отсутствуют в Vue коде (бэкенд для Эпиков 1/3/4/5/6/8 DONE, фронтенд не реализован). Закрываю первую и самую компактную из 9 — Эпик 1 / Story 1.9-1.10 (L1 onboarding: приём кооперативом ЦПП «Стол заказов»).

Что сделано:
- SDK wrappers Queries.Marketplace.MarketplaceCppStatus + Mutations.Marketplace.MarketplaceAcceptCpp над уже существующими marketplaceCppStatus и marketplaceAcceptCpp GraphQL (Zeus схема в SDK уже содержит обе операции).
- Vue page components/desktop/src/pages/Marketplace/OnboardingCoopAcceptCpp/{api,ui,index.ts} (UX-DR конвенции: mp-role-admin, marketplace-tokens.scss).
- Роут /market/onboarding/coop-cpp в extensions/market/install.ts (roles: chairman/member, через agreementsBase).

Аудит остальных 8 страниц + приоритизация — в PLAN.md §9.7.

Ограничение: desktop в worktree через symlink тянет shared /home/admin/mono-ai-4/components/sdk, поэтому без пересборки shared sdk страница не подхватится в dev на :3029. Это будет решено отдельным PR (либо merge ветки, либо синхронизация SDK к shared).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:33:07 +00:00
coopops 25b6d6c42b chore: убрать закоммиченный symlink node_modules 2026-05-21 16:25:22 +00:00
coopops 05a8ac0a51 fix(domain): E8 — copy-кнопки, отступы и compact-layout
- AccountBadge: copy-кнопка не сжимается (flex-shrink: 0), размер 20×20 + icon 14px — раньше 18×18 + 12px тонула рядом с текстом badge.
- DataRow: column-gap var(--p-3, 12px) → var(--p-5, 20px) + min-label-width 140 → 160px + padding-right на label. Между label и value был визуально слипшийся стык.
- ContactSheet: comfortable margin-top между label и value 2px → 8px (compact 4px). Раньше label «налипал» на значение.
- IdentityPanel compact: переделан в flat flex-row (avatar + ФИО + AccountBadge + status badge) — раньше grid с .body загонял имя и AccountBadge в две строки и avatar «уезжал» от имени. Spec 8.1 требует «только avatar + ФИО + EntityIdBadge в одну строку».
2026-05-21 16:24:45 +00:00
ant 459d181152 [598][@ant] docs(marketplace): admonition «Эпик X — в разработке» для 9 нереализованных страниц + ревизия PLAN.md §9.4/§9.6
Зачем: пользователь, открывая ленту страниц marketplace в навигации, не должен решать, что заявленный функционал уже работает. Каждая из 9 страниц-заглушек (onboarding L1/L2, board/agenda-writeoff, board/payouts-readonly, branch-chairman/apl-reception-close, branch-chairman/branch-orders, chairman/category-whitelist, offerer/offers, offerer/incoming-orders, orderer/consolidated) получила `!!!warning "Эпик X — в разработке"` admonition с указанием конкретного Эпика и текущего обходного пути на MVP-стенде.

PLAN.md §9.4 переписан: Магистраль I empty-state фактически исчерпана UI-кодом, приоритет смещается на Магистраль II (сидер marketplace-flow в installExtraData). Добавлена секция §9.6 «Дыры UI» с таблицей 9 нереализованных страниц и ожидаемых Эпиков.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:12:59 +00:00
coopops 8344808676 feat(ui): волна 2 / E8 — идентификация и контакты
5 props-only canon-компонентов в src/shared/ui/domain/, регистрация
в boot/ui.ts, demo-секции 21-25 в _dev/ui:

- AccountBadge (story 8.5) — on-chain account name в mono-шрифте,
  copyable + опциональный explorer link. Назван AccountBadge вместо
  EntityIdBadge: имя EntityIdBadge занято под numeric ID пайщика
  в capital. Decision зафиксирован в epics.md и planning-artifacts.
- DataRow (story 8.3) — `label: value` пара для реестровых карточек,
  mono-режим, copyable, slot value-override, hint.
- ContactSheet (story 8.2) — email/phone/address/tg/web с автоиконками,
  кликабельными mailto:/tel:/t.me ссылками, copy и verified-чек.
- IdentityPanel (story 8.1) — Avatar + ФИО + AccountBadge + статус
  (active/blocked/pending) + role + actions-slot; compact/full.
- PersonCard (story 8.4) — Avatar + ФИО + role + AccountBadge +
  ContactSheet + slot meta; compact/comfortable.

Все props-only: useStore/useRouter/useApi внутри запрещены. Темо-зависимые
значения только через --p-* токены, никаких body--dark селекторов.
2026-05-21 15:57:21 +00:00
ant ae55a3a723 [598][@ant] feat(harness,docs): preserveNotifications + каталог с offer'ом + stub order-create
Контекст: после одобрения председателем «Берёзовый сок» в прошлой
сессии moderation page стенд имеет один APPROVED offer. Перенимаем
этот факт для документации:

1. harness.mjs / cleanViteOverlays — добавлен опциональный
   preserveNotifications=true. По умолчанию shot() продолжает чистить
   .q-notification (как раньше), но сценарии для UI-заглушек могут
   попросить оставить тост в кадре.

2. orderer/catalog.mjs — пересминаем как 01-catalog-with-offer.
   Снимок показывает CatalogOfferCard (UX-DR10) со статус-чипом
   «Опубликовано», фильтр-чипы категорий «Все 1 / Овощи и фрукты 1».
   Старый 01-catalog-empty.png удалён из docs/assets, ссылка в
   catalog.md обновлена.

3. orderer/order-create.mjs — новый сценарий: клик «Заказать» в
   каталоге показывает Quasar Notify с фактическим текстом
   «Создание заказа на Offer X — будет доступно после Эпика 4»
   (Story 4.1 ещё не реализована). Снимок взят с
   preserveNotifications=true. В order-create.md добавлен !!!warning
   «Story 4.1 в разработке» с этим снимком в начале страницы.

Результат: документация честно отражает текущее состояние MVP —
там, где UI ещё заглушка, читатель видит заглушку и причину.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 15:55:17 +00:00
ant def29aed0c [598][@ant] feat(market,docs): новая страница «История выплат» поставщика — Эпик 5 Story 5.9
Vue-страница OffererPaymentHistoryPage существовала в src/pages/Marketplace
после Story 5.9, но не была подключена ни в роутинг, ни в документацию.
Закрыло пробел Эпика 5 — поставщик не мог посмотреть, какие платёжные
запросы кооператив сформировал по его закрытым актам приёмки.

Добавлено:
- Роут /market/payments (роль `[]`, доступ всем пайщикам).
- Сценарий offerer/payments.mjs со снимком empty state.
- MD-страница new/marketplace/offerer/payments.md с описанием статусов
  INITIATED/CONFIRMED/FAILED, банковского референса и поведения «Обновить».
- Пункт «История выплат» в mkdocs.yml под Стол поставщика.
- §9.2 PLAN.md обновлён: operator 5/5, offerer 3/5+1.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 15:41:13 +00:00
coopops a192e2c398 fix(BaseCheckbox): приглушить заливку галочки через --p-primary-press
q-checkbox по умолчанию красит .q-checkbox__bg в --q-primary, который в
dark теме равен #2dd4bf — на маленьком квадрате 18px этот яркий бирюзовый
смотрится ядовито. Переопределяем заливку filled-состояния на
--p-primary-press (#134e4a light / #14b8a6 dark) — глубже и спокойнее,
чек-символ остаётся белым.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 15:37:24 +00:00
ant 18d1df7978 [598][@ant] feat(market,docs): зарегистрированы 4 marketplace-роута + e2e снимки
Vue-страницы OperatorReception/OperatorInventoryLabeling/OffererPendingAplReceptions/
OffererSupplyPreparation существовали в src/pages/Marketplace, но не были
подключены в extensions/market/install.ts (мёртвый код после Эпиков 5-6).
Регистрируем 4 роута:
- /market/supply-prep                 — поставщик подтверждает готовность отгрузки
- /market/apl-receptions              — поставщик подписывает signapl1
- /market-pvz/reception               — председатель КУ создаёт АПП на партию
- /market-pvz/labeling                — оператор клеит EAN-13/CODE128

Каждый закрывает страницу плана PLAN.md (operator/apl-reception-create,
operator/inventory-label, offerer/apl-reception-sign, offerer/shipment-prep)
empty-state снимками для документации.

Также: §9 в scenarios/marketplace/PLAN.md обновлён — живой статус Фазы 1
(19→23/30 сценариев), приоритеты для /loop-цикла, известные блокеры.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 15:36:48 +00:00
coopops d99413f896 fix(tokens): приглушить --p-accent + унифицировать .agreement-link
Старое значение --p-accent (#D84315 light / #FF7043 dark) — слишком
насыщенное, выглядит ядовито рядом с тёплой палитрой ink/canvas. Меняем
на тёплую терракоту: #B85C38 (light) / #E89472 (dark) — тот же warm-tone,
но без перевозбуждения красного канала.

.agreement-link (псевдо-ссылки на просмотр документа в согласиях) была
hardcoded в #1c64f2 (синий) light / #ff9f43 (жёлто-оранжевый) dark и не
совпадала со ссылкой «Устав кооператива». Унифицируем: все ссылки внутри
согласий теперь var(--p-accent), визиt-цвет тот же — без фиолета.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 15:34:28 +00:00
coopops 77ae6bb47d fix(ReadStatement): чекбоксы согласий — выравнивание + gap + accent ссылка
BaseCheckbox block-вариант: убран собственный display:flex + align-items:
flex-start, который перебивал внутренний flex Quasar и сдвигал label вверх
относительно inner. Теперь Quasar сам центрирует inner на первой строке.
Добавлен padding-left: 8px на .q-checkbox__label — больше воздуха между
галочкой и текстом согласия.

ReadStatement: ссылка «Устав кооператива» — --p-primary → --p-accent
(тёплый оранжевый #D84315 / #FF7043 dark) как акцентный цвет палитры.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 15:26:19 +00:00
ant e67d7ca77b [598][@ant] feat: страница модерации offer'ов председателем — закрыть пробел Эпика 3
ChairmanModerationPage.vue (mp-role-admin) с CatalogOfferCard,
route marketplace-moderation, типизированные SDK-обёртки
(ListPendingOffers / ApproveOffer / ListModerationLog) — backend
уже работал, не было только UI. harness scenario + автоматическое
прохождение onboarding-каскада соглашений в loginAsChairman.
2026-05-21 15:02:10 +00:00
ant 4d7ac317fc [598][@ant] refactor(boot): market extension рядом с capital в postgres-init — как и должно быть
Перенёс INSERT extension market из ad-hoc функции в installExtraData (где
он был хаком сразу после фикса) в initExtensionsInPostgres() — там же где
capital и reports. Это правильное архитектурное место: postgres-init.ts
владеет seed'ом extensions при первичной инициализации PG, и market теперь
получает ту же обработку, что capital.

schema_version=0 в INSERT означает: при следующем старте coopback
ExtensionSchemaMigrationService прогонит marketplace-bootstrap миграции
v1→v6 по порядку. v4 (upsertBaseline) сидирует 9 baseline-категорий.

Заодно убрал дубликат ensureMarketplaceExtensionEnabled() из infra.ts.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 10:30:56 +00:00
ant 2a73af90d3 [598][@ant] fix(boot): дефолтный bank_transfer для каждой КУ — фикс getBranches NPE
ensureBranchPaymentMethod() сохраняет dummy bank_transfer paymentMethod
для каждой КУ в Mongo. Без него branch.interactor.getBranch() падает
TypeError в строке 55 (new BankPaymentMethodDTO(null) destructure-ит
username из null), и весь getBranches возвращает 500 → desktop UI
«Кооперативные участки» показывает «Нет данных».

После фикса страница показывает 3 КУ с председателями и реквизитами
банка (ПАО Сбербанк / Тинькофф / ВТБ). MARKETPLACE_BRANCHES расширен
полем bank_name; payment method сохраняется через generator.save
('paymentMethod', ...) после ensureMarketplaceBranch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 10:12:22 +00:00
ant 433ae6dc6a [598][@ant] feat(boot): активация extension market в installExtraData — категории и marketplace mutations
ensureMarketplaceExtensionEnabled() делает INSERT в `extensions` с
schema_version=0, чтобы при следующем старте coopback bootstrap-миграции
v1→v6 marketplace-extension прогнались по порядку. v4 (marketplace-
bootstrap-v4) делает upsertBaseline() для marketplace_category — заливает
9 baseline-категорий (8 продовольственных + «Прочее»).

Без записи MarketplacePlugin.initialize() бросает «Конфиг не найден» и
весь marketplace-стэк выключен: marketplaceListCategories возвращает [],
форма «Категория *» залипает на валидации, marketplaceCreateOffer
недоступна. В проде это делает chairman через installExtension; для
dev-стенда сидируем напрямую через PG (идемпотентно — ON CONFLICT DO NOTHING).

Подтверждено: после активации marketplaceCreateOffer отрабатывает за
29мс, offer попадает в marketplace_offer со статусом PENDING_MODERATION.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 10:07:04 +00:00
ant 730ccefdea [598][@ant] fix(factory,harness): PDF EROFS + signin reload — разблокировка signAgreement и marketplace submit
- factory/PDFService.generatePDFBuffer: писать tmp в os.tmpdir(), не в __dirname/tmp.
  dist/ внутри docker bind-mount RO → mkdirSync падал EROFS на каждом generateDocument.
  Блокировало онбординг (4 соглашения) + marketplace submit + любой signAgreement.
  Видно в логах: generateDocument теперь успешно за 3.8с для registry_id 1-4.

- harness/loginAs: вынес общую часть в ensureSigninReady() c 3 попытками reload
  и cleanViteOverlays. После рестарта desktop dev контейнера первый goto оставлял
  пустой spinner (vite-plugin-checker overlay перехватывает клики), reload фиксит.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 09:55:36 +00:00
ant 068e425c4a [598][@ant] feat(boot): расширение installExtraData фикстурами marketplace MVP — 3 КУ Подмосковья + 7 ролевых пайщиков + on-chain selectBranch на 12 пайщиков, чтобы reboot:extra сразу давал рабочий стенд для harness без блокирующего SelectBranchOverlay.
Также:
- harness.mjs dismissOnboardingDialogs ловит «Формируем документ…» состояние SignAgreementDialog по фразе в q-bar/body (ModalBase рендерит title в span, не в .q-toolbar__title).
- chairman/branches.mjs адаптирован под просмотр готовых КУ из installExtraData (без UI-сабмита, КУ уже создаются on-chain в boot:extra).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 09:25:24 +00:00
ant a0daf255e2 Merge pull request 'fix(marketplace): ownership :own-KU учитывает trustees председателя КУ' (#16) from chore/trustees-ownership into marketplace2
Reviewed-on: #16
2026-05-21 08:16:53 +00:00
ant 8cd011e9a7 Merge remote-tracking branch 'origin/marketplace2' into chore/trustees-ownership 2026-05-21 08:15:07 +00:00
ant 339f530833 [598][@ant] docs(marketplace): пересняты e2e-скриншоты после полного reboot:extra — актуальные пустые состояния столов
Why:
- backend mono-ai-4 :3028 был перезагружен с нуля (boot:extra с правильным
  MONGO_URI?directConnection=true и POSTGRES для per-instance портов),
  cooperative voskhod + 5 членов совета загружены в БД через cooparser initial-load
- старые скриншоты в PR были сняты до wipe — UI мог показывать stale state

Также:
- components/desktop/quasar.config.cjs: viteConf.server.fs.strict=false для assets
  из bind-mount /home/admin/mono-ai-N (worktree compose override)
- components/docs-harness/bin/shoot.mjs:
  - ensureOneFixture передаёт CHAIN_URL и MONGO_URI?directConnection=true в env
    add-plain-participant (без CHAIN_URL скрипт падает на ECONNREFUSED)
  - runScenario передаёт BASE_URL в env под лежащий снизу harness.mjs
    (раньше default :2999 — это mono-ai-1 desktop, не свой)
2026-05-21 07:29:23 +00:00
ant b0055eb8ce [598][@ant] chore(docs): добавлены fronts-маттер теги в 5 marketplace MD — единая фильтрация в mkdocs
Файлы первой партии docs-harness имели title/generated_by/scenario, но без tags.
Без tags страница не попадает в фильтры «Пайщик / Председатель / Член совета»
на главной mkdocs. Привёл к единой конвенции:

- chairman/dashboard.md — tags: Председатель
- chairman/design-system.md — tags: Председатель, Член совета (UI доступен chairman/member)
- chairman/market-tour.md — tags: Председатель
- offerer/offer-create.md — tags: Пайщик
- orderer/marketplace-tour.md — tags: Пайщик
2026-05-21 01:45:13 +00:00
ant 6d3044ec97 [598][@ant] fix(docs-harness): сценарий offer-create — корректные селекторы q-select категории и submit-кнопки
Submit-кнопка называется «Опубликовать на модерацию», не «Создать».
Quasar рендерит label как div.q-field__label, не <label>, поэтому
locator('.q-field').filter({ has: locator('.q-field__label', ...) }) —
корректный путь к контейнеру q-select. Кадр 03 теперь снимается.

Каталог в кадре 04 остаётся пустым: backend :2998 в текущей среде не
имеет marketplace-резолверов (marketplaceListCategories отсутствует
в GraphQL-схеме), fetchCategories возвращает []. Сценарий готов: когда
controller с marketplace-extension будет поднят, выбор категории и
публикация offer пройдут автоматически.
2026-05-21 01:11:01 +00:00
ant f1ed8e35cb [598][@ant] docs(marketplace): «Каталог витрины» — добавлен e2e скриншот пустого стенда
Существующая проза описывает функционал каталога; добавил один кадр
с реальным состоянием на свежем стенде (фильтр «Все 0», плейсхолдер
«Ничего не найдено», свежая пайщица Екатерина без баланса).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 00:24:49 +00:00
ant 006e659c95 [598][@ant] docs(marketplace): «Склад моего КУ» — 1 e2e скриншот operator-стола
Story 9.1: /market-pvz/warehouse, поштучный реестр единиц имущества
на складе одного КУ. Фильтр Код КУ/Состояние/Заказчик + кнопка
«Обновить». Таблица «Штрих-код / Товар / Заказчик / Ед. / Состояние /
Возраст / Промаркировано» с 4-стадийным жизненным циклом единицы
(LABELED/ISSUED/RETURNED/WRITTEN_OFF). Backend ACL: read:own-KU.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 00:17:50 +00:00
ant edeb5f9b7c [598][@ant] docs(marketplace): «Обработка возвратов на ПВЗ» — 1 e2e скриншот
Story 7.2-7.4: /market-pvz/returns. Председатель КУ загружает ленту
заявлений своего ПВЗ. Три секции жизненного цикла: «Ждут удалённого
рассмотрения» (teal), «Ожидают очного визита» (orange), «Архив»
(grey). Compensating forward через транзит 91: пара o.mkt.return +
o.mkt.return2 атомарно возвращает средства и имущество.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 00:16:07 +00:00
ant 5fd715de42 [598][@ant] docs(marketplace): «Открытие выдачи» — 1 e2e скриншот operator-стола
Story 6.6: /market-pvz/issuance. Председатель КУ загружает ленту
выдач своего ПВЗ (поле «ID КУ» + кнопка «Загрузить ленту выдач»).
Таблица заказа/заказчика/количества/суммы/статуса с плейсхолдером
«Нет заказов, ожидающих выдачи». Двухшаговая выдача: signiss1
открывает READY_TO_RECEIVE, signiss2 заказчика закрывает в RECEIVED.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 00:14:25 +00:00
ant 754202ec55 [598][@ant] docs(marketplace): «Сеть ПВЗ» — 1 e2e скриншот списка ПВЗ кооператива
/market-pvz/list (workspace market-pvz). Снимок: пустой список с
жёлтым баннером «Карта недоступна: не задан YANDEX_MAPS_API_KEY» —
ключ Yandex.Maps не входит в стандартный локальный .env. ПВЗ
регистрируется ончейне через o.mkt.regbranch после создания КУ.
Канон-виджет KUMapWithList (UX-DR16). Доступ — chairman.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 00:12:32 +00:00
ant 610d543f68 [598][@ant] docs(marketplace): «Экосистема кооперативов» — 1 e2e скриншот admin-стола
Story 9.4: /market/ecosystem, read-only заглушка до ecosystem_registry
(NFR-Sc2 / AR18). Снимок: карточка «Текущий кооператив voskhod» +
карточка «Другие кооперативы» с информкой «Межкооперативная торговля
включится позже». Подключение братских кооперативов — вторая фаза.
Подключил страницу в mkdocs.yml.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 00:10:32 +00:00
ant c822052b6a [598][@ant] docs(marketplace): «Сводный склад» — 1 e2e скриншот admin-стола
Story 9.2: /market/warehouse-summary, read-only обзор для chairman/member.
Снимок: две вкладки (Сводный склад / Поток заказов и поставок), 4 KPI
(Активных позиций / КУ с движением / Товарных позиций в обороте /
Списано накопительно), таблица SKU×ПВЗ с поиском по SKU, плейсхолдер
«Нет данных». Списания/выдачи здесь не редактируются — только триггерами
контракта marketplace.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 00:08:13 +00:00
ant e5b90062c4 [598][@ant] docs(marketplace): «Списания скоропорта» — 1 e2e скриншот стола председателя
Story 8.7: admin-стол /market/writeoffs. Снимок раскрывает регламент
1106→1105 (Заявление председателя + Протокол совета), кнопку
«+ Новый черновик», карточку «В работе совета», пустые ленты проектов
и архива у свежего стенда. Списание per-item через атомарную пару
o.mkt.wroff + o.mkt.wroff2; ролевой доступ chairman/member.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 00:06:05 +00:00
ant 5ddb76be54 [598][@ant] docs(marketplace): «Гарантийные возвраты» — 1 e2e скриншот orderer-стола
Story 7.1: orderer-стол /market/returns. Снимок раскрывает структуру:
форма «Идентификатор полученного заказа» + кнопка «Подать заявление»,
блок «Активные заявления» (пусто) и «Архив заявлений» (пусто) у свежей
пайщицы Екатерины. Подпись заявления 1104 ЭП пайщика — обязательна;
условие подачи — статус RECEIVED + неистёкший гарантийный срок.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 00:03:38 +00:00
ant 85588c2049 [598][@ant] docs(marketplace): «Готово к получению» — 1 e2e скриншот orderer-ленты
Story 6.7: отдельная страница для статуса READY_TO_RECEIVE, открывающаяся
из push-уведомления marketplace-order-ready. Снимок: пустая таблица
«Заказ / Пункт выдачи / Количество / Сумма заказа / Открыто к выдаче»
с плейсхолдером «Нет заказов, готовых к получению» для свежей пайщицы.
Подключил страницу в mkdocs.yml в навигации Стола заказчика.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 00:01:14 +00:00
ant f003822a5a [598][@ant] docs(marketplace): «Мои заказы» — 1 e2e скриншот orderer-стола
Лента my-orders пайщицы Екатерины: 8 табов-фильтров по статусу
жизненного цикла заказа (Все/Активные/Ждут поставщика/Приняты/
Готовы к выдаче/Получены/Отменены заказчиком/Отменены поставщиком)
и пустое состояние «нет заказов» для свежей пайщицы. Селекта статусов
нет — фильтрация табовая, всё видно на одном кадре.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 23:58:26 +00:00
ant 14da4a4de5 [598][@ant] docs(marketplace): онбординг L3 — 3 e2e скриншота гейта Стола заказов
Документирует фактическое поведение свежей пайщицы Екатерины при первом
входе на Стол заказов: модальный стек 4 общих соглашений (ЦПП Кошелёк +
3 базовых) держит любую навигацию в /market/*; отдельной гейт-оферты
расширения сегодня нет; после подписания каталог открывается пустой
в контексте «Стола пайщика». Проза переписана с гипотетического
описания на наблюдаемые факты со скриншотов.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 23:53:19 +00:00
ant 4bf9d3cd7b [598][@ant] docs(marketplace): дизайн-система Эпик 10 — 3 e2e скриншота витрины
- сценарий marketplace/chairman/design-system (chairman, 3 кадра прокрутки страницы)
- проза chairman/design-system.md: список 13 custom-компонентов, UX-DR19/20/21/22/23/29/30/31
- покрыт последний marketplace-маршрут /market/design-system (виден только chairman/member)
- mkdocs.yml: добавлен пункт «Дизайн-система Стола заказов» под Стол председателя (Marketplace)
2026-05-20 23:33:21 +00:00
ant 4d150f6140 [598][@ant] docs(marketplace): создание предложения — 3 e2e скриншота формы и каталога
- сценарий marketplace/offerer/offer-create (ivanpetrov, harness signAllAgreements)
- проза offerer/offer-create.md: 7 полей формы + 4 типа отсечки заказов + длительность цикла
- ключевая находка: без выбранной категории предложение не создаётся (категория обязательна)
- скриншоты: пустая форма, заполненная (без категории), каталог остался пустым
2026-05-20 23:28:53 +00:00
ant 388ad4e376 [598][@ant] docs(marketplace): добавлен обзор Стола заказчика глазами пайщика — 4 e2e скриншота
- сценарий marketplace/orderer/marketplace-tour (4 кадра, ivanpetrov + harness signAllAgreements)
- проза orderer/marketplace-tour.md: Каталог, Мои заказы (7 фильтров), Готово к получению, Гарантийный возврат
- ключевая находка: marketplace-маршруты доступны заказчику по прямому URL, но в меню Стола Пайщика их нет
- mkdocs.yml: добавлен пункт «Обзор разделов заказчика» под «Стол заказчика»
2026-05-20 23:24:44 +00:00
ant 5c333f8fa1 [598][@ant] fix+docs: разблокировка marketplace UI + e2e обзор Стола председателя
Зафиксированы 2 блокера фронта, мешавшие снять marketplace-документацию,
плюс 23 скриншота и 2 инструкции (`dashboard.md`, `market-tour.md`).

## Фиксы desktop

1. `ProcessesPage.vue` — убраны TS non-null assertion `!.` из pug-template
   v-for. Они проникали в скомпилированный render-функцию и приводили к
   runtime `SyntaxError: missing ) after argument list` на import-time всех
   extensions, что блокировало init-app целиком.

2. `Environment.ts` — раскомментирован fallback на `/config.default.js`,
   когда SSR middleware не запущен и `/config.js` отдаёт SPA-fallback HTML.
   Без этого фронт получал пустой BACKEND_URL и зависал на loadSystemInfo.

3. `config.default.js` — обновлён под реальный локальный стек
   (BACKEND_URL=127.0.0.1:2998, CHAIN_ID соответствует blockchain-data).

4. `watch-branch-overlay/index.ts` — добавлен harness escape hatch
   `localStorage.harness:noBranchOverlay` для снятия скриншотов разделов
   без процедуры выбора КУ. В prod localStorage пуст, поведение не меняется.

## Документация

- `new/marketplace/chairman/dashboard.md` (11 шотов) — обзорная инструкция
  по разделам Стола председателя: онбординг, запросы одобрений, магазин
  приложений, члены совета, ПВЗ, регистрационные взносы, ключ кооператива,
  провайдер платежей, контакты.
- `new/marketplace/chairman/market-tour.md` (12 шотов) — экскурсия по всем
  маршрутам Стола заказов (`/market/*` и `/market-pvz/*`): каталог, создание
  предложения, мои заказы, готовые к выдаче, возвраты, списания скоропорта,
  склад, реестр экосистемы, ПВЗ-разделы.
- `mkdocs.yml` — добавлены 2 пункта в nav.

## Harness

- `dashboard-overview.mjs` — переписан под кликовый обход меню (без goto, чтоб
  не триггерить branch-overlay в navigation guard).
- `market-tour.mjs` (новый) — 12-шаговая экскурсия по marketplace-маршрутам
  с harness-bypass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 23:01:16 +00:00
ant 6a523db8b1 fix(init-wallet): URL-fallback на coopname + harness scenario chairman dashboard
- init-wallet: info.coopname может быть undefined из-за race между login и
  loadSystemInfo. Когда login завершается быстрее, init-wallet вызывает
  loadUserWallet({coopname: undefined}) → GraphQL 400 → catch → reload →
  фронт никогда не редиректит после login. URL-fallback извлекает coopname
  из window.location.hash/pathname как safety net.
- harness scenarios/marketplace/chairman/dashboard-overview.mjs: обзорный
  сценарий по 5 marketplace URL-ам (/marketplace, /marketplace/catalog,
  /marketplace/orders, /chairman, /chairman/approval-requests). Без
  waitForURL — фронт сейчас не делает redirect после login.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 21:26:42 +00:00
ant db48339051 fix(infra): protect Environment.ts eval + add seed-cooperative-base script
- Environment.ts: check content-type before eval(/config.js) — в dev-режиме Vite catch-all отдаёт index.html на отсутствующий /config.js, и eval(htmlString) бросает SyntaxError из синхронного XHR, который всплывает как «Quasar boot error» в обход try/catch и ломает последующую init-app. Проверка content-type включает eval только если ответ действительно JavaScript.
- boot/src/scripts/seed-cooperative-base.ts: новый idempotent скрипт для восстановления базовых сущностей кооператива в Mongo (vars, monos, vaults). Полезен когда chain (blockchain-data) сохранился, а Mongo volume сбросился — boot:extra падает на adduser duplicate, а этот скрипт сидит ровно то что не на chain.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 21:17:08 +00:00
ant acb0cf2021 docs(marketplace): 30 страниц прозы по столам + mkdocs.yml nav
Полная документация Marketplace MVP «Стол заказов» (Эпики 1-10):

- new/marketplace/index.md
- new/marketplace/onboarding/ (3 страницы, L1/L2/L3)
- new/marketplace/orderer/ (6 страниц, стол заказчика)
- new/marketplace/offerer/ (5 страниц, стол поставщика)
- new/marketplace/operator/ (5 страниц, стол оператора ПВЗ)
- new/marketplace/branch-chairman/ (4 страницы, стол председателя ПВЗ)
- new/marketplace/chairman/ (4 страницы, стол председателя коопа)
- new/marketplace/board/ (3 страницы, стол совета)

Каждая страница 250-500 слов бизнес-языком (без «mutation» /
«resolver» / «controller»), с cross-link'ами внутри одного стола
и между столами.

mkdocs.yml: добавлен кустарник «Стол заказов» в nav между
«Бухгалтерия» и «Благорост», структура — по 6 ролевым столам.

Все 30 ссылок в nav верифицированы (файлы существуют).
2026-05-20 20:20:19 +00:00
ant 54d1ae268f docs(marketplace): scaffold harness + fix pug-template inline arrays
Phase 0 ground work for «Стол заказов» MVP docs:

- harness:
  - extend KNOWN_FIXTURES with 7 marketplace participants
    (chairkrg/chairodn/chairmyt/trustedkrg/opkrg/sidorov/petrova)
  - add cleanViteOverlays() helper to strip vite/HMR overlays before
    every shot/shotElement screenshot
  - loginAsChairman/loginAs: call cleanViteOverlays before clicks +
    waitForFunction (hash-router friendly) instead of waitForURL

- scenarios:
  - scenarios/marketplace/PLAN.md (E2E план + структура столов)
  - scenarios/marketplace/chairman/branches.mjs — Сеть ПВЗ

- boot seed:
  - boot/src/scripts/seed-marketplace-branches.ts —
    идемпотентно создаёт 3 КУ Подмосковья (krg/odn/myt)
    on-chain + Mongo organizations + addtrusted trustedkrg

- desktop pug fixes (8 файлов в pages/Marketplace):
  vite vue-pug parser падал с «Unterminated string constant» на
  многострочных inline-массивах :columns="[...]" / :options="[...]"
  внутри pug templates. Выношу массивы в <script setup> как const —
  парсер проходит, типизация q-table через QTableProps['columns'].

- quasar.config.cjs: vitePlugins=[] временно отключает
  vite-plugin-checker (overlay перехватывал клики Playwright +
  vue-tsc worker отъедал >4 ГБ heap).

Refs: components/docs-harness/scenarios/marketplace/PLAN.md
2026-05-20 20:07:08 +00:00
coopops 6016c0597b feat(UserDataForm): canon-стиль полей + тип аккаунта на BaseRadioCard
UserDataForm/UserDataForm.vue:
- Выбор типа аккаунта: три q-btn(glossy, height:75px) → три BaseRadioCard
  с title/description; больше нет teal-«кнопок-плиток»
- Возврат к выбору типа: flat q-btn → BaseButton(ghost) с arrow_back

Sub-forms (Individual / Entrepreneur / Organization + Create*):
- q-input/q-select: standout="bg-teal text-white" → outlined color='primary'
  (canon-tone, единый primary-акцент вместо устаревшего teal)
- .q-gutter-sm.q-mt-md → .user-data-stack (flex column, gap токены)
- OrganizationDataForm: кнопка «совпадает» — color='teal' → flat primary

Валидация q-form.validate() и :rules сохранены — это всё ещё q-input
с rules-массивом, только tone и контейнер переехали на canon-токены.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 19:16:31 +00:00
coopops eeecab96da feat(ui): BaseCheckbox + BaseRadioCard и миграция SignUp-шагов
- BaseCheckbox: обёртка q-checkbox с canon-стилями, block-вариант для длинных
  согласий (чекбокс прижат к верху, label многострочный)
- BaseRadioCard: карточка-опция с радио-индикатором справа, props title /
  description / meta + slot fallback'и; используется для выбора программы

Миграции SignUp:
- ReadStatement: q-checkbox → BaseCheckbox(block); согласия в .agreements-стек
- SelectProgram: q-list+q-radio → список BaseRadioCard
- GenerateAccount: q-input → BaseInput(readonly, mono) с copy в #append;
  q-checkbox → BaseCheckbox; автоселект через querySelector('input').select()
  вместо ref.select() (BaseInput не пробрасывает внутренний API q-input)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 19:11:56 +00:00
coopops 127cf36ca9 fix(desktop): .ws-switcher__title padding-top 2px
Воздух над заголовком стола внутри WorkspaceSwitcher — заголовок не упирается
в верхнюю границу caption «ПК «ВОСХОД»».

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 19:03:05 +00:00
coopops f158afe485 fix(desktop): .rail__top padding 16px 8px + убрать disabled-стиль у WorkspaceSwitcher
- .rail__top: padding 18px 20px → 16px 8px (унификация боковых отступов)
- WorkspaceSwitcher: убраны :disabled-атрибут и hover:not(:disabled) — у пайщика, которому доступен только один стол, кнопка выглядит как обычно (без not-allowed-cursor, без приглушённого цвета). Меню q-menu просто не открывается через v-if='workspaces.length > 1'. Тихое игнорирование клика вместо явного запрета.
2026-05-20 18:55:36 +00:00
coopops 521575ab6a fix(desktop): WorkspaceSwitcher — фильтр столов по ролям + 3-строчный title
WorkspaceSwitcher показывал ВСЕ workspaceMenus, включая столы выше роли пользователя (например, «Стол председателя» видел рядовой пайщик), что приводило к ошибке access-denied при попытке переключения.

- Добавлена фильтрация по иерархии: chairman ⊇ member ⊇ user. Председатель видит столы любых ролей; член совета — user+member; пайщик — только user. Workspace без meta.roles (или с пустым массивом) — публичный для всех (та же логика, что в legacy WorkspaceMenu, плюс корректное наследование прав).
- Title допускает перенос на 3 строки (line-clamp 3) — «Стол вычислительных ресурсов» теперь умещается полностью
- Расширена кнопка switcher'а (margin сторон 8→4 в LeftDrawerMenu), даёт больше места под текст
2026-05-20 18:41:42 +00:00
coopops c2eefbf244 fix(desktop,signup): WorkspaceSwitcher polish + Bank toggle QR↔реквизиты
WorkspaceSwitcher:
- Brand-строка теперь собирается из vars.short_abbr + «vars.name» (получается «ПК «ВОСХОД»»). Раньше показывал только short_abbr — название кооператива было «потеряно»
- Title допускает перенос на 2 строки (line-clamp 2) — длинные названия типа «Стол вычислительных ресурсов» больше не обрезаются ellipsis'ом с одного слова
- Chevron приклеен к верху строки (align-self start) — корректно при двухстрочном title
- Меню workspace: canon padding (8/12) вокруг q-item, отступ между avatar-section и текстом — иконка и название больше не упираются в края
- Затемнение фона при открытом меню (rgba(9,9,11,0.32) overlay через Teleport в body) — меню больше не сливается с контентом за drawer'ом

Bank.vue (карточка платежа):
- Убрали q-expansion-item (раскрытие вниз внутри карточки выглядело перегружено)
- Кнопка-toggle: «Показать реквизиты» рядом с «Скачать QR». Клик переключает зону под сводкой между QR и списком 9 BaseInput'ов с copy-кнопками
- В режиме реквизитов: кнопки меняются на «Скопировать всё» (primary) + «Показать QR» (secondary)
- QR-canvas использует v-show чтобы сохранять состояние при toggle назад; реквизиты — v-if чтобы не держать BaseInput'ы в DOM
2026-05-20 18:22:58 +00:00
coopops 3f43a77167 feat(desktop): WorkspaceSwitcher + фикс ReadStatement стилей + плоский Bank.vue
ReadStatement: backend-HTML генерирует с inline-стилями и Quasar text-h* классами, которые перебивали :deep правила. Усилили все ключевые селекторы !important + добавили охват .text-h1/h2/h3/h4/h5/h6 и .text-right (под мета-блок «УТВЕРЖДЕНО…»).

Bank.vue (карточка платежа):
- убрали отдельные surface-карточки вокруг сводки и QR (получалось «карточка-в-карточке-в-карточке»)
- сводка → плоский <dl> со строкой-разделителем снизу (border-bottom var(--p-line))
- QR → просто центрированный canvas без обёртки/фона/рамки; белый цвет внутри canvas — функциональное требование контрастного сканирования, не дизайн-выбор
- detail-expansion → без своего фона, только border-top/bottom

WorkspaceSwitcher (новый widget src/widgets/Desktop/WorkspaceSwitcher/):
- Карточка-кнопка в шапке drawer: иконка кооп + caption «ПК «ВОСХОД»» + bold «Стол совета» (текущий) + chevron
- Клик → q-menu со списком всех workspaceMenus (Стол пайщика, Стол совета, Стол председателя и т.д.); выбор → selectWorkspace + goToDefaultPage
- Подключён через #brand slot AppDrawer'а в LeftDrawerMenu, заменяет дефолтный brand-row
2026-05-20 17:48:25 +00:00
ant 46f5269b89 [review][@ant] fix(marketplace): ownership :own-KU учитывает trustees председателя КУ
Резолверы marketplace с capability :own-KU теперь принимают и доверенных
лиц председателя кооперативного участка (branch.trusted[]), а не только
самого председателя (branch.trustee). По комментариям ревью к PR #10 / #15.

Источник правды — таблица branches контракта branch (поля trustee + trusted[]).
Helper MarketplaceBranchOwnershipService инжектится через @Global()
BlockchainModule.BRANCH_BLOCKCHAIN_PORT.

Затронутые места:
* marketplace-issuance.resolver:marketplaceListIssuancesByBraname — добавлен
  member + ownership-check (раньше отсутствовал любой ownership).
* marketplace-inventory.resolver:marketplaceListInventory — role-fork
  admin/board (весь склад) vs operator (свой КУ с trustee-учётом).
* marketplace-return-claim.resolver — 4 mutations (approveReturnVisit,
  rejectReturnRemote, acceptReturnAtVisit, rejectReturnAtVisit) +
  listReturnClaimsByBraname + marketplaceReturnClaim (single): ownership
  по data.braname / claim.delivery_braname с учётом trustees.
* marketplace-apl-reception.resolver:marketplaceListAplReceptionsByBraname
  — добавлен ownership-check (раньше отсутствовал).

Phase 2 follow-ups (не в scope этого PR):
* Mutation marketplaceLabelInventory / marketplaceLabelShipmentInventory —
  проверка operator имеет право label'ить inventory по order.delivery_braname
  должна сделаться в service-уровне.
* Кеш branch lookup'ов с TTL (сейчас каждый запрос — chain RPC).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 17:20:54 +00:00
coopops 9ea9467c2c feat(signup): canon-типографика заявления + рефакторинг карточки платежа
ReadStatement (этап 6.1):
- Backend генерирует HTML документа — локально нормализуем через :deep
- h1 уменьшен с монструозного дефолта до canon h3 (20px), центрирован
- h2/h3, p, strong, ul/ol, table, hr — типографика по var(--p-*)
- мета-блок .approved/.meta-right (УТВЕРЖДЕНО…) — ink-2, мельче, плотнее
- Чекбоксы соглашений снизу не трогали (как просил пользователь)

Bank (этап 6.2 — карточка платежа PayWithProvider):
- QR-код вынесен в фокус: сначала сводка (получатель/сумма/назначение) → QR → действия → реквизиты для ручного перевода скрыты под q-expansion-item
- Полные 9 полей (ИНН/БИК/КПП/корр-счёт/...) переведены на BaseInput(readonly, mono) с copy-кнопкой в #append; раскрываются только при необходимости
- Кнопки «Скачать QR» (primary) и «Скопировать реквизиты» (secondary) — BaseButton
- Удалены: q-btn(push), case-mixed «скопировать реквизиты», стиль #qr через global селектор
2026-05-20 17:20:15 +00:00
ant 505a33f1db Merge pull request 'Эпик 5 re-ревью: fail-fast tx_hash в APL signsupp/signchair loop' (#14) from chore/re-review-E5-apl-fail-fast into marketplace2
Reviewed-on: #14
2026-05-20 17:06:44 +00:00
ant 864b8cbb9d Merge pull request 'Эпик 10 ревью: a11y aria-labelledby TakeoverDialog + BarcodeScanner timer cleanup' (#11) from chore/review-E10-design-fixes into marketplace2
Reviewed-on: #11
2026-05-20 17:06:18 +00:00
ant afbb89a21f Merge pull request 'Эпик 4 re-ревью: fail-fast tx_hash' (#13) from chore/re-review-E4-fail-fast-tx-hash into marketplace2
Reviewed-on: #13
2026-05-20 17:03:28 +00:00
ant 0501ecb3e2 Merge pull request 'Эпик 8 ревью: tx_hash трекинг + fail-fast + cron-фиксы' (#9) from chore/review-E8-spisanie-fixes into marketplace2
Reviewed-on: #9
2026-05-20 17:03:03 +00:00
ant b6f0f8fcd1 Merge pull request 'Эпик 3 re-ревью: withdraw Offer'а блокируется при активных Order'ах' (#12) from chore/re-review-E3-fix-withdraw-stub into marketplace2
Reviewed-on: #12
2026-05-20 17:02:26 +00:00
ant 4b74378afd Merge pull request 'Эпик 7 ревью: ownership + fail-fast tx_hash + limits' (#8) from chore/review-E7-vozvrat-fixes into marketplace2
Reviewed-on: #8
2026-05-20 17:01:34 +00:00
ant e412d7ee4e Merge pull request 'Эпик 6 ревью: fail-fast при пустом tx_hash от блокчейна' (#7) from chore/review-E6-vydacha-fixes into marketplace2
Reviewed-on: #7
2026-05-20 17:00:46 +00:00
ant 1033f1af76 Merge pull request 'Эпик 4 ревью: cancel guard + supplier_account фильтр консолидированных заявок' (#5) from chore/review-E4-order-fixes into marketplace2
Reviewed-on: #5
2026-05-20 17:00:17 +00:00
ant 333212593f Merge pull request 'Эпик 3 ревью: каталог через SDK Zeus + модерация FIFO + edit clear' (#4) from chore/review-E3-vitrina-fixes into marketplace2
Reviewed-on: #4
2026-05-20 16:59:39 +00:00
ant 187cb67ab4 Merge pull request 'Эпик 5 ревью: sign:as-supplier + EAN13 crypto-random' (#6) from chore/review-E5-postavka-fixes into marketplace2
Reviewed-on: #6
2026-05-20 16:58:23 +00:00
coopops 9a3ee9afa4 feat(invite): этап 5 — миграция Invite на canon AuthCard + BaseInput/BaseButton
- InvitePage: row/col-обёртка → .invite-page flex-center + canon padding (как у SignInPage)
- Invite widget: AuthCard со штатными title/subtitle (вместо CAPS-заголовка с background-clip градиент-текста)
- BaseInput(mono, readonly) для приватного ключа — canon-оформление key/hash полей
- BaseButton(ghost) для копирования + BaseButton(primary) для «Установить ключ»
- Удалены: градиент-текст, semi-transparent blue/green tint карточки вокруг инструкции и чекбокса, hover translateY + box-shadow glow, custom border-radius/box-shadow для q-input и кнопок, мобильные @media-блоки (canon-токены и AuthCard сами адаптивны)
2026-05-20 16:54:02 +00:00
coopops fd4bb48870 fix(signup): canon-рамка для контейнера подписи вместо неонового glow
- .signature-container: 3px primary border + 3-слойный box-shadow glow → 1px dashed по var(--p-line-2), surface-2 фон, без свечения; на hover лишь окрашивается рамка
- .signature-hint: переехала в центр контейнера, canon ink-2 цвет, убран frosted-chip с blur/shadow — теперь спокойная метка «Оставьте собственноручную подпись в рамке», pointer-events: none пропускает клики на canvas
- min-height снижен 300→220, padding на canon-токены
2026-05-20 16:52:23 +00:00
coopops d41ce5712d fix(signup): отступ сверху, canon фон q-stepper, ширина email-поля
- SignUp.vue: добавлена обёртка .signup-page (padding + center + min-height) — как у SignInPage; раньше страница была прижата к шапке
- AuthCard maxWidth: 1000 → 720 — историческое 1000 для трёх-колоночной UserDataForm; для остальных шагов выглядит абсурдно широко
- q-stepper: убраны собственный фон/тень/padding (не «карточка в карточке»), линии-разделители переведены на var(--p-line)
- EmailInput: max-width поля 360px — email не должен растягиваться во всю ширину карточки
2026-05-20 16:39:11 +00:00
ant 437bf62a20 Эпик 5 re-ревью: fail-fast tx_hash в APL signsupp/signchair loop
HIGH:
- submitOnChainSignSupp / submitOnChainSignChair использовали
  «last-non-empty» с fallback `signsupp-<reception_id>` /
  `signchair-<reception_id>` если ВСЕ tx_hash в группе оказались
  пусты. Внутри for-of цикла tx_hash чек шёл с `if (txHash)
  lastTxHash = txHash` — пропускал пустой ответ молча.
- Теперь: на любой пустой ответ от цепи внутри loop — fail-fast
  `ConflictException` с указанием конкретного Order'а. Часть группы
  могла быть уже подписана выше — продолжать и финализировать APL
  с фантомным tx_hash в БД категорически нельзя; ретрай безопасен
  (контракт идемпотентен по order_hash).

Тесты: jest apl-reception — 6/6 passed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 16:31:38 +00:00
ant 70265826ff Эпик 4 re-ревью: fail-fast tx_hash в createorder / cancelorder / supplier action
HIGH:
- 3 сервиса Эпика 4 имели одинаковый anti-pattern:
  `t?.transaction?.id ?? t?.processed?.id ?? 'unknown'`. При пустом
  ответе цепи в БД записывалась строка 'unknown' как tx_hash —
  фантомный audit-trail, невозможно cross-reference в blockchain.
- normalizeTxHash / normalizeTxResult → fail-fast `BadRequestException`
  при отсутствии tx_hash. Соответствует фиксу аналогичной дыры в
  Эпиках 6 (PR #7), 7 (PR #8) и 8 (PR #9).

Затронуты:
- MarketplaceOrderCreateService.normalizeTxResult (createorder).
- MarketplaceOrderCancelService.normalizeTxHash (cancelorder).
- MarketplaceOrderSupplierActionService.normalizeTxHash
  (acceptorder + declineorder).

Тесты: jest marketplace-order-(cancel|create|supplier) — 37/37 passed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 16:28:03 +00:00
ant ddd34b60d4 Эпик 3 re-ревью: withdraw Offer'а должен блокироваться при активных Order'ах
CRITICAL:
- `MarketplaceOfferService.hasActiveOrders` был TODO-stub (`return false`)
  с комментарием «Order'ов нет — withdraw разрешён всегда. Story 4.x
  перепишет на OrderRepository.countActiveByOffer». Эпик 4 уже merged
  в marketplace2 (PR #5) — Order'ы существуют. Stub НЕ был обновлён →
  supplier мог withdraw'нуть Offer с активными заказами, у пайщиков
  средства оставались в blocked, заказы зависали.
- Подключён `MARKETPLACE_ORDER_REPOSITORY`. Метод `hasActiveOrders`
  через `orderRepo.list` с фильтром по 6 не-терминальным статусам
  (ACTIVE / ACCEPTED_PENDING_SUPPLIER[_INDIVIDUAL] / ACCEPTED /
  ACCEPTED_TO_COOP / READY_TO_RECEIVE). Пагинация 1×1 — нужен только
  factOfExistence через totalCount > 0.

Тесты:
- makeOrderRepo() helper в unit-spec, batch-обновление 40 instances
  `new MarketplaceOfferService(repo, cats, makeOrderRepo())`.
- jest marketplace-offer-service — 40/40 passed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 16:22:52 +00:00
coopops ed9f483e7a fix(authcard): убрать оборванный barrel-экспорт после удаления старого AuthCard
vue-tsc ругался: shared/ui/index.ts:21 — Cannot find module './AuthCard'.
- shared/ui/index.ts: убран `export * from './AuthCard'`
- LoginRedirectPage.vue: импорт переключён на canon `shared/ui/domain/AuthCard`, CAPS-заголовок заменён на canon `title` prop
- Invite.vue: импорт переключён на canon (полная миграция Invite — этап 5)
2026-05-20 16:16:11 +00:00
coopops d974d7698b feat(signup): этап 4 — миграция SignUp на canon AuthCard + BaseButton/BaseInput
- canon `shared/ui/domain/AuthCard` переписан на pug
- SignUp.vue переключён со старого «глянцевого» AuthCard на canon (title prop вместо CAPS-заголовка)
- старый `shared/ui/AuthCard` удалён (использовался только в SignUp)
- 7 подэкранов SignUp (EmailInput, SetUserData, SelectProgram, GenerateAccount, SelectBranch, ReadStatement, SignStatement): q-btn → BaseButton (ghost для «назад», primary для «Продолжить»)
- EmailInput: q-input → BaseInput, валидация rules переведена на computed :error
- q-input в GenerateAccount оставлен из-за зависимости от ref.select()
2026-05-20 16:10:50 +00:00
ant 475c91b485 PR #4 ревью-фиксы: селекторы + IOutput + paginated канон
По ревью-комментариям к PR #4 (Эпик 3 vitrina):

1. categoryOfferCounts.ts query: было inline-объявление полей в
   `Selector('Query')(...)`. Создан селектор
   `marketplaceCategoryOfferCountSelector` с _validate-проверкой
   на `MakeAllFieldsRequired<ValueTypes['MarketplaceCategoryOfferCount']>`,
   query использует его.

2. listCatalog.ts query: было inline `{ items, totalCount, totalPages,
   currentPage }`. Создан селектор `marketplaceOfferPaginationSelector`
   (по канону `paginatedAgreementsSelector`, `paginatedMarketplace
   WriteoffProposalsSelector`) на тип `MarketplaceOfferPaginationResult`
   с _validate. Query берёт его целиком.

3. MarketplaceCatalog/types.ts: убраны ручные типы `MarketplaceOfferView`,
   `MarketplaceOfferPage`, `MarketplaceCategoryView`,
   `MarketplaceCategoryOfferCount` — теперь все четыре выведены из
   `Queries.Marketplace.{ListCatalog,ListCategories,CategoryOfferCounts}.
   IOutput`. Канон как в других страницах desktop'а.

4. api/index.ts: убраны `as MarketplaceOfferPage`/`as
   MarketplaceCategoryView[]`/`as MarketplaceCategoryOfferCount[]` cast'ы
   и `?? []` fallback'и — destructure от Queries даёт уже типизированный
   результат через IOutput.

5. selectors/marketplace/index.ts: добавлены оба новых selector файла.

SDK перебилжен (unbuild succeeded, 438 kB). controller/tsc + vue-tsc по
изменённым файлам без ошибок.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 16:08:06 +00:00
ant b52b68c752 Эпик 10 ревью: a11y aria-labelledby + timer cleanup
HIGH:
- TakeoverDialog: добавлены role/aria-modal/aria-labelledby/aria-describedby
  на q-card (Story 10.2 NFR-A1). Раньше screen reader'ы не озвучивали
  заголовок takeover-диалога — несоответствие AC «aria-label/
  aria-describedby на custom-компонентах где Quasar базовых
  недостаточно».

MEDIUM:
- BarcodeScanner: timer cleanup при unmount через onBeforeUnmount.
  Иначе при закрытии TakeoverDialog раньше 1.5 s scanning-таймера
  scanned/error эмитится на размонтированный компонент → memory leak
  + lost-event race.

Story coverage:
- Story 10.1 «дизайн-токены»: marketplace-tokens.scss проверен —
  spacing/touch-targets (44/48 POS-режим)/per-role layout/dark mode
  реализованы корректно. Без правок.
- Story 10.2 «13 custom-компонентов»: все 13 на месте
  (12 в widgets/Marketplace/ + KUMapWithList в widgets/KUMapWithList/).
  A11y базовая обеспечена через QDialog/QInput штатные aria;
  TakeoverDialog получил недостающие atribute'ы.
- Story 10.3 «performance baseline»: NFR-P1/P2/P3 — manual measurements
  в MVP (без CI gate), нет автоматизации; код-аудит не выявил blockers.

Known limitations (Phase 2):
- Storybook stories — AC Story 10.2 требует, но папка stories/ пустая.
- Unit-тесты widgets/Marketplace/* отсутствуют (AC Story 10.2 требует
  Jest + Vue Test Utils).
- BarcodeScanner — mock-режим (camera-API / USB не реализованы; AC
  UX-DR26 Phase 2). Также используется в Эпике 7 OnSiteDecisionDialog.
- Полный WCAG 2.1 AA audit + axe-core CI — Phase 2 (NFR-A1 MVP
  покрыт базово).
- Lighthouse / bundle-size CI gate — Phase 2.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 15:59:52 +00:00
coopops bc5d7deda5 content(brand): og-image из poster-logo-horizontal + новый длинный дескриптор
- og-image.png — взят brand-постер ~/blago/production/shared/poster-logo-horizontal.png,
  отресайзен до 1000px по ширине и центрирован на полотне 1200×630
  с белыми краями по вертикали. Шрифт/композиция родные бренда.
- description / og:description / twitter:description: «Цифровой Кооператив —
  система управления хозяйством. Регистрация пайщиков, заказы на поставку
  и приобретение имущества, собрания совета, общие собрания пайщиков,
  взаимные расчёты на блокчейне, автоматический документооборот и бухбаланс
  на основе простой электронной подписи.»
2026-05-20 15:56:13 +00:00
ant 987b28811f Эпик 8 ревью: tx_hash трекинг, fail-fast, cron-фиксы
CRITICAL:
- service: tx_hash propwroff/createagenda/execwroff раньше нигде не
  сохранялся — audit-trail отсутствовал. Теперь extractTxHash и пишется
  в decision_log при submitToCouncil (propwroff_tx_hash, createagenda_tx_hash)
  и при executeAuthorizedProposal (execwroff_tx_hash per-item).
- service: пустой tx_hash от цепи → fail-fast ConflictException
  (submitToCouncil + executeAuthorizedProposal). Раньше silent fall-through.
- service: pre-gate в submitToCouncil — если proposal с тем же hash уже
  отправлен в совет, повторный submit отбивается (защита от двойного
  клика → дублирующего propwroff + createagenda).
- service: inventory.applyStatusTransition WRITTEN_OFF теперь обязателен —
  раньше catch → warn без re-throw; позиция могла остаться
  RETURNED_TO_WAREHOUSE при успешном on-chain wroff'е и попасть в
  следующий cron-цикл → повторное списание. Теперь re-throw, proposal
  остаётся в EXECUTING для retry.
- service: signer fallback на coopname удалён (coopname — аккаунт
  кооператива, не пайщик; контракт execwroff требует
  branch.is_user_authorized(signer) — coopname этого не пройдёт).
- cron: amount=0 placeholder отбивался validateAndNormalizeItems
  (`amount <= 0`) — крон при auto_proposal_enabled=true ВСЕГДА падал.
  Теперь крон фильтрует кандидатов без unit_cost; если все без цены —
  отправляет напоминание председателю вместо broken DRAFT.

MEDIUM:
- resolver: throw new Error → BadRequestException для «DRAFT-only»
  валидации (унификация с остальными resolver'ами).

Known limitations (Phase 2):
- Story 8.1 AC требует ЧЕТЫРЕ типа кандидатов
  (RETURNED_TO_WAREHOUSE / EXCESS_RETURNED_TO_WAREHOUSE / без юр.
  оформления / expiry_date). Сейчас крон покрывает только expiry_date.
  Расширение требует добавить поля age_days/returned_at в inventory.
- Story 8.3 AC «единая Antelope-транзакция NFR-R2» — by design
  контракта per-item execwroff (комментарий cpp прямо говорит
  «снимает ограничение на максимальный размер протокола»). AC ↔
  контракт конфликт — требует переоформления AC.
- Number()/toFixed() float-арифметика на суммах — на больших суммах
  возможна потеря точности; нужен переход на BigNumber.
- cycle_started_at.toISOString() hash-mismatch при round-trip через PG
  с разной precision timestamptz.
- onCouncilAuthorized synchronously вызывает execute — long-running
  в delta-listener'е, риск duplicate execute при retry. Нужен
  decoupling через background worker.

Test plan:
- [x] jest marketplace-writeoff — 31 passed.
- [ ] e2e: двойной клик «Подписать и отправить» — второй submit
  отбивается ConflictException.
- [ ] e2e: цепь возвращает {} вместо tx — ConflictException, статус
  draft остаётся DRAFT.
- [ ] e2e: inventory.applyStatusTransition бросает — proposal в
  EXECUTING, retry возможен.
- [ ] e2e: cron при auto=true и пустом unit_cost — reminder вместо
  DRAFT'а с amount=0.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 15:49:17 +00:00
coopops 44628dfde4 content(brand): обновить превью и мета — система управления хозяйством на платформе «Благо»
- og-image.png перерисован: «Цифровой Кооператив» / «Система управления
  хозяйством» / «на платформе БЛАГО — кооперативной экономики для жизни» /
  «Регистрация пайщиков · заказы на поставку и приобретение имущества ·
  расчёты на блокчейне».
- description / og:description / twitter:description выровнены под этот
  же копирайт. Слово «взаимоотношения с пайщиками» убрано как слишком
  узкое — управление хозяйством включает в себя ещё заказы, реестры
  и документы.
2026-05-20 15:44:57 +00:00
ant 0f20dc0eb7 Эпик 7 ревью: ownership, fail-fast tx_hash, limits, cleanup
CRITICAL:
- resolver: добавлена ownership-проверка `:own`/`:own-KU` для
  marketplaceReturnClaim и marketplaceListReturnClaimsByBraname —
  matrix capability guard'ом не закрывает data-уровень (matrix явно
  делегирует ownership resolver'у).
- service: extractTxHash → fail-fast ConflictException вместо записи
  фейкового tx_hash `<action>-<claim.id>` в БД при пустом ответе цепи
  (5 мест: submretrn/aprretrem/rejretrem/accretrn/rejretrn). Аналог
  фикса PR #7 для signiss1/signiss2.
- service: reason_text лимит 500 → 2000 (соответствие AC Story 7.1).
- service: warranty_until === null → fail-closed (ConflictException
  «гарантия не предусмотрена»). Раньше null читался как «без проверки»,
  возврат принимался даже на заказы без гарантии.
- frontend SubmitReturnClaimDialog: payload-документ генерируется один
  раз вместе с preview (с reason_text + defect_category), подписывается
  именно показанный snapshot — устраняет hash-mismatch preview ≠ signed.

HIGH:
- service: assertBranameMatchesClaim — backend проверяет совпадение
  input.braname с claim.delivery_braname перед on-chain action.
  Раньше председатель КУ-X мог одобрить заявление с delivery=КУ-Y
  (контракт проверял только is_user_authorized signer для braname,
  не cross-link с return_request).
- service: удалён костыль barcode-валидации
  `.includes(order_id.slice(0,8))` — UUID Order'а никогда не пересекается
  с EAN-13 inventory; проверка ложно отбивала валидные сканы.
  Полноценная сверка с marketplace_inventory — Эпик 5/9.
- service: validatePhotoPayloads — размер фото (10 МБ) валидируется
  в service до bucket.put, не полагаясь только на @UseBucket maxBytes.
- service: cleanupBucketPhotos — orphan-фото удаляются из bucket'а
  при провале on-chain submit'а (submretrn/accretrn/rejretrn).
- adapter listByDeliveryBraname: default без status возвращает ВСЕ
  заявления (включая архив). Раньше default был ACTIVE_STATUSES — это
  делало архивную секцию в operator-столе всегда пустой.

Frontend:
- maxlength 500 → 2000 для reason_text в q-input.

Known limitations (Phase 2):
- chairman_account lookup через `coop_ku.chairman_account` (сейчас
  fallback `chairmanAccount = delivery_braname` в notifications +
  resolver — ownership `:own-KU` приближение).
- Загрузка фото base64-payload одной mutation (AC требует per-file
  upload в bucket → image_registry_id). Реальный лимит body-parser
  упрётся в ~10MB.
- C++ submretrn не сохраняет delivery_braname в return_request →
  cross-branch decision разрешён by design контракта (backend закрывает
  ту же дыру service-level проверкой).
- BarcodeScanner в OnSiteDecisionDialog — mock-режим (UX-DR26 Phase 2).
- BarcodeScanner backend-сверка с marketplace_inventory — Эпик 5/9.
- Уведомление председателю шлётся на delivery_braname account
  (TODO в notification.service.ts) — Phase 2.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 15:40:43 +00:00
coopops 4fd79ffda9 feat(brand): системный логотип ЦК + чистка ассетов + OG-превью
- src/assets/logo.svg, public/favicon.svg, public/logo.svg — единая SVG
  логотипа кооператива (path с «ц» внутри). Скачано с
  https://цифровой-кооператив.рф из шапки сайта.
- src/assets/logo.svg: fill -> currentColor чтобы лого внутри
  canon .rail__brand наследовал --p-primary (для тёмной темы тоже).
- LeftDrawerMenu подключает лого через slot #brand-icon AppDrawer
  (импорт ?raw + v-html). Внутри зелёного квадрата шапки рейла теперь
  наш системный знак, а не дефолтный q-icon "dashboard".
- index.html: favicon → /favicon.svg, apple-touch-icon тоже на SVG.
  Удалены неработающие ссылки на /favicon.ico и /apple-touch-icon-*.png.
- index.html: добавлены description + расширенные og:* и twitter:*
  с конкретным описанием и og-image (1200×630), чтобы ссылки в чатах
  выглядели прилично.
- public/og-image.png — превью 1200×630, ImageMagick-сгенерированный
  композит из логотипа + текста.
- Удалены неиспользуемые ассеты:
  - public/icons/, public/pwa/ — старые favicon/PWA-PNG'и всех размеров;
  - src/assets/* — 40 легаси SVG/PNG (anime, blockchain, dacom*, flow*,
    header-logo, quasar-logo-vertical, system*, club*, welcome.jpeg и т.д.).
  Оставлены только src/assets/pin.svg (используется в Map.vue) и
  src/assets/logo.svg (новый).
2026-05-20 15:35:23 +00:00
coopops fca07fc3c7 fix(layout): убрать TS-каст из pug-шаблона AppDrawer
Pug-template передаёт выражения в Vue compiler как строки; конструкции
вида (entry as RailItem).route не превращаются в JS и падают
SyntaxError в браузере: «Unexpected identifier 'as'».

Касты унесены в <script>: добавлены computed flatItems и
normalizedGroups, шаблон работает с уже типизированными массивами без
inline-TS.
2026-05-20 15:20:25 +00:00
ant ea32c80abb [598-9][@ant] fix(marketplace): ревью Эпика 6 — fail-fast при пустом tx_hash от блокчейна 2026-05-20 15:14:55 +00:00
coopops dfd1ebd2a9 fix(layout): этап 3.3 — все canon-компоненты на pug + фикс импорта cmdk
- LeftDrawerMenu: импорт useCmdkStore → useCmdkMenuStore (правильное имя).
  Без этого ломался ESM build всего drawer'а: «no exported member
  useCmdkStore» — RailUserCard не монтировался, поэтому и chevron свёртки
  «не работал», и Найти/Пополнить не реагировали.
- AppDrawer.vue, AppHeader.vue, RailUserCard.vue переведены на pug
  (lang="pug"). В проекте везде pug — никакого исторического HTML в
  canon-компонентах быть не должно.
2026-05-20 15:12:38 +00:00
ant 6e8754587e [598-8][@ant] fix(marketplace): ревью Эпика 5 — sign:as-supplier в матрице + EAN13 crypto-random 2026-05-20 15:03:20 +00:00
coopops 7d6592d1b6 fix(layout): этап 3.2 — pug + v-model:collapsed + рабочие триггеры
- q-drawer width 240 → 248px (совпадает с canon --p-rail-w, кошелёк больше
  не уплывает справа из-за обрезки на 8px)
- LeftDrawerMenu: убран весь :deep override на .rail__usercard/.rail__signout,
  возвращаем canon margin/padding как есть
- LeftDrawerMenu: добавлен v-model:collapsed на RailUserCard с сохранением
  в localStorage — chevron «свернуть/развернуть» снова работает
- LeftDrawerMenu: кнопка Найти теперь дёргает cmdkStore.openDialog() напрямую
  (фейковый KeyboardEvent не доходил до глобального обработчика)
- LeftDrawerMenu: «Пополнить» → useDepositDialog().open(); DepositButton +
  WithdrawButton рендерятся скрыто как держатели q-dialog (q-portal в body)
- Header.vue и LeftDrawerMenu.vue переписаны на pug (как остальной проект)
2026-05-20 14:40:31 +00:00
ant 233a22ee75 [598-7][@ant] fix(marketplace): ревью Эпика 4 — cancel guard по can_be_cancelled + supplier_account фильтр 2026-05-20 14:32:41 +00:00
coopops e5799ba702 fix(layout): этап 3.1 — RailUserCard в footer + canvas-фон + отступы рейла
- AppDrawer: плоский items оборачивается в .rail__nav, появляется gap 4-8px до cmdk
- cmdk margin 16→8px, чтоб выровнять с rail__nav (8px от стенки)
- LeftDrawerMenu footer: WalletCardMini+LogoutButton → один canon RailUserCard
  (avatar+balance в primary-soft + Пополнить + встроенный signout)
- :deep override на .rail__usercard margin 16→12/8 и .rail__signout padding 12/20→12/12
- quasar-canon: body--light/dark + q-layout/q-page-container/q-page красятся
  через --p-canvas, чтобы фон страницы соответствовал шапке и рейлу
2026-05-20 14:19:18 +00:00
ant dd862f20ed [598-6][@ant] fix(marketplace): ревью Эпика 3 — каталог через SDK Zeus + модерация FIFO + чистка approve/reject при edit 2026-05-20 14:07:13 +00:00
coopops 1e1a70ded2 feat(layout): этап 3 — WalletCardMini + canon LogoutButton в footer AppDrawer
LeftDrawerMenu:
- legacy `MicroWallet` (ColorCard teal с большой карточкой пайщика + ИП-badge
  + Deposit/Withdraw micro-кнопки) → canon `WalletCardMini` из
  widgets/wallet-card-mini (тонкая карточка из shared/ui/domain/WalletCard
  compact-variant, читает walletStore сама).
- Убран toggle «свернуть/развернуть нижнюю секцию» — нижняя секция теперь
  всегда видна (короткий компактный блок: WalletCardMini + Выйти).
- Удалены неиспользуемые `onMounted`/`ref` импорты, slide-анимация и CSS
  toggle-кнопки.

LogoutButton:
- pug + q-item с красной полупрозрачной плашкой → плоский ghost-button в
  стиле rail-пункта AppDrawer: прозрачный фон в покое, на hover —
  `--p-neg-soft` фон + `--p-neg` текст/иконка.
- Из uppercase «ВЫЙТИ» в title-case «Выйти» (canon — никаких uppercase).

Визуально левый drawer теперь полностью в canon-семье: единый rail с
пунктами + поиском + компактным wallet-балансом + ghost-кнопкой выхода.

Закрывает этап 3 из 5 в волне «приземление DS на реальный layout».
Дальше — SignUp (этап 4) и Invite (этап 5).
2026-05-20 14:01:08 +00:00
ant 9435942b3d Merge pull request 'Эпик 1 ревью: guards membership + MEDIUM/LOW' (#1) from chore/review-E1-onboarding-fixes into marketplace2
Reviewed-on: #1
2026-05-20 12:01:36 +00:00
ant cdbce2b967 Merge pull request 'Эпик 2 ревью: race-guard в геокодинге ПВЗ + фикс flaky теста' (#2) from chore/review-E2-pvz-fixes into marketplace2
Reviewed-on: #2
2026-05-20 11:59:54 +00:00
coopops 61bd6f2000 feat(layout): этап 2 — AppHeader в реальном layout (заменяет MainHeader)
widgets/Header/CommonHeader/Header.vue теперь использует canon AppHeader
из shared/ui/layout. Высота шапки 56px (--p-topbar-h), border-bottom
hairline, без shadow и фоновых градиентов.

Слоты:
- #crumb: BackButton (для авторизованных), либо title с названием
  кооператива из system.info.vars для гостей/install.
- #actions: headerActions injection через useHeaderActionsReader (всё что
  страницы инжектят — Deposit/Withdraw, SettingsDropdown и пр. — рендерим
  как было).
- #notifications: NotificationCenter (только loggedIn + isClient).
- #theme: ToogleDarkLight — пока legacy, сохраняет storage + PWA-цвет;
  наш canon ThemeToggle их не делает (привяжу позже отдельным эпиком).
- #profile: BaseButton primary для гостей — Регистрация/Вход (в зависимости
  от текущего route).

Удалены:
- MainHeader.vue (вся логика scroll arrows + carousel actions group ушла —
  canon `.topbar__actions { gap: 8px }` достаточно; overflow-scroll
  будет вернут отдельно если потребуется на узких экранах).
- HeaderStyles.scss (стили scroll-arrow и q-toolbar overrides больше
  не нужны — canon-разметка через `<header class="topbar">`).

q-header Quasar остаётся wrapper'ом (sticky-poзиционирование в q-layout),
внутри — AppHeader даёт canon-разметку.
2026-05-20 11:54:27 +00:00
coopops f651f05a06 feat(layout): этап 1 — AppDrawer в реальном layout (заменяет LeftDrawerMenu)
Переписан widgets/Desktop/LeftDrawerMenu/LeftDrawerMenu.vue: внутри теперь
canon `AppDrawer` из shared/ui/layout с rail-видом MONO v2 (логотип ПК +
короткое имя кооператива в шапке рейла, ⌘K-поиск, пункты-router-link'и
с иконкой/бэйджем, sticky-footer).

Адаптер store→canon:
- items: desktop.activeSecondLevelRoutes → RailItem[], с прежней
  фильтрацией по roles + meta.conditions + meta.hidden (один-к-одному
  логика из SecondLevelMenuList).
- activeKey: вычисляется из router.currentRoute с поддержкой группового
  паттерна project-* → projects-list.
- @select: router.push({name, params:{coopname}}) или
  actionsStore.executeAction(meta.action), закрытие drawer на mobile.
- @cmdk: эмиттим keyboard event ⌘K → CmdkMenu (он смонтирован глобально
  в default.vue) подхватывает.

Footer-слот: пока сохранён legacy MicroWallet + LogoutButton + toggle
свернуть/развернуть (заменим на WalletCardMini в этапе 3).

default.vue: q-drawer width 200 → 240 (canon-ширина rail'а).

Что НЕ затронуто этим коммитом: верхняя шапка (legacy MainHeader),
правый drawer для actions, q-footer для гостей. Их меняем отдельными
этапами 2–3.
2026-05-20 11:38:56 +00:00
ant 1720f8667e [598-5][@ant] fix(marketplace): ревью Эпика 2 — race-guard в геокодинге + flaky тест 2026-05-20 11:33:16 +00:00
coopops f3d0192e6b fix(ui): убрать конфликт с Quasar — .row → .u-row
Корневая причина «оторванной» галочки в ResetKeyForm: я ранее задал в
mono-platform/components.css глобальное `.row { display: flex;
align-items: center; gap: 12px; }`. Quasar внутри `q-checkbox` (а также
q-radio, q-toggle, q-btn-toggle и др.) сам добавляет class="row" на
корень компонента и использует его как inline-flex — мой `gap: 12px`
раздвигал `.q-checkbox__inner` и `.q-checkbox__label` на 12px по всему
приложению.

Решение:
- Переименовать утилиту `.row` → `.u-row` (та же семантика, тот же набор
  модификаторов --wrap, --gap-2/4/6). У Quasar теперь свободный .row.
- Обновить все 6 использований в /_dev/ui.
- Откатить предыдущий :deep(.q-checkbox__label) padding-left:4px workaround
  в ResetKeyForm — лечил симптом, причина устранена.

Затрагивает только нашу dev-витрину и canon-utility; legacy pug-виджеты
с `.row.justify-center` продолжают работать через стандартный Quasar
flex-grid класс.
2026-05-20 11:18:39 +00:00
ant a61c5ce076 [598-4][@ant] fix(marketplace): ревью Эпика 1 — security/membership + MEDIUM/LOW 2026-05-20 11:10:22 +00:00
coopops 7e429c481b fix(ui): ResetKeyForm — сжать зазор между галочкой и текстом «Я сохранил ключ»
Quasar по умолчанию даёт ~12–16px padding на .q-checkbox__label, в
compact-форме сохранения ключа это смотрелось «оторвано». Уменьшаем до 4px
через :deep override на классе .rk-form__confirm.
2026-05-20 11:05:02 +00:00
coopops 7819d9c5bb feat(dev): секция 20 — ResetKey двухэтапный flow в /_dev/ui
Три кадра в одной секции: LostKey (ввод email), ResetKeyForm check-mail
(уведомление «письмо отправлено»), ResetKeyForm save-key (демонстрация
сохранения ключа с моковым сгенерированным аккаунтом). Submit на третьем
кадре регенерирует мок-ключ для повторной проверки.

Заменяет ошибочную секцию ChangeKey из предыдущей итерации.
2026-05-20 10:44:50 +00:00
coopops c0df025e1f fix(ui): откатить ChangeKey + мигрировать настоящий ResetKey flow на base-компоненты
ChangeKey-фича (формы current/new/confirm WIF + успешный диалог) была
ошибочной интерпретацией E7. Реальный flow «сменить ключ» в продукте —
это **двухэтапный ResetKey**:

  1. LostKey: пайщик вводит email (он потерял ключ — текущего WIF нет),
     бэк присылает письмо со ссылкой `?token=...`.
  2. ResetKey: при заходе по ссылке клиент сам генерирует новый ключ
     прямо в браузере (через `useCreateUser().generateAccount()`),
     показывает приватный ключ readonly с «Скопировать», требует
     подтверждения «Я сохранил ключ» и вызывает on-chain
     `resetKey({token, public_key})`.

Текущий ключ ввести нельзя (его нет). Новый ключ ввести нельзя (только
генерируется). Никакого подтверждения нового ключа — он один и тот же.

Что сделано:
- Удалена ошибочная `features/User/ChangeKey/`.
- Старый `widgets/Registrator/ResetKey/ui/ResetKey.vue` (pug + сырые
  q-input/q-checkbox + кастомные градиентные стили) разбит на:
  * `ResetKeyForm.vue` — presentation (props-driven, режимы
    `check-mail` / `save-key`), без useRouter/useApi — пригоден для
    /_dev/ui витрины.
  * `ResetKey.vue` — connected обёртка, читает `route.query.token`,
    генерит account через `useCreateUser`, диспатчит `resetKey` через
    `useResetKey`, на успех → router.push на signin.
- Новая UI собрана на BaseForm + BaseInput readonly mono + BaseButton +
  BaseBanner + AuthCard.

/_dev/ui секция 20 теперь показывает три кадра подряд: LostKey (email
шаг), ResetKey check-mail, ResetKey save-key с моковым сгенерированным
ключом.

FR11, UX-DR9, UX-DR15.
2026-05-20 10:44:32 +00:00
coopops 5dba22d457 fix(ui): BaseInput/BaseSelect — явный проброс слотов вместо v-for in $slots
Generic-проброс `<template v-for="(_, slotName) in $slots" #[slotName]>`
рушится в Quasar 2.19 рендере: при наличии scoped-слота #append вылетает
`Cannot read properties of null (reading 'key')` в renderSlot + Vue warns
по `_isVue`/`constructor` свойствам. Заменяем на явный список слотов
(prepend / append / before / after / hint у q-input; +option, selected-item
у q-select) — синтаксис, который Quasar держит корректно.

Кейс всплыл на ChangeKeyForm: #append с q-icon (toggle visibility) ронял
страницу /_dev/ui целиком.
2026-05-20 08:47:48 +00:00
coopops 698b1d6333 feat(dev): секция 20 — ChangeKey flow в /_dev/ui (визуальная проверка E7)
Подключение ChangeKeyForm + ChangeKeySuccessDialog к dev-витрине.
Submit запускает заглушку useChangeKey().changeKey() (≈600 мс), по успеху
открывает диалог с новым WIF. Generate генерирует валидный K1 ключ через
SDK и заполняет оба новых поля. Confirmed сбрасывает форму.
2026-05-19 20:03:38 +00:00
coopops 8b3263bf70 feat(ui): E7.2 ChangeKeySuccessDialog — подтверждение через BaseDialog без backdrop-exit
Диалог успеха смены ключа: новый WIF в моноширинном блоке с copy-кнопкой,
BaseDialog с closeOnBackdrop=false, closeOnEscape=false, hideCloseButton=true.
Закрытие только через primary-кнопку «Я сохранил ключ»; до копирования
кнопка показывает guard-баннер вместо немедленного закрытия. По confirmed
эмитим событие — реальный flow подключит router.push на дашборд.

FR11, UX-DR11, UX-DR15. Story 7.2 из epics.md.
2026-05-19 20:03:26 +00:00
coopops 9578dc1b86 feat(ui): E7.1 ChangeKeyForm — смена ключа на BaseForm + BaseInput + BaseBanner
Форма смены ключа пайщика на новых компонентах дизайн-системы.
BaseBanner severity=warn с предупреждением о потере, checklist последствий,
поля current/new/confirm WIF моноширинные с toggle visibility, ghost-кнопка
«Сгенерировать» через PrivateKey.generate из @wharfkit/session.

Бизнес-логика — заглушка с TODO на привязку on-chain change-key action
(pattern см. features/User/ResetKey/api) и TODO на OtpInput шаг подтверждения
(приходит в Эпике 10).

FR11, UX-DR9, UX-DR15. Story 7.1 из epics.md.
2026-05-19 20:03:12 +00:00
coopops 45cfa29940 fix(ui): Avatar tone — единая семантика neutral/primary/accent, убрать ink
Bug после rename --p-accent → --p-primary: класс .avatar--accent смотрел
на --p-primary-soft (canon teal), а должен был — на --p-accent-soft
(warm orange по новой семантике). И вариант ink (белый bg + dark text)
визуально выпадал из палитры — крайний xl-аватар на демо-витрине казался
«не пришей к звезде рукав».

Новая семантика AvatarTone:
- neutral (default) — серый surface-3 + ink-1 (нейтральный)
- primary — canon-teal soft + canon-teal text (основной выделение)
- accent — warm-orange soft + warm-orange text (редкое выделение,
  например подсветка пайщика по batch_id)

Удалён вариант ink — не использовался в реальном коде.
В dev-витрине xl-аватар: tone="ink" → tone="primary".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 19:46:27 +00:00
coopops d0a8dc0080 revert(ui): убрать override центрирования suffix/prefix — оставить Quasar дефолт
Override (padding-top:0 + align-self:center на dense) не отрабатывал —
Quasar в dense держит prefix/suffix по baseline вводимого значения,
и пользователю это устраивает. Убираем неработающий override, чтобы
не мешался в дальнейших правках.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 19:42:30 +00:00
coopops 8d3cabab9b fix(ui): BaseDialog — отступ через margin-top второго ребёнка
Inline `style="margin:0"` на <p>-описании перебивал мой margin-bottom
через CSS (специфичность inline > selector). Переключаюсь на
`> :first-child + *` margin-top — отступ крепится ко второму ребёнку,
не зависит от inline-стилей первого.

Работает универсально: <p> + <BaseInput>, <p> + <BaseForm>, любая
другая пара «описание + контент».

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 19:39:29 +00:00
coopops 5ba95a2e6d feat(ui): --p-accent (warm orange) + suffix-центр + точная вёрстка gap
1. --p-accent (canon token): #D84315 (light) / #FF7043 (dark).
   Совпадает с legacy Quasar $accent (warm orange, Material Deep Orange).
   Проброшен в --q-accent через CSS var — legacy color="accent",
   var(--q-accent), text-accent продолжают работать без изменений,
   но теперь следуют за canon-темой (light/dark). Резерв под редкое
   выделение: ссылки batch_id и т.п.
2. quasar-canon.css: .q-field--dense .q-field__suffix/prefix —
   padding-top:0 + align-self:center. Quasar по умолчанию ставит
   padding-top: 24px под floating-label, отчего «RUB» визуально
   опускался к низу control'а. Теперь в одну линию со значением.
3. BaseForm: убран gap 12px из .base-form__body — reserve-hint-space
   у q-input даёт ~24px снизу под error/hint, дополнительный gap
   делал расстояние между инпутами неприятно большим.
4. BaseDialog: убран универсальный gap из body, оставлен margin-bottom
   только на первом <p>/.intro — между описанием и первым инпутом
   воздух нужен, между остальными детьми reserve-hint-space хватает.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 19:33:11 +00:00
coopops 6d5d614a2c fix: убрать случайный symlink node_modules из индекса
Промахнулся `git add -A` — захватил worktree-symlink node_modules
(он pnpm-shared с основным репо). .gitignore покрывает node_modules/
но симлинк-файл — отдельный путь.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 19:24:25 +00:00
coopops 0bac215035 refactor(ui): --p-accent → --p-primary — устранить конфликт с Quasar primary
Контекст: после проброса --q-primary = var(--p-primary) (4fddfe2343)
наш токен --p-accent и Quasar primary стали обозначать ОДИН и тот же
canon-teal — два имени для одного цвета это когнитивный шум, и при
росте платформы будет путать. Имя `accent` оставляем свободным под
будущий ВТОРОЙ цвет (warm-orange, для редкого выделения вроде ссылок
по batch_id) — это отдельная задача когда понадобится.

Переименовано везде (109 ссылок в 6 файлах):
- --p-accent          → --p-primary
- --p-accent-hover    → --p-primary-hover
- --p-accent-press    → --p-primary-press
- --p-accent-soft     → --p-primary-soft
- --p-accent-line     → --p-primary-line
- --p-accent-strong   → --p-primary-strong
- --p-ink-on-accent   → --p-ink-on-primary

Файлы: tokens.css, quasar-canon.css, components.css, RailUserCard,
AuthCard, _dev/ui (включая user-facing labels палитры «Accent → Primary»
и заголовок секции «Акцент → Основной цвет»).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 19:23:58 +00:00
coopops 4fddfe2343 feat(ui): canon-цвет через --q-primary CSS var; вернуть dense + rounded
- tokens.css: проброс canon-палитры в Quasar brand-переменные
  (--q-primary/negative/positive/warning/info = соответствующие
  --p-* токены). Резолвится lazy → автоматически следует за темой.
  Теперь Quasar САМ красит focus q-field, q-btn color="primary",
  bg-primary utility-классы в canon-цвета без CSS-overrides;
- quasar-canon.css: убраны border-color overrides на ::before
  (default/focus/error) — Quasar управляет цветом через --q-primary
  и собственный механизм --focused/--highlighted. Оставлены только
  background, text-color и rounded углы (border-radius var(--p-r-sm));
- BaseInput/Select: вернули `dense` (компактный 40px) и явный
  `color="primary"` (canon teal через --q-primary).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 19:14:20 +00:00
coopops 9f38148dc4 fix(ui): BaseDialog — уменьшить отступ между body и footer-кнопками
Между нижним инпутом и footer-кнопками был ~32px (q-card-section
padding-bottom 20 + q-card-actions padding-top 12) — выглядело
растянуто. Сводим к 16px (8+8) — кнопки сидят ближе к контенту.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 19:06:27 +00:00
coopops 559493a4b2 fix(ui): q-field overrides — оставить только цвет, всё остальное дефолт Quasar
Регрессия мерцания границы и приплюснутости — следствие борьбы наших
overrides с встроенной Quasar анимацией. Возвращаем дефолт: Quasar
сам решает геометрию (height/padding/border-radius/font), анимации,
opacity ::before и focus-индикацию. Наша задача — только покрасить
в canon-цвета.

Удалено из .q-field--outlined:
- height/padding/border-radius/font-size/font-family overrides;
- transition border-color (Quasar сам делает свой transition);
- opacity:1 !important на ::before (был костыль от мерцания);
- box-shadow:none на --focused/--highlighted;
- display:none на ::after underline;
- override .q-field--dense;
- override .q-field--float .q-field__label (Quasar анимирует label сам);
- focus-shadow / outline reset на native input.

Оставлено (только цвет):
- background: var(--p-surface);
- border-color: line-1 / accent (focus) / neg (error);
- color текста, placeholder, label, prefix/suffix, hint/error.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 19:05:57 +00:00
coopops ed61bb46b6 fix(ui): мерцание границы q-input на focus + unused import в BaseTable
- Quasar при focus анимирует opacity ::before (прячет старую границу
  перед показом ::after underline). Так как ::after мы спрятали,
  получался видимый gap ~200-400 ms «граница исчезла → появилась
  с подсветкой». Фикс: opacity:1 !important на ::before,
  transition только по border-color.
- BaseTable.vue: убран неиспользуемый импорт BaseTableColumn.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 19:01:31 +00:00
coopops 113a7b8a1b fix(ui): убрать dense из BaseInput/Select; gap в Dialog/Form body; wallet 17px
- BaseInput/BaseSelect: убран `dense` — возвращаемся к дефолтному
  outlined Quasar (control ~56px). Floating-label получает достаточно
  воздуха, текст значения визуально центрируется без сжатия;
- BaseDialog: `.base-dialog__body` теперь flex-column с gap 16px —
  описание/инпуты/таблицы внутри диалога получают единый воздух
  без правки потребителя (пример: «Паевой взнос: укажите сумму…» →
  отступ до инпута появляется автоматически);
- BaseForm: `.base-form__body` gap 4px → 12px — те же требования
  внутри формы;
- WalletCard: `.wallet__metric-val` 19→17px — итоговое значение
  для аккуратного баланс-блока FULL.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 19:00:14 +00:00
coopops 390c5e9a5c fix(ui): убрать stack-label, двойную focus-подсветку и mini-wallet из шапки
1. Stack-label: BaseInput/BaseSelect → только `dense`. Quasar
   стандартно сам корректно держит floating-label в dense+outlined,
   без CSS-override метка работает по документации Quasar.
2. Двойная focus-подсветка: убран наш box-shadow var(--p-focus-ring)
   поверх Quasar внутренней focus-индикации (виден был как «белый
   ринг» на тёмной + наш teal). Теперь только border-color меняется
   через ::before (canon-минимализм). Дополнительно `box-shadow: none
   !important` на --focused/--highlighted control и `display:none` на
   ::after underline (на случай dense Quasar variants).
3. Mini-wallet в правом верхнем углу AppHeader: слот #wallet удалён
   из API (баланс уже отображается в RailUserCard в drawer'е,
   дубль в шапке избыточен). Убран из dev-витрины каркаса.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 18:52:12 +00:00
coopops 0515994833 fix(ui): WalletCard и mini-wallet — снизить визуальный вес цифр
Регрессия после Quasar wrapper-pivot: метрики кошельков выглядели
крупно по сравнению с заголовком/sub-label, а padding'и были
избыточны для compact-варианта.

- .wallet (FULL): padding 18→14px по верт, иконка 44→40px,
  metric-val 22→19px, ccy 13→12px;
- .wallet--row (compact): padding 14→10px, иконка 36→32px,
  metric-val 18→16px — выглядит как row в списке, а не как карточка;
- .rail__balance-val (mini-wallet в drawer): 22→18px — теперь не
  доминирует над identity-блоком пайщика.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 18:38:31 +00:00
coopops b7717e0e1f fix(ui): canon q-field/q-btn пропорции — убрать раздавливание floating-label
Корень регрессии: жёсткий height: 40px в .q-field--outlined раздавил
вертикальную разметку q-input (Quasar держит padding-top:24px под
floating-label слот; при сжатии текст значения сдвигается). Те же
эффекты для select.

- BaseInput/BaseSelect → dense + stack-label по умолчанию;
  Quasar сам корректно держит 40px в dense, метка статически стоит
  над инпутом без floating-механики;
- .q-field--outlined: убран принудительный height; .q-field--dense
  получает только padding-x, высоту контролирует Quasar;
- .q-btn: font-weight 500→450, min-height 40→36px, padding 16→14px —
  кнопки больше не выглядят «жирно/тяжело»;
- .q-btn--outline: hairline-граница через ::before (Quasar pattern),
  без двойной рамки;
- размеры sm/lg/dense пропорционально уменьшены;
- .q-chip: высота 24→26px, padding 10→12px, font-weight 500→450;
  + .q-chip__icon размер 14px (для точечных чипов).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 18:36:29 +00:00
coopops 1ac70c0405 refactor(desktop/base): остальные base/ и AuthCard → обёртки Quasar
BaseSelect → q-select (outlined, reserve-hint-space, map-options/emit-value
для совместимости с {value,label} options API).

BaseCard → q-card flat с q-card-section head/body. Variants flat/inset/quiet
override-ят через scoped style.

BaseChip → q-chip с color mapping (variant → primary/positive/negative/
warning/info). Square для canon-look.

BaseBadge → q-badge с тем же color mapping. Dot-вариант через scoped style
(8×8 круг).

BaseBanner → q-banner rounded dense с border-left по variant.

BaseDialog → q-dialog + q-card с size→maxWidth mapping. closeOnBackdrop/
closeOnEscape пробрасываются через :persistent / :no-*-dismiss.

BaseTable → q-table с трансформацией columns в QTableProps['columns'].
Hide-pagination, binary-state-sort. Cell-slots работают через body-cell-{key}.

AuthCard (domain) — был на native canon `.card` div. Переведён на q-card flat
для консистентности. Усиленный shadow + accent-stripe сверху сохранены.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 18:12:12 +00:00
coopops 8ddcd30bc9 refactor(desktop/base): BaseInput/Button/Form → обёртки Quasar
BaseInput теперь рендерит <q-input outlined reserve-hint-space no-error-icon>
с проброшенными slots. reserve-hint-space от Quasar резервирует место
под error/hint встроенно — раньше делали вручную через NBSP. Все Quasar
features (rules через v-bind \$attrs, mask, ref API, native slots) работают
прозрачно. Props API совместим (modelValue, label, hint, error, prefix,
suffix, mono, readonly, disabled, autocomplete, name, id, type).

BaseButton — q-btn с variant→Quasar mapping:
  primary → :unelevated color="primary" (заливка accent)
  secondary → :outline (border + surface)
  ghost → :flat (transparent)
  danger → :flat color="negative" (transparent + neg-cвет)
:no-caps :ripple=false для canon-look. Props API совместим.

BaseForm — q-form-обёртка с сводным error-баннером (q-banner) и slot footer.
banner—neg styling прилетает из quasar-canon.css через .bg-negative-soft.

Глобальная стилизация Quasar в quasar-canon.css даёт canon-look всем
этим компонентам и существующим q-input/q-btn/q-card по всему проекту.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 18:09:34 +00:00
coopops 82f44284fa feat(desktop/css): canon-стилизация Quasar поверх (quasar-canon.css)
Архитектурный pivot: вместо native HTML5-компонентов под canon-классами
стилизуем Quasar поверх. Причины:
- Сохраняется весь Quasar API из коробки (rules/mask/dense/slots/refs)
- Существующий код (сотни q-input, q-btn, q-card, q-select) автоматом
  получает canon-look без правок
- Меньше регрессий — Quasar отвечает за accessibility/keyboard/focus

Файл src/css/mono-platform/quasar-canon.css содержит overrides под canon
для: q-field--outlined (q-input/q-select), q-btn (unelevated/outline/flat),
q-card, q-chip, q-badge, q-banner, q-dialog/q-menu, q-item, q-table,
q-checkbox/q-toggle.

Подключен в quasar.config.cjs css[] после components.css. tokens.css
без изменений — все цвета/радиусы/тени тянутся как переменные.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 18:08:36 +00:00
coopops 6029342c7e fix(desktop): удалить legacy глобальный .card { transform: scale(1.03) }
Истинный источник «прыгалки» AuthCard на hover: глобальное правило
`.card:hover { transform: scale(1.03) }` в src/app/styles/style.css
(и его дубль в pages/Marketplace/MainPage/ui/MainPage.vue, тоже не scoped)
бьёт по любому элементу с классом card. AuthCard имеет корневой
<div class="card auth-card"> — каждый hover дёргает её на 3%.

Поиск показал: класс `card` без модификаторов в templates никто кроме
новых canon-компонентов (BaseCard, AuthCard) не использует. Этот глобал —
мёртвый legacy, удаляем безопасно.

NBSP-фикс layout-shift из предыдущего коммита (2a2d9a786a) остаётся
актуальным — он лечил другую (на уровне поля) проблему.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 18:06:59 +00:00
coopops 2a2d9a786a fix(desktop/ui): добить layout-shift, ужать gap полей, accent-стрип AuthCard, затемнить canvas
1) BaseInput — layout shift жив: text-interpolation `{{ error || hint || ' ' }}`
   рендерила обычный пробел, браузер схлопывает text-node с одним whitespace
   в 0-width content → div сжимается → форма прыгает. Заменили на
   `<span>&nbsp;</span>` (NBSP в DOM, не схлопывается).

2) BaseInput — canon `.field { margin-bottom: 16px }` создавал слишком
   большой разрыв между полями (16px margin + 10px form gap + 17px message =
   ~43px). Override `.field { margin-bottom: 0 }` в scoped — расстояние
   управляется gap'ом BaseForm.

3) AuthCard — статичный accent-стрип 3px сверху (`::before`). Визуальный
   якорь без анимации — даёт бренд-маркер «здесь главное».

4) tokens.css light — canvas #fafafa → #f4f4f5 (cool-gray), canvas-2
   #f4f4f5 → #ebebed. Surface остаётся #ffffff, теперь карточка чётче
   проступает на фоне.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 17:54:32 +00:00
coopops d6ab6e3cc6 fix(desktop/ui): убрать layout shift в BaseInput и оживить AuthCard на светлой
BaseInput: контейнер field__message теперь рендерится всегда с min-height
calc(--p-fs-meta * 1.4) — раньше div появлялся/исчезал на каждом keystroke
при валидации (см. LostKey emailError), форма прыгала вверх-вниз.
Когда нет ни ошибки, ни хинта — рендерится NBSP, высота сохраняется.

AuthCard: добавлен box-shadow поверх canon-минималистского .card —
canvas #fafafa и surface #ffffff на светлой почти неразличимы, без тени
карточка выглядит плоской. На тёмной отдельный shadow усилен (поверх
canon `--p-shadow-card`, который оптимизирован под data-cards, не hero).
border-color подкручен с --p-line на --p-line-1 для большей чёткости.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 17:48:01 +00:00
coopops 5d1d79d9ea feat(desktop/LostKey): E6 Story 6.3 — миграция «Перевыпуск ключа» на canon
Scope-adjustment: plan'овая Story 6.3 предполагала миграцию SignUp как
второго auth-flow, но Register — multi-step wizard (10+ файлов в
pages/Registrator/SignUp/), который требует VerticalStepper из Эпика 10
и не помещается в Волну 1. Вместо этого мигрируем LostKey — простую
одно-полевую auth-страницу, того же auth-домена.

LostKey (widget): pug → html, старый AuthCard (q-card с shimmer) →
canon AuthCard. q-input → BaseInput с inline email-валидацией;
q-btn → BaseButton; всё обёрнуто в BaseForm с loading/error.
Бизнес-логика (useLostKey.startResetKey + router redirect на resetkey)
сохранена 1:1.

LostKeyPage: footer-кнопка «Назад» через ghost BaseButton в footer-slot
AuthCard; центровка через flexbox.

Не мигрировано в Волне 1 (известный gap):
- SignUp wizard (требует VerticalStepper из E10)
- ResetKey / Invite (на старом AuthCard, мигрируются в Волне 2)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 17:29:04 +00:00
coopops 34e1ea8a37 feat(desktop/SignIn): E6 Story 6.2 — миграция Login на canon-компоненты
LoginForm (feature): q-input → BaseInput, q-btn → BaseButton, обёрнут в
BaseForm с управлением loading/error. Бизнес-логика (useLoginUser, redirect,
OpenReplay tracking, NotificationPermissionDialog) сохранена 1:1.
Ошибки входа теперь показываются через сводный error BaseForm
(плюс legacy FailAlert остаётся как fallback).

SignIn (widget): импорт AuthCard переключён на новый canon-композит
(src/shared/ui/domain/AuthCard), убран pug-template и градиентные стили.
Добавлен footer-slot — пробрасывается в AuthCard.

SignInPage: переписан с pug → html, footer-кнопки «Потеряли ключ?» /
«Нет аккаунта?» вынесены в footer-slot AuthCard через BaseButton ghost-sm.
Центровка через flexbox вместо QRow grid.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 17:28:31 +00:00
coopops ef80504a80 feat(desktop/AuthCard): E6 Story 6.1 — AuthCard композит + AuthLayout
AuthCard (src/shared/ui/domain/AuthCard) — canon .card + center-aligned
head (title/subtitle/slot head) + body-slot + опциональный footer-slot.
Старый src/shared/ui/AuthCard (q-card с shimmer-градиентом) остаётся
для не-мигрированных страниц (SignUp wizard, ResetKey, Invite).

AuthLayout (src/app/layouts/AuthLayout.vue) — wrapper-обёртка для full-page
auth-страниц: фон var(--p-canvas), центрирование, тогл темы в правом верхнем.

Витрина: dev-секция 19 с примером login-формы (BaseForm + BaseInput ×2 +
BaseButton + footer-ссылки).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 17:27:11 +00:00
coopops 51f89b3fe4 feat(desktop/widgets/wallet-card-mini): E5 Story 5.2 — connected-обёртка WalletCard
WalletCardMini подключает WalletCard к Pinia (useWalletStore.program_wallets) +
System store (info.symbols.root_govern_symbol) + useRouter (клик →
name: 'wallet'). Программный mapping: wallet→MAIN, blagorost→BLAGOROST,
generator→GENERATOR (Zeus.ProgramType). Loading определяется автоматически
по наличию данных в store (или пробрасывается через prop).

Подключено в слот wallet AppHeader на dev-витрине (секция 16 — каркас) +
выделенная секция 18 с тремя программами. На dev-странице покажет
skeleton-loading т.к. loadUserWallet не вызывался; на реальных страницах
после init процесса — настоящий баланс.

Со-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 17:00:57 +00:00
coopops 50b95de57b fix(desktop/_dev/ui): корректные подписи программ Благорост/Генератор в витрине WalletCard
Благорост — капитализация РИД (не «накопления на жильё»), Генератор —
генерация РИД (не «доходы от программы»). Кошелёк остаётся «свободный
остаток». Черновые ярлыки риском попадают в production через копипасту.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 16:48:01 +00:00
coopops 345b226f84 feat(desktop/WalletCard): E5 Story 5.1 — props-only компонент с программными акцентами
Канонический .wallet с компактным (--row) и full-вариантами, программные
акценты через --prog-blagorost / --prog-wallet / --prog-generator (UX-DR20).
Витрина на /_dev/ui секция 17: три программы × оба варианта + loading +
empty + locked-line.

Compact (.wallet--row): 36px icon, 18px sum, 14px padding — для слота шапки.
Full (.wallet): 44px icon, 22px sum, 18px padding — для дашборда.

Connected-обёртка widgets/wallet-card-mini (Story 5.2) делается отдельным
коммитом, требует интеграции с реальным wallet-store и MARKETPLACE_ASSET_CONFIG.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 16:41:54 +00:00
Alex Ant 4877c29274 chore(marketplace2): sync с origin/dev + DI-фиксы controller для запуска dev-стека (#411)
* chore(release): publish

* chore(release): publish

* ci: атомарный release.yaml, убрать workflow_run-связку (#366)

build-contracts + build-containers через workflow_run упёрлись в:
(а) default-branch caveat (новая логика не активна, пока не в main),
(б) `${{ github.event.workflow_run.head_sha }}` иногда пуст в YAML-
выражениях — описание см. в шаге Resolve tag, инцидент v2026.5.14
не дёрнул PRODUCTION_WEBHOOK_URL.

Замена — один `release.yaml` на push тэга `v*`: резолвит ветку через
`git branch --contains`, собирает контракты → пушит
`dicoop/contracts:<branch>`, собирает базу + сервисные образы →
пушит `dicoop/<svc>:<tag>`, шлёт webhook. Гонок нет by construction.

`build-contracts.yaml` остаётся только на push веток для CI-
обновления `dicoop/contracts:dev|testnet|main` без релизного тэга.
Триггер `tags: ['*']` и логика резолва ветки через --contains
оттуда удалены — это теперь забота release.yaml.

Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>

* chore(release): publish

* chore(release): publish

* ci: tag-only триггеры для build-contracts и docs (#367)

build-contracts.yaml — только workflow_dispatch (ручная пересборка
`dicoop/contracts:<branch>` для отладки на dev-ноде). Тэги обрабатывает
release.yaml атомарно (контракты + контейнеры + webhook), отдельная
сборка по push'у в ветку только давала вторую параллельную сборку.

publish-docs.yaml и build-contracts-docs.yaml — на push:tags v* с
gate-job'ом, пропускающим только продакшн-тэги (без -alpha/-beta/-rc/
-test) на main. Раньше docs пересобирались на каждый push в
main/testnet/dev/reports/marketplace2 — впустую.

Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>

* fix(capital/commit): nullable description/meta в CommitOutputDTO + не дефолтить satisfaction в 5

Если syncCommit не дождался delta из блокчейна, interactor возвращает DB-only
entity, где description/meta остаются undefined — non-nullable GraphQL field
ломал ответ мутации capitalCreateCommit. Сделал оба поля nullable.

UI CreateCommitButton.vue: satisfaction_stars=0 по умолчанию, label "не указано"
пока пользователь не выбрал; блок contribution_feedback в payload только если
stars >= 1 или review_text непустой.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix commit process

* chore(release): publish

* fix(file-storage): MinIO стартует только при заданном MINIO_ENDPOINT

Раньше MINIO_ENDPOINT имел default http://minio:9000, и на проде без
minio-контейнера контроллер падал на bootstrap в HeadBucket с
getaddrinfo ENOTFOUND minio — Nest application не поднимался вообще.

Теперь MINIO_ENDPOINT optional без default; адаптер хранит enabled-флаг
по наличию endpoint и при отсутствии — onApplicationBootstrap логирует
warning и возвращается без сетевых вызовов. Любая попытка getBucket /
fetchObjectForReadProxy кидает InterFileStorageBackendUnavailableError
с понятным сообщением.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(release): publish

* chore(release): publish

* fix(capital/time-tracking): личные доли estimate + revert при decline (#387)

* chore(release): publish

* chore(release): publish

* ci: атомарный release.yaml, убрать workflow_run-связку (#366)

build-contracts + build-containers через workflow_run упёрлись в:
(а) default-branch caveat (новая логика не активна, пока не в main),
(б) `${{ github.event.workflow_run.head_sha }}` иногда пуст в YAML-
выражениях — описание см. в шаге Resolve tag, инцидент v2026.5.14
не дёрнул PRODUCTION_WEBHOOK_URL.

Замена — один `release.yaml` на push тэга `v*`: резолвит ветку через
`git branch --contains`, собирает контракты → пушит
`dicoop/contracts:<branch>`, собирает базу + сервисные образы →
пушит `dicoop/<svc>:<tag>`, шлёт webhook. Гонок нет by construction.

`build-contracts.yaml` остаётся только на push веток для CI-
обновления `dicoop/contracts:dev|testnet|main` без релизного тэга.
Триггер `tags: ['*']` и логика резолва ветки через --contains
оттуда удалены — это теперь забота release.yaml.

Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>

* chore(release): publish

* chore(release): publish

* ci: tag-only триггеры для build-contracts и docs (#367)

build-contracts.yaml — только workflow_dispatch (ручная пересборка
`dicoop/contracts:<branch>` для отладки на dev-ноде). Тэги обрабатывает
release.yaml атомарно (контракты + контейнеры + webhook), отдельная
сборка по push'у в ветку только давала вторую параллельную сборку.

publish-docs.yaml и build-contracts-docs.yaml — на push:tags v* с
gate-job'ом, пропускающим только продакшн-тэги (без -alpha/-beta/-rc/
-test) на main. Раньше docs пересобирались на каждый push в
main/testnet/dev/reports/marketplace2 — впустую.

Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>

* fix(capital/time-tracking): личные доли estimate, partial-split, revert при decline

Три бага в распределении билетов времени, вскрытые на прод-инциденте voskhod
(проект CC7-1 «Концепция», estimate=15 ч, 3 creators):

БАГ #1 — recalcDoneEstimatesForContributorProject раздавал «общий остаток пула»
(estimate − committed_total) / N всем creators поровну. Закоммитивший свою долю
получал её ещё раз, остальные — урезанную (15/3=5 → после committed 5 у одного
становилось 10/3=3.33 у каждого, включая того кто уже закоммитил).

Фикс: личная доля = estimate/N − собственный committed estimate. Введён общий
helper redistributeIssueEstimateEntries, который используют и applyExplicit-
EstimateToTimeEntries (force=true), и recalcDoneEstimates (force=false с
no-op оптимизацией если раскладка уже совпадает с планом).

БАГ #2 — commitTime при partial split (entry.hours > requested) создавал новую
committed-запись без entry_type и estimate_snapshot. По default'у БД сохраняла
её как entry_type='hourly', что ломало последующий recalc (он фильтрует только
entry_type='estimate'). Фикс: явно копировать entry_type и estimate_snapshot
из оригинального entry.

БАГ #3 — declineCommit / handleDeclineCommit меняли только commit.status в БД,
но не возвращали time-entries в is_committed=false. После отказа мастера часы
оставались в total_committed_hours и не возвращались в available_hours. Фикс:
новый метод revertEntriesForDeclinedCommit в TimeTrackingInteractor + методы
findCommittedByCommitHash / revertCommittedEntriesByCommitHash в TimeEntry-
Repository. После revert делается force=true redistribute для затронутых DONE
задач, чтобы доли вернулись в норму.

Покрытие тестами: 17 unit-тестов в time-tracking.interactor.spec.ts с
регрессионными сценариями под каждый из трёх багов плюс integration-сценарий
полного lifecycle CC7-1 (estimate → коммит → decline → revert).

---------

Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>

* fix commit process

* chore(release): publish

* chore(release): publish

* feat(controller): is_server_init флаг в initSystem для provider-overwrite (#391)

* feat(docs-harness): onboarding 01..06 — визуальная цепочка регистрация → активация

Шесть сценариев visual docs-harness, покрывающих полный путь подключения
нового кооператива через провайдера Восход:

  01-register-coop                 — регистрация кооператива-клиента
  02-sign-and-submit               — подпись заявления о вступлении + PayInitial
  03-operator-approve              — chairman принимает заявку в реестре одобрений
  04-sign-connection-agreement     — Партнёр-1 видит ConnectionAgreementStepper
                                     (на текущем стенде получаем заглушку
                                     /signup, пока пайщик не принят)
  05-activate-from-registry        — оператор открывает карточку инстанса
                                     в provider-frontend, выбирает preset
  06-wait-instance-active          — pending → ACTIVE (overrideInstance
                                     для имитации финального статуса в шоте)

Обвязка:
  • lib/registrator-signup.mjs — переиспользуемый helper подписания.
  • lib/harness.mjs — расширенный dismissOnboardingDialogs (Положение ЦПП
    и связанные модалки chairman'а).
  • state/cooperatives/{,.gitkeep} — папка для фикстур; partner1.json
    (с приватным wif) игнорируется (.gitignore обновлён).

ОГРАНИЧЕНИЕ: в 06 финальный ACTIVE — это playwright-override JSON, а не
реальный POST /instances/activate с боевым Ansible до testnet300.coopenomics.world.
Реальный E2E (аренда VM на Hostkey + поднятие кооператива на домене)
будет следующим шагом Эпика 0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(controller): is_server_init flag в initSystem для разблокировки provider-overwrite

Поле data.is_server_init: boolean (optional) в InitDTO + домейн-интерфейсе.
Если true (вызов от провайдера через server-secret) — coopback ставит
init_by_server=true безусловно, даже если до этого пользователь успел
заполнить визард первым (user-init). Это разблокирует ситуацию, когда
провайдер не успел вызвать initSystem до того как chairman открыл
/install — следующий callInitSystemMutation от провайдера перезапишет
organization_data и пометит её readonly для визарда.

Без флага сохраняется прежняя логика: первая инициализация — серверная,
повторная наследует флаг.

Инцидент 2026-05-18 на partner1: при первой установке provider вообще
не успел/не сходил в callInitSystemMutation, визард пользователя
проинициализировал систему как user-init (init_by_server=false), визард
2-го захода не предзаполнил форму. С этим фиксом следующий вызов
provider'а поднимет флаг и фикстура встанет на место.

---------

Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(capital/convert): унифицировать шаблон 1080, убрать 1081/1082 (#392)

* refactor(capital/convert): унифицировать 1080 как универсальное заявление о конвертации, убрать 1081/1082

Шаблон 1080 (GenerationConvertStatement) уже технически универсален — содержит
обе суммы (main_wallet_amount/blagorost_wallet_amount) и условные блоки в
context. Шаблоны 1081 (GenerationToProjectConvertStatement) и 1082
(GenerationToCapitalizationConvertStatement) были недоделанными заглушками
без полей и нигде не подключены в UI.

Изменения:
- cooptypes: 1080 переименован GenerationToMainWalletConvertStatement →
  GenerationConvertStatement; title/description нейтральные. 1081/1082 удалены.
- factory: Template + Action 1080 переименованы; в Action добавлено
  super.formatAsset(...) для обеих сумм (как в Action 1020). 1081/1082 удалены.
- controller: DTO переименован; appendix_hash убран из generate-input и
  перенесён в signed-meta-input; добавлен enrich appendix_hash через
  AppendixRepository.findConfirmedByUsernameAndProjectHash в
  DistributionManagementInteractor.prepareGenerationConvertStatementData
  (по образцу InvestsManagementInteractor для 1020). Резолвер мутации
  переименован в capitalGenerateGenerationConvertStatement; убраны два
  резолвера 1081/1082. mutation-log-mapper обновлён.
- sdk: мутация переименована, две удалены, zeus regenerated.
- desktop: Distribution-фичи 1081/1082 удалены, 1080-фича переименована.
  ConvertSegment теперь шлёт project_hash + обе суммы (formatToEosioAsset) +
  to_wallet/to_blagorost; appendix_hash подтягивается на бекенде.
- controller schema.gql regenerated.

registry_id=1080 не меняется — on-chain контракт convertsegm не сверяет
registry_id, миграций БД/контракта не требуется.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(capital/convert): применить ревью — title «трансляция паевого взноса из программы Генерация»

По комментарию ревью в PR #392 (строка 37): принять доменный термин
«трансляция паевого взноса» (перенос между программами) вместо
«конвертация»; description согласован в том же стиле.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(capital): подтянуть 3 ссылки на GenerationConvertStatement → MoneyInvestStatement (#394)

PR #392 переименовал Cooperative.Registry.GenerationConvertStatement в
GenerationMoneyInvestStatement в @coopenomics/document, но в controller
осталось 3 несинхронизированные ссылки:

- distribution-management.service.ts:56 — registry_id метода generation
- distribution-management.interactor.ts:48,66 — тип Action в return и as-cast
- generation-convert-statement-document.dto.ts:12 — type action в DTO

Coopback падал на ts-node compile (TS2551/2724), что блокировало старт
всего dev-stack после reboot dev-chain 2026-05-18.

Co-authored-by: coopops <coopos@coopenomics.world>

* fix(controller): IS_UNIONED zod-парсер принимает string из .env (#395)

z.boolean().default(true) валится для переменной из .env, потому что
process.env всегда отдаёт строку: zod не приводит "true"/"false" к
boolean, в результате validateEnv падает с «IS_UNIONED: параметр не
установлен» и coopback не стартует, если в .env стоит IS_UNIONED=false
(стандартный dev-обход messenger-гейта, см. flow подключения партнёра).

Заменено на string().default('true').transform(v => v === 'true') —
сохранение прежнего default=true и поддержка string-форм из env.

Co-authored-by: coopops <coopos@coopenomics.world>

* fix(capital): revert ошибочной замены GenerationConvertStatement → MoneyInvestStatement (#394) (#396)

PR #394 «починил» TS-ошибки coopback заменой типа `Cooperative.Registry.GenerationConvertStatement`
на `GenerationMoneyInvestStatement` в 3 файлах controller'а. Это семантически неверно:

- 1080 GenerationConvertStatement — заявление о трансляции паевого взноса
  (поля: project_hash, main_wallet_amount, blagorost_wallet_amount, to_wallet, to_blagorost, appendix_hash)
- 1020 GenerationMoneyInvestStatement — заявление о денежном паевом взносе по программе Генерация
  (совсем другой набор полей)

DTO BaseGenerationConvertStatementMetaDocumentInputDTO декларирует поля 1080, но `implements ExcludeCommonProps<action>`
где action = тип 1020 → TS2352 на as-cast в interactor, потому что Action'ы не пересекаются по полям.

Реальная причина исходных ошибок coopback после #392 — несвежий dist `@coopenomics/cooptypes`
на dev-узле (старое имя символа). Лечится пересборкой пакета, не переименованием ссылок.

Изменено (откат #394):
- generation-convert-statement-document.dto.ts:12 — `action = ...GenerationConvertStatement.Action`
- distribution-management.service.ts:56 — `registry_id: ...GenerationConvertStatement.registry_id`
- distribution-management.interactor.ts:48,66 — `Promise<...GenerationConvertStatement.Action>`

Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>

* fix(capital/convertsegm): регистрировать convert_statement в реестре документов как completed (#403)

Финальная фаза процесса p.cap.rid (convertsegm) принимала document2 convert_statement (шаблон 1080)
параметром, но:

1. **Не проверяла подпись** — `verify_document_or_fail` отсутствовала, поэтому on-chain
   принял бы любую сконструированную document2 без валидной user-подписи.
   У signact1/signact2 (соседние фазы того же процесса) verify есть — здесь забыли.

2. **Не регистрировала документ в реестре** — `newlink`/`newsubmitted`/`newresolved`
   не вызывался, заявление о трансляции 1080 «терялось»: off-chain controller (process_instance)
   не видел финальный документ привязанным к result_hash, процесс p.cap.rid не помечался completed.
   У pushrslt (create_approval) и signact2 (newlink с SIGN_ACT2_RESULT) линковка есть.

Канон есть в soviet/src/system/converttoaxn.cpp:54 и soviet/src/agreement/sndagreement.cpp:104:
паттерн `Soviet::make_complete_document(calling_contract, coopname, username, action, package_hash, document)`
шлёт newsubmitted + newresolved одной парой, package = анкер процесса (здесь result_hash).

Изменено:
- names.hpp: новая константа Names::Capital::CONVERT_SEGMENT = "convertsegm"_n (12 символов)
- convertsegm.cpp:
  - verify_document_or_fail(convert_statement, {username}) сразу после require_auth
  - Soviet::make_complete_document(...) ДО delete_result (чтобы линковка прошла, пока result_hash ещё анкер)

Контракт capital собирается без ошибок (cdt-cpp testnet mode), warnings — старые ricardian.

Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>

* feat(epic-0): onboarding harness 01-08 + install bundle adduser+createBoard в одну tx (#398)

* fix(controller): IS_UNIONED zod-парсер принимает string из .env

z.boolean().default(true) валится для переменной из .env, потому что
process.env всегда отдаёт строку: zod не приводит "true"/"false" к
boolean, в результате validateEnv падает с «IS_UNIONED: параметр не
установлен» и coopback не стартует, если в .env стоит IS_UNIONED=false
(стандартный dev-обход messenger-гейта, см. flow подключения партнёра).

Заменено на string().default('true').transform(v => v === 'true') —
сохранение прежнего default=true и поддержка string-форм из env.

* feat(epic-0): partner onboarding harness — signin → sign agreements → connect

Что добавлено:
- desktop/quasar.config.cjs: vite server.allowedHosts для voskhod-dev/
  partner-dev/api-dev (Vite 5.4+ блокирует cross-origin Host без явного
  списка — иначе SSR/HMR-сервер возвращает 403 «Blocked request»).
- docs-harness/lib/harness.mjs: helper signOnboardingAgreements —
  реальная подпись каскада SignAgreementDialog (wallet/signature/
  privacy/user), каждый клик отправляет sendAgreement → wallet::signagree
  on-chain. В отличие от dismissOnboardingDialogs делает on-chain эффект
  (см. inc 2026-05-18: stale vault SERVER_SECRET → bad decrypt).
- scenarios/onboarding/01..07: новый partner-flow Эпика 0 — partner1
  заходит в Восход, подписывает 4 типовых соглашения, идёт на «Подключение»,
  ant одобряет, наблюдаем установку partner-dev.
- scenarios/registration/01..04: архив старого registration-doc как
  отдельная история (см. mem feedback_provider_mono_pr_flow).
- scripts/debug-*: вспомогательные скрипты для портал-структуры и
  chairman onboarding'а.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(docs-harness/08): chairman install wizard на partner-dev

Сценарий 08 проходит установочный wizard /:coopname/install целиком:
шаг 1 (RequestKeyForm: WIF partner1 из state/cooperatives/partner1.json),
шаг 2 (SetInitForm: readonly orgdata из is_server_init=true, «Далее»),
шаг 3 (SetSovietForm: один председатель — Иванов И.И.),
шаг 4 (SetVariablesForm: ОПФ+ во всех падежах, устав, конф.email).

Финальный submit «Завершить установку» сейчас падает on-chain в
soviet::create — `assertion: Один из аккаунтов не найден в реестре
пайщиков`. Причина: в install.interactor.ts adduser и createBoard
шлются двумя отдельными tx; partner1 nodeos не producer, между tx есть
лаг p2p-репликации, createBoard приходит до того как participants[N]
обновился. Шот 06-error-state снимается при таймауте; шот
06-completed появится после фикса race в install.interactor (отдельный
коммит).

Сценарий запускается:

  BASE_URL=https://partner-dev.coopenomics.world COOPNAME=partner1 \
  node run.mjs onboarding/08-chairman-install-on-partner-dev

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(controller/install): bundle adduser+createBoard в одну tx

Cause. install.interactor.ts шлёт adduser×N и createBoard двумя отдельными
tx через BlockchainService. На production-нодах с producer-схемой это
работает (lag реплицирования минимальный), но на dev-loop'е partner-
coopback соединён со своим nodeos, который p2p-репликой подтягивает
блоки от producer'а — после accept'а adduser в local state ещё нет
soviet::participants[username] к моменту push'а createBoard. Контракт
soviet::createboard падает «Один из аккаунтов не найден в реестре
пайщиков».

Fix. Объединил все adduser-action'ы и createBoard-action в одну tx
через новый метод BlockchainPort.installSoviet(). Обе action'ы теперь
атомарны в одном блоке — soviet::addpartcpnt (inline action от
adduser) обновляет participants и createBoard видит запись сразу же
в том же блоке.

Поток в install.interactor.ts перестроен в два шага:
  1. Цикл по soviet: createUser в БД + setupNotificationSubscriber +
     сбор addUserActions[] и members[] (без on-chain активности).
  2. installSoviet(addUserActions, createBoardData) — одна tx.

Catch при ошибке on-chain (как и раньше) откатывает users из БД.

Зачем. Закрывает блокер сценария 08-chairman-install-on-partner-dev:
без этого финальный экран wizard'а «Установка завершена» недостижим
на dev-loop'е (Эпик 0 не закрывается).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(docs-harness/09): invite-token → WIF → signin chairman

Закрывает Эпик 0: после сценария 08 (chairman install wizard) на
partner1 в Postgres появляется invite-токен председателя, но Novu не
доставит его на @example.com адрес. Сценарий 09 идёт за токеном
напрямую в БД через `ssh partner1 → docker exec postgres → psql` и
прогоняет до финального signin под новым ключом.

Шаги:
1. fetchLatestInviteToken — SELECT token FROM tokens WHERE
   type='invite' AND blacklisted=false AND expires > NOW() LIMIT 1.
2. Открываем `${BASE_URL}/${COOPNAME}/auth/invite?token=<token>` —
   widget Invite.vue клиентски генерирует новый WIF (generateAccount).
3. Извлекаем WIF из q-input, сохраняем в
   state/cooperatives/partner1-chairman.json для последующих сценариев.
4. Чекбокс «Я сохранил ключ» → «Установить ключ» → resetKey шлёт
   on-chain ChangeKey, фронт редиректит на /auth/signin.
5. Финальный signin под chairman.partner1@example.com + новый WIF;
   ждём перехода на /chairman или /participant.

Запускается:

  BASE_URL=https://partner-dev.coopenomics.world COOPNAME=partner1 \
    node run.mjs onboarding/09-chairman-key-and-login

Требует SSH-доступа к partner1 (PARTNER_SSH=user1@91.218.246.46 по
умолчанию) и предварительно прогнанный сценарий 08 (после merge
fix(controller/install) — иначе токен не появится).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(controller/install): откатить installSoviet bundle на sleep 2s

Bundle adduser×N + createBoard в одну tx работает, но требует расширения
BlockchainPort и больше read'а; для не-producer-нод (dev-loop) достаточно
короткой паузы между adduser и createBoard, чтобы p2p-реплика подтянула
блок с participants. 2с гарантированно перекрывают и prod (~50ms), и
dev-loop (1-3с).

Отменён 3f2ecc42cf (installSoviet в blockchain.port + blockchain.service +
install.interactor), добавлен `await new Promise(setTimeout, 2000)` между
циклом adduser и createBoard.

Это однострочный фикс race-condition; bundle вернётся когда понадобится
многошаговая атомарность.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: coopops <coopops@coopenomics.world>

* fix(asset-formatting): truncate (toward-zero) до 2 знаков вместо math-round (#404)

UI показывал баланс 99999.9999 RUB как 100 000,00 — пользователь видел
больше, чем реально на кошельке, и попытка инвестировать «весь баланс»
падала в walletop TRANSFER с insufficient funds (precision=4 на цепи
vs precision=2 в UI).

Принцип: «никогда не показывать сумму больше реальной». Truncate
выполняется на уровне строки, без преобразования в Number, чтобы
избежать FP-погрешности (0.29 * 100 = 28.999... в IEEE 754).

Изменения:
- desktop: новая утилита floorDecimalString — единый источник истины.
- desktop: formatAsset2Digits / formatToAsset / addAssets используют floor.
- desktop: убран double-formatting amount.toFixed(4) перед formatAsset2Digits
  в ParticipantWalletsPage.
- factory: Factory.formatAsset / Factory.formatShare через приватный
  truncateDecimal — тот же принцип в документах (заявления, акты).

Не затронуто (отдельный фолоу-ап): 10 файлов с inline .toFixed(4)
перед отправкой в контракт — рекомендую заменить на formatToAsset().

Co-authored-by: coopops <coopos@coopenomics.world>

* feat(capital): требовать допуск для просмотра артефактов проекта/компонента (#407)

Любой авторизованный пользователь раньше мог запросить capitalStories /
capitalStory и получить требования всех проектов кооператива. Закрываем эту
лазейку на бекенде и показываем аккуратную заглушку на фронте.

Backend (controller):
- GenerationService.getStories: фильтрует список разрешённых project_hash
  через PermissionsService. Допуск к корневому проекту каскадно открывает
  все его компоненты; допуск к одному компоненту — только этот компонент.
  Запрос без project_hash/issue_hash доступен только chairman/member.
- GenerationService.getStoryByHash: проверяет допуск через project_hash
  (или issue.project_hash для story с issue_hash) и возвращает null без
  доступа.
- getCapitalStories / getCapitalStory резолверы теперь получают currentUser.
- ProjectPermissionsOutputDTO: добавлены has_parent_clearance и
  can_view_artifacts — для отображения заглушек на фронте.

Frontend (desktop):
- ProjectRequirementsPage / ComponentRequirementsPage: показывают
  ArtifactsAccessPlaceholder когда can_view_artifacts=false, с кнопкой
  получения допуска (или PendingClearanceButton при поданном запросе).
- Новый shared компонент ArtifactsAccessPlaceholder в стиле остальных
  заглушек расширения capital.

SDK:
- projectSelector обновлён под новые поля; schema.gql / zeus
  регенерированы.

Co-authored-by: coopops <coopos@coopenomics.world>

* fix(capital/convertsegm): линковать 1080 к пакету через newlink, не make_complete_document (#409)

PR #403 использовал Soviet::make_complete_document — это вытащило заявление о трансляции (1080)
на верхний уровень реестра как самостоятельный документ. В разделе «Документы» 1080 оказался
отдельной строкой, под ним болтались протокол+акт из соседнего пакета (агрегатор подтягивал их
по тому же result_hash без главного документа).

Правильно — линковать 1080 к существующему пакету процесса p.cap.rid, где ведущим документом
является заявление о внесении РИД (1040, statement из pushrslt). Канон уже есть в signact1/signact2:

  Action::send<newlink_interface>(_soviet, "newlink"_n, _capital, coopname, username,
                                  Names::Capital::SIGN_ACT*_RESULT, result_hash, act);

Здесь по тому же паттерну, action = Names::Capital::CONVERT_SEGMENT (константа уже в dev из #403,
её не трогаем), package = result_hash. Off-chain controller добавит 1080 в группу пакета 1040,
а не на верхний уровень.

verify_document_or_fail оставлен — это безопасность, не связана с проблемой реестра.

Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>

* migration complete

* [598-14][@ant] fix(desktop): vue-tsc на marketplace2 после merge'ов

— OperatorInventoryLabelingPage: BarcodeStrategy/Format через Zeus.MarketplaceBarcode*
— OperatorOwnWarehousePage: pug-tернарник в одну строку (vue-tsc не парсит мультиline | text-block)
— AdminWriteoffsPage: statuses[] через Zeus.MarketplaceWriteoffProposalStatus, trigger === CRON
— SubmitToCouncilDialog: useSessionStore вместо удалённого useStore
— ProcessesPage: ProcessDocument → { hash, source.{code,table,...} } + sortOrder в PaginationInput

* [598-14][@ant] fix(controller): DI marketplace модулей чтобы controller стартовал на marketplace2

— application/marketplace-application.module: импортирует MarketplaceInfrastructureModule (резолверы инжектят MARKETPLACE_*_REPOSITORY) и AccountInfrastructureModule (MarketplaceNotificationService требует ACCOUNT_DATA_PORT). Импорт ExtensionDomainModule удалён — порождал циркуляр AppModule → ExtensionDomainModule → ExtensionsModule → MarketplacePluginModule → MarketplaceExtensionApplicationModule, из-за чего Nest падал с "module at index [6] is undefined" сразу после merge Эпика 8.
— marketplace-writeoff-cron.service: ExtensionDomainService инжектится через @Optional() с null-fallback (cron сам читает env при отсутствии extension config). Это убирает необходимость в проблемном импорте ExtensionDomainModule.
— infrastructure/marketplace-infrastructure.module: импорт VaultDomainModule, потому что MarketplaceCanonicalBlockchainAdapter инжектит VAULT_DOMAIN_SERVICE.

Также добавлен MINIO_ENDPOINT в controller/.env (Эпик 7 / Story 7.1 stol-zakazov:images bucket теперь корректно поднимается на dev).

---------

Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: coopops <coopops@coopenomics.world>
Co-authored-by: ant <ant@noreply.local>
2026-05-19 21:32:16 +05:00
coopops 8af8bf7512 revert(desktop/RailUserCard): убрать transition свёртки — baseline-skipping
Анимация max-height + opacity при свёртке давала видимый прыжок крупной
суммы баланса (`font-size: 22px` + `display: flex; align-items: baseline`):
во время промежуточных кадров baseline переcчитывался относительно
схлопывающегося контейнера → текст «прыгал в середину» и обратно.

Возвращаемся к canon `display: none`. Resize моментальный, без skipping.
Поведение rail (top опускается, bottom-chevron остаётся через flex-spacer)
работает и без анимации — пользователь это и подтвердил в фидбеке.

Chevron rotation через :deep(.q-icon) и balance-route не трогаем —
они независимы.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 15:33:04 +00:00
coopops 5ef499a75f feat(desktop/_dev/ui): TokenPalette + chevron full-width fix
1. **Палитра системы.** Новая секция «00» в шапке dev-страницы со всеми
   основными --p-* токенами:
   - Акцент (accent / hover / press)
   - Поверхности (canvas / canvas-2 / surface / 2 / 3)
   - Текст (ink / ink-on-accent)
   - Линии (line / 1 / 2) — текстовый input для rgba
   - Статусы (pos / neg / warn / info)
   - Программные тинты (blagorost / wallet / generator)

   Color-picker для hex-токенов, текстовый ввод для rgba. Live-override
   через `document.documentElement.style.setProperty()` — inline-style на
   :root перебивает CSS-правила, перекрашивание мгновенное. Подсветка
   переопределённых токенов + счётчик в шапке + кнопка «Сбросить» (через
   removeProperty). Watch на Quasar Dark — при смене темы пикеры
   обновляются на новые resolved-значения.

2. **Chevron в свёрнутом виде — full-width.** Из-за того что для плавной
   анимации primary остаётся в DOM (max-height:0 вместо display:none),
   у него сохранялся `flex: 1` и он съедал ширину карточки → chevron не
   мог растянуться через canon-правило `width: 100%`. В `.is-collapsed`
   добавили `flex: 0 0 0` + `width: 0` для primary — chevron теперь
   корректно занимает всю ширину.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 15:31:54 +00:00
coopops 79afee2b82 fix(desktop/RailUserCard): chevron rotation, плавная свёртка, balance-route
По фидбеку:

1. **Chevron не переворачивается.** Canon-правило
   `.rail__usercard__collapse svg { transform: rotate(180deg) }` рассчитано
   на `<svg>`, а Quasar `<q-icon>` рендерит `<i class="q-icon">`. Прокидываем
   transition + rotate через scoped `:deep(.q-icon)` — chevron теперь крутится
   при свёртке/развёртке.

2. **«Карточка улетает»** — резкий jump при `display:none` создаёт визуальный
   прыжок. Подменяем display:none на `max-height: 0` + `opacity: 0` с
   transition'ом. Логика canon-селектора `.is-collapsed` не нарушается,
   просто добавлена плавность. Если структурное поведение rail (top
   опускается, bottom-chevron остаётся на месте) всё равно неудобно —
   перевёрстаем во вторую итерацию.

3. **Клик по балансу → маршрут кошелька.** Новый prop `balanceRoute` —
   если задан, блок `.rail__balance` оборачивается в `<router-link :to>`.
   Параллельно эмитим `balance-click` — для аналитики или функционального
   handler'а. На витрине привязал к `/_dev/ui#wallet` чтобы проверить.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 15:28:31 +00:00
coopops 0a977dcd83 feat(desktop/domain): RailUserCard + фикс RouteMeta для dev-роута
1. `RouteMeta.icon` объявлен обязательным в `src/env.d.ts` — добавили
   `icon: 'fa-solid fa-flask'` для dev-роута, иначе vue-tsc валится.

2. `RailUserCard` (`shared/ui/domain/RailUserCard`) — доменный
   общеплатформенный компонент мини-кошелька в нижней части drawer'а.
   Это будущий E11 (Волна 2), но логично вытащить раньше, чтобы основной
   layout можно было заменить целиком уже сейчас.

   Реализован по canon строго: rail__usercard с usertop+balance+actions,
   collapse-chevron справа с поворотом arrow при свёртке (через
   `.is-collapsed` модификатор + canon CSS), отдельный `rail__signout`
   блок (выходит вне `.rail__usercard` — рендерится сразу после, в том
   же `aside.rail` через footer-slot).

   Контракт строго dumb по архитектуре: только props/emits/slots.
   - props: name, role, avatarSrc, balance, symbol, balanceLabel,
     lockedBalance, lockedLabel, primaryActionLabel, collapsed,
     showSignout, signoutLabel
   - emits: primary-action, update:collapsed (v-model), signout
   - slots: usertop-extra, actions

   Coнnected-обёртки (с инжектом store/auth) — на уровне страницы,
   не здесь.

3. Витрина `/_dev/ui`:
   - Секция 12 (AppDrawer): заменили временный огрызок rail-demo__user
     на полноценный `<RailUserCard show-signout>` через footer-slot.
   - Секция 16 (Каркас приложения): аналогично — теперь у rail полноценный
     footer.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 15:19:09 +00:00
Alex Ant 58652e5fae Эпик 11 (тех.долги): FR45 AC5 jest-тесты + 598-21 frontend batch-flow (#408)
Publish Docs / build-and-publish-docs (push) Failing after 11m42s
* [598-14][@ant] feat: дозакрытие техдолгов Эпика 11 — FR45 AC5 jest-тесты + 598-21 frontend batch-flow

— FR45 AC5 (598-15): jest unit-тесты compensating-rollback signSupp/signChair
  с mock chainPort failure → status не сдвинут + idempotent retry.
  6 тестов в marketplace-apl-reception.service.spec.ts (76 сек локально).
— 598-21 frontend batch-flow в OperatorInventoryLabelingPage:
  тогл «Маркировка партии целиком / Поштучно по заказу», выбор default
  стратегии и формата, селектор партии из marketplaceListShipments,
  идемпотентность через skipped_order_ids в toast, A4 print grid в
  @media print, page-break-inside: avoid на этикетку.
— SDK добавлена mutation marketplaceLabelShipmentInventory + селектор
  результата (labeled_order_ids / skipped_order_ids); zeus regen из
  controller schema.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* [598-14][@ant] fix: добавить _validate typeguard на marketplaceLabelShipmentInventoryResultSelector

Селектор должен проходить compile-time проверку MakeAllFieldsRequired<ValueTypes[...]>
как все остальные селекторы в проекте; добивает review-комментарий PR #408.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: ant <ant@noreply.local>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 13:15:05 +05:00
coopops 74539a6c46 feat(desktop/layout): E4 — AppDrawer + AppHeader + PageHead + PageTabs
E4 «Layout-каркас» Волны 1: 4 структурные обёртки в `src/shared/ui/layout/`.
По архитектуре — только props/emits/slots, без store/router/api, без знания
о данных. Cтраницы передают menu-items, breadcrumbs, табы и активные ключи
сверху.

Состав:
- `AppDrawer` — canon `.rail`: solid surface, активный пункт soft-accent
  пилюлей + 2-px рейл слева. Поддерживает плоский список + секции
  (RailSection с eyebrow-меткой), opt-in ⌘K-кнопку, slot `footer` для
  user-card / logout.
- `AppHeader` — canon `.topbar`: бургер · crumb · `topbar__actions` ·
  `topbar__right`. Поддерживает простой title и multi-level breadcrumb.
  Глобальные действия через слоты `notifications`/`theme`/`wallet`/`profile`
  или общий `right`.
- `PageHead` — canon `.page-head`: eyebrow + title + subtitle + slot
  `actions`. Используется когда страница БЕЗ подстраниц.
- `PageTabs` — canon `.tabbar` + `.tab`: вкладки подстраниц с tab__count,
  опциональный hairline-разделитель и `tabbar__actions` справа.

Также в составе коммита:
- Убрали AuthCard из base — он композит (cap + body + head + footer),
  переедет в E6 Auth flow рядом с SignUp/ResetKey/Invite.
- BaseForm: gap полей 16→10px (тесные формы как в каноне).
- Витрина `/_dev/ui`: 4 новые секции (12-15) на каждый layout-компонент +
  секция 16 «Каркас приложения» — AppDrawer + AppHeader + PageTabs в живой
  связке без legacy QLayout/QDrawer.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 06:17:01 +00:00
coopops dc93704990 fix(desktop/mono-platform): убрать HTML-теги <style> из canon CSS
Скрипт извлечения из bundled HTML оставил `  <style>...` в начале и
`</style>` в конце обоих файлов. CSS-парсер ловил `<` как ошибочный
селектор и пропускал блок до ближайшей `}` — съедая весь первый блок
правил.

В tokens.css первым блоком был `:root, [data-theme="light"]` со светлой
палитрой — она целиком игнорировалась. `[data-theme="dark"]` ниже
парсился нормально, поэтому тёмная тема работала, а светлая — нет:
кнопки primary/ghost/danger оставались с transparent background и
сливались с canvas.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 05:54:25 +00:00
coopops 02a4a2aa14 feat(desktop): MONO Platform v2 — базовые компоненты дизайн-системы
Заносим эталонную дизайн-систему в проект — только базовые элементы для
дальнейшей компоновки. Source — MONO Design System.html.

Архитектура (по `_bmad-output/planning-artifacts/architecture.md`):
- `src/css/mono-platform/{tokens.css,components.css}` — canon токены и стили
  компонентов 1:1 из эталона; импортируются первыми в quasar.config css[].
- `src/shared/ui/base/<Имя>/{Vue,types,index}` — 14 базовых обёрток (Vue 3 +
  TS, native HTML над canon BEM, no Quasar-deps кроме Dark API в ThemeToggle).
  FSD-граница: только props/emits/slots, никаких store/router/api.
- `src/boot/ui.ts` — глобальная регистрация 14 базовых компонентов.
- `src/boot/theme.ts` — sync `html[data-theme]` ↔ Quasar Dark.
- Inter + JetBrains Mono в index.html.

14 базовых:
BaseButton, BaseInput, BaseSelect, BaseCard, BaseTable, BaseChip, BaseBadge,
BaseDialog, BaseBanner, BaseForm, EmptyState, Avatar, ThemeToggle, AuthCard.

Витрина `/_dev/ui` (dev-only) — каждая обёртка отдельной секцией над чистым
canon-канвасом, без layout-обёрток. Маршрут смонтирован на корневом уровне,
ВНЕ DynamicLayoutWrapper, чтобы legacy default.vue не перебивал стили.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 05:30:28 +00:00
Alex Ant a1513127d1 Эпик 11 «Контрактный аудит и регуляторика релиза» MVP Стола заказов (598-14) (#405)
* Эпик 11: «Реестр процессов» на столе бухгалтера (598-14)

UI-надстройка над уже работающим Phase A/B ProcessRegistry в core controller:
страница /reports/processes показывает листинг процессов ledger2 с фильтрами
по типу процесса (LEDGER2_PROCESS_REGISTRY) и пайщику, при раскрытии строки
рисует events + documents процесса (через getProcess), отдаёт слот
processInfoFactory для бизнес-описания (marketplace handlers уже регистрируют
SUPPLY/RETURN/WRITEOFF в extensions/market/app/extensions.ts) и кнопку
deep-link «Открыть в реестре операций» (filter process_hash).

Используется existing SDK query `Queries.Processes.ListProcesses` —
расширены entities/Process api+store+types методом `listProcesses` и
type alias'ами IProcessListInput/IProcessListResult/IProcessSummary.

Стол бухгалтера приобретает четвёртый базовый реестр (операции, проводки,
**процессы**, кошельки, счета) — закрывает аудит-видимость marketplace
flow «Стола заказов» в рамках Эпика 11.

* Эпик 11 техдолг 598-21: batch-маркировка партии за один вызов

Mutation `marketplaceLabelShipmentInventory(shipment_id, default_strategy?,
per_order_overrides?)` массово маркирует все Order'ы партии поставки —
оператор перестаёт проходить N карточек по одной.

Service `MarketplaceInventoryLabelService.labelShipment`:
  1. Shipment.findById → берёт coopname/cycle_id/braname.
  2. orderRepo.findByCycleId(coopname, cycle_id) → filter по
     delivery_braname партии = все Order'ы партии.
  3. Для каждого Order'а зовёт уже работающий execute() — единый
     источник правил маркировки (стратегия из Offer.barcode_strategy
     по 598-22, per-Order override для смешанных Offer'ов в партии).
  4. Идемпотентность: ConflictException от execute() при повторной
     маркировке → skipped_order_ids в результате (повторный заход на
     страницу не валится fatal'ом).

Возврат включает labeled_order_ids + skipped_order_ids + сами наклейки
(`MarketplaceInventoryItem[]`) — UI оператора может одним вызовом
получить grid этикеток для печати и понимать что было сделано.

`@media print` grid layout и расширение `OperatorInventoryLabelingPage`
под выбор default-стратегии на всю партию — следующий шаг, mutation
теперь доступна.

* Эпик 11 Stories 11.2 + 11.3: off-chain инварианты ledger2 marketplace + coverage трассировки 13 op-кодов

Backend-side scaffold контрактных проверок Stories 11.2 (Journal/wjournal
трассировка) + 11.3 (CI-инварианты ledger2). Реализация — pure-функциями
без NestJS/PG: работают на синтетических массивах Ledger2OperationDTO,
гонятся unit-тестом без реального chain'а (отвечает требованию off-chain
агрегации: никаких сумм по всем кошелькам пайщиков в смарт-контракте).

Состав:

- `marketplace-ledger2-invariants.ts` — 6 pure-агрегаторов (I1 payout
  balance, I2 Δ86, I3 acc.10, I4 acc.91 transit = 0, I5 blocked в
  w.mkt.member, I6 нет orphan o.mkt.block). Та же логика что UI стола
  бухгалтера (`/reports/operations`, `/reports/postings`).
- `marketplace-ledger2-invariants.spec.ts` — 86 тестов: happy path +
  violation path для каждого I1..I6 + property-based на 50 случайных
  последовательностях (mix happy/cancel/return/writeoff).
- `marketplace-process-trace-coverage.spec.ts` — 10 групп: coverage по
  всем 13 marketplace operation_code (12 o.mkt.* + o.wal.conv) в
  cooptypes + OPERATION_CODE_TO_PROCESS_TYPE + PROCESS_HASH_LOCATOR;
  Phase-A якорь связности apply+walletop+debit+credit под одним
  process_hash; full-flow scenarios для supply/return/wroff.

Контрактные тесты в Antelope simulator (mono-ai-5) остаются за другим
агентом; здесь — backend-side mapping контроль и off-chain агрегация
инвариантов, доступные для CI и admin-вью.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: ant <ant@noreply.local>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 10:23:10 +05:00
Alex Ant 8326ddeb4e Эпик 9 «Склад и отчётность кооператива» MVP Стола заказов (598-12) (#400)
Publish Docs / build-and-publish-docs (push) Failing after 11m38s
* [598-12][@ant] chore: process-hash-locator Phase B готов к активации marketplace

Эпик 9 / Story 9.3: обновлён комментарий к ключам p.mkt.supply / p.mkt.return /
p.mkt.wroff. Phase A через OPERATION_CODE_TO_PROCESS_TYPE уже связывает все
o.mkt.* с тремя process_type из cooptypes; Phase B ждёт появления entity-
таблицы marketplace::requests с полем request_hash (Locked Decision L10).
Заглушка [] остаётся корректной — Phase B наполняется одной правкой при
готовности C++.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-12][@ant] feat: operator-стол «Склад моего КУ» — Story 9.1

Лента marketplace_inventory отфильтрована по braname текущего КУ;
per-row статусы (LABELED / ISSUED / RETURNED / WRITTEN_OFF) через
mp-status-chip, фильтры по статусу/orderer, summary count by status
(activeOnly + writtenOff). Сортировка по labeled_at DESC по умолчанию.

Backend-доступ ограничивается guard'ом Warehouse: ['read:own-KU'] —
оператор видит только свой участок. Real-time подписки AR37 SDK
платформы подключаются отдельным шагом эпика (на MVP — refresh кнопкой).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-12][@ant] feat: admin-стол «Сводный склад» через WarehouseSummaryGrid — Story 9.2

Две вкладки admin-стола (UX-DR16 + DR31, mp-role-admin):
- «Сводный склад» — агрегация marketplace_inventory по ku × sku через
  canon-виджет WarehouseSummaryGrid (incoming = LABELED + RETURNED, outgoing
  = ISSUED, balance — транзитный остаток без ISSUED);
- «Поток заказов и поставок» — sums маркировок / выдач / возвратов /
  списаний + топ-10 SKU по обороту. Графики динамики (orders_per_day,
  supplies_per_day, writeoffs_per_month) подключаются по AR37 SDK-
  подпискам платформы отдельным шагом.

Backend-доступ Warehouse: ['read:all'] (chairman + member).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-12][@ant] feat: SDK обёртки processes + страница «История операций» — Story 9.5

SDK:
- selectors/processes/processViewSelector + processSummarySelector
  (полное покрытие ProcessView / ProcessSummary / Pagination)
- queries/processes/getProcess + listProcesses
- queries/index.ts: export * as Processes

Desktop pages/Marketplace/OperationsHistory:
- лента всех процессов с фильтрами по coopname / processType / username;
- разворот через стандартный core processRegistry.getProcess(process_hash)
  показывает actions, delta_history и связанные documents;
- кнопка «Скачать JSON» для аудиторской выгрузки конкретного процесса
  (AR36, L13 pull-модель).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-12][@ant] feat: UI-stub «Экосистема кооперативов» — Story 9.4

Раздел admin-стола под межкооперативную торговлю Phase 2 (NFR-Sc2 / AR18).
На MVP — заглушка с информацией о текущем кооперативе и явным сообщением
что список других controller'ов с marketplace появится после подключения
платформенного ecosystem_registry. Backend-регистрация controller через
core-API в отдельном Эпике 9 + техдолге platform (см. issue эпика).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-12][@ant] feat: маршруты market workspace под Эпик 9

Регистрирует 4 новые страницы Эпика 9 в desktop/extensions/market/install:
- market-pvz/warehouse — Story 9.1 «Склад моего КУ» (operator-стол КУ);
- market/warehouse-summary — Story 9.2 «Сводный склад» (admin-стол);
- market/history — Story 9.5 «История операций» (admin-стол);
- market/ecosystem — Story 9.4 «Экосистема» (admin-стол, MVP-stub).

Доступы соответствуют access-matrix marketplace: warehouse-стол КУ —
только chairman; сводный склад / история / экосистема — chairman +
member (board_readonly).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-12][@ant] chore: регенерация schema.gql / zeus / SDK Zeus + fix DTO writeoff

Source-of-truth GraphQL-схема была устаревшая после Эпика 8 — DTO
`MarketplaceWriteoffProposalItemDTO` использовал `?: string | null` без
explicit-`@Field(() => String)`, и Nest GraphQL падал при
`generate-schema` ⇒ Zeus в SDK не получил writeoff queries и Processes
из Story 9.5.

- DTO: пять nullable string-полей writeoff'а получили `@Field(() => String, { nullable: true })`.
- schema.gql регенерирован (`pnpm run generate-schema`).
- zeus обновлён в controller/zeus и sdk/src/zeus (`pnpm run generate-client`).
- В Zeus теперь видны: ProcessView / ProcessSummary / ProcessesFilter,
  MarketplaceWriteoffProposal / PaginatedMarketplaceWriteoffProposals и т.п.

После этого `cd components/sdk && pnpm run build` собирается без ошибок;
desktop потребители (AdminWriteoffs API + новая страница OperationsHistory)
получают типизированный доступ к Zeus.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-12][@ant] fix: enum-значения статусов + убрать as const в bucket аккумуляторе

После регенерации Zeus `MarketplaceInventoryStatus` стал nominal enum
вместо string-literal union — статусы передаются через
`Zeus.MarketplaceInventoryStatus.LABELED|ISSUED|RETURNED|WRITTEN_OFF`,
а не голыми строками.

В `AdminWarehouseSummaryPage.warehouseRows` убран лишний `as const` на
дефолтном bucket'е — он делал поля `in/out/balance` readonly и ломал
накопление через `b.in += qty`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-12][@ant] feat: Phase B locator p.mkt.* — реальные ссылки на marketplace::orders/retrequests/wroffprops

Story 9.3. Контракт marketplace опубликован в Эпике 11; все три marketplace-процесса
(supply / return / writeoff) уже имеют свои entity-таблицы с одноимённым
checksum256-полем `hash`. Pre-existing заглушка `[]` с упоминанием Locked Decision L10
снята: теперь Phase B end-to-end отдаёт текущий снэпшот процесса из blockchain_deltas.

  - p.mkt.supply -> marketplace::orders.hash
  - p.mkt.return -> marketplace::retrequests.hash
  - p.mkt.wroff  -> marketplace::wroffprops.hash

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-12][@ant] refactor: убрать дубль «Истории операций marketplace» — стол бухгалтера единая точка

Старая страница `pages/Marketplace/OperationsHistory` повторяла стол бухгалтера
(`extensions/reports`) — actions / delta_history / documents видны там в общем
реестре операций. По ревью к PR #400 удаляем полностью; вместо неё marketplace
будет инжектировать бизнес-описание процесса в стол бухгалтера через фабрику
(см. следующие коммиты).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-12][@ant] feat: фабрика processInfoFactory и три marketplace info-widget'а — слот в стол бухгалтера

Аналог DecisionFactory, но для бизнес-описания процесса в реестре операций
стола бухгалтера. Каждое расширение регистрирует свой компонент по `process_type`;
стол бухгалтера рендерит его в развёрнутой строке после проводок и движений
по кошелькам. Дубля «истории» больше нет.

  - shared/lib/process-info-factory + types: реестр + getInfoComponent
  - widgets/Marketplace/ProcessSupplyInfoWidget — заказчик, поставщик, КУ,
    состояние, единицы; deep-link «открыть заказ на столе ПВЗ»
  - widgets/Marketplace/ProcessReturnInfoWidget — заказчик, состояние,
    исходный заказ, причина; deep-link «открыть заявление на столе ПВЗ»
  - widgets/Marketplace/ProcessWriteoffInfoWidget — источник проекта (cron/manual),
    состояние, цикл, сумма; deep-link «открыть проект на столе списания»
  - extensions/market/app/extensions.ts — регистрация по `LEDGER2_PROCESS_REGISTRY.name`
    (SUPPLY/RETURN/WRITEOFF), без string-литералов в коде
  - extensions/reports/pages/OperationsPage — слот «Содержание процесса» в развороте

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-12][@ant] style: страницы Эпика 9 на <template lang="pug"> + русские лейблы — снять техжаргон из UI

Привожу страницы к канону стола заказов: PUG-шаблоны, бизнес-словарь
в подписях/фильтрах (Состояние / Кооперативный участок / Заказчик),
никаких `p.mkt.*` или raw-имён enum-значений в видимой части. Внутри —
строгая типизация через `Zeus.MarketplaceInventoryStatus`, switch на
status-полях больше не сравнивается со строками.

  - OperatorOwnWarehouse (Story 9.1) — PUG, enum-fix, человеческие лейблы
  - AdminWarehouseSummary (Story 9.2) — PUG, enum-fix, человеческие лейблы
  - EcosystemRegistry (Story 9.4) — PUG

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-12][@ant] refactor: вынести запрос процесса в entities/Process + чистая типизация в info-widget'ах

По канону Ledger2: api/store/types. Widget'ы больше не вызывают client.Query
и не делают cast'ов вида `as Record<string, unknown>` — снэпшот приходит
строго типизированным `IProcessSnapshot` из SDK через `useProcessStore`.

  - entities/Process/types — IProcessView / IProcessSnapshot / IProcessDelta
    выведены из Queries.Processes.GetProcess.IOutput
  - entities/Process/api — getProcess + pickLatestSnapshot (единственная
    точка приведения JSON-скаляра)
  - entities/Process/model — Pinia store с loadProcess / loadLatestSnapshot
  - widgets/Marketplace/Process{Supply,Return,Writeoff}InfoWidget — используют
    processStore, типы из entities/Process

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-12][@ant] style: убрать пользовательский техжаргон со сводного склада и экосистемы

  - AdminWarehouseSummary: удалить сноску про SDK-подписки и формулу остатка —
    пользователю кооператива это не нужно
  - EcosystemRegistry: оставить короткую фразу «включится позже», без режима
    просмотра и упоминаний платформенного реестра

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-12][@ant] docs: зафиксировать паттерн фабрик инжекции в стол совета и стол бухгалтера

Новый документ `components/desktop/docs/extension-factories.md`:
- общий принцип «общие столы единственная точка просмотра, расширения
  инжектируют бизнес-описание»
- DecisionFactory: контракт, пример регистрации (capital → стол совета)
- ProcessInfoFactory: контракт, пример регистрации (market → стол
  бухгалтера) с использованием LEDGER2_PROCESS_REGISTRY вместо
  строковых литералов
- чек-лист для нового расширения (виджет/PUG → app/extensions.ts →
  install.ts → реестр в cooptypes)
- что делать НЕЛЬЗЯ: повторять у себя реестры операций/проводок/повестки
  и пихать техжаргон в infoComponent

В README desktop — ссылка из раздела «Система расширений».

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: ant <ant@noreply.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 22:09:52 +05:00
Alex Ant 80ebdb4a73 Эпик 8 «Списание скоропорта по решению совета» MVP Стола заказов (598-11) (#399)
* [598-11][@ant] feat: Эпик 8 «Списание скоропорта по решению совета» — перевод процесса на канонический паттерн soviet::createagenda с двумя документами

ОСНОВНОЕ ИЗМЕНЕНИЕ ОТ ПРЕДЫДУЩЕЙ ИТЕРАЦИИ:
- Предыдущая реализация Эпика 8 (PR #399 — reverted) шла «мимо совета»: backend
  напрямую дергал propwroff/execwroff/declwroff, в UI председатель «исполнял»
  и «отклонял» сам. Это не совпадало с каноническим паттерном принятия решений
  совета (приём в пайщики 100→501, паевой взнос имуществом 700→701, свободное
  решение 600, ResultContribution 1040→1041 — и т.д.).
- Текущая ветка переделана с нуля под канон: документ-Предложение (1106)
  подписывается председателем → soviet::createagenda(type=mktwroff) ставит
  повестку → совет голосует и авторизует Протокол (1105) → callback от soviet
  (onmktwoauth / onmktwodecl) фиксирует решение на цепи → backend per-item
  проводит execwroff.

C++ КОНТРАКТЫ:
- contracts/cpp/marketplace: добавлены actions onmktwoauth(coopname, hash,
  authorization) и onmktwodecl(coopname, hash, reason) с require_auth(_soviet).
  Сигнатуры совпадают с AUTHORIZE_CALLBACK_SIGNATURE и DECLINE_CALLBACK_SIGNATURE
  в soviet.hpp.
- contracts/cpp/marketplace: добавлен статус AUTHORIZED в WroffStatus; execwroff
  упрощён — больше не принимает протокол (он лежит в proposal.protocol после
  callback'а onmktwoauth), проверяет статус == AUTHORIZED.
- contracts/cpp/marketplace: удалён declwroff (его роль исполняет
  soviet::cancelexprd → callback onmktwodecl).
- contracts/cpp/lib: добавлен type "mktwroff"_n в consts; dispatch в
  soviet::exec.cpp идёт по default-branch на authorize_action_effect.
- p.mkt.wroff.standard.yaml: states/transitions/documents переведены под
  новый граф.

COOPTYPES + FACTORY:
- registry 1106.MarketplaceWriteoffStatement (Заявление председателя о списании)
  — подписывается перед propwroff + createagenda.
- registry 1105.MarketplaceWriteoffProtocol (Протокол совета о списании) —
  подписывается chairman'ом в стандартном sov.decision flow, ложится в
  wroffprops.protocol через callback onmktwoauth.
- factory/Actions + Templates для обоих registry; registry.ts обновлён.
- contracts/marketplace/actions: declWroff удалён, добавлены onMktWoAuth и
  onMktWoDecl; IExecWroff в interfaces/marketplace.ts больше не несёт protocol.

CONTROLLER:
- domain/infrastructure MarketplaceWriteoffProposal: статусы DRAFT → ON_AGENDA
  → AUTHORIZED → EXECUTING → EXECUTED | REJECTED; trigger cron/manual;
  decision_id для связки с soviet.decisions; items с inventory_id для
  крон-пометки записей WRITTEN_OFF после executed.
- partial-unique индексы запрещают параллельные DRAFT/ON_AGENDA per coopname.
- application/services/marketplace-writeoff.service: createDraft / updateDraft /
  cancelDraft / submitToCouncil (propwroff + createagenda) / onCouncilAuthorized
  (callback → AUTHORIZED → executeAuthorizedProposal) / executeAuthorizedProposal
  (цикл execwroff per-item) / onCouncilDeclined.
- detected proposal_hash = sha256('writeoff|coopname|cycle_started_at|draft_id|items').
- application/services/marketplace-writeoff-cron: ежемесячный сканер
  inventory.expiry_date; если config.writeoff.auto_proposal_enabled — формирует
  DRAFT-предложение по позициям LABELED с приближающимся сроком (грейс
  заданный конфигом), иначе только reminder председателю.
- infrastructure/inventory: новая колонка expiry_date (TypeORM synchronize),
  partial-индекс под крон-сканер; проставление при labeling
  (`labeled_at + Offer.warranty_days * 86400`).
- types.ts: IConfig.writeoff{auto_proposal_enabled, expiry_grace_days}.
- migrations/marketplace-bootstrap-v6: bump schema_version + merge writeoff
  defaults.
- domain/ports/marketplace-canonical-blockchain: propWroff / execWroff /
  createWriteoffAgenda. Adapter использует BlockchainService + VaultDomainService.
- application/dto/marketplace-writeoff.dto: enum'ы (StatusEnum / TriggerEnum)
  через registerEnumType; PaginatedMarketplaceWriteoffProposals на
  createPaginationResult; PaginationInputDTO в resolver.
- application/resolvers/marketplace-writeoff.resolver: marketplaceOpenWriteoffDraft,
  marketplaceListWriteoffProposals, marketplaceWriteoffProposal,
  marketplaceCreateWriteoffDraft (admin/manage_draft),
  marketplaceUpdateWriteoffDraft, marketplaceCancelWriteoffDraft,
  marketplaceWriteoffStatementSignablePayload (admin/propose, генерирует 1106),
  marketplaceSubmitWriteoffDraft (admin/propose, подписанное 1106 → propwroff +
  createagenda). marketplaceExecuteWriteoffProposal / marketplaceDeclineWriteoffProposal
  больше нет — это теперь делает совет.
- access-matrix: admin.Writeoff = ['manage_draft','propose','read:all'],
  board.Writeoff = ['decide','read:all'], board_readonly.Writeoff = ['read:all'].

SDK (@coopenomics/sdk):
- selectors/marketplace/writeoffSelector (item, decision_log, proposal,
  paginated) с MakeAllFieldsRequired-валидаторами.
- queries.marketplace: openWriteoffDraft, listWriteoffProposals,
  getWriteoffProposal, writeoffStatementSignablePayload.
- mutations.marketplace: createWriteoffDraft, updateWriteoffDraft,
  cancelWriteoffDraft, submitWriteoffDraft.

NOTIFICATIONS:
- 5 Novu workflows: marketplace-writeoff-draft-built (адресат:
  председатель/админ), -proposed (адресат: совет), -authorized, -executed,
  -rejected (адресат: админ/председатель). Liquid-синтаксис.
- Эмиты событий marketplace.writeoff.* добавлены в
  marketplace-notification.events.ts; listener-bridge wiring оставлен Phase 2
  (отдельный коммит, паттерн 598-20).

DESKTOP:
- pages/Marketplace/AdminWriteoffs/api: типизированные обёртки над
  @coopenomics/sdk.
- AdminWriteoffsPage.vue: open-draft + лента «в работе совета» +
  архив (EXECUTED/REJECTED).
- DraftEditorDialog: добавление/удаление/правка позиций с live-total.
- SubmitToCouncilDialog: превью Заявления 1106 → подпись DigitalDocument →
  submitWriteoffDraft (запускает propwroff + createagenda).
- WriteoffProposalDetailsDialog: детали, items.executed, decision_log.
- extensions/market/install.ts: маршрут /<coopname>/market/writeoffs для
  ролей chairman+member; mp-role-admin / --mp-font-body токены по канону.

ИЗВЕСТНЫЕ ОГРАНИЧЕНИЯ / PHASE 2:
- Listener-bridge `MarketplaceNotificationService` для каналов
  marketplace.writeoff.* остаётся на отдельный коммит (паттерн 598-20).
- Sync writeoff_proposals из chain delta (двусторонняя сверка decision_id ↔
  soviet.decisions) — Phase 2, если будет нужен reconciler.
- Крон в auto-proposal-mode проставляет amount=0 placeholder (нет
  attached unit-cost при labeling) — председатель руками поправит при ревью.
- Переуступка по сниженной цене — out of MVP.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-11][@ant] fix: ревью PR #399 — asset из config, стандарт на бизнес-языке, spec и docs-фабрика

- ASSET_DECIMALS/SYMBOL в writeoff.service / writeoff-cron.service берутся из
  MARKETPLACE_ASSET_CONFIG (DI), не хардкод (как в order-create.service)
- p.mkt.wroff.standard.yaml переписан без техжаргона (callback/soviet::exec/
  AUTHORIZE_CALLBACK_SIGNATURE убраны, эталон p.mkt.return)
- Заявление 1106 и Протокол 1105 включены в documents-1000-plus тест фабрики
  с моком getDecision для 1105
- spec для MarketplaceWriteoffService: createDraft/updateDraft/cancelDraft,
  computeProposalHash детерминистичность, submitToCouncil happy + 4 reject-кейса,
  onCouncilAuthorized/Declined, executeAuthorizedProposal per-item
- coopname в writeoff.resolver — из config.coopname (IMarketplaceCurrentMember
  не несёт coopname); статичный сигнатурный долг toDocument убран
- CreateAgenda путь — SovietContract.Actions.Decisions.CreateAgenda

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: ant <ant@noreply.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 20:10:32 +05:00
Alex Ant f0311afaa7 Эпик 7 «Гарантийный возврат имущества» MVP Стола заказов (598-10) (#397)
* [598-10][@ant] feat: backend Эпика 7 «Гарантийный возврат» (Stories 7.1-7.4) — завести MarketplaceReturnClaimService с 5 mutations (submretrn/aprretrem/rejretrem/accretrn/rejretrn) и хранилище фото в bucket stol-zakazov:images

— Domain: ReturnClaim entity + types (5 статусов pendrev/approvvisit/accepted/rejremote/rejatku с маппингом на ReturnStatus::* контракта), decision_log append-only журнал решений председателя, on_site_inspection-снапшот очного осмотра, ledger_snapshot снапшот compensating-forward пары после accretrn. Photo type хранит bucket_key + sha256 content_hash + mime_type — хеш используется как анкер on-chain (параметр photos[] submretrn).
— Repository: create/findById/findByRequestHash/findActiveByOrderId (idempotency check) /listByOrderer/listByDeliveryBraname/applyDecision (append к decision_log + опциональная фиксация on_site_inspection / ledger_snapshot). Partial-unique индекс (coopname, order_id) ограниченный активными статусами — пайщик не может одновременно открыть две заявки на один заказ.
— Canonical blockchain port + adapter: 5 новых методов submRetrn/aprRetRem/rejRetRem/accRetrn/rejRetrn — обёртки над marketplace::submretrn/aprretrem/rejretrem/accretrn/rejretrn (контракты уже реализованы в cpp/marketplace/src/p.mkt.return/). accretrn — композитная транзакция через транзит 91: o.mkt.return + o.mkt.return2 (compensating forward к o.mkt.consum, AR9/AR14 L3).
— MarketplaceReturnClaimService: submitReturnClaim (Story 7.1: валидация warranty_until>now, reason_text 1-500, photos 1-10, request_hash детерминирован от order_hash+orderer+actual_quantity, фото загружаются в bucket до on-chain submit, on-chain photos[] = sha256-хеши), approveReturnVisit/rejectReturnRemote (Story 7.2 FR31: PENDING_CHAIRMAN_REVIEW → APPROVED_FOR_VISIT/REJECTED_REMOTELY, comment обязателен), acceptReturnAtVisit/rejectReturnAtVisit (Story 7.3-7.4 FR32-FR33: APPROVED_FOR_VISIT → ACCEPTED_AT_VISIT/REJECTED_AT_VISIT, при accept атомарно compensating forward o.mkt.return+o.mkt.return2 восстанавливает w.mkt.member.available заказчика и возвращает имущество на склад КУ, barcode-сверка с Order). getReturnClaimSignablePayload — preview через factory registry_id=800 (ReturnByAssetStatement, переиспользован для членской модели по стандарту p.mkt.return.standard.yaml).
— MarketplaceReturnClaimImagesService: @UseBucket('stol-zakazov:images') — конвенция «один сервис на один bucket» из README file-storage. maxBytes 10MB, allowedMime jpeg/png/webp, metadataSchema требует ownerAccount+orderId+claimId. Ключ объекта детерминирован: returns/<claim_id>/<role>/<index>.<ext>; backend хеширует bytes (sha256-hex) — этот же хеш идёт on-chain в submretrn.photos[].
— Resolver + DTO: 5 mutations + 3 queries (marketplaceListMyReturnClaims, marketplaceListReturnClaimsByBraname, marketplaceReturnClaimSignablePayload, marketplaceReturnClaim) + Photo/Decision/Inspection/Ledger ObjectType. GraphQL enum через registerEnumType (MarketplaceReturnClaimStatus / MarketplaceReturnClaimDefectCategory / MarketplaceReturnClaimExpectedResolution). Подписанное заявление приходит как SignedDigitalDocumentInputDTO meta=MarketplaceReturnClaimSignedStatementInputDTO; toDocument() конвертит в Cooperative.Registry.ReturnByAssetStatement.Action для submretrn.statement.
— Signed-document DTO: переиспользуем шаблон 800 «Заявление на возврат паевого взноса имуществом» (по стандарту p.mkt.return — структурно совместим с членской моделью; поля request.hash/title/units/unit_cost/total_cost заполняются из Order). request.hash = backend-computed request_hash → двусторонняя сверка backend ↔ on-chain.
— Access-matrix: orderer ['create:own', 'read:own'] на ReturnClaim, operator ['read:own-KU', 'decide:remote', 'decide:on-site']. resolver-уровень проверка ownership (orderer_account == member.username, braname == председатель чьего КУ — отложено до core coop_ku.chairman_account, в MVP передаётся параметром).
— Регистрация в marketplace-infrastructure.module.ts (TypeORM entity + repository) и marketplace-application.module.ts (FileStorageInfrastructureModule.forFeature + service + resolver).

Why: реализация Эпика 7 «Гарантийный возврат имущества» MVP «Стол Заказов» — пайщик подаёт заявление с фото-доказательством, председатель КУ удалённо рассматривает и при необходимости приглашает на очный осмотр, при принятии возврата атомарно через транзит 91 восстанавливаются средства на программном кошельке заказчика и имущество возвращается на склад участка (compensating forward — отдельное событие в журнале, не revert исходного o.mkt.consum). Возврат поставщику и работа с поставщиком по претензиям — Phase 2 (out of MVP).

* [598-10][@ant] feat: notifications Эпика 7 — 3 Novu workflow и слушатели per-contract event-bus (submitted/decided/finalized) для гарантийного возврата

— @coopenomics/notifications: 3 workflow: marketplace-return-claim-submitted (председателю КУ на новое заявление; email + in-app + push с deep-link на operator-стол), marketplace-return-claim-decided (заказчику на промежуточные решения председателя — invite на очный визит), marketplace-return-claim-finalized (заказчику на финальный исход с восстановленной суммой если accept_at_visit).
— controller: 3 события в marketplace-notification.events.ts — MARKETPLACE_RETURN_CLAIM_SUBMITTED_EVENT/DECIDED/FINALIZED + типы payload. Сервис эмитит их после save в PG (INV-12).
— marketplace-notification.service.ts: 3 listener (handleReturnClaimSubmitted/Decided/Finalized) тянут subscriber_id/email из AccountDataPort, формируют payload с display-name и deep-link через config.frontend_url, шлют через NovuWorkflowPort. Ошибки доставки logged как warn — основной flow возврата не блокируется.

Why: уведомления (push/email/in-app) — обязательный канал коммуникации по UX-DR38; заявление на гарантийный возврат должно дойти до председателя КУ без поляризации UI (председатель не всегда сидит в operator-столе), решение председателя — до заказчика без необходимости вручную проверять статус заявления.

* [598-10][@ant] feat: SDK Эпика 7 — Zeus selectors, 5 mutations и 3 queries для гарантийного возврата + перегенерированная schema.gql

— Selectors: marketplaceReturnClaimSelector (полный набор полей заявления вместе с photos / decision_log / on_site_inspection / ledger_snapshot), marketplaceReturnClaimResultSelector (claim + tx_hash) и поднаборы (photo / decision-entry / inspection / ledger). Валидаторы MakeAllFieldsRequired гарантируют полное покрытие type.
— Mutations: CreateReturnClaim / ApproveReturnVisit / RejectReturnRemote / AcceptReturnAtVisit / RejectReturnAtVisit. Каждая принимает соответствующий Input DTO и возвращает marketplaceReturnClaimResultSelector.
— Queries: ListMyReturnClaims (без args), ListReturnClaimsByBraname (data.delivery_braname), ReturnClaimSignablePayload (data.order_id + actual_quantity? — preview registry_id=800 для подписи пайщиком).
— schema.gql пересчитана; zeus/const.ts + zeus/index.ts перегенерированы. Все ID-поля заявления через @Field(() => String) (Zeus резолвит scalar ID как unknown без глобального scalar-resolver — поэтому Order и ReturnClaim единообразно используют String).

Why: типобезопасный SDK-слой между backend GraphQL и desktop UI; правило «никаких raw GraphQL-строк в desktop» (feedback_graphql_no_raw_strings_desktop) требует прохода через Zeus client.

* [598-10][@ant] feat: desktop UI Эпика 7 — orderer и operator страницы возврата на канон-widgets (TakeoverDialog + BarcodeScanner)

— OrdererReturnClaims (страница /:coopname/market/returns): список заявлений пайщика разделён на «активные» (PENDING_CHAIRMAN_REVIEW + APPROVED_FOR_VISIT) и «архив» (финальные статусы). Подача нового заявления через SubmitReturnClaimDialog — full-screen TakeoverDialog с q-stepper 3 шага: описание (reason_text + defect_category + actual_quantity?) → фото (q-file 1-10 шт jpeg/png/webp до 10МБ → base64) → подпись заявления (preview registry_id=800 + Classes.Document.signDocument приватным ключом из useGlobalStore). Просмотр через ReturnClaimDetailsDialog — фото-thumbnails со ссылками на signed-URL bucket'а, q-timeline decision_log председателя, снапшот compensating-forward при ACCEPTED_AT_VISIT.
— OperatorReturnClaims (страница /:coopname/market-pvz/returns): три ленты — pending (PENDING_CHAIRMAN_REVIEW) с inline-thumbnails фото и кнопкой «Принять решение» → RemoteDecisionDialog, approved (APPROVED_FOR_VISIT) с кнопкой «Очный осмотр» → OnSiteDecisionDialog, archive. RemoteDecisionDialog — q-option-group approve/reject + обязательный комментарий 1-500 симв. OnSiteDecisionDialog — q-stepper: 1) BarcodeScanner сканирует штрих-код имущества для сверки с Order; 2) inspection_result 1-2000 симв; 3) опциональные inspection_photos до 10 шт; 4) accept (compensating forward предупреждение) либо reject (имущество остаётся у заказчика).
— API: типобезопасные обёртки в pages/<page>/api/index.ts через destructuring результата `{ [Queries.X.name]: result } = await client.Query(...)`. Типы выведены из Queries/Mutations.X.IOutput без cast'ов. Фото-загрузка через FileReader→base64 в browser, backend кладёт в bucket stol-zakazov:images.
— DateTime utility: `formatDateTime(value: unknown)` для GraphQL DateTime scalar — Zeus резолвит как unknown без глобального scalar-resolver, простой helper парсит и форматирует через toLocaleString('ru-RU').
— install.ts: 2 новых маршрута зарегистрированы — workspace 'market' (orderer-стол) → '/:coopname/market/returns' (roles []); workspace 'market-pvz' (operator-стол) → '/:coopname/market-pvz/returns' (roles ['chairman']). Иконки fa-rotate-left / fa-clipboard-check; agreements: agreementsBase.

Why: продолжение поведенческой части Эпика 7 — пайщик и председатель кооперативного участка получают функциональные UI для подачи и рассмотрения заявлений на гарантийный возврат, со всеми атрибутами (фото, подпись заявления, барскод-сверка, compensating forward) согласно FR29-FR33. Соблюдён канон дизайн-системы Эпика 10: используются только widgets/Marketplace/* (TakeoverDialog + BarcodeScanner), inline-вёрстка кастомных диалогов запрещена; токены через marketplace-tokens.scss (var(--mp-space-md), mp-role-* классы на корнях страниц).

* [598-10][@ant] feat: добавить registry_id=1104 MarketplaceReturnStatement в cooptypes — отделить документ Эпика 7 «Стол заказов» от системы клиринга

Marketplace по системе членских взносов ведёт свой контур документов рядом
с 1102 «Акт приёма-передачи» и 1103 «ТТН». До этого Эпик 7 ошибочно
переиспользовал registry_id=800 ReturnByAssetStatement из старой системы
клиринга «по структурному совпадению» — это смешало два процесса в одном
signed-document пайплайне. Завожу полноценный собственный документ:
заявление пайщика о гарантийном возврате имущества с marketplace-полями
(order_id / order_hash / fact_cost / actual_quantity / reason_text /
defect_category / braname). Шаблон 800 остаётся как есть на случай
будущего клиринга.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* [598-10][@ant] feat: проложить factory-цепочку для registry_id=1104 — Action + Template + регистрация в Registry/index

Action 1104.MarketplaceReturnStatement.Factory повторяет паттерн 1102
(getCooperative/getVars/getUser/getRequest/getProgram/getOrganization),
но кладёт в Model marketplace-поля (fact_cost / actual_quantity /
reason_text / defect_category) и не требует transmitter/decision —
заявление подписывает только пайщик.

Также: убираю tsconfig.tsbuildinfo из индекса (build-артефакт, не должен
жить в git).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* [598-10][@ant] feat: переключить controller + SDK на registry_id=1104 для Эпика 7 — заявление о гарантийном возврате имущества

- marketplace-return-statement-document.dto.ts — новый Signed-DTO под
  Cooperative.Registry.MarketplaceReturnStatement.Action; старый
  marketplace-return-claim-document.dto.ts (ссылавшийся на 800
  ReturnByAssetStatement) удалён.
- MarketplaceReturnClaimService.generateStatementDocument теперь строит
  Action.MarketplaceReturnStatement с marketplace-полями (order_id /
  order_hash / fact_cost / reason_text / defect_category / braname),
  а не reuse'ит request{hash,units,unit_cost,total_cost} из старого 800.
- getReturnClaimSignablePayload принимает reason_text — превью теперь
  показывает реальный текст обращения пайщика.
- Регенерированы schema.gql + zeus client + SDK zeus (вход
  MarketplaceReturnStatementSignedInput с marketplace-меткой meta).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* [598-10][@ant] feat: desktop SubmitReturnClaimDialog — превью под registry_id=1104 с пробросом reason_text

- API getReturnClaimSignablePayload теперь принимает reason_text и
  defect_category — превью показывает реальный текст обращения пайщика.
- Убрал `result as MarketplaceGeneratedDocumentView` cast (Zeus уже
  выдаёт нужный тип после регенерации schema/SDK).
- Комментарии и подсказки UI обновлены: 1104 MarketplaceReturnStatement
  вместо устаревшего 800 ReturnByAssetStatement.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* [598-10][@ant] refactor: десктоп-API Эпика 7 на строгие типы из SDK — variables передаём целиком data

Канон @coopenomics/sdk: каждый api-метод принимает `data: Mutations.<X>.<Y>.IInput['data']`
(или `Queries.<X>.<Y>.IInput['data']`) и передаёт его в variables объектом
целиком — `{ variables: { data } }`. Без раскладки полей вручную и без
самодельных интерфейсов аргументов.

Затронуто:
- OrdererReturnClaims/api: getReturnClaimSignablePayload + createReturnClaim
- OperatorReturnClaims/api: listReturnClaimsByBraname, approveReturnVisit,
  rejectReturnRemote, acceptReturnAtVisit, rejectReturnAtVisit
- SubmitReturnClaimDialog: defectCategory типизирован как enum
  Zeus.MarketplaceReturnClaimDefectCategory (раньше падал на string→enum,
  но был замаскирован ручной раскладкой полей в variables)
- OnSiteDecisionDialog: ReturnClaimPhotoUploadInput выведен из IAcceptReturnAtVisitInput
- OperatorReturnClaimsPage: listReturnClaimsByBraname({ delivery_braname })

Зачем — ревью PR #397: при ручной раскладке полей в variables.data
изменение схемы в SDK не подсвечивает call-site как ошибку TS;
регрессии маскируются. Канон даёт строгую сверку при изменении бэкенда.

---------

Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 15:32:47 +05:00
Alex Ant 6250abd56b Эпик 6 «Выдача пайщику с двойной подписью и корректировкой» MVP Стола заказов (598-9) (#390)
Publish Docs / build-and-publish-docs (push) Failing after 12m39s
* [598-9][@ant] feat: backend Эпика 6 «Выдача пайщику с двойной подписью» (Stories 6.1-6.4) — расширить Order полями выдачи, завести MarketplaceIssuanceService с двумя mutations (signiss1 + signiss2) и push «заказ готов к получению»

— Order entity + typeorm + mapper: добавлены поля выдачи (current_warehouse_braname, issuance_fact, chairman_signed_at/chairman_account/signiss1_tx_hash, orderer_signed_at/delivery_signer_account/signiss2_tx_hash), derived-геттеры awaits_chairman_issue_open/awaits_orderer_issue_final/is_received. На Order живут metadata подписей; жирные Document2 хранятся в on-chain row marketplace::orders.
— Repository: applyIssuanceOpened (статус ACCEPTED_TO_COOP → READY_TO_RECEIVE, проставление chairman_signed_at + signiss1_tx_hash + current_warehouse_braname = delivery_braname), applyIssuanceFinalized (READY_TO_RECEIVE → RECEIVED + issuance_fact + warranty_until), listForIssuanceByBraname (operator), listReadyToReceiveByOrderer (orderer).
— Chain port + adapter: signIss1, signIss2 — обёртки над marketplace::signiss1/signiss2 (контракт уже реализован в cpp/marketplace/src/p.mkt.supply/).
— MarketplaceIssuanceService: openIssuance (председатель КУ открывает выдачу), finalizeIssuance (заказчик закрывает финальной подписью; actual_quantity влияет на корректирующие операции в C++ signiss2 — три ветки FR23), getOpenIssuanceSignablePayload / getFinalizeIssuanceSignablePayload (preview документов через factory registry_id=1102).
— GraphQL DTO + resolver: MarketplaceIssueActSignedDocumentInputDTO, MarketplaceOpenIssuanceInputDTO, MarketplaceFinalizeIssuanceInputDTO, MarketplaceIssuanceResultDTO; mutations marketplaceOpenIssuance / marketplaceFinalizeIssuance; queries marketplaceListIssuancesByBraname / marketplaceListMyReadyToReceive + preview payloads.
— Access matrix: operator получает Issuance:{create,sign:first,read:own-KU}; orderer получает Issuance:{sign:final,read:own}.
— Push notification (Story 6.4 / FR22): новый workflow @coopenomics/notifications/marketplace-order-ready (email + in-app + push) + listener в MarketplaceNotificationService, эмит после applyIssuanceOpened (INV-12 — после save в PG).
— MarketplaceOrderDTO расширен публичными полями выдачи (issuance_fact с enum diff_state, chairman/orderer signatures metadata) — UI карточки заказа в RECEIVED видит фактическое количество и diff_state.

Issue: 598-9 / Эпик 6 MVP Стола Заказов

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-9][@ant] feat: SDK Эпика 6 — обёртки openIssuance/finalizeIssuance + queries listing/preview, обновлены схема и zeus-клиент (Story 6.5)

— schema.gql + zeus regen после расширения marketplace-process-controller: MarketplaceOpenIssuance/FinalizeIssuance/IssuanceResult/IssueActPayload + 4 query/mutation для столa выдачи.
— Selectors: orderSelector расширен полями выдачи (current_warehouse_braname, issuance_fact с diff_state, chairman/orderer signatures metadata) + новый marketplaceOrderIssuanceFactSnapshotSelector + marketplaceIssuanceResultSelector — UI карточки заказа в RECEIVED увидит фактическое количество.
— Mutations: openIssuance.ts, finalizeIssuance.ts — обе через единый marketplaceIssuanceResultSelector.
— Queries: listIssuancesByBraname.ts (operator-стол), listMyReadyToReceive.ts (orderer-стол), issueActChairmanSignablePayload.ts / issueActOrdererSignablePayload.ts (preview документа для подписи).
— Side-fix техдолгов рассинхронизации cooptypes dist ↔ controller, мешавших generate-schema:
  * marketplace-apl-reception.service.ts — добавлено обязательное reception_id в Action 1102.
  * marketplace-shipment-create.service.ts — Action 1103.MarketplaceTransportNote/PrivateData приведены к any (поля экспедитора уже off-chain в privatePayload через doc_data_hash).
  * generator.service.ts — saveDocData/getDocData проброшены через cast (методы из @coopenomics/factory есть в runtime, в типах ещё не публиковались).

Issue: 598-9 / Эпик 6 MVP Стола Заказов

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-9][@ant] feat: каркас столов выдачи в desktop — OperatorIssuance + OrdererReadyToReceive (Stories 6.6-6.7)

Минимальные каркасные страницы по образцу OperatorReception для Эпика 6 — лента заказов на ПВЗ + точки входа в подписи. Полный flow подписи (BarcodeScanner со штрих-кодом заказа, CorrectionTable для сверки факт vs заказ, TakeoverDialog для двух подписей) — следующий UI PR; backend уже принимает signed_document и исполняет три ветки сверки в C++ signiss2 атомарно.

— OperatorIssuance: список заказов на КУ выдачи (ACCEPTED_TO_COOP + READY_TO_RECEIVE) + точки входа «Открыть выдачу» (signiss1) / «Завершить выдачу» (signiss2). API через @coopenomics/sdk — listIssuancesByBraname / openIssuance / finalizeIssuance + getChairman/OrdererSignablePayload.
— OrdererReadyToReceive: лента заказов пайщика в READY_TO_RECEIVE — визуальное продолжение push-уведомления marketplace-order-ready (FR22). API — listMyReadyToReceive через @coopenomics/sdk.
— Никаких raw GraphQL-строк — только типизированные обёртки @coopenomics/sdk, как требует components/desktop/extensions/market/CLAUDE.md.

Issue: 598-9 / Эпик 6 MVP Стола Заказов

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-9][@ant] feat: полный UI flow подписи Эпика 6 через канон-widgets + регистрация в роутинге

Закрываем техдолг Эпика 6, оставленный из предыдущего коммита. Оба диалога — это реальная подпись приватным ключом через @coopenomics/sdk Classes.Document, не моки.

— IssueActOpenDialog (Story 6.1 / FR21): full-screen TakeoverDialog с предварительным актом выдачи (registry_id=1102). Председатель кооперативного участка подписывает первой подписью приватным ключом из useGlobalStore (signatureId=1), backend верифицирует и отправляет on-chain signiss1; статус заказа READY_TO_RECEIVE + push заказчику.
— IssueActFinalizeDialog (Story 6.3 / FR23-FR25): full-screen takeover с тремя шагами через q-stepper:
  * **Шаг 1 — штрих-код.** BarcodeScanner (DR11) сверяет штрих-код заказа.
  * **Шаг 2 — сверка факта.** CorrectionTable (DR13) показывает «план vs факт», оператор корректирует actual_quantity. UI вычисляет fact_cost и diff_state (equal/less/more) и подсказывает, какие корректирующие операции исполнит контракт (o.mkt.unblk при less, o.wal.conv + o.mkt.assign + o.mkt.block при more).
  * **Шаг 3 — двойная подпись.** Заказчик вводит свой WIF (signatureId=1, signer = orderer_account), оператор подписывает вторым ключом из сессии (signatureId=2, delivery_signer = current member). Backend верифицирует обе подписи и отправляет on-chain signiss2 — C++ исполняет три ветки сверки в одной композитной транзакции; при недостатке средств на доплату (more) транзакция падает с человеческим сообщением (L6 guard / FR25), backend пробрасывает его клиенту.
— OperatorIssuancePage переключён на эти диалоги — теперь кнопки «Открыть выдачу» и «Завершить выдачу» делают реальный сценарий, а не NotifyAlert-заглушку.

Регистрация в extensions/market/install.ts:
— `/<coopname>/market-pvz/issuance` — operator-стол выдачи (roles: chairman, иконка handshake).
— `/<coopname>/market/ready-to-receive` — orderer лента «Готово к получению» (roles: все, иконка box-check).

Issue: 598-9 / Эпик 6 MVP Стола Заказов

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-9][@ant] refactor: убрать any/unknown по ревью — пересобрать cooptypes/factory + типизировать desktop api через Queries.X.IOutput и Types.Document

Реакция на ревью PR #390: ревьюер требует строгую типизацию, any/unknown запрещены вне очень редких случаев.

Корневая причина прошлых хаков — рассинхронизация cooptypes/factory dist ↔ source в monorepo. Source 1102 уже без reception_id и 1103 уже с PrivateData, но dist в node_modules был собран из старой версии. Решение: пересборка cooptypes и factory из текущего source — dist приходит в соответствие с типами, controller компилируется без cast'ов.

— marketplace-shipment-create.service.ts: `const action: any` → `Cooperative.Registry.MarketplaceTransportNote.Action`, `privatePayload: any` → `PrivateData`. Все expeditor-поля живут off-chain в privatePayload, как и задумано в архитектуре 1103.
— marketplace-apl-reception.service.ts: убран лишний `reception_id` (его нет в свежей dist Action 1102).
— marketplace-issue-act-document.dto.ts: убрано поле `reception_id` из подписной формы АПП-выдачи — тоже не требуется.
— generator.service.ts: убран `(this.generator as any).saveDocData/getDocData` — методы есть в новой factory dist.
— catch (err: any) → catch (err) с `err instanceof Error ? err.message : String(err)` в marketplace-issuance.service.ts.

Desktop:
— OperatorIssuance/api/index.ts полностью переписан: вместо локальных View-интерфейсов и `as unknown as` — производные типы от SDK: `Queries.Marketplace.ListMyReadyToReceive.IOutput['marketplaceListMyReadyToReceive'][number]` для Order'а в контексте выдачи, `Types.Document.IGeneratedDocument` для preview-документа, `Types.Document.ISignedDocumentInput` для signed_document, `Mutations.Marketplace.OpenIssuance.IOutput['marketplaceOpenIssuance']` для результата. Все client.Query / client.Mutation возвращают строго типизированный `result` через destructuring `const { [Mutations.X.name]: result }` — паттерн из features/User/LoginUser/api/index.ts.
— OrdererReadyToReceive/api/index.ts: то же — без `as unknown as`.
— IssueActOpenDialog/FinalizeDialog: убраны все `generated as unknown as Parameters<...>` и `signed as unknown as SignedDocumentInput` — `docSigner.signDocument(generated, ...)` принимает результат `getXxxSignablePayload` напрямую через `Types.Document.IGeneratedDocument`. catch (err: any) → catch (err) с проверкой `instanceof Error`.

Issue: 598-9 / Эпик 6 MVP Стола Заказов

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 11:57:46 +05:00
Alex Ant 9ac3d8ea33 Эпик 5 «Поставка и приёмка имущества на КУ» MVP Стола заказов (598-8) (#388)
* [598-8][@ant] feat: добавить формирование партий поставки из акцептованной заявки с жёстким акцептом состава — закрыть Stories 5.1 и 5.2 Эпика 5

Backend (controller/extensions/marketplace) — старт Эпика 5 «Поставка и
приёмка имущества на КУ». Story 5.1 — pre-shipment группировка Order'ов
консолидированной заявки по КУ-получателю с выбором варианта доставки.
Story 5.2 — жёсткий акцепт состава поставки backend'ом перед формированием
партий.

Domain (новое):
- MarketplaceShipmentDomainEntity + types (delivery_variant A/B, status
  DRAFT/SUPPLY_PREPARED/RECEPTION_IN_PROGRESS/ACCEPTED_TO_COOP/CANCELLED,
  TTNData с полями экспедитора, ТС, погрузки, доставки). Backend-only PG
  без on-chain зеркала: фиксация поставки идёт через АПП-приёмки и
  signsupp/signchair (Stories 5.3/5.4/5.6).
- MarketplaceSupplyValidationLogDomainEntity + типы Outcome/Reason —
  append-only audit-журнал валидаций состава.

Infrastructure (новое):
- MarketplaceShipmentEntity TypeORM с unique-индексом (coopname,
  cycle_id, ku_id) + partial unique-индексом по ttn_number + hot-path
  индексами по offerer/ku/status.
- MarketplaceSupplyValidationLogEntity TypeORM.
- Адаптеры репозиториев + мапперы Row→Domain под обе сущности.

Application (новое):
- MarketplaceShipmentCreateService — guard ACCEPTED + ownership заявки,
  жёсткая валидация (Story 5.2): покрытие всех КУ заявки группами,
  непересечение групп, статус Order'ов ACCEPTED. Атомарное создание
  Shipment'ов per группа + перевод Order'ов в SUPPLY_PREPARED. Для
  Варианта Б — генерация уникального ttn_number и stub-URL ТТН
  (реальная подпись через document factory подключается Story 5.4
  follow-up).
- MarketplaceShipmentResolver — мутация marketplaceCreateShipment под
  Shipment:create:own access-matrix, query marketplaceListShipments
  + marketplaceGetShipment для стола подготовки поставки.

Wiring:
- marketplace-infrastructure.module.ts регистрирует обе TypeORM-сущности,
  адаптеры и мапперы, экспортирует репозиторные токены.
- marketplace-application.module.ts регистрирует create-service и
  resolver.

Не входит в Stories 5.1/5.2 (отложено по scope в follow-up Stories
эпика):
- Apollo-codegen интеграция нового стола «Подготовка поставки» в desktop
  offerer-flow — обвязка через @coopenomics/sdk после generate-schema +
  generate-client (паттерн PR #386 Эпика 4).
- Реальная подпись ТТН через AR33 document factory — закрывается
  Story 5.4 (асинхронная подпись поставщика).
- GraphQL Subscription marketplaceShipmentUpdated для live-tracking
  партий — follow-up по аналогии с Order subscriptions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-8][@ant] feat: добавить маркировку имущества штрих-кодом Code128/EAN-13 на приёмке КУ — закрыть Story 5.5 Эпика 5

Backend (controller/extensions/marketplace) — оператор КУ при приёмке
маркирует имущество внутренним штрих-кодом (стандарт маркетплейсов;
QR-коды в MVP запрещены).

Domain (новое):
- MarketplaceInventoryDomainEntity с конструктор-валидацией формата
  EAN-13 (13 цифр + checksum) и обязательного barcode_value.
- MarketplaceBarcodeFormat (CODE128/EAN13), MarketplaceBarcodeStrategy
  (PER_ORDER/PER_UNIT/PER_PACKAGE), MarketplaceInventoryStatus
  (LABELED/ISSUED/RETURNED/WRITTEN_OFF — последние три задел для Эпиков
  6/7/8).

Infrastructure (новое):
- MarketplaceInventoryEntity TypeORM с unique-индексом по
  (coopname, barcode_value) — поиск сканером при выдаче — плюс hot-path
  индексами по order/ku/shipment + status.
- Адаптер репозитория + маппер Row→Domain.

Application (новое):
- MarketplaceInventoryLabelService — guard перехода статуса Order
  (SUPPLY_PREPARED/READY_TO_RECEIVE/ACCEPTED_TO_COOP), guard повторной
  маркировки (countByOrder>0 → 409), привязка к Shipment через
  cycle_id+ku_id, planLabels по стратегии (PER_ORDER → 1 этикетка;
  PER_UNIT → N=quantity; PER_PACKAGE → ceil(quantity/pack_size)),
  генерация уникального barcode_value с retry-loop против коллизий,
  корректный checksum-digit EAN-13.
- MarketplaceInventoryResolver — мутация marketplaceLabelInventory под
  новый access-matrix permission Inventory:label (роль operator), query
  marketplaceListInventory под Warehouse:read:own-KU для admin-стола.
- Access matrix дополнен `operator → Inventory: ['label']`.

Wiring:
- MarketplaceInventoryEntity подключён в connection «marketplace»,
  адаптер и маппер зарегистрированы в infrastructure-module, сервис
  и резолвер — в application-module.

Не входит в Story 5.5 (отложено по scope):
- Apollo-codegen интеграция BarcodeScanner/BarcodeDisplay в operator-стол
  — обвязка через @coopenomics/sdk после generate-schema +
  generate-client.
- Batch-маркировка одной страницей для всей партии (UX-уровень,
  обвязка вокруг marketplaceLabelInventory).
- barcode_strategy как поле Offer — в MVP стратегия передаётся в
  mutation. Поле на Offer'е — техдолг витрины (Stories 3.x follow-up).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-8][@ant] feat: добавить акт приёмки партии с двухподписной state machine для Вариантов А и Б — закрыть Stories 5.3 и 5.4 Эпика 5

Backend (controller/extensions/marketplace) — АПП приёмки на КУ:
оператор формирует акт, поставщик подписывает первой подписью
(лично — Вариант А или асинхронно через push — Вариант Б), председатель
закрывающей. Backend state machine (PG) + расширение canonical-port
методами signSupp / signChair.

Domain (новое):
- MarketplaceAplReceptionDomainEntity + types: вариант A/B, статусы
  PENDING_SUPPLIER_SIGN → PENDING_CHAIRMAN_RECEPTION_SIGN →
  ACCEPTED_TO_COOP (плюс CANCELLED резерв), fact_quantity_per_order
  json для корректировки Варианта Б, expeditor_data снапшот из
  Shipment.ttn_data.
- canonical-port расширен signSupp / signChair (Marketplace.ISignSupp /
  ISignChair из cooptypes).

Infrastructure (новое):
- MarketplaceAplReceptionEntity TypeORM с unique-индексом по
  (coopname, shipment_id) — одна активная АПП на партию + partial
  unique по ttn_number + hot-path индексами по ku/offerer.
- Адаптер репозитория + маппер Row→Domain.
- MarketplaceCanonicalBlockchainAdapter дополнен submit'ами signsupp
  и signchair (подпись кооператива через VaultDomainService).

Application (новое):
- MarketplaceAplReceptionService — state machine:
  * create(shipment_id, fact_quantity?) — guard статуса партии
    SUPPLY_PREPARED + ownership + одна-АПП-на-партию через unique-
    constraint; для Варианта Б копирует TTN-данные снапшотом;
    fact_quantity по умолчанию = order.quantity; total_amount =
    Σ fact_quantity * price_per_unit. Shipment →
    RECEPTION_IN_PROGRESS.
  * signAsSupplier(reception_id) — переход
    PENDING_SUPPLIER_SIGN → PENDING_CHAIRMAN_RECEPTION_SIGN;
    placeholder tx_hash до подключения on-chain signsupp с реальной
    подписью документа Document2.
  * signAsChairman(reception_id, chairman_account) — переход
    PENDING_CHAIRMAN_RECEPTION_SIGN → ACCEPTED_TO_COOP; Order'ы
    группы переходят в ACCEPTED_TO_COOP (FR19a — выдача
    разблокируется только здесь); Shipment → ACCEPTED_TO_COOP.
- MarketplaceAplReceptionResolver:
  * marketplaceCreateAplReception под Receiving:create.
  * marketplaceSignAplReceptionAsSupplier под Receiving:sign:first
    (теперь принадлежит роли offerer — корректировка матрицы).
  * marketplaceSignAplReceptionAsChairman под Receiving:sign:closing
    (новый permission на роли operator).
  * marketplaceListAplReceptionsByKu — список текущих АПП КУ для
    operator-стола.
  * marketplaceListAplReceptionsAsSupplier — список ждущих подписи
    поставщика для offerer-стола.
- Access-matrix:
  * offerer + Receiving: ['sign:first'] — первая подпись поставщика
    в обоих вариантах.
  * operator: Receiving: ['create', 'sign:closing'] — формирование
    АПП и закрывающая подпись председателя КУ.

Wiring:
- Сущность подключена в connection «marketplace», адаптер/маппер в
  infrastructure-module, сервис/резолвер в application-module.

Не входит в Stories 5.3/5.4 (отложено по scope):
- FR45 signing infrastructure: реальная подпись Document2 (IPFS +
  AR33 document factory) + on-chain submit signsupp/signchair. Сейчас
  поля supplier_signsupp_tx_hash / chairman_signchair_tx_hash
  содержат placeholder-значения; chain-port методы реализованы и
  готовы к вызову, но сервис их пока не дёргает (TODO Story 5.6
  follow-up).
- Push-уведомление поставщику в Варианте Б при создании АПП — обвязка
  через notification-module.
- outgoing_payment и o.mkt.purch / o.mkt.payout — закрываются Story
  5.6 / 5.7 отдельным коммитом эпика.
- GraphQL Subscription marketplaceAplReceptionUpdated — follow-up
  по аналогии с Order subscriptions.
- Apollo-codegen интеграция TakeoverDialog в operator-стол.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-8][@ant] feat: добавить marketplace-реестр исходящих платежей с подтверждением кассиром — закрыть Stories 5.6 и 5.7 Эпика 5

Backend (controller/extensions/marketplace) — на закрывающей подписи
АПП-приёмки (Story 5.3/5.4) backend автоматически создаёт запрос
исходящего платежа поставщику; кассир видит задачу в своём столе,
подтверждает факт банковского перевода, статус переходит в
LEDGER_RECORDED.

Domain (новое):
- MarketplaceOutgoingPaymentRequestDomainEntity + types: статусы
  PENDING_CASHIER_ACTION → CONFIRMED_BY_CASHIER → LEDGER_RECORDED
  (плюс BLOCKED при отказе банка). Поля payment_reference (банковский
  ПП), bank_statement_ref (URL/hash выписки), payout_tx_hash под
  lazy-вариант L12, blocked_reason для аудита отказов.

Infrastructure (новое):
- MarketplaceOutgoingPaymentRequestEntity TypeORM с unique-индексом по
  (coopname, apl_reception_id) — один платёж на одну АПП + hot-path
  индексами по payee и статусу.
- Адаптер репозитория + маппер Row→Domain.

Application (новое):
- MarketplaceAplReceptionService.signAsChairman расширен: после
  закрывающей подписи председателя создаётся запрос платежа со статусом
  PENDING_CASHIER_ACTION. Промах не блокирует основной flow АПП
  (логируется warn, наблюдаемость по статусам реестра).
- MarketplaceOutgoingPaymentService:
  * confirm(payment_request_id, payment_reference, bank_statement_ref?)
    — кассир подтверждает физический перевод; статус → LEDGER_RECORDED
    (MVP-поведение pre-L12, см. ограничения ниже).
  * markBlocked(payment_request_id, reason) — отказ банка; платёж
    помечается, обязательство по 86 остаётся открытым, кооператив
    разбирает вручную.
- MarketplaceOutgoingPaymentResolver:
  * marketplaceConfirmOutgoingPayment — мутация подтверждения кассиром.
  * marketplaceBlockOutgoingPayment — мутация блокировки.
  * marketplaceListOutgoingPaymentsForCashier — стол кассира.
  * marketplaceListOutgoingPaymentsAsSupplier — история выплат для
    offerer-стола.

Wiring:
- Сущность подключена в connection «marketplace», адаптер/маппер в
  infrastructure-module, сервис/резолвер в application-module.

MVP-ограничения и техдолг:
- C++ marketplace::signchair выполняет композитную пару
  o.mkt.purch + o.mkt.payout атомарно (pre-L12 поведение). Поэтому
  в текущей реализации подтверждение кассира трактуется как
  audit-trail банковского перевода (LEDGER_RECORDED сразу), а не как
  триггер lazy on-chain payout. Полная L12 (отдельная транзакция
  o.mkt.payout на confirm) — требует разделения signchair и payout в
  C++ контракте; техдолг PRD/FR57.
- Реестр marketplace-scoped (PG-only). Синхронизация с core
  outgoing_payment (AR35) — отдельным follow-up'ом со связкой
  core/gateway-API.
- On-chain submit signsupp/signchair с реальной подписью Document2
  через AR33 document factory — FR45 техдолг, отдельный коммит после
  настройки инфраструктуры подписания.
- Push-уведомления кассиру при создании запроса и поставщику при
  подтверждении — обвязка через notification-module.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-8][@ant] feat: закрыть техдолги Эпика 5 — barcode_strategy на Offer, AR35 core sync, FR45 signing, UI SDK + каркасы 6 столов — чтобы Эпик 5 был cohesive и не висел недоделками

Закрывает блокеры завершения Эпика 5 «Поставка и приёмка имущества на КУ»
(см. план в `_blago/.../598-8-epik-5-...md`, секция «Sprint-план доделки»).

598-22 — barcode_strategy + pack_size на marketplace_offer:
- ALTER через TypeORM synchronize + bootstrap-v5 миграция для bump версии;
- Domain Offer entity + types + mapper + repo create/update;
- Валидация PER_PACKAGE → pack_size (диапазон 1..1000);
- @InputType расширен enum MarketplaceBarcodeStrategy + pack_size;
- MarketplaceInventoryLabelService теперь читает стратегию из Offer
  (per-call параметр оставлен как admin-override);
- +5 unit-тестов для новых валидаций; toMarketplaceOfferDTO вынесен
  в shared helper, убраны 3 дублирующих локальных toDTO.

598-17 / AR35 — синхронизация marketplace выплат с core outgoing_payment:
- Payment entity дополнен опциональными related_extension /
  related_entity_id, PaymentDomainInterface — теми же полями;
- GatewayInteractor.createSystemOutgoingPayment без statement/method_id
  (идемпотентность через payment_hash, source = extension);
- GatewayInteractorPort + оба адаптера расширены новым методом;
- marketplace_outgoing_payment_request получил core_payment_id колонку
  и applyCorePaymentId / deleteById repository-методы;
- AplReceptionService.createOutgoingPaymentRequest вызывает core
  с compensating delete на core-fail; OutgoingPaymentService на
  confirm/block отзеркаливает статус в core (COMPLETED / CANCELLED).

598-15 — FR45 signing infrastructure для signsupp/signchair:
- MarketplaceAplReceptionDocumentFactory строит unsigned Document2
  per-Order с meta (schema, reception_id, fact_quantity, accept_braname);
- Queries marketplaceAplReceptionSupplierSignablePayloads /
  marketplaceAplReceptionChairmanSignablePayloads возвращают payload'ы
  клиенту для подписания приватным ключом;
- Mutations marketplaceSignAplReception* расширены опциональным
  signed_documents (массив signed Document2 per-Order);
- При наличии signed_documents AplReceptionService.submitOnChainSign*
  отправляет реальные chainPort.signSupp/signChair и сохраняет
  реальный tx_hash; compensating-rollback при on-chain fail (статус
  АПП не меняется, ошибка наверх — клиент решает retry);
- Backwards-compat: без signed_documents сохраняется placeholder tx.

598-18 — SDK regenerated с операциями Эпика 5 + 6 UI-pages каркас:
- pnpm generate-schema + generate-client + sdk build (пятишаговая
  процедура feedback_graphql_no_raw_strings_desktop);
- Selectors: shipment, inventory, aplReception (+signable payload),
  outgoingPayment;
- Mutations.Marketplace: CreateShipment, LabelInventory,
  CreateAplReception, SignAplReceptionAsSupplier,
  SignAplReceptionAsChairman, ConfirmOutgoingPayment,
  BlockOutgoingPayment;
- Queries.Marketplace: ListShipments, GetShipment, ListInventory,
  ListAplReceptionsByKu, ListAplReceptionsAsSupplier,
  ListOutgoingPaymentsForCashier, ListOutgoingPaymentsAsSupplier,
  AplReceptionSupplierSignablePayloads,
  AplReceptionChairmanSignablePayloads;
- 6 pages в pages/Marketplace/{OffererSupplyPreparation,
  OffererPendingAplReceptions, OffererPaymentHistory,
  OperatorReception, OperatorInventoryLabeling,
  CashierOutgoingPayments} — каркасы с импортом @coopenomics/sdk и
  per-role классами; полная интеграция с canonical widgets
  (ExpeditorGroupingBoard, TTNPrintPreview, BarcodeScanner) —
  вторым UI-PR'ом после визуальной приёмки в браузере.

598-21 — batch-маркировка штрих-кодом:
- OperatorInventoryLabeling-стол поддерживает режим «печать партии»:
  все этикетки текущего КУ выводятся в одну CSS Grid с break-inside:
  avoid + @media print 2-колончатая разметка для печати на термопринтере.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-20][@ant] feat: push-уведомления marketplace flow — поставщик/кассир/поставщик в 3 точках Эпика 5 — чтобы вариант Б АПП не висел polling'ом и кассир сразу видел новые задачи

— 3 Novu workflow в @coopenomics/notifications:
  marketplace-apl-supplier-sign-request, marketplace-cashier-new-payment,
  marketplace-supplier-payment-confirmed. Email + InApp + Push шаги,
  payload-схемы (z.object) + slugify(name) → workflowId.
— MarketplaceNotificationService с 3 @OnEvent listener'ами:
  слушает event-bus EventEmitter2 (marketplace.aplReception.b.supplier.signRequested,
  marketplace.outgoingPayment.cashier.newTask,
  marketplace.outgoingPayment.supplier.confirmed),
  получает аккаунт через ACCOUNT_DATA_PORT, дёргает NOVU_WORKFLOW_PORT.
  Ошибки Novu/missing subscriber_id — warn без блокировки flow (INV-12:
  emit ПОСЛЕ save в PG, не блокирующий).
— Emit-точки в marketplace-services после save:
  • MarketplaceAplReceptionService.create — push поставщику для
    Варианта Б (EXPEDITOR), для Варианта А поставщик подписывает на
    стойке оператора — push не нужен.
  • MarketplaceAplReceptionService.signAsChairman — push кассиру
    после createOutgoingPaymentRequest успеха (compensating-rollback
    при core-fail убирает запись — push в этом случае не идёт).
  • MarketplaceOutgoingPaymentService.confirm — push поставщику
    после фиксации в LEDGER_RECORDED.
— MVP-ограничение: кассиром выступает chairman кооператива (role:
  'chairman'); когда появится extension-роль cashier — поменять
  фильтр в MarketplaceNotificationService.handleCashierNewPayment.

Закрывает 598-20. Из 7 техдолгов Эпика 5 в этом PR'е закрыто 6
(598-22, 598-17, 598-15, 598-18, 598-21, 598-20). 598-16 (L12 split
signchair/payout в C++ marketplace::supply) остаётся cross-repo
(mono-ai-5 contracts) — выходит за scope PR #388.

* [598-20][@ant] refactor: централизовать кассира в gateway по L12 — снять marketplace-only ручки confirm/markBlocked и подцепиться к payconfirm/paydecline через delta — кассир работает на весь кооператив, marketplace только зеркалит результат

Поток L12 (PR #389):
- backend marketplace::payout → inline gateway::createoutpay (outcome pending)
- кассир в общем столе gateway подтверждает банковский перевод → callback marketplace::payconfirm
- marketplace::paydecline при отказе кассира с reason
Listener (MarketplacePayoutSyncService) переводит per-Order projection PENDING → COMPLETED/DECLINED + setPaymentStatus в core + emit push поставщику.

Backend:
- entity/repository/mapper marketplace_outgoing_payment_request: 1 запись = 1 Order (order_hash unique), статусы PENDING/COMPLETED/DECLINED
- удалён MarketplaceOutgoingPaymentService.confirm/markBlocked + resolver мутации и query listForCashier — кассирского стола в marketplace нет
- MarketplaceAplReceptionService.signAsChairman: per-Order payOut() через chainPort + createSystemOutgoingPayment в core + projection PENDING (вместо агрегированной заявки)
- Novu workflow marketplace-supplier-payment-declined + событие MARKETPLACE_SUPPLIER_PAYMENT_DECLINED_EVENT

Frontend / SDK:
- удалён desktop стол CashierOutgoingPayments (кассир в gateway-расширении)
- удалены SDK mutations confirmOutgoingPayment/blockOutgoingPayment и query listOutgoingPaymentsForCashier
- selector marketplaceOutgoingPaymentRequest обновлён под per-Order поля
- regenerate schema + zeus client + sdk build зелёный

Resolves: ревью PR #388 (кассир централизован), часть E11 техдолга 598-16

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-20][@ant] refactor: документы АПП приёмки переведены на канон factory+SignedDocument — снять локальную самодельную обвязку Document2 (signature.info, builder hash) и подключиться к GENERATOR_PORT + ядерный SignedDigitalDocumentInputDTO как у participant/capital, плюс обязательная подпись на mutation подписания

Что было не по канону (ревью PR #388):
- локальная фабрика marketplace-apl-reception-document-factory.ts с самописным sha256(meta||doc_hash);
- MarketplaceSignatureInfoInputDTO / MarketplaceSignedDocumentInputDTO дублировали SignedDigitalDocumentInputDTO ядра;
- signed_documents nullable + placeholder-tx fallback на backend (документ мог не передаваться вообще);
- output query — кастомный SignablePayload, не GeneratedDocument из ядра.

Канон (как в participant/capital):
- cooptypes: новый registry 1102.MarketplaceAplReception (Action extends IGenerate, Model + html placeholder + переводы);
- controller/application/document/documents-dto/marketplace-apl-reception-document.dto.ts — Base + Generate + SignedMeta + SignedDocumentInput через IntersectionType с GenerateMetaDocumentInputDTO и MetaDocumentInputDTO;
- preview формируется через DocumentDomainService.generateDocument с registry_id=1102 per-Order;
- сервис принимает MarketplaceAplReceptionSignedDocumentInputDTO[], верифицирует подпись через @wharfkit/antelope (PublicKey.from + Signature.verifyDigest), индексирует документы по meta.order_id;
- signed_documents — обязательное (ArrayMinSize(1)), placeholder-режим удалён;
- resolver возвращает [GeneratedDocument!]!;
- module marketplace-application подключает DocumentDomainModule.

Заодно убран any в extract tx hash (G задача из ревью) — типизирован через узкий object literal.

Frontend OffererPendingAplReceptionsPage временно показывает уведомление «UI подписи в разработке» — backend ждёт реальную подпись пайщика через wif (диалог подписания — следующий этап).

Resolves: ревью PR #388 (документы по канону, A + G), удаляет техдолг FR45 на backend.

* [598-20][@ant] refactor: ku_id → braname в marketplace Эпика 5 — выровнять имена с контрактом, где приёмный КУ описывается полем accept_braname, а сам идентификатор КУ — это просто braname; «ku_id» путал агентов и десинхронизировал backend ↔ on-chain поля

Затронуты все слои Эпика 5: shipment / inventory / apl-reception в controller + sdk + desktop.

- domain entity/types: поле и геттеры braname вместо ku_id;
- TypeORM column + индексы: `IDX_marketplace_shipment_cycle_braname_unique` (был cycle_ku) + per-table covered index по (coopname, braname, status);
- repository: findByBraname / listByBraname (вместо ByKu);
- application services + DTO + resolvers: `marketplaceListAplReceptionsByBraname` (query name), `@Args('braname')`;
- SDK: queries `ListAplReceptionsByBraname` (файл переименован), selectors с полем `braname`;
- Desktop: `fetchInventoryByBraname`, view-интерфейсы (operator-inventory + apl-reception);
- schema.gql + zeus client регенерированы.

В dev-стеке `synchronize: true` подхватит column-rename автоматически; для prod-окружения отдельная двухэтапная миграция (ADD braname → backfill ← copy from ku_id → DROP ku_id) — вне scope этого PR, пока БД stateless для разработки.

Resolves: ревью PR #388 (C — ku_id → braname).

* [598-20][@ant] refactor: канон data/input + pug в шаблонах Эпика 5 — все мутации/queries marketplace принимают единый @Args('data', InputDTO) как в core/meet, голые скаляры обёрнуты в Input; шаблоны 5 столов переведены на pug — выровнять стиль с остальной частью desktop

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-8][@ant] refactor: новые столы Эпика 5 переведены на SuccessAlert/FailAlert/NotifyAlert — убрать сырой Notify.create в пользу канона src/shared/api; ошибки идут через FailAlert(e, 'кастомный текст'), успех через SuccessAlert, заглушки UI-этапа — через NotifyAlert; кейс ревью PR #388 (OffererSupplyPreparationPage:22)

* [598-8][@ant] feat: акт АПП registry 1102 — содержание 1-к-1 из 802.ReturnByAssetAct, переменные адаптированы под marketplace — текст и переводы юр.отдела выверены; никакой самодеятельности с placeholder-шаблоном; в Action добавлены act_id и transmitter, сервис проставляет их при генерации (act_id формируется из reception+order short-id, transmitter — account поставщика для строки «Передал заказ»); кейс ревью PR #388 (1102.MarketplaceAplReception:1)

* [598-8][@ant] feat: Story 5.4 ТТН — реальная генерация через document-factory (registry_id=1103) + локальный реестр marketplace_ttn_document, Shipment.ttn_document_id вместо MVP-stub pdf_url — закрыть замечание ревью «MVP-stub в shipment-create.service.ts:321»: при формировании партии Варианта Б backend факторит документ ТТН тем же путём, что и АПП (DocumentDomainService.generateDocument с Cooperative.Registry.MarketplaceTransportNote.Action), и сохраняет в отдельной marketplace-таблице — общий реестр документов кооператива не трогаем, экспедиторы пока не пайщики и подписывают перевозку вне платформы; из Shipment удалены ttn_pdf_url и ttn_document_registry_id, заменены на ttn_document_id (uuid → marketplace_ttn_document.id); кейс ревью PR #388 (marketplace-shipment-create.service:321)

* [598-8][@ant] chore: regenerate schema.gql + SDK zeus + shipment selector после правок ревью PR #388 — pnpm run generate-schema + generate-client; в SDK zeus теперь нет ttn_pdf_url/ttn_document_registry_id (удалены из MarketplaceShipment DTO), добавлен ttn_document_id; shipmentSelector.ts руками поправлен под новый набор полей; SDK typecheck и build зелёные

* [598-8][@ant] fix: desktop typecheck — починить рассинхрон tsc/vue-tsc + убрать pug-мусор — без vue-tsc и с trailing \ в шаблонах PR не катит тайпчек

— components/desktop/package.json: `typecheck` возвращён на `vue-tsc --noEmit --skipLibCheck` (был «hard update» 2025-10-14 на чистый tsc; tsc не парсит .vue и не видит named re-export типов из них — отсюда 21 ошибка TS2614 на widget index.ts из Эпика 10).
— 4 страницы Marketplace (OffererPaymentHistory/OffererPendingAplReceptions/OffererSupplyPreparation/OperatorReception): убраны trailing `\` внутри `:columns="[...]"` — pug не требует line-continuation внутри `()`, vue-tsc ругался TS1127 Invalid character.
— components/sdk/src/queries/marketplace/listOutgoingPaymentsAsSupplier.ts: `query` приведён к канону остальных queries (объект через `$()`, не функция (input)=>Selector(...)); desktop вызывал как объект — отсюда TS2560 «Did you mean to call it?».
— components/desktop/tsconfig.vue-tsc.tsbuildinfo: удалён tracked билд-кэш vue-tsc.

* [598-8][@ant] feat: паттерн doc_data в @coopenomics/factory + ввести 1102/1103 как канонные документы — приватные данные документа off-chain, on-chain только sha256 hash, шаблон обращается через зарезервированную {{ doc_data.* }}

Закрывает крайнее ревью PR #388 (2026-05-17):

— components/factory/src/Services/DocData/index.ts: новый DocDataService — sha256 + Mongo upsert по hash (идемпотентный save), get по hash (null если запись удалена — graceful degradation).
— components/factory/src/Factory/index.ts: в DocFactory методы saveDocData/getDocData/loadDocData; factory автоматически даёт фабрике помощник для подгрузки приватного payload по data.doc_data_hash.
— components/factory/src/index.ts: Generator-фасад выставляет saveDocData/getDocData в IGenerator.
— components/cooptypes/src/cooperative/document/index.ts: IDocDataRef { doc_data_hash?: string } — зарезервированная ссылка, которую Action любого нового документа расширяет.
— components/factory/README.md + AGENTS.md: раздел «Приватные данные документа: doc_data» с потоком, зарезервированными именами и graceful degradation.
— components/factory/test/doc-data.test.ts: unit — sha256, идемпотентность, стабильный порядок ключей, null при удалении, делегирование через Generator-фасад.

1102.MarketplaceAplReception:
— cooptypes registry/1102: Action упрощён до канона 802.ReturnByAssetAct (registry_id, order_id, order_hash, act_id, transmitter, braname?, doc_data_hash?); семантика — `user` = пайщик-получатель (USERNAME), `transmitter` = USERNAME председателя КУ или доверенного им лица, передающего имущество (ФИО подставляется factory через getUser → getFirstLastMiddleName). Текст и переводы — 1-в-1 с 802 (юр.выверенный текст). Никаких «филиалов».
— factory/Actions/1102, factory/Templates/1102: каноничная фабрика по образцу 802.ReturnByAssetAct (factory.getUser(username) → пайщик, factory.getUser(transmitter) → председатель); decision для marketplace — стаб (нет протокола совета на каждый order).
— controller/extensions/marketplace/.../marketplace-apl-reception.service.ts: при сборке Action в Action идут только канонические поля; ФИО пайщика берётся по order.orderer_account, transmitter = chairman_account ?? created_by_operator_account.

1103.MarketplaceTransportNote:
— cooptypes registry/1103: Action очищен — { ttn_number, cycle_id, shipment_id, accept_braname, supplier_account, total_amount, currency, doc_data_hash }. Все персональные данные экспедитора (ФИО, паспорт, телефон, госномер, адрес погрузки, время доставки) вынесены в interface PrivateData — payload off-chain.
— Шаблон обращается через {{ doc_data.expeditor_full_name }}, {{ doc_data.vehicle_number }} и т.п.
— factory/Actions/1103, factory/Templates/1103: фабрика загружает приватный payload через loadDocData; ошибка если doc_data_hash не задан или payload удалён.
— controller/extensions/marketplace/.../marketplace-shipment-create.service.ts: убран MVP-stub — приватные поля экспедитора сохраняются через documentDomainService.saveDocData(...) → doc_data_hash → in Action; on-chain Action не содержит персональных данных.

Прочие документы (700-1099, 300-304, 50-51, …) остаются как были — паттерн прогрессивный, для новых документов с PII и при явной потребности.

Архитектурный артефакт (_bmad-output/planning-artifacts/architecture.md в blago проекте 1-prilozhenie-stol-zakazov): дополнен разделом «Document Generation Pattern: doc_data (Private Document Data Store)». Story 5.8 заведена в blago через `blago create req`.

---------

Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 09:31:50 +05:00
Alex Ant 83e6b654a9 fix(marketplace): Эпик 11 техдолг 598-16 — выплата поставщику через gateway (L12) (#389)
Publish Docs / build-and-publish-docs (push) Failing after 11m31s
* feat(marketplace): Эпик 11 техдолг 598-16 — split signchair и lazy payout (L12)

Locked Decision L12 — выплата поставщику должна выполняться только после
фактического банковского перевода кассиром, не атомарно с приёмкой имущества.
Текущая реализация signchair выполняла composite-пару o.mkt.purch + o.mkt.payout
в одной транзакции — ledger закрывал обязательство Кт 51 ДО реального
cashflow, формальное расхождение между bookkeeping и расчётным счётом.

Изменения:

C++ контракт marketplace:
- signchair оставляет только o.mkt.purch (Дт 10 / Кт 86); status
  supply_prepared → accepted_to_coop, current_warehouse_braname = accept_braname.
- Новый action `payout(coopname, order_hash)` с require_auth(coopname) — единственная
  операция o.mkt.payout (Дт 86 / Кт 51); статус Order'а не меняется.
- Order struct +bool payout_done — guard от двойного списания со счёта 51.

YAML стандарт p.mkt.supply:
- В actions добавлен marketplace::payout (actor: backend).
- В transitions: signchair → operations [o.mkt.purch]; новый transition
  accepted_to_coop → accepted_to_coop через payout с operations [o.mkt.payout].
- В operations: o.mkt.payout.triggered_by = marketplace::payout (раньше signchair).
- Сценарий: step 6 — приёмка без оплаты; новый step 6a — подтверждение выплаты.
- Описание состояния accepted_to_coop отражает открытое обязательство выплаты.

cooptypes:
- IPayout interface (coopname, order_hash); actions/payOut.ts wrapper.
- IOrder + payout_done: IBool.
- Index/jsdoc signChair приведён в соответствие.

Backend controller:
- MarketplaceCanonicalBlockchainPort.payOut + adapter с require_auth(coopname).

Что НЕ входит:
- MarketplaceOutgoingPaymentService.confirm — сервиса ещё нет (ожидается
  в Story 5.6 Эпика 5); реализуется отдельно поверх этого port'а.

Сборка:
- cdt build marketplace зелёный, ABI = 19 actions (был 18 + payout),
  order.payout_done присутствует.
- pnpm --filter cooptypes build green.
- pnpm exec tsc --noEmit (controller) green.

Refs: 598-14 / 598-16 (E11 техдолг), Locked Decision L12.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor(marketplace): payout проходит через gateway с callback'ами payconfirm/paydecline

Уточнение архитектуры техдолга 598-16: исходящая выплата поставщику не должна
быть единичным action'ом контракта marketplace — она проходит через
существующий contract gateway, который умеет принимать запрос и дёргать
callback'и обратно по факту действия кассира. Перепроектировано:

C++ контракт marketplace:
- `payout(coopname, order_hash)` — backend инициирует исходящий платёж;
  inline-вызовом `Gateway::create_outcome` регистрирует запись в
  `gateway::outcomes` со статусом pending и callback'ами на marketplace.
  Ledger2 не двигается; статус Order'а не меняется.
- `payconfirm(coopname, outcome_hash)` — callback от `gateway::outcomplete`
  (require_auth(_gateway)) после действия кассира. ТОЛЬКО здесь применяется
  `Ledger2::apply(o.mkt.payout, ...)` — Дт 86 / Кт 51.
- `paydecline(coopname, outcome_hash, reason)` — callback от
  `gateway::outdecline` (require_auth(_gateway)). Без ledger-движения;
  обязательство Кт 86 остаётся открытым; reason сохраняется на Order.
- Order struct: убран `bool payout_done`, заменён на
  `eosio::name payout_status` (none/pending/completed/declined) + `string
  payout_decline_reason`. Введён namespace OrderPayoutStatus.

YAML стандарт p.mkt.supply: добавлены три новых action, transitions
accepted_to_coop → accepted_to_coop через payout/payconfirm/paydecline,
scenario 6a/6b + alternative «Кассир отклонил банковский перевод», роль
`gateway` в списке ролей; `o.mkt.payout.triggered_by` = `payconfirm`.

cooptypes:
- IPayout (без изменений по сигнатуре), новые IPayConfirm/IPayDecline.
- IOrder: payout_status + payout_decline_reason (вместо payout_done).
- payOut/payConfirm/payDecline action wrappers; index.ts экспорт.

Controller:
- MarketplaceCanonicalBlockchainPort.payOut — обновлён docstring; backend
  callback'и НЕ отправляет (parser2 подхватывает delta).

Документация:
- `components/contracts/cpp/marketplace/payout-via-gateway.md` — шпаргалка
  для агентов: триплет действий, под-граф payout_status, контрактные
  детали, anti-patterns, источники правды.

Сборка:
- cdt build marketplace зелёный; ABI = 21 action (было 19 + payconfirm +
  paydecline); order.payout_status:name, payout_decline_reason:string.
- pnpm --filter cooptypes build green.
- pnpm exec tsc --noEmit (controller) green.

Refs: 598-14 / 598-16 (E11 техдолг), Locked Decision L12.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 20:02:54 +05:00
Alex Ant 75a22fe613 Эпик 4 «Заказ и блокировка средств» MVP Стола заказов (#386)
* [598-7][@ant] feat: добавить создание Order с атомарной серией блока средств — закрыть Story 4.1 Эпика 4

Backend (controller/extensions/marketplace):
- MarketplaceOrderDomainEntity (composite db + bc; IBlockchainSynchronizable) +
  types (cycle_type / status / create_tx snapshot) + repository interface +
  TypeORM entity + mapper + delta-mapper для marketplace::orders +
  adapter (persistAfterBlock / findBySyncKey / sync-fork API).
- MarketplaceCanonicalBlockchainPort + adapter — submit canonical
  Actions.CreateOrder (cooptypes из PR #385 / Story 11.1 PR #375); подпись
  кооператива через VaultDomainService по data.coopname.
- MarketplaceOrderSyncService extends AbstractEntitySyncService: подписка
  на `delta::marketplace::orders` через EventEmitter2 + fork handler с
  компенсирующим offerCounters.onOrderRolledBack для Order'ов в block-state.
- MarketplaceOrderCreateService: guard FR11a (Offer existence/ACTIVE/
  quantity_available); optimistic onOrderBlocked → chain createorder
  (o.wal.conv → o.mkt.assign → o.mkt.block в ledger2::apply) → compensating
  onOrderRolledBack при chain-failure → persist PG с tx_snapshot.
- GraphQL Mutation marketplaceCreateOrder (через DTO/Input + Resolver)
  под Order:create marketplace-access-matrix.
- Wiring marketplace-infrastructure.module + marketplace-application.module.

Tests:
- 7 сценариев composite-entity (db-only, db+bc, sync-key mismatch, update bc
  без затрагивания backend полей, derived флаги для каждого статуса,
  hash normalization, hash валидация).
- 7 сценариев create-service (Guard FR11a 4 ветки, quantity ≤ 0,
  compensating rollback при chain submit fail, happy path с проверкой
  всех 11 полей ICreateOrder payload).

UI (desktop/widgets/Marketplace/CreateOrderDialog):
- TakeoverDialog с формой quantity + KU-выбор через KUMapWithList +
  total-cost preview + mp-role-orderer обёртка; токены только через
  marketplace-tokens.scss; CSS-vars / no hard-coded colors.

Не входит в Story 4.1 (отложено по scope):
- Apollo-codegen интеграция CreateOrderDialog в MarketplaceCatalogPage —
  Story 4.6 («Мои заказы» нужен apollo) подключит mutation marketplaceCreateOrder
  и WalletTimeline mount после успеха.
- GraphQL Subscription marketplaceOrderUpdated — Story 4.6 для live-tracking.
- Удаление legacy controller/application/marketplace/* (interactor/service/
  resolver под клиринг) — отдельный refactor-PR; TSC уже сломан этим
  legacy после PR #385 (canonical cooptypes), не блокирующий Story 4.1
  canonical layer.
- Access-matrix регистрация Order:create permission — в общем update
  Story 4.6.

Spec и контракт интеграции — _bmad-output/implementation-artifacts/
spec-3-4-bc-integration.md (Эпик 3 → Эпик 4) + controller/CLAUDE.md
(Composite-entity / Dispatch pipeline / Fork handling).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-8][@ant] feat: добавить cycle-type-aware агрегацию Order'ов в консолидированную заявку — закрыть Story 4.2 Эпика 4

Backend-only (Locked Decision L10): on-chain представления заявки НЕТ;
агрегация целиком в PG. Order'ы привязываются через
marketplace_order.cycle_id.

Поведение per cycle_type (Locked Decision L11):

- time_based: cron `@Cron(EVERY_5_MINUTES)` сканирует ACTIVE Offer'ы
  с cycle_type='time_based' + cycle_days; для каждого находит cycle_start
  как min(blocked_at) среди unassigned active Order'ов; если now >=
  cycle_start + cycle_days решает:
    sum >= min_threshold (или null) → consolidated_request status=
    'PENDING_SUPPLIER_ACCEPT' + expires_at = now + 48ч + assignToCycle
    Orders → 'ACCEPTED_PENDING_SUPPLIER';
    sum < min_threshold → consolidated_request status='EXPIRED_NO_THRESHOLD'
    БЕЗ assignToCycle (Story 4.3 unblk per-Order'ам пула).

- volume_based: метод `evaluateVolumeBasedAfterCreate` вызывается
  СИНХРОННО из `MarketplaceOrderCreateService.applyCycleTypeHook` после
  persist нового Order'а; если sum >= target_volume → instant
  consolidated_request status='PENDING_SUPPLIER_ACCEPT'. Cron-fallback
  `aggregateVolumeBasedExpired` (EVERY_HOUR) — если объём не накоплен
  и now > cycle_start + max_wait_days → consolidated_request status=
  'EXPIRED_NO_VOLUME' (Story 4.3 unblk).

- open_subscription: backend НЕ создаёт автоматически. Mutation
  `marketplaceTriggerOpenSubscription` (Offer:update:own); поставщик
  жмёт «Запустить» → consolidated_request status='ACCEPTED' сразу,
  Orders пула → 'ACCEPTED' (нажатие = акцепт всего пула).

- individual: backend НЕ агрегирует. `applyCycleTypeHook` в
  OrderCreateService делает Order.status: ACTIVE →
  'ACCEPTED_PENDING_SUPPLIER_INDIVIDUAL' сразу после persist.

Backend (controller/extensions/marketplace):
- domain/entities/marketplace-consolidated-request.{types,entity}.ts +
  spec (4 сценария: PENDING/ACCEPTED/terminal-таблица/open_subscription
  triggered).
- domain/repositories/marketplace-consolidated-request.repository.ts +
  infrastructure (TypeORM entity + mapper + adapter; индексы под
  offerer-стол / pre-aggregator idempotency / cron-scan).
- application/services/marketplace-cycle-aggregator.service.ts —
  @Cron(EVERY_5_MINUTES) aggregateTimeBased + @Cron(EVERY_HOUR)
  aggregateVolumeBasedExpired + evaluateVolumeBasedAfterCreate +
  triggerOpenSubscription. ScheduleModule.forRoot() добавлен в
  MarketplaceExtensionApplicationModule imports.
- application/resolvers/marketplace-cycle.resolver.ts — Mutation
  marketplaceTriggerOpenSubscription + Query marketplaceListConsolidatedRequests
  (Offer:update:own / Offer:read access).
- Расширены MarketplaceOrderDomainRepository (findUnassignedActiveByOffer
  / assignToCycle / sumUnassignedActiveByOffer) и
  MarketplaceOfferDomainRepository (listAllActiveTimeBased /
  listAllActiveVolumeBased).
- MarketplaceOrderCreateService.applyCycleTypeHook — per-cycle_type
  backend hook сразу после persist Order'а; для individual instant
  transition status, для volume_based — instant аггрегация при пороге.

Tests (10 сценариев):
- consolidated-request.entity.spec: PENDING/ACCEPTED/4 terminal/
  open_subscription triggered.
- cycle-aggregator.spec: aggregateTimeBased (3: достигнут + sum >= threshold;
  достигнут + sum < threshold; не достигнут), evaluateVolumeBasedAfterCreate
  (2: ниже target / достигнут), triggerOpenSubscription (4: happy /
  ForbiddenException не владелец / BadRequestException неправильный
  cycle_type / BadRequestException пустой пул).
- OrderCreateService spec обновлён под новый constructor signature
  (cycleAggregator dependency).

Не входит в Story 4.2 (отложено):
- UI для offerer-стола «Консолидированные заявки» (Story 4.5 supplier
  accept/decline UI) — там же добавится списочный widget на канон
  дизайн-системы.
- Mutation marketplaceAcceptConsolidatedRequest / DeclineConsolidatedRequest —
  Story 4.5.
- Auto-decline по expires_at (PENDING > 48ч) — Story 4.3 cron-cleanup.
- Persist полей `acceptance_window_hours` в config кооператива (сейчас
  захардкожено 48ч) — Story 9.x.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-9][@ant] feat: добавить auto-unblk Order'ов при провале триггера и cleanup непринятых заявок — закрыть Story 4.3 Эпика 4

Continuation Story 4.2 (PR #386? коммит 0ba2d51): на cron-агрегаторе и
volume_based-cron при достижении terminal-condition (sum < min_threshold
для time_based, sum < target_volume для volume_based, expires_at < now
для PENDING_SUPPLIER_ACCEPT) теперь автоматически разблокируются средства
каждого Order'а пула — без участия пайщика-заказчика или поставщика.

Что сделано (per acceptance criteria Story 4.3):

  1. Расширен canonical port (`MarketplaceCanonicalBlockchainPort`) +
     adapter (`MarketplaceCanonicalBlockchainAdapter`) методом
     `expireOrder({coopname, order_hash})` — обёртка над C++
     `marketplace::expireorder` (canonical action из PR #385 Story 11.1;
     authorization = кооператив, action_name = 'expireorder', серия
     o.mkt.unblk на total_cost + on-chain Order.status: active → cancelled).

  2. Расширен Order-domain-repository: `findByCycleId(coopname, cycle_id)`
     — все Order'ы привязанные к данной консолидированной заявке. Нужно
     для нового cron-cleanup'а — нужно итерироваться по пулу заявки,
     которая не дождалась ответа поставщика.

  3. `MarketplaceCycleAggregatorService` теперь полноценно завершает
     цикл при terminal-condition:

     - В `processTimeBasedOffer` после persist `consolidated_request`
       статуса `EXPIRED_NO_THRESHOLD` (sum < min_threshold) — серия
       `expireOrder` per-Order пула + counter `onOrderUnblocked` +
       Order.status → `EXPIRED_NO_THRESHOLD`.
     - В `aggregateVolumeBasedExpired` аналогично, но Order.status →
       `EXPIRED_NO_VOLUME` при sum < target_volume + now > cycle_start
       + max_wait_days.
     - Новый `@Cron('*/10 * * * *') expireUnacceptedPending()` — каждые
       10 минут сканирует `PENDING_SUPPLIER_ACCEPT` заявки с
       `expires_at < now` (поставщик не нажал ни Accept ни Decline в
       48-часовом окне): переводит заявку в `EXPIRED_NO_RESPONSE` с
       `decline_reason='expired_no_response'` + серия `expireOrder` пулу
       + Order.status → `CANCELLED_BY_SUPPLIER` с тем же reason.

  4. Private helper `expirePoolOnChain(pool, terminalStatus, reason)` —
     инкапсулирует pattern «per-Order chain.expireOrder → counter
     unblock → applyStatusTransition». Best-effort с partial-fail
     tolerance: chain-fail одного Order'а лог error + продолжаем
     остальные (следующий cron-тик подберёт повторно для time_based /
     volume_based; expireUnacceptedPending же — single-shot, mismatch
     админ-reconciliation Story 9.x). Counter-fail (Offer уже неактивен
     и т.п.) — лог warn + всё равно applyStatusTransition (on-chain unblk
     прошёл; единственное место рассинхронизации — quantity_available
     Offer'а, восстанавливается reconciliation).

  5. Side-by-side с `marketplace-order-delta.mapper.ts` поправлен
     устаревший импорт `Interfaces.IOrderRow` → `Interfaces.Marketplace.IOrder`
     (after PR #385 — единый `IOrder` тип).

Тесты (`marketplace-cycle-aggregator.service.spec.ts`):

  Расширены mocks (chainPort + offerCounters + findByCycleId +
  findExpiredAwaitingResponse + applyStatusTransition на orderRepo).
  Добавлены 5 новых сценариев Story 4.3:

  - **EXPIRED_NO_THRESHOLD pool unblk** — time_based ниже порога:
    `chainPort.expireOrder` per-Order × N, `onOrderUnblocked` per-Order × N,
    `applyStatusTransition(_, 'EXPIRED_NO_THRESHOLD', _)` × N.
  - **EXPIRED_NO_VOLUME pool unblk** — volume_based по истечении
    `max_wait_days` без `target_volume`: аналогично.
  - **partial chain fail tolerance** — один Order падает на `expireOrder`,
    остальные `succeeded`; лог error по failed, applyStatusTransition не
    вызывается на failed Order'е.
  - **counter onOrderUnblocked fail — applyStatusTransition всё равно** —
    Offer удалён / неактивен → counter throws, Order всё равно идёт в
    terminal-status (on-chain unblk уже прошёл).
  - **expireUnacceptedPending happy + empty** — pending > 48ч заявка →
    `EXPIRED_NO_RESPONSE` + Order'ы → `CANCELLED_BY_SUPPLIER`. Пустой
    список — early return без расходов.

  Существующий «EXPIRED_NO_THRESHOLD без assignToCycle» расширен Story 4.3
  ассертами (expireOrder × 2, onOrderUnblocked × 2, applyStatusTransition × 2).

НЕ сделано в Story 4.3 (отложено):

  - **acceptance_window_hours в config кооператива** — захардкожено 48ч,
    задача Story 9.x (общая параметризация кооператива).
  - **Реальные интеграционные тесты с C++ marketplace::expireorder** —
    blockchain-side test через nodeos staging, Story 4.6 / CI e2e.
  - **Manual reconciliation endpoint** для partial-fail случаев,
    которые cron не подобрал — Story 9.x (admin tool).
  - **GraphQL Subscription marketplaceOrderUpdated** — pubsub-канал
    Order при auto-expire должен пушиться, но Story 4.1 sync-сервис
    подписан на `delta::marketplace::orders` (on-chain expire триггерит
    дельту через parser2 → syncer → pubsub). Прямой emit pubsub из
    application-сервиса — anti-pattern (INV-12).

Файлы:

  - `domain/ports/marketplace-canonical-blockchain.port.ts` — +expireOrder
  - `domain/repositories/marketplace-order.repository.ts` — +findByCycleId
  - `infrastructure/adapters/marketplace-canonical-blockchain.adapter.ts`
    — +expireOrder impl
  - `infrastructure/adapters/marketplace-order-repository.adapter.ts`
    — +findByCycleId impl
  - `infrastructure/mappers/marketplace-order-delta.mapper.ts` —
    Interfaces.IOrderRow → Interfaces.Marketplace.IOrder fix
  - `application/services/marketplace-cycle-aggregator.service.ts` —
    constructor +chainPort +offerCounters, processTimeBasedOffer и
    aggregateVolumeBasedExpired вызывают expirePoolOnChain, +cron
    expireUnacceptedPending, +private expirePoolOnChain
  - `application/services/marketplace-cycle-aggregator.service.spec.ts`
    — 5 новых сценариев Story 4.3

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-10][@ant] feat: добавить отмену Order'а заказчиком до акцепта поставщика — закрыть Story 4.4 Эпика 4

Story 4.4 закрывает разрыв между Story 4.1 («заказчик создаёт Order и
блокирует средства») и Story 4.5 («поставщик акцептует или declined'ит»):
до того, как поставщик нажал Accept (либо до того, как time/volume-
agregator закрыл цикл за него), заказчик может отозвать свой Order сам.

Реализация:

  1. Canonical port + adapter: `cancelOrder({coopname, orderer, order_hash})`
     — обёртка над C++ `marketplace::cancelorder` (action_name='cancelorder',
     authorization = кооператив; C++ внутри `require_auth(coopname)` +
     `eosio::check(actor == order.orderer)` + status==ACTIVE).
     Серия ledger2 `UNBLOCK_ON_CANCEL` (o.mkt.unblk на total_cost) +
     on-chain Order.status: ACTIVE → CANCELLED. Сумма возвращается на
     `w.mkt.member.available` пайщика — может быть потрачена на новый
     заказ или выведена через `o.mkt.recall`.

  2. Новый application service `MarketplaceOrderCancelService`:

     **Guard** — Order существует / coopname match / orderer match /
     status='ACTIVE'. Backend разрешает отмену только в ACTIVE (это
     match с C++ check). После того, как:
      - cron `aggregateTimeBased` / `aggregateVolumeBasedExpired`
        перевёл Order в `ACCEPTED_PENDING_SUPPLIER`, или
      - persist-hook `applyCycleTypeHook` перевёл в
        `ACCEPTED_PENDING_SUPPLIER_INDIVIDUAL` (individual cycle),
     заказчик не может отменить сам — поставщик либо акцептует, либо
     declined'ит через Story 4.5. Это защищает поставщика от
     «закаривал-передумал» паттерна на пограничном acceptance-окне.

     **Chain submit** — `chainPort.cancelOrder({coopname, orderer,
     order_hash})`. Любой C++ check fail (например parallel acceptorder
     race) — clean `BadRequestException` с extracted assertion message;
     ни counter, ни Order.status не модифицируются.

     **Counter** — `offerCounters.onOrderUnblocked(offer_id, quantity)`
     best-effort: counter-fail (Offer уже не ACTIVE) → лог warn + всё
     равно applyStatusTransition (on-chain unblk прошёл).

     **Persist** — Order.status: ACTIVE → CANCELLED_BY_ORDERER с reason
     'Отменён заказчиком'. `cancelled_at` = now (через
     applyStatusTransition).

  3. GraphQL Mutation `marketplaceCancelOrder({order_id})` →
     `MarketplaceCancelOrderResult{order, tx_hash}`. ACL
     `@RequireMarketplaceAccess('Order', 'cancel:own')` — marketplace-role
     orderer + owner-check внутри сервиса (через guard orderer match).
     Полная регистрация permission'а — в Story 4.6 access-matrix update.

  4. UI: канон `OrderCard.vue` (`components/desktop/src/widgets/Marketplace/
     OrderCard`) — для orderer-роли в статусе `placed` (mapping канона
     для backend.ACTIVE) добавлена кнопка `{key:'cancel', label:'Отменить',
     kind:'danger'}`. Parent-страница (Story 4.6 «Мои заказы») перехватит
     `@action` событие с `key='cancel'` и оркестрирует confirm dialog +
     apollo `marketplaceCancelOrder` mutation. OrderCard сам по
     дизайн-канону не диалогизирует — лишь публикует event.

Тесты (`marketplace-order-cancel.service.spec.ts`, 8 сценариев):

  - **happy path** — chain.cancelOrder с {coopname, orderer, order_hash} →
    counter.onOrderUnblocked(offer_id, quantity) → applyStatusTransition
    (order.id, 'CANCELLED_BY_ORDERER', 'Отменён заказчиком'); result
    включает tx_hash и Order DTO.
  - **NotFound** — Order не существует.
  - **Forbidden** — Order чужого кооператива.
  - **Forbidden** — отменяет не заказчик-владелец.
  - **BadRequest для статусов** — `ACCEPTED_PENDING_SUPPLIER`,
    `ACCEPTED_PENDING_SUPPLIER_INDIVIDUAL`, `ACCEPTED`, `RECEIVED`,
    `CANCELLED_BY_ORDERER`: jest.each покрывает 5 ветвей "статус ≠ ACTIVE".
  - **chain fail — clean BadRequest** — Order не модифицирован,
    counter не дёрнут.
  - **counter fail — applyStatusTransition всё равно** — best-effort
    (on-chain unblk уже прошёл, counter рассинхрон правится
    reconciliation Story 9.x).

НЕ сделано (отложено):

  - **Confirm dialog UI** — родительская страница «Мои заказы» Story 4.6
    обернёт `@action(cancel)` в `q-dialog` с подтверждением. На Story 4.4
    OrderCard.vue лишь публикует event (per канон-разделение).
  - **GraphQL Subscription marketplaceOrderUpdated** — pubsub Order при
    CANCELLED_BY_ORDERER должен идти по `delta::marketplace::orders`
    (parser2 → syncer → pubsub). Прямой emit из application — anti-pattern
    (controller/CLAUDE.md INV-12).
  - **Apollo client mutation hook + composable** — клиентская часть
    Story 4.6 («Мои заказы»).
  - **Полная регистрация Order:cancel:own permission в access-matrix** —
    Story 4.6 access-matrix-update package.

Файлы:

  - `domain/ports/marketplace-canonical-blockchain.port.ts` — +cancelOrder
  - `infrastructure/adapters/marketplace-canonical-blockchain.adapter.ts`
    — +cancelOrder impl (паттерн как expireOrder/createOrder)
  - `application/services/marketplace-order-cancel.service.ts` — новый
  - `application/services/marketplace-order-cancel.service.spec.ts` —
    новый (8 сценариев)
  - `application/dto/marketplace-order-input.dto.ts` — +MarketplaceCancelOrderInputDTO
  - `application/dto/marketplace-order.dto.ts` — +MarketplaceCancelOrderResultDTO
  - `application/resolvers/marketplace-order.resolver.ts` —
    +marketplaceCancelOrder Mutation
  - `application/marketplace-application.module.ts` — providers + exports
  - `components/desktop/src/widgets/Marketplace/OrderCard/OrderCard.vue`
    — orderer.placed += cancel/danger кнопка

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-11][@ant] feat: добавить accept/decline поставщика по cycle_type — закрыть Story 4.5 Эпика 4

— расширил port + adapter методами acceptOrder/declineOrder (canonical actions из cooptypes PR #385)
— MarketplaceConsolidatedRequestAcceptDeclineService для batch (time_based/volume_based): per-Order chain.acceptOrder | chain.declineOrder + counter.onOrderUnblocked + applyStatusTransition + consolidated_request → ACCEPTED|DECLINED_BY_SUPPLIER (L10)
— MarketplaceOrderSupplierActionService: acceptIndividual / declineIndividual для cycle_type=individual + declineFromOpenPool для частичного отказа из open_subscription до запуска поставки
— 5 GraphQL Mutation: marketplaceAcceptConsolidatedRequest / marketplaceDeclineConsolidatedRequest / marketplaceAcceptIndividualOrder / marketplaceDeclineIndividualOrder / marketplaceDeclineOrderFromOpenPool с ACL Offer:update:own
— DTO: MarketplaceConsolidatedRequestActionResult / MarketplaceSupplierOrderActionResult + 5 input-DTO
— Best-effort partial-fail tolerance для batch: chain-fail per-Order → лог error + продолжаем; counter-fail → warn + applyStatusTransition всё равно
— UI OrderCard.vue: offerer.placed += decline-кнопка (parent оркестрирует confirm-dialog в Story 4.6)
— Spec: 13 сценариев для consolidated (happy / partial-chain-fail / NotFound / Forbidden / cycle_type / status / counter-fail) + 14 для supplier-action (3 группы happy/forbidden/cycle/status/chain-fail)

* [598-12][@ant] feat: добавить orderer-стол «Мои заказы» с жизненным циклом и отменой — закрыть Story 4.6 Эпика 4

— 3 GraphQL Query в MarketplaceOrderResolver: marketplaceListMyOrders / marketplaceListSupplierOrders / marketplaceGetOrder (ACL Order:read:own | read:to-self | read:all)
— MarketplaceListOrdersInput + MarketplaceGetOrderInput + MarketplaceOrderPaginationResult DTO
— toMarketplaceOrderDTO / toMarketplaceOrderCreateTxSnapshotDTO в dto.ts (общие helpers вне resolver)
— access-matrix: admin += Order:read:all (Story 4.6 AC)
— UI desktop: pages/Marketplace/MyOrders с api/index.ts (raw POST /v1/graphql, паттерн каталога) + types.ts + MyOrdersPage.vue
— MyOrdersPage: grid canon OrderCard, фильтр по статусам, polling 10s (Subscription marketplaceOrderUpdated отложена до kernel pubsub Story 9.x), confirm-dialog для cancel (Story 4.4 mutation)
— mp-role-orderer обёртка + STATUS_TO_CARD маппинг доменных статусов на canon OrderCard статусы (UX-DR20 цветные точки)
— Route /:coopname/market/my-orders переключён с legacy UserSuppliesListPage на MyOrdersPage; legacy сохранён под hidden my-supplies-legacy

* [598-13][@ant] feat: cycle_type при публикации Offer'а — закрыть Story 4.7 Эпика 4

Backend (Story 4.7 / L11):
* MarketplaceOfferService.assertCycleConditionals — per cycle_type
  required-валидация для create и update:
    - time_based       → cycle_days REQUIRED (>=1); min_threshold optional
    - volume_based     → target_volume + max_wait_days REQUIRED (>=1)
    - open_subscription → ничего не required
    - individual        → ничего не required
* На update смена cycle_type валидируется по merged-снимку (текущее
  значение из БД + patch), частичный update без касания cycle-полей
  не валидируется заново.
* Reset status → PENDING_MODERATION при любом update уже работает с
  Story 3.2 (существенное изменение покрыто).
* +11 unit-тестов: create per cycle_type (4 invalid + 3 valid) + update
  cycle_type change (4 сценария).

UI (Story 4.7 canon-форма):
* Новая страница `pages/Marketplace/CreateMarketplaceOffer` —
  offerer-форма публикации canonical Offer'а через mutation
  `marketplaceCreateOffer` (раньше роут `create-offer` указывал на
  legacy CreateParentOfferPage с program_id/unit_cost через старый API).
* mp-role-offerer обёртка + токены `marketplace-tokens.scss`.
* q-option-group для cycle_type с подсказкой текущего варианта
  (4 значения, каждый со своей пометкой).
* Conditional cycle-fields:
    - time_based       → cycle_days (required) + min_threshold (опц.)
    - volume_based     → target_volume + max_wait_days (оба required)
    - open_subscription → max_wait_days (опц.)
    - individual        → нет полей
* При переключении cycle_type — defaults и зачистка чужих полей.
* Frontend-валидация дублирует backend per cycle_type (быстрый UX);
  бэкенд — source of truth.
* После успеха — Notify positive + redirect на `marketplace-user-offers`.
* Legacy CreateParentOfferPage спрятан под `create-offer-legacy`
  (hidden=true) на случай fallback'а; полное удаление — техдолг
  marketplace2.

Не вошло (deferred):
* Seed-миграция «legacy Offer'ы без cycle_type → time_based» не
  нужна: до Story 4.7 в БД нет Offer'ов с NULL cycle_type
  (Story 3.2 ввела NOT NULL колонку с дефолтом). Если позднее
  потребуется backfill старых данных — отдельная миграция Phase 2.
* Замена raw POST `/v1/graphql` на Zeus-типизированные Mutations
  отложена до регенерации `@coopenomics/sdk` (общий техдолг
  Marketplace UI).

Тесты:
* `controller`: marketplace-offer-service.test.ts → 35 passed (11 новых).
* TSC моих файлов: 0 ошибок (89 pre-existing legacy не в скоупе Story).

* [598-13][@ant] fix: правки ревью PR #386 Эпика 4 «Заказ и блокировка средств»

— описания GraphQL DTO/resolver переписаны бизнес-языком (без story/epic-меток)
— `cycle_type` / `status` в DTO и сервисах — через ENUM (registerEnumType + companion-константы), вместо строковых литералов
— стандартная пагинация `PaginationInputDTO + createPaginationResult` в `marketplaceListConsolidatedRequests`, `marketplaceListMyOrders`, `marketplaceListSupplierOrders`
— `ASSET_SYMBOL` / `ASSET_DECIMALS` из хардкода RUB/4 убраны в DI через `MARKETPLACE_ASSET_CONFIG` (factory из `config.blockchain.root_govern_symbol/precision`)
— user-facing ошибки в `marketplace-order-supplier-action.service` переписаны на понятный русский язык с уникальными маркерами поиска `[E4SAS-…]`
— комментарий в `marketplace-order-sync.service.afterForkProcessing` явно фиксирует разделение ответственности: сущности откатываются через `deleteByBlockNumGreaterThan` + parser2 replay, hook компенсирует только счётчики Offer'а
— legacy `CreateParentOfferPage` маршрут снесён вместе с зависимостями (`features/Request/CreateParentOffer/`, `pages/Marketplace/CreateParentOffer/`); канон — `CreateMarketplaceOfferPage`
— `frontend MyOrders/api` адаптирован к новому каноническому пагинационному аргументу `options`
— конфликт двух `@ObjectType('MarketplaceCategory')` разрешён переименованием в `category-tree.dto.ts` → `MarketplaceCategoryTreeNode`
— правила зафиксированы: `controller/CLAUDE.md` (описания, ENUM, пагинация), `desktop/extensions/market/CLAUDE.md` (Zeus pipeline)

Zeus regen на схеме блокирован устаревшим legacy `application/marketplace/*` стэком — отдельная story по cleanup'у; raw GraphQL во `CreateMarketplaceOffer/api` и `MyOrders/api` помечен явной TODO-меткой блокера.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-13][@ant] refactor: снести legacy marketplace стэк, regen Zeus, типизированный SDK для Эпика 4

Backend (controller):
— удалены `src/application/marketplace/*` и `src/domain/marketplace/*` (legacy
  cooplace/marketplace resolver, interactor, service, port, ~30 DTO);
— удалён `infrastructure/blockchain/adapters/marketplace-blockchain.adapter.ts`
  и его регистрация в `blockchain.module.ts`/`app.module.ts`;
— удалены `documents-dto/asset-contribution-*` и `documents-dto/return-by-asset-*`
  (6 DTO) — оставались сиротами после сноса legacy resolver'а;
— переименован `@ObjectType('MarketplaceCategory')` в `category-tree.dto.ts`
  → `MarketplaceCategoryTreeNode` (конфликт имени с baseline-категорией Эпика 3);
— `schema.gql` регенерирована.

SDK (`@coopenomics/sdk`):
— удалены 23 legacy zeus-обёртки в `mutations/marketplace/` (publishRequest,
  updateRequest, supplyOnRequest, unpublishRequest, receiveOnRequest,
  prohibitRequest, moderateRequest, declineRequest, deliverOnRequest,
  disputeOnRequest, completeRequest, cancelRequest, completeReceiveOnRequest,
  confirmSupplyOnRequest, acceptChildOrder, createChildOrder, createParentOffer,
  generateAssetContribution*, generateReturnByAsset*);
— Zeus client regenerated (`zeus/`, `types/controller/`);
— добавлены типизированные обёртки для нового стэка: `Mutations.Marketplace.{CreateOrder,
  CancelOrder, CreateOffer, DetailKU, SetKUStatus, RetryKUGeocode}` и
  `Queries.Marketplace.{ListMyOrders, GetOrder, ListCategories, ListKUDetails}`;
— добавлены селекторы `orderSelector` (Order/CreateResult/CancelResult/
  PaginationResult), `offerSelector` (Offer/Category);
— `DetailKUInput`/`SetKUStatusInput` обновлены до `MarketplaceDetailKUInput`/
  `MarketplaceSetKUStatusInput` (новые имена в схеме).

Desktop:
— удалены 16 legacy `features/Request/*` (Accept/Cancel/Complete/ConfirmRecieve/
  ConfirmSupply/CreateChildOrder/Decline/DeliverOn/DisputeOn/Moderate/Prohibit/
  Publish/RecieveOn/SupplyOn/Unpublish/UpdateRequest);
— удалены legacy `entities/Request/`, `widgets/Marketplace/CreateChildOrderCard/`,
  `widgets/Marketplace/SupplyOrderRequestCard/`, `widgets/Marketplace/RequestCard/`;
— удалены legacy страницы `OfferPage/UserSuppliesList/WarehousePage/ShipmentsPage/
  DisputePage/Moderation/Showcase/UserParentOffers/SuppliesList` и их маршруты;
— `pages/Marketplace/CreateMarketplaceOffer/api/index.ts` и `pages/Marketplace/
  MyOrders/api/index.ts` переписаны с raw GraphQL на типизированные
  `Mutations.Marketplace.*` / `Queries.Marketplace.*`;
— `entities/MarketplaceKUDetails/api/index.ts` — те же обёртки под новые
  имена мутаций;
— redirect после создания Offer'а → `marketplace-catalog` (вместо удалённого
  `marketplace-user-offers`);
— `AGENTS.md` обновлён под актуальный набор страниц.

Тесты:
— `tests/unit/marketplace/marketplace-{moderation,offer-counters}-service.test.ts`
  — добавлены `listAllActiveTimeBased` / `listAllActiveVolumeBased` в моки
  репозитория (приведено в соответствие с domain-интерфейсом).

Память: `feedback_graphql_no_raw_strings_desktop.md` обновлён — блокер
regen снят, raw GraphQL запрещён, процедура generate-schema/generate-client/
sdk build документирована.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 14:45:44 +05:00
Alex Ant b3c5c0b1a7 feat(cooptypes): canonical TS-foundation marketplace под Story 11.1 (#385)
Publish Docs / build-and-publish-docs (push) Failing after 15m59s
Зеркало TS-сторон контракта marketplace под членскую модель «Стола заказов»
(PR #375). Источник правды — components/contracts/build/contracts/marketplace/marketplace.abi.

Interfaces (src/interfaces/marketplace.ts):
- 18 action-payload интерфейсов: I{CreateOrder, CancelOrder, ExpireOrder,
  AcceptOrder, DeclineOrder, SignSupp, SignChair, SignIss1, SignIss2,
  SubmRetrn, AprRetRem, RejRetRem, AccRetrn, RejRetrn, PropWroff, ExecWroff,
  DeclWroff, Migrate}
- 4 entity-интерфейса: IOrder (с current_warehouse_braname / accept_braname /
  delivery_braname), IReturnRequest, IWriteoffProposal, IWroffItem (с executed)
- Базовые IDocument2 / ISignatureInfo

Contracts (src/contracts/marketplace/):
- Удалены 24 legacy action'а (createOffer/createOrder-old/publishRequest/
  acceptRequest/confirmReceive/coopstock/...) под старую клиринговую модель.
- 18 новых action-обёрток + tables/{orders, retrequests, wroffprops}.
- index.ts реэкспорт по процессам p.mkt.{supply,return,wroff}.

Build: pnpm --filter cooptypes build — green; tsc --noEmit — green.

⚠ Breaking change: legacy desktop-фичи (Request feature, SupplyOrderRequestCard,
OfferPage, CreateChildOrder и др.) сломаются при tsc — их перепишут Stories
эпиков 3.1+/4.x на новые типы. Это ожидаемо: данный PR — pre-foundation.

Discovery Story 4.1 (Эпик 4) показал блокер — без canonical TS Эпик 4 не может
стартовать. Этот PR разблокирует Stories 4.x.

Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 16:41:16 +05:00
Alex Ant ce3bee0aba refactor(marketplace): Story 3.5 — каталог переведён на канон дизайн-системы (Эпик 10) (#384)
- удалён локальный pages/.../CatalogOfferCard.vue (дублировал вёрстку карточки);
- MarketplaceCatalogPage импортирует canonical CatalogOfferCard из
  widgets/Marketplace/CatalogOfferCard (UX-DR10, Story 10.2.4);
- маппер MarketplaceOfferView → CatalogOffer: title/price/unit/stock/status;
  «Заказать» вынесено в slot actions виджета;
- корневой класс mp-role-orderer + токены --mp-* для spacing/colors,
  отказ от hard-coded color="grey-3/7" в фильтр-чипах;
- empty-state перекрашен через --mp-on-surface-muted.

Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 15:41:28 +05:00
Alex Ant cc694dedcc feat(marketplace): Эпик 10 — Дизайн-система Стола Заказов (Stories 10.1+10.2+10.3) (#383)
* [598-13][@ant] feat: добавить токены marketplace и каркас витрины дизайн-системы — Story 10.1 даёт владельцу продукта единую страницу /market/design-system для утверждения 13 UI-компонентов до их применения в функциональных эпиках 1-9 без локального запуска

- src/app/styles/marketplace-tokens.scss — spacing (UX-DR22), touch-targets 44/48 (UX-DR23), per-role layout (UX-DR31), statusbar (UX-DR20), helper-классы
- src/pages/Marketplace/DesignSystem/ — каркас витрины: aside-навигация по 13 компонентам, переключатели роли/темы/breakpoint в URL query, заглушки Placeholder для будущих секций
- TokensSection — превью палитры, типографики, spacing-scale, touch-targets, per-role layout
- extensions/market/install.ts — route marketplace-design-system под /market/design-system (roles: chairman, member)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-13][@ant] feat: добавить CatalogOfferCard (UX-DR10) — карточка предложения в каталоге Витрины становится каноническим компонентом Стола Заказов, обёртка над существующим RequestCard с расширенным API status (draft/moderation/published/paused/sold-out/completed) + slot actions + fallback изображения для применения в эпиках 3 (Витрина) и 4 (Заказ)

- widgets/Marketplace/CatalogOfferCard — типизированный CatalogOffer + 6 статусов + slot actions + per-role POS-увеличение title
- секция CatalogOfferCard в витрине дизайн-системы со всеми 6 статусами + edge cases (нет preview, длинный текст, slot actions)
- статус в каталоге: imported → ready

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-13][@ant] feat: добавить OrderCard (UX-DR9) — карточка заказа с 10 статусами жизненного цикла и per-роль actions для применения в эпиках 4 (Заказ), 5 (Поставка), 6 (Выдача), 9 (Склад) — Стол Заказов получает единый компонент списка заказов вместо ad-hoc вёрстки

- widgets/Marketplace/OrderCard — Order interface с 10 status (draft → issued + dispute/returned) + 4 роли (orderer/offerer/operator/admin) с собственным набором actions
- секция витрины переключает симуляцию роли и показывает все 10 статусов в одной сетке
- статус в каталоге: planned → ready

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-13][@ant] feat: добавить TakeoverDialog (UX-DR7) — full-screen takeover с 4 kind для критических действий выдачи (Эпик 6), отмены заказа (Эпик 4), спора (Эпик 7), списания скоропорта (Эпик 8) даёт единый паттерн необратимых операций со встроенным confirm-loader и slot actions

- widgets/Marketplace/TakeoverDialog — TakeoverKind info/success/warning/danger + цветной header-bar + slot для body и actions + confirm-loader
- секция витрины с 4 пресетами (open by kind) + комментарий + последнее событие
- статус в каталоге: planned → ready

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-13][@ant] feat: добавить WalletTimeline (UX-DR8) — лента движений кошелька с 6 типами операций (пополнение, блокировка, разблокировка, списание, возврат, выплата) для применения в эпиках 4 (Заказ и блокировка средств) и 9 (Склад и отчётность), даёт пайщику читаемую историю и связку с конкретными заказами

- widgets/Marketplace/WalletTimeline — WalletEntry + WalletEntryKind (6 типов) + цветовая семантика + знак суммы + связь с orderId
- секция витрины с фильтром по типам + рендер бок-о-бок normal vs empty state
- статус в каталоге: planned → ready

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-13][@ant] feat: добавить BarcodeScanner (UX-DR11) и BarcodeDisplay (UX-DR12) — mock-сканер с visual feedback вместо звукового пика (UX-DR26) и SVG-рендер штрих-кода для печати на ТТН — операторский стол ПВЗ (Эпик 5 Поставка, Эпик 6 Выдача) получает согласованный паттерн ввода/вывода штрих-кодов до подключения jsbarcode/quagga в функциональной реализации

- widgets/Marketplace/BarcodeScanner — 5 состояний (idle/requesting/scanning/success/error) с lazer-анимацией и flash-эффектом
- widgets/Marketplace/BarcodeDisplay — SVG-генерация с 3 size (sm/md/lg) и опцией без подписи
- секции витрины: успех vs ошибка камеры; sizes-grid + кастомный код в инпуте
- статусы в каталоге: planned → ready (для обоих)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-13][@ant] feat: добавить CorrectionTable (UX-DR13) и TTNPrintPreview (UX-DR15) — таблица корректировки приёмки факт vs план и предпросмотр печатной ТТН А5 со встроенным штрих-кодом — операторский стол ПВЗ (Эпики 5 Поставка и 6 Выдача) получает согласованный паттерн приёмки имущества и сопровождающего печатного документа

- widgets/Marketplace/CorrectionTable — inline-input факт, цветовая подсветка дельты, счётчики совпадений/недостач/избытков
- widgets/Marketplace/TTNPrintPreview — формат А5 (148×210 мм), BarcodeDisplay в шапке, таблица позиций, две подписи, @media print
- секции витрины: CorrectionTable с 5 примерами (совпадение/недостача/избыток/полное отсутствие); TTNPrintPreview с реалистичным mock-документом
- статусы в каталоге: planned → ready

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-13][@ant] feat: добавить ExpeditorGroupingBoard (UX-DR14) WarehouseSummaryGrid (UX-DR16) OnboardingCPPGate (UX-DR17) MultiChannelStatus (UX-DR18) и секцию KUMapWithList — закрыть оставшиеся 5 custom-компонентов Эпика 10, чтобы все 13 имели визуальный канон для эпиков 1 (Онбординг), 5 (Доставка/группировка), 9 (Склад), и cross-cutting NotificationCenter

- ExpeditorGroupingBoard — drag-n-drop карточек между маршрутами + empty-state, событие move
- WarehouseSummaryGrid — admin-плотная таблица приход/расход/остаток с фильтром и сортировкой
- OnboardingCPPGate — L3-gate с required/optional/locked документами, два примера (orderer vs offerer)
- MultiChannelStatus — chip-row push/email/SMS с tooltip-details и 6 status
- KUMapWithList — секция-обёртка над существующим widgets/KUMapWithList с пояснением что реальная карта работает на /market-pvz/list
- все 13 статусов в каталоге переведены в ready

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-13][@ant] feat: добавить Story 10.3 performance baseline и правила для агентов — закрыть Эпик 10 секцией performance в витрине, инструкцией PERFORMANCE.md с эталонными страницами P1-P4 и целевыми метриками MVP/Phase 2, плюс CLAUDE.md в extensions/market/ как канон-директива для всех агентов работающих с marketplace UI

- pages/Marketplace/DesignSystem/PERFORMANCE.md — manual Lighthouse workflow, NFR-P1/P2/P3, целевые метрики
- pages/Marketplace/DesignSystem/ui/sections/PerformanceSection.vue — секция витрины с эталонными страницами и таблицей метрик
- extensions/market/CLAUDE.md — таблица 13 канонических компонентов, 5 жёстких правил для агентов (inline-вёрстка запрещена, токены только через CSS-переменные, per-роль через корневой класс, новый компонент сначала в витрине, расширение API через витрину)
- статус в каталоге: добавлена секция performance (ready)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-13][@ant] fix: восстановить плоские реэкспорты в entities/MarketplaceKUDetails — индекс экспортировал ./model только через namespace MarketplaceKUDetailsModel, из-за чего useMarketplaceKUDetailsStore / IMarketplaceKUDetails / IWorkingHours не резолвились в PvzListPage, KUMapWithList и MarketplaceDetailKUDialog (vue-tsc валился на всех ветках marketplace2) — добавлен `export * from './model'` параллельно с namespace, обратной совместимости не теряем

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-13][@ant] fix: починить ESLint и pug-парсер на marketplace2 — pug-парсер в ShipmentsPage и DisputePage падал на `#{{ id }}` (символ # после текста = id-селектор Pug), из-за чего ESLint считал весь скрипт unused; заменены на `№{{ id }}`; параллельно убраны мелкие unused в WarehousePage, PvzListPage, useDesignSystemState, ExpeditorGroupingBoard — финально npm run lint чист на src/extensions

- pages/Marketplace/ShipmentsPage/ui/ShipmentsPage.vue — `Перевозка #{{ id }}` → `Перевозка №{{ id }}` (pug syntax fix)
- pages/Marketplace/DisputePage/ui/DisputePage.vue — `Претензия #{{ id }}` → `Претензия №{{ id }}` (pug syntax fix)
- pages/Marketplace/WarehousePage/ui/WarehousePage.vue — удалён неиспользуемый import client; параметр _request заглушен через void (ESLint не учитывает underscore-prefix)
- pages/Marketplace/PvzList/ui/PvzListPage.vue — _unused-driver и _pvz-параметр заглушены через void
- pages/Marketplace/DesignSystem/composables/useDesignSystemState.ts — убран лишний import ref
- widgets/Marketplace/ExpeditorGroupingBoard — убрано неиспользуемое `const props =`, defineProps без присваивания

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [598-13][@ant] refactor: минимализм Айва в токенах и каталоге — нейтральная шкала + статус-чипы с точкой + терминология «заказать/закончилось» — фидбэк владельца продукта 2026-05-15 о попугайных бэйджах, выпадающей кнопке «Купить» и расхождении со словарём ЦПП (это не торговля, а заказы)

* [598-13][@ant] refactor: адаптив карточек на xs/sm — meta-grid в OrderCard и карточный fallback в CorrectionTable с touch-friendly полем «факт» — фидбэк 2026-05-15 что на узких экранах верстка просто сжимается и в поле «факт» невозможно попасть пальцем; теперь meta переносится через auto-fit grid а на <600px CorrectionTable рендерится карточками

* [598-13][@ant] refactor: единый стиль кнопок и статуса в OrderCard — primary unelevated без тени + flat second-level + mp-status-chip вместо разноцветного q-badge — фидбэк 2026-05-15 что «Открыть» (с тенью) и «Подробнее» (flat) выглядят попугайно и не из одного дизайна; теперь все actions одной формы с единым радиусом

* [598-13][@ant] refactor: убрать «попугайную» цветную шапку TakeoverDialog — нейтральный header + 4px accent-stripe слева + цветная иконка — фидбэк 2026-05-15 что info-карточка с залитой primary-шапкой выглядит броско и одинаково для info и success; теперь акцент через 4px полосу (primary/positive/warning/negative)

* [598-13][@ant] fix: контраст BarcodeScanner на тёмной теме — явный rgba-overlay вместо опоры на text-grey-7 + flash-анимация возвращается в полупрозрачный чёрный а не в transparent — фидбэк 2026-05-15 что на dark внизу мелькает белая полоска и текст «Сканер готов» теряет фон

* [598-13][@ant] feat: ExpeditorGroupingBoard — снять инлайн text-color/bg + добавить move-all для оптом-переноса — фидбэк 2026-05-15 что на dark черный текст «3 заявки ожидают группировки» уходит в чёрный фон и что перетаскивать 10 единиц одного заказа по одной — неудобно; теперь токены поверхности + кнопка «Переместить всё из N» рядом с droptarget

* [598-13][@ant] refactor: WarehouseSummaryGrid — снять колонку статус «достаточно/мало» — фидбэк 2026-05-15 что КУ кооператива работает транзитом (пришло — ушло), это не торговая точка с минимальным остатком; ввели подсветку остатка серым при нуле — этого достаточно

* [598-13][@ant] refactor: OnboardingCPPGate — одна оферта ЦПП per-роль и нейтральный «обязательно»-чип — фидбэк 2026-05-15 что в архитектуре нет ПВЗ-логистики/ценовой политики/режима самозанятого и подписки на рассылку; L2 на регистрации уже принимает обработку данных и базовые правила платформы — здесь дублировать нельзя

* [598-13][@ant] refactor: MultiChannelStatus — нейтральные чипы с точкой состояния и нормальные отступы — фидбэк 2026-05-15 что разноцветные q-chip (info/positive/primary/negative) выглядят попугайно и прижаты друг к другу; теперь surface-1 фон + 1px-граница + цветная точка-индикатор + gap 16px

* [598-13][@ant] fix: безопасное переключение light/dark — снять хардкод-фоны на CatalogOfferCard статус-чипе, DesignSystemPage preview-обёртке и заменить bg-grey-2 в секциях на mp-event-banner поверх токенов — фидбэк 2026-05-15 что статус-чип «Распродано» сливается на dark и часть баннеров не реагирует на тему; теперь все surface — через --mp-surface-* и --mp-border-subtle, переключение работает автоматически

---------

Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 15:04:58 +05:00
Alex Ant 173014f712 feat(marketplace): Эпик 3 — Витрина: публикация, модерация, каталог — Stories 3.1+3.2+3.3+3.4+3.5 (#382)
* [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>
2026-05-15 13:55:12 +05:00
Alex Ant a02e0fe0bf feat(marketplace): Story 11.1 — 18 canonical actions через Ledger2::apply на 13 операциях (#375)
Publish Docs / build-and-publish-docs (push) Failing after 15m19s
* fix(ledger2): handle WalletOp::REVOKE in walletop dispatcher (Story 11.4)

REVOKE (op_code=6) was added to enum and OPERATION_REGISTRY for o.mkt.consum
(2026-05-11), but walletop.cpp rejected op_code > 4. Added explicit case:
списывает amount с wallet_from.blocked в пустоту, без зачисления; L3-зеркало
для USER_SHARED. NONE добавлен как unreachable case для исчерпывающего switch.

Прерывает блокер для Story 11.1 (canonical marketplace actions).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(marketplace): canonical scaffolding — entity tables + 18 action skeletons (Story 11.1)

Удалены donor-actions старой клиринговой модели (AR30):
- 50 .cpp файлов в components/contracts/cpp/marketplace/src/{deliver_on_offer,
  deliver_on_order,shipment,dispute_on_offer,корень};
- legacy marketplace.{cpp,hpp} с ~30 actions (orderoffer/accept/supply/dispute/
  shipment/etc) на legacy Wallet:: API мимо ledger2;
- три donor entity-таблицы: lib/domain/table_marketplace_{requests,segments,
  shipments}.hpp;
- lib/core/marketplace/marketplace.hpp donor-helpers (get_request_by_hash,
  get_shipment_by_hash, namespace DocumentNames клирингового состава).

Создана canonical инфраструктура трёх процессов p.mkt.{supply,return,wroff}:
- lib/domain/table_marketplace_orders.hpp — Order entity с полным
  жизненным циклом p.mkt.supply (active → cancelled / accepted →
  ship_ready → supply_prepared → accepted_to_coop → ready_to_receive →
  received), 8 secondary индексов, поля для трассировки signsupp/signchair/
  signiss1/signiss2 АПП-документов и warranty_until для submretrn гарда.
- lib/domain/table_marketplace_return_requests.hpp — ReturnRequest entity
  для p.mkt.return (pending_review → approved_for_visit → return_accepted /
  rejected_at_ku / rejected_remote), original_consume_op_id для
  compensating-forward трассировки (d6 A4), photos vector<checksum256>.
- lib/domain/table_marketplace_writeoff_proposals.hpp — WriteoffProposal
  entity для p.mkt.wroff с vector<wroff_item> per-КУ позиций.
- lib/core/marketplace/marketplace.hpp — canonical helpers
  get_*_by_hash[_or_fail] / update_* + cross-contract get_user_wallet_balance
  для guard'а Locked Decision L6 в createorder.

Контракт marketplace.{cpp,hpp} переписан целиком под 18 canonical actions
(p.mkt.supply 10 + p.mkt.return 5 + p.mkt.wroff 3) согласно YAML-стандартам
рядом с .hpp. Каждый action — отдельный .cpp файл в marketplace/src/
по образцу Capital'а; на этом коммите все 18 — skeleton с
eosio::check(false, "TODO Story 11.1 Шаг N: <action> ещё не реализован")
и подробным @brief docstring'ом со ссылкой на Story / guards / ledger2-
операции из YAML. Реальная реализация actions — последующими коммитами
(Шаги 4-6 спецификации Story 11.1).

Batch (consolidated request) намеренно НЕ выведен в on-chain entity —
Locked Decision L10: backend-only сущность, on-chain действия per-Order;
batch_hash на Order'е — opaque ссылка для трассировки.

Foundation Story 11.4 на месте (предыдущий коммит): WalletOp::REVOKE
обработан walletop dispatcher'ом; OPERATION_REGISTRY содержит все 13
marketplace-операций; счета 10/91 в LEDGER2_ACCOUNT_MAP.

Spec артефакт: blago/production/1-prilozhenie-stol-zakazov/components/
3-minimalnyy-produkt/_bmad-output/implementation-artifacts/
spec-11-1-ledger2-marketplace-ops.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(marketplace): implement createorder action (Story 4.1, p.mkt.supply)

Первая реализация canonical action — proof-of-pattern для остальных 17.
Серия трёх ledger2-операций (атомарно в одной транзакции Antelope):
- o.wal.conv (conditional, недостача из w.wal.share);
- o.mkt.assign (conditional, недостача из w.wal.member);
- o.mkt.block (всегда, total_cost на w.mkt.member).

L6 guard через cross-contract read трёх кошельков (Marketplace::
get_user_wallet_balance) — фейлится с понятной ошибкой если суммарных
средств не хватает. process_hash для всех трёх ops = order_hash.

Создан Order entity со status=ACTIVE; cycle_type валидируется по
enum (timebased/volumebased/opensubscr/individual). actual_quantity
и fact_cost ставятся равными quantity/total_cost — обновятся в
signiss2 при выдаче.

Plus: rename WroffStatus::DRAFT → PROPOSED (DRAFT конфликтует с macro
из lib/consts.hpp; on-chain строка осталась "draft"_n как в YAML).

Build: pnpm build:contract:test marketplace — green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(marketplace): implement 17 canonical actions (Story 11.1, p.mkt.{supply,return,wroff})

Реализованы все оставшиеся 17 actions поверх createorder (proof-of-pattern):

p.mkt.supply (9 actions, дополнительно к уже реализованному createorder):
- cancelorder — отмена до акцепта; o.mkt.unblk; ACTIVE → CANCELLED.
- expirecycle — закрытие цикла; threshold_reached==false → o.mkt.unblk per
  order + ACTIVE → CANCELLED; threshold_reached==true — no-op (backend
  формирует batch).
- acceptbatch — поставщик акцепт; ACTIVE → ACCEPTED, batch_hash зафиксирован;
  без ledger2 (L10).
- declinebatch — отказ до акцепта; o.mkt.unblk per order + ACTIVE → CANCELLED.
- prepship — сборка партии; ACCEPTED → SHIP_READY; shipping_method
  валидируется (varianta | variantb).
- signsupp — первая подпись АПП приёмки поставщиком; verify_document_or_fail
  ({offerer}); SHIP_READY → SUPPLY_PREPARED + acceptance_act_signsupp.
- signchair — закрывающая подпись АПП приёмки председателем; per-Order
  Ledger2::apply(o.mkt.purch) + Ledger2::apply(o.mkt.payout) атомарно;
  SUPPLY_PREPARED → ACCEPTED_TO_COOP + acceptance_act_signchair.
- signiss1 — открытие выдачи председателем; verify_document_or_fail({chairman});
  ACCEPTED_TO_COOP → READY_TO_RECEIVE + issue_act_signiss1.
- signiss2 — финальная подпись заказчика с поддержкой actual_quantity ≠
  ordered: при actual<ordered — o.mkt.unblk на разницу; при actual>ordered —
  L6 guard на доступность diff в трёх кошельках, conditional o.wal.conv +
  o.mkt.assign + o.mkt.block на разницу; всегда — o.mkt.consum + o.mkt.consum2
  на fact_cost; READY_TO_RECEIVE → RECEIVED + warranty_until.

p.mkt.return (5 actions):
- submretrn — заявление на возврат; гард warranty_until > now; создание
  return_request в PENDING_REVIEW; order.return_request_id для двусторонней
  связи. photos обязательны.
- aprretrem — удалённое одобрение визита; PENDING_REVIEW → APPROVED_FOR_VISIT.
- rejretrem — удалённый отказ; PENDING_REVIEW → REJECTED_REMOTE + reason_remote.
- accretrn — приём возврата на очном осмотре; Ledger2::apply(o.mkt.return) +
  Ledger2::apply(o.mkt.return2) (compensating forward, не revert);
  APPROVED_FOR_VISIT → RETURN_ACCEPTED.
- rejretrn — отказ на очном осмотре; APPROVED_FOR_VISIT → REJECTED_AT_KU +
  reason_visit.

p.mkt.wroff (3 actions):
- propwroff — формирование проекта списания; total_amount = Σ items.amount;
  PROPOSED.
- execwroff — исполнение per-item: Ledger2::apply(o.mkt.wroff) +
  Ledger2::apply(o.mkt.wroff2) каждой позиции; PROPOSED → EXECUTED;
  process_hash единый = proposal.hash, item.ku_chairman как username для
  per-КУ аналитики.
- declwroff — отклонение советом; PROPOSED → REJECTED + reject_reason.

Все compose'ятся через единый Ledger2::apply orchestrator, не нарушая
foundation Story 11.4. Composite-операции (consum+consum2, return+return2,
wroff+wroff2) в одной транзакции Antelope обеспечивают атомарность L1
проводок через транзит счёта 91. Cross-contract balance read через
Marketplace::get_user_wallet_balance(_ledger2 scope) — для L6-гарда
createorder и signiss2 (correction>).

Build: pnpm build:contract:test marketplace — green.
Foundation: walletop REVOKE + 13 marketplace operations + WalletOp NONE/REVOKE
+ счета 10/91 + кошельки w.mkt.{member,payout} — на месте (предыдущие коммиты).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(marketplace): структура по процессам + чистка избыточных дат on-chain

Структура:
- src/p.mkt.supply/  — 10 actions процесса прямой поставки-приобретения
                       (createorder, cancelorder, expirecycle, acceptbatch,
                        declinebatch, prepship, signsupp, signchair,
                        signiss1, signiss2);
- src/p.mkt.return/  — 5 actions процесса гарантийного возврата
                       (submretrn, aprretrem, rejretrem, accretrn, rejretrn);
- src/p.mkt.wroff/   — 3 actions процесса утилизации скоропорта (списания)
                       (propwroff, execwroff, declwroff).
Имена подпапок 1:1 совпадают с process_type из YAML-стандартов рядом —
прозрачная навигация от .cpp к стандарту.

Удалены избыточные date-поля из 3 entity-таблиц (orders / return_requests /
writeoff_proposals): created_at, accepted_at, shipped_at, received_to_coop_at,
ready_at, received_at, cancelled_at, reviewed_at, resolved_at, proposed_at,
decided_at. Контракт ничего не проверяет по этим датам — все timestamp'ы
переходов состояний восстанавливаются на бэкенде из blockchain_actions[at]
по соответствующим actions через ParserClient. Сокращает RAM-стоимость
state на каждый Order / ReturnRequest / WriteoffProposal.

Сохранено on-chain:
- order.warranty_until — нужно для guard'а submretrn (`now() < warranty_until`)
  без cross-action lookup; вычисляется в signiss2 как
  signiss2.timestamp + warranty_period_secs.
- batch_hash, status, документы (acceptance_act_signsupp/signchair,
  issue_act_signiss1/signiss2, statement, decision_remote/visit,
  protocol, photos), reason fields, fact_cost / actual_quantity, items
  для wroff_proposal, links (return_request_id, original_order_id,
  original_consume_op_id) — это смысловая on-chain информация, не
  выводимая из истории.

Удалены secondary-индексы bycreated и byproposed (бэкенд индексирует
по сохранённым в Postgres timestamp'ам блоков, on-chain pagination
по чрон. порядку = по primary key id).

Build: pnpm build:contract:test marketplace — green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(marketplace): ревью PR #375 — braname/branch, per-Order, memos, без prepship

- ku_chairman заменён на braname (приёмка/выдача) в Order; в return_request
  привязка к КУ удалена — каждое решение принимает braname параметром, авторизация
  через Branch::is_user_authorized (председатель/trustee/доверенные)
- Все per-batch actions теперь per-Order (acceptorder/declineorder/expireorder,
  signsupp/signchair); векторов order_hashes нет — backend проходит циклом
- prepship удалён как избыточный шаг (после acceptorder сразу signsupp);
  OrderStatus::SHIP_READY убран
- execwroff per-item: один item за вызов с item_index; финализация PROPOSED → EXECUTED
  автоматически после исполнения последней позиции; declwroff фейлится если
  хотя бы одна позиция уже исполнена
- propwroff валидирует существование braname для каждого item через get_branch_or_fail
- Memos вынесены в lib/core/marketplace/memo.hpp по аналогии с Capital::Memo;
  все Ledger2::apply используют человекочитаемые memo с подстановкой id
- Все eosio::check сообщения переписаны на пользовательский русский текст
  без префиксов action/Locked Decision (UI показывает их напрямую)
- walletop REVOKE дополнен подробным docstring: разница с BURN (blocked vs
  available, обязательная пара Dr/Cr, отсутствие альтернативы через UNBLOCK+BURN)

Build: marketplace.wasm + ledger2.wasm — green под coopos 5.2.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(marketplace): order.current_warehouse_braname (текущая точка хранения)

Поле введено сейчас, чтобы избежать binary_extension при включении полноценного
процесса перемещения имущества между КУ после релиза.

Заполнение:
- signchair (приёмка кооперативом): current_warehouse_braname = accept_braname
- signiss1 (готово к выдаче): current_warehouse_braname = delivery_braname

Промежуточные перемещения между приёмным и выдающим КУ контрактом не
подписываются (бездокументарно) — точка хранения переходит «скачком» в момент
готовности к выдаче. Подписание ТТН по внутренним передачам — отдельная
история после релиза, текущее поле даёт ей точку расширения без миграции.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 11:25:07 +05:00
Alex Ant 3f2f205abd feat(marketplace): Эпик 2 — Сеть кооперативных участков (ПВЗ) — Stories 2.1+2.2+2.3 (#381)
* [598-5][@ant] feat: backend marketplace_ku_details + Yandex Geocoder — Stories 2.1+2.2

Эпик 2 (Сеть кооперативных участков), бэкенд-часть:

— Story 2.1: marketplace_ku_details (1:1 расширение core coop_ku) — entity,
  domain, mapper, repository adapter; GraphQL мутации
  marketplaceDetailKU / marketplaceSetKUStatus / marketplaceRetryKUGeocode +
  query marketplaceListKUDetails. ACL — @AuthRoles(['chairman']) для
  mutations, ['chairman','member','user'] для list (Story 1.6/1.8 access-matrix
  не merged в marketplace2 — chairman ≈ marketplace-role admin).

— Story 2.2: GeocoderPort + YandexGeocoderAdapter с локальным rate-limit
  (sliding window) и таймаутом запроса; конфиг через
  YANDEX_GEOCODER_API_KEY/BASE_URL/RATE_LIMIT_RPS/TIMEOUT_MS env.
  Post-effect hook detailKU при смене addressFull сбрасывает координаты
  в PENDING и асинхронно перезаписывает на OK или FAILED + errorMessage,
  не прерывая основную мутацию.

Тесты: 18 unit-кейсов в tests/unit/marketplace (mapper round-trip,
service happy/edge paths, geocoder happy/empty/HTTP500/exception/no-key/
rate-limit). pnpm typecheck + targeted jest проходят.

* [598-5][@ant] feat: KUMapWithList + админ-страница ПВЗ — Story 2.3

Эпик 2, фронтенд:

— `entities/MarketplaceKUDetails` — FSD entity со store/api/types.
  API — raw GraphQL (fetch) поверх backend-резолверов Story 2.1/2.2.
  Типизация — ручная, до регенерации Zeus в `@coopenomics/sdk` (техдолг).
— `widgets/KUMapWithList` — компонент карта Yandex + список ПВЗ с
  синхронным выбором (клик в списке центрирует карту и открывает balloon
  пина; клик на пин подсвечивает карточку). Responsive: на мобильных
  layout вертикальный, карта сверху. Динамическая загрузка Yandex Maps
  SDK через `loadYandexMaps`; деградированный UI если ключа нет.
  Aria-label на регионе/карточках, q-skeleton на loading.
— `features/MarketplaceDetailKU` — диалог формы детализации ПВЗ
  (адрес, контакты, режим работы по дням). Используется на admin-странице
  для create/edit.
— `pages/Marketplace/PvzList` — admin/orderer/offerer страница списка ПВЗ.
  Для admin'а — кнопки edit/деактивировать/активировать/повторить
  геокодинг (Story 2.1 acceptance); slot `cardAction` позволяет
  заказчику/поставщику расширить под «Выбрать как место получения» /
  «Везти сюда» (передаётся при reuse).
— Регистрация роута `/:coopname/market/pvz` в `extensions/market/install.ts`.

Limitations (вынесены в PR-описание):
  • Зависит от регенерации Zeus типов в SDK — пока через raw GraphQL.
  • Не интегрирован в orderer/offerer create-order/pre-shipment flow —
    integration на стороне Эпиков 4-5.
  • WCAG 2.1 AA полный audit — Phase 2 post-MVP (Эпик 10 Story 10.2 notes).
  • Yandex Maps JS API требует `YANDEX_MAPS_API_KEY` в env (window.__APP_CONFIG__).

* [598-5][@ant] refactor: GeocoderPort провайдер-агностичен + чистка @description ревью PR #381

— env YANDEX_GEOCODER_* → GEOCODER_*; добавлен GEOCODER_PROVIDER (yandex|noop)
— infra adapters: NoopGeocoderAdapter + geocoderPortFactory; YandexGeocoderAdapter
  читает config.geocoder.*; module использует useFactory
— resolver / DTO: убраны ссылки на «Story X.Y / Эпик 2» из GraphQL @description
  (остаются в JSDoc-комментариях кода)
— jest: maxWorkers=1 в jest.config.js + явный --runInBand в package.json scripts
  (вешает dev-стек docker при параллельном пуле)
— schema.gql + controller/zeus регенерированы

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* [598-5][@ant] feat: SDK marketplace KU details — селектор, 3 мутации, 1 query ревью PR #381

— Selectors.marketplaceKUDetailsSelector + workingHoursSelector/workingHoursDaySelector
— Mutations.Marketplace.DetailKU/SetKUStatus/RetryKUGeocode
— Queries.Marketplace.ListKUDetails (новый модуль queries/marketplace)
— zeus regenerated (controller schema → SDK)
— убирает техдолг raw-graphql во фронте

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* [598-5][@ant] refactor: ПВЗ в отдельный workspace + переход с raw-graphql на Zeus ревью PR #381

— Стол ПВЗ — отдельный workspace 'market-pvz', defaultRoute marketplace-pvz,
  доступ roles:['chairman'] (заказчик/поставщик его не видят и не перегружены)
— entities/MarketplaceKUDetails/api: client.Query/Mutation из @coopenomics/sdk
  (Mutations.Marketplace.DetailKU/SetKUStatus/RetryKUGeocode +
  Queries.Marketplace.ListKUDetails); raw-graphql.ts удалён

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* [598-5][@ant] refactor(sdk): kuDetailsSelector — _validate для всех селекторов как в других модулях

— rawWorkingHoursBreakSelector / Day / Sselector + rawMarketplaceKUDetailsSelector
— const _validate: MakeAllFieldsRequired<ValueTypes['Type']> = rawXxxSelector
  по аналогии с capital/voteSelector.ts: typecheck падёт, если в schema
  появится новое поле, не покрытое селектором.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 11:03:11 +05:00
Alex Ant d36b168752 feat(marketplace): Эпик 1 (Stories 1.4-1.11) — установка Стола и трёхуровневый онбординг (#380)
Publish Docs / build-and-publish-docs (push) Failing after 15m0s
[598-4][@ant]
* feat(marketplace): Story 1.2 — регистрация оферты в AgreementRegistry

Перепиcано через существующий core-механизм `AgreementRegistrationPort`
(закрытый PR #369 шёл через параллельный coop_cpp_registry — техдолг).
Аналогично тому, как Capital регистрирует свои оферты —
register-capital-in-agreement-registry.ts.

- constants/marketplace-agreement-ids.ts — MARKETPLACE_EXTENSION_NAME='market',
  MARKETPLACE_OFFER_AGREEMENT_ID='order_table_offer',
  MARKETPLACE_AGREEMENT_TYPE='order_table',
  MARKETPLACE_OFFER_TEMPLATE_REGISTRY_ID=0 (placeholder; Story 1.7 заменит
  на реальный registry_id из платформенной document factory, по аналогии с
  Cooperative.Registry.GeneratorOffer/BlagorostOffer).
- application/registration/register-marketplace-in-agreement-registry.ts —
  чистая функция: при ID>0 зовёт port.registerAgreement с одной офертой
  (applicable_account_types: individual + entrepreneur, order: 7); при ID<=0
  возвращает false без вызовов. Programs marketplace не регистрирует —
  Стол заказов ЦПП без program-надстройки.
- marketplace-extension.module — @Inject(AGREEMENT_REGISTRATION_PORT) +
  вызов registerMarketplaceInAgreementRegistry в initialize после initBucket;
  info-лог при skip и при успешной регистрации, error при exception.

Tests:
- tests/unit/marketplace/marketplace-plugin-register.test.ts (3 кейса):
  чистая функция — port не вызван пока placeholder=0, константы согласованы,
  programs не регистрируются.
- tests/unit/marketplace/marketplace-plugin-initialize.test.ts — обновлён
  конструктор (добавлен makeAgreementPort стаб); 4 кейса Story 1.1
  продолжают проходить.

10/10 unit-тестов зелёных, tsc 0 ошибок.

Ref: blago/.../components/3-minimalnyy-produkt/_bmad-output/implementation-artifacts/spec-1-2-cpp-registry-link.md
(spec будет переписан вместе с blago-issue после merge).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(marketplace): MARKETPLACE_AGREEMENT_TYPE = 'marketplace' (валидный eosio::name)

Per review PR #370: 'order_table' содержит '_' — не валидный eosio::name
(допустимы только [.a-z1-5], max 12). В lib/consts.hpp уже определено
program-имя `_marketplace_program = "marketplace"_n` (program_id=2), его
и принимает `soviet::coagreements`/`sndagreement`.

- MARKETPLACE_AGREEMENT_TYPE: 'order_table' → 'marketplace'.
- MARKETPLACE_OFFER_AGREEMENT_ID: 'order_table_offer' → 'marketplace_offer'
  (консистентно с 'generator_offer'/'blagorost_offer' у Capital).
- AgreementRegistrationSpec.agreement_type — JSDoc уточнён: должен быть
  eosio::name (max 12, без `_`), пример обновлён.
- Тест проверяет eosio::name regex `/^[.a-z1-5]{1,12}$/` для type.

10/10 unit зелёные, tsc 0 ошибок.

* feat(marketplace): Story 1.3 — доступ к Столу заказов по членству

Реализует:
- MarketplaceMembershipGuard: 401 (нет JWT), 403 (status != active),
  server-secret bypass, формирует request.currentMember и
  ctx.currentMember для GraphQL.
- mapUserRoleToCoreRoles: user.role → core_roles[]
  - 'user'     → ['User']
  - 'member'   → ['User', 'Member']
  - 'chairman' → ['User', 'Member', 'Chairman']
  - 'admin'/unknown → []
- @CurrentMarketplaceMember decorator + MarketplaceCurrentMemberDTO
  ({username, core_roles[], marketplace_roles[]}).
- Query marketplaceWhoAmI(): возвращает контекст текущего пайщика для
  фронта (UI открывается на primary столе по роли).
- marketplace_roles[] пока пустой — наполняется в Story 1.6.

Тесты: 18/18 зелёных (5 mapper + 6 guard + 4 initialize + 3 register).
tsc 0 ошибок.

* feat(marketplace): Story 1.4 — L3 fallback gate онбординга

Domain service `MarketplaceOnboardingService.getOnboardingState(username)`:
- placeholder=0 (Story 1.7 не выполнена) → {requires_gate:false,
  source:'not_configured'} — фронт пропускает на стол.
- agreementRepository.findByUsername → найдена запись type='marketplace'
  c подходящим draft_id (или без draft_id как fallback) →
  {requires_gate:false, source:'agreement_signed', completed_at,
  agreement_id}.
- иначе {requires_gate:true, source:'gate_required'}.

Не заводим локальную таблицу marketplace_onboarding_state — core
AgreementRepository уже даёт state через PG-кеш agreements3
(AgreementSyncService); TTL 60s из PRD не нужен.

Mutation подписания не реализуется в этой Story — при placeholder=0 gate
не показывается, путь активируется после Story 1.7.

GraphQL Query `marketplaceOnboardingState: MarketplaceOnboardingState`
под GqlJwtAuthGuard + MarketplaceMembershipGuard. username берётся из
@CurrentMarketplaceMember, фронт не подменяет member_id.

Тесты: 22/22 зелёных (4 onboarding-service + 18 предыдущих).
tsc 0 ошибок.

* feat(marketplace): Story 1.5 — кошелёк ЦК пайщика на столе

Query marketplaceMemberWallet под GqlJwtAuthGuard + MarketplaceMembershipGuard,
делегирует чтение в core WalletService.getProgramWallet(coopname, username,
program_type=MAIN). PG-кеш ledger2::userwallets уже консистентен с
blockchain (UserWalletSyncService); локальная таблица
marketplace_member_wallet_link из PRD не заводится — техдолг PRD.

DTO MarketplaceMemberWallet возвращает:
- available/blocked — из w.wal.share части ledger2-кошелька;
- membership_contribution — w.wal.member.value;
- contract='wallet', coopname, username.

Если PG-кеш пуст → NotFoundException (CLAUDE.md запрещает RPC fallback).
Поля undefined нормализуются в '0'.

Тесты: 25/25 зелёных (3 wallet resolver + 22 предыдущих).
tsc 0 ошибок.

* feat(marketplace): Story 1.6 — массив marketplace-ролей + RoleGuard

mapCoreRolesToMarketplaceRoles(coreRoles[], context):
- User → [orderer] +opt[offerer] +opt[operator]
- Member → +board_readonly
- Chairman → +admin, +board
- без User → []

isOfferer/isKuChairman context-флаги зарезервированы (Эпики 2/3).

MarketplaceMembershipGuard теперь заполняет marketplace_roles[] через
mapper (раньше был placeholder []).

MarketplaceRoleGuard (новый): отдельный guard под
@RequireMarketplaceRole('admin', 'board') (OR-семантика). Если декоратор
не задан → guard разрешает. При запрете:
- ForbiddenException "Forbidden: marketplace role 'X' required, member has [...]"
- warn-лог forbidden-attempt: member=, action=, requested_role=[],
  actual_marketplace_roles=[], actual_core_roles=[].

server-secret bypass обоих guards (inter-service).

README рядом с guards описывает паттерн UseGuards + декоратор + Phase 2
миграцию в CASL (Guard остаётся, меняется источник policy).

Тесты: 40/40 зелёных (7 marketplace-mapper + 8 role-guard + 6 membership
+ 5 core-mapper + 4 onboarding + 3 wallet + 4 init + 3 register).
tsc 0 ошибок.

* feat(cooptypes,marketplace): Story 1.7 — template ЦПП «Стол заказов» в registry

cooptypes registry:
- 1100.MarketplaceOfferTemplate — шаблон оферты ЦПП marketplace
  (по аналогии с 999.BlagorostOfferTemplate). Содержит placeholder
  content (рыбу) с явным caveat-блоком; финальный юридический текст
  готовится отдельно и заменит content без изменения registry_id.
- 1101.MarketplaceOffer — instance, renderуется для пайщика при подписании
  (Story 1.4 L3 или Story 1.11 L2; аналог 1000.BlagorostOffer).
- Зарегистрированы в registry/index.ts.

marketplace controller:
- MARKETPLACE_OFFER_TEMPLATE_REGISTRY_ID импортируется из
  Cooperative.Registry.MarketplaceOfferTemplate.registry_id (=1100).
- Добавлена MARKETPLACE_OFFER_INSTANCE_REGISTRY_ID (=1101) для будущей
  mutation подписания.

Auto-activates:
- Story 1.2: registerMarketplaceInAgreementRegistry больше не пропускает
  регистрацию; port.registerAgreement вызывается при restart расширения.
- Story 1.4: marketplaceOnboardingState возвращает requires_gate=true для
  пайщика без подписи (а не not_configured).

Тесты обновлены под id=1100:
- marketplace-plugin-register: AC successful registration; rollback кейс
  через jest.doMock.
- marketplace-onboarding-service: placeholder=0 через jest.doMock; добавлен
  кейс «Story 1.7 размещена → requires_gate=true».

42/42 marketplace unit зелёных, tsc 0 ошибок.

Юридический финальный текст оферты — отдельный todo (todo-tspp-templates.md).

* feat(marketplace): Story 1.8 — централизованная access-matrix (CASL-compat)

marketplaceAccessMatrix: Record<MarketplaceRole, Record<Resource, Action[]>>
для всех 6 ролей. Покрывает Эпики 1-4 (Order, Offer, KU, Receiving,
Issuance, Warehouse, Vitrine, Whitelist, Agenda, Writeoff, Decision,
Extension). Эпики 5-10 расширят без правок resolver-ов.

canAccess(roles[], resource, action) — OR по ролям, exact-match action.
roleHasAnyAction(roles, resource, baseAction) — UI-фильтр меню.

Новый декоратор @RequireMarketplaceAccess(resource, action). Старый
@RequireMarketplaceRole сохранён для обратной совместимости и для
требований, не привязанных к resource.

MarketplaceRoleGuard расширен: читает оба декоратора через Reflector;
если оба заданы — AND-семантика. Лог-формат отказа по access:
`requested_access=Resource:action`.

README рядом с guards: секция access-matrix, нотация actions (:own/:all/
:to-self/:own-KU/:first), обновлённый Phase 2 plan (canAccess →
CASL ability.can без изменения вызывающего кода).

Тесты: 60/60 marketplace зелёных (10 suites). tsc 0 ошибок.

CI-checker orphan resolvers/matrix entries не реализован (требует
AST-парсер) — фиксируется как Phase 1.5 техдолг.

* feat(marketplace): Story 1.9 — L1 решение совета о принятии положения ЦПП

extension.config расширяется coopAcceptance: {accepted, document_registry_id,
accepted_at, accepted_by_board_decision_id}. Bootstrap-миграция v2 добавляет
поле к существующим конфигам идемпотентно.

MarketplaceCoopAcceptanceService:
- getStatus() → {status: 'active'|'not_accepted', ...} из extension.config.
- accept({document_registry_id, accepted_by_board_decision_id, accepted_at?})
  → пишет coopAcceptance в config, возвращает 'active' state.

GraphQL:
- Query marketplaceCppStatus — открыт всем активным пайщикам (фронт решает,
  показывать ли баннер «Не подключено»).
- Mutation marketplaceAcceptCpp — admin-only через
  @RequireMarketplaceAccess('Extension', 'configure').

MVP-stub: реальная интеграция с повесткой совета (FR40, Эпик 8) ещё не
готова — председатель передаёт board_decision_id как string. Phase 2 после
Эпика 8: mutation валидирует существование решения / переедет в side-effect
core-controlled повестки.

Тесты: 66/66 marketplace unit зелёных. tsc 0 ошибок.

* feat(marketplace): Story 1.10 — статус оферты в core AgreementRegistry

Query marketplaceRegistrationOfferStatus → читает через AGREEMENT_QUERY_PORT.
getAgreementById(MARKETPLACE_OFFER_AGREEMENT_ID), возвращает
{registered, agreement_id, registry_id, agreement_type, title,
applicable_account_types}. Используется admin UI для статуса «Оферта
зарегистрирована в registration-flow».

MarketplaceCoopAcceptanceService.accept (Story 1.9) теперь имеет
side-effect: re-register оферту в core AgreementRegistry через
registerMarketplaceInAgreementRegistry (идемпотентно). Покрывает кейс,
когда Story 1.7 поставила template позже restart-а расширения и
MarketplacePlugin.initialize пропустил регистрацию (placeholder=0 → ok).

AGREEMENT_REGISTRATION_PORT инжектируется @Optional — старые unit-тесты
Story 1.9 (constructor без порта) проходят.

AC PRD `coop_registration_offers_registry` = платформенный AgreementRegistry
с extension_name ключом. Per-coop scoping не нужен для одно-coop
controller'а; multi-coop приложит CooperativeConfigService без правок
marketplace-кода (тех-долг PRD, принят).

Тесты: 68/68 marketplace зелёных (новый 1.10 resolver + предыдущие).
tsc 0 ошибок.

* feat(marketplace): Story 1.11 — L2 онбординг через core registration-flow

Backend marketplace для L2 не вносит нового кода — flow держится на
уже реализованных компонентах + core registration-flow:

1. Story 1.2/1.7/1.10: marketplace_offer зарегистрирована в
   AgreementRegistry, видна core SignUp.
2. Core registration-flow рендерит instance 1101.MarketplaceOffer
   через documentFactory.
3. Подпись через wallet::signagree / soviet::sndagreement('marketplace')
   — работа core.
4. AgreementSyncService наполняет AgreementRepository.
5. Story 1.4 MarketplaceOnboardingService читает PG-кеш и НЕ показывает
   L3 gate если подпись есть.

Добавлено:
- onboarding/README.md — описание полного L1→L2→L3 цикла, какие компоненты
  предоставляет marketplace и что требует от core; раздел фоллоуапов.
- marketplace-l2-onboarding-flow.test.ts — 3 scenario-кейса (подпись через
  L2 → нет gate; нет подписи → gate; ownership by username).

Эпик 1 закрыт: 11/11 stories реализованы, 71 marketplace unit-тест
покрывает критические инварианты онбординга.

Открытые фоллоуапы (вынесены как отдельный backlog):
- L3 mutation marketplaceSignOnboardingOffer (write-pool + sndagreement).
- Source-маркер registration_flow vs extension_gate в DTO.
- Mutation marketplaceAcceptCpp валидация реальной повестки совета (Эпик 8).

* [58bfe503][@ant] fix: переписать marketplaceMemberWallet на массив кошельков по стандарту marketplace — устранить подгонку под старую нотацию membership_contribution, показывать share/member/mkt.member как есть из ledger2::userwallets

Review @dacom-dark-sun на PR #380 (line 13/53 в DTO+Resolver) указал: текущая
реализация Story 1.5 сворачивает w.wal.share + w.wal.member в свёрнутый
ProgramWallet и подставляет membership_contribution — это «полная хуйня,
старая нотация, кошельков система обновилась». По стандарту marketplace
(components/contracts/cpp/marketplace/p.mkt.supply.standard.yaml, секция wallets)
у пайщика 3 USER_SHARED-кошелька: w.wal.share (ЦК паевые), w.wal.member
(универсальный членский — транзитный share→mkt), w.mkt.member (программный
членский ЦПП Стол Заказов). Каждый со своим available+blocked.

Перепиcан DTO MarketplaceMemberWalletDTO: вместо плоских полей возвращается
массив MarketplaceWalletEntryDTO[{name, human_name, program_id, program_label,
kind, available, blocked}]. Resolver зовёт core UserWalletRepository.findByUsername
напрямую (PG-кеш ledger2::userwallets, ADR-011), фильтрует 3 релевантных wallet_name,
для каждого ставит 0/0 если L3-запись ещё не создана (w.mkt.member появляется
после первого orderoffer/createorder). WalletService.getProgramWallet больше не
зовётся — он сворачивает share+member и неприменим для UX, требующего видеть
каждый кошелёк отдельно. WalletModule убран из marketplace-application.module
(USER_WALLET_REPOSITORY доступен глобально из @Global TypeormModule).
Unit-тесты переписаны: 4 кейса (массив с заполнениями, нулевой пайщик, human_name
из cooptypes, w.mkt.payout COOPERATIVE не попадает в выдачу) — 4/4 зелёных.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [58bfe503][@ant] fix: переименовать program_label → label с UX-нотацией «<тип взноса> | <программа>» — review @dacom-dark-sun PR #380 line 34

Прежние значения «ЦК» / «Marketplace» — это технические идентификаторы программ
из LEDGER2_USER_SHARED_PROGRAM_MAPPING, а не UX-метки. Review указал нужный
формат: «Паевой | Цифровой Кошелёк», «Членский | Цифровой Кошелёк», «Членский | Стол Заказов».

Поле `program_label` переименовано в `label`. Семантика: короткая
человеческая метка кошелька для panel/list. Длинное юридическое название
по-прежнему доступно в `human_name` (из cooptypes LEDGER2_WALLET_REGISTRY).
program_id (1/2) остаётся машинно-читаемым полем для группировки.

Тест переписан: 4/4 зелёных (label проверяется для всех 3 кошельков).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 00:30:35 +05:00
coopops 0a5dc3dabd Merge remote-tracking branch 'origin/dev' into marketplace2 2026-05-14 18:16:11 +00:00
Alex Ant 540c4ceab1 feat(marketplace): Story 1.3 — доступ к Столу по членству (#371)
* feat(marketplace): Story 1.2 — регистрация оферты в AgreementRegistry

Перепиcано через существующий core-механизм `AgreementRegistrationPort`
(закрытый PR #369 шёл через параллельный coop_cpp_registry — техдолг).
Аналогично тому, как Capital регистрирует свои оферты —
register-capital-in-agreement-registry.ts.

- constants/marketplace-agreement-ids.ts — MARKETPLACE_EXTENSION_NAME='market',
  MARKETPLACE_OFFER_AGREEMENT_ID='order_table_offer',
  MARKETPLACE_AGREEMENT_TYPE='order_table',
  MARKETPLACE_OFFER_TEMPLATE_REGISTRY_ID=0 (placeholder; Story 1.7 заменит
  на реальный registry_id из платформенной document factory, по аналогии с
  Cooperative.Registry.GeneratorOffer/BlagorostOffer).
- application/registration/register-marketplace-in-agreement-registry.ts —
  чистая функция: при ID>0 зовёт port.registerAgreement с одной офертой
  (applicable_account_types: individual + entrepreneur, order: 7); при ID<=0
  возвращает false без вызовов. Programs marketplace не регистрирует —
  Стол заказов ЦПП без program-надстройки.
- marketplace-extension.module — @Inject(AGREEMENT_REGISTRATION_PORT) +
  вызов registerMarketplaceInAgreementRegistry в initialize после initBucket;
  info-лог при skip и при успешной регистрации, error при exception.

Tests:
- tests/unit/marketplace/marketplace-plugin-register.test.ts (3 кейса):
  чистая функция — port не вызван пока placeholder=0, константы согласованы,
  programs не регистрируются.
- tests/unit/marketplace/marketplace-plugin-initialize.test.ts — обновлён
  конструктор (добавлен makeAgreementPort стаб); 4 кейса Story 1.1
  продолжают проходить.

10/10 unit-тестов зелёных, tsc 0 ошибок.

Ref: blago/.../components/3-minimalnyy-produkt/_bmad-output/implementation-artifacts/spec-1-2-cpp-registry-link.md
(spec будет переписан вместе с blago-issue после merge).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(marketplace): MARKETPLACE_AGREEMENT_TYPE = 'marketplace' (валидный eosio::name)

Per review PR #370: 'order_table' содержит '_' — не валидный eosio::name
(допустимы только [.a-z1-5], max 12). В lib/consts.hpp уже определено
program-имя `_marketplace_program = "marketplace"_n` (program_id=2), его
и принимает `soviet::coagreements`/`sndagreement`.

- MARKETPLACE_AGREEMENT_TYPE: 'order_table' → 'marketplace'.
- MARKETPLACE_OFFER_AGREEMENT_ID: 'order_table_offer' → 'marketplace_offer'
  (консистентно с 'generator_offer'/'blagorost_offer' у Capital).
- AgreementRegistrationSpec.agreement_type — JSDoc уточнён: должен быть
  eosio::name (max 12, без `_`), пример обновлён.
- Тест проверяет eosio::name regex `/^[.a-z1-5]{1,12}$/` для type.

10/10 unit зелёные, tsc 0 ошибок.

* feat(marketplace): Story 1.3 — доступ к Столу заказов по членству

Реализует:
- MarketplaceMembershipGuard: 401 (нет JWT), 403 (status != active),
  server-secret bypass, формирует request.currentMember и
  ctx.currentMember для GraphQL.
- mapUserRoleToCoreRoles: user.role → core_roles[]
  - 'user'     → ['User']
  - 'member'   → ['User', 'Member']
  - 'chairman' → ['User', 'Member', 'Chairman']
  - 'admin'/unknown → []
- @CurrentMarketplaceMember decorator + MarketplaceCurrentMemberDTO
  ({username, core_roles[], marketplace_roles[]}).
- Query marketplaceWhoAmI(): возвращает контекст текущего пайщика для
  фронта (UI открывается на primary столе по роли).
- marketplace_roles[] пока пустой — наполняется в Story 1.6.

Тесты: 18/18 зелёных (5 mapper + 6 guard + 4 initialize + 3 register).
tsc 0 ошибок.

---------

Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 21:45:27 +05:00
Alex Ant db80dea2b6 feat(marketplace): Story 1.2 — регистрация оферты в AgreementRegistry (#370)
* feat(marketplace): Story 1.2 — регистрация оферты в AgreementRegistry

Перепиcано через существующий core-механизм `AgreementRegistrationPort`
(закрытый PR #369 шёл через параллельный coop_cpp_registry — техдолг).
Аналогично тому, как Capital регистрирует свои оферты —
register-capital-in-agreement-registry.ts.

- constants/marketplace-agreement-ids.ts — MARKETPLACE_EXTENSION_NAME='market',
  MARKETPLACE_OFFER_AGREEMENT_ID='order_table_offer',
  MARKETPLACE_AGREEMENT_TYPE='order_table',
  MARKETPLACE_OFFER_TEMPLATE_REGISTRY_ID=0 (placeholder; Story 1.7 заменит
  на реальный registry_id из платформенной document factory, по аналогии с
  Cooperative.Registry.GeneratorOffer/BlagorostOffer).
- application/registration/register-marketplace-in-agreement-registry.ts —
  чистая функция: при ID>0 зовёт port.registerAgreement с одной офертой
  (applicable_account_types: individual + entrepreneur, order: 7); при ID<=0
  возвращает false без вызовов. Programs marketplace не регистрирует —
  Стол заказов ЦПП без program-надстройки.
- marketplace-extension.module — @Inject(AGREEMENT_REGISTRATION_PORT) +
  вызов registerMarketplaceInAgreementRegistry в initialize после initBucket;
  info-лог при skip и при успешной регистрации, error при exception.

Tests:
- tests/unit/marketplace/marketplace-plugin-register.test.ts (3 кейса):
  чистая функция — port не вызван пока placeholder=0, константы согласованы,
  programs не регистрируются.
- tests/unit/marketplace/marketplace-plugin-initialize.test.ts — обновлён
  конструктор (добавлен makeAgreementPort стаб); 4 кейса Story 1.1
  продолжают проходить.

10/10 unit-тестов зелёных, tsc 0 ошибок.

Ref: blago/.../components/3-minimalnyy-produkt/_bmad-output/implementation-artifacts/spec-1-2-cpp-registry-link.md
(spec будет переписан вместе с blago-issue после merge).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(marketplace): MARKETPLACE_AGREEMENT_TYPE = 'marketplace' (валидный eosio::name)

Per review PR #370: 'order_table' содержит '_' — не валидный eosio::name
(допустимы только [.a-z1-5], max 12). В lib/consts.hpp уже определено
program-имя `_marketplace_program = "marketplace"_n` (program_id=2), его
и принимает `soviet::coagreements`/`sndagreement`.

- MARKETPLACE_AGREEMENT_TYPE: 'order_table' → 'marketplace'.
- MARKETPLACE_OFFER_AGREEMENT_ID: 'order_table_offer' → 'marketplace_offer'
  (консистентно с 'generator_offer'/'blagorost_offer' у Capital).
- AgreementRegistrationSpec.agreement_type — JSDoc уточнён: должен быть
  eosio::name (max 12, без `_`), пример обновлён.
- Тест проверяет eosio::name regex `/^[.a-z1-5]{1,12}$/` для type.

10/10 unit зелёные, tsc 0 ошибок.

---------

Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 21:39:41 +05:00
Alex Ant ab8c644ef7 feat(marketplace): Story 1.1 — установка Стола заказов из Каталога приложений (#368)
Publish Docs / build-and-publish-docs (push) Failing after 13m10s
- Подключить MarketplacePluginModule в ExtensionsModule.register (расширение
  не загружалось в NestJS DI-контейнер).
- Унифицировать имя расширения: MarketplacePlugin.name = 'market' (совпадает
  с ключом AppRegistry; install через installExtension({name:"market"}) теперь
  не падает на findByName('marketplace')).
- Расширить MarketplacePlugin.initialize: bucket coop-<coopname> через
  optional file-storage порт (@Optional injection, fallback под warn-логом до
  merge PR #359); три AC-лога backend'а — 'Создан физический бакет ...',
  'File storage готов', 'marketplace-extension готов'.
- Bootstrap config-миграция v1 для расширения market.
- Atomic rollback в ExtensionInteractor.installApp: при провале runApp снести
  запись через uninstallApp, только если её не было до install; ошибка
  пробрасывается наверх (UI Каталога показывает 'Ошибка').
- Process locator: обновить TODO p.mkt.* — заглушки готовы, активация в
  Story 4.1 при создании marketplace::requests.

Unit-тесты:
- tests/unit/marketplace/marketplace-plugin-initialize.test.ts (4 проходят).
- tests/unit/appstore/extension-interactor-install-rollback.test.ts (3 проходят).

Refs: blago/production/1-prilozhenie-stol-zakazov/components/3-minimalnyy-produkt/_bmad-output/implementation-artifacts/spec-1-1-install-from-catalog.md

Co-authored-by: coopops <coopos@coopenomics.world>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 15:50:51 +05:00
coopops 467f33ff09 Merge remote-tracking branch 'origin/dev' into marketplace2
# Conflicts:
#	components/contracts/cpp/lib/core/ledger2/wallets.hpp
#	components/cooptypes/src/ledger2/wallets.generated.ts
2026-05-14 09:14:14 +00:00
coopops d98041d44a fix stantards 2026-05-12 04:25:54 +00:00
coopops 44a0030fd6 sync(controller/process-registry): убран legacy p.mkt.reqst, заглушки под членскую модель
- process-hash-locator.ts: удалён мёртвый ключ p.mkt.reqst (привязка к legacy таблице marketplace::requests). Добавлены пустые locators для p.mkt.supply / p.mkt.return / p.mkt.wroff — реализация контракта marketplace под членскую модель ещё не написана, операции на бэкенд не прилетают. TODO-комментарий зафиксирован: при реализации использовать поле request_hash (универсальное; order_hash избегаем — пересекается с offer_hash из orderoffer-actions).
- process-registry.service.ts: убрано упоминание p.mkt.reqst из устаревшего комментария-примера мульти-операционных процессов.

Без этого фикса не пуш-критично (Phase A никогда не маппила бы operation на удалённый process_type), но мёртвый ключ создавал бы путаницу.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 18:33:33 +00:00
coopops a0194f698b sync(cooptypes/ledger2): зеркало TS-реестров под членскую модель «Стола заказов»
- accounts.ts: + 10 «Материалы», 91 «Прочие доходы и расходы».
- processes.ts: убран legacy p.mkt.reqst (REQUEST), добавлены p.mkt.supply / p.mkt.return / p.mkt.wroff.
- operations.ts: WalletOp += 'REVOKE'; legacy o.mkt.supply/recv (CONFIRM_SUPPLY/RECEIPT) заменены на 12 операций членской модели (CONVERT_TO_MEMBER, ASSIGN_TO_PROGRAM, BLOCK_FOR_ORDER, UNBLOCK_ON_CANCEL, RECALL_TO_UNIVERSAL, PURCHASE_FROM_SUPPLIER, PAY_SUPPLIER, CONSUME_BY_MEMBER + CONSUME_TRANSIT_CLOSE, RETURN_BY_MEMBER + RETURN_TRANSIT_CLOSE, WRITE_OFF_PERISHABLE + WRITE_OFF_TRANSIT_CLOSE); composite-проводки через 91 разбиты на пары *...*2.
- wallets.generated.ts: auto-regen из wallets.hpp — +w.mkt.member (program_id=2).

Без этого в standards-site новые кошельки и операции marketplace показывались «нет в реестре».

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 18:08:09 +00:00
coopops 0a8e226417 [@ant] feat(ledger2): членская модель «Стола заказов» — 12 операций marketplace, счета 10/91, кошелёк w.mkt.member, WalletOp REVOKE; удалены legacy o.mkt.supply/recv (клиринг)
- operations.hpp: enum WalletOp + REVOKE (списание blocked + обяз. Dr/Cr); удалены CONFIRM_SUPPLY/CONFIRM_RECEIPT; добавлены 12 операций членской модели — CONVERT_TO_MEMBER в operations::wallet и 11 в operations::marketplace (ASSIGN_TO_PROGRAM, BLOCK_FOR_ORDER, UNBLOCK_ON_CANCEL, RECALL_TO_UNIVERSAL, PURCHASE_FROM_SUPPLIER, PAY_SUPPLIER, CONSUME_BY_MEMBER + CONSUME_TRANSIT_CLOSE, RETURN_BY_MEMBER + RETURN_TRANSIT_CLOSE, WRITE_OFF_PERISHABLE + WRITE_OFF_TRANSIT_CLOSE); composite-проводки через транзит счёта 91 «Прочие доходы и расходы» разбиты на пары операций (часть 1 + часть 2, *2-суффикс), вызываемые последовательно в одной транзакции Antelope — атомарность через транзакцию.
- accounts.hpp: добавлены счета 10 «Материалы» (А) и 91 «Прочие доходы и расходы» (А/П).
- wallets.hpp: добавлен USER_SHARED-кошелёк w.mkt.member («ЦПП Стол Заказов — программный членский») с program_id=2 (marketplace).
- processes.hpp: удалён p.mkt.reqst (клиринговый), добавлены p.mkt.supply / p.mkt.return / p.mkt.wroff.
- p.mkt.{supply,return,wroff}.standard.yaml: composite-проводки разнесены на пары operations[]; транзит через 91 явно описан в комментариях и description (нетто-эффект — Дт 86 / Кт 10 на выдаче/утилизации и Дт 10 / Кт 86 на возврате).

Согласовано с Ангелиной 2026-05-11.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 18:08:09 +00:00
Alex Ant e9fca4138b update standart 2026-05-11 23:06:19 +05:00
coopops 2028f032bb Merge remote-tracking branch 'origin/dev' into marketplace2 2026-05-11 08:27:10 +00:00
coopops acd2a4afee feat(standards): p.reg.accept человеческим языком + регистрационный взнос; standards-site infra
- p.reg.accept.standard.yaml: прозаические поля (purpose/description/guards/pre/post) переписаны
  для НЕ-технарей — без имён экшенов, операций, кошельков, проводок, таблиц
- введён термин «регистрационный взнос» (вступительный + минимальный паевой) с раскрытием
  при первом упоминании
- standards-site/FocusBar.vue: ROLE_HUMAN += candidate → «Кандидат»
- standards-site/vite.config.ts: плагин watch-cpp-standards подключает ../cpp/ к vite watcher
  и инвалидирует loader.ts при изменении *.standard.yaml — HMR срабатывает без перезапуска

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 08:26:38 +00:00
coopops c87d6c658e Merge remote-tracking branch 'origin/ledger3' into marketplace2
# Conflicts:
#	components/contracts/cpp/capital/p.cap.rid.standard.yaml
2026-05-09 14:42:10 +00:00
coopops 93fa5c1210 refactor(standards): объединить summary+purpose — оставить только purpose
В §1 паспорта стандарта поле `summary` упразднено. Единственное прозаическое
описание процесса теперь в `purpose` — рендерится на главной странице
(line-clamp 3) и в карточке «Начало процесса». Чтобы не было соблазна писать
дублирующее «краткое описание процесса» вроде «от подписанного заявления до
карточки активного пайщика».

- 15 *.standard.yaml: блок summary удалён
- standards-site: type Standard.summary удалён, рендер в FocusBar/HomePage/StartNode/EndNode
  переключён на purpose; CSS-класс home-card__summary → home-card__purpose
- SKILL.md и a2-spetsifikatsiya: помечено что summary упразднён в v1

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:39:48 +00:00
coopops b3cb03b38b feat(standards-site): WalletOp += 'NONE' — синхронизация с cooptypes
После мержа origin/ledger3 в WalletOp добавлен NONE (внутрибалансовые
проводки без движения кошелька, например ACCEPT_RID — Dr 04 / Cr 08
без перемещения w.cap.gen).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 07:03:09 +00:00
coopops 3318b64602 Merge remote-tracking branch 'origin/ledger3' into marketplace2 2026-05-08 18:41:41 +00:00
coopops 6fed2e1af0 Merge remote-tracking branch 'origin/ledger3' into marketplace2
# Conflicts:
#	components/contracts/standards-site/src/components/FocusBar.vue
#	components/contracts/standards-site/src/types/standard.ts
2026-05-08 18:39:01 +00:00
coopops 8afcb0e0ba Merge remote-tracking branch 'origin/dev' into marketplace2 2026-05-08 05:43:55 +00:00
coopops 9dae639800 [mp2-23][@ant] refactor: убрать «Связанные стандарты» — концепция связей оказалась лишней — 15 YAML стандартов лишились блока related: и заголовка §7 (sed-truncate); из standards-site убраны: section в ProcessPage с slider'ом, computed relatedLinks, RELATION_HUMAN, relationHuman, RelatedView interface, импорты standardsIndex/RouterLink; из types/standard.ts удалены RelatedStandard, RelationKind, поле related; из graph/layout.ts — hasRelated; из EndNode.vue — prop hasRelated; SKILL.md — убрана §6 «Связи» (template и описание provides/triggers/affects); .process-page grid теперь auto / minmax(0,1fr) — рабочая зона растягивается на всю освободившуюся высоту 2026-05-04 19:28:11 +00:00
coopops 0428a9f1a1 [mp2-22][@ant] tune(standards-site): START_ZOOM 1.5 → 1.2 — отдалить стартовый кадр на ~1.2 zoom-позиции (шаг ×1.2 в Vue Flow), 1.5/1.2^1.2 ≈ 1.21 2026-05-04 19:03:57 +00:00
coopops 48a01b9deb [mp2-21][@ant] fix(standards-site): фиксированный стартовый zoom для всех стандартов — раньше считали targetZoom = fittedZoom × 4.0 (capped at 3.0), и fittedZoom зависел от bounding box графа: большие стандарты (supply с 8+ статусами) получали мелкий fitted ≈ 0.3 → старт 1.2, маленькие (wroff) — fitted ≈ 0.7 → старт 2.8 (упирался в потолок); пользователь видел разный масштаб при переходе между стандартами; теперь START_ZOOM = 1.5 константа — все стандарты открываются на одной и той же приближённости, ∅-узел всегда в центре канвы; fitView оставлен только как fallback на случай отсутствия start-узла 2026-05-04 18:58:28 +00:00
coopops f417a77d58 [mp2-20][@ant] feat(standards-site): FocusBar — отдельная колонка слева, не наложение поверх канвы — раньше панель была absolute-overlay 12px/12px от левого края процесс-графа поверх VueFlow, и центрирование активного узла на ←/→ работало по середине ВСЕЙ канвы — а это «середина» приходилась под левую панель, и узел вылезал из-под неё; теперь .process-graph = flex-row из двух соседей: .focus-panel (360px / max-width 38% / min-width 260px, левая колонка с border-right) и .process-graph__canvas (flex 1 1 0, правая зона); добавлен canvasRef отдельно от containerRef — все вычисления doFit / ensureInView идут от размеров КАНВЫ (а не всего процесс-графа), поэтому центрирование стало честным внутри видимой канвы; стартовая x-позиция 0.65 → 0.5 (середина канвы); фуллскрин по-прежнему на containerRef — раскрывает обе колонки целиком; пан/пинч 2 пальцами и колесо работают в правой канве, не дёргая страницу 2026-05-04 18:48:43 +00:00
coopops 9222fb9572 [mp2-19][@ant] feat(standards-site): включить двухпальцевый pan/zoom рабочей области — раньше pan-on-scroll/zoom-on-pinch были выключены, потому что viewpoint страницы дёргался при прокрутке колесом (мы листали страницу, а VueFlow ловил wheel-event и одновременно панорамировал); теперь страница залочена (overflow:hidden, mp2-17/18), скролл страницы невозможен в принципе — pan-on-scroll и zoom-on-pinch стали уместны: два пальца на трекпаде → панорама холста, pinch → зум; prevent-scrolling=true чтобы wheel-events точно не утекали наружу 2026-05-04 18:42:09 +00:00
coopops a3f9de9eab [mp2-18][@ant] fix(standards-site): добавить min-width:0 по цепочке grid/flex — слайдер связанных стандартов скроллится, viewport не растягивает рабочую зону — без minmax(0, 1fr) в .app-shell дочерний 1fr-трек получал implicit min-width:auto и расширялся под фактическую ширину контента (горизонтальный список карточек 320px × N), что выталкивало рабочую зону вправо за пределы экрана и прятало зум-контролы; теперь .app-shell = 240px / minmax(0,1fr), .app-main / .process-page / .process-page__workspace / .related / .graph-row все получили min-width:0 — overflow:auto в .related__list заработал, контент больше не «толкает» родителей наружу 2026-05-04 18:41:27 +00:00
coopops 3d4493e72b [mp2-17][@ant] feat(standards-site): полноэкранный layout без скролла страницы — header full-width / workspace 1fr / related-слайдер; убрать некорректный фрагмент purpose в supply.yaml — страница стандарта = grid auto/1fr/auto в 100vh app-main, скролл страницы заблокирован (overflow:hidden), 95% высоты — workspace; шапка с метой (Контракт/Процесс/Сущность/Зона/Статус) растянута в горизонтальную flex-полосу под заголовком, не прижата справа; «Связанные стандарты» — горизонтальный слайдер внизу с scroll-snap (фикс-карточки 320px, скролл колесом/жестом); ProcessGraph отвязан от calc(100vh) и растягивается через flex/grid stretch родителя; в supply.yaml вырезан второй абзац purpose («Используется, когда пайщики хотят провести сделку через кооператив...») — он не отражал реальный смысл процесса 2026-05-04 18:38:38 +00:00
coopops ef5746ac15 [mp2-16][@ant] feat(standards-site): описание процесса = карточка ∅-узла, viewport стартует в правой зоне — purpose/summary стандарта больше не отдельная секция над графом, а контент карточки FocusBar при фокусе на узле «начало процесса» (∅); FocusBar теперь раскрывается по умолчанию (фокус на ∅ — было исключено из v-if); viewport canvas: стартовый круглешок встаёт в правой зоне (cw*0.65 вместо 0.32) — слева остаётся место под FocusBar (~42% ширины) и контент не уезжает за панель; zoom +2 позиции (boost 2.8→4.0, max 2.0→3.0); рабочая область выше (min-height 520→640, height calc 240→160), потому что интро-секция убрана и осталось больше места под граф 2026-05-04 18:33:04 +00:00
coopops 609201e75f [mp2-15][@ant] refactor(standards): убрать бессмысленный f-префикс из имён операций marketplace — fconv→conv, ftarg→assign, fblock→block, funblk→unblk, fefund→recall, payspl→payout — f (от funds) не нёс информации и путал чтение, новые имена односложные и говорят сами за себя; зеркальные пары: assign↔recall (целевое назначение / отзыв), block↔unblk (unblock=13 не помещается в eosio::name); правки только в стандартах + sister-doc архитектуры — operations.ts/hpp ещё не содержат этих кодов (войдут одной chunk-PR при формализации стандарта с командой) 2026-05-04 18:25:13 +00:00
coopops 1fdbbba22e [mp2-14][@ant] fix(standards): o.wal.fconv — двусторонний L3 на двух USER_SHARED одного пайщика — конвертация паевого в членский = перевод между двумя USER_SHARED одного и того же пайщика, не выпуск; должно быть -X на available паевого (w.wal.share) и +X на available членского (w.wal.member); правка в standard и в SKILL.md-примере (там же переименован устаревший o.mkt.fconv в актуальный o.wal.fconv) 2026-05-04 18:14:42 +00:00
coopops 8188c83ca1 [mp2-13][@ant] docs(standards): переписать ВСЕ описания (actions.purpose, states.description, scenario.steps) простым языком — стандарт читают аналитик/председатель/пайщик/бухгалтер, не разработчик; не дублируем ledger2-операции / кошельки / проводки в прозу — они уже видны структурно в карточках L1/L2/L3 и графе состояний; правило расширено в SKILL.md и feedback memory с summary/purpose на ВСЕ прозаические поля стандарта 2026-05-04 18:07:09 +00:00
coopops dc57d280cb [mp2-12][@ant] refactor: customer/supplier → orderer/offerer + L1/L2/L3-карточки во всю ширину — кооперативный пайщик может быть и заказчиком и поставщиком (обратная поставка), нейтральные orderer/offerer держат симметрию ролей; standards-site рендерит каждую под-карточку (Проводки / Кошельки кооператива / Кошелёк пайщика / Сумма) отдельной строкой во всю ширину вместо grid auto-fit, где L1/L2 зажимались в узкую колонку 2026-05-04 17:58:26 +00:00
coopops fdc0035954 [mp2-11][@ant] feat(standards): двусторонний L3 + игнор Дт=Кт same-account — расширяем схему Ledger2Operation массивом l3[] для случаев перевода между двумя USER_SHARED одного пайщика (классика — w.wal.member ↔ w.mkt.member); FocusBar рендерит каждое движение отдельной строкой; supply ftarg/fefund переведены на массив l3 (-X на источнике, +X на получателе) + L1 проводки Дт 86 / Кт 86 убраны (нетто-нулевое изменение по 86 — смарт-контракт игнорирует, аналитика «по программе» фиксируется на L2/L3) 2026-05-04 17:50:56 +00:00
coopops 0f41f0d9d4 [mp2-10][@ant] feat(standards): трёхступенчатая модель членских взносов в marketplace — universal w.wal.member + programmatic w.mkt.member, серия createorder = o.wal.fconv → o.mkt.ftarg → o.mkt.fblock; cancel/return оставляют средства на w.mkt.member.available, fefund переопределён как явный вывод в общий членский кошелёк 2026-05-04 17:34:46 +00:00
coopops 28d4583a15 [mp2-9][@ant] feat(standards-site): уровень 3 кошельков пайщика, UX рабочей области и happy-path навигация
Учётная модель ledger2 теперь выражается в стандарте на трёх уровнях:
L1 — бухгалтерские проводки (debit/credit), L2 — кошельки кооператива
(wallet_from → wallet_to, агрегаты), L3 — кошелёк пайщика (per-user split
available/blocked: user_wallet, user_ref, available_delta, blocked_delta).
В types/standard.ts добавлены поля под L3, расширен enum WalletOp до полного
набора _blago (BURN/REVOKE/ACCOUNT_ONLY); FocusBar показывает три отдельные
карточки «Проводки / Кошельки кооператива / Кошелёк пайщика» с условным
показом — пустой уровень не выводится; L3-карточка тянется на всю ширину
op-сетки и идёт горизонтальной строкой (wallet · user · Доступно · Заблокировано),
лейблы по-русски. На прозрачные --soft-варианты фона добавлен override на
непрозрачный --bg, чтобы overlay-панель не светила сквозь себя.

Рабочая область процесса доработана: FocusBar переехал из нижней горизонтальной
полосы в левую вертикальную панель (правый верх занят зум-контролами VueFlow,
куда добавлена кнопка Maximize/Minimize2 для fullscreen-режима через нативный
Fullscreen API + автo-фит при смене размера); стартовый зум усилен (boost
2.0 → 2.8 ≈ +3 нажатия «+»). В fullscreen появляются дублирующие prev/next
оверлеями у нижнего края (внешние ◀/▶ остаются за экраном). Навигация по ←/→
теперь идёт по happy-path: pickHappyTransition в graph/layout.ts штрафует
имена-отказы (cancel/decline/expire/reject/abort/abandon/fail/revoke/discard)
и legacy actions[].role === 'reject', штрафует самопетли (from === to),
бонус за target-статус с продолжением; tie-break — порядок YAML.

Vite dev: cooptypes (workspace ESM-бандл ~2 МБ) добавлен в optimizeDeps.include —
без этого vite отдавал его через @fs трансформ-конвейером (раздувался до 10 МБ),
по сети валился dynamic import ProcessPage с «Failed to fetch dynamically
imported module». Pre-bundled chunk теперь 987 КБ.

Пилот L3 на p.mkt.supply.standard.yaml (createorder + полный жизненный цикл):
переименовано w.mkt.fund → w.mkt.member по всему файлу (33 случая в supply +
return — это per-user членский кошелёк программы Marketplace, fund-наименование
вводило в заблуждение); o.mkt.fconv (TRANSFER) получил L3 +order.total_cost
на available заказчика, o.mkt.fblock (BLOCK) — only-L3 split −available/+blocked,
o.mkt.funblk (UNBLOCK) — зеркало, o.mkt.fefund (TRANSFER) — −available на возврат,
o.mkt.consum (REVOKE) — only-L3 −blocked + L1 Дт 86/Кт 10. Также введён
промежуточный статус ship_ready («Отгрузка собрана») между accepted и
supply_prepared: prepship больше не самопетля accepted→accepted, а accepted→
ship_ready, signsupp идёт от ship_ready → supply_prepared — линейный поток
без обратных стрелок dagre.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 17:21:23 +00:00
coopops 5ab5234507 [mp2-8][@ant] docs(standards): summary/purpose трёх стандартов marketplace переписаны простым языком — описание в шапке должно быть для председателя/пайщика, не для разработчика, отвечать на «что/зачем/когда»; идентификаторы кошельков (w.mkt.fund), имена операций (o.mkt.purch), проводки (Дт 10/Кт 86), Backend/крон/compensating forward — всё это уже расписано дальше по странице, в шапке только смысл процесса 2026-05-04 15:19:40 +00:00
coopops 75d4fff416 [mp2-7][@ant] chore: empty commit для перетриггера publish-docs на marketplace2 — coopenomics-docs теперь клонирует именно эту ветку, нужно ещё одна сборка чтобы реестр стандартов на docs.coopenomics.world подхватил три новых yaml (p.mkt.supply/return/wroff) 2026-05-04 14:59:03 +00:00
coopops 7da8ddec71 Merge branch 'marketplace2' of github.com:coopenomics/mono into marketplace2 2026-05-04 14:45:20 +00:00
coopops 1325943601 Merge branch 'reports' into marketplace2 2026-05-04 14:44:24 +00:00
coopops 2f9495ff83 [598-3][@ant] feat(standards): 3 стандарта Marketplace MVP под членскую модель
Добавлены кооперативные стандарты для процессов контракта marketplace
в режиме членских взносов (упрощённая модель с одним программным
кошельком w.mkt.fund, без транзитов):

  • p.mkt.supply.standard.yaml   — Прямая поставка-приобретение имущества
                                    (7 операций ledger2, двухвариантная приёмка А/Б,
                                     двойные подписи АПП приёмки и АПП выдачи)
  • p.mkt.return.standard.yaml   — Гарантийный возврат имущества
                                    (compensating forward o.mkt.return,
                                     без ledger2::revert)
  • p.mkt.wroff.standard.yaml    — Утилизация скоропорта
                                    (ACCOUNT_ONLY с транзитом через счёт 91)

Все wallet_from/wallet_to используют eosio::name-строки
(w.mkt.fund, w.wal.share, w.mkt.payout) согласно рефакторингу 2026-04-27
на ветке reports. Документы привязаны к существующим шаблонам:
  • АПП приёмки = registry_id 702 (AssetContributionAct)
  • АПП выдачи  = registry_id 802 (ReturnByAssetAct)
  • Заявление возврата = registry_id 800 (ReturnByAssetStatement)
  • ТТН и протокол списания = 0 + TODO (нет шаблонов в реестре).

Все операции имеют статус proposed; имена ≤12 chars eosio::name.
Бухгалтерские проводки помечены TBD-Standardization для последующей
сверки с Ангелиной/Игорем.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 14:44:14 +00:00
coopops 638824d9c7 [598-3][@ant] chore(standards): удалить устаревший p.mkt.reqst.standard.yaml
Стандарт описывал клиринговую модель donor'а (паевый взнос имуществом
+ возврат паевого взноса имуществом). На ветке marketplace2 заменён
тремя новыми стандартами под членскую модель Стол заказов MVP:
  • p.mkt.supply.standard.yaml   — Прямая поставка-приобретение имущества
  • p.mkt.return.standard.yaml   — Гарантийный возврат имущества
  • p.mkt.wroff.standard.yaml    — Утилизация скоропорта

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 14:09:37 +00:00
coopops c96518d4b3 Merge branch 'reports' into marketplace2 2026-05-04 11:27:16 +00:00
coopops 3a3144a8c3 Merge branch 'reports' into marketplace2 2026-05-04 11:24:36 +00:00
coopops d29a9d624f [598-3][@ant] refactor: унифицировать cooplace → marketplace по всему стеку
Цель — убрать дублирующее имя 'cooplace' в пользу единого 'marketplace'
во всех слоях (controller, sdk, desktop). Имя 'cooplace' было артефактом
переходного периода между плагин-стилем (старая marketplace ветка) и
extension-стилем (feat/marketplace-orders); оставалось 4 параллельных
каталога с одной семантикой.

Каталоги (git mv с историей):
- controller/src/application/cooplace → application/marketplace
- controller/src/domain/cooplace → domain/marketplace
- controller/src/extensions/cooplace → extensions/marketplace-cards
  (отдельная extension со своей сферой ProductCard/Category/SupplyOrder,
   именно поэтому marketplace-cards, а не marketplace — чтобы не
   коллизировать с extensions/marketplace, который про request/attribute)
- sdk/src/mutations/cooplace → mutations/marketplace
- sdk/src/selectors/cooplace → selectors/marketplace

Файлы:
- cooplace.module.ts → marketplace.module.ts (в application и domain)
- cooplace.service/interactor/resolver.ts → marketplace.*
- cooplace-extension.module.ts → marketplace-cards.module.ts
- cooplace-blockchain.port/adapter.ts → marketplace-blockchain.*

Классы:
- CooplaceModule → MarketplaceModule
- CooplaceDomainModule → MarketplaceDomainModule
- CooplaceExtensionModule → MarketplaceCardsModule
- CooplaceInteractor/Resolver/Service → Marketplace*
- CooplaceBlockchainPort/Adapter → MarketplaceBlockchainPort/Adapter
- COOPLACE_BLOCKCHAIN_PORT (Symbol) → MARKETPLACE_BLOCKCHAIN_PORT
- camelCase: cooplaceInteractor/Service/BlockchainPort → marketplace*

Чтобы избежать коллизии класс-имён в extensions/marketplace
(где уже были MarketplaceDomainModule и MarketplaceApplicationModule
для catalog-слоя), переименовал их:
- MarketplaceDomainModule → MarketplaceExtensionDomainModule
- MarketplaceApplicationModule → MarketplaceExtensionApplicationModule
Эти классы видны только внутри extensions/marketplace, наружу торчит
только MarketplacePluginModule + MarketplacePlugin.

SDK re-exports:
- mutations/index.ts: 'export * as Cooplace' → 'export * as Marketplace'
- selectors/index.ts: 'export * from ./cooplace' → './marketplace'

Сборки после изменений:
- controller tsc --noEmit: EXIT=0
- desktop vue-tsc --noEmit --skipLibCheck: EXIT=0
- контракт marketplace не затронут (cooplace там никогда не было)
2026-04-25 12:57:07 +00:00
coopops cfd98f780a [mp2-5][@ant] fix(desktop): vue-tsc зелёный после трансплантации
1. Убран лишний import { withDefaults } from 'vue' из 9 файлов:
   - features/Request/CreateChildOrder/ui/CreateChildOrderButton.vue
   - widgets/Marketplace/SupplyOrderRequestCard/ui/Steps/{First..Eighth}Step.vue
   В Vue 3 withDefaults — compiler macro, доступен глобально без импорта,
   явный import конфликтует с локальным declaration (TS2440).

2. widgets/Marketplace/SupplyOrderRequestCard/ui/Base/Base.vue —
   request.membership_fee_amount → request.membership_fee
   (поле в IRequestData/SDK называется membership_fee).

vue-tsc --noEmit --skipLibCheck: 0 ошибок.
2026-04-25 11:34:00 +00:00
coopops cdddea93b5 [mp2-4][@ant] feat: подцепить marketplace extensions в registries
controller:
- app.module.ts — импорт MarketplacePluginModule + CooplaceExtensionModule в imports
- extensions/extensions.registry.ts — заменить заглушку 'orders' на реальный
  entry 'market' с MarketplacePluginModule + MarketplacePlugin + MarketplaceSchema

desktop:
- src/processes/init-installed-extensions/extensions-registry.ts —
  импорт marketInstall из extensions/market/install и регистрация ключа 'market'

tsc --noEmit (controller): EXIT=0
2026-04-25 11:24:24 +00:00
coopops dd2220b299 [mp2-3][@ant] fix(controller): tsc --noEmit зелёный после трансплантации
1. extensions/marketplace/* — заменить пути:
   - ~/modules/auth/* → ~/application/auth/*
   - ~/modules/logger/logger-app.service → ~/application/logger/logger-app.service
   На reports эти каталоги называются application/, не modules/.

2. extensions/marketplace/marketplace-extension.module.ts — убрать
   ExtensionPortsModule (на reports его нет, capital его не использует —
   это артефакт marketplace-orders без поддержки на текущей базе).

3. infrastructure/blockchain/adapters/cooplace-blockchain.adapter.ts —
   взять с feat/marketplace-orders: добавлены 5 методов
   (reqReturn, coopstock, acceptStock, destroy, reoffer), которые
   требует CooplaceBlockchainPort.

cooptypes пересобран (pnpm -C cooptypes build) — Ledger2/Ledger2Contract
теперь экспортируются.

tsc --noEmit: EXIT=0
2026-04-25 11:22:51 +00:00
coopops 7c75408348 [mp2-2][@ant] fix(marketplace): добиться сборки контракта в режиме :test после трансплантации
Поправки на reports-base, чтобы новые actions из marketplace-orders
скомпилировались:

1. lib/core/document.hpp — добавлен Document::remove_document(docs, name)
   (вызывается в reqreturn.cpp; в исходном marketplace-orders отсутствовал —
   значит контракт там и не собирался).

2. lib/domain/table_marketplace_requests.hpp — добавлены поля request:
   - delivery_type (internal/external)
   - contribution_type (share/member)
   Эти поля устанавливаются в orderoffer/createorder и используются в
   проводках complete (см. MARKET-LOGIC.md, Правила 3 и 4).

3. marketplace.hpp — переключён include с ../lib/common.hpp на ../lib/index.hpp
   (на reports common.hpp удалён рефактором 1744fab580).

Сборка: bash build.sh marketplace test → marketplace.wasm OK.
2026-04-25 11:14:29 +00:00
coopops 9bf899ec2d [mp2-1.1] chore: убрать случайно добавленный current_project.md 2026-04-25 11:10:37 +00:00
coopops 6ef93b19b9 [mp2-1][@ant] feat(marketplace2): трансплантировать marketplace из feat/marketplace-orders
Перенесено с origin/feat/marketplace-orders (commit 585afb16eb):
- contracts/cpp/marketplace/* — новые actions deliver_on_order/{createorder,respondoffer},
  deliver_on_offer/{acceptstock,coopstock,destroy,reoffer,reqreturn}
- cooptypes/contracts/marketplace/* — типы новых actions
- sdk/mutations/cooplace/* — disputeOnRequest и др.
- controller/extensions/marketplace (87 файлов) + extensions/cooplace (split) — НОВЫЕ
- controller/application/cooplace + domain/cooplace — обновлены под новые actions
- desktop/pages/Marketplace/{DisputePage,ShipmentsPage,WarehousePage} — НОВЫЕ
- desktop/widgets/Marketplace/SupplyOrderRequestCard/ui/Steps/{ReqReturnStep,RetAuthorizedStep}
- desktop/features/Request — обновлены
- desktop/extensions/market — НОВОЕ
- boot/tests/marketplace.test.ts — НОВОЕ
- MARKET-LOGIC.md — описание бизнес-процессов

Поправлен include marketplace.hpp: ../lib/common.hpp → ../lib/index.hpp
(на reports common.hpp удалён, заменён на index.hpp).

shared_marketplace.hpp НЕ переносим — на reports его содержимое размазано
по lib/core/marketplace/marketplace.hpp + lib/domain/table_marketplace_*.hpp

Сборка пока не проверена — следующий шаг.
2026-04-25 11:10:29 +00:00
1966 changed files with 137013 additions and 41772 deletions
+1
View File
@@ -0,0 +1 @@
{"sessionId":"5b149d17-5cbc-4201-8975-a9def97637b9","pid":19728,"procStart":"44281","acquiredAt":1776935929080}
+3
View File
@@ -1,4 +1,5 @@
node_modules/
node_modules
lerna-debug.log
components/controller/graph.png
blockchain-data/
@@ -22,3 +23,5 @@ components/contracts/cpp/ledger2/scripts/out/
.env.testnet
.pnpm-store
schema.gql
.claude
+60 -1
View File
@@ -62,6 +62,30 @@ Workflow:
**Анти-паттерн:** worktree от `feat/1-2-...`, потом от `feat/1-3-...`, и каждый PR в `marketplace2`. Цепочка branches правильная (изоляция), но цепочка PR — нет. Кейс Эпика 1 Стола заказов 2026-05-14: 11 PR `#370-#380` подряд от `marketplace2`, каждый +N stories назад. Пользователь дошёл до review #372 и обнаружил дубли. Закрыл #372-#379, оставил только #380.
## Сборка контрактов на ARM-машине
Скрипты `components/contracts/build-all.sh` и `build.sh` хардкодят образ
`dicoop/blockchain:latest`, который опубликован **только под `linux/amd64`**
на ARM (Raspberry Pi 5, Apple Silicon без qemu-binfmt и пр.) `docker run` падает
с `exec /bin/bash: exec format error` сразу после старта контейнера.
**Для ARM использовать multi-arch образ `dicoop/blockchain_v5.1.1:dev`**
у него тег `dev` указывает на manifest list с нативными `dev-arm64` и
`dev-amd64` под капотом. Запуск:
```bash
cd components/contracts
rm -rf build && mkdir build
docker run --rm --name cdt \
--volume "$(pwd)/:/project" \
-w /project/build \
dicoop/blockchain_v5.1.1:dev \
/bin/bash -c "cmake -DBUILD_TARGET='marketplace' -DTEST_TARGET= -DVERBOSE=ON -DBUILD_TESTS=OFF -DIS_TESTNET=OFF .. && make -j2"
```
Замена образа в build-скриптах на ARM — отдельный фикс (этот файл документирует
рабочий обход, пока скрипты не перевели на multi-arch манифест глобально).
## Локальные тесты
**Не запускать полный jest локально** ни в mono-ai-1, ни в mono-ai-4: живой dev-стек в docker (`nodeos`, `controller dev` nodemon, `parser dev`, `n8n`) вешает CPU/RAM и блокирует chain. Полный suite — задача CI после push'а PR.
@@ -72,7 +96,7 @@ Workflow:
pnpm jest tests/unit/marketplace/marketplace-onboarding-service.test.ts --runInBand
```
`pnpm generate-schema` / `pnpm generate-client`**не запускать локально**; та же memory/CPU полка вешает контейнер controller'а. Либо CI, либо пользователь сам когда контейнер остановлен.
`pnpm generate-schema` / `pnpm generate-client`**МОЖНО и НУЖНО запускать когда требуется** (разрешено пользователем явно). Любое изменение GraphQL-сигнатуры резолвера или типов контрактов обязано сопровождаться полным циклом регенерации (generate-schema → generate-client → sdk build, при правках cooptypes — ещё и cooptypes build), чтобы desktop получил актуальные типы. Не оставлять заглушку «до regen».
Перед коммитом достаточно `tsc --noEmit` (быстрый, не блокирует).
@@ -86,6 +110,12 @@ pnpm jest tests/unit/marketplace/marketplace-onboarding-service.test.ts --runInB
**Не дёргать `Mutations.Auth.Login` напрямую**`LoginInput` ждёт `{email, now, signature}`, генерация подписи внутри SDK Client. Refresh: `Mutations.Auth.Refresh.mutation` с `{access_token, refresh_token}`. Канон используется в `blago-cli/src/session/index.ts` (loginInteractive) и в EMP-коннекторе `connectors/cooperative-tsk-login-connector` (Story 11.5).
## DRY — любое 2-кратное повторение выносится в общее (ОБЯЗАТЕЛЬНО)
Любой кусок кода (валидация, маппинг, guard, построение payload, helper-логика), повторённый **второй раз**, обязан быть вынесен в общее: `shared/`-helper / util / базовый класс (controller) или соответствующий FSD-слой `shared/` (desktop). Это **обязательное правило**, не рекомендация — не «то тут то там стряпать одно и то же».
**Триггер:** заметил второе вхождение → сразу выноси, не копируй. Применяется и в controller, и в desktop. (Зафиксировано пользователем в ревью PR #17 «Стол заказов».)
## Backend (controller) каноны
### 3 базовые роли — User / Member / Chairman
@@ -184,6 +214,35 @@ await sendPOST('/v1/graphql', { query: QUERY, variables });
Канон — `features/Branch/CreateBranch/{api,model}/index.ts`: тип в model `export type IXxxInput = Mutations.X.Y.IInput['data']`; функция в api `function (data: IXxxInput) { ... variables: { data } }`. Не делать `as` cast'ов.
## ⚠️ ДИЗАЙН-КАНОН desktop — ОБЯЗАТЕЛЕН К ИСПОЛЬЗОВАНИЮ
**ПРИ ЛЮБОЙ ВЁРСТКЕ В `components/desktop/` НЕ ВЫДУМЫВАТЬ СТИЛИ.** Всегда сначала свериться с каноном. Не строй гипотез о цветах/радиусах/типографике/паттернах из памяти — иди и читай.
**Полная спецификация канона — в skill `/mono-desktop-canon`** (правила обёрток, цвета/токены, иконки, структура страницы, stop-signals). Здесь — короткая выжимка; при расхождении побеждает skill.
**Источник истины — в самом репозитории** (НЕ внешние HTML/прототипы):
| Что | Путь |
|---|---|
| Токены палитры/типографики/spacing/radii (`--p-*`) | `components/desktop/src/css/mono-platform/tokens.css` |
| Прототип-классы компонентов + typography-утилиты `.t-*` | `components/desktop/src/css/mono-platform/components.css` |
| Оверрайды Quasar под канон | `components/desktop/src/css/mono-platform/quasar-canon.css` |
| Минимальная SCSS-палитра Quasar (`$primary`/`$dark`) | `components/desktop/src/css/quasar.variables.scss` |
| Обёртки / доменные блоки / layout | `components/desktop/src/shared/ui/{base,domain,layout}/` |
| **Живой эталон** (`/_dev/ui` в dev-сборке) | `components/desktop/src/pages/_dev/ui/index.vue` |
**При сомнении — открыть `tokens.css` и `_dev/ui/index.vue`, смотреть как сделано там.** Внешний `shared/MONO Design System.html` и `auth-prototype/` каноном НЕ являются — это устаревшие прототипы.
**Запреты:**
- Экран собирается из готовых компонентов: `shared/ui/base` (вместо сырых `q-input`/`q-btn`/`q-card`/`q-table`/`q-chip`/`q-dialog`/`q-select`), `shared/ui/domain` (WalletCard, DataRow, DocumentRow, IdentityPanel…), `shared/ui/layout` (PageHead, PageTabs, AppHeader/AppDrawer). Голый Quasar — только где обёртки нет (`q-icon`, `q-toggle`, `q-list`, `q-menu`, `q-tooltip`, `q-tabs`, `q-separator`, `q-spinner`, `q-inner-loading`…). Props обёрток не угадывать — читать `*.types.ts` рядом.
- Цвет — только токены `var(--p-*)` (поверхности `--p-surface*`, текст `--p-ink*`, линии `--p-line*`, акцент `--p-primary`, статусы `--p-pos/neg/warn/info`) либо utility-классы/color-props. Никаких сырых hex/rgb. Темы light/dark переключаются через `[data-theme]` на `<html>` — токены следуют сами.
- Spacing/радиусы/типографика — токены `--p-1..--p-10` (4px…72px), `--p-r-sm/md/lg/xl`, `--p-fs-*`/`--p-lh-*` либо классы `.t-*`. Без хардкод-px.
- Иконки — `q-icon(name='…')` именами Material Icons. **FontAwesome (`fa-*`) запрещён** — заменять на Material-эквивалент попутно.
- Запрещённые/удаляемые токены: `--mp-*` (старый `marketplace-tokens.scss`), `--tr-*`, `--ds-*`. Легитимны только `--p-*`, `--q-*` (Quasar автоген), `--prog-*`.
- Никаких локальных переопределений `.q-btn`/`.q-card`/`.q-notification` в feature-файлах — Quasar-overrides централизованы в `quasar-canon.css`.
**Кейс 2026-05-28:** при сомнении в каноне пошёл искать его во внешнем HTML/auth-prototype вместо репозитория — перевёрстал не туда, переделывал. SoT — репо (`tokens.css` + `_dev/ui`) и skill `/mono-desktop-canon`, не внешние файлы.
## Frontend desktop — English имена
В `components/desktop/` и любом Vue/TS frontend коде **все имена идентификаторов — английские**:
+337
View File
@@ -0,0 +1,337 @@
# 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В
### Бизнес-правила и ограничения
**Правило 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: Карточки в БД до match**
- **Описание:** Карточки хранятся в PostgreSQL (draft → moderation → published). В блокчейн уходят только при match
- **Применение:** Все процессы создания заявок
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@coopenomics/blago-cli",
"version": "2026.5.25-3",
"version": "2026.5.30-alpha-2",
"description": "CLI синхронизации артефактов Благорост с бэкендом через @coopenomics/sdk",
"type": "module",
"private": true,
+2 -2
View File
@@ -362,9 +362,9 @@ export async function runCli(argv: string[]): Promise<void> {
)
.action(async (paths: string[]) => {
const root = requireRoot()
const { stagedPaths, skippedUnchanged, skippedIgnored, skippedPullOnlyArtifacts } = await runAdd(root, paths)
const { stagedPaths, skippedUnchanged, skippedIgnored, skippedPullOnlyArtifacts, skippedNonEntity } = await runAdd(root, paths)
success(
`Staging обновлён: в списке ${stagedPaths.length} путь(ей). Пропущено без изменений относительно индекса: ${skippedUnchanged}; по .blagoignore: ${skippedIgnored}; артефакты только pull (messages/ и meetings/): ${skippedPullOnlyArtifacts}.`,
`Staging обновлён: в списке ${stagedPaths.length} путь(ей). Пропущено без изменений относительно индекса: ${skippedUnchanged}; по .blagoignore: ${skippedIgnored}; артефакты только pull (messages/ и meetings/): ${skippedPullOnlyArtifacts}; не blago-сущности (нет type=project/issue/story во frontmatter): ${skippedNonEntity}.`,
)
})
+16
View File
@@ -38,6 +38,22 @@ export function parseBlagoMarkdown(raw: string): ParsedBlagoFile {
}
}
/** Не бросает: возвращает тип сущности blago, если файл — корректный project/issue/story; иначе undefined. */
export function peekBlagoEntityType(raw: string): EntityFrontmatterType | undefined {
let file: ReturnType<typeof matter>
try {
file = matter(raw)
}
catch {
return undefined
}
const t = file.data?.type
if (t === 'project' || t === 'issue' || t === 'story') {
return t
}
return undefined
}
export function serializeBlagoMarkdown(data: Record<string, unknown>, body: string): string {
const trimmed = body.endsWith('\n') ? body : `${body}\n`
// matter.stringify(string, data) сначала парсит string как frontmatter — body, начинающийся
+40 -11
View File
@@ -3,6 +3,7 @@
import * as fs from 'node:fs/promises'
import * as path from 'node:path'
import { peekBlagoEntityType } from '../format/index.js'
import { sha256Hex } from '../lib/hash.js'
import { expandBlagoUserTargetsToRelativePaths } from './capital-target-expand.js'
import { isBlagoSyncExcludedDirName, isIgnoredRelativePath, loadBlagoIgnoreRules } from './ignore.js'
@@ -28,19 +29,39 @@ async function collectMarkdownFiles(absDir: string): Promise<string[]> {
return out
}
/** true — файл стоит поставить в staging: нет в индексе или SHA256 на диске ≠ content_etag_local. */
async function isDirtyVsIndex(root: string, rel: string, index: Awaited<ReturnType<typeof loadIndex>>): Promise<boolean> {
const entry = findByRelativePath(index, rel)
if (!entry) {
return true
}
type FileCheck =
| { kind: 'unreadable' }
| { kind: 'non-entity' }
| { kind: 'clean' }
| { kind: 'dirty' }
/**
* Решение по одному файлу для staging:
* — нечитаемый → dirty (push сам сообщит вменяемую ошибку),
* — без корректного frontmatter type → non-entity (README/notes/CLAUDE/AGENTS и пр. не отправляем),
* — есть в индексе и sha256 совпал → clean,
* — иначе dirty.
*/
async function classifyFileForAdd(
root: string,
rel: string,
index: Awaited<ReturnType<typeof loadIndex>>,
): Promise<FileCheck> {
let raw: string
try {
const raw = await fs.readFile(path.join(root, rel), 'utf8')
return sha256Hex(raw) !== entry.content_etag_local
raw = await fs.readFile(path.join(root, rel), 'utf8')
}
catch {
return true
return { kind: 'unreadable' }
}
if (peekBlagoEntityType(raw) === undefined) {
return { kind: 'non-entity' }
}
const entry = findByRelativePath(index, rel)
if (!entry) {
return { kind: 'dirty' }
}
return sha256Hex(raw) === entry.content_etag_local ? { kind: 'clean' } : { kind: 'dirty' }
}
export interface RunAddResult {
@@ -50,6 +71,8 @@ export interface RunAddResult {
skippedIgnored: number
/** messages/ и meetings/ — только синхронизация с сервера, в push не идут */
skippedPullOnlyArtifacts: number
/** Файлы без корректного blago-frontmatter (README, notes, CLAUDE/AGENTS и пр.) — в staging не попадают */
skippedNonEntity: number
}
export async function runAdd(root: string, targets: string[]): Promise<RunAddResult> {
@@ -65,6 +88,7 @@ export async function runAdd(root: string, targets: string[]): Promise<RunAddRes
let skippedUnchanged = 0
let skippedIgnored = 0
let skippedPullOnlyArtifacts = 0
let skippedNonEntity = 0
for (const t of expanded) {
const abs = path.resolve(root, t)
@@ -86,7 +110,12 @@ export async function runAdd(root: string, targets: string[]): Promise<RunAddRes
skippedPullOnlyArtifacts += 1
continue
}
if (!(await isDirtyVsIndex(root, rel, index))) {
const check = await classifyFileForAdd(root, rel, index)
if (check.kind === 'non-entity') {
skippedNonEntity += 1
continue
}
if (check.kind === 'clean') {
skippedUnchanged += 1
continue
}
@@ -96,5 +125,5 @@ export async function runAdd(root: string, targets: string[]): Promise<RunAddRes
const stagedPaths = [...set].sort()
await saveStaging(root, { paths: stagedPaths })
return { stagedPaths, skippedUnchanged, skippedIgnored, skippedPullOnlyArtifacts }
return { stagedPaths, skippedUnchanged, skippedIgnored, skippedPullOnlyArtifacts, skippedNonEntity }
}
+43 -3
View File
@@ -6,7 +6,7 @@ import * as fs from 'node:fs/promises'
import * as path from 'node:path'
import { Mutations, Queries } from '@coopenomics/sdk'
import { parseBlagoMarkdown } from '../format/index.js'
import { parseBlagoMarkdown, peekBlagoEntityType } from '../format/index.js'
import { sha256Hex } from '../lib/hash.js'
import { effectiveParentHash } from '../lib/parent-hash.js'
import { warn } from '../ui/output.js'
@@ -58,6 +58,30 @@ export async function runPush(ctx: AuthenticatedContext): Promise<void> {
}
staging = await loadStaging(ctx.root)
}
// Не-сущности (README/notes/CLAUDE/AGENTS и пр., в .md без type=project|issue|story) тоже не должны валить весь push.
// Чистим их автоматически — иначе одна забытая заметка блокирует отправку реальных правок.
const stagingAbs = (p: string): string => path.join(ctx.root, normalizeRelativePath(p))
const nonEntityInStaging: string[] = []
for (const p of staging.paths) {
try {
const raw = await fs.readFile(stagingAbs(p), 'utf8')
if (peekBlagoEntityType(raw) === undefined) {
nonEntityInStaging.push(normalizeRelativePath(p))
}
}
catch {
// нечитаемый файл — пусть основной цикл бросит понятную ошибку с путём
}
}
if (nonEntityInStaging.length > 0) {
const drop = new Set(nonEntityInStaging)
const kept = staging.paths.filter(p => !drop.has(normalizeRelativePath(p)))
await saveStaging(ctx.root, { paths: [...new Set(kept.map(p => normalizeRelativePath(p)))].sort() })
for (const p of nonEntityInStaging) {
warn(`Убрано из staging (нет blago-frontmatter type=project|issue|story): ${p}`)
}
staging = await loadStaging(ctx.root)
}
if (staging.paths.length === 0) {
throw new Error('Нечего отправлять. Добавьте файлы: blago add <путь | id проекта | projectId-issueId>')
}
@@ -75,8 +99,24 @@ export async function runPush(ctx: AuthenticatedContext): Promise<void> {
}
const abs = path.join(ctx.root, n)
const raw = await fs.readFile(abs, 'utf8')
const parsed = parseBlagoMarkdown(raw)
const { type, hash } = validateParsedForPush(parsed)
let parsed: ReturnType<typeof parseBlagoMarkdown>
try {
parsed = parseBlagoMarkdown(raw)
}
catch (err) {
// Без префикса путём из staging вылезает «type: undefined» без указания файла.
const msg = err instanceof Error ? err.message : String(err)
throw new Error(`Файл «${n}»: ${msg}`)
}
let type: ReturnType<typeof validateParsedForPush>['type']
let hash: string
try {
({ type, hash } = validateParsedForPush(parsed))
}
catch (err) {
const msg = err instanceof Error ? err.message : String(err)
throw new Error(`Файл «${n}»: ${msg}`)
}
const entry = findByHash(index, type, hash)
const pKind = pendingKindForEntityType(type)
const pending = pKind ? await findPendingForParsed(ctx.root, pKind, hash) : undefined
+2 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@coopenomics/boot",
"type": "module",
"version": "2026.5.25-3",
"version": "2026.5.30-alpha-2",
"private": true,
"packageManager": "pnpm@9.0.6",
"description": "CLI-утилита инициализации блокчейна и кооператива",
@@ -24,6 +24,7 @@
"boot:clean": "esno src/index.ts boot:clean",
"boot:extra": "esno src/index.ts boot:extra",
"boot:remote": "esno src/index.ts boot:remote",
"seed:marketplace-ku": "esno src/scripts/seed-marketplace-ku-details.ts",
"reboot:clean": "cd components/boot/scripts && bash clean_reboot.sh",
"reboot:extra": "cd components/boot/scripts && bash extra_reboot.sh",
"create-coop": "esno src/index.ts create-coop",
+11 -8
View File
@@ -10,20 +10,23 @@ if [ -f "$ROOT_DIR/.env" ]; then
set +a
fi
# Останавливаем и удаляем контейнеры вместе с volumes
echo "Останавливаем и удаляем контейнеры с volumes..."
docker compose down -v mongo postgres monoredis cooparser coopback || true
# Останавливаем и удаляем контейнеры (сеть НЕ трогаем — к ней может быть
# подключён внешний контейнер вроде provider-backend; down её снёс бы с ошибкой).
echo "Останавливаем и удаляем контейнеры..."
docker compose rm -fsv mongo postgres monoredis cooparser coopback || true
# Останавливаем blockchain контейнер перед удалением данных
echo "Останавливаем blockchain контейнер..."
docker compose stop node || true
# Удаляем blockchain data.
# Контейнерный wipe (alpine под root) стирает данные независимо от их владельца —
# без sudo на любой ноде (на Pi нет passwordless sudo; на проде nodeos пишет
# данные под root). Единый способ с reboot.sh / extra_reboot.sh.
# Удаляем тома баз данных для чистого старта (named volumes rm -v не чистит)
echo "Удаляем тома баз данных..."
PROJECT="${COMPOSE_PROJECT_NAME:-$(basename "$ROOT_DIR")}"
docker volume rm "${PROJECT}_postgres_data" "${PROJECT}_mongo_data" 2>/dev/null || true
# Удаляем blockchain data через контейнер (alpine под root стирает данные
# независимо от владельца — без sudo на любой ноде; на проде nodeos пишет под root).
echo "Удаляем blockchain data..."
# sudo chmod -R 755 ../blockchain-data/ 2>/dev/null || true
docker run --rm -v "$(cd .. && pwd)/blockchain-data:/d" alpine sh -c 'rm -rf /d/* /d/.[!.]* 2>/dev/null || true'
# Пересоздаем и запускаем базы данных + Redis (monoredis).
+20
View File
@@ -60,4 +60,24 @@ docker compose up -d cooparser
echo "Запускаем контроллер..."
docker compose up -d --force-recreate coopback || true
# Контроллер создаёт marketplace-таблицы через TypeORM synchronize при старте.
# Ждём появления marketplace_ku_details, затем засеваем 3 ПВЗ Подмосковья
# (krg/odn/myt, ACTIVE) — иначе на свежем стенде таблица пуста, select ПВЗ
# приходит пустым/disabled и harness-сценарии ломаются. Сидер идемпотентен.
echo "Ждём создания marketplace_ku_details контроллером..."
KU_READY=""
for _ in $(seq 1 60); do
if docker compose exec -T postgres psql -U "${POSTGRES_USERNAME:-postgres}" -d "${POSTGRES_DATABASE:-voskhod}" -tAc "SELECT to_regclass('public.marketplace_ku_details')" 2>/dev/null | grep -q marketplace_ku_details; then
KU_READY=1
break
fi
sleep 2
done
if [ -n "$KU_READY" ]; then
echo "Засеваем 3 ПВЗ Подмосковья (krg/odn/myt)..."
pnpm run seed:marketplace-ku || echo "⚠ seed ПВЗ не выполнен (не критично для остального стенда)"
else
echo "⚠ marketplace_ku_details не появилась за отведённое время — пропускаем seed ПВЗ"
fi
echo "Перезапуск завершен!"
+53 -29
View File
@@ -229,42 +229,66 @@ export default class Blockchain {
// console.log(data)
// console.log("abi: ", serializedAbiHexString)
await this.api.transact(
{
actions: [
// Retry на CPU-timeout: тяжёлые WASM'ы (capital, marketplace, soviet)
// на старте требуют ~290-300ms CPU при первой компиляции в eos-vm.
// Дефолт chain `max_transaction_cpu_usage=290000us` срезает их.
// CPU usage варьируется по +-N us; повторные попытки иногда влезают.
const MAX_ATTEMPTS = 8
let lastErr: unknown
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
try {
await this.api.transact(
{
account: 'eosio',
name: 'setcode',
authorization: [
actions: [
{
actor: contract.target,
permission: 'active',
account: 'eosio',
name: 'setcode',
authorization: [
{
actor: contract.target,
permission: 'active',
},
],
data,
},
{
account: 'eosio',
name: 'setabi',
authorization: [
{
actor: contract.target,
permission: 'active',
},
],
data: {
account: contract.target,
abi: serializedAbiHexString,
},
},
],
data,
},
{
account: 'eosio',
name: 'setabi',
authorization: [
{
actor: contract.target,
permission: 'active',
},
],
data: {
account: contract.target,
abi: serializedAbiHexString,
},
blocksBehind: 3,
expireSeconds: 30,
},
],
},
{
blocksBehind: 3,
expireSeconds: 30,
},
)
console.log('contract setted: ', contract.target)
)
if (attempt > 1) console.log(`contract setted (attempt ${attempt}): `, contract.target)
else console.log('contract setted: ', contract.target)
lastErr = undefined
break
}
catch (txErr) {
const txMsg = txErr instanceof Error ? txErr.message : String(txErr)
const isCpuTimeout = /executing for too long|max_transaction_cpu_usage/i.test(txMsg)
if (!isCpuTimeout || attempt === MAX_ATTEMPTS) {
lastErr = txErr
break
}
console.warn(`[setContract] CPU timeout on '${contract.target}' attempt ${attempt}/${MAX_ATTEMPTS} — retrying in 2s`)
await new Promise(resolve => setTimeout(resolve, 2000))
}
}
if (lastErr) throw lastErr
}
catch (e) {
// Отсутствующий wasm/abi — soft warn. Контракт может быть
@@ -0,0 +1,11 @@
{
"protocol_feature_type": "builtin",
"dependencies": [],
"description_digest": "2ad6b7aabba8edbc87b46f3340df4d0db0e4b65474db3e1cd5db33fb685a506b",
"subjective_restrictions": {
"earliest_allowed_activation_time": "1970-01-01T00:00:00.000",
"preactivation_required": true,
"enabled": true
},
"builtin_feature_codename": "ASSERT_RECOVER_KEY_ACCOUNT"
}
+6 -8
View File
@@ -1,7 +1,6 @@
import config from '../configs'
import { initExtensionsInPostgres, initSystemStatus } from '../postgres-init'
import { installExtraData, installInitialData, startInfra } from './infra'
import { CooperativeClass, startCoop } from './cooperative'
import { startCoop } from './cooperative'
export async function boot() {
const blockchain = await startInfra()
@@ -14,12 +13,11 @@ export async function boot() {
}
export async function bootClean() {
const blockchain = await startInfra()
console.log('Создаём программы (Благорост и маркетплейс)')
const cooperative = new CooperativeClass(blockchain)
await cooperative.createPrograms(config.provider)
// Только инфраструктура: контракты, фичи, токен, системные параметры.
// Ни совета, ни программ — программы (createPrograms) требуют существующий
// совет и создаются в boot/bootExtra внутри installInitialData. В clean их
// не делаем (иначе soviet::createprog падает с «Совет не найден»).
await startInfra()
}
export async function bootExtra() {
+111 -9
View File
@@ -1,6 +1,5 @@
/* eslint-disable node/prefer-global/process */
import { Client } from 'pg'
import type { Cooperative } from 'cooptypes'
export async function initSystemStatus() {
console.log('Инициализация статуса системы для coopname: voskhod')
@@ -157,6 +156,82 @@ export async function initUsersInPostgres(
}
}
/**
* Засев dev-фикстуры карты ПВЗ: 3 кооперативных участка Подмосковья
* (krg/odn/myt, status=ACTIVE) в таблицу `marketplace_ku_details`.
*
* Зачем: на свежем стенде после `reboot:extra` таблица пуста — select ПВЗ
* приходит пустым/disabled, harness-сценарии и UI выбора участка ломаются.
* Раньше строки заводили ручным `psql INSERT` после каждого reboot.
*
* Ordering: DDL `marketplace_ku_details` создаёт контроллер (TypeORM
* `synchronize:true`) при старте coopback — ПОЗЖЕ, чем boot:extra. Поэтому
* сидер вызывается из `extra_reboot.sh` уже ПОСЛЕ `up -d coopback`, а не из
* installExtraData; здесь дополнительно проверяем существование таблицы и
* мягко пропускаем seed, если контроллер ещё не создал её. Идемпотентно
* (`ON CONFLICT (coopname, core_braname) DO NOTHING`). Данные совпадают с
* `seed-marketplace-branches.ts` (адреса/контакты тех же krg/odn/myt).
*
* Не для прода: прод не запускает `extra_reboot.sh`, ПВЗ там заводит
* председатель через UI — этот сидер живёт только в dev-reboot-цепочке.
*/
export async function seedMarketplaceKuDetails(coopname = 'voskhod') {
const client = new Client({
host: process.env.POSTGRES_HOST,
port: parseInt(process.env.POSTGRES_PORT || '5432'),
user: process.env.POSTGRES_USERNAME,
password: process.env.POSTGRES_PASSWORD,
database: process.env.POSTGRES_DATABASE,
})
// Будни 09:0018:00, суббота 10:00–16:00, воскресенье выходной.
const weekday = { open: '09:00', close: '18:00', breaks: [] as Array<{ start: string, end: string }> }
const workingHours = {
mon: weekday,
tue: weekday,
wed: weekday,
thu: weekday,
fri: weekday,
sat: { open: '10:00', close: '16:00', breaks: [] as Array<{ start: string, end: string }> },
}
const KU = [
{ braname: 'krg', address: 'Московская область, г. Красногорск, ул. Заводская, д. 1', phone: '+79991230101', email: 'krg@voskhod.coop' },
{ braname: 'odn', address: 'Московская область, г. Одинцово, ул. Центральная, д. 12', phone: '+79991230202', email: 'odn@voskhod.coop' },
{ braname: 'myt', address: 'Московская область, г. Мытищи, Олимпийский проспект, д. 5', phone: '+79991230303', email: 'myt@voskhod.coop' },
]
try {
await client.connect()
console.log('Подключение к PostgreSQL установлено для сидинга ПВЗ marketplace')
const exists = await client.query(`SELECT to_regclass('public.marketplace_ku_details') AS t`)
if (!exists.rows[0]?.t) {
console.warn('⚠ marketplace_ku_details ещё не создана контроллером — пропускаем seed ПВЗ')
return
}
for (const ku of KU) {
await client.query(`
INSERT INTO marketplace_ku_details
(coopname, core_braname, address_full, contact_phone, contact_email,
working_hours_json, status, geocode_status, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, 'ACTIVE', 'PENDING', now(), now())
ON CONFLICT (coopname, core_braname) DO NOTHING
`, [coopname, ku.braname, ku.address, ku.phone, ku.email, JSON.stringify(workingHours)])
}
console.log(`Засеяно ${KU.length} ПВЗ (krg/odn/myt, ACTIVE) в marketplace_ku_details для ${coopname}`)
}
catch (error) {
console.error('Ошибка сидинга ПВЗ marketplace в PostgreSQL:', error)
throw error
}
finally {
await client.end()
}
}
export async function initVaultInPostgres() {
const client = new Client({
host: process.env.POSTGRES_HOST,
@@ -241,9 +316,9 @@ export async function initExtensionsInPostgres() {
// Вставляем запись для capital extension
const capitalConfig = {
level_depth_base: 100000000,
onboarding_init_at: "2026-02-09T07:16:18.380Z",
onboarding_init_at: '2026-02-09T07:16:18.380Z',
expense_pool_percent: 100,
onboarding_expire_at: "2026-03-11T07:16:18.380Z",
onboarding_expire_at: '2026-03-11T07:16:18.380Z',
voting_period_in_days: 1,
authors_voting_percent: 62.8,
creators_voting_percent: 62.8,
@@ -253,15 +328,15 @@ export async function initExtensionsInPostgres() {
energy_decay_rate_per_day: 0.02,
coordinator_invite_validity_days: 30,
onboarding_blagorost_provision_done: true,
onboarding_blagorost_provision_hash: "CDE57D987E3C945E79E108920CE02A4A80CFA7980CAA912949BB6C2111B7027A",
onboarding_blagorost_provision_hash: 'CDE57D987E3C945E79E108920CE02A4A80CFA7980CAA912949BB6C2111B7027A',
onboarding_blagorost_offer_template_done: true,
onboarding_blagorost_offer_template_hash: "5CA88BBD303E5CCDA01E565FFE47E51855515176EC6C957F0FBDBCA4C53DBFD2",
onboarding_blagorost_offer_template_hash: '5CA88BBD303E5CCDA01E565FFE47E51855515176EC6C957F0FBDBCA4C53DBFD2',
onboarding_generator_offer_template_done: true,
onboarding_generator_offer_template_hash: "8DA31574E8CC764C3A1FCAE1172726656A3DCDB1BB82AB0E567E2732070C3A44",
onboarding_generator_offer_template_hash: '8DA31574E8CC764C3A1FCAE1172726656A3DCDB1BB82AB0E567E2732070C3A44',
onboarding_generator_program_template_done: true,
onboarding_generator_program_template_hash: "E55564D8946C55C93490B5277968FC890FDCB10A049DB5B2E0FE9F67FDA80896",
onboarding_generator_program_template_hash: 'E55564D8946C55C93490B5277968FC890FDCB10A049DB5B2E0FE9F67FDA80896',
onboarding_generation_contract_template_done: true,
onboarding_generation_contract_template_hash: "A4BD579D6130CCE2D8C34337DFA591807C1F028A148DD53689881B12AC2627E2",
onboarding_generation_contract_template_hash: 'A4BD579D6130CCE2D8C34337DFA591807C1F028A148DD53689881B12AC2627E2',
github_sync_branch: 'dev',
github_sync_poll_interval_minutes: 5,
github_api_token_encrypted: '',
@@ -282,7 +357,7 @@ export async function initExtensionsInPostgres() {
JSON.stringify(capitalConfig),
1,
new Date('2026-02-09T02:13:06.620Z'),
new Date('2026-02-09T02:27:57.155Z')
new Date('2026-02-09T02:27:57.155Z'),
])
// reports — встроенный extension, конфиг пустой (BuiltinSchema).
@@ -294,6 +369,33 @@ export async function initExtensionsInPostgres() {
ON CONFLICT (name) DO NOTHING
`, ['reports', true, '{}', 1])
// market — расширение «Стол заказов» (Marketplace MVP). Без записи в
// extensions MarketplacePlugin.initialize() бросает «Конфиг не найден»,
// bootstrap-миграции v1-v6 не запускаются → marketplace_category пустая,
// marketplaceListCategories возвращает [], форма create-offer падает на
// валидации «Категория *». schema_version=0 чтобы все 6 миграций marketplace
// прогнались по порядку (v4 → upsertBaseline для 9 категорий).
const marketConfig = {
enabled: true,
debug: false,
lastSyncTimestamp: '',
coopAcceptance: {
accepted: false,
document_registry_id: 0,
accepted_at: '',
accepted_by_board_decision_id: '',
},
writeoff: {
auto_proposal_enabled: false,
expiry_grace_days: 7,
},
}
await client.query(`
INSERT INTO "extensions" (name, enabled, config, schema_version)
VALUES ($1, $2, $3, $4)
ON CONFLICT (name) DO NOTHING
`, ['market', true, JSON.stringify(marketConfig), 0])
console.log('Extensions инициализированы в PostgreSQL')
}
catch (error) {
+159
View File
@@ -0,0 +1,159 @@
/**
* E2E-валидация канона двухподписного АПП приёмки (шаги 4→7 магистрали II)
* на СВЕЖЕМ заказе — без UI, без чтения цепи, всё через backend GraphQL.
* Проверяет именно новую ручку: marketplaceAplReceptionChairmanSignablePayloads
* отдаёт DocumentAggregate (rawDocument + document с подписью поставщика),
* председатель накладывает 2-ю подпись (Capital-паттерн).
*
* Шаги:
* 1. ivanpetrov (offerer): marketplaceAcceptIndividualOrder → авто-Shipment.
* 2. ivanpetrov: marketplaceListShipments(SUPPLY_PREPARED) → берём свежую.
* 3. chairkrg (operator КУ): marketplaceCreateAplReception → APL.
* 4. ivanpetrov: supplier payloads → signDocument(id=1) → signAsSupplier.
* 5. chairkrg: chairman payloads (DocumentAggregate) →
* signDocument(rawDocument, chairkrg, 2, [document]) → signAsChairman.
*
* Запуск (mono-ai-4):
* API_URL=http://127.0.0.1:3028/v1/graphql CHAIN_URL=http://127.0.0.1:8918 \
* ORDER_ID=39578e30-2bb4-4495-836b-c91c89eb744f \
* pnpm --filter @coopenomics/boot exec esno src/scripts/diag-e2e-apl.ts
*/
import ecc from 'eosjs-ecc'
import { Classes } from '@coopenomics/sdk'
const API_URL = process.env.API_URL || 'http://127.0.0.1:3028/v1/graphql'
const CHAIN_URL = process.env.CHAIN_URL || 'http://127.0.0.1:8918'
// individual-оффер «Мёд алтайский» ivanpetrov (cycle_type=individual) + ПВЗ krg.
const OFFER_ID = process.env.OFFER_ID || '670c87d3-4e55-474e-a4e3-646c9ff0a4cd'
const BRANAME = process.env.BRANAME || 'krg'
const SUPPLIER = { email: 'ivan.petrov@example.com', wif: '5JL7fbB6kqsmk38zns2NMqqs5sWUojNGNhoAgE5G4fxXw5Uz733', account: 'ivanpetrov' }
const CHAIR = { email: 'chairkrg@voskhod.coop', wif: '5KN4NRRdqNG9SH7sLP9pA87zvRSWmCR3a43mTHXbi7SwDTietBL', account: 'chairkrg' }
const ORDERER = { email: 'ekaterina.smirnova@example.com', wif: '5KXg5ZDL7yu34gYSxzL7XKeePsY2Y2wN63WEYoWNzDwoMUihWG9', account: 'ekaterina' }
async function gql(token: string | null, query: string, variables?: any): Promise<any> {
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
if (token) headers.Authorization = `Bearer ${token}`
const res = await fetch(API_URL, { method: 'POST', headers, body: JSON.stringify({ query, variables }) })
const payload: any = await res.json()
if (payload.errors) throw new Error('gql: ' + JSON.stringify(payload.errors))
return payload.data
}
async function login(who: { email: string; wif: string; account: string }): Promise<string> {
const info = await (await fetch(`${CHAIN_URL}/v1/chain/get_info`)).json()
const now = info.head_block_time
const digest = ecc.sha256(Buffer.from(now, 'utf8'), 'hex')
const signature = ecc.signHash(digest, who.wif)
const m = `mutation($d:LoginInput!){ login(data:$d){ tokens{ access{ token } } account{ username } } }`
const d = await gql(null, m, { d: { email: who.email, now, signature } })
console.error('[e2e] login', d.login.account.username)
return d.login.tokens.access.token
}
async function main() {
// 0. orderer (ekaterina) оформляет СВЕЖИЙ individual-заказ — backend-хук
// сразу переводит ACTIVE → ACCEPTED_PENDING_SUPPLIER_INDIVIDUAL, и мы
// принимаем его тут же, не давая race #151 откатить статус.
const ordTok = await login(ORDERER)
const created = await gql(ordTok, `mutation($i:MarketplaceCreateOrderInput!){ marketplaceCreateOrder(input:$i){ order{ id status } } }`, { i: { offer_id: OFFER_ID, quantity: 1, delivery_braname: BRANAME } })
const ORDER_ID = created.marketplaceCreateOrder.order.id as string
console.error('[e2e] order created:', ORDER_ID, created.marketplaceCreateOrder.order.status)
// 1. accept individual order (offerer) → авто-Shipment
const supTok = await login(SUPPLIER)
const acc = await gql(supTok, `mutation($i:MarketplaceAcceptIndividualOrderInput!){ marketplaceAcceptIndividualOrder(input:$i){ order{ id status } tx_hash } }`, { i: { order_id: ORDER_ID } })
console.error('[e2e] accepted:', JSON.stringify(acc.marketplaceAcceptIndividualOrder))
// 2. найти свежую партию SUPPLY_PREPARED именно по нашему заказу (через cycle_id)
const acceptedOrder = acc.marketplaceAcceptIndividualOrder.order
const ordInfo = await gql(supTok, `query($d:MarketplaceGetOrderInput!){ marketplaceGetOrder(data:$d){ id status cycle_id } }`, { d: { order_id: ORDER_ID } }).catch(() => null)
const ourCycleId = ordInfo?.marketplaceGetOrder?.cycle_id ?? null
console.error('[e2e] accepted order:', JSON.stringify(acceptedOrder), 'cycle_id:', ourCycleId)
const sh = await gql(supTok, `query($d:MarketplaceListShipmentsInput){ marketplaceListShipments(data:$d){ id status braname cycle_id } }`, { d: { statuses: ['SUPPLY_PREPARED'] } })
const shipments = sh.marketplaceListShipments as any[]
console.error('[e2e] SUPPLY_PREPARED shipments:', JSON.stringify(shipments))
const shipment = (ourCycleId && shipments.find((s) => s.cycle_id === ourCycleId)) || shipments[0]
if (!shipment) throw new Error('Нет SUPPLY_PREPARED партии после accept')
if (Number(process.env.STOP_AFTER) === 2) {
console.error(`[e2e] STOP_AFTER=2 — SHIPMENT ${shipment.id} в SUPPLY_PREPARED (готов к маркировке EAN-13), ORDER ${ORDER_ID}`)
return
}
// 3. оператор КУ создаёт АПП
const chairTok = await login(CHAIR)
const cr = await gql(chairTok, `mutation($d:MarketplaceCreateAplReceptionInput!){ marketplaceCreateAplReception(data:$d){ apl_reception{ id status braname } } }`, { d: { shipment_id: shipment.id } })
const aplId = cr.marketplaceCreateAplReception.apl_reception.id
console.error('[e2e] APL создан:', aplId, cr.marketplaceCreateAplReception.apl_reception.status)
if (Number(process.env.STOP_AFTER) === 5) {
console.error(`[e2e] STOP_AFTER=5 — APL ${aplId} в PENDING_SUPPLIER_SIGN (ждёт подпись поставщика), ORDER ${ORDER_ID}`)
return
}
// 4. поставщик подписывает (id=1) — backend сохранит supplier_signed_documents + тело (skip_save:false)
const supTok2 = await login(SUPPLIER)
const sp = await gql(supTok2, `query($d:MarketplaceAplReceptionByIdInput!){ marketplaceAplReceptionSupplierSignablePayloads(data:$d){ full_title hash meta binary } }`, { d: { apl_reception_id: aplId } })
const supPayloads = sp.marketplaceAplReceptionSupplierSignablePayloads as any[]
const supSigner = new Classes.Document(SUPPLIER.wif)
const supplierSigned: any[] = []
for (const p of supPayloads) supplierSigned.push(await supSigner.signDocument(p, SUPPLIER.account, 1))
const ss = await gql(supTok2, `mutation($d:MarketplaceSignAplReceptionInput!){ marketplaceSignAplReceptionAsSupplier(data:$d){ apl_reception{ id status supplier_signed_at } } }`, { d: { apl_reception_id: aplId, signed_documents: supplierSigned } })
console.error('[e2e] supplier sign OK:', JSON.stringify(ss.marketplaceSignAplReceptionAsSupplier))
if (Number(process.env.STOP_AFTER) === 6) {
console.error(`[e2e] STOP_AFTER=6 — APL ${aplId} в PENDING_CHAIRMAN_RECEPTION_SIGN (ждёт закрывающую подпись председателя), ORDER ${ORDER_ID}`)
return
}
// 5. председатель: новая ручка DocumentAggregate → подпись id=2 поверх подписи поставщика
const chairTok2 = await login(CHAIR)
const cp = await gql(chairTok2, `query($d:MarketplaceAplReceptionByIdInput!){ marketplaceAplReceptionChairmanSignablePayloads(data:$d){ hash rawDocument{ full_title html hash meta binary } document{ version hash doc_hash meta_hash meta signatures{ id signer public_key signature signed_at signed_hash meta } } } }`, { d: { apl_reception_id: aplId } })
const chairPayloads = cp.marketplaceAplReceptionChairmanSignablePayloads as any[]
console.error('[e2e] chairman aggregates:', chairPayloads.length, 'supplier sig:', JSON.stringify(chairPayloads[0]?.document?.signatures?.map((s: any) => `${s.id}:${s.signer}`)))
const chairSigner = new Classes.Document(CHAIR.wif)
const chairSigned: any[] = []
for (const p of chairPayloads) {
const signed = await chairSigner.signDocument(p.rawDocument, CHAIR.account, 2, [p.document])
console.error('[e2e] order', p.rawDocument?.meta?.order_id, 'sigs:', signed.signatures.map((s: any) => `${s.id}:${s.signer}`).join(','))
chairSigned.push(signed)
}
const cs = await gql(chairTok2, `mutation($d:MarketplaceSignAplReceptionInput!){ marketplaceSignAplReceptionAsChairman(data:$d){ apl_reception{ id status chairman_signed_at chairman_account chairman_signchair_tx_hash } } }`, { d: { apl_reception_id: aplId, signed_documents: chairSigned } })
console.error('[e2e] CHAIRMAN SIGN OK:', JSON.stringify(cs.marketplaceSignAplReceptionAsChairman))
// ── Магистраль II шаг 8: оператор КУ маркирует имущество EAN-13 ───────
// переиспользуем токен председателя: повторный login того же аккаунта в
// пределах одного блока даёт одинаковый now → одинаковую подпись → дубль
// токена сессии (UQ constraint).
const lbl = await gql(chairTok2, `mutation($d:MarketplaceLabelInventoryInput!){ marketplaceLabelInventory(data:$d){ inventory{ id barcode_value barcode_format quantity_per_label status product_name_snapshot } } }`, { d: { order_id: ORDER_ID, format: 'EAN13' } })
console.error('[e2e] LABEL OK (EAN-13):', JSON.stringify(lbl.marketplaceLabelInventory.inventory?.map((i: any) => `${i.barcode_value}/${i.barcode_format}/${i.status}`)))
// ── Магистраль II шаги 9-10: ВЫДАЧА (тот же двухподписный канон) ──────
// 9. председатель КУ выдачи открывает выдачу первой подписью (signiss1).
const oip = await gql(chairTok2, `query($d:MarketplaceIssueActPayloadInput!){ marketplaceIssueActChairmanSignablePayload(data:$d){ full_title hash meta binary } }`, { d: { order_id: ORDER_ID } })
const openPayload = oip.marketplaceIssueActChairmanSignablePayload
const chairIssSigner = new Classes.Document(CHAIR.wif)
const chairIssSigned = await chairIssSigner.signDocument(openPayload, CHAIR.account, 1)
const oi = await gql(chairTok2, `mutation($d:MarketplaceOpenIssuanceInput!){ marketplaceOpenIssuance(data:$d){ order{ id status } tx_hash } }`, { d: { order_id: ORDER_ID, signed_document: chairIssSigned } })
console.error('[e2e] OPEN ISSUANCE OK (signiss1):', JSON.stringify(oi.marketplaceOpenIssuance))
if (Number(process.env.STOP_AFTER) === 9) {
console.error(`[e2e] STOP_AFTER=9 — ORDER ${ORDER_ID} в READY_TO_RECEIVE (ждёт финальную подпись заказчика)`)
return
}
// 10. заказчик получает DocumentAggregate (подписанный председателем) и
// накладывает 2-ю подпись поверх → финальная подпись (signiss2).
const ordTok2 = await login(ORDERER)
const fp = await gql(ordTok2, `query($d:MarketplaceIssueActPayloadInput!){ marketplaceIssueActOrdererSignablePayload(data:$d){ hash rawDocument{ full_title html hash meta binary } document{ version hash doc_hash meta_hash meta signatures{ id signer public_key signature signed_at signed_hash meta } } } }`, { d: { order_id: ORDER_ID } })
const finAgg = fp.marketplaceIssueActOrdererSignablePayload
console.error('[e2e] issuance aggregate — chairman sig:', JSON.stringify(finAgg?.document?.signatures?.map((s: any) => `${s.id}:${s.signer}`)))
const ordSigner = new Classes.Document(ORDERER.wif)
const finSigned = await ordSigner.signDocument(finAgg.rawDocument, ORDERER.account, 2, [finAgg.document])
console.error('[e2e] issuance sigs:', finSigned.signatures.map((s: any) => `${s.id}:${s.signer}`).join(','))
const fi = await gql(ordTok2, `mutation($d:MarketplaceFinalizeIssuanceInput!){ marketplaceFinalizeIssuance(data:$d){ order{ id status received_at } tx_hash } }`, { d: { order_id: ORDER_ID, actual_quantity: 1, delivery_signer: CHAIR.account, signed_document: finSigned } })
console.error('[e2e] FINALIZE ISSUANCE OK (signiss2):', JSON.stringify(fi.marketplaceFinalizeIssuance))
}
main().then(() => process.exit(0)).catch((e) => { console.error('[e2e] FAILED:', e.message ?? e); process.exit(1) })
@@ -0,0 +1,88 @@
/**
* Диагностика ЗАКРЫВАЮЩЕЙ подписи АПП председателем КУ вне UI (шаг 7
* магистрали II) по КАНОНУ двухподписного документа (как приём РИД в
* Capital). Воспроизводит КЛИЕНТСКУЮ цепочку фронтенда:
*
* 1. query marketplaceAplReceptionChairmanSignablePayloads → backend отдаёт
* DocumentAggregate на каждый Order: rawDocument (исходный документ для
* ознакомления, оригинальный порядок meta) + document (подписанный
* поставщиком, с его подписью).
* 2. signDocument(rawDocument, chairman, 2, [document]) — председатель
* накладывает свою подпись (id=2) поверх подписи поставщика (id=1).
* 3. mutation marketplaceSignAplReceptionAsChairman → backend signchair с
* документом, где ОБЕ подписи.
*
* ФРОНТ ЦЕПЬ НЕ ЧИТАЕТ — всё отдаёт backend. (Раньше тут было чтение
* order.acceptance_act_signsupp из get_table_rows — это антипаттерн, см.
* memory reference_2sig_canon_marketplace_capital; on-chain meta-строка к
* тому же переупорядочена и для re-sign непригодна.)
*
* Запуск (mono-ai-4):
* API_URL=http://127.0.0.1:3028/v1/graphql CHAIN_URL=http://127.0.0.1:8918 \
* SERVER_SECRET=SECRET APL_ID=<свежий-апп-после-skip_save:false> \
* pnpm --filter @coopenomics/boot exec esno src/scripts/diag-sign-apl-chairman.ts
*/
import ecc from 'eosjs-ecc'
import { Classes } from '@coopenomics/sdk'
const API_URL = process.env.API_URL || 'http://127.0.0.1:3028/v1/graphql'
const CHAIN_URL = process.env.CHAIN_URL || 'http://127.0.0.1:8918'
const SERVER_SECRET = process.env.SERVER_SECRET || 'SECRET'
const APL_ID = process.env.APL_ID || ''
const EMAIL = process.env.EMAIL || 'chairkrg@voskhod.coop'
const WIF = process.env.WIF || '5KN4NRRdqNG9SH7sLP9pA87zvRSWmCR3a43mTHXbi7SwDTietBL'
const ACCOUNT = process.env.ACCOUNT || 'chairkrg'
async function gqlRaw(token: string | null, query: string, variables?: any): Promise<any> {
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
if (token) headers.Authorization = `Bearer ${token}`
else headers['server-secret'] = SERVER_SECRET
const res = await fetch(API_URL, { method: 'POST', headers, body: JSON.stringify({ query, variables }) })
const payload: any = await res.json()
if (payload.errors) throw new Error('gql: ' + JSON.stringify(payload.errors))
return payload.data
}
async function main() {
if (!APL_ID) throw new Error('APL_ID обязателен (свежий АПП, подписанный поставщиком после skip_save:false)')
// 1. login as chairman
const info = await (await fetch(`${CHAIN_URL}/v1/chain/get_info`)).json()
const now = info.head_block_time
const digest = ecc.sha256(Buffer.from(now, 'utf8'), 'hex')
const signature = ecc.signHash(digest, WIF)
const loginM = `mutation($d:LoginInput!){ login(data:$d){ tokens{ access{ token } } account{ username } } }`
const ld = await gqlRaw(null, loginM, { d: { email: EMAIL, now, signature } })
const token = ld.login.tokens.access.token
console.error('[diag] logged in as', ld.login.account.username)
// 2. backend отдаёт supplier-подписанные акты (DocumentAggregate) — без цепи
const q = `query($d:MarketplaceAplReceptionByIdInput!){
marketplaceAplReceptionChairmanSignablePayloads(data:$d){
hash
rawDocument{ full_title html hash meta binary }
document{ version hash doc_hash meta_hash meta signatures{ id signer public_key signature signed_at signed_hash meta } }
}
}`
const qd = await gqlRaw(token, q, { d: { apl_reception_id: APL_ID } })
const payloads = qd.marketplaceAplReceptionChairmanSignablePayloads as any[]
console.error('[diag] aggregates:', payloads.length,
'sig поставщика по 1-му:', JSON.stringify(payloads[0]?.document?.signatures?.map((s: any) => `${s.id}:${s.signer}`)))
// 3. накладываем подпись председателя (id=2) поверх подписи поставщика (id=1)
const signer = new Classes.Document(WIF)
const signed_documents: any[] = []
for (const p of payloads) {
const signed = await signer.signDocument(p.rawDocument, ACCOUNT, 2, [p.document])
console.error('[diag] order_id', p.rawDocument?.meta?.order_id, '→ подписей:', signed.signatures.length,
signed.signatures.map((s: any) => `${s.id}:${s.signer}`).join(','))
signed_documents.push(signed)
}
// 4. mutation — backend signchair (контракт требует обе подписи)
const m = `mutation($d:MarketplaceSignAplReceptionInput!){ marketplaceSignAplReceptionAsChairman(data:$d){ apl_reception{ id status chairman_signed_at chairman_account chairman_signchair_tx_hash } } }`
const md = await gqlRaw(token, m, { d: { apl_reception_id: APL_ID, signed_documents } })
console.error('[diag] signAsChairman OK:', JSON.stringify(md.marketplaceSignAplReceptionAsChairman))
}
main().then(() => process.exit(0)).catch((e) => { console.error('[diag] FAILED:', e.message ?? e); process.exit(1) })
@@ -0,0 +1,64 @@
/**
* Диагностика подписи АПП поставщиком вне UI: воспроизводит клиентскую
* цепочку (query payloads → Classes.Document.signDocument → mutation
* signAsSupplier) от лица ivanpetrov, чтобы изолировать backend/подпись
* от проблем desktop (globalStore.wif / клик по диалогу).
*
* Запуск (mono-ai-4):
* API_URL=http://127.0.0.1:3028/v1/graphql CHAIN_URL=http://127.0.0.1:8918 \
* SERVER_SECRET=SECRET APL_ID=8d7cf06c-7c9d-4e05-86c9-a41d94165c9c \
* pnpm --filter @coopenomics/boot exec esno src/scripts/diag-sign-apl.ts
*/
import ecc from 'eosjs-ecc'
import { Classes } from '@coopenomics/sdk'
const API_URL = process.env.API_URL || 'http://127.0.0.1:3028/v1/graphql'
const CHAIN_URL = process.env.CHAIN_URL || 'http://127.0.0.1:8918'
const SERVER_SECRET = process.env.SERVER_SECRET || 'SECRET'
const APL_ID = process.env.APL_ID || '8d7cf06c-7c9d-4e05-86c9-a41d94165c9c'
const EMAIL = 'ivan.petrov@example.com'
const WIF = '5JL7fbB6kqsmk38zns2NMqqs5sWUojNGNhoAgE5G4fxXw5Uz733'
const ACCOUNT = 'ivanpetrov'
async function gqlRaw(token: string | null, query: string, variables?: any): Promise<any> {
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
// server-secret шлём ТОЛЬКО на публичный login. На аутентифицированных
// вызовах он бы обошёл MarketplaceMembershipGuard (return true без
// наполнения currentMember) → @CurrentMarketplaceMember бросает 401.
if (token) headers.Authorization = `Bearer ${token}`
else headers['server-secret'] = SERVER_SECRET
const res = await fetch(API_URL, { method: 'POST', headers, body: JSON.stringify({ query, variables }) })
const payload: any = await res.json()
if (payload.errors) throw new Error('gql: ' + JSON.stringify(payload.errors))
return payload.data
}
async function main() {
const info = await (await fetch(`${CHAIN_URL}/v1/chain/get_info`)).json()
const now = info.head_block_time
const digest = ecc.sha256(Buffer.from(now, 'utf8'), 'hex')
const signature = ecc.signHash(digest, WIF)
const loginM = `mutation($d:LoginInput!){ login(data:$d){ tokens{ access{ token } } account{ username } } }`
const ld = await gqlRaw(null, loginM, { d: { email: EMAIL, now, signature } })
const token = ld.login.tokens.access.token
console.error('[diag] logged in as', ld.login.account.username)
const q = `query($d:MarketplaceAplReceptionByIdInput!){ marketplaceAplReceptionSupplierSignablePayloads(data:$d){ full_title hash meta binary } }`
const qd = await gqlRaw(token, q, { d: { apl_reception_id: APL_ID } })
const payloads = qd.marketplaceAplReceptionSupplierSignablePayloads
console.error('[diag] payloads:', payloads.length, 'meta[0]:', JSON.stringify(payloads[0]?.meta))
const signer = new Classes.Document(WIF)
const signed_documents: any[] = []
for (const p of payloads) {
const s = await signer.signDocument(p, ACCOUNT, 1)
signed_documents.push(s)
}
console.error('[diag] signed', signed_documents.length, 'docs; first signatures:', JSON.stringify(signed_documents[0]?.signatures?.length))
const m = `mutation($d:MarketplaceSignAplReceptionInput!){ marketplaceSignAplReceptionAsSupplier(data:$d){ apl_reception{ id status supplier_signed_at } } }`
const md = await gqlRaw(token, m, { d: { apl_reception_id: APL_ID, signed_documents } })
console.error('[diag] signAsSupplier OK:', JSON.stringify(md.marketplaceSignAplReceptionAsSupplier))
}
main().then(() => process.exit(0)).catch((e) => { console.error('[diag] FAILED:', e.message ?? e); process.exit(1) })
+66
View File
@@ -0,0 +1,66 @@
/**
* Регистрирует организацию кооперативного участка через GraphQL-мутацию
* `editBranch` (тот же серверный путь, что фронтовый createBranch:
* interactor.editBranch → organizationRepository.create). Нужен потому,
* что branch krg уже создан on-chain (createBranch бросил бы «уже
* создан»), а организация в Mongo контроллера отсутствует — из-за чего
* фабрика документов 1102 (Акт приёмки) падает «Организация не найдена».
*
* Запуск (mono-ai-4):
* API_URL=http://127.0.0.1:3028/v1/graphql \
* CHAIN_URL=http://127.0.0.1:8918 \
* SERVER_SECRET=SECRET \
* pnpm --filter @coopenomics/boot exec esno src/scripts/fix-ku-org.ts
*/
import { gql, loginAsChairman } from '../tests/shared/apiClient'
const COOP = 'voskhod'
const BRANCHES = [
{
braname: 'krg',
trustee: 'chairkrg',
short_name: 'КУ Красногорск',
full_name: 'Кооперативный участок «Красногорск»',
fact_address: 'Московская область, г. Красногорск, ул. Заводская, д. 1',
phone: '+79991230101',
email: 'krg@voskhod.coop',
based_on: 'решение собрания совета №СС-1 от 20 мая 2026 г',
},
]
// Банковский реквизит КУ — копия дефолтного банка кооператива (КУ не
// отдельное юрлицо, действует от реквизитов кооператива). createBranch
// создаёт его автоматически, editBranch — нет, поэтому добавляем явно.
const COOP_BANK = {
currency: 'RUB',
card_number: '',
bank_name: 'ПАО Сбербанк',
account_number: '40703810038000110117',
details: { bik: '044525225', corr: '30101810400000000225', kpp: '773643001' },
}
async function main() {
const { token, username } = await loginAsChairman()
console.error('[fix-ku-org] logged in as', username)
const addPm = `mutation($d:AddPaymentMethodInput!){ addPaymentMethod(data:$d){ method_id } }`
const editBranch = `mutation($d:EditBranchInput!){ editBranch(data:$d){ braname } }`
for (const b of BRANCHES) {
try {
const pm = await gql(token, addPm, {
d: { username: b.braname, is_default: true, bank_transfer_data: COOP_BANK },
})
console.error('[fix-ku-org] addPaymentMethod', b.braname, 'OK', JSON.stringify(pm))
} catch (e: any) {
console.error('[fix-ku-org] addPaymentMethod', b.braname, 'FAILED:', e.message ?? e)
}
try {
const res = await gql(token, editBranch, { d: { coopname: COOP, ...b } })
console.error('[fix-ku-org] editBranch', b.braname, 'OK', JSON.stringify(res))
} catch (e: any) {
console.error('[fix-ku-org] editBranch', b.braname, 'FAILED:', e.message ?? e)
}
}
}
main().then(() => process.exit(0)).catch((e) => { console.error(e); process.exit(1) })
@@ -0,0 +1,104 @@
/**
* One-shot пополнение L3 Main Wallet тестовых пайщиков для прогона магистрали II
* Marketplace (orderer/order-create → consolidated → offerer/incoming-orders).
*
* Запускается отдельно (не часть boot:extra), потому что L3-эмиссия в текущем
* стенде делается ручным `wallet::createdeposit + gateway::completeincome` от
* имени кооператива. После reboot:extra нужно повторно пополнить.
*
* Pattern взят из `seed-capital/phases/08-investments.ts:depositToWallet` —
* чистая версия без vitest-зависимости.
*
* ВАЖНО (блокер 2026-05-22): wallet::createdeposit требует, чтобы пайщик уже
* был **членом кооператива** (запись в soviet::participants). На текущем
* стенде add-plain-participant.ts и ensureMarketplaceParticipant создают
* on-chain user + mongo + pg, но НЕ делают joincoop. Поэтому createdeposit
* падает «Пайщик не найден в кооперативе» (coops_access_helpers.hpp:57).
* Полноценная адаптация требует soviet decision type='joincoop' + 3 votefor +
* authorize + exec (см. validate.cpp:29, sndagreement.cpp:65, addbal.cpp:43).
* До починки joincoop в seed — этот скрипт работает только для пайщиков,
* прошедших полный UI-флоу адаптации.
*
* Usage:
* pnpm --filter @coopenomics/boot exec esno src/scripts/marketplace-deposit-fund.ts
*
* По умолчанию пополняет: ekaterina, ivanpetrov, petrova, sidorov по 10000 RUB
* каждому (хватит на десятки Order'ов цены 120 ₽).
*/
import { createHash, randomInt } from 'node:crypto'
import { GatewayContract, WalletContract } from 'cooptypes'
import Blockchain from '../blockchain'
import config from '../configs'
const COOPNAME = process.env.COOPNAME ?? config.provider
const AMOUNT_RUB = Number.parseFloat(process.env.FUND_AMOUNT_RUB ?? '10000')
const USERNAMES = (process.env.FUND_USERNAMES ?? 'ekaterina,ivanpetrov,petrova,sidorov')
.split(',')
.map((s) => s.trim())
.filter(Boolean)
const log = (...a: unknown[]) => console.error('[marketplace-deposit-fund]', ...a)
function rndHash(): string {
return createHash('sha256').update(`mkt-fund:${Date.now()}:${randomInt(1_000_000_000)}`).digest('hex')
}
async function depositToWallet(blockchain: Blockchain, username: string, amountRub: number): Promise<void> {
const depositHash = rndHash()
const quantity = `${amountRub.toFixed(4)} RUB`
log(`wallet::createdeposit ${username}${quantity} (deposit=${depositHash.slice(0, 10)}...)`)
await blockchain.api.transact({
actions: [{
account: WalletContract.contractName.production,
name: WalletContract.Actions.CreateDeposit.actionName,
authorization: [{ actor: COOPNAME, permission: 'active' }],
data: {
coopname: COOPNAME,
username,
deposit_hash: depositHash,
quantity,
},
}],
}, { blocksBehind: 3, expireSeconds: 30 })
log(`gateway::completeincome ${depositHash.slice(0, 10)}...`)
await blockchain.api.transact({
actions: [{
account: GatewayContract.contractName.production,
name: GatewayContract.Actions.CompleteIncome.actionName,
authorization: [{ actor: COOPNAME, permission: 'active' }],
data: {
coopname: COOPNAME,
income_hash: depositHash,
},
}],
}, { blocksBehind: 3, expireSeconds: 30 })
}
async function main(): Promise<void> {
const blockchain = new Blockchain(config.network, config.private_keys)
await blockchain.update_pass_instance()
log(`Coopname: ${COOPNAME}, amount: ${AMOUNT_RUB} RUB, users: ${USERNAMES.join(', ')}`)
const results: Array<{ username: string; ok: boolean; reason?: string }> = []
for (const username of USERNAMES) {
try {
await depositToWallet(blockchain, username, AMOUNT_RUB)
results.push({ username, ok: true })
log(`${username} пополнен`)
} catch (e: any) {
const reason = e?.message ?? String(e)
results.push({ username, ok: false, reason })
log(`${username} fail: ${reason}`)
}
}
console.log(JSON.stringify({ coopname: COOPNAME, amount: AMOUNT_RUB, results }, null, 2))
if (results.some((r) => !r.ok)) process.exit(1)
}
main().catch((e) => {
log(`fatal: ${e?.stack ?? e}`)
process.exit(2)
})
@@ -0,0 +1,88 @@
/**
* Idempotent сидинг базовых сущностей кооператива в Mongo:
* - vars(coopname=voskhod) — описание кооператива (отображается в UI хедер/футер)
* - monos(coopname=voskhod, status=active) — маркер, что инсталляция активна
* - vaults(username=voskhod, permission=active, wif=<encrypted from infra.ts>)
*
* Эти данные обычно создаются `pnpm boot:extra`, но если Mongo volume сбросился
* (а chain нет), их надо восстановить отдельно — этот скрипт делает только эту часть.
*
* Запуск:
* MONGO_URI=mongodb://127.0.0.1:27047/cooperative-x \
* pnpm --filter @coopenomics/boot exec esno src/scripts/seed-cooperative-base.ts
*/
import mongoose from 'mongoose'
async function main() {
const mongoUri = process.env.MONGO_URI
if (!mongoUri) throw new Error('MONGO_URI not set')
await mongoose.connect(mongoUri)
const vars = {
coopname: 'voskhod',
full_abbr: 'Потребительский Кооператив',
full_abbr_genitive: 'Потребительского Кооператива',
full_abbr_dative: 'Потребительскому Кооперативу',
short_abbr: 'ПК',
website: 'цифровой-кооператив.рф',
name: 'Восход',
confidential_link: 'coopenomics.world/privacy',
confidential_email: 'privacy@coopenomics.world',
contact_email: 'contact@coopenomics.world',
passport_request: 'no',
wallet_agreement: { protocol_number: '10-04-2024', protocol_day_month_year: '10 апреля 2024 г.' },
signature_agreement: { protocol_number: '10-04-2024', protocol_day_month_year: '10 апреля 2024 г.' },
privacy_agreement: { protocol_number: '10-04-2024', protocol_day_month_year: '10 апреля 2024 г.' },
user_agreement: { protocol_number: '10-04-2024', protocol_day_month_year: '10 апреля 2024 г.' },
participant_application: { protocol_number: '10-04-2024', protocol_day_month_year: '10 апреля 2024 г.' },
generator_program: { protocol_number: '1', protocol_day_month_year: '09.02.2026 10:24' },
generation_contract_template: { protocol_number: '2', protocol_day_month_year: '09.02.2026 10:24' },
blagorost_program: { protocol_number: '3', protocol_day_month_year: '09.02.2026 10:24' },
generator_offer_template: { protocol_number: '4', protocol_day_month_year: '09.02.2026 10:27' },
blagorost_offer_template: { protocol_number: '5', protocol_day_month_year: '09.02.2026 10:27' },
deleted: false,
block_num: 1,
}
const varsCol = mongoose.connection.collection('vars')
const existing = await varsCol.findOne({ coopname: 'voskhod' })
if (existing) {
await varsCol.updateOne({ coopname: 'voskhod' }, { $set: vars })
console.log('vars upserted (was present)')
}
else {
await varsCol.insertOne({ ...vars, _created_at: new Date() })
console.log('vars inserted')
}
const monosCol = mongoose.connection.collection('monos')
const mExisting = await monosCol.findOne({ coopname: 'voskhod' })
if (mExisting) {
await monosCol.updateOne({ coopname: 'voskhod' }, { $set: { status: 'active' } })
console.log('monos upserted (was present)')
}
else {
await monosCol.insertOne({ coopname: 'voskhod', status: 'active' })
console.log('monos inserted')
}
const vaultsCol = mongoose.connection.collection('vaults')
const vExisting = await vaultsCol.findOne({ username: 'voskhod', permission: 'active' })
if (vExisting) {
console.log('vaults already present (kept as is)')
}
else {
await vaultsCol.insertOne({
username: 'voskhod',
permission: 'active',
wif: '9d6479a9d77ead53fb0e5e54b3608a95:2046ee3c1577d48aecbee49e8f25c4c2df37ab02f15d73d0d1b6352f53a4b774cb9e71b6028fd7caf64568e195c7878dfbb5d2bf10a3766d90ba9e92ea724428',
})
console.log('vaults inserted')
}
await mongoose.disconnect()
}
main()
.then(() => process.exit(0))
.catch((e) => { console.error(e); process.exit(1) })
@@ -0,0 +1,178 @@
/**
* Seed-скрипт для трёх кооперативных участков Подмосковья:
* krg — Красногорск, председатель chairkrg
* odn — Одинцово, председатель chairodn
* myt — Мытищи, председатель chairmyt
* + доверенное лицо trustedkrg для КУ Красногорск.
*
* Делает:
* 1. eosio::newaccount для каждого braname (если ещё не существует).
* 2. branch::createbranch (actor=coopname active).
* 3. branch::addtrusted для krg → trustedkrg.
* 4. Mongo organization upsert (short_name + контакты, для desktop UI).
*
* Использование:
* MONGO_URI=mongodb://127.0.0.1:27017/cooperative-x \
* pnpm --filter @coopenomics/boot exec esno \
* src/scripts/seed-marketplace-branches.ts
*/
import { BranchContract, type Cooperative } from 'cooptypes'
import { Generator } from '@coopenomics/factory'
import Blockchain from '../blockchain'
import config from '../configs'
const log = (...a: unknown[]) => console.error('[seed-branches]', ...a)
interface BranchSeed {
braname: string
trustee: string
short_name: string
full_name: string
city: string
fact_address: string
phone: string
email: string
based_on: string
}
const BRANCHES: BranchSeed[] = [
{
braname: 'krg',
trustee: 'chairkrg',
short_name: 'КУ Красногорск',
full_name: 'Кооперативный участок «Красногорск»',
city: 'Красногорск',
fact_address: 'Московская область, г. Красногорск, ул. Заводская, д. 1',
phone: '+79991230101',
email: 'krg@voskhod.coop',
based_on: 'решение собрания совета №СС-1 от 20 мая 2026 г',
},
{
braname: 'odn',
trustee: 'chairodn',
short_name: 'КУ Одинцово',
full_name: 'Кооперативный участок «Одинцово»',
city: 'Одинцово',
fact_address: 'Московская область, г. Одинцово, ул. Центральная, д. 12',
phone: '+79991230202',
email: 'odn@voskhod.coop',
based_on: 'решение собрания совета №СС-1 от 20 мая 2026 г',
},
{
braname: 'myt',
trustee: 'chairmyt',
short_name: 'КУ Мытищи',
full_name: 'Кооперативный участок «Мытищи»',
city: 'Мытищи',
fact_address: 'Московская область, г. Мытищи, Олимпийский проспект, д. 5',
phone: '+79991230303',
email: 'myt@voskhod.coop',
based_on: 'решение собрания совета №СС-1 от 20 мая 2026 г',
},
]
const TRUSTED_KRG = ['trustedkrg']
async function main() {
const blockchain = new Blockchain(config.network, config.private_keys)
await blockchain.update_pass_instance()
const coopname = config.provider
// === 1. Mongo organization upsert для каждого КУ ===
const mongoUri = process.env.MONGO_URI
if (!mongoUri) throw new Error('MONGO_URI не задан')
const generator = new Generator()
await generator.connect(mongoUri)
for (const b of BRANCHES) {
const orgData: Cooperative.Users.IOrganizationData = {
username: b.braname,
type: 'coop',
short_name: b.short_name,
full_name: b.full_name,
represented_by: {
first_name: '',
last_name: '',
middle_name: '',
position: 'председатель кооперативного участка',
based_on: b.based_on,
},
country: 'Российская Федерация',
city: b.city,
fact_address: b.fact_address,
full_address: b.fact_address,
email: b.email,
phone: b.phone,
details: { inn: '', ogrn: '', kpp: '' },
}
try {
await generator.save('organization', orgData)
log(`mongo organization ${b.braname} upserted`)
} catch (e: any) {
log(`mongo organization ${b.braname} failed: ${e.message ?? e}`)
}
}
// === 2. ончейн createbranch (actor=coopname active, ключ кооператива) ===
for (const b of BRANCHES) {
try {
// Проверяем не существует ли КУ уже on-chain
const existing = await blockchain.api.rpc.get_table_rows({
code: 'branch',
scope: coopname,
table: 'branches',
lower_bound: b.braname,
upper_bound: b.braname,
limit: 1,
}).catch(() => ({ rows: [] }))
if (existing.rows && existing.rows.length > 0) {
log(`skip createbranch ${b.braname} (already on-chain)`)
continue
}
const data: BranchContract.Actions.CreateBranch.ICreateBranch = {
coopname,
braname: b.braname,
trustee: b.trustee,
}
await blockchain.api.transact({
actions: [{
account: BranchContract.contractName.production,
name: BranchContract.Actions.CreateBranch.actionName,
authorization: [{ actor: coopname, permission: 'active' }],
data,
}],
}, { blocksBehind: 3, expireSeconds: 30 })
log(`createbranch ${b.braname} → trustee=${b.trustee} OK`)
} catch (e: any) {
log(`createbranch ${b.braname} failed: ${e.message ?? e}`)
}
}
// === 3. addtrusted для krg ===
for (const trusted of TRUSTED_KRG) {
try {
const data: BranchContract.Actions.AddTrusted.IAddTrusted = {
coopname,
braname: 'krg',
trusted,
}
await blockchain.api.transact({
actions: [{
account: BranchContract.contractName.production,
name: BranchContract.Actions.AddTrusted.actionName,
authorization: [{ actor: coopname, permission: 'active' }],
data,
}],
}, { blocksBehind: 3, expireSeconds: 30 })
log(`addtrusted krg ← ${trusted} OK`)
} catch (e: any) {
log(`addtrusted ${trusted} failed: ${e.message ?? e}`)
}
}
await generator.close?.()
log('done')
}
main().then(() => process.exit(0)).catch((e) => { console.error(e); process.exit(1) })
@@ -0,0 +1,121 @@
/**
* Pre-flight: эмиссия RUB в Main Wallet тестовых пайщиков marketplace.
*
* orderer/order-create через marketplaceCreateOrder требует положительный
* баланс в кошельке program_id=1 (Кошелёк) — иначе backend тихо отказывает
* на этапе подсчёта стоимости, диалог остаётся открытым и тест-сценарий
* не понимает что произошло (см. task #177).
*
* Pre-conditions, которые скрипт ожидает выполненными:
* - soviet::participants[username] существует, status=accepted
* (`add-plain-participant.ts` + автоматический accept через провайдера
* или registrator уже отработали)
* - wallet::users(scope=voskhod)[username].programs содержит program_id=1
* (ЦПП Кошелёк подписан хотя бы один раз — это произошло либо через
* UI-onboarding 4 диалогов, либо через `soviet::sndagreement(wallet)`
* в seed-capital phase08)
*
* Если оба условия выполнены — `wallet::createdeposit + gateway::completeincome`
* проходят без membership-исключения и зачисляют RUB на свободный остаток.
*
* Идемпотентность: создаётся новый deposit_hash каждый раз; повторный запуск
* добавит ещё одну эмиссию (а не upsert'нет существующую). Это нормально для
* dev-стенда — баланс просто увеличится.
*
* Запуск:
* MONGO_URI=mongodb://127.0.0.1:27047/cooperative-x \
* pnpm --filter @coopenomics/boot exec esno \
* src/scripts/seed-marketplace-deposits.ts
*/
import path from 'node:path'
import fs from 'node:fs'
import { createHash, randomInt } from 'node:crypto'
import { fileURLToPath } from 'node:url'
import { WalletContract, GatewayContract } from 'cooptypes'
import Blockchain from '../../blockchain'
import config from '../../configs'
const log = (...a: unknown[]) => console.error('[seed-marketplace-deposits]', ...a)
const COOPNAME = 'voskhod'
const here = path.dirname(fileURLToPath(import.meta.url))
const REPO_ROOT = path.resolve(here, '../../../..')
const STATE_DIR = path.join(REPO_ROOT, 'components/docs-harness/state/participants')
// Целевые пайщики и сумма эмиссии. Сумма с запасом, чтобы хватало на несколько
// заказов в magistral II (типичный заказ — 500–5000 RUB) + комиссии.
const TARGETS: Array<{ username: string; amountRub: number }> = [
{ username: 'ivanpetrov', amountRub: 30_000 },
{ username: 'ekaterina', amountRub: 30_000 },
]
function rndHash(): string {
return createHash('sha256').update(`mkt-dep:${Date.now()}:${randomInt(1_000_000_000)}`).digest('hex')
}
async function readFixtureIfPresent(username: string): Promise<{ wif: string } | null> {
const file = path.join(STATE_DIR, `${username}.json`)
if (!fs.existsSync(file)) return null
const data = JSON.parse(fs.readFileSync(file, 'utf8')) as { wif?: string | null }
return data.wif ? { wif: data.wif } : null
}
async function depositToWallet(blockchain: Blockchain, username: string, amountRub: number): Promise<void> {
const depositHash = rndHash()
const quantity = `${amountRub.toFixed(4)} RUB`
log(`wallet::createdeposit ${username}${quantity} (deposit=${depositHash.slice(0, 10)}…)`)
await blockchain.api.transact({
actions: [{
account: WalletContract.contractName.production,
name: WalletContract.Actions.CreateDeposit.actionName,
authorization: [{ actor: COOPNAME, permission: 'active' }],
data: {
coopname: COOPNAME,
username,
deposit_hash: depositHash,
quantity,
},
}],
}, { blocksBehind: 3, expireSeconds: 30 })
log(`gateway::completeincome ${depositHash.slice(0, 10)}`)
await blockchain.api.transact({
actions: [{
account: GatewayContract.contractName.production,
name: GatewayContract.Actions.CompleteIncome.actionName,
authorization: [{ actor: COOPNAME, permission: 'active' }],
data: {
coopname: COOPNAME,
income_hash: depositHash,
},
}],
}, { blocksBehind: 3, expireSeconds: 30 })
}
export async function fund(): Promise<void> {
// Подгружаем WIF фикстур если есть (для случая, когда захотим расширить скрипт
// на подписи самих пайщиков — сейчас deposit подписывается voskhod active).
const fixtureKeys: string[] = []
for (const t of TARGETS) {
const fx = await readFixtureIfPresent(t.username)
if (fx) fixtureKeys.push(fx.wif)
}
const blockchain = new Blockchain(config.network, [...config.private_keys, ...fixtureKeys])
await blockchain.update_pass_instance()
for (const t of TARGETS) {
try {
await depositToWallet(blockchain, t.username, t.amountRub)
log(`${t.username}: +${t.amountRub} RUB зачислены в Main Wallet`)
} catch (e) {
const msg = (e as Error).message ?? String(e)
log(`${t.username}: эмиссия упала — ${msg.slice(0, 200)}`)
log(` проверь: soviet::participants[${t.username}].status='accepted' + wallet::users[${t.username}].programs содержит program_id=1`)
throw e
}
}
log('эмиссия завершена')
}
@@ -0,0 +1,45 @@
/**
* CLI-диспетчер seed-фаз эмиссии L3 кошельков marketplace-фикстур.
*
* pnpm --filter @coopenomics/boot exec esno \
* src/scripts/seed-marketplace-deposits/index.ts <phase>
*
* Используется в harness через meta.prepare = ['marketplace-deposits:fund'].
*
* Фазы:
* - fund — wallet::createdeposit + gateway::completeincome для ekaterina и
* ivanpetrov; восстанавливает баланс Main Wallet для magistral II.
*
* Каждая фаза идемпотентна по failure semantics (ошибка остановит цепь). По
* созданию — каждый запуск добавляет ещё одну эмиссию (баланс растёт), что
* безопасно для dev-стенда.
*/
import { fund } from './fund'
const log = (...a: unknown[]) => console.error('[seed-marketplace-deposits]', ...a)
const phaseMap: Record<string, () => Promise<void>> = {
fund,
}
async function main() {
const args = process.argv.slice(2)
if (args.length === 0) {
console.error('Usage: seed-marketplace-deposits/index.ts <phase>')
console.error(`Available phases: ${Object.keys(phaseMap).join(', ')}`)
process.exit(2)
}
for (const phase of args) {
const fn = phaseMap[phase]
if (!fn) {
console.error(`Неизвестная фаза: «${phase}». Доступно: ${Object.keys(phaseMap).join(', ')}`)
process.exit(2)
}
log(`=== фаза ${phase} ===`)
await fn()
}
}
main()
.then(() => process.exit(0))
.catch((e) => { console.error(e); process.exit(1) })
@@ -0,0 +1,17 @@
/* eslint-disable node/prefer-global/process */
/**
* CLI-обёртка для сидинга карты ПВЗ (3 КУ Подмосковья krg/odn/myt, ACTIVE)
* в `marketplace_ku_details`. Запускается из `extra_reboot.sh` ПОСЛЕ подъёма
* контроллера (он создаёт таблицу через TypeORM synchronize), чтобы свежий
* стенд сразу имел непустой select ПВЗ. Идемпотентно.
*
* pnpm --filter @coopenomics/boot run seed:marketplace-ku
*/
import { seedMarketplaceKuDetails } from '../postgres-init'
seedMarketplaceKuDetails()
.then(() => process.exit(0))
.catch((e) => {
console.error('seed:marketplace-ku failed:', e)
process.exit(1)
})
@@ -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))
})
})
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@coopenomics/cleos",
"version": "2026.5.25-3",
"version": "2026.5.30-alpha-2",
"private": true,
"description": "Обёртка над кошельком cleos для EOSIO блокчейна",
"scripts": {
@@ -359,3 +359,138 @@ topUpAmount: z
- **SDK:**
- `sdk/src/types/controller/extension.types.ts` - копия типов из controller
---
# Канон авторизации столов (grants)
Единый механизм видимости рабочих столов и страниц расширения. Решает проблему
дублирования: раньше «кто что видит» задавалось в ДВУХ несвязанных местах с
разными словарями — `meta.roles` на фронте (core-роли) и access-matrix на бэке
(marketplace capability). Теперь авторизацию вычисляет ТОЛЬКО backend, а фронт
её лишь отражает. Онбординг (гейтинг до принятия ЦПП) — частный случай этого же
механизма, а не отдельный код.
## Принцип
> Авторизацию вычисляет backend один раз. Фронт декларирует СТРУКТУРУ (какие
> роуты есть, их компоненты, заголовки) и НИКОГДА не переопределяет, кому они
> видны.
- **backend** = КТО получает КАКОЕ право (capability) — единый источник истины;
- **frontend** = КАКАЯ страница требует КАКОГО права (`meta.requires`).
`getDesktop` отдаёт по каждому столу набор прав текущего пользователя
(`DesktopWorkspace.grants: string[]`). Фронт показывает страницу/стол, если
её `meta.requires` входит в `grants` (plain `includes`).
## Контракт
**1. Порт расширения** (`domain/desktop/ports/extension-grants.port.ts`):
```typescript
interface IExtensionDesktopGrantsProvider {
readonly extensionName: string; // 'market'
resolveGrants(ctx: IDesktopGrantsContext): Promise<string[]>; // гость → []
}
```
`IDesktopGrantsContext` = `{ coopname, username?, userRole?, userStatus?, config }`.
**2. Глобальный реестр** (`application/desktop/extension-grants.registry.ts`,
`@Global`): расширение САМО регистрирует свой провайдер в `onModuleInit`
(self-registration) — платформа не импортирует модули расширений (нет цикла).
**3. `getDesktop`** (`desktop.interactor.ts`): для каждого desktop-расширения
зовёт `grantsRegistry.resolve(name, ctx)` и кладёт результат в
`DesktopWorkspace.grants` всех столов расширения. Резолвер `getDesktop`
под `OptionalGqlJwtAuthGuard` (открыт и гостю: состав ответа зависит от того,
кто спрашивает).
**4. Маршруты фронта** (`extensions/<name>/install.ts`): каждый роут объявляет
`meta.requires: 'Resource:action'` вместо `meta.roles`.
**5. Стор фронта** (`entities/Desktop/model/store.ts`) — ЕДИНАЯ логика видимости,
все потребители (переключатель столов, левое меню, cmdk-поиск, навигационный
гард) зовут её, а не фильтруют сами:
- `isPageVisible(meta, workspaceName)` — для пункта/доступа к странице;
- `isWorkspaceVisible(menuItem)` — стол виден, если есть ≥1 доступная страница;
- `hasRouteAccess(matchedNames, meta)` — для навигационного гарда (глушит лишь
при явном невыполненном `requires`);
- `firstAccessibleRoute(extensionName)` — для редиректа после установки/включения.
## Разворот грантов (важная деталь)
Фронт сверяет `requires` простым `includes` и НЕ знает иерархию охвата. Поэтому
право `<base>:all` РАЗВОРАЧИВАЕТСЯ провайдером в подмножества `:own` / `:own-KU`
/ `:to-self` — та же иерархия, что в `canAccess` (marketplace-access-matrix).
Так у админа (`Warehouse:read:all`) проходит требование оператора
(`Warehouse:read:own-KU`), а вся policy остаётся на backend
(см. `marketplace/application/access/marketplace-grants.ts`).
## Онбординг как частный случай грантов
Отдельного кода гейтинга больше нет. Провайдер market
(`marketplace-desktop-grants.provider.ts`) при `config.coopAcceptance.accepted
!== true` отдаёт:
- председателю — только `['Extension:configure']`;
- остальным — `[]`.
Страница «Подключение ЦПП» требует `Extension:configure`; все прочие admin-страницы
требуют `Order:read:all` / `Whitelist:manage`, прочие столы — `Offer:read` /
`Warehouse:read:own-KU`. Поэтому до принятия ЦПП виден лишь Стол администратора
с одной страницей подключения и только председателю; у всех остальных — ничего.
После принятия провайдер отдаёт полный набор по ролям, и столы появляются.
## Маппинг видимости market (как воспроизведена прежняя картина по ролям)
| Стол | requires страниц | Кому виден (после ЦПП) |
|----------------|-----------------------------|-----------------------------------|
| market | `Offer:read`, `Order:read:own`, … | любой онбординутый пайщик |
| market-supplier| `Offer:read` | любой онбординутый пайщик |
| market-pvz | `Warehouse:read:own-KU` | оператор КУ + председатель (admin) |
| market-admin | `Order:read:all` | совет + председатель |
| └ category-whitelist | `Whitelist:manage` | только председатель |
| └ onboarding/coop-cpp | `Extension:configure`| только председатель (до и после) |
## Миграционная безопасность (опционально)
- Расширение БЕЗ провайдера → `grants === undefined` → фронт работает по-старому
(видимость по `meta.roles`). Стор сам выбирает ветку: grant-стол (по
`requires`) или legacy (по `roles`). capital/soviet/participant не тронуты.
- Роут с `requires` → новый канон; роут без `requires` в grant-столе скрыт
(иначе пустой набор грантов открыл бы его); роут с `roles` без grants → legacy.
## Жизненный цикл / ограничения
- `getDesktop` (и грантов) пере-вычисляется при **старте приложения** и при
**включении** расширения; НЕ реактивен в рантайме. После принятия ЦПП советом
столы появятся после перезагрузки страницы. Для MVP приемлемо.
- Источник **безопасности** — backend-резолверы расширения (guard + access-matrix);
grants на фронте — UX-слой поверх, не замена enforcement.
- **Codegen обязателен:** поле `grants` в `DesktopWorkspace` — это изменение
GraphQL-схемы. Нужно `generate-schema``generate-client` → сборка SDK; в
SDK добавлен `grants: true` в `selectors/desktop/workspaceSelector.ts`. До
регена SDK не запрашивает поле, фронт видит `grants===undefined` и market
падает на legacy (а `meta.roles` у market удалены → временно «видно всем»).
## Связанные файлы
- **Backend:**
- `src/domain/desktop/ports/extension-grants.port.ts` — порт + токен.
- `src/application/desktop/extension-grants.registry.ts``@Global` реестр.
- `src/application/desktop/interactors/desktop.interactor.ts` — сборка грантов.
- `src/application/desktop/resolvers/desktop.resolver.ts``OptionalGqlJwtAuthGuard`.
- `src/application/auth/{guards/optional-graphql-jwt-auth.guard,decorators/optional-current-user.decorator}.ts`.
- `src/extensions/marketplace/application/desktop/marketplace-desktop-grants.provider.ts`.
- `src/extensions/marketplace/application/access/marketplace-grants.ts` — разворот.
- **SDK:** `src/selectors/desktop/workspaceSelector.ts``grants: true`.
- **Frontend:**
- `desktop/src/entities/Desktop/model/store.ts``isPageVisible` /
`isWorkspaceVisible` / `hasRouteAccess` / `firstAccessibleRoute`.
- `desktop/src/processes/navigation-guard-setup/index.ts` — единый гард.
- `desktop/src/widgets/Desktop/{WorkspaceMenu,SecondLevelMenuList}/*`,
`desktop/src/entities/CmdkMenu/model/store.ts` — потребители.
- `desktop/extensions/market/install.ts``meta.requires` на всех роутах.
- `desktop/src/features/Extension/{InstallExtension,EnableExtension}/ui/*Button.vue`
— редирект через `firstAccessibleRoute`.
@@ -0,0 +1,86 @@
# Платформенное ядро L1-онбординга расширений (приём ЦПП Советом)
Как кооператив «подключает» расширение (capital/marketplace/chairman): Совет утверждает
набор документов ончейн, и состояние расширения **автоматически** меняется по факту
реально принятого решения. Механизм — общий, платформенный. Расширение лишь декларирует
свои шаги; весь flow (генерация проекта решения → публикация в Совет → отслеживание
ончейн-принятия → смена состояния → перерегистрация оферт) делает ядро.
НЕ путать с L2 (выбор ЦПП пайщиком при регистрации) и L3 (fallback-gate на столе).
Здесь только L1 — приём кооперативом.
## Где что лежит (controller)
| Слой | Путь | Роль |
|---|---|---|
| Spec шага | `domain/onboarding/dto/extension-onboarding-step-spec.ts` | `{extension_name, step_key, event_type: SOVIET_DECISION\|MEET_DECISION, vars_field, generator: free_decision\|meet, default_title, order}` |
| Реестр шагов | `domain/onboarding/services/onboarding-steps-registry.service.ts` | in-memory; реализует `OnboardingStepRegistrationPort` (registerStep/unregisterStepsByExtension) + `OnboardingStepQueryPort` (getStep/getStepsByExtension) |
| Generic-сервис | `domain/onboarding/services/extension-onboarding.service.ts` | `getState()` / `completeStep()` — собирает состояние из config-полей `onboarding_<step_key>_done`/`_hash`; в `completeStep` гонит free_decision/meet generator |
| Generic-слушатель | `domain/onboarding/services/extension-onboarding-events.service.ts` | `@OnEvent(DecisionTrackedEvent)`: ставит `_done=true`, при всех done эмитит `ONBOARDING_COMPLETED_EVENT` |
| Generic-резолвер | `application/onboarding/resolvers/extension-onboarding.resolver.ts` | Query `getExtensionOnboardingState(extension_name)` (chairman/member/user) + Mutation `completeExtensionOnboardingStep(data)` (chairman) |
| Трекинг решений | `infrastructure/decision-tracking/adapters/decision-tracking.adapter.ts` | `@OnEvent(action::soviet::newdecision / action::meet::newdecision)``processDecision(hash)` → ищет tracking-rule по hash → эмитит `DecisionTrackedEvent` |
| Lifecycle | `domain/extension/services/extension-lifecycle-domain.service.ts` | `@OnEvent(ONBOARDING_COMPLETED_EVENT)``restartApp(extension_name)``module.initialize(config)` |
| Глобальный модуль | `domain/onboarding/onboarding-domain.module.ts` | `@Global`; экспортит порты `ONBOARDING_STEP_REGISTRATION_PORT` / `_QUERY_PORT` — доступны любому расширению без импорта |
## Поток «Совет принял ончейн → состояние сменилось»
```
1. Председатель: completeExtensionOnboardingStep({extension_name, step_key, title, question, decision})
decision = HTML самого документа (фронт заранее рендерит через generateDocument(registry_id))
2. ExtensionOnboardingService.completeStep → runFreeDecisionGenerator:
- freeDecisionPort.createProjectOfFreeDecision(...)
- generateProjectOfFreeDecisionDocument(registry=ProjectFreeDecision) → hash
- publishProjectOfFreeDecision(...) → проект решения уходит в Совет ончейн
- decisionTrackingPort.registerTrackingRule({hash, event_type, vars_field,
metadata:{onboarding_step, project_id, extension}})
- сохраняет onboarding_<step>_hash в extensions.config → шаг 'in_progress'
3. Совет голосует и принимает решение ончейн (soviet::exec → soviet::newdecision)
4. DecisionTrackingAdapter ловит action::soviet::newdecision → processDecision(hash)
→ находит rule по hash → эмитит DecisionTrackedEvent(metadata)
5. ExtensionOnboardingEventsService: onboarding_<step>_done=true → шаг 'completed'
когда ВСЕ шаги done → emit ONBOARDING_COMPLETED_EVENT({extension_name})
6. ExtensionLifecycleDomainService → restartApp(extension_name) → initialize(config)
расширение в initialize() перерегистрирует оферты/программы и выставляет свой
L1-флаг готовности (для marketplace — coopAcceptance.accepted=true).
```
Состояние шага для фронта: `done → completed`; `hash есть, done нет → in_progress` («Ожидаем
решение совета»); иначе `pending`.
## Как подключить НОВОЕ расширение (рецепт)
1. **Шаги**: `extensions/<ext>/application/onboarding/register-<ext>-onboarding-steps.ts`
функция зовёт `port.unregisterStepsByExtension(name)` + `port.registerStep({...})` на каждый
документ. step_key и vars_field обычно совпадают; order задаёт порядок утверждения.
2. **Wiring**: в плагине расширения `@Inject(ONBOARDING_STEP_REGISTRATION_PORT)` и вызвать
`register<Ext>OnboardingSteps(port)` в `initialize()`. Порт глобальный — импорт модуля не нужен.
3. **НЕ добавлять** расширение в `LEGACY_EXTENSIONS_WITH_OWN_LISTENER`
(`extension-onboarding-events.service.ts`) — тогда generic-слушатель сам обслужит флаги и
completion. Свой events-сервис нужен ТОЛЬКО legacy (chairman/capital хардкодят step-mapping).
4. **L1-флаг готовности**: в `initialize()` вывести флаг видимости из платформенного состояния
(например, «все onboarding_*_done → accepted=true»). Идемпотентно — initialize зовётся и на
boot, и на auto-restart после ONBOARDING_COMPLETED.
5. **Документы**: каждый документ = запись в `cooptypes/src/cooperative/registry/<id>.Name/` +
генератор в `factory/src/{Actions,Templates}/<id>.Name.ts` + регистрация в индексах factory и
диспетчере `factory/src/index.ts`. Пересобрать cooptypes и factory (`pnpm run build`).
6. **Фронт**: переиспользовать `desktop/src/shared/ui/CouncilOnboarding` (`CouncilOnboardingCard`,
проп `config: ICouncilOnboardingConfig`, эмит `step-submit`). Composable строит config из
`Queries.Onboarding.GetExtensionOnboardingState`, заранее рендерит HTML документов через
`Mutations.Documents.GenerateDocument``step.decision`), а `step-submit` шлёт
`Mutations.Onboarding.CompleteExtensionOnboardingStep`. **GraphQL-схему менять не нужно**
generic onboarding API уже в схеме и SDK.
## Marketplace как пример (Эпик 12)
- Шаги: `marketplace_provision` (Положение, registry **1107.MarketplaceProgramTemplate**) и
`marketplace_offer_template` (Оферта-шаблон, registry **1100.MarketplaceOfferTemplate**).
Регистрация: `extensions/marketplace/application/onboarding/register-marketplace-onboarding-steps.ts`.
- L1-флаг: `MarketplacePlugin.initialize()``syncCoopAcceptanceFromOnboarding()` выставляет
`config.coopAcceptance.accepted=true`, когда оба `onboarding_marketplace_*_done`. Этот флаг
читают `MarketplaceDesktopGrantsProvider` (видимость столов) и `marketplaceCppStatus`.
- Документ Положения 1107 — placeholder («РЫБА») до юр. редакции, по образцу 1100/999.
- Прежняя stub-мутация `marketplaceAcceptCpp` (ручное проставление флага) больше не
используется фронтом — осталась как мёртвый код для последующей чистки.
Связано: `CONTROLLER_DECISIONS_TRACKING_FACTORY.md`, `BLAGOROST-CHAIRMAN-ONBOARDING.md`,
`EXTENSIONS_SCHEMA_SYSTEM.md` (грантовый канон видимости столов).
@@ -20,9 +20,6 @@ title: Выдача займа пайщику
slug: debt
status: proposed
contract: capital
summary: >
Пайщик получает из паевого фонда кооператива беспроцентный целевой заём
на срок проекта; возврат происходит при сдаче акта-2 проекта.
purpose: >
«Заём пайщику» — кооператив выдаёт пайщику беспроцентный целевой заём
из паевого фонда на срок проекта. Заявку последовательно одобряют
@@ -281,17 +278,3 @@ operations:
(Кт 58), сумма зачитывается в паевой фонд (Дт 80) и появляется как
доступный остаток на SHARE_FUND_PAY пайщика.
# ── Секция 7. Связи ─────────────────────────────────────────────────────────
related:
- process_type: p.cap.rid
id: public_capital_rid_process
relation: triggers
note: >
Возврат займа (o.cap.repay) технически происходит в процессе
«Приём РИД» при подписании акта-2 проекта.
- process_type: p.reg.accept
id: public_registrator_accept_process
relation: provides
note: >
Заём может получить только активный пайщик кооператива.
@@ -22,10 +22,6 @@ title: Приём инвестиции в программу
slug: invest
status: proposed
contract: capital
summary: >
Пайщик направляет ранее внесённые паевые взносы деньгами в программу
«Благорост» — без оформления договора, простой переброской средств между
своими кошельками.
purpose: >
«Приём инвестиции в программу» — пайщик переводит часть своих ранее
внесённых паевых средств в инвестицию в программу «Благорост».
@@ -125,18 +121,3 @@ operations:
кооператива, но в учёте пайщика они теперь считаются инвестицией
в «Благорост», а не свободными паевыми средствами.
# ── Секция 7. Связи ─────────────────────────────────────────────────────────
related:
- process_type: p.wal.depo
id: public_wallet_deposit_process
relation: provides
note: >
Чтобы инвестировать, у пайщика должен быть остаток на SHARE_FUND_PAY —
его создаёт «Внесение паевого взноса» (p.wal.depo).
- process_type: p.cap.rid
id: public_capital_rid_process
relation: triggers
note: >
После инвестирования участник может коммитить РИД в проект
программы «Благорост» (p.cap.rid).
@@ -20,10 +20,6 @@ title: Приём имущественного паевого взноса
slug: property
status: proposed
contract: capital
summary: >
Пайщик передаёт кооперативу имущество (не деньги, не РИД) как паевой
взнос. Процесс многоступенчатый: предложение, одобрение, авторизация,
два акта приёма-передачи.
purpose: >
«Приём имущественного паевого взноса» — пайщик передаёт кооперативу
имущество (не деньги, не результат интеллектуальной деятельности)
@@ -265,18 +261,3 @@ operations:
«Благорост» у пайщика (w.cap.blago). Двойная запись Дт 51 / Кт 80 —
кооператив принял имущество и оно учтено как часть паевого фонда.
# ── Секция 7. Связи ─────────────────────────────────────────────────────────
related:
- process_type: p.cap.rid
id: public_capital_rid_process
relation: affects
note: >
Альтернативный имущественный путь — приём результата интеллектуальной
деятельности (p.cap.rid). РИД отличается от обычного имущества схемой
двух последовательных операций и привязкой к проекту.
- process_type: p.reg.accept
id: public_registrator_accept_process
relation: provides
note: >
Имущественный взнос может внести только активный пайщик кооператива.
@@ -26,12 +26,6 @@ title: Приём результата интеллектуальной деят
slug: rid
status: proposed
contract: capital
summary: >
Участник программы «Благорост» оформляет результат интеллектуальной
деятельности (РИД) как имущественный паевой взнос: коммит → одобрение
мастера → заявление о результате → одобрение председателем → авторизация
советом → акт приёма-передачи (две подписи) → распределение паевого
взноса между Цифровым Кошельком и программой «Благорост».
purpose: >
«Приём результата интеллектуальной деятельности» — участник проекта
программы «Благорост» оформляет результат своей работы (РИД) как
@@ -584,32 +578,3 @@ operations:
пайщика. Бухгалтерская запись была сделана ранее в o.cap.accept
на полную сумму сегмента — здесь только перемещение по кошелькам.
# ── Секция 7. Связи ─────────────────────────────────────────────────────────
related:
- process_type: p.cap.invest
id: public_capital_invest_process
relation: provides
note: >
Чтобы коммитить РИД в проект, участник должен быть инвестором
«Благорост» — это устанавливает p.cap.invest.
- process_type: sov.authpkg
relation: triggers
note: >
Авторизация результата советом (`authrslt`) выполняется через
универсальный процесс автоматизированного принятия решений.
- process_type: p.cap.debt
id: public_capital_debt_process
relation: triggers
note: >
Если у участника был беспроцентный заём проекта, на акте-2
срабатывает дополнительная операция o.cap.repay — заём закрывается
из паевого фонда.
- process_type: p.cap.prop
id: public_capital_property_process
relation: affects
note: >
Альтернативный имущественный путь — приём имущественного паевого
взноса (p.cap.prop), отдельный от РИД.
+3 -1
View File
@@ -67,6 +67,7 @@ static constexpr eosio::name _free_decision_action = "freedecision"_n;
static constexpr eosio::name _change_action = "change"_n;
static constexpr eosio::name _product_contribution_action = "productcntr"_n;
static constexpr eosio::name _product_return_action = "productrtrn"_n;
static constexpr eosio::name _marketplace_writeoff_action = "mktwroff"_n; ///< Списание скоропорта по решению совета (p.mkt.wroff)
// capitalization linked actions
@@ -109,6 +110,7 @@ static const std::set<eosio::name> soviet_actions = {
//MARKETPLACE
"authoffs2c"_n, //заявление на взнос имуществом
"authoffc2r"_n, //заявление на возврат паевого взноса имуществом
"mktwroff"_n, //проект списания скоропорта на повестку совета (p.mkt.wroff)
};
//program_ids
@@ -239,7 +241,7 @@ static constexpr uint64_t _capital_program_id = 4;
#ifdef IS_TESTNET
static constexpr uint64_t MIN_SOVIET_MEMBERS_COUNT = 1; /*!< минимальное количество членов совета (тест) */
#else
static constexpr uint64_t MIN_SOVIET_MEMBERS_COUNT = 5; /*!< минимальное количество членов совета (прод) */
static constexpr uint64_t MIN_SOVIET_MEMBERS_COUNT = 3; /*!< минимальное количество членов совета (прод) */
#endif
/**
@@ -73,4 +73,20 @@ namespace Document {
return document.hash == checksum256{};
}
/**
* @brief Удалить документ по имени из вектора
* @param docs Вектор документов
* @param name Имя документа для удаления
* @return true если документ был найден и удалён
*/
inline bool remove_document(std::vector<named_document>& docs, const name& name) {
for (auto it = docs.begin(); it != docs.end(); ++it) {
if (it->name == name) {
docs.erase(it);
return true;
}
}
return false;
}
}
@@ -32,17 +32,22 @@ enum class AccountType : uint8_t {
* состояния «принятый коммит» (паевой взнос имуществом в переходе РИД
* в программу Благорост): commit → Dr 08 / Cr 80, accept → Dr 04 / Cr 08.
*
* Состав (6 счетов):
* Состав (8 счетов):
*
* - 04 — Нематериальные активы (РИД, принятые в паевой фонд)
* - 08 — Вложения во внеоборотные активы (промежуточное состояние)
* - 10 — Материалы (склад имущества на кооперативных участках; per-КУ субсчета)
* - 51 — Расчётный счёт
* - 58 — Финансовые вложения (выданные пайщикам беспроцентные займы)
* - 80 — Паевой фонд (складочный капитал)
* - 86 — Целевое финансирование (без субсчетов)
* - 91 — Прочие доходы и расходы (транзит при выдаче имущества пайщику
* и при утилизации скоропорта по «Столу заказов»)
*
* Счёт 67 удалён: беспроцентные займы пайщикам идут через 58/51, без 67.
*
* Счета 10 и 91 добавлены 2026-05-11 под членскую модель «Стола заказов».
*
* Правило кодирования id: integer(code) * 1000.
* Детализация — через wallets (см. wallets.hpp).
*
@@ -52,12 +57,16 @@ struct ledger2_accounts {
// Активы
static constexpr uint64_t INTANGIBLE_ASSETS = 4 * 1000; ///< 04 — Нематериальные активы (А)
static constexpr uint64_t NON_CURRENT_INVESTMENTS = 8 * 1000; ///< 08 — Вложения во внеоборотные активы (А)
static constexpr uint64_t MATERIALS = 10 * 1000; ///< 10 — Материалы (А; склад имущества на КУ, per-КУ субсчета)
static constexpr uint64_t BANK_ACCOUNT = 51 * 1000; ///< 51 — Расчётный счёт (А)
static constexpr uint64_t FINANCIAL_INVESTMENTS = 58 * 1000; ///< 58 — Финансовые вложения (А)
// Пассивы
static constexpr uint64_t SHARE_FUND = 80 * 1000; ///< 80 — Паевой фонд (П)
static constexpr uint64_t TARGET_RECEIPTS = 86 * 1000; ///< 86 — Целевое финансирование (П)
// Активно-пассивный
static constexpr uint64_t OTHER_INCOME_EXPENSES = 91 * 1000; ///< 91 — Прочие доходы и расходы (А/П; транзит на выдаче и при утилизации скоропорта)
};
/**
@@ -74,18 +83,20 @@ struct Ledger2AccountMeta {
};
/**
* @brief Хардкод-справочник плана счетов (MVP, 7 записей).
* @brief Хардкод-справочник плана счетов (MVP, 8 записей).
*
* `constexpr std::array` + `string_view` — чтобы не было dynamic init
* при загрузке контракта и тип был полностью заморожен на этапе сборки.
*/
inline constexpr std::array<Ledger2AccountMeta, 6> LEDGER2_ACCOUNT_MAP = {{
inline constexpr std::array<Ledger2AccountMeta, 8> LEDGER2_ACCOUNT_MAP = {{
{ ledger2_accounts::INTANGIBLE_ASSETS, "Нематериальные активы", AccountType::ACTIVE },
{ ledger2_accounts::NON_CURRENT_INVESTMENTS, "Вложения во внеоборотные активы", AccountType::ACTIVE },
{ ledger2_accounts::MATERIALS, "Материалы", AccountType::ACTIVE },
{ ledger2_accounts::BANK_ACCOUNT, "Расчётный счёт", AccountType::ACTIVE },
{ ledger2_accounts::FINANCIAL_INVESTMENTS, "Финансовые вложения", AccountType::ACTIVE },
{ ledger2_accounts::SHARE_FUND, "Паевой фонд (складочный капитал)", AccountType::PASSIVE },
{ ledger2_accounts::TARGET_RECEIPTS, "Целевое финансирование", AccountType::PASSIVE },
{ ledger2_accounts::OTHER_INCOME_EXPENSES, "Прочие доходы и расходы", AccountType::ACTIVE_PASSIVE },
}};
static constexpr size_t LEDGER2_ACCOUNT_MAP_SIZE = LEDGER2_ACCOUNT_MAP.size();
@@ -64,6 +64,7 @@ namespace operations {
inline constexpr eosio::name COMPLETE_WITHDRAW = "o.wal.wthcpl"_n; ///< Завершение возврата паевого взноса (Dr 80 / Cr 51, BURN с WITHDRAW_PENDING — деньги уходят из системы, без wallet_to).
inline constexpr eosio::name REQUEST_WITHDRAW = "o.wal.wthreq"_n; ///< Запрос на возврат паевого: TRANSFER SHARE_FUND_PAY → WITHDRAW_PENDING (резерв, без Dr/Cr).
inline constexpr eosio::name DECLINE_WITHDRAW = "o.wal.wthdec"_n; ///< Отклонение запроса на возврат: TRANSFER WITHDRAW_PENDING → SHARE_FUND_PAY (снятие резерва, без Dr/Cr).
inline constexpr eosio::name CONVERT_TO_MEMBER = "o.wal.conv"_n; ///< Конвертация цифрового рубля в универсальный членский кошелёк (Dr 80 / Cr 86, TRANSFER SHARE_FUND_PAY → CK_MEMBER). Conditional-шаг серии createorder в «Столе заказов».
}
// capital
@@ -82,10 +83,17 @@ namespace operations {
inline constexpr eosio::name CONVERT_TO_BLAGO = "o.cap.cnvbl"_n; ///< Конвертация сегмента: РИД → ЦПП «Благорост» (TRANSFER GENERATOR_FUND → BLAGOROST_FUND, без Dr/Cr — бухпроводка уже была сделана в ACCEPT_RID).
}
// marketplace
// marketplace — членская модель «Стола заказов».
namespace marketplace {
inline constexpr eosio::name CONFIRM_SUPPLY = "o.mkt.supply"_n; ///< Подтверждение поставки (Dr 51 / Cr 80, ISSUE SHARE_FUND_PAY).
inline constexpr eosio::name CONFIRM_RECEIPT = "o.mkt.recv"_n; ///< Подтверждение получения (Dr 80 / Cr 51, TRANSFER SHARE_FUND_PAY → SUPPLIER_PAYMENTS).
inline constexpr eosio::name LOCK_ORDER = "o.mkt.lock"_n; ///< Резервирование средств заказчика под конкретный Order (TRANSFER w.wal.share → w.mkt.order, Dr 80 / Cr 86 — паевой переходит в целевое финансирование на резерв-кошелёк). Единственный обязательный шаг ledger2 при createorder.
inline constexpr eosio::name CONVERT_TO_MKT_MEMBER = "o.mkt.conv"_n; ///< Конвертация паевого взноса в членский кошелёк «Стола заказов» (TRANSFER w.wal.share → w.mkt.member, Dr 80 / Cr 86). Доплата по факту (signiss2, actual > ordered) идёт ИМЕННО с членского программы — паевой сперва конвертируется сюда, напрямую с паевого не списываем.
inline constexpr eosio::name LOCK_FROM_MEMBER = "o.mkt.lockm"_n; ///< Добор резерва заказа с членского «Стола заказов» (TRANSFER w.mkt.member → w.mkt.order, без Dr/Cr — оба кошелька на 86). Парный к o.mkt.conv шаг доплаты по факту: после конвертации добирает резерв под этот же Order.
inline constexpr eosio::name UNLOCK_ORDER = "o.mkt.unlock"_n; ///< Снятие резерва при отмене Order'а или недовыдаче (TRANSFER w.mkt.order → w.mkt.member, без Dr/Cr — оба кошелька на 86). Средства возвращаются на членский «Стола заказов» (не на универсальный членский) — остаются в программе и могут быть потрачены на следующие заказы.
inline constexpr eosio::name PURCHASE_FROM_SUPPLIER = "o.mkt.purch"_n; ///< Приёмка имущества кооперативом по АПП приёмки от поставщика (Dr 10 / Cr 86, NONE — только бухпроводка, кошельки не двигаются; имущество — аналитика по 10). Атомарно с PAY_SUPPLIER на закрывающей подписи председателя.
inline constexpr eosio::name PAY_SUPPLIER = "o.mkt.payout"_n; ///< Оплата поставщику с расчётного счёта по факту приёмки (Dr 86 / Cr 51, ISSUE ∅ → SUPPLIER_PAYMENTS). Атомарно с PURCHASE_FROM_SUPPLIER.
inline constexpr eosio::name CONSUME_BY_MEMBER = "o.mkt.consum"_n; ///< Выдача имущества пайщику по АПП выдачи (BURN с w.mkt.order, Dr 86 / Cr 10 — сжигание резерва заказа и выбытие имущества со склада через целевое финансирование).
inline constexpr eosio::name RETURN_BY_MEMBER = "o.mkt.return"_n; ///< Гарантийный возврат имущества пайщиком — compensating forward к CONSUME_BY_MEMBER (ISSUE ∅ → w.mkt.member, Dr 10 / Cr 86 — восстановление средств на членском «Стола заказов» заказчика и возврат имущества на склад). Реверты ledger2::revert в Столе заказов не используются.
inline constexpr eosio::name WRITE_OFF_PERISHABLE = "o.mkt.wroff"_n; ///< Утилизация скоропорта со склада (NONE Dr 86 / Cr 10). По протоколу совета.
}
// soviet
@@ -265,16 +273,82 @@ static constexpr OperationRegistryEntry OPERATION_REGISTRY[] = {
ledger2_accounts::SHARE_FUND, ledger2_accounts::FINANCIAL_INVESTMENTS,
"Возврат беспроцентного займа пайщика по акту-2" },
// 12. Подтверждение поставки: Dr 51 / Cr 80, ISSUE SHARE_FUND_PAY
{ operations::marketplace::CONFIRM_SUPPLY, processes::marketplace::REQUEST, WalletOp::ISSUE, eosio::name{}, ledger2_wallets::SHARE_FUND_PAY,
ledger2_accounts::BANK_ACCOUNT, ledger2_accounts::SHARE_FUND,
"Подтверждение поставки товара/услуги" },
// 12a. p.mkt.supply: Резервирование под Order (TRANSFER w.wal.share → w.mkt.order,
// Dr 80 / Cr 86). Единственный обязательный шаг ledger2 при createorder.
// Паевой переходит в целевое финансирование на резерв-кошелёк под
// конкретный заказ.
{ operations::marketplace::LOCK_ORDER, processes::marketplace::SUPPLY, WalletOp::TRANSFER,
ledger2_wallets::SHARE_FUND_PAY, ledger2_wallets::MARKETPLACE_ORDER_LOCK,
ledger2_accounts::SHARE_FUND, ledger2_accounts::TARGET_RECEIPTS,
"Резервирование под заказ" },
// 13. Подтверждение получения: Dr 80 / Cr 51, TRANSFER SHARE_FUND_PAY → SUPPLIER_PAYMENTS
{ operations::marketplace::CONFIRM_RECEIPT, processes::marketplace::REQUEST, WalletOp::TRANSFER,
ledger2_wallets::SHARE_FUND_PAY, ledger2_wallets::SUPPLIER_PAYMENTS,
ledger2_accounts::SHARE_FUND, ledger2_accounts::BANK_ACCOUNT,
"Подтверждение получения товара/услуги — выплата поставщику" },
// 12a². p.mkt.supply: Конвертация паевого в членский «Стола заказов» под доплату
// (TRANSFER w.wal.share → w.mkt.member, Dr 80 / Cr 86). signiss2 при
// actual > ordered: доплата идёт ИМЕННО с членского программы — паевой
// сперва конвертируется сюда, напрямую с паевого не списываем.
{ operations::marketplace::CONVERT_TO_MKT_MEMBER, processes::marketplace::SUPPLY, WalletOp::TRANSFER,
ledger2_wallets::SHARE_FUND_PAY, ledger2_wallets::MARKETPLACE_MEMBER_FUND,
ledger2_accounts::SHARE_FUND, ledger2_accounts::TARGET_RECEIPTS,
"Конвертация паевого в членский «Стола заказов» под доплату" },
// 12a³. p.mkt.supply: Добор резерва заказа с членского «Стола заказов»
// (TRANSFER w.mkt.member → w.mkt.order, без Dr/Cr — оба кошелька на 86).
// Парный к CONVERT_TO_MKT_MEMBER шаг доплаты по факту.
{ operations::marketplace::LOCK_FROM_MEMBER, processes::marketplace::SUPPLY, WalletOp::TRANSFER,
ledger2_wallets::MARKETPLACE_MEMBER_FUND, ledger2_wallets::MARKETPLACE_ORDER_LOCK,
0, 0,
"Добор резерва заказа с членского «Стола заказов»" },
// 12b. p.mkt.supply: Снятие резерва (TRANSFER w.mkt.order → w.mkt.member,
// без Dr/Cr — оба кошелька на 86). Срабатывает на cancelorder /
// declineorder / expireorder; для signiss2 — на разницу при
// actual < ordered. Средства возвращаются на членский «Стола заказов»
// (не на универсальный членский) — остаются в программе.
{ operations::marketplace::UNLOCK_ORDER, processes::marketplace::SUPPLY, WalletOp::TRANSFER,
ledger2_wallets::MARKETPLACE_ORDER_LOCK, ledger2_wallets::MARKETPLACE_MEMBER_FUND,
0, 0,
"Снятие резерва при отмене заказа" },
// 12c. p.mkt.supply: Приёмка имущества кооперативом по АПП приёмки
// (Dr 10 / Cr 86, NONE — только бухпроводка, кошельки не двигаются).
// Имущество — аналитикой по счёту 10 (per-КУ субсчета), без отдельного кошелька.
// Атомарно с PAY_SUPPLIER на закрывающей подписи председателя АПП приёмки.
{ operations::marketplace::PURCHASE_FROM_SUPPLIER, processes::marketplace::SUPPLY, WalletOp::NONE,
eosio::name{}, eosio::name{},
ledger2_accounts::MATERIALS, ledger2_accounts::TARGET_RECEIPTS,
"Приёмка имущества кооперативом по АПП приёмки" },
// 12d. p.mkt.supply: Оплата поставщику с расчётного счёта
// (Dr 86 / Cr 51, ISSUE ∅ → w.mkt.payout). Атомарно с PURCHASE_FROM_SUPPLIER.
{ operations::marketplace::PAY_SUPPLIER, processes::marketplace::SUPPLY, WalletOp::ISSUE,
eosio::name{}, ledger2_wallets::SUPPLIER_PAYMENTS,
ledger2_accounts::TARGET_RECEIPTS, ledger2_accounts::BANK_ACCOUNT,
"Оплата поставщику с расчётного счёта по факту приёмки" },
// 12e. p.mkt.supply: Выдача имущества пайщику по АПП выдачи
// (BURN с w.mkt.order, Dr 86 / Cr 10 — сжигание резерва заказа и
// выбытие имущества со склада через целевое финансирование).
{ operations::marketplace::CONSUME_BY_MEMBER, processes::marketplace::SUPPLY, WalletOp::BURN,
ledger2_wallets::MARKETPLACE_ORDER_LOCK, eosio::name{},
ledger2_accounts::TARGET_RECEIPTS, ledger2_accounts::MATERIALS,
"Выдача имущества пайщику по АПП выдачи" },
// 12f. p.mkt.return: Гарантийный возврат имущества пайщиком
// (ISSUE ∅ → w.mkt.member, Dr 10 / Cr 86 — восстановление средств на
// членском «Стола заказов» заказчика и возврат имущества на склад).
// Compensating forward к CONSUME_BY_MEMBER; ledger2::revert в Столе
// заказов не используется.
{ operations::marketplace::RETURN_BY_MEMBER, processes::marketplace::RETURN, WalletOp::ISSUE,
eosio::name{}, ledger2_wallets::MARKETPLACE_MEMBER_FUND,
ledger2_accounts::MATERIALS, ledger2_accounts::TARGET_RECEIPTS,
"Гарантийный возврат — восстановление средств и имущества" },
// 12g. p.mkt.wroff: Утилизация скоропорта со склада (NONE Dr 86 / Cr 10).
// По протоколу совета.
{ operations::marketplace::WRITE_OFF_PERISHABLE, processes::marketplace::WRITEOFF, WalletOp::NONE,
eosio::name{}, eosio::name{},
ledger2_accounts::TARGET_RECEIPTS, ledger2_accounts::MATERIALS,
"Утилизация скоропорта" },
// 14. Конвертация в AXN: Dr 80 / Cr 86, TRANSFER SHARE_FUND_PAY → DELEGATE_FEES
{ operations::soviet::CONVERT_AXN, processes::soviet::AXN_CONVERT, WalletOp::TRANSFER,
@@ -15,7 +15,10 @@
* явно разрешённая модель мульти-операционных процессов:
* - processes::registrator::ACCEPT ← o.reg.payent + o.reg.putmin
* - processes::capital::RID ← o.cap.commit + o.cap.accept (+ o.cap.repay) + o.cap.cnvshr/o.cap.cnvbl
* - processes::marketplace::REQUEST ← o.mkt.supply + o.mkt.recv
* - processes::marketplace::SUPPLY ← o.mkt.lock + o.mkt.unlock + o.mkt.purch +
* o.mkt.payout + o.mkt.consum
* - processes::marketplace::RETURN ← o.mkt.return
* - processes::marketplace::WRITEOFF ← o.mkt.wroff
*
* Одноактовые процессы: `capital::IMPORT`, `capital::PROPERTY`,
* `capital::INVEST`, `soviet::AXN_CONVERT` (process_type совпадает с
@@ -57,7 +60,9 @@ namespace processes {
// marketplace
namespace marketplace {
inline constexpr eosio::name REQUEST = "p.mkt.reqst"_n; ///< Цикл запроса маркетплейса (o.mkt.supply + o.mkt.recv).
inline constexpr eosio::name SUPPLY = "p.mkt.supply"_n; ///< Прямая поставка-приобретение имущества (5 операций: o.mkt.lock + o.mkt.unlock + o.mkt.purch + o.mkt.payout + o.mkt.consum).
inline constexpr eosio::name RETURN = "p.mkt.return"_n; ///< Гарантийный возврат имущества пайщиком — compensating forward к o.mkt.consum (o.mkt.return).
inline constexpr eosio::name WRITEOFF = "p.mkt.wroff"_n; ///< Утилизация скоропорта со склада КУ (o.mkt.wroff, по протоколу совета).
}
// soviet
@@ -65,8 +65,10 @@ struct ledger2_wallets {
static constexpr eosio::name GENERATOR_FUND = "w.cap.gen"_n; ///< Генератор — единый агрегированный кошелёк программы (COOPERATIVE — кооперативный пул, без L3-разреза по пайщику; L3-разрез из ADR-009 отменён из-за несовместимости с CRPS-перераспределением, см. wallets.hpp:107)
static constexpr eosio::name PREIMP_FUND = "w.cap.preimp"_n; ///< Первичный учёт РИД-взносов до перехода на электронный учёт (USER_SHARED; o.cap.preimp / o.cap.drppre)
// marketplace — выплаты
static constexpr eosio::name SUPPLIER_PAYMENTS = "w.mkt.payout"_n; ///< Выплаты поставщикам (sink RECEIVE_CONFIRM, COOPERATIVE)
// marketplace — резерв под Order + членский «Стола заказов» + выплаты
static constexpr eosio::name MARKETPLACE_ORDER_LOCK = "w.mkt.order"_n; ///< ЦПП «Стол Заказов» — резерв средств пайщика под конкретный Order (USER_SHARED). TRANSFER w.wal.share → w.mkt.order на createorder (Дт 80 / Кт 86); обратный TRANSFER на w.mkt.member при cancel/decline/expire (без проводки); BURN с w.mkt.order на signiss2 (Дт 86 / Кт 10).
static constexpr eosio::name MARKETPLACE_MEMBER_FUND = "w.mkt.member"_n; ///< ЦПП «Стол Заказов» — членский кошелёк пайщика программы (USER_SHARED, счёт 86). Сюда возвращается остаток резерва при отмене/недовыдаче и гарантийном возврате; сюда же конвертируется паевой взнос (o.mkt.conv) при доплате по факту — списание под заказ идёт ИМЕННО с членского программы, не с паевого напрямую.
static constexpr eosio::name SUPPLIER_PAYMENTS = "w.mkt.payout"_n; ///< Выплаты поставщикам (sink PAYOUT, COOPERATIVE)
};
/**
@@ -95,13 +97,15 @@ struct Ledger2WalletMeta {
WalletKind kind;
};
inline constexpr std::array<Ledger2WalletMeta, 15> LEDGER2_WALLET_REGISTRY = {{
// USER_SHARED (5) — L3-разрез по пайщику
{ ledger2_wallets::MIN_SHARE_FUND, "Минимальный паевой взнос", WalletKind::USER_SHARED },
{ ledger2_wallets::SHARE_FUND_PAY, "Паевой взнос пайщика", WalletKind::USER_SHARED },
{ ledger2_wallets::CK_MEMBER, "ЦК — членская часть пайщика", WalletKind::USER_SHARED },
{ ledger2_wallets::BLAGOROST_FUND, "ЦПП «Благорост» — единый кошелёк программы у пайщика", WalletKind::USER_SHARED },
{ ledger2_wallets::PREIMP_FUND, "Первичный учёт РИД-взносов до перехода на электронный учёт", WalletKind::USER_SHARED },
inline constexpr std::array<Ledger2WalletMeta, 17> LEDGER2_WALLET_REGISTRY = {{
// USER_SHARED (7) — L3-разрез по пайщику
{ ledger2_wallets::MIN_SHARE_FUND, "Минимальный паевой взнос", WalletKind::USER_SHARED },
{ ledger2_wallets::SHARE_FUND_PAY, "Паевой взнос пайщика", WalletKind::USER_SHARED },
{ ledger2_wallets::CK_MEMBER, "ЦК — членская часть пайщика", WalletKind::USER_SHARED },
{ ledger2_wallets::BLAGOROST_FUND, "ЦПП «Благорост» — единый кошелёк программы у пайщика", WalletKind::USER_SHARED },
{ ledger2_wallets::PREIMP_FUND, "Первичный учёт РИД-взносов до перехода на электронный учёт", WalletKind::USER_SHARED },
{ ledger2_wallets::MARKETPLACE_ORDER_LOCK,"ЦПП «Стол Заказов» — резерв под заказ у пайщика", WalletKind::USER_SHARED },
{ ledger2_wallets::MARKETPLACE_MEMBER_FUND,"ЦПП «Стол Заказов» — членский кошелёк пайщика программы", WalletKind::USER_SHARED },
// COOPERATIVE (10) — единый кооперативный баланс, без L3
// GENERATOR_FUND переведён сюда из USER_SHARED (см. wallets.hpp:64) —
@@ -230,13 +234,15 @@ struct Ledger2WalletProgramMapping {
uint64_t required_program_id; // 0 = исключение (без проверки)
};
inline constexpr std::array<Ledger2WalletProgramMapping, 6> LEDGER2_USER_SHARED_PROGRAM_MAPPING = {{
{ ledger2_wallets::MIN_SHARE_FUND, 0 /* w.reg.minshr — без проверки */ },
{ ledger2_wallets::SHARE_FUND_PAY, 1 /* ЦК */ },
{ ledger2_wallets::CK_MEMBER, 1 /* ЦК */ },
{ ledger2_wallets::BLAGOROST_FUND, 4 /* Благорост */ },
{ ledger2_wallets::GENERATOR_FUND, 3 /* Генератор */ },
{ ledger2_wallets::PREIMP_FUND, 0 /* w.cap.preimp — РИД-учёт до перехода на электронный учёт, без проверки */ },
inline constexpr std::array<Ledger2WalletProgramMapping, 8> LEDGER2_USER_SHARED_PROGRAM_MAPPING = {{
{ ledger2_wallets::MIN_SHARE_FUND, 0 /* w.reg.minshr — без проверки */ },
{ ledger2_wallets::SHARE_FUND_PAY, 1 /* ЦК */ },
{ ledger2_wallets::CK_MEMBER, 1 /* ЦК */ },
{ ledger2_wallets::BLAGOROST_FUND, 4 /* Благорост */ },
{ ledger2_wallets::GENERATOR_FUND, 3 /* Генератор */ },
{ ledger2_wallets::PREIMP_FUND, 0 /* w.cap.preimp — РИД-учёт до перехода на электронный учёт, без проверки */ },
{ ledger2_wallets::MARKETPLACE_ORDER_LOCK, 2 /* Marketplace */ },
{ ledger2_wallets::MARKETPLACE_MEMBER_FUND,2 /* Marketplace */ },
}};
/**
@@ -1,7 +1,7 @@
#pragma once
#include <functional>
#include <optional>
#include <set>
#include <string>
#include <eosio/crypto.hpp>
@@ -9,93 +9,132 @@
#include "../../consts.hpp"
#include "../../domain/document_core.hpp"
#include "../../domain/table_ledger2_userwallets.hpp"
#include "../../domain/table_marketplace_orders.hpp"
#include "../../domain/table_marketplace_return_requests.hpp"
#include "../../domain/table_marketplace_writeoff_proposals.hpp"
#define AUTH_SIGNATURE eosio::name coopname, checksum256 request_hash, document2 authorization
using auth_interface = void(AUTH_SIGNATURE);
/**
* @brief Canonical helpers контракта marketplace (Story 11.1).
*
* Donor-helpers (`get_request_by_hash`, `get_shipment_by_hash`, namespace
* `DocumentNames`, `marketplace_callback_actions`) удалены вместе с
* соответствующими actions и таблицами (AR30).
*
* Этот файл содержит только утилиты доступа к canonical-сущностям трёх
* процессов p.mkt.supply / p.mkt.return / p.mkt.wroff и helper'ы для
* проверки доступного баланса (для createorder guard'а Locked Decision L6).
*/
namespace Marketplace {
using namespace eosio;
static const std::set<eosio::name> marketplace_callback_actions = {
"authoffs2c"_n,
"authoffc2r"_n,
"authordcont"_n,
"authordret"_n,
"declineacc"_n,
// ── Orders ──────────────────────────────────────────────────────────────
inline std::optional<order> get_order_by_hash(eosio::name coopname, const checksum256& order_hash) {
orders_index orders(_marketplace, coopname.value);
auto idx = orders.get_index<"byhash"_n>();
auto it = idx.find(order_hash);
if (it == idx.end()) return std::nullopt;
return *it;
}
inline order get_order_by_hash_or_fail(eosio::name coopname, const checksum256& order_hash,
const std::string& msg = "Заказ не найден по хэшу") {
auto o = get_order_by_hash(coopname, order_hash);
eosio::check(o.has_value(), msg);
return *o;
}
inline void update_order(eosio::name coopname, uint64_t order_id, const std::function<void(order&)>& fn) {
orders_index orders(_marketplace, coopname.value);
auto it = orders.find(order_id);
eosio::check(it != orders.end(), "Заказ не найден по id");
orders.modify(it, _marketplace, [&](auto& o) { fn(o); });
}
// ── Return requests ─────────────────────────────────────────────────────
inline std::optional<return_request> get_return_request_by_hash(eosio::name coopname,
const checksum256& request_hash) {
return_requests_index requests(_marketplace, coopname.value);
auto idx = requests.get_index<"byhash"_n>();
auto it = idx.find(request_hash);
if (it == idx.end()) return std::nullopt;
return *it;
}
inline return_request get_return_request_by_hash_or_fail(eosio::name coopname,
const checksum256& request_hash,
const std::string& msg = "Заявление на возврат не найдено по хэшу") {
auto r = get_return_request_by_hash(coopname, request_hash);
eosio::check(r.has_value(), msg);
return *r;
}
inline void update_return_request(eosio::name coopname, uint64_t request_id,
const std::function<void(return_request&)>& fn) {
return_requests_index requests(_marketplace, coopname.value);
auto it = requests.find(request_id);
eosio::check(it != requests.end(), "Заявление на возврат не найдено по id");
requests.modify(it, _marketplace, [&](auto& r) { fn(r); });
}
// ── Writeoff proposals ──────────────────────────────────────────────────
inline std::optional<writeoff_proposal> get_writeoff_proposal_by_hash(eosio::name coopname,
const checksum256& proposal_hash) {
writeoff_proposals_index proposals(_marketplace, coopname.value);
auto idx = proposals.get_index<"byhash"_n>();
auto it = idx.find(proposal_hash);
if (it == idx.end()) return std::nullopt;
return *it;
}
inline writeoff_proposal get_writeoff_proposal_by_hash_or_fail(eosio::name coopname,
const checksum256& proposal_hash,
const std::string& msg = "Проект списания не найден по хэшу") {
auto p = get_writeoff_proposal_by_hash(coopname, proposal_hash);
eosio::check(p.has_value(), msg);
return *p;
}
inline void update_writeoff_proposal(eosio::name coopname, uint64_t proposal_id,
const std::function<void(writeoff_proposal&)>& fn) {
writeoff_proposals_index proposals(_marketplace, coopname.value);
auto it = proposals.find(proposal_id);
eosio::check(it != proposals.end(), "Проект списания не найден по id");
proposals.modify(it, _marketplace, [&](auto& p) { fn(p); });
}
// ── Cross-contract read: ledger2 wallet/userwallet balances ─────────────
//
// Используется в createorder для guard'а Locked Decision L6 (без отрицательного
// баланса) — проверка достаточности средств заказчика на паевом кошельке
// перед вызовом o.mkt.lock.
//
// ВАЖНО: контракт marketplace не вызывает ledger2::walletop напрямую, а только
// читает state (RAM-таблицы wallets2 / userwallets через cross-contract scope).
// Все мутации идут через `Ledger2::apply` (см. lib/core/ledger2/ledger2.hpp).
struct UserWalletAvailable {
eosio::asset available = eosio::asset(0, _root_govern_symbol);
eosio::asset blocked = eosio::asset(0, _root_govern_symbol);
bool exists = false;
};
inline eosio::name get_valid_marketplace_action(const eosio::name &action) {
eosio::check(marketplace_callback_actions.contains(action), "Недопустимое имя действия marketplace");
return action;
}
static std::optional<request> get_request_by_hash(eosio::name coopname, checksum256 request_hash) {
requests_index requests(_marketplace, coopname.value);
auto idx = requests.get_index<"byhash"_n>();
auto req = idx.find(request_hash);
if (req != idx.end()) {
return *req;
inline UserWalletAvailable get_user_wallet_balance(eosio::name coopname,
eosio::name wallet_id,
eosio::name username) {
// userwallets_index — глобальный typedef в lib/domain/table_ledger2_userwallets.hpp.
// Для cross-contract read берём scope = coopname.value, code = _ledger2.
userwallets_index user_wallets(_ledger2, coopname.value);
auto idx = user_wallets.get_index<"byuserwallet"_n>();
auto it = idx.find(combine_ids(wallet_id.value, username.value));
if (it == idx.end()) {
return UserWalletAvailable{};
}
return std::nullopt;
return UserWalletAvailable{ it->available, it->blocked, true };
}
static std::optional<shipment> get_shipment_by_hash(eosio::name coopname, checksum256 shipment_hash) {
shipments_index shipments(_marketplace, coopname.value);
auto idx = shipments.get_index<"byhash"_n>();
auto ship = idx.find(shipment_hash);
if (ship != idx.end()) {
return *ship;
}
return std::nullopt;
}
static request get_request_by_hash_or_fail(eosio::name coopname, checksum256 request_hash,
const std::string &error_msg = "Заявка не найдена по хэшу") {
auto request_opt = get_request_by_hash(coopname, request_hash);
eosio::check(request_opt.has_value(), error_msg);
return *request_opt;
}
static shipment get_shipment_by_hash_or_fail(eosio::name coopname, checksum256 shipment_hash,
const std::string &error_msg = "Перевозка не найдена по хэшу") {
auto shipment_opt = get_shipment_by_hash(coopname, shipment_hash);
eosio::check(shipment_opt.has_value(), error_msg);
return *shipment_opt;
}
namespace DocumentNames {
static constexpr const name RETURN_STMT = "returnstmt"_n;
static constexpr const name CONVERT_FROM = "convertfrom"_n;
static constexpr const name CONVERT_TO = "convertto"_n;
static constexpr const name CONTRIB_STMT = "contribstmt"_n;
static constexpr const name CONTRIB_AUTH = "contribauth"_n;
static constexpr const name RETURN_AUTH = "returnauth"_n;
static constexpr const name RECEIVE_ACT = "receiveact"_n;
static constexpr const name RECEIVE_ACT_CONF = "receiveconf"_n;
static constexpr const name TRANSPORT1 = "transport1"_n;
static constexpr const name TRANSPORT2 = "transport2"_n;
static constexpr const name TRANSPORT3 = "transport3"_n;
static constexpr const name TRANSPORT4 = "transport4"_n;
static constexpr const name SUPPLY_ACT = "supplyact"_n;
static constexpr const name SUPPLY_ACT_CONF = "supplyconf"_n;
static constexpr const name SHIPMENT_ACT = "shipmentact"_n;
static constexpr const name DELIVERY_ACT = "deliveryact"_n;
static constexpr const name SHIPMENT_SEND_ACT = "shsendact"_n;
static constexpr const name SHIPMENT_LOADING_ACT = "shloadact"_n;
static constexpr const name SHIPMENT_ARRIVE_ACT = "sharriveact"_n;
static constexpr const name SHIPMENT_RECV_ACT = "shrecvact"_n;
static constexpr const name WDISPUTE = "wdispute"_n;
static constexpr const name WRETURN_AUTH = "wreturnauth"_n;
static constexpr const name WSUPPLY_AUTH = "wsupplyauth"_n;
static constexpr const name WRETURN_ACT = "wreturnact"_n;
static constexpr const name WOFFER_ACT = "wofferact"_n;
static constexpr const name WACCEPT_ACT = "wacceptact"_n;
} // namespace DocumentNames
} // namespace Marketplace
@@ -0,0 +1,102 @@
#pragma once
#include <eosio/asset.hpp>
#include <eosio/crypto.hpp>
#include <eosio/eosio.hpp>
#include <string>
#include "../../core/utils.hpp"
/**
* @file memo.hpp
* @brief Человекочитаемые memo для marketplace ledger2-операций.
*
* Аналог `Capital::Memo` (см. `cpp/capital/domain/entities/memo.hpp`).
* Текст memo попадает в журнал ledger2 (поле `journal.memo`) и в выписки
* пайщика / отчёт бухгалтеру — это пользовательский слой, поэтому никаких
* технических токенов ("p.mkt.supply", "L6", op_code) здесь быть не должно.
*
* Параметризуется значимыми идентификаторами (id Order'а) — для трассировки
* в выписке без необходимости резолвить hash через backend.
*/
namespace Marketplace::Memo {
// ---------------------------------------------------------------- p.mkt.supply
inline std::string get_create_order_block_memo(uint64_t order_id) {
return "Резерв средств под заказ имущества № " + std::to_string(order_id) + " в Столе заказов";
}
inline std::string get_create_order_assign_memo(uint64_t order_id) {
return "Целевое назначение взноса под заказ имущества № " + std::to_string(order_id) + " в Столе заказов";
}
inline std::string get_create_order_convert_memo(uint64_t order_id) {
return "Конвертация паевого взноса в членский для заказа имущества № " + std::to_string(order_id) + " в Столе заказов";
}
inline std::string get_cancel_order_memo(uint64_t order_id) {
return "Возврат резерва по отменённому заказу имущества № " + std::to_string(order_id) + " в Столе заказов";
}
inline std::string get_decline_order_memo(uint64_t order_id) {
return "Возврат резерва по отклонённому поставщиком заказу имущества № " + std::to_string(order_id) + " в Столе заказов";
}
inline std::string get_expire_order_memo(uint64_t order_id) {
return "Возврат резерва по неисполненному в срок заказу имущества № " + std::to_string(order_id) + " в Столе заказов";
}
inline std::string get_purchase_from_supplier_memo(uint64_t order_id) {
return "Приёмка имущества от поставщика по заказу № " + std::to_string(order_id) + " на склад кооператива";
}
inline std::string get_pay_supplier_memo(uint64_t order_id) {
return "Оплата поставщику имущества по заказу № " + std::to_string(order_id);
}
inline std::string get_signiss2_correction_less_memo(uint64_t order_id) {
return "Возврат разницы пайщику: фактически выдано меньше заказанного по заказу имущества № " + std::to_string(order_id);
}
inline std::string get_signiss2_correction_more_convert_memo(uint64_t order_id) {
return "Конвертация паевого взноса в членский на доплату по заказу имущества № " + std::to_string(order_id) + " (фактически выдано больше заказанного)";
}
inline std::string get_signiss2_correction_more_assign_memo(uint64_t order_id) {
return "Целевое назначение взноса на доплату по заказу имущества № " + std::to_string(order_id) + " (фактически выдано больше заказанного)";
}
inline std::string get_signiss2_correction_more_block_memo(uint64_t order_id) {
return "Резерв на доплату по заказу имущества № " + std::to_string(order_id) + " (фактически выдано больше заказанного)";
}
inline std::string get_consume_by_member_memo(uint64_t order_id) {
return "Выдача имущества пайщику по заказу № " + std::to_string(order_id) + ": выбытие со склада";
}
inline std::string get_consume_transit_close_memo(uint64_t order_id) {
return "Выдача имущества пайщику по заказу № " + std::to_string(order_id) + ": списание целевого назначения членского взноса";
}
// ---------------------------------------------------------------- p.mkt.return
inline std::string get_return_by_member_memo(uint64_t return_request_id, uint64_t order_id) {
return "Гарантийный возврат имущества пайщиком по заявлению № " + std::to_string(return_request_id) + " (исходный заказ № " + std::to_string(order_id) + "): восстановление членского взноса";
}
inline std::string get_return_transit_close_memo(uint64_t return_request_id, uint64_t order_id) {
return "Гарантийный возврат имущества пайщиком по заявлению № " + std::to_string(return_request_id) + " (исходный заказ № " + std::to_string(order_id) + "): возврат имущества на склад";
}
// ---------------------------------------------------------------- p.mkt.wroff
inline std::string get_writeoff_memo(uint64_t proposal_id, uint64_t item_index) {
return "Утилизация скоропорта по решению совета № " + std::to_string(proposal_id) + ", позиция " + std::to_string(item_index + 1) + ": выбытие со склада";
}
inline std::string get_writeoff_transit_close_memo(uint64_t proposal_id, uint64_t item_index) {
return "Утилизация скоропорта по решению совета № " + std::to_string(proposal_id) + ", позиция " + std::to_string(item_index + 1) + ": списание целевого назначения";
}
} // namespace Marketplace::Memo
@@ -65,9 +65,14 @@
#include "table_ledger2_meta.hpp"
#include "table_loan_debts.hpp"
#include "table_loan_summaries.hpp"
#include "table_marketplace_requests.hpp"
#include "table_marketplace_segments.hpp"
#include "table_marketplace_shipments.hpp"
// marketplace (Story 11.1, canonical) — anchor-таблицы трёх процессов
// p.mkt.supply / p.mkt.return / p.mkt.wroff. Donor-таблицы (requests/segments/
// shipments) удалены вместе с donor-actions (AR30). Batch (consolidated request)
// — backend-only, on-chain не хранится (Locked Decision L10).
#include "table_marketplace_orders.hpp"
#include "table_marketplace_return_requests.hpp"
#include "table_marketplace_writeoff_proposals.hpp"
// apps (каталог приложений)
#include "table_apps_packages.hpp"
@@ -0,0 +1,175 @@
#pragma once
#include <eosio/asset.hpp>
#include <eosio/crypto.hpp>
#include <eosio/eosio.hpp>
#include <string>
#include "../consts.hpp"
#include "../core/document.hpp"
#include "../core/utils.hpp"
namespace Marketplace {
using namespace eosio;
/**
* @brief Статусы Order'а в процессе p.mkt.supply.
*
* Граф: ∅ → active → cancelled (canceled by orderer | expireorder | declineorder)
* → accepted → supply_prepared → accepted_to_coop
* → ready_to_receive → received
*
* Источник правды — `p.mkt.supply.standard.yaml` секция `states:`.
*
* Промежуточный статус `ship_ready` (после prepship поставщика) удалён:
* после `acceptorder` поставщик автоматически считается обязанным доставить
* партию — двойного подтверждения «принял заявку» + «готов отгружать» не
* требуется (отдельная подпись «готов отгрузить» лишь добавляет шум в UX).
* Переход accepted → supply_prepared идёт сразу через signsupp.
*/
namespace OrderStatus {
inline constexpr eosio::name ACTIVE = "active"_n;
inline constexpr eosio::name CANCELLED = "cancelled"_n;
inline constexpr eosio::name ACCEPTED = "accepted"_n;
inline constexpr eosio::name SUPPLY_PREPARED = "supplyprep"_n;
inline constexpr eosio::name ACCEPTED_TO_COOP = "acceptcoop"_n;
inline constexpr eosio::name READY_TO_RECEIVE = "readyrecv"_n;
inline constexpr eosio::name RECEIVED = "received"_n;
}
/**
* @brief Состояние выплаты поставщику по Order'у (Locked Decision L12, E11
* техдолг 598-16). Выплата идёт через gateway::createoutpay → действие
* кассира → callback `marketplace::payconfirm` / `marketplace::paydecline`.
*
* Допустимые переходы:
* none → pending — `marketplace::payout` отправил inline в gateway.
* pending → completed — gateway::outcomplete → callback `payconfirm`.
* Здесь применяется o.mkt.payout (Дт 86 / Кт 51).
* pending → declined — gateway::outdecline → callback `paydecline`.
* Без ledger-движения; обязательство Кт 86 остаётся.
* declined → pending — повторная попытка `marketplace::payout` после
* исправления реквизитов кассиром.
*/
namespace OrderPayoutStatus {
inline constexpr eosio::name NONE = "none"_n;
inline constexpr eosio::name PENDING = "pending"_n;
inline constexpr eosio::name COMPLETED = "completed"_n;
inline constexpr eosio::name DECLINED = "declined"_n;
}
/**
* @brief On-chain Order — анкер процесса p.mkt.supply.
*
* scope = coopname; primary_key = id; уникальность через `byhash` индекс на
* `order.hash` — этот hash используется как `process_hash` во всех ledger2-
* операциях процесса (BLOCK/UNBLOCK/PURCH/PAYOUT/CONSUM/CONSUM2/RETURN).
*
* Привязка к кооперативным участкам (КУ) идёт через `braname` (см. контракт
* `branch`, таблица `branches`). Председатель / trustee / доверенные лица
* каждого КУ известны контракту `branch` — авторизация подписей актов
* выполняется через `Branch::is_user_authorized(coopname, braname, signer)`,
* а не по сохранённому имени председателя (председатель может делегировать
* подпись доверенному лицу из `coobranch.trusted[]`, состав которого может
* меняться независимо от Order'а).
*
* Точки контракта:
* - `delivery_braname` — КУ выдачи имущества пайщику; задаётся пайщиком на
* createorder и неизменна. Источник проверки signiss1/signiss2/p.mkt.return.
* - `accept_braname` — КУ приёмки от поставщика; заполняется на signsupp
* как параметр action'а (поставщик указывает, в какой КУ сдаёт партию).
* Источник проверки signchair.
* - `current_warehouse_braname` — текущая точка хранения имущества по этому
* Order'у. Заполняется на signchair (= `accept_braname`, имущество на
* приёмном складе) и обновляется на signiss1 (= `delivery_braname`, готово
* к выдаче — фиксирует факт логистической передачи). Бездокументарно —
* промежуточные перемещения по заготовочным КУ контрактом не подписываются;
* точка хранения переходит «скачком» в момент готовности к выдаче.
*
* История внутренних передач между КУ (заготовочный → точка выдачи и т.п.)
* с подписью ТТН — отложена. Backend может реконструировать движение из
* blockchain_actions если потребуется. Поле введено заранее, чтобы не
* добавлять его потом через binary_extension.
*
* `acceptance_act` (АПП приёмки) и `issue_act` (АПП выдачи) хранятся
* полным document2 — это дублирование в случае batch-поставки (один
* физический акт → копия в каждом order'е batch'а), но это допустимо для
* on-chain (минимум данных + hash) и упрощает аудит.
*
* `batch_hash` — opaque ссылка на off-chain consolidated request (Locked
* Decision L10: batch — backend-only сущность). Контракт не валидирует
* существование batch'а, только хранит ссылку для трассировки и группировки
* Order'ов в UI. Все per-batch операции на on-chain делаются per-Order
* (backend проходит циклом по orders батча) — векторов order'ов в action'ах нет.
*
* `actual_quantity` / `fact_cost` заполняются на signiss2 (Story 6.2/6.3).
* До signiss2 равны соответственно `quantity` / `total_cost`.
*
* `warranty_until` — рассчитывается в signiss2 как `now() + warranty_period_secs`
* (period приходит с Offer'а через backend; в `submretrn` валидируется только это поле).
*/
struct [[eosio::table, eosio::contract(MARKETPLACE)]] order {
uint64_t id; ///< внутренний ID
checksum256 hash; ///< process_hash для p.mkt.supply
eosio::name coopname; ///< scope-валидация
eosio::name orderer; ///< пайщик-заказчик
eosio::name offerer; ///< пайщик-поставщик из Offer'а (для acceptorder/declineorder/signsupp guard'а)
checksum256 offer_hash; ///< ссылка на Offer (off-chain в backend)
eosio::name delivery_braname; ///< КУ выдачи (выбран пайщиком на createorder); проверка signiss1/signiss2/p.mkt.return через Branch::is_user_authorized
eosio::name accept_braname; ///< КУ приёмки от поставщика (заполняется на signsupp); проверка signchair через Branch::is_user_authorized
eosio::name current_warehouse_braname; ///< текущая точка хранения; signchair: = accept_braname; signiss1: = delivery_braname (фиксация готовности к выдаче)
uint64_t quantity = 0; ///< заказанное количество
uint64_t actual_quantity = 0; ///< фактически выданное (signiss2); до signiss2 == quantity
eosio::asset unit_price = asset(0, _root_govern_symbol); ///< цена за единицу
eosio::asset total_cost = asset(0, _root_govern_symbol); ///< quantity * unit_price (заблокированная сумма)
eosio::asset fact_cost = asset(0, _root_govern_symbol); ///< actual_quantity * unit_price (после signiss2)
uint32_t warranty_period_secs = 0; ///< из Offer'а — для submretrn гард'а
time_point_sec warranty_until = time_point_sec(0); ///< now() + warranty_period_secs (заполняется в signiss2)
eosio::name status = OrderStatus::ACTIVE; ///< canonical статус
checksum256 batch_hash; ///< opaque ссылка на consolidated request (off-chain)
document2 acceptance_act_signsupp; ///< АПП приёмки — первая подпись поставщика (signsupp)
document2 acceptance_act_signchair; ///< АПП приёмки — финальная подпись председателя приёмного КУ (signchair)
document2 issue_act_signiss1; ///< АПП выдачи — первая подпись председателя КУ выдачи (signiss1)
document2 issue_act_signiss2; ///< АПП выдачи — финальная подпись заказчика (signiss2)
eosio::name payout_status = OrderPayoutStatus::NONE; ///< Locked Decision L12 — состояние выплаты поставщику через gateway (см. namespace OrderPayoutStatus)
std::string payout_decline_reason; ///< Заполняется только при payout_status == DECLINED (текст причины из gateway::outdecline)
uint64_t return_request_id = 0; ///< 0 если активного гарантийного возврата нет
// Все timestamp'ы переходов состояний (createorder/accepted/received_to_coop/
// ready/received/cancelled) восстанавливаются на бэкенде из blockchain_actions[at]
// по соответствующим action'ам — нет смысла держать их в RAM-таблице.
// Единственное исключение — warranty_until (выше): нужен on-chain для
// submretrn guard `now() < warranty_until` без cross-action lookup.
uint64_t primary_key() const { return id; }
checksum256 by_hash() const { return hash; }
uint64_t by_orderer() const { return orderer.value; }
uint64_t by_offerer() const { return offerer.value; }
uint64_t by_status() const { return status.value; }
checksum256 by_batch() const { return batch_hash; }
checksum256 by_offer() const { return offer_hash; }
uint64_t by_delivery_bra() const { return delivery_braname.value; }
uint64_t by_accept_bra() const { return accept_braname.value; }
};
typedef eosio::multi_index<
"orders"_n, order,
eosio::indexed_by<"byhash"_n, eosio::const_mem_fun<order, checksum256, &order::by_hash>>,
eosio::indexed_by<"byorderer"_n, eosio::const_mem_fun<order, uint64_t, &order::by_orderer>>,
eosio::indexed_by<"byofferer"_n, eosio::const_mem_fun<order, uint64_t, &order::by_offerer>>,
eosio::indexed_by<"bystatus"_n, eosio::const_mem_fun<order, uint64_t, &order::by_status>>,
eosio::indexed_by<"bybatch"_n, eosio::const_mem_fun<order, checksum256, &order::by_batch>>,
eosio::indexed_by<"byoffer"_n, eosio::const_mem_fun<order, checksum256, &order::by_offer>>,
eosio::indexed_by<"bydelivbra"_n, eosio::const_mem_fun<order, uint64_t, &order::by_delivery_bra>>,
eosio::indexed_by<"byacceptbra"_n, eosio::const_mem_fun<order, uint64_t, &order::by_accept_bra>>>
orders_index;
} // namespace Marketplace
@@ -1,109 +0,0 @@
#pragma once
#include <eosio/asset.hpp>
#include <eosio/crypto.hpp>
#include <eosio/eosio.hpp>
#include <string>
#include <vector>
#include "../consts.hpp"
#include "../core/document.hpp"
#include "../core/utils.hpp"
#include "document_core.hpp"
namespace Marketplace {
using namespace eosio;
struct [[eosio::table, eosio::contract(MARKETPLACE)]] request {
uint64_t id;
checksum256 hash;
name coopname;
name type;
name status;
name username;
name braname;
name warehouse;
name token_contract;
name receiver_braname;
name supplier_braname;
asset unit_cost;
asset base_cost;
asset membership_fee_amount;
asset total_cost;
uint64_t units;
std::string meta;
name money_contributor;
name product_contributor;
std::vector<Document::named_document> documents;
uint64_t product_lifecycle_secs;
uint64_t warranty_period_secs;
asset cancellation_fee_amount;
time_point_sec warranty_delay_until;
time_point_sec deadline_for_receipt;
bool is_warranty_return = false;
uint64_t warranty_return_id;
time_point_sec created_at;
time_point_sec accepted_at;
time_point_sec supplied_at;
time_point_sec delivered_at;
time_point_sec received_at;
time_point_sec completed_at;
time_point_sec declined_at;
time_point_sec disputed_at;
time_point_sec canceled_at;
uint64_t primary_key() const { return id; }
uint64_t by_coop() const { return coopname.value; }
uint64_t by_status() const { return status.value; }
uint64_t by_type() const { return type.value; }
checksum256 by_hash() const { return hash; }
uint64_t by_username() const { return username.value; }
uint64_t by_created() const { return created_at.sec_since_epoch(); }
uint64_t by_completed() const { return completed_at.sec_since_epoch(); }
uint64_t by_declined() const { return declined_at.sec_since_epoch(); }
uint64_t by_canceled() const { return canceled_at.sec_since_epoch(); }
uint64_t by_warranty_id() const { return warranty_return_id; }
name get_money_contributor() const {
return is_warranty_return ? product_contributor : money_contributor;
}
name get_product_contributor() const {
return is_warranty_return ? money_contributor : product_contributor;
}
name get_payer() const { return get_money_contributor(); }
name get_supplier() const { return get_product_contributor(); }
name get_product_backer() const { return money_contributor; }
name get_defective_supplier() const { return product_contributor; }
};
typedef eosio::multi_index<
"requests"_n, request,
eosio::indexed_by<"bycoop"_n, eosio::const_mem_fun<request, uint64_t, &request::by_coop>>,
eosio::indexed_by<"bystatus"_n, eosio::const_mem_fun<request, uint64_t, &request::by_status>>,
eosio::indexed_by<"bytype"_n, eosio::const_mem_fun<request, uint64_t, &request::by_type>>,
eosio::indexed_by<"byhash"_n, eosio::const_mem_fun<request, checksum256, &request::by_hash>>,
eosio::indexed_by<"byusername"_n, eosio::const_mem_fun<request, uint64_t, &request::by_username>>,
eosio::indexed_by<"bycreated"_n, eosio::const_mem_fun<request, uint64_t, &request::by_created>>,
eosio::indexed_by<"bycompleted"_n, eosio::const_mem_fun<request, uint64_t, &request::by_completed>>,
eosio::indexed_by<"bydeclined"_n, eosio::const_mem_fun<request, uint64_t, &request::by_declined>>,
eosio::indexed_by<"bycanceled"_n, eosio::const_mem_fun<request, uint64_t, &request::by_canceled>>,
eosio::indexed_by<"bywarrantyid"_n, eosio::const_mem_fun<request, uint64_t, &request::by_warranty_id>>>
requests_index;
} // namespace Marketplace
@@ -0,0 +1,103 @@
#pragma once
#include <eosio/asset.hpp>
#include <eosio/crypto.hpp>
#include <eosio/eosio.hpp>
#include <string>
#include <vector>
#include "../consts.hpp"
#include "../core/document.hpp"
#include "../core/utils.hpp"
namespace Marketplace {
using namespace eosio;
/**
* @brief Статусы заявления на гарантийный возврат (процесс p.mkt.return).
*
* Граф: ∅ → pending_review → approved_for_visit → return_accepted (final)
* → rejected_at_ku (final)
* → rejected_remote (final)
*
* Источник правды — `p.mkt.return.standard.yaml` секция `states:`.
*/
namespace ReturnStatus {
inline constexpr eosio::name PENDING_REVIEW = "pendrev"_n;
inline constexpr eosio::name APPROVED_FOR_VISIT = "approvvisit"_n;
inline constexpr eosio::name RETURN_ACCEPTED = "accepted"_n;
inline constexpr eosio::name REJECTED_REMOTE = "rejremote"_n;
inline constexpr eosio::name REJECTED_AT_KU = "rejatku"_n;
}
/**
* @brief On-chain Заявление на гарантийный возврат — анкер процесса p.mkt.return.
*
* scope = coopname; primary_key = id; уникальность через `byhash` индекс на
* `return_request.hash` — этот hash используется как `process_hash` во всех
* ledger2-операциях процесса (RETURN + RETURN2).
*
* Привязка к КУ не сохраняется — она может измениться от шага к шагу
* (председатель delivery-КУ может рассмотреть удалённо, а очный осмотр сделать
* на любом другом КУ; состав доверенных лиц коробки `branches` тоже может
* меняться). На каждом действии (aprretrem/rejretrem/accretrn/rejretrn) braname
* приходит параметром action'а и валидируется через
* `Branch::is_user_authorized(coopname, braname, signer)`. Контракт хранит
* только неизменные участники процесса: orderer / offerer (через original Order)
* и coopname.
*
* Связь с исходным Order'ом — `original_order_id` + `original_order_hash`;
* Order.return_request_id ставится в submretrn для двусторонней связи.
*
* `photos` — vector<checksum256> хешей файлов в bucket'е stol-zakazov:images
* (Story 7.1, AR32). Реальные изображения off-chain в file-storage (PR #359);
* on-chain — только ссылки (hash для дедупликации + URL восстанавливает backend).
*
* `decision_remote` / `decision_visit` — декларативные документы решений
* председателя (rejretrem / accretrn / rejretrn). reason_remote / reason_visit
* — текстовые причины отказа для пользовательского UI (заполняются в
* rejretrem / rejretrn соответственно).
*/
struct [[eosio::table, eosio::contract(MARKETPLACE)]] return_request {
uint64_t id;
checksum256 hash; ///< process_hash для p.mkt.return
eosio::name coopname;
eosio::name orderer; ///< пайщик-заказчик (заявитель)
uint64_t original_order_id; ///< внутренний id Order'а
checksum256 original_order_hash; ///< process_hash оригинального p.mkt.supply
checksum256 original_consume_op_id; ///< ссылка на оригинальный o.mkt.consum (для journal трассировки compensating forward; см. d6 A4)
uint64_t actual_quantity = 0; ///< возвращаемое количество (по умолчанию = order.actual_quantity, может быть меньше)
eosio::asset fact_cost = asset(0, _root_govern_symbol); ///< возвращаемая сумма (actual_quantity * unit_price)
std::string reason_text; ///< причина обращения (≤ 500 символов)
std::vector<checksum256> photos; ///< хеши файлов в bucket'е stol-zakazov:images
eosio::name status = ReturnStatus::PENDING_REVIEW;
document2 statement; ///< заявление пайщика (опционально подписанное)
document2 decision_remote; ///< решение председателя удалённо (aprretrem | rejretrem)
document2 decision_visit; ///< решение председателя по итогам очного осмотра (accretrn | rejretrn)
std::string reason_remote; ///< причина отказа удалённо (для rejretrem)
std::string reason_visit; ///< причина отказа на очном осмотре (для rejretrn)
// Timestamp'ы submretrn/aprretrem/rejretrem/accretrn/rejretrn — на бэкенде
// из blockchain_actions[at]. В контракте никаких guard'ов по датам нет.
uint64_t primary_key() const { return id; }
checksum256 by_hash() const { return hash; }
uint64_t by_orderer() const { return orderer.value; }
uint64_t by_status() const { return status.value; }
uint64_t by_original_order() const { return original_order_id; }
};
typedef eosio::multi_index<
"retrequests"_n, return_request,
eosio::indexed_by<"byhash"_n, eosio::const_mem_fun<return_request, checksum256, &return_request::by_hash>>,
eosio::indexed_by<"byorderer"_n, eosio::const_mem_fun<return_request, uint64_t, &return_request::by_orderer>>,
eosio::indexed_by<"bystatus"_n, eosio::const_mem_fun<return_request, uint64_t, &return_request::by_status>>,
eosio::indexed_by<"byorigorder"_n, eosio::const_mem_fun<return_request, uint64_t, &return_request::by_original_order>>>
return_requests_index;
} // namespace Marketplace
@@ -1,52 +0,0 @@
#pragma once
#include <eosio/eosio.hpp>
#include "../consts.hpp"
#include "document_core.hpp"
namespace Marketplace {
using namespace eosio;
struct [[eosio::table, eosio::contract(MARKETPLACE)]] segment {
uint64_t id;
uint64_t request_id;
name type;
name status;
document2 convert_in;
document2 statement;
uint64_t decision_id;
document2 authorization;
document2 act1;
document2 act2;
document2 convert_out;
document2 transport_act_1;
document2 transport_act_2;
document2 transport_act_3;
document2 transport_act_4;
name coopactor;
name username;
name driver_username;
name receive_from_driver_coopactor;
time_point_sec created_at;
time_point_sec updated_at;
uint64_t primary_key() const { return id; }
uint64_t by_request() const { return request_id; }
uint64_t by_type() const { return type.value; }
uint64_t by_status() const { return status.value; }
};
typedef eosio::multi_index<
"segments"_n, segment,
eosio::indexed_by<"byrequest"_n, eosio::const_mem_fun<segment, uint64_t, &segment::by_request>>,
eosio::indexed_by<"bytype"_n, eosio::const_mem_fun<segment, uint64_t, &segment::by_type>>,
eosio::indexed_by<"bystatus"_n, eosio::const_mem_fun<segment, uint64_t, &segment::by_status>>>
segments_index;
} // namespace Marketplace
@@ -1,55 +0,0 @@
#pragma once
#include <eosio/crypto.hpp>
#include <eosio/eosio.hpp>
#include <vector>
#include "../consts.hpp"
#include "../core/document.hpp"
#include "../core/utils.hpp"
#include "document_core.hpp"
namespace Marketplace {
using namespace eosio;
struct [[eosio::table, eosio::contract(MARKETPLACE)]] shipment {
uint64_t id;
checksum256 hash;
name coopname;
name driver_username;
name source_braname;
name destination_braname;
name status;
std::vector<checksum256> request_hashes;
std::vector<Document::named_document> documents;
time_point_sec created_at;
time_point_sec loaded_at;
time_point_sec delivered_at;
time_point_sec completed_at;
uint64_t primary_key() const { return id; }
checksum256 by_hash() const { return hash; }
uint64_t by_coop() const { return coopname.value; }
uint64_t by_driver() const { return driver_username.value; }
uint64_t by_source() const { return source_braname.value; }
uint64_t by_destination() const { return destination_braname.value; }
uint64_t by_status() const { return status.value; }
uint64_t by_created() const { return created_at.sec_since_epoch(); }
};
typedef eosio::multi_index<
"shipments"_n, shipment,
eosio::indexed_by<"byhash"_n, eosio::const_mem_fun<shipment, checksum256, &shipment::by_hash>>,
eosio::indexed_by<"bycoop"_n, eosio::const_mem_fun<shipment, uint64_t, &shipment::by_coop>>,
eosio::indexed_by<"bydriver"_n, eosio::const_mem_fun<shipment, uint64_t, &shipment::by_driver>>,
eosio::indexed_by<"bysource"_n, eosio::const_mem_fun<shipment, uint64_t, &shipment::by_source>>,
eosio::indexed_by<"bydest"_n, eosio::const_mem_fun<shipment, uint64_t, &shipment::by_destination>>,
eosio::indexed_by<"bystatus"_n, eosio::const_mem_fun<shipment, uint64_t, &shipment::by_status>>,
eosio::indexed_by<"bycreated"_n, eosio::const_mem_fun<shipment, uint64_t, &shipment::by_created>>>
shipments_index;
} // namespace Marketplace
@@ -0,0 +1,120 @@
#pragma once
#include <eosio/asset.hpp>
#include <eosio/crypto.hpp>
#include <eosio/eosio.hpp>
#include <string>
#include <vector>
#include "../consts.hpp"
#include "../core/document.hpp"
#include "../core/utils.hpp"
namespace Marketplace {
using namespace eosio;
/**
* @brief Статусы проекта решения совета о списании скоропорта (процесс p.mkt.wroff).
*
* Граф (выровнен под канонический паттерн «решение совета»
* `soviet::createagenda` + callback'и):
*
* ∅
* ├─ propwroff (admin) ─────────────► proposed
* │ └─ soviet::createagenda(type=mktwroff, callback=onmktwoauth/onmktwodecl)
* ├─ onmktwoauth (callback от soviet) ► authorized (хранит protocol2)
* │ └─ execwroff per-item (backend цикл) ► executed (final)
* └─ onmktwodecl (callback от soviet) ► rejected (final, без ledger2-операций)
*
* Источник правды — `p.mkt.wroff.standard.yaml` секция `states:`.
*/
namespace WroffStatus {
// on-chain имена 1:1 совпадают с YAML; PROPOSED/AUTHORIZED/EXECUTED/REJECTED —
// C++-константы (имя PROPOSED вместо DRAFT — чтобы не конфликтовать с макросом
// DRAFT из lib/consts.hpp).
inline constexpr eosio::name PROPOSED = "proposed"_n;
inline constexpr eosio::name AUTHORIZED = "authorized"_n;
inline constexpr eosio::name EXECUTED = "executed"_n;
inline constexpr eosio::name REJECTED = "rejected"_n;
}
/**
* @brief Позиция к списанию в составе writeoff_proposal.
*
* Может ссылаться:
* - на конкретный Order (если позиция «не выдана первичному заказчику») —
* `source_order_id` != 0;
* - на излишек в результате signiss2 fact > ordered + остаток на складе —
* `source_order_id` == 0, аналитика только через `braname` + `meta`;
* - на возвращённое имущество из p.mkt.return — `source_order_id` ссылка
* через original Order, через который имущество физически вернулось.
*
* `braname` — кооперативный участок (склад) источник списания. Пер-КУ
* аналитика счёта 10. Подпись протокола проверяется через
* `Branch::is_user_authorized(coopname, braname, signer)` — председатель
* соответствующего КУ может делегировать подпись доверенному лицу.
*
* `amount` — сумма к списанию по этой позиции (Дт 91 / Кт 10 в части 1
* и Дт 86 / Кт 91 в части 2 — обе в одной транзакции execwroff).
*
* `meta` — произвольная строка для UI / отчёта (название позиции, причина);
* не валидируется контрактом.
*
* `executed` — true после успешного списания этой позиции (см. execwroff
* per-item action). Когда все items в proposal.items имеют executed=true,
* статус proposal автоматически переходит в EXECUTED.
*/
struct wroff_item {
uint64_t source_order_id = 0; ///< 0 если списание из складского остатка без привязки к order'у
eosio::name braname; ///< КУ-склад источник списания
eosio::asset amount = asset(0, _root_govern_symbol);
std::string meta;
bool executed = false; ///< true после execwroff(proposal_hash, item_index)
};
/**
* @brief On-chain Проект решения совета о списании скоропорта — анкер процесса p.mkt.wroff.
*
* scope = coopname; primary_key = id; уникальность через `byhash` индекс на
* `proposal.hash` — этот hash используется как `process_hash` во всех
* ledger2-операциях процесса (o.mkt.wroff per-item).
*
* `items` — vector<wroff_item> позиций к списанию; на execwroff контракт
* последовательно вызывает `Ledger2::apply(o.mkt.wroff, item.amount, …)`
* для каждой позиции в одной транзакции Antelope.
*
* `protocol` — document2 решения совета (signed_by: council_members).
* Подпись — через стандартный sov.decision-протокол (см. p.mkt.wroff.standard.yaml
* секция documents).
*/
struct [[eosio::table, eosio::contract(MARKETPLACE)]] writeoff_proposal {
uint64_t id;
checksum256 hash; ///< process_hash для p.mkt.wroff
eosio::name coopname;
eosio::name proposed_by; ///< backend / админ — инициатор propwroff
eosio::name decided_by; ///< actor execwroff/declwroff (председатель / совет)
std::vector<wroff_item> items;
eosio::asset total_amount = asset(0, _root_govern_symbol); ///< Σ items.amount (для UI / отчёта)
eosio::name status = WroffStatus::PROPOSED;
document2 protocol; ///< Подписанный советом протокол решения; кладётся в callback onmktwoauth/onmktwodecl
std::string reject_reason; ///< Причина отклонения (можно достать из meta protocol в onmktwodecl)
// Связка с soviet.decisions — через decisions.hash == proposal.hash; backend стыкует обе таблицы по hash.
// Timestamp'ы propwroff / onmktwoauth / onmktwodecl / execwroff фиксируются
// backend'ом из блокчейн-дельт (поле blockchain_actions[at]).
uint64_t primary_key() const { return id; }
checksum256 by_hash() const { return hash; }
uint64_t by_status() const { return status.value; }
};
typedef eosio::multi_index<
"wroffprops"_n, writeoff_proposal,
eosio::indexed_by<"byhash"_n, eosio::const_mem_fun<writeoff_proposal, checksum256, &writeoff_proposal::by_hash>>,
eosio::indexed_by<"bystatus"_n, eosio::const_mem_fun<writeoff_proposal, uint64_t, &writeoff_proposal::by_status>>>
writeoff_proposals_index;
} // namespace Marketplace
+36 -30
View File
@@ -1,37 +1,43 @@
#include "marketplace.hpp"
#include <eosio/transaction.hpp>
// Процесс поставки по заявкам orderoffer
#include "src/deliver_on_offer/orderoffer.cpp"
#include "src/deliver_on_offer/accept.cpp"
#include "src/deliver_on_offer/authcontrib.cpp"
#include "src/deliver_on_offer/authreturn.cpp"
#include "src/deliver_on_offer/declineacc.cpp"
#include "src/deliver_on_offer/supply.cpp"
#include "src/deliver_on_offer/supplcnf.cpp"
// Раскладка по процессам соответствует YAML-стандартам рядом с этим файлом
// (p.mkt.supply.standard.yaml / p.mkt.return.standard.yaml /
// p.mkt.wroff.standard.yaml). Имена подпапок 1:1 совпадают с process_type
// — связь от файла → к стандарту прозрачная.
#include "src/deliver_on_offer/delivered.cpp"
#include "src/deliver_on_offer/receive.cpp"
#include "src/deliver_on_offer/receivecnf.cpp"
#include "src/deliver_on_offer/complete.cpp"
#include "src/deliver_on_offer/decline.cpp"
#include "src/deliver_on_offer/cancel.cpp"
// ── p.mkt.supply (9 actions) ─── Stories Эпиков 4-5-6 ──────────────────
#include "src/p.mkt.supply/createorder.cpp"
#include "src/p.mkt.supply/cancelorder.cpp"
#include "src/p.mkt.supply/expireorder.cpp"
#include "src/p.mkt.supply/acceptorder.cpp"
#include "src/p.mkt.supply/declineorder.cpp"
#include "src/p.mkt.supply/signsupp.cpp"
#include "src/p.mkt.supply/signchair.cpp"
#include "src/p.mkt.supply/payout.cpp"
#include "src/p.mkt.supply/payconfirm.cpp"
#include "src/p.mkt.supply/paydecline.cpp"
#include "src/p.mkt.supply/signiss1.cpp"
#include "src/p.mkt.supply/signiss2.cpp"
// Новая система перевозок
#include "src/shipment/createship.cpp"
#include "src/shipment/signbydriver.cpp"
#include "src/shipment/arrived.cpp"
#include "src/shipment/receiveshipm.cpp"
#include "src/shipment/retransport.cpp"
// ── p.mkt.return (5 actions) ──── Stories Эпика 7 ──────────────────────
#include "src/p.mkt.return/submretrn.cpp"
#include "src/p.mkt.return/aprretrem.cpp"
#include "src/p.mkt.return/rejretrem.cpp"
#include "src/p.mkt.return/accretrn.cpp"
#include "src/p.mkt.return/rejretrn.cpp"
// Диспуты
#include "src/dispute_on_offer/dispute.cpp"
#include "src/dispute_on_offer/wauthorize.cpp"
#include "src/dispute_on_offer/wreturn.cpp"
#include "src/dispute_on_offer/woffer.cpp"
#include "src/dispute_on_offer/waccept.cpp"
// ── p.mkt.wroff (4 actions) ───── Stories Эпика 8 ──────────────────────
// Канонический паттерн «решение совета»: propwroff (admin) → soviet::createagenda
// → onmktwoauth / onmktwodecl (callback от soviet после голосования) → execwroff
// per-item (backend цикл).
#include "src/p.mkt.wroff/propwroff.cpp"
#include "src/p.mkt.wroff/onmktwoauth.cpp"
#include "src/p.mkt.wroff/onmktwodecl.cpp"
#include "src/p.mkt.wroff/execwroff.cpp"
[[eosio::action]] void marketplace::migrate(){
// require_auth(_marketplace);
[[eosio::action]] void marketplace::migrate() {
// Donor-таблиц нет (AR30 — donor-actions удалены вместе с requests/segments/
// shipments). Заглушка остаётся для совместимости с прежним ABI; вызывать
// не имеет эффекта.
require_auth(_marketplace);
}
+349 -71
View File
@@ -1,3 +1,5 @@
#pragma once
#include <eosio/asset.hpp>
#include <eosio/contract.hpp>
#include <eosio/crypto.hpp>
@@ -6,40 +8,68 @@
#include <eosio/system.hpp>
#include <eosio/time.hpp>
#include "../lib/index.hpp"
#include <string>
#include <vector>
#include "../lib/index.hpp"
#include "../lib/core/marketplace/marketplace.hpp"
#include "../lib/core/marketplace/memo.hpp"
#include "../lib/core/branch/branch.hpp"
#include "../lib/core/ledger2/ledger2.hpp"
using namespace eosio;
using namespace Marketplace;
/**
* \ingroup public_contracts
* @brief Класс `marketplace` предоставляет функционал кооперативного маркетплейса, позволяя пользователям
* создавать, обновлять, принимать и отменять заявки на обмен товаров и услуг. Этот контракт служит
* центральной точкой для всех операций обмена в рамках кооперативной экосистемы.
* \ingroup public_contracts
*
* Основные функции класса:
* - Создание и управление заявками типа orderoffer (заказчик → поставщик).
* - Операции обновления, принятия, отказа и завершения обменных операций.
* - Модерация и управление публикацией заявок на обмен.
* - Административные функции, такие как создание идентификаторов и авторизация операций.
*
* ## Процесс поставки orderoffer:
*
* 1. **orderoffer** - заказчик создает заявку с документами на возврат и конвертацию, средства блокируются
* 2. **accept** - поставщик принимает заявку и предоставляет документы на взнос и конвертацию
* 3. **authcontrib/authreturn** - совет авторизует оба заявления раздельно
* 4. **supply** → **supplcnf** - поставка товара и подтверждение председателем КУ
* 5. **deliver1** → **deliver2** → **deliver3** → **deliver4** - этапы транспортировки
* 6. **receive** → **receivecnf** - получение товара заказчиком
* 7. **complete** - завершение поставки после гарантийного периода
*
* ## Документооборот:
*
* Все документы сохраняются в векторе `std::vector<document2> documents` в заявке.
* Каждый этап процесса добавляет необходимые документы в этот вектор.
*
* \note Контракт маркетплейса является центральной точкой экономической активности на платформе.
* \note Система упрощена для работы с одной заявкой вместо двух встречных.
*/
* @brief Контракт `marketplace` — кооперативный «Стол заказов» в режиме
* членских взносов.
*
* Реализует canonical actions трёх процессов из YAML-стандартов:
* - **p.mkt.supply** (9 actions): createorder, cancelorder, expireorder,
* acceptorder, declineorder, signsupp, signchair, signiss1, signiss2.
* - **p.mkt.return** (5 actions): submretrn, aprretrem, rejretrem, accretrn,
* rejretrn.
* - **p.mkt.wroff** (4 actions): propwroff, execwroff, onmktwoauth, onmktwodecl.
* Cписание скоропорта идёт через канонический паттерн «решение совета»:
* backend подписывает Заявление о списании (registry 1106) ключом
* кооператива, вызывает `propwroff` (запись wroffprops::proposed) +
* `soviet::createagenda(type=mktwroff, callback=onmktwoauth/onmktwodecl)`.
* После голосования совета и подписи Протокола (registry 1105) chairman'ом
* soviet::exec автоматически вызывает callback `onmktwoauth` (PROPOSED →
* AUTHORIZED, сохраняется protocol2) или `onmktwodecl` (PROPOSED → REJECTED).
* Только после AUTHORIZED backend циклом по items вызывает `execwroff`
* per-item (o.mkt.wroff на каждой позиции).
*
* Все per-batch операции на on-chain выполняются per-Order (бэкенд
* проходит циклом по Order'ам соответствующего batch'а, объединяя их по
* `batch_hash`). Векторов order_hashes в actions нет — это ограничение
* на размер транзакции в Antelope (тысячи orders в одной транзакции
* не пройдут).
*
* Все ledger2-движения средств — через `Ledger2::apply(_marketplace, …)`,
* никаких прямых wallet/account-операций. 13 marketplace-операций
* зарегистрированы в `lib/core/ledger2/operations.hpp` (`OPERATION_REGISTRY`).
*
* Composite-операции (consum+consum2, return+return2, wroff+wroff2) —
* последовательные `Ledger2::apply` в одной транзакции Antelope (атомарность
* через single-action wrapper).
*
* Авторизация подписей под актами / решениями привязана к кооперативному
* участку (КУ) через контракт `branch` и helper
* `Branch::is_user_authorized(coopname, braname, signer)` — председатель
* КУ может делегировать подпись доверенному лицу из `coobranch.trusted[]`.
*
* Источник правды по логике actions, гардам, state-переходам и
* операциям — три YAML-файла рядом с этим .hpp:
* - `p.mkt.supply.standard.yaml`
* - `p.mkt.return.standard.yaml`
* - `p.mkt.wroff.standard.yaml`
*
* Donor-actions старой клиринговой модели (FR19a, AR30) удалены вместе с
* соответствующими таблицами `Marketplace::request/segment/shipment`.
*/
class [[eosio::contract(MARKETPLACE)]] marketplace : public eosio::contract {
public:
@@ -47,47 +77,295 @@ public:
eosio::datastream<const char *> ds)
: eosio::contract(receiver, code, ds) {}
void apply(uint64_t receiver, uint64_t code, uint64_t action);
// ── p.mkt.supply ─────────────────────────────────────────────────────
/**
* @brief Заказчик размещает заказ на товар из каталога (Story 4.1).
* Один шаг ledger2: o.mkt.lock (TRANSFER w.wal.share → w.mkt.order).
* @ingroup public_marketplace_actions
*/
[[eosio::action]] void createorder(eosio::name coopname,
eosio::name orderer,
checksum256 order_hash,
checksum256 offer_hash,
eosio::name offerer,
eosio::name delivery_braname,
uint64_t quantity,
eosio::asset unit_price,
uint32_t warranty_period_secs,
checksum256 batch_hash);
/**
* @brief Заказчик отменяет заказ до акцепта (Story 4.4). Триггерит o.mkt.unlock.
* @ingroup public_marketplace_actions
*/
[[eosio::action]] void cancelorder(eosio::name coopname,
eosio::name orderer,
checksum256 order_hash);
/**
* @brief Backend закрывает Order по таймауту цикла отсечки (Story 4.3).
* Per-Order: o.mkt.unlock + статус active → cancelled. Backend вычисляет
* threshold по batch'у вне контракта; для каждого истёкшего Order'а
* вызывается отдельный `expireorder`.
* @ingroup public_marketplace_actions
*/
[[eosio::action]] void expireorder(eosio::name coopname,
checksum256 order_hash);
/**
* @brief Поставщик акцептует один Order (Story 4.5).
* Без ledger2-операций — статус active → accepted. Backend проходит циклом
* по orders соответствующего batch'а, вызывая `acceptorder` per Order.
* @ingroup public_marketplace_actions
*/
[[eosio::action]] void acceptorder(eosio::name coopname,
eosio::name offerer,
checksum256 order_hash);
/**
* @brief Поставщик отказывается от одного Order'а до акцепта (Story 4.5).
* Per-Order: o.mkt.unlock на total_cost + статус active → cancelled.
* Backend проходит циклом по orders батча, вызывая `declineorder` per Order.
* @ingroup public_marketplace_actions
*/
[[eosio::action]] void declineorder(eosio::name coopname,
eosio::name offerer,
checksum256 order_hash);
/**
* @brief Поставщик первой подписью на АПП приёмки фиксирует партию по одному
* Order'у (Story 5.3/5.4). Без ledger2-операций — статус accepted →
* supply_prepared. Параметр `accept_braname` указывает приёмный КУ; запись
* в Order. Подпись валидируется как `verify_document_or_fail(act, {offerer})`.
* Backend проходит циклом по orders батча с одинаковым `act`.
* @ingroup public_marketplace_actions
*/
[[eosio::action]] void signsupp(eosio::name coopname,
eosio::name offerer,
checksum256 order_hash,
eosio::name accept_braname,
document2 act);
/**
* @brief Председатель приёмного КУ ставит закрывающую подпись на АПП
* приёмки одного Order'а (Story 5.3/5.4). Per-Order: только o.mkt.purch
* (Дт 10 / Кт 86). Выплата поставщику (o.mkt.payout) — отдельным lazy
* action'ом `payout` после подтверждения кассиром фактического банковского
* перевода (E11 техдолг 598-16, Locked Decision L12). Авторизация подписи:
* председатель / trustee / trusted ∈ branches[o.accept_braname]. Backend
* проходит циклом по orders батча с одинаковым `act`.
* @ingroup public_marketplace_actions
*/
[[eosio::action]] void signchair(eosio::name coopname,
eosio::name signer,
checksum256 order_hash,
uint64_t actual_quantity,
eosio::asset actual_unit_price,
document2 act);
/**
* @brief Инициация исходящей выплаты поставщику через контракт gateway по
* одному Order'у (E11 техдолг 598-16, Locked Decision L12). Per-Order:
* inline-вызов `gateway::createoutpay` с callback'ами на `payconfirm` /
* `paydecline`. Ledger2-операция o.mkt.payout (Дт 86 / Кт 51) применяется
* НЕ здесь, а в callback'е `payconfirm` после действия кассира. Статус
* Order'а не меняется; защита от двойного запроса — через
* `order.payout_status` (NONE/DECLINED → PENDING).
* @ingroup public_marketplace_actions
*/
[[eosio::action]] void payout(eosio::name coopname,
checksum256 order_hash);
/**
* @brief Callback от gateway::outcomplete — кассир подтвердил
* банковский перевод поставщику (E11 техдолг 598-16, Locked Decision L12).
* Здесь применяется o.mkt.payout (Дт 86 / Кт 51) и `payout_status`
* переходит PENDING → COMPLETED. Авторизация: `_gateway`. `outcome_hash`
* совпадает с `order.hash` (так задано при `payout`).
* @ingroup public_marketplace_actions
*/
[[eosio::action]] void payconfirm(eosio::name coopname,
checksum256 outcome_hash);
/**
* @brief Callback от gateway::outdecline — кассир отметил, что
* банковский перевод не состоялся (E11 техдолг 598-16, Locked Decision L12).
* Ledger2-операция НЕ применяется; обязательство Кт 86 остаётся открытым.
* `payout_status` PENDING → DECLINED; `payout_decline_reason` сохраняется.
* Авторизация: `_gateway`.
* @ingroup public_marketplace_actions
*/
[[eosio::action]] void paydecline(eosio::name coopname,
checksum256 outcome_hash,
std::string reason);
/**
* @brief Председатель КУ выдачи открывает выдачу первой подписью АПП-выдачи
* (Story 6.1). Без ledger2-операций — статус ready_to_receive. Авторизация:
* подписант ∈ branches[o.delivery_braname].
* @ingroup public_marketplace_actions
*/
[[eosio::action]] void signiss1(eosio::name coopname,
eosio::name signer,
checksum256 order_hash,
document2 act);
/**
* @brief Заказчик ставит финальную подпись АПП-выдачи (Story 6.3).
* Per-Order с поддержкой actual_quantity ≠ ordered (Story 6.2).
* Atomic: [o.mkt.unlock на разницу если actual<ordered |
* o.mkt.lock на разницу если actual>ordered]
* + o.mkt.consum.
* Подпись акта: orderer + любой авторизованный из branches[o.delivery_braname].
* @ingroup public_marketplace_actions
*/
[[eosio::action]] void signiss2(eosio::name coopname,
eosio::name orderer,
checksum256 order_hash,
uint64_t actual_quantity,
eosio::asset actual_unit_price,
eosio::name delivery_signer,
document2 act);
// ── p.mkt.return ─────────────────────────────────────────────────────
/**
* @brief Пайщик подаёт заявление на гарантийный возврат (Story 7.1).
* @ingroup public_marketplace_actions
*/
[[eosio::action]] void submretrn(eosio::name coopname,
eosio::name orderer,
checksum256 request_hash,
checksum256 original_order_hash,
uint64_t actual_quantity,
std::string reason_text,
std::vector<checksum256> photos,
document2 statement);
/**
* @brief Председатель удалённо одобряет очный визит (Story 7.2). Авторизация:
* подписант ∈ branches[braname]; параметр `braname` фиксирует КУ, в котором
* рассматривается заявление.
* @ingroup public_marketplace_actions
*/
[[eosio::action]] void aprretrem(eosio::name coopname,
eosio::name signer,
eosio::name braname,
checksum256 request_hash,
document2 decision);
/**
* @brief Председатель удалённо отказывает (Story 7.2). Авторизация:
* подписант ∈ branches[braname].
* @ingroup public_marketplace_actions
*/
[[eosio::action]] void rejretrem(eosio::name coopname,
eosio::name signer,
eosio::name braname,
checksum256 request_hash,
std::string reason,
document2 decision);
/**
* @brief Председатель принимает возврат на очном осмотре (Story 7.4).
* Один шаг: o.mkt.return (compensating forward к o.mkt.consum).
* Авторизация: подписант ∈ branches[braname].
* @ingroup public_marketplace_actions
*/
[[eosio::action]] void accretrn(eosio::name coopname,
eosio::name signer,
eosio::name braname,
checksum256 request_hash,
document2 decision);
/**
* @brief Председатель отказывает на очном осмотре (Story 7.3).
* Авторизация: подписант ∈ branches[braname].
* @ingroup public_marketplace_actions
*/
[[eosio::action]] void rejretrn(eosio::name coopname,
eosio::name signer,
eosio::name braname,
checksum256 request_hash,
std::string reason,
document2 decision);
// ── p.mkt.wroff ──────────────────────────────────────────────────────
/**
* @brief Backend выносит проект списания на повестку совета (Story 8.1).
* Без ledger2-операций — только создание proposal с N позициями (статус
* proposed) и тем же inline-вызовом ставит повестку: `soviet::createagenda`
* от `permission_level{_marketplace, active}` с `callback_contract=marketplace`,
* `confirm_callback=onmktwoauth`, `decline_callback=onmktwodecl`,
* `type=mktwroff`, `hash=proposal_hash`, `statement` (Заявление 1106).
* Проект может подаваться председателем (за подписью) либо автоматически —
* мост повестки целиком на контракте, без участия backend.
* @ingroup public_marketplace_actions
*/
[[eosio::action]] void propwroff(eosio::name coopname,
eosio::name proposed_by,
checksum256 proposal_hash,
std::vector<wroff_item> items,
document2 statement,
std::string meta);
/**
* @brief Callback от `soviet::exec` после авторизации Протокола совета
* (registry 1105) председателем (Story 8.4). PROPOSED → AUTHORIZED;
* сохраняется `authorization` в `wroffprops.protocol`. Цикл per-item
* списания запускает backend через `execwroff` после получения этой дельты.
*
* Авторизация: контракт `_soviet` (`require_auth(_soviet)`); сигнатура
* соответствует `authorize_action_effect` в soviet — `(coopname, hash,
* authorization)`.
* @ingroup public_marketplace_actions
*/
[[eosio::action]] void onmktwoauth(eosio::name coopname,
checksum256 hash,
document2 authorization);
/**
* @brief Callback от `soviet::cancelexprd` (или от любого decline-эффекта в
* soviet) — повестка отклонена или просрочена (Story 8.4). PROPOSED →
* REJECTED; `reason` сохраняется в `wroffprops.reject_reason`. Без
* ledger2-движений.
*
* Сигнатура `(coopname, hash, reason)` соответствует
* `DECLINE_CALLBACK_SIGNATURE` в `lib/core/soviet/soviet.hpp:19`.
* Авторизация: `_soviet`.
* @ingroup public_marketplace_actions
*/
[[eosio::action]] void onmktwodecl(eosio::name coopname,
checksum256 hash,
std::string reason);
/**
* @brief Backend исполняет одну позицию авторизованного проекта списания
* (Story 8.4). Per-item: `o.mkt.wroff`, `items[item_index].executed = true`.
* Когда все items исполнены, статус AUTHORIZED → EXECUTED.
*
* Защита от газового лимита (тысячи позиций в одной транзакции Antelope
* не помещаются) — backend проходит цикл и вызывает `execwroff` per item.
*
* Guards:
* - proposal.status == AUTHORIZED (callback `onmktwoauth` уже отработал);
* - подписант (`signer`) авторизован для КУ-источника
* (`branches[items[item_index].braname]`).
* @ingroup public_marketplace_actions
*/
[[eosio::action]] void execwroff(eosio::name coopname,
eosio::name signer,
checksum256 proposal_hash,
uint64_t item_index);
// ── service ──────────────────────────────────────────────────────────
/**
* @brief Заглушка миграции — donor-таблиц нет, мигрировать нечего.
* Оставлена для совместимости с CMake-build и прежним ABI.
* @ingroup public_marketplace_actions
*/
[[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);
static void cancel_request(eosio::name coopname, eosio::name username, checksum256 request_hash);
// Статические методы для отклонения заявок
static void decline_request(eosio::name coopname, const request& change);
// Методы для направления заявок
[[eosio::action]] void accept(eosio::name coopname, eosio::name supplier_braname, eosio::name username, checksum256 request_hash, document2 convert_out, document2 return_document);
[[eosio::action]] void authcontrib(eosio::name coopname, checksum256 request_hash, document2 authorization);
[[eosio::action]] void authreturn(eosio::name coopname, checksum256 request_hash, document2 authorization);
[[eosio::action]] void declineacc(eosio::name coopname, checksum256 hash, std::string reason);
[[eosio::action]] void supply(eosio::name coopname, eosio::name username, checksum256 request_hash, document2 act);
[[eosio::action]] void supplcnf(eosio::name coopname, eosio::name username, checksum256 request_hash, document2 act);
// Новая система перевозок
[[eosio::action]] void createship(eosio::name coopname, checksum256 hash, eosio::name driver_username, eosio::name source_braname, eosio::name destination_braname, std::vector<checksum256> request_hashes, document2 transport_act_sender);
[[eosio::action]] void signbydriver(eosio::name coopname, checksum256 hash, document2 transport_act_driver);
[[eosio::action]] void arrived(eosio::name coopname, checksum256 hash, document2 transport_act_delivery);
[[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);
// Доставка заказчику
[[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);
[[eosio::action]] void receivecnf(eosio::name coopname, eosio::name username, checksum256 request_hash, document2 document);
[[eosio::action]] void complete(eosio::name coopname, eosio::name username, checksum256 request_hash);
[[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 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);
[[eosio::action]] void wreturn(eosio::name coopname, eosio::name username, checksum256 request_hash, document2 document);
[[eosio::action]] void woffer(eosio::name coopname, eosio::name username, checksum256 request_hash, document2 document);
[[eosio::action]] void waccept(eosio::name coopname, eosio::name username, checksum256 request_hash, bool accept, document2 document);
struct [[eosio::table, eosio::contract("marketplace")]] balances : balances_base {};
struct [[eosio::table, eosio::contract("marketplace")]] counts : counts_base {};
};
@@ -0,0 +1,346 @@
# ─────────────────────────────────────────────────────────────────────────────
# Стандарт «Гарантийный возврат имущества» — кооперативный процесс возврата
# имущества пайщиком на склад КУ в пределах гарантийного срока.
#
# Возврат реализован как **compensating forward** — отдельная именованная
# операция o.mkt.return с собственными проводками, семантически обратными
# исходной выдаче (o.mkt.consum). Откат через ledger2::revert НЕ
# используется (упрощение реализации MVP — реверты исключены из системы).
#
# **Модель кошельков:** возврат суммы при гарантии происходит на членский
# кошелёк «Стола заказов» пайщика (w.mkt.member.available восстанавливается);
# средства остаются в программе — заказчик направляет их на следующие заказы
# либо отдельным действием выводит в универсальный членский.
#
# Идентификация кошельков — eosio::name с префиксом w.<contract>.<waltype>.
# Sentinel '' (пустая строка) — «кошелёк вне системы» для ISSUE.
#
# Возврат поставщику и работа с поставщиком по претензиям — out of MVP;
# вернувшееся имущество остаётся на складе КУ как материальный остаток,
# его дальнейшая судьба вне этого процесса.
#
# В процессе участвует 1 ledger2-операция (compensating forward к выдаче):
# • o.mkt.return — ISSUE ∅ → w.mkt.member (восстановление .available
# на членском «Стола заказов» заказчика) + Дт 10 / Кт 86
# (имущество назад на склад через целевое финансирование).
#
# Канон формата:
# coopenomics-docs/docs/standards/_spec/canon.md
# Источники правды в коде:
# • cpp/marketplace/marketplace.hpp — actions (status: proposed)
# • cpp/marketplace/src/p.mkt.return/ — реализация
# • cpp/lib/core/ledger2/operations.hpp — OPERATION_REGISTRY
# расширение o.mkt.return
# • cpp/lib/core/ledger2/processes.hpp — processes::marketplace::RETURN
# • cpp/lib/core/ledger2/wallets.hpp — w.mkt.member (членский «Стола заказов»)
# • cpp/lib/core/ledger2/accounts.hpp — Целевое финансирование (86),
# Материалы (10)
# ─────────────────────────────────────────────────────────────────────────────
# ── Секция 1. Паспорт ───────────────────────────────────────────────────────
process_type: p.mkt.return
id: public_marketplace_return_process
title: Гарантийный возврат имущества
slug: return
status: proposed
contract: marketplace
purpose: >
Гарантийная защита заказчика по сделкам «Стола заказов». Если после
получения товара заказчик обнаружил дефект, недокомплект или
истечение срока годности в пределах гарантии, заданной поставщиком,
он подаёт заявление на гарантийный возврат. Председатель участка
сначала рассматривает заявление удалённо — по фото и описанию — и
принимает одно из двух решений: одобрить очный визит или отказать
удалённо. Если визит одобрен, заказчик приходит с продукцией на
участок, председатель очно осматривает товар и выносит финальное
решение — принять возврат или отказать на месте. При принятии
возврата товар остаётся на складе участка, а сумма заказа
возвращается заказчику на программный членский кошелёк Стола
заказов: он может направить её на следующий заказ программы либо
вывести в универсальный членский кошелёк отдельным действием.
При отказе — на этом гарантийный возврат завершён, движений по
имуществу и средствам не происходит. Дальнейшая судьба возвращённого
имущества (возврат поставщику, перепоставка, списание) — вне этого
процесса.
roles:
- orderer # пайщик-заказчик, инициатор возврата
- chairman # председатель кооперативного участка (КУ)
# ── Секция 2. Действия контракта (блокчейн-уровень) ─────────────────────────
# Имена actions ≤12 символов eosio::name. Контракт описан в целевом виде —
# реализация в .cpp предстоит после согласования стандарта.
actions:
- name: marketplace::submretrn
human: Подать заявление на гарантийный возврат
actor: orderer
role: opener
purpose: >
Заказчик подаёт заявление на гарантийный возврат имущества:
указывает причину обращения (некондиция, истёк срок годности,
иное) и прикладывает фотографии товара. Подать заявление можно
только пока не истёк гарантийный срок, заданный поставщиком.
- name: marketplace::aprretrem
human: Одобрить очный визит
actor: chairman
role: progress
purpose: >
Председатель участка по результатам удалённого рассмотрения
решает, что для разбора обращения нужен очный осмотр товара, и
приглашает заказчика прийти на участок с продукцией.
- name: marketplace::rejretrem
human: Отказать удалённо
actor: chairman
role: reject
purpose: >
Председатель участка по результатам удалённого рассмотрения
решает отказать в гарантийном возврате с указанием причины — без
приглашения на очный осмотр. Решение финальное, движений по
имуществу и средствам не происходит.
- name: marketplace::accretrn
human: Принять возврат
actor: chairman
role: closer
purpose: >
Председатель по результатам очного осмотра принимает гарантийный
возврат: товар остаётся на складе участка, сумма заказа
возвращается заказчику на программный членский кошелёк Стола
заказов.
- name: marketplace::rejretrn
human: Отказать на месте
actor: chairman
role: reject
purpose: >
Председатель по результатам очного осмотра отказывает в
гарантийном возврате с указанием причины. Заказчик забирает
товар обратно. Решение финальное, движений по имуществу и
средствам не происходит.
# ── Секция 3. Граф состояний ────────────────────────────────────────────────
# Сущность: marketplace::return_request (таблица return_requests, scope=coopname).
entity: marketplace::return_request
entity_human: Заявление на гарантийный возврат
entity_source: cpp/marketplace/src/return/
states:
- name: pending_review
human: Заявление на рассмотрении
description: >
Заявление подано заказчиком и ждёт решения председателя участка.
kind: normal
- name: approved_for_visit
human: Очный визит одобрен
description: >
Председатель одобрил очное рассмотрение. Заказчику предстоит
прийти на участок с продукцией для очного осмотра.
kind: normal
- name: return_accepted
human: Возврат принят
description: >
Возврат принят кооперативом. Сумма заказа возвращена заказчику на
программный членский кошелёк Стола заказов — он может направить её
на следующий заказ программы либо вывести в универсальный членский
кошелёк отдельным действием. Дальнейшая судьба возвращённого
имущества (возврат поставщику, перепоставка, списание) — отдельная
процедура по регламенту кооператива; в рамках этого процесса товар
остаётся на складе участка.
kind: final
- name: rejected_remote
human: Отказ удалённо
description: >
Председатель отказал по результатам удалённого рассмотрения.
Гарантийный возврат завершён, движений по имуществу и средствам
не происходит.
kind: final
- name: rejected_at_ku
human: Отказ на месте
description: >
Председатель отказал по результатам очного осмотра. Заказчик
забирает товар обратно. Гарантийный возврат завершён, движений
по имуществу и средствам не происходит.
kind: final
transitions:
- from: "∅"
to: pending_review
action: marketplace::submretrn
actor: orderer
guards:
- Заявитель — заказчик-пайщик исходного заказа, по которому имущество уже выдано.
- Гарантийный срок, заданный поставщиком в предложении, ещё не истёк.
- К заявлению приложены фотографии товара и указана причина возврата.
- from: pending_review
to: approved_for_visit
action: marketplace::aprretrem
actor: chairman
guards:
- Председатель решил пригласить заказчика на очный осмотр.
- from: pending_review
to: rejected_remote
action: marketplace::rejretrem
actor: chairman
guards:
- Председатель решил отказать в гарантийном возврате удалённо с указанием причины.
- from: approved_for_visit
to: return_accepted
action: marketplace::accretrn
actor: chairman
ledger_code: p.mkt.return
operations:
- o.mkt.return
guards:
- Заказчик прибыл на участок с продукцией.
- Председатель очно осмотрел имущество и решил принять гарантийный возврат.
- Гарантийный срок ещё не истёк.
- from: approved_for_visit
to: rejected_at_ku
action: marketplace::rejretrn
actor: chairman
guards:
- Председатель очно осмотрел имущество и решил отказать в гарантийном возврате с указанием причины.
# ── Секция 4. Сценарий ──────────────────────────────────────────────────────
scenario:
steps:
- step: 1
title: Подача заявления
actor: orderer
action: marketplace::submretrn
description: >
Заказчик подаёт заявление на гарантийный возврат имущества:
указывает причину обращения, прикладывает фотографии товара,
ссылается на акт приёма-передачи, по которому получал имущество.
Подать заявление
можно только пока не истёк гарантийный срок, заданный
поставщиком.
pre:
- Заказ закрыт, имущество выдано заказчику.
- Гарантийный срок ещё не истёк.
- Приложены фотографии товара и указана причина обращения.
post:
- Заявление зарегистрировано и направлено председателю на рассмотрение.
- step: 2
title: Удалённое рассмотрение — приглашение на очный визит
actor: chairman
action: marketplace::aprretrem
description: >
Председатель участка изучает заявление и приложенные материалы
и решает, что для разбора обращения нужен очный осмотр товара.
Заказчику предстоит прийти на участок с продукцией.
pre:
- Заявление на рассмотрении.
post:
- Очный визит одобрен.
- step: 3
title: Очный осмотр — принятие возврата
actor: chairman
action: marketplace::accretrn
description: >
Заказчик приходит на участок с продукцией. Председатель очно
осматривает товар и принимает гарантийный возврат: товар
остаётся на складе участка, сумма заказа возвращается
заказчику на программный членский кошелёк Стола заказов.
pre:
- Очный визит одобрен.
- Заказчик прибыл на участок с продукцией.
post:
- Товар остаётся на складе участка.
- Сумма заказа возвращена заказчику на программный членский кошелёк.
alternatives:
- branch: Гарантийный срок истёк
at_step: 1
action: null
actor: orderer
description: >
Если гарантийный срок поставщика истёк, заявление на гарантийный
возврат подать нельзя.
- branch: Удалённое рассмотрение — отказ
at_step: 2
action: marketplace::rejretrem
actor: chairman
description: >
Председатель отказывает в гарантийном возврате удалённо с
указанием причины — например, обращение очевидно не подпадает
под гарантию. Решение финальное, движений по имуществу и
средствам не происходит.
- branch: Очный осмотр — отказ
at_step: 3
action: marketplace::rejretrn
actor: chairman
description: >
Председатель отказывает в гарантийном возврате по результатам
очного осмотра с указанием причины — например, характер
повреждений не покрывается гарантией. Заказчик забирает товар
обратно. Решение финальное, движений по имуществу и средствам
не происходит.
# ── Секция 5. Документы и подписи ───────────────────────────────────────────
# В процессе подписывается заявление пайщика на возврат с приложениями
# (фото товара). Решение председателя — процедурное действие в системе,
# отдельным документом не оформляется в MVP (фиксируется в стейт-машине
# return_request с кем и когда принято решение). Если регулятор / устав
# потребуют документного оформления решения председателя — это будет
# отдельным шаблоном (TODO).
documents:
- action: marketplace::submretrn
title: Заявление пайщика на гарантийный возврат имущества
registry_id: 800
signed_by: [orderer]
stored_in: return_requests.statement
note: "Используется существующий шаблон 800.ReturnByAssetStatement из cooptypes/cooperative/registry/. Оригинально создан под клиринговую модель donor'а («Заявление на возврат паевого взноса имуществом»); форма заявления пайщика на возврат структурно подходит и для членской модели гарантийного возврата. При необходимости методолог может создать специализированный шаблон в новой серии (1100+) — тогда registry_id обновится."
- action: marketplace::accretrn
title: Решение председателя КУ о принятии гарантийного возврата
registry_id: 0
signed_by: [chairman]
stored_in: return_requests.chairman_decision
note: "TODO: в MVP решение принимается единолично председателем КУ, не советом — существующий шаблон 801.ReturnByAssetDecision не подходит (рассчитан на коллегиальное решение совета по новации). Создать специализированный шаблон в registry либо оформлять как in-system запись без отдельного документа."
# ── Секция 6. Операции (Ledger2) ────────────────────────────────────────────
# Одна ledger2-операция — compensating forward к o.mkt.consum (без
# использования ledger2::revert). Атомарно: восстановление .available на
# членском кошельке «Стола заказов» заказчика + возврат имущества на склад.
operations:
- ledger_code: o.mkt.return
human_name: Гарантийный возврат — восстановление средств и имущества
wallet_op: ISSUE
# L1 — двойная запись: имущество возвращается на склад через целевое
# финансирование (обратная к выдаче, одной операцией).
debit: 10 # Материалы — имущество на складе
credit: 86 # Целевое финансирование
# L2 — восстановление средств на членском кошельке «Стола заказов» заказчика
wallet_from: null # эмиссия — источника нет
wallet_to: w.mkt.member # Членский кошелёк «Стола заказов» пайщика
# L3 — пайщику восстанавливается available на членском «Стола заказов»
user_wallet: w.mkt.member
user_ref: return_request.orderer
available_delta: +order.fact_cost
blocked_delta: null
amount_ref: order.fact_cost
triggered_by: marketplace::accretrn
description: >
Compensating forward к выдаче имущества (o.mkt.consum). ISSUE на
членский кошелёк «Стола заказов» заказчика (w.mkt.member) — .available
+fact_cost (восстановление ранее списанной суммы). Двойная запись
Дт 10 / Кт 86 — имущество возвращается на склад за счёт целевого
финансирования (зеркало выдачи). Журнал содержит прикладное поле
original_consume_op_id (ссылка на исходный o.mkt.consum для
трассировки) — это не часть инфраструктуры revert. Возвращённая сумма
остаётся в программе «Стол заказов» — на следующие заказы.
@@ -0,0 +1,986 @@
# ─────────────────────────────────────────────────────────────────────────────
# Стандарт «Прямая поставка-приобретение имущества» — кооперативный процесс,
# при котором поставщик-пайщик передаёт имущество кооперативу, а заказчик-
# пайщик получает его в счёт членского взноса по ЦПП «Стол заказов».
#
# Это базовый процесс контракта Marketplace в режиме членских взносов
# (закупка имущества кооперативом + потребление пайщиком). Содержит
# атомарную серию операций по программному кошельку при создании заказа,
# цикл отсечки заявок поставщика, двухвариантную модель передачи партии
# на участок (самовывоз или через экспедитора), двойные подписи на двух
# актах приёма-передачи — приёмки кооперативом от поставщика и выдачи
# заказчику от кооператива.
#
# **Модель кошельков:**
# Средства заказчика движутся напрямую между тремя кошельками пайщика:
# • w.wal.share (контракт wallet, USER_SHARED) — паевой взнос деньгами.
# Источник средств при создании заказа.
# • w.mkt.order (контракт marketplace, USER_SHARED) — резерв средств
# пайщика под конкретный Order. Сюда уходят средства с
# паевого при createorder; отсюда возвращаются на членский
# «Стола заказов» при отмене/истечении/недовыдаче,
# либо сжигаются при выдаче имущества (signiss2).
# • w.mkt.member (контракт marketplace, USER_SHARED) — членский кошелёк
# пайщика «Стола заказов». Приёмник возврата резерва и
# конвертации паевого под доплату. Средства остаются в
# программе — идут на следующие заказы (не уходят в
# универсальный членский).
# Платформенный кошелёк w.mkt.payout (Выплаты поставщикам) — без изменений.
# Имущество отслеживается бухгалтерской аналитикой по счёту 10
# (per-КУ субсчета), без отдельного кошелька.
#
# Идентификация кошельков — eosio::name с префиксом w.<contract>.<waltype>.
# null для незатронутых уровней; sentinel '' — legacy-допуск (рендерится как ∅).
#
# В процессе участвуют 5 ledger2-операций marketplace:
# • o.mkt.lock — резервирование средств под Order
# (TRANSFER, w.wal.share → w.mkt.order, Дт 80 / Кт 86).
# Единственный обязательный шаг createorder.
# • o.mkt.unlock — снятие резерва при отмене Order'а / недовыдаче
# (TRANSFER, w.mkt.order → w.mkt.member, без проводки —
# оба кошелька на 86; средства возвращаются на членский
# «Стола заказов» — остаются в программе под след. заказы).
# • o.mkt.conv — конвертация паевого в членский «Стола заказов» под доплату
# (TRANSFER, w.wal.share → w.mkt.member, Дт 80 / Кт 86).
# • o.mkt.lockm — добор резерва заказа с членского «Стола заказов»
# (TRANSFER, w.mkt.member → w.mkt.order, без проводки).
# • o.mkt.purch — приёмка имущества кооперативом по АПП приёмки
# (NONE, только проводка Дт 10 / Кт 86)
# • o.mkt.payout — оплата поставщику с расчётного счёта
# (ISSUE ∅ → w.mkt.payout, Дт 86 / Кт 51)
# • o.mkt.consum — выдача имущества пайщику по АПП выдачи
# (BURN с w.mkt.order — сжигание резерва, Дт 86 / Кт 10)
#
# Канон формата:
# coopenomics-docs/docs/standards/_spec/canon.md
# Источники правды в коде:
# • cpp/marketplace/marketplace.hpp — actions (status: proposed)
# • cpp/marketplace/src/ — реализация (предстоит)
# • cpp/lib/core/ledger2/operations.hpp — OPERATION_REGISTRY (расширение)
# • cpp/lib/core/ledger2/processes.hpp — processes::marketplace::SUPPLY
# • cpp/lib/core/ledger2/wallets.hpp — w.wal.share (паевой),
# w.mkt.member (членский «Стола заказов»),
# w.mkt.order (резерв под Order),
# w.mkt.payout (платформенный)
# • cpp/lib/core/ledger2/accounts.hpp — Расчётный (51),
# Паевой фонд (80),
# Целевое финансирование (86),
# Материалы (10)
# ─────────────────────────────────────────────────────────────────────────────
# ── Секция 1. Паспорт ───────────────────────────────────────────────────────
process_type: p.mkt.supply
id: public_marketplace_supply_process
title: Прямая поставка-приобретение имущества
slug: supply
status: proposed
contract: marketplace
purpose: >
Базовый процесс «Стола заказов»: пайщик-заказчик выбирает у пайщика-
поставщика имущество, направляет на этот заказ свой членский взнос,
кооператив принимает имущество на склад участка и затем выдаёт его
заказчику. Процесс описывает весь путь — от размещения заказа до
передачи имущества на руки заказчику.
Как это устроено по шагам.
Поставщик-пайщик публикует предложение поставить имущество — в нём
он указывает товар, цену, минимальное количество, при котором ему
выгодно собрать партию, и срок, до которого собирает заказы. Это
предложение и есть исходная точка процесса.
Пока предложение открыто, заказчики-пайщики добавляют к нему свои
заказы. Этот период называется циклом отсечки заявок — окно, в
течение которого кооператив принимает заказы под одно предложение
поставщика. При создании заказа кооператив резервирует средства
заказчика на программном членском кошельке Стола заказов — до конца
цикла они не могут быть использованы.
Когда цикл завершается, кооператив смотрит, набрался ли минимальный
порог поставки — заданное поставщиком количество заказов, при
котором партию имеет смысл везти. Если порог набрался — все заказы
цикла объединяются в одну консолидированную заявку, и она уходит
поставщику на акцепт. Если не набрался — все заказы цикла
отменяются, резерв со средств заказчиков снимается.
Поставщик соглашается выполнить консолидированную заявку, готовит
партию и передаёт её на кооперативный участок. Приёмка оформляется
актом приёма-передачи с двойной подписью: первой — поставщик
подтверждает передачу, второй — председатель участка подтверждает
приёмку. С этого момента кооператив юридически принял имущество,
поставщику уходит оплата.
Дальше председатель открывает выдачу, заказчик приходит на пункт
выдачи участка. Выдача оформляется отдельным актом приёма-передачи
также с двойной подписью: первой — председатель подтверждает
передачу имущества заказчику, финальной — заказчик подтверждает
приёмку. Заказ закрыт, начинается гарантийное окно.
roles:
- orderer # пайщик-заказчик
- offerer # пайщик-поставщик
- chairman # председатель кооперативного участка (КУ-приёмщик/КУ-выдающий)
- backend # Backend Marketplace (внеблокчейн-логика)
- gateway # Контракт процессинга платежей (источник callback'ов о выплате; не человек, а контракт-инициатор)
# ── Секция 2. Действия контракта (блокчейн-уровень) ─────────────────────────
# Имена actions ≤12 символов eosio::name. Контракт описан в целевом виде —
# реализация в .cpp предстоит после согласования стандарта.
actions:
- name: marketplace::createorder
human: Создать заказ
actor: orderer
role: opener
purpose: >
Заказчик размещает заказ на товар из каталога: указывает количество
и кооперативный участок, на котором заберёт товар. Кооператив
резервирует средства заказчика под этот заказ — до окончания цикла
поставки они не могут быть использованы. Заказ становится частью
консолидированной заявки поставщику.
- name: marketplace::cancelorder
human: Отменить заказ
actor: orderer
role: progress
purpose: >
Заказчик отменяет заказ до того, как поставщик принял
консолидированную заявку в работу. Кооператив снимает резерв со
средств — заказчик снова свободно ими распоряжается в рамках
программы.
- name: marketplace::expirecycle
human: Закрыть цикл отсечки заявок
actor: backend
role: progress
purpose: >
Цикл отсечки заявок поставщика завершается. Кооператив сверяет
количество заказов с минимальным порогом поставки, заданным
поставщиком. Если порог набрался — заказы объединяются в
консолидированную заявку и направляются поставщику для акцепта.
Если порог не набрался — все заказы цикла отменяются, резерв со
средств заказчиков снимается.
- name: marketplace::acceptbatch
human: Акцептовать консолидированную заявку
actor: offerer
role: progress
purpose: >
Поставщик соглашается выполнить консолидированную заявку и берёт
на себя обязательство собрать и передать партию кооперативу.
Заказы переходят от ожидания к подготовке отгрузки.
- name: marketplace::declinebatch
human: Отказаться от консолидированной заявки
actor: offerer
role: reject
purpose: >
Поставщик отказывается выполнить консолидированную заявку. Все
заказы партии отменяются, резерв со средств заказчиков снимается.
Договорные санкции при отказе — вне этого процесса, по регламенту
кооператива.
- name: marketplace::prepship
human: Собрать партию к отгрузке
actor: offerer
role: progress
purpose: >
Поставщик собирает партию: распределяет заказы по кооперативным
участкам и выбирает способ доставки — везёт сам или передаёт через
экспедитора. Состав партии должен в точности соответствовать тому,
что поставщик акцептовал: заменять или подменять позиции запрещено
(жёсткий акцепт).
- name: marketplace::signsupp
human: Поставщик передал партию
actor: offerer
role: progress
purpose: >
Поставщик первой подписью на акте приёма-передачи подтверждает,
что передал партию кооперативу. Имущество физически на складе
участка, но юридически ещё не оприходовано — выдача заказчикам
открывается только после второй подписи председателя.
- name: marketplace::signchair
human: Председатель принял поставку
actor: chairman
role: progress
purpose: >
Председатель участка ставит вторую — завершающую — подпись на
акте приёма-передачи. С этого момента партия юридически принята
кооперативом, имущество приходуется на склад участка и становится
доступным к выдаче заказчикам, а у кооператива возникает обязательство
выплатить поставщику. Сама выплата — отдельным шагом по факту
подтверждения кассиром банковского перевода.
- name: marketplace::payout
human: Инициировать выплату поставщику
actor: backend
role: progress
purpose: >
Кооператив регистрирует исходящий платёж поставщику в системе
процессинга (контракт gateway). По этому запросу кассир увидит у
себя в столе задачу — провести банковский перевод. Бухгалтерия
по выплате на этом шаге ещё не двигается: обязательство перед
поставщиком остаётся открытым до подтверждения кассиром.
- name: marketplace::payconfirm
human: Выплата поставщику подтверждена
actor: gateway
role: progress
purpose: >
Кассир подтвердил факт реального банковского перевода поставщику
(через свой стол в gateway). Контракт процессинга вызывает callback
сюда — обязательство перед поставщиком закрывается, расход целевых
средств с расчётного счёта отражается в бухгалтерии. Статус заказа
не меняется — выплата шла параллельно шагам выдачи.
- name: marketplace::paydecline
human: Выплата поставщику отклонена
actor: gateway
role: reject
purpose: >
Кассир отметил в столе процессинга, что банковский перевод
поставщику не состоялся — нет реквизитов, ошибка ввода, банк
отклонил. Контракт процессинга вызывает callback сюда — обязательство
перед поставщиком остаётся открытым, причина сохраняется. Кооператив
может повторно инициировать выплату после исправления реквизитов.
- name: marketplace::signiss1
human: Председатель открыл выдачу
actor: chairman
role: progress
purpose: >
Председатель открывает выдачу: подтверждает, что готов передать
имущество заказчику. Заказ становится готов к получению на пункте
выдачи участка.
- name: marketplace::signiss2
human: Заказчик получил имущество
actor: orderer
role: closer
purpose: >
Заказчик пришёл на пункт выдачи и финальной подписью на акте
выдачи забрал свой заказ. Если фактически выданное расходится с
заказом — сумма заранее корректируется до фактического: при
меньшем количестве остаток возвращается на программный членский
кошелёк «Стола заказов», при большем — недостающая сумма вносится
дополнительным паевым взносом, который конвертируется в членский
«Стола заказов» и им добирается доплата (напрямую с паевого не
списываем). Заказ закрыт, открывается гарантийное окно.
# ── Секция 3. Граф состояний ────────────────────────────────────────────────
# Сущность: marketplace::order (таблица orders, scope=coopname).
# Граф спрямлён: статус pending_offerer_sign модели B описан в сценарии,
# в графе сворачивается в supply_prepared с разными путями входа.
entity: marketplace::order
entity_human: Заказ на поставку
entity_source: cpp/marketplace/src/
states:
- name: active
human: Заказ размещён
description: >
Заказ создан и ждёт завершения цикла отсечки заявок и акцепта
поставщика. Средства заказчика зарезервированы под этот заказ на
программном членском кошельке Стола заказов.
kind: normal
- name: cancelled
human: Заказ отменён
description: >
Заказ отменён по одному из трёх оснований: заказчиком до акцепта,
кооперативом по итогам закрытия цикла без достижения минимального
порога поставки, поставщиком при отказе от консолидированной
заявки. Во всех трёх случаях резерв со средств заказчика снимается,
и заказчик снова свободно ими распоряжается в рамках программы.
kind: final
- name: accepted
human: Заявка акцептована
description: >
Поставщик согласился выполнить консолидированную заявку. Средства
заказчика по-прежнему зарезервированы; поставщик готовит партию к
отгрузке.
kind: normal
- name: ship_ready
human: Партия собрана к отгрузке
description: >
Поставщик собрал партию точно по составу акцептованной заявки и
выбрал способ доставки на участок — самовывоз или через
экспедитора. Имущество ещё не передано кооперативу.
kind: normal
- name: supply_prepared
human: Имущество поставлено
description: >
Поставщик передал партию на участок и подтвердил это первой
подписью на акте приёма-передачи. Имущество физически на складе
участка, но юридически ещё не оприходовано — выдача заказчикам
откроется только после второй подписи председателя.
kind: normal
- name: accepted_to_coop
human: Поставка принята кооперативом
description: >
Председатель участка завершил приёмку второй подписью. Партия
юридически принята кооперативом, имущество числится на складе
участка и ждёт открытия выдачи. Обязательство выплаты поставщику
открыто и закрывается отдельным действием `marketplace::payout` по
факту подтверждения кассиром банковского перевода (Locked Decision L12).
kind: normal
- name: ready_to_receive
human: Заказ готов к выдаче
description: >
Председатель открыл выдачу. Имущество готово к передаче заказчику
на пункте выдачи участка.
kind: normal
- name: received
human: Имущество выдано
description: >
Заказчик получил имущество, заказ закрыт. Открыто гарантийное
окно — в его пределах заказчик может подать заявление на возврат,
если товар окажется некондиционным.
kind: final
transitions:
- from: "∅"
to: active
action: marketplace::createorder
actor: orderer
ledger_code: p.mkt.supply
operations:
- o.mkt.lock
guards:
- Заказчик — активный пайщик кооператива.
- Заказчик подписал Соглашение ЦПП «Стол заказов».
- Стоимость заказа не превышает упрощённый порог приёма имущества (100 000 ₽).
- На паевом взносе заказчика (w.wal.share) достаточно средств для полной стоимости заказа.
- Указанный пункт выдачи принадлежит тому же кооперативу.
- from: active
to: cancelled
action: marketplace::cancelorder
actor: orderer
ledger_code: p.mkt.supply
operations:
- o.mkt.unlock
guards:
- Заказ ещё не вошёл в акцептованную поставщиком консолидированную заявку.
- from: active
to: cancelled
action: marketplace::expirecycle
actor: backend
ledger_code: p.mkt.supply
operations:
- o.mkt.unlock
guards:
- Цикл отсечки заявок поставщика завершился.
- Минимальный порог поставки по количеству заказов не достигнут.
- from: active
to: accepted
action: marketplace::acceptbatch
actor: offerer
guards:
- Минимальный порог поставки достигнут к концу цикла отсечки.
- Действие выполняет тот поставщик, чьё предложение лежит в основе консолидированной заявки.
- from: active
to: cancelled
action: marketplace::declinebatch
actor: offerer
ledger_code: p.mkt.supply
operations:
- o.mkt.unlock
guards:
- Поставщик ещё не акцептовал консолидированную заявку.
- from: accepted
to: ship_ready
action: marketplace::prepship
actor: offerer
guards:
- Состав отгрузки в точности равен составу акцептованной заявки (жёсткий акцепт; замены номенклатуры запрещены).
- Поставщик выбрал способ доставки на участок — самовывоз или через экспедитора.
- from: ship_ready
to: supply_prepared
action: marketplace::signsupp
actor: offerer
guards:
- На участок передана партия точно в составе акцептованной заявки — самовывозом поставщика или через экспедитора.
- При самовывозе поставщик ставит первую подпись на акте приёма-передачи лично на участке.
- При доставке через экспедитора председатель и экспедитор очно сверяют партию, и поставщик ставит первую подпись дистанционно в установленный кооперативом срок.
- from: supply_prepared
to: accepted_to_coop
action: marketplace::signchair
actor: chairman
ledger_code: p.mkt.supply
operations:
- o.mkt.purch
guards:
- На акте приёма-передачи имеется первая подпись поставщика.
- Действие выполняет председатель того участка, который принимает поставку.
# Lazy выплата поставщику через контракт gateway (Locked Decision L12;
# E11 техдолг 598-16). Триплет: инициация → callback подтверждения /
# callback отказа. Графа состояний Order'а сама по себе не меняется —
# выплата идёт параллельно шагам выдачи; всё описано на под-графе
# `order.payout_status` (none → pending → completed | declined → pending).
- from: accepted_to_coop
to: accepted_to_coop
action: marketplace::payout
actor: backend
guards:
- Приёмка поставки кооперативом завершена (signchair выполнен).
- Выплата по этому заказу не находится в процессе и не закрыта (`order.payout_status ∈ { none, declined }`).
- from: accepted_to_coop
to: accepted_to_coop
action: marketplace::payconfirm
actor: gateway
ledger_code: p.mkt.supply
operations:
- o.mkt.payout
guards:
- Callback инициирован контрактом gateway (`require_auth(_gateway)`).
- Order находится в ожидании подтверждения выплаты (`order.payout_status == pending`).
- from: accepted_to_coop
to: accepted_to_coop
action: marketplace::paydecline
actor: gateway
guards:
- Callback инициирован контрактом gateway (`require_auth(_gateway)`).
- Order находится в ожидании подтверждения выплаты (`order.payout_status == pending`).
- from: accepted_to_coop
to: ready_to_receive
action: marketplace::signiss1
actor: chairman
guards:
- Партия принята кооперативом и числится на складе участка.
- from: ready_to_receive
to: received
action: marketplace::signiss2
actor: orderer
ledger_code: p.mkt.supply
operations:
- o.mkt.consum
guards:
- Заказчик лично присутствует на пункте выдачи участка.
- Председатель сверил фактически выдаваемое имущество с составом заказа.
- При расхождении факта с заказом сумма заказа предварительно скорректирована до фактической в пределах допустимого регламентом отклонения.
# ── Секция 4. Сценарий ──────────────────────────────────────────────────────
scenario:
steps:
- step: 1
title: Создание заказа
actor: orderer
action: marketplace::createorder
description: >
Заказчик выбирает позицию из каталога своего кооператива,
указывает количество и пункт выдачи на участке, на котором
заберёт товар. Кооператив проверяет, что стоимость заказа не
превышает упрощённый порог приёма имущества, и резервирует под
этот заказ необходимую сумму со средств заказчика. Заказ
присоединяется к текущему циклу отсечки заявок поставщика.
pre:
- Заказчик — активный пайщик кооператива.
- Заказчик подписал Соглашение ЦПП «Стол заказов».
- Совокупных средств заказчика — на паевом и членских кошельках — достаточно.
- Стоимость заказа не превышает упрощённый порог приёма имущества.
post:
- Заказ размещён и активен.
- Средства заказчика зарезервированы под заказ.
- step: 2
title: Закрытие цикла отсечки заявок
actor: backend
action: marketplace::expirecycle
description: >
Цикл отсечки заявок завершается. Кооператив сверяет количество
собранных заказов с минимальным порогом поставки, заданным
поставщиком. Если порог набрался — заказы объединяются в
консолидированную заявку и направляются поставщику для акцепта.
Если порог не набрался — все заказы цикла отменяются, резерв со
средств заказчиков снимается.
pre:
- Цикл отсечки заявок поставщика завершился.
post:
- При достижении порога — заказы активны, поставщику направлена консолидированная заявка.
- При недостижении порога — заказы отменены, резерв со средств снят.
- step: 3
title: Акцепт консолидированной заявки
actor: offerer
action: marketplace::acceptbatch
description: >
Поставщик видит консолидированную заявку и подтверждает её к
исполнению. С этого момента он принимает на себя обязательство
собрать и передать партию кооперативу.
pre:
- Минимальный порог поставки достигнут.
- Действие выполняет тот поставщик, чьё предложение лежит в основе консолидированной заявки.
post:
- Заказы партии перешли к подготовке отгрузки.
- step: 4
title: Подготовка партии к отгрузке
actor: offerer
action: marketplace::prepship
description: >
Поставщик собирает партию по составу акцептованной заявки —
точно в той же номенклатуре, без замен — и выбирает способ
доставки на участок: самовывоз или через экспедитора.
pre:
- Заявка акцептована поставщиком.
- Состав партии в точности соответствует составу акцепта.
post:
- Партия собрана к отгрузке.
- Способ доставки выбран.
- step: 5
title: Передача партии на участок
actor: offerer
action: marketplace::signsupp
description: >
Поставщик передаёт партию кооперативу. При самовывозе он лично
сдаёт партию председателю и очно ставит первую подпись на акте
приёмки-передачи. При доставке через экспедитора председатель и
экспедитор очно сверяют груз с составом партии, и по итогам этой
сверки поставщик ставит первую подпись дистанционно в
установленный кооперативом срок. В обоих случаях имущество уже
физически на складе участка, но юридически ещё не оприходовано —
выдача заказчикам пока не открыта.
pre:
- Партия собрана к отгрузке.
- Состав акта приёма-передачи в точности соответствует составу акцептованной заявки.
post:
- Имущество поставлено, первая подпись на акте приёма-передачи поставлена.
- Выдача заказчикам не открыта до второй подписи председателя.
- step: 6
title: Приёмка поставки председателем
actor: chairman
action: marketplace::signchair
description: >
Председатель участка ставит вторую — завершающую — подпись на
акте приёма-передачи. С этого момента партия юридически принята
кооперативом, имущество приходуется на склад участка, у кооператива
возникает обязательство выплатить поставщику. Сама выплата —
отдельным шагом по факту подтверждения кассиром банковского
перевода (Locked Decision L12).
pre:
- На акте приёма-передачи имеется первая подпись поставщика.
- Действие выполняет председатель того участка, который принимает поставку.
post:
- Поставка принята кооперативом.
- Обязательство выплаты поставщику открыто и ждёт фактического банковского перевода.
- step: 6a
title: Инициация выплаты поставщику
actor: backend
action: marketplace::payout
description: >
Кооператив через свой backend регистрирует исходящий платёж
поставщику в контракте gateway. У кассира появляется задача в
столе процессинга. Бухгалтерия по выплате на этом шаге не
двигается — обязательство перед поставщиком остаётся открытым
вплоть до подтверждения банковского перевода кассиром.
pre:
- Поставка принята кооперативом (приёмка завершена).
- Выплата по этому заказу не в процессе и не завершена.
post:
- В контракте gateway появилась запись об исходящем платеже со статусом pending.
- Заказ помечен как «выплата ожидает подтверждения».
- step: 6b
title: Подтверждение выплаты поставщику
actor: gateway
action: marketplace::payconfirm
description: >
Кассир провёл реальный банковский перевод поставщику и
подтвердил это в столе процессинга. Контракт gateway вызывает
callback в контракт marketplace, и кооператив фиксирует
выплату — обязательство перед поставщиком закрывается, расход
целевых средств с расчётного счёта отражается в бухгалтерии.
pre:
- В контракте gateway зарегистрирован исходящий платёж в статусе pending.
- Кассир подтвердил фактический банковский перевод поставщику.
post:
- Обязательство выплаты поставщику закрыто.
- Заказ помечен как «выплата проведена».
- step: 7
title: Открытие выдачи
actor: chairman
action: marketplace::signiss1
description: >
Председатель открывает выдачу первой подписью на акте
приёма-передачи. Заказ становится готов к получению на пункте
выдачи участка.
pre:
- Поставка принята кооперативом.
- Имущество числится на складе участка.
post:
- Заказ готов к выдаче.
- step: 8
title: Выдача заказа
actor: orderer
action: marketplace::signiss2
description: >
Заказчик приходит на пункт выдачи участка. Председатель сверяет
фактически выдаваемое имущество с составом заказа. Если факт
совпадает с заказом — выдача проходит без корректировок. При
расхождении (например, развесной товар: фактический вес меньше
заказанного) сумма заказа предварительно корректируется до
фактической: при меньшем количестве остаток возвращается на
программный членский кошелёк «Стола заказов» заказчика, при большем —
недостающая сумма вносится дополнительным паевым взносом, который
конвертируется в членский «Стола заказов» и им добирается доплата
(напрямую с паевого не списываем). Заказчик ставит финальную
подпись на акте приёма-передачи и забирает заказ.
pre:
- Заказ готов к выдаче.
- Заказчик лично присутствует на пункте выдачи.
post:
- Имущество выдано заказчику.
- Заказ закрыт.
- Гарантийное окно открыто — в его пределах заказчик может подать заявление на возврат.
alternatives:
- branch: Отмена заказчиком до акцепта
at_step: 2
action: marketplace::cancelorder
actor: orderer
description: >
Заказчик отменяет заказ до того, как поставщик принял
консолидированную заявку. Кооператив снимает резерв со средств,
и заказчик снова свободно ими распоряжается в рамках программы.
- branch: Цикл закрыт без достижения порога
at_step: 2
action: marketplace::expirecycle
actor: backend
description: >
Цикл отсечки заявок завершился, но минимальный порог поставки
по количеству заказов не достигнут. Все заказы цикла отменяются,
резерв со средств заказчиков снимается.
- branch: Отказ поставщика от консолидированной заявки
at_step: 3
action: marketplace::declinebatch
actor: offerer
description: >
Поставщик отказывается выполнить консолидированную заявку до её
акцепта. Все заказы партии отменяются, резерв со средств
заказчиков снимается. Договорные санкции при отказе — вне этого
процесса, по регламенту кооператива.
- branch: Кассир отклонил банковский перевод поставщику
at_step: 6b
action: marketplace::paydecline
actor: gateway
description: >
Кассир в столе процессинга отметил, что банковский перевод
поставщику не состоялся — нет реквизитов, ошибка ввода, банк
отклонил. Контракт gateway callback'ом дёргает контракт
marketplace. Бухгалтерия не двигается — обязательство перед
поставщиком остаётся открытым с сохранением причины отказа.
Backend может повторно инициировать выплату после исправления
реквизитов (`marketplace::payout` ещё раз).
- branch: Передача партии через экспедитора
at_step: 5
action: marketplace::signsupp
actor: offerer
description: >
Партия доставлена на участок через экспедитора. Председатель и
экспедитор очно сверяют груз с составом партии. При расхождении
председатель фиксирует фактический состав, экспедитор
подтверждает это своей подписью на сопроводительных документах.
По итогам очной сверки поставщик дистанционно ставит первую
подпись на акте приёма-передачи в установленный кооперативом
срок. Дальнейшие шаги — как при самовывозе.
- branch: Поставщик не подписал акт приёма-передачи в срок
at_step: 5
action: null
actor: offerer
description: >
Поставщик не поставил дистанционную первую подпись на акте
приёмки-передачи в установленный срок. Выдача заказчикам не
открывается; дальнейшее решение — по регламенту кооператива
(досверка, перепоставка, претензия).
# ── Секция 5. Документы и подписи ───────────────────────────────────────────
# В процессе подписываются два юридически значимых документа: АПП приёмки
# (двойная подпись поставщик → председатель) и АПП выдачи (двойная подпись
# председатель → заказчик). Дополнительно в модели B печатается и подписывается
# бумажная ТТН (вне блокчейна) с фотофиксацией для целей дистанционной ЭЦП
# поставщика. Соглашение ЦПП «Стол заказов» подписывается ОДНОКРАТНО при
# первом входе пайщика и относится к процессу подключения к ЦПП (не входит
# в этот процесс).
documents:
- action: marketplace::signsupp
title: АПП приёмки имущества кооперативом (первая подпись поставщика)
registry_id: 702
signed_by: [offerer]
stored_in: orders.acceptance_act
note: "Используется существующий шаблон 702.AssetContributionAct из cooptypes/cooperative/registry/. Оригинально создан под клиринговую модель donor'а; форма акта приёма имущества кооперативом структурно подходит и для членской модели. При необходимости методолог может создать специализированный шаблон в новой серии (1100+) — тогда registry_id обновится."
- action: marketplace::signchair
title: АПП приёмки имущества кооперативом (финальная подпись председателя)
registry_id: 702
signed_by: [chairman]
stored_in: orders.acceptance_act
note: "Тот же шаблон что у signsupp (registry_id 702). Контракт verify_document_or_fail дополняет вторую подпись на тот же АПП."
- action: marketplace::signiss1
title: АПП выдачи имущества пайщику (первая подпись председателя)
registry_id: 802
signed_by: [chairman]
stored_in: orders.issue_act
note: "Используется существующий шаблон 802.ReturnByAssetAct из cooptypes/cooperative/registry/. Оригинально создан под клиринговую модель «возврат паевого взноса имуществом»; форма акта передачи имущества от кооператива пайщику структурно подходит и для членской модели выдачи. При необходимости методолог может создать специализированный шаблон."
- action: marketplace::signiss2
title: АПП выдачи имущества пайщику (финальная подпись заказчика)
registry_id: 802
signed_by: [orderer]
stored_in: orders.issue_act
note: "Тот же шаблон что у signiss1 (registry_id 802)."
- action: marketplace::prepship
title: Товарно-транспортная накладная (при доставке через экспедитора)
registry_id: 0
signed_by: [offerer, chairman, expediter]
stored_in: shipments.ttn_pdf
note: "TODO: создать шаблон ТТН в registry. При самовывозе не используется. Подписи проставляются вне блокчейна (на бумажном носителе); фотофиксация председателя по результатам очной сверки сохраняется как основание для дистанционной подписи поставщика."
# ── Секция 6. Операции (Ledger2) ────────────────────────────────────────────
# Пять ledger2-операций marketplace для процесса прямой поставки-приобретения.
# Имена ≤12 символов eosio::name. Кошельки идентифицируются eosio::name-строками
# с префиксом w.<contract>.<waltype>:
# • w.wal.share — Паевой взнос пайщика (контракт wallet, USER_SHARED).
# Источник средств при создании заказа.
# • w.mkt.member — ЦПП «Стол Заказов» — членский кошелёк пайщика программы
# (USER_SHARED). Приёмник возврата резерва при отмене Order'а
# и при недовыдаче; приёмник конвертации паевого под доплату.
# Средства остаются в программе под следующие заказы (не
# уходят в универсальный членский).
# • w.mkt.order — ЦПП «Стол Заказов» — резерв средств пайщика под
# конкретный Order (USER_SHARED). Сюда переводятся
# средства на createorder с w.wal.share; обратно на
# w.mkt.member — при cancel/expire/decline; сжигаются
# BURN'ом — на signiss2.
# • w.mkt.payout — Выплаты поставщикам [платформенный]
# null — для незатронутого уровня; sentinel '' — legacy-допуск (∅ в UI).
operations:
- ledger_code: o.mkt.lock
human_name: Резервирование средств под заказ
wallet_op: TRANSFER
# L1 — двойная запись (паевой → целевое финансирование)
debit: 80 # Паевой фонд
credit: 86 # Целевое финансирование
# L2 — реальный перевод между двумя кошельками заказчика
wallet_from: w.wal.share # Паевой взнос пайщика
wallet_to: w.mkt.order # Резерв под конкретный Order
# L3 — две стороны движения у одного и того же пайщика
l3:
- user_wallet: w.wal.share
user_ref: order.orderer
available_delta: -order.total_cost
blocked_delta: null
- user_wallet: w.mkt.order
user_ref: order.orderer
available_delta: +order.total_cost
blocked_delta: null
amount_ref: order.total_cost
triggered_by: marketplace::createorder
description: >
Единственный обязательный шаг createorder. Резервирование суммы
order.total_cost под конкретный Order заказчика: средства переводятся
с паевого взноса пайщика (w.wal.share) на отдельный кошелёк-резерв
(w.mkt.order). Двойная запись Дт 80 / Кт 86 — переход из складочного
капитала в целевое финансирование. Резерв снимается либо при выдаче
(o.mkt.consum — сжигание резерва BURN'ом при подписании АПП выдачи),
либо при отмене (o.mkt.unlock — обратный TRANSFER в членский кошелёк
«Стола заказов»).
- ledger_code: o.mkt.unlock
human_name: Снятие резерва при отмене заказа
wallet_op: TRANSFER
# L1 — без проводки (оба кошелька на 86, фиктивная Дт=Кт игнорируется)
debit: null
credit: null
# L2 — обратный перевод из резерва в членский «Стола заказов» пайщика
wallet_from: w.mkt.order # Резерв под Order
wallet_to: w.mkt.member # Членский кошелёк «Стола заказов» пайщика
# L3 — две стороны движения у одного и того же пайщика
l3:
- user_wallet: w.mkt.order
user_ref: order.orderer
available_delta: -order.total_cost
blocked_delta: null
- user_wallet: w.mkt.member
user_ref: order.orderer
available_delta: +order.total_cost
blocked_delta: null
amount_ref: order.total_cost
triggered_by: marketplace::cancelorder | marketplace::expirecycle | marketplace::declinebatch
description: >
Снятие резерва при отмене Order'а (заказчик отменяет до акцепта;
цикл истёк без достижения порога; поставщик отказался от партии),
а также возврат остатка при недовыдаче (signiss2, факт меньше заказа).
Средства возвращаются с кошелька-резерва (w.mkt.order) на членский
кошелёк «Стола заказов» заказчика (w.mkt.member.available) — остаются
в программе и могут быть направлены на следующие заказы. По бухгалтерии —
без проводки (оба кошелька на счёте 86 — перевод внутри одной аналитики).
- ledger_code: o.mkt.conv
human_name: Конвертация паевого в членский «Стола заказов» под доплату
wallet_op: TRANSFER
# L1 — двойная запись (паевой → целевое финансирование)
debit: 80 # Паевой фонд
credit: 86 # Целевое финансирование
# L2 — паевой взнос пайщика конвертируется в членский программы
wallet_from: w.wal.share # Паевой взнос пайщика
wallet_to: w.mkt.member # Членский кошелёк «Стола заказов» пайщика
# L3 — две стороны движения у одного и того же пайщика
l3:
- user_wallet: w.wal.share
user_ref: order.orderer
available_delta: -diff
blocked_delta: null
- user_wallet: w.mkt.member
user_ref: order.orderer
available_delta: +diff
blocked_delta: null
amount_ref: fact_cost - order.total_cost
triggered_by: marketplace::signiss2
description: >
Доплата по факту при выдаче большего количества/по большей цене НЕ
списывается с паевого напрямую. Дополнительный паевой взнос заказчика
конвертируется в членский взнос «Стола заказов»: TRANSFER w.wal.share →
w.mkt.member, двойная запись Дт 80 / Кт 86 (переход из складочного
капитала в целевое финансирование). Парно срабатывает o.mkt.lockm,
добирая этим членским резерв заказа. Если на паевом недостаточно средств
на разницу — выдача с доплатой не проводится.
- ledger_code: o.mkt.lockm
human_name: Добор резерва заказа с членского «Стола заказов»
wallet_op: TRANSFER
# L1 — без проводки (оба кошелька на 86, фиктивная Дт=Кт игнорируется)
debit: null
credit: null
# L2 — перевод из членского «Стола заказов» в резерв под Order
wallet_from: w.mkt.member # Членский кошелёк «Стола заказов» пайщика
wallet_to: w.mkt.order # Резерв под конкретный Order
# L3 — две стороны движения у одного и того же пайщика
l3:
- user_wallet: w.mkt.member
user_ref: order.orderer
available_delta: -diff
blocked_delta: null
- user_wallet: w.mkt.order
user_ref: order.orderer
available_delta: +diff
blocked_delta: null
amount_ref: fact_cost - order.total_cost
triggered_by: marketplace::signiss2
description: >
Парный к o.mkt.conv шаг доплаты по факту: сконвертированный членский
взнос «Стола заказов» (w.mkt.member) добирает резерв этого же Order'а
(w.mkt.order) на разницу. Списание под заказ идёт ИМЕННО с членского
программы, не с паевого напрямую. По бухгалтерии — без проводки
(оба кошелька на счёте 86 — перевод внутри одной аналитики).
- ledger_code: o.mkt.purch
human_name: Приёмка имущества кооперативом
wallet_op: NONE
# L1 — двойная запись приёмки имущества
debit: 10 # Материалы
credit: 86 # Целевое финансирование
# L2 — без движения по кошелькам кооператива (имущество — не кошелёк)
wallet_from: null
wallet_to: null
# L3 — без движения по кошелькам пайщиков
user_wallet: null
user_ref: null
available_delta: null
blocked_delta: null
amount_ref: order.total_cost
triggered_by: marketplace::signchair
description: >
Имущество принято на склад КУ-приёмщика по АПП приёмки (финальная
подпись председателя). Двойная запись Дт 10 / Кт 86 — приобретение
имущества за счёт целевых средств кооператива. Имущество отслеживается
бухгалтерской аналитикой по счёту 10 (per-КУ субсчета), без отдельного
кошелька. Срабатывает атомарно с o.mkt.payout.
- ledger_code: o.mkt.payout
human_name: Оплата поставщику с расчётного счёта
wallet_op: ISSUE
# L1
debit: 86 # Целевое финансирование
credit: 51 # Расчётный счёт
# L2 — эмиссия суммы в платформенный кошелёк выплат поставщикам
wallet_from: null # ISSUE: источника нет
wallet_to: w.mkt.payout # Выплаты поставщикам (платформенный)
# L3 — w.mkt.payout агрегатный (не USER_SHARED), L3 не заполняется
user_wallet: null
user_ref: null
available_delta: null
blocked_delta: null
amount_ref: order.total_cost
triggered_by: marketplace::payconfirm
description: >
Оплата поставщику с расчётного счёта кооператива по факту реального
банковского перевода (Locked Decision L12 — lazy payout). Двойная
запись Дт 86 / Кт 51 — расход целевых средств на оплату поставки
(упрощённая модель без счёта 60 «Расчёты с поставщиками»). Сумма
приземляется на платформенный кошелёк w.mkt.payout. Не атомарно с
o.mkt.purch — выплата проходит через контракт gateway: backend
инициирует исходящий платёж (marketplace::payout → inline
gateway::createoutpay); кассир проводит перевод банком и подтверждает
в столе процессинга (gateway::outcomplete); контракт gateway
callback'ом дёргает marketplace::payconfirm — и только здесь
применяется эта ledger2-операция. До этого момента обязательство
перед поставщиком висит на счёте 86 как открытое; защита от двойного
запроса — на уровне order.payout_status.
- ledger_code: o.mkt.consum
human_name: Выдача имущества пайщику по АПП выдачи
wallet_op: BURN
# L1 — двойная запись: целевое финансирование расходуется на выбытие
# имущества со склада (одной операцией, без транзита)
debit: 86 # Целевое финансирование
credit: 10 # Материалы
# L2 — BURN на кошельке-резерве (резерв сжигается, средства уходят из системы)
wallet_from: w.mkt.order # Резерв под Order
wallet_to: null # BURN — без получателя
# L3 — резерв заказчика обнуляется (целевое расходование)
user_wallet: w.mkt.order
user_ref: order.orderer
available_delta: -order.fact_cost
blocked_delta: null
amount_ref: order.fact_cost
triggered_by: marketplace::signiss2
description: >
Выдача имущества заказчику-пайщику по АПП выдачи (финальная подпись
заказчика). BURN на кошельке-резерве (w.mkt.order) — зарезервированная
сумма сжигается без получателя (целевое расходование членского взноса).
Двойная запись Дт 86 / Кт 10 — расход целевых средств на выбытие
имущества со склада. При расхождении факт vs заказ предшествуют
корректирующие операции: o.mkt.unlock на остаток → членский «Стола
заказов» (если факт меньше); o.mkt.conv + o.mkt.lockm на разницу
(если факт больше — паевой конвертируется в членский «Стола заказов»
и им добирается резерв, напрямую с паевого не списываем).
@@ -0,0 +1,294 @@
# ─────────────────────────────────────────────────────────────────────────────
# Стандарт «Утилизация скоропорта» — кооперативный процесс периодического
# списания со склада участка имущества, которое физически пропало или стало
# непригодным к выдаче заказчику (просрочка, повреждения, малоценные позиции).
#
# Процесс инициируется кооперативом по расписанию (по умолчанию раз в месяц;
# точную дату согласовывает бухгалтер). По итогам опроса складов председатель
# подписывает заявление о списании и вносит его на рассмотрение совета.
# Совет рассматривает заявление через типовой процесс решения совета
# (стандарт sov.decision) и подписывает протокол. По принятому протоколу
# кооператив списывает каждую позицию проекта со склада участка.
#
# Списание фиксирует, что имущество безвозвратно исчезло из кооператива
# (а не передано пайщику). По кошелькам заказчиков движений не происходит —
# это чисто кооперативный учётный расход.
#
# **Модель кошельков:** процесс не двигает кошельки — это бухгалтерское
# событие через проводки счёта 10 (учёт имущества) и 86 (ЦФ программы).
# Имущество отслеживается аналитикой по счёту 10 (per-КУ субсчета), без
# отдельного кошелька.
#
# В процессе участвует 1 ledger2-операция:
# • o.mkt.wroff — Дт 86 / Кт 10 (списание со склада через ЦФ программы)
#
# Канон формата:
# coopenomics-docs/docs/standards/_spec/canon.md
# Источники правды в коде:
# • cpp/marketplace/marketplace.hpp — actions (status: proposed)
# • cpp/marketplace/src/p.mkt.wroff/ — реализация
# • cpp/lib/core/ledger2/operations.hpp — OPERATION_REGISTRY
# расширение o.mkt.wroff
# • cpp/lib/core/ledger2/processes.hpp — processes::marketplace::WRITEOFF
# • cpp/lib/core/ledger2/accounts.hpp — Целевое финансирование (86),
# Материалы (10)
# ─────────────────────────────────────────────────────────────────────────────
# ── Секция 1. Паспорт ───────────────────────────────────────────────────────
process_type: p.mkt.wroff
id: public_marketplace_writeoff_process
title: Утилизация скоропорта
slug: writeoff
status: proposed
contract: marketplace
purpose: >
Штатный путь корректно отразить в учёте имущество, которое физически
пропало или стало непригодным к выдаче. Применяется, когда товар на
складе участка просрочился, испортился или иначе не может быть передан
заказчику. Без этого процесса такие позиции висели бы на складе
бесконечно — здесь кооператив честно фиксирует свои потери через
решение совета.
Процесс состоит из двух шагов. На первом шаге кооператив по расписанию
(по умолчанию раз в месяц; точную дату согласовывает бухгалтер)
опрашивает склады участков и собирает позиции, удовлетворяющие
критериям списания. Председатель оформляет проект списания, подписывает
заявление и вносит его на рассмотрение совета. На втором шаге совет
по типовому процессу решения совета рассматривает заявление и
подписывает протокол: при положительном решении каждая позиция
списывается со склада как безвозвратные потери; при отрицательном
проект отклоняется и позиции остаются на складе.
По кошелькам заказчиков движений не происходит — это чисто кооперативный
учётный расход на счёт целевого финансирования программы.
roles:
- chairman # председатель кооператива (подписывает заявление, вносит на совет)
- council # совет кооператива (принимает решение по протоколу)
# ── Секция 2. Действия контракта (блокчейн-уровень) ─────────────────────────
# Имена actions ≤12 символов eosio::name. Заявление председателя на
# рассмотрение совета вносится по типовому процессу «Решение совета»
# (стандарт sov.decision), поэтому здесь перечислены только специфические
# для списания действия — внесение проекта и исполнение позиций по
# принятому протоколу совета.
actions:
- name: marketplace::propwroff
human: Внести проект списания на рассмотрение совета
actor: chairman
role: opener
purpose: >
Председатель подписывает заявление о списании скоропорта — список
позиций, которые невозможно выдать заказчику (просроченные и не
востребованные, повреждённые, малоценные), — и вносит проект на
рассмотрение совета. Дальнейшее движение проекта (повестка совета,
голосование, подписание протокола) — по типовому процессу решения
совета.
- name: marketplace::execwroff
human: Исполнить позицию списания по протоколу совета
actor: chairman
role: closer
purpose: >
После принятия советом положительного решения по каждой позиции
проекта выполняется списание: имущество выбывает со склада участка,
сумма закрывается на счёт целевого финансирования программы. На
каждую позицию — операция o.mkt.wroff. Когда все позиции проекта
исполнены, проект считается завершённым.
# ── Секция 3. Граф состояний ────────────────────────────────────────────────
# Сущность: marketplace::writeoff_proposal (таблица wroffprops, scope=coopname).
# Запись агрегирует список позиций, попавших в один цикл списания, и проходит
# свой жизненный цикл (proposed → authorized → executed | rejected).
entity: marketplace::writeoff_proposal
entity_human: Проект списания скоропорта
entity_source: cpp/marketplace/src/p.mkt.wroff/
states:
- name: proposed
human: На рассмотрении совета
description: >
Председатель подписал заявление о списании и внёс проект на
рассмотрение совета. Ожидается решение совета по типовому процессу
решения совета.
kind: normal
- name: authorized
human: Решение совета принято
description: >
Совет принял положительное решение по проекту и подписал протокол.
По каждой позиции проекта последовательно выполняется списание со
склада участка.
kind: normal
- name: executed
human: Списание исполнено
description: >
Кооператив списал все позиции проекта со складов участков.
Имущество ушло из учёта как безвозвратные потери. По кошелькам
заказчиков движений не было.
kind: final
- name: rejected
human: Совет отклонил проект
description: >
Совет отклонил проект списания. Позиции остаются на складах
участков и могут попасть в следующий цикл списания.
kind: final
transitions:
- from: "∅"
to: proposed
action: marketplace::propwroff
actor: chairman
guards:
- Наступил регламентный срок очередного цикла списания (или ручной запуск председателем).
- Председатель подписал Заявление о списании скоропорта.
- На складах участков найдены позиции, удовлетворяющие критериям списания.
- from: proposed
to: authorized
actor: council
guards:
- Совет рассмотрел проект и принял положительное решение по типовому процессу решения совета.
- Председатель подписал Протокол совета о списании скоропорта.
- from: authorized
to: executed
action: marketplace::execwroff
actor: chairman
ledger_code: p.mkt.wroff
operations:
- o.mkt.wroff
guards:
- Решение совета по проекту принято и протокол подписан.
- Все позиции проекта последовательно исполнены.
- from: proposed
to: rejected
actor: council
guards:
- Совет рассмотрел проект и отклонил его по типовому процессу решения совета.
# ── Секция 4. Сценарий ──────────────────────────────────────────────────────
scenario:
steps:
- step: 1
title: Формирование и внесение проекта на рассмотрение совета
actor: chairman
action: marketplace::propwroff
description: >
Кооператив по расписанию (по умолчанию раз в месяц; точную дату
согласовывает бухгалтер) опрашивает склады участков и собирает
позиции, которые невозможно выдать заказчику: просроченные и не
востребованные, повреждённые, малоценные. Председатель оформляет
проект списания со списком позиций, подписывает заявление и
вносит проект на рассмотрение совета.
pre:
- Наступил регламентный срок очередного цикла списания.
- На складах участков найдены позиции, удовлетворяющие критериям списания.
- Председатель подписал Заявление о списании скоропорта.
post:
- Проект списания сформирован и подписан председателем.
- Проект внесён на рассмотрение совета.
- step: 2
title: Рассмотрение проекта советом
actor: council
action: null
description: >
Совет рассматривает заявление председателя по типовому процессу
решения совета — на очном заседании или в форме заочного
голосования — и принимает решение. При положительном решении
председатель подписывает протокол о списании. При отрицательном —
проект отклоняется.
pre:
- Проект внесён на рассмотрение совета.
post:
- Совет принял решение по проекту и подписал протокол.
- step: 3
title: Исполнение списания
actor: chairman
action: marketplace::execwroff
description: >
По принятому советом протоколу кооператив последовательно
списывает каждую позицию проекта со склада участка. Имущество
безвозвратно выбывает из учёта; сумма закрывается на счёт
целевого финансирования программы. По кошелькам заказчиков
движений нет.
pre:
- Совет принял положительное решение по проекту.
- Протокол совета о списании подписан.
post:
- Все позиции проекта списаны со складов участков.
- Имущество ушло из учёта как безвозвратные потери.
alternatives:
- branch: Совет отклонил проект
at_step: 2
action: null
actor: council
description: >
Совет отклонил проект списания — например, требуется
дополнительная экспертиза — либо отложил рассмотрение. Позиции
остаются на складах участков и могут попасть в следующий цикл
списания. Движений по имуществу и средствам не происходит.
- branch: Альтернатива — переуступка имущества по сниженной цене
at_step: 1
action: null
actor: chairman
description: >
Альтернативный путь для имущества, ещё пригодного к выдаче —
например, накануне истечения срока годности: кооператив сам
выступает поставщиком в новом предложении по сниженной цене и
ставит имущество на следующий цикл отсечки заявок. В MVP только
помечен; реализуется отдельным процессом в более поздней фазе.
# ── Секция 5. Документы и подписи ───────────────────────────────────────────
# Заявление председателя — собственный документ процесса (вносится советом
# на рассмотрение). Протокол совета — документ типового процесса решения
# совета (по стандарту sov.decision), к которому привязан проект списания.
documents:
- action: marketplace::propwroff
title: Заявление о списании скоропорта
registry_id: 1106
signed_by: [chairman]
stored_in: writeoff_proposals.statement
note: "Подписывается председателем перед внесением проекта на рассмотрение совета. Содержит список позиций к списанию (участок, наименование, количество, сумма, причина). Источник правды — components/cooptypes/src/cooperative/registry/1106.MarketplaceWriteoffStatement"
- action: null
title: Протокол совета о списании скоропорта
registry_id: 1105
signed_by: [chairman]
stored_in: writeoff_proposals.protocol
note: "Протокол по типовому процессу решения совета (стандарт sov.decision). Подписывается председателем после принятия советом положительного решения. Источник правды — components/cooptypes/src/cooperative/registry/1105.MarketplaceWriteoffProtocol"
# ── Секция 6. Операции (Ledger2) ────────────────────────────────────────────
# Одна ledger2-операция — списание имущества со склада через целевое
# финансирование программы. Срабатывает по каждой позиции проекта в
# транзакции исполнения списания.
operations:
- ledger_code: o.mkt.wroff
human_name: Утилизация скоропорта
wallet_op: NONE # только бухпроводка без движения по кошелькам
# L1 — двойная запись: целевое финансирование расходуется на выбытие
# имущества со склада (одной операцией).
debit: 86 # Целевое финансирование
credit: 10 # Материалы
# L2 — без перевода между кошельками кооператива
wallet_from: null
wallet_to: null
# L3 — без движения по кошелькам пайщиков (чисто кооперативный расход)
user_wallet: null
user_ref: null
available_delta: null
blocked_delta: null
amount_ref: writeoff_item.cost
triggered_by: marketplace::execwroff
description: >
Списание имущества со склада участка по протоколу совета. Двойная
запись Дт 86 / Кт 10 — расход целевых средств программы на выбытие
имущества со склада. Кошельки не двигаются — расход чисто кооперативный.
Применяется по каждой позиции из списка проекта — несколько
последовательных операций в одной транзакции исполнения.
@@ -0,0 +1,113 @@
# Выплата поставщику через контракт gateway
Шпаргалка для агентов: как устроен поток исходящей выплаты поставщику в
процессе `p.mkt.supply` после разделения `signchair` и `payout` (E11 техдолг
598-16, Locked Decision L12).
## Зачем разделили
Прежний `signchair` атомарно выполнял `o.mkt.purch + o.mkt.payout`
кооператив закрывал обязательство перед поставщиком по счёту 51 ДО
фактического банковского перевода. Формальное расхождение между bookkeeping
и реальной кассой; недопустимо для регуляторного релиза.
L12 требует, чтобы `o.mkt.payout` (Дт 86 / Кт 51) применялся только по факту
реального банковского перевода. Реальный перевод делает кассир через свой
стол в контракте `gateway`а контракт `gateway` уже умеет дёргать обратно
contract-callback'ом, как только кассир подтвердил/отказал.
## Триплет действий
| Action | Кто вызывает | Что делает |
|---------------------------|------------------------|-----------------------------------------------------------------|
| `marketplace::payout` | backend (auth coopname) | inline `gateway::createoutpay` — регистрирует исходящий платёж |
| `marketplace::payconfirm` | gateway (auth _gateway) | callback от `gateway::outcomplete` — применяет `o.mkt.payout` |
| `marketplace::paydecline` | gateway (auth _gateway) | callback от `gateway::outdecline` — фиксирует отказ, без ledger |
## Под-граф `order.payout_status`
```
marketplace::payout marketplace::payconfirm
none ────────────────────────► pending ────────────────────────► completed
│ marketplace::paydecline
declined ──┐
▲ │ marketplace::payout (повтор)
└───────┘
```
- `none` — приёмка завершена, выплата ещё не инициировалась.
- `pending` — gateway хранит запись `outcomes` со статусом pending, ждёт
действия кассира.
- `completed` — gateway стёр запись (на outcomplete) и callback'ом дёрнул
`payconfirm`; применён o.mkt.payout, обязательство закрыто.
- `declined` — gateway стёр запись (на outdecline) и callback'ом дёрнул
`paydecline`; обязательство Кт 86 остаётся открытым, сохраняется
`payout_decline_reason`. Backend может повторить `payout`.
Статус самого `Order.status` (READY_TO_RECEIVE / RECEIVED) в этом поток
никак не участвует — выплата может идти параллельно шагам выдачи.
## Контрактные детали
- `outcome_hash` для gateway = `order.hash`. Уникальность гарантирована
индексом `byhash` orders. На decline gateway стирает свою запись, что
снимает коллизию для повторной инициации.
- `username` для gateway = `order.offerer` — это пайщик-поставщик, который
получит деньги.
- `quantity` = `order.total_cost`.
- `callback_contract = _marketplace`, `confirm_callback = "payconfirm"_n`,
`decline_callback = "paydecline"_n`.
- `marketplace` уже в `contracts_whitelist` в `lib/consts.hpp`, поэтому
inline-вызов `gateway::createoutpay` проходит auth-чек на gateway-стороне.
Гарды:
- `payout`: `order.status ∈ { accepted_to_coop, ready_to_receive, received }`
и `order.payout_status ∈ { none, declined }`.
- `payconfirm` / `paydecline`: `require_auth(_gateway)` +
`order.payout_status == pending`.
`payout_decline_reason` очищается при инициации новой попытки (`payout`) и
при успехе (`payconfirm`); заполняется только в `paydecline`.
## Что меняется в backend (controller)
- `MarketplaceCanonicalBlockchainPort.payOut(coopname, order_hash)`
единственный action, который backend сам отправляет. Кассир будет вызывать
его из админки через application service (Story 5.6 ещё не реализована —
port готов).
- `payconfirm` / `paydecline` backend сам **не отправляет**. Контракт
gateway вызывает их inline'ом, parser2 разбирает их как обычные
blockchain_actions, controller подхватывает через delta-stream и обновляет
`marketplace_outgoing_payment_request.status` (PENDING_CASHIER_ACTION →
CONFIRMED_BY_CASHIER → LEDGER_RECORDED при `payconfirm`; CASHIER_DECLINED
при `paydecline`). Это уже зона Story 5.6 — здесь сделан только
C++/cooptypes/port-foundation.
## Что НЕ делать (anti-patterns)
- Не дёргать `Ledger2::apply(o.mkt.payout, …)` где-либо кроме `payconfirm`.
- Не выставлять `payout_status = completed` ни в одном action кроме
`payconfirm`.
- Не отправлять `payout` если `payout_status == pending` — это создаст
дубль `outcomes` в gateway (gateway сам ругнётся, но лучше отсечь раньше).
- Не разрешать backend дёргать `payconfirm` / `paydecline` напрямую —
только gateway-контракт через inline-action.
- Не вводить отдельный `outcome_hash` поле в `order` — пока он совпадает
с `order.hash`, дублирование избыточно.
## Источники правды
- `components/contracts/cpp/marketplace/src/p.mkt.supply/payout.cpp` /
`payconfirm.cpp` / `paydecline.cpp` — реализация.
- `components/contracts/cpp/lib/domain/table_marketplace_orders.hpp`
`order.payout_status`, `payout_decline_reason`, `OrderPayoutStatus` enum.
- `components/contracts/cpp/marketplace/p.mkt.supply.standard.yaml`
states/transitions/operations/scenario (`step 6a` / `6b` + alternative
«Кассир отклонил банковский перевод»).
- `components/contracts/cpp/lib/core/gateway/gateway.hpp`
`Gateway::create_outcome` helper и сигнатура `CREATEOUTPAY_SIGNATURE`.
- `components/contracts/cpp/gateway/src/outpay/outcomplete.cpp` /
`outdecline.cpp` — где gateway отправляет callback.
@@ -1,55 +0,0 @@
/**
\ingroup public_actions
\brief Подтверждение готовности выполнить заявку.
@details Данный метод позволяет пользователю, который получил предложение по своей заявке, подтвердить свою готовность его принять и выполнить. При этом формируется пакет документов, который отправляется в совет на утверждение.
@param username Имя пользователя, подтверждающего готовность выполнить предложение.
@param exchange_id ID предложения, которое следует подтвердить.
@note Авторизация требуется от аккаунта: @p username
*/
[[eosio::action]] void marketplace::accept(eosio::name coopname, eosio::name username, uint64_t exchange_id, document2 document) {
require_auth(coopname);
requests_index exchange(_marketplace, coopname.value);
auto change = exchange.find(exchange_id);
eosio::check(change != exchange.end(), "Заявка не найдена");
eosio::check(change -> status == "published"_n, "Только заявка в статусе ожидания может быть принята");
auto parent_change = exchange.find(change -> parent_id);
eosio::check(parent_change != exchange.end(), "Родительская заявка не найдена");
eosio::check(parent_change -> username == username, "Недостаточно прав доступа");
eosio::check(parent_change -> remain_units >= change -> remain_units, "Недостаточно объектов для поставки");
// Проверяем подпись документа
verify_document_or_fail(document);
exchange.modify(parent_change, _marketplace, [&](auto &i) {
i.remain_units -= change -> remain_units;
i.supplier_amount = (parent_change -> remain_units - change -> remain_units ) * parent_change -> unit_cost;
i.blocked_units += change -> remain_units;
});
exchange.modify(change, _marketplace, [&](auto &o){
o.status = "accepted"_n;
o.blocked_units += change -> remain_units;
o.remain_units = 0;
o.accepted_at = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
if (change -> type == "order"_n) {
o.contribute_product_statement = document;
} else if (change -> type == "offer"_n) {
o.return_product_statement = document;
};
});
action(
permission_level{ _marketplace, "active"_n},
_soviet,
_change_action,
std::make_tuple(change -> coopname, username, change -> username, exchange_id, change -> money_contributor, change -> product_contributor)
).send();
}
@@ -1,28 +0,0 @@
/**
\ingroup public_actions
\brief Добавление единиц товара к заявке.
@details Метод позволяет владельцу заявки дополнительно увеличить количество товара, доступное для обмена в рамках указанной заявки.
Используется, когда у продавца появляется дополнительное количество товара, которое он хочет добавить к существующей заявке.
@param username Имя пользователя, инициировавшего добавление.
@param exchange_id Идентификатор заявки, к которой добавляются единицы товара.
@param units Количество новых единиц товара, которые следует добавить к заявке.
@note Авторизация требуется от аккаунта: @p username
*/
[[eosio::action]] void marketplace::addunits(eosio::name coopname, eosio::name username, uint64_t exchange_id, uint64_t units) {
require_auth(coopname);
requests_index exchange(_marketplace, coopname.value);
auto change = exchange.find(exchange_id);
eosio::check(change -> username == username, "У вас нет прав на редактирование данной заявки");
eosio::check(change -> parent_id == 0, "Нельзя отредактировать количество единиц во встречной заявке. Отмените её и пересоздайте");
exchange.modify(change, _marketplace, [&](auto &c){
c.remain_units += units;
c.supplier_amount = (change -> remain_units + units) * change -> unit_cost;
});
};
@@ -1,32 +0,0 @@
/**
\ingroup public_actions
\brief Авторизация обмена советом.
@details Метод используется для подтверждения согласия совета на заявленный обмен.
Обычно этот метод вызывается после прохождения определенного процесса голосования или принятия решения советом.
Авторизованный обмен считается утвержденным и может быть выполнен.
@param exchange_id Идентификатор заявки на обмен, которую следует авторизовать.
@note Авторизация требуется от аккаунта: @p _soviet
*/
[[eosio::action]] void marketplace::authorize(eosio::name coopname, uint64_t exchange_id, uint64_t contribution_product_decision_id, document2 contribution_product_authorization, uint64_t return_product_decision_id, document2 return_product_authorization) {
require_auth(_soviet);
requests_index exchange(_marketplace, coopname.value);
auto change = exchange.find(exchange_id);
eosio::check(change != exchange.end(), "Ордер не найден");
exchange.modify(change, _soviet, [&](auto &o) {
o.status = "authorized"_n;
o.contribution_product_decision_id = contribution_product_decision_id;
o.contribution_product_authorization = contribution_product_authorization;
o.return_product_decision_id = return_product_decision_id;
o.return_product_authorization = return_product_authorization;
});
};
@@ -1,27 +0,0 @@
/**
\ingroup public_actions
\brief Отмена заявки и возврат токенов.
@details Позволяет пользователю отменить родительскую или дочернюю заявку, а также обеспечивает возврат токенов владельцу (если применимо). При отмене проверяется наличие заявки и её текущий статус.
@param username Имя пользователя, инициировавшего отмену.
@param exchange_id Идентификатор заявки для отмены.
@note Авторизация требуется от аккаунта: @p username
*/
[[eosio::action]] void marketplace::cancel(eosio::name coopname, eosio::name username, uint64_t exchange_id) {
require_auth(coopname);
requests_index exchange(_marketplace, coopname.value);
auto change = exchange.find(exchange_id);
eosio::check(change != exchange.end(), "Заявка не найдена");
//TODO перенести под проверку пользователя и вообще сценарий отмены проверить полностью
eosio::check(change -> status != "accepted"_n, "Заявка не может быть отменена сейчас");
if (change -> parent_id == 0) {
marketplace::cancel_parent(coopname, username, exchange_id);
} else {
marketplace::cancel_child(coopname, username, exchange_id);
};
}
@@ -1,325 +0,0 @@
// /**
// * @mainpage Описание системы заявок
// *
// * В системе имеются два типа заявок: order (заказ) и offer (предложение), а также два уровня заявок: родительская (base) и встречная (quote).
// * Используется одна таблица заявок: exchange(_marketplace, _marketplace). Встречная заявка всегда должна быть противополжного типа к родительской.
// *
// * <b>Группы заявок:</b>
// *
// * <b>1. Группа предложений от родителя:</b>
// * - Parent: type == 'offer' && (parent_id == 0) => имущественный паевый взнос
// * - Child: type == 'order' && (parent_id > 0) => денежный паевый взнос
// *
// * <b>2. Группа заказа от родителя:</b>
// * - Parent: type == "order" && (parent_id == 0) => денежный паевый взнос
// * - Child: type == "offer" && (parent_id > 0) => имущественный паевый взнос
// *
// * <b>Обозначения:</b>
// * - order => денежный паевый взнос
// * - offer => имущественный паевый взнос
// */
/**
* @brief Создание заявки на обмен
*
* @param type Тип заявки
* @param params Параметры заявки
*
* Общая функция для создания как родительских, так и дочерних заявок.
*/
void marketplace::create (eosio::name type, const exchange_params& params) {
cooperatives2_index coops(_registrator, _registrator.value);
auto coop = coops.find(params.coopname.value);
eosio::check(coop != coops.end() && coop -> is_coop(), "Кооператив не найден");
eosio::check(params.unit_cost.symbol == coop -> initial.symbol, "Неверный символ токен");
eosio::check(params.units > 0, "Количество единиц в заявке должно быть больше нуля");
eosio::check(params.unit_cost.amount >= 0, "Цена не может быть отрицательной");
if (params.parent_id == 0) {
marketplace::create_parent(type, params);
} else {
marketplace::create_child(type, params);
};
};
/**
* @brief Создание родительской заявки
*
* @param type Тип заявки
* @param params Параметры заявки
*
* Специализированная функция для создания родительской заявки.
*/
void marketplace::create_parent(eosio::name type, const exchange_params& params) {
eosio::check(type == "offer"_n, "В родительском заявке может быть только предложение");
requests_index exchange(_marketplace, params.coopname.value);
uint64_t id = get_global_id(_marketplace, "exchange"_n);
eosio::check(params.parent_id == 0, "Родительская заявка создаётся без указания родителя");
cooperatives2_index coops(_registrator, _registrator.value);
auto coop = coops.find(params.coopname.value);
eosio::check(coop != coops.end(), "Кооператив не найден");
eosio::check(coop -> is_coop() == true, "Организация - не кооператив");
participants_index participants(_soviet, params.coopname.value);
auto participant = participants.find(params.username.value);
eosio::check(participant != participants.end(), "Вы не являетесь членом указанного кооператива");
auto program = get_program_or_fail(params.coopname, params.program_id);
//срок гарантийного возврата должен быть установлен
eosio::check(params.product_lifecycle_secs > 0, "Гарантийный срок возврата для имущества должен быть установлен");
exchange.emplace(_marketplace, [&](auto &i) {
i.id = id;
i.type = type;
i.program_id = params.program_id;
i.username = params.username;
i.coopname = params.coopname;
i.status = "moderation"_n;
i.remain_units = params.units;
i.unit_cost = params.unit_cost;
i.supplier_amount = params.unit_cost * params.units;
i.membership_fee = asset(0, coop -> initial.symbol);
i.total_cost = asset(0, coop -> initial.symbol);
i.product_lifecycle_secs = params.product_lifecycle_secs;
i.data = params.data;
i.meta = params.meta;
i.created_at = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
i.cancellation_fee_amount = asset(0, params.unit_cost.symbol);
});
action(
permission_level{ _marketplace, "active"_n},
_marketplace,
"newid"_n,
std::make_tuple(id, type)
).send();
};
/**
* @brief Создание дочерней заявки
*
* @param type Тип заявки
* @param params Параметры заявки
*
* Специализированная функция для создания дочерних заявок, связанных с родительской заявкой.
*/
void marketplace::create_child(eosio::name type, const exchange_params& params) {
eosio::check(type == "order"_n, "В дочерней заявки может быть только заказ");
requests_index exchange(_marketplace, params.coopname.value);
auto parent_change = exchange.find(params.parent_id);
eosio::check(parent_change != exchange.end(), "Заявка не обнаружена");
eosio::check(parent_change -> status == "published"_n, "Заявка не опубликована или не прошла модерацию");
cooperatives2_index coops(_registrator, _registrator.value);
auto coop = coops.find(params.coopname.value);
eosio::check(coop != coops.end(), "Кооператив не найден");
eosio::check(coop -> is_coop() == true, "Организация - не кооператив");
eosio::check(parent_change -> unit_cost.amount == params.unit_cost.amount, "Торги запрещены");
eosio::check(params.parent_id > 0, "Встречная заявка создаётся с указанием родителя");
eosio::check(params.document.has_value(), "Документ должен быть приложен к транзакции");
//проводим проверку подписи документа
verify_document_or_fail(*params.document);
uint64_t id = get_global_id(_marketplace, "exchange"_n);
auto program = get_program_or_fail(params.coopname, params.program_id);
eosio::check(parent_change -> program_id == params.program_id, "Целевые программы должны совпадать");
participants_index participants(_soviet, params.coopname.value);
auto participant = participants.find(params.username.value);
eosio::check(participant != participants.end(), "Вы не являетесь членом указанного кооператива");
uint64_t product_lifecycle_secs = 0;
eosio::asset membership_fee;
eosio::asset supplier_amount = params.unit_cost * params.units;
if (program.calculation_type == "absolute"_n) {
membership_fee = program.fixed_membership_contribution;
} else if (program.calculation_type == "relative"_n) {
membership_fee = supplier_amount * HUNDR_PERCENTS / program.membership_percent_fee;
} else if (program.calculation_type == "free"_n) {
membership_fee = asset(0, _root_govern_symbol);
};
eosio::asset total_cost = params.unit_cost * params.units + membership_fee;
//Специальные проверки
if (type == "offer"_n) {
//родительская заявка должна быть противоположного типа
eosio::check(parent_change -> type == "order"_n, "Неверный тип родительской заявки");
} else if(type == "order"_n) {
//родительская заявка должна быть противоположного типа
eosio::check(parent_change -> type == "offer"_n, "Неверный тип родительской заявки");
std::string memo = "Начало поставки по программе №" + std::to_string(params.program_id) + " с ID: " + std::to_string(id);
//Для блокировки средств необходимо их иметь на ЦПП, т.е. предварительно необходимо сконвертировать их с ЦПП кошелька
action(
permission_level{ _marketplace, "active"_n},
_soviet,
"blockbal"_n,
std::make_tuple(params.coopname, params.username, params.program_id, total_cost, memo)
).send();
}
exchange.emplace(_marketplace, [&](auto &i) {
i.id = id;
i.parent_id = params.parent_id;
i.parent_username = parent_change -> username;
i.type = type;
i.program_id = parent_change -> program_id;
i.coopname = params.coopname;
i.username = params.username;
i.status = "published"_n;
i.remain_units = params.units;
i.unit_cost = params.unit_cost;
i.membership_fee = membership_fee;
i.supplier_amount = supplier_amount;
i.total_cost = total_cost;
// не дублируем информацию
// i.data = params.data;
// i.meta = params.meta;
i.created_at = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
i.cancellation_fee_amount = asset(0, params.unit_cost.symbol);
if (type == "order"_n) {
print("on create child order");
i.return_product_statement = *params.document;
i.money_contributor = params.username;
i.product_contributor = parent_change -> username;
i.product_lifecycle_secs = parent_change -> product_lifecycle_secs;
} else if (type == "offer"_n) {
print("on create child offer");
i.contribute_product_statement = *params.document;
i.money_contributor = parent_change -> username;
i.product_contributor = params.username;
i.product_lifecycle_secs = params.product_lifecycle_secs;
};
});
action(
permission_level{ _marketplace, "active"_n},
_marketplace,
"newid"_n,
std::make_tuple(id, type)
).send();
};
/**
* @brief Отмена родительской заявки.
*
* Вызывается из `cancel`, если заявка является родительской.
* Выполняется проверка, что заявка не имеет заблокированных единиц товара, и удаляется из хранилища.
*
* @param username Имя пользователя, осуществляющего отмену заявки.
* @param exchange_id ID родительской заявки, которую нужно отменить.
*/
void marketplace::cancel_parent(eosio::name coopname, eosio::name username, uint64_t exchange_id) {
requests_index exchange(_marketplace, coopname.value);
auto change = exchange.find(exchange_id);
//Удаление, если заблокированных объектов на поставке - нет.
eosio::check(change -> remain_units + change -> blocked_units == 0, "Заявка не может быть отменена из-за наличия заблокированных единиц товара");
exchange.erase(change);
};
/**
* @brief Отмена дочерней заявки.
*
* Вызывается из `cancel`, если заявка является дочерней.
* Обновляет количество оставшихся и заблокированных единиц товара в родительской заявке и удаляет дочернюю заявку из хранилища.
* В зависимости от статуса и типа заявки возможен возврат токенов "покупателю".
*
* @param username Имя пользователя, осуществляющего отмену заявки.
* @param exchange_id ID дочерней заявки, которую нужно отменить.
*/
void marketplace::cancel_child(eosio::name coopname, eosio::name username, uint64_t exchange_id) {
requests_index exchange(_marketplace, coopname.value);
auto change = exchange.find(exchange_id);
auto parent_change = exchange.find(change -> parent_id);
eosio::asset quantity = change -> unit_cost * change -> blocked_units;
// оповещаем совет об отмене и разблокируем средства
if (change -> type == "order"_n) {
std::string memo = "Отмена поставки по программе №" + std::to_string(change -> program_id) + " с ID: " + std::to_string(change -> id);
action(
permission_level{ _marketplace, "active"_n},
_soviet,
"unblockbal"_n,
std::make_tuple(coopname, change -> money_contributor, change -> program_id, change -> total_cost, memo)
).send();
};
if (change -> status == "authorized"_n) {
//возвращаем единицы товара в родительскую заявку
exchange.modify(parent_change, _marketplace, [&](auto &e) {
e.remain_units += change -> blocked_units;
e.blocked_units -= change -> blocked_units;
e.supplier_amount += change -> supplier_amount;
});
exchange.modify(change, _marketplace, [&](auto &c){
c.status = "canceled"_n;
c.canceled_at = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
});
//удаляем дочернюю заявку
// exchange.erase(change);
} else if (change -> status == "published"_n) {
exchange.modify(change, _marketplace, [&](auto &c){
c.status = "canceled"_n;
c.canceled_at = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
});
//удаляем дочернюю заявку
// exchange.erase(change);
} else {
//TODO здесь должно быть допустимо, но для каждого статуса по-своему
eosio::check(false, "Заявка находится в недопустимом статусе для отмены");
}
}
@@ -1,91 +0,0 @@
/**
\ingroup public_actions
\brief Подписание акта о приёме-передаче имущества.
* @details После успешного получения товара, получатель подписывает акт о приёме-передаче, что свидетельствует о юридическом завершении сделки. Этот акт делает пакет документов по данной сделке полным. После проведения ряда проверок, обновляются статусы и количество объектов в основной заявке и предложении. Если все объекты основной заявки обработаны, заявка удаляется из публикации. В зависимости от типа предложения, может осуществляться перевод токенов.
* @param username Имя пользователя-получателя товара.
* @param exchange_id ID предложения, под которым следует подписать акт.
* @note Авторизация требуется от аккаунта: @p username
*/
[[eosio::action]] void marketplace::complete(eosio::name coopname, eosio::name username, uint64_t exchange_id) {
require_auth(coopname);
requests_index exchange(_marketplace, coopname.value);
auto change = exchange.find(exchange_id);
eosio::check(change != exchange.end(), "Заявка не найдена");
eosio::check(change -> parent_id > 0, "У указанной заявки нет встречной заявки");
auto parent_change = exchange.find(change -> parent_id);
eosio::check(parent_change != exchange.end(), "Родительская заявка не найдена");
// eosio::check(change -> username == username, "Вы не можете подтвердить исполнение заявки");
eosio::check(change -> status == "recieved2"_n, "Заявка находится в неверном статусе для утверждения обмена");
eosio::check(change -> warranty_delay_until.sec_since_epoch() < eosio::current_time_point().sec_since_epoch(), "Время гарантийной задержки еще не истекло");
exchange.modify(parent_change, _marketplace, [&](auto &i) {
i.delivered_units += change -> blocked_units;
i.blocked_units -= change -> blocked_units;
if (i.blocked_units + parent_change -> remain_units == 0) {
i.status = "unpublished"_n; //снимаем родительскую заявку с публикации, если она исполнена
};
});
exchange.modify(change, _marketplace, [&](auto &o) {
o.status = "completed"_n;
o.delivered_units += change -> blocked_units;
o.blocked_units = 0;
o.completed_at = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
});
auto program = get_program_or_fail(coopname, change -> program_id);
std::string memo = "Успешное завершение поставки имущества по программе №" + std::to_string(change -> program_id) + " с ID: " + std::to_string(change -> id);
//Заказчику разблокируем баланс ЦПП кооплекса и списываем его
action(
permission_level{ _marketplace, "active"_n},
_soviet,
"unblockbal"_n,
std::make_tuple(coopname, change -> money_contributor, change -> program_id, change -> total_cost, memo)
).send();
action(
permission_level{ _marketplace, "active"_n},
_soviet,
"subbal"_n,
std::make_tuple(coopname, change -> money_contributor, change -> program_id, change -> total_cost, false, memo)
).send();
//Поставщику разблокируем средства в программе кооплейса
action(
permission_level{ _marketplace, "active"_n},
_soviet,
"unblockbal"_n,
std::make_tuple(coopname, change -> product_contributor, change -> program_id, change -> supplier_amount, memo)
).send();
if (change -> membership_fee.amount > 0) {
//распределяем членские взносы по фондам
action(
permission_level{ _marketplace, "active"_n},
_fund,
"spreadamount"_n,
std::make_tuple(coopname, change -> membership_fee)
).send();
// отмечаем распределение членских взносов в программе и кошельке пользователя
action(
permission_level{ _marketplace, "active"_n},
_soviet,
"addmemberfee"_n,
std::make_tuple(coopname, change -> money_contributor, change -> program_id, change -> membership_fee, memo)
).send();
}
}
@@ -1,48 +0,0 @@
/**
\ingroup public_actions
\brief Отказ от предложения.
* @details Этот метод позволяет пользователю отклонить предложение, представленное к его заявке.
* Выполняются следующие проверки:
* - Существование предложения с указанным ID.
* - Существование основной заявки.
* - Предложение находится в статусе "ожидание".
*
* Если отклонено предложение к заявке типа "order", осуществляется возврат токенов пользователю, которому были заблокированы токены при создании предложения.
*
* @param username Имя пользователя, отклоняющего предложение.
* @param exchange_id ID предложения, которое следует отклонить.
* @param meta Дополнительные метаданные, связанные с отказом.
*
* @note Авторизация требуется от аккаунта: @p username
*/
[[eosio::action]] void marketplace::decline(eosio::name coopname, eosio::name username, uint64_t exchange_id, std::string meta) {
require_auth(coopname);
requests_index exchange(_marketplace, coopname.value);
auto change = exchange.find(exchange_id);
auto parent_change = exchange.find(change -> parent_id);
eosio::check(change != exchange.end(), "Заявка не найдена");
eosio::check(parent_change != exchange.end(), "Родительская заявка не найдена");
eosio::check(change -> status == "published"_n, "Только заявка в статусе ожидания может быть отклонена");
exchange.modify(change, coopname, [&](auto &o){
o.status = "declined"_n;
o.declined_at = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
o.meta = meta;
});
if (change -> type == "order"_n) {
std::string memo = "Отказ в поставке по программе №" + std::to_string(change -> program_id) + " с ID: " + std::to_string(change -> id);
action(
permission_level{ _marketplace, "active"_n},
_soviet,
"unblockbal"_n,
std::make_tuple(coopname, change -> money_contributor, change -> program_id, change -> total_cost, memo)
).send();
};
}
@@ -1,86 +0,0 @@
/**
\ingroup public_actions
\brief Принятие заявки поставщиком.
@details Поставщик принимает заявку orderoffer на поставку имущества и предоставляет необходимые документы.
@param coopname Имя кооператива
@param supplier_braname Имя кооперативного участка поставщика
@param username Имя поставщика
@param request_hash Хэш заявки
@param convert_out Заявление на конвертацию
@param product_contribution_statement Заявление на имущественный паевой взнос
@note Авторизация требуется от аккаунта: @p coopname
**/
[[eosio::action]] void marketplace::accept(eosio::name coopname, eosio::name supplier_braname, eosio::name username, checksum256 request_hash, document2 convert_out, document2 product_contribution_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 == "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){
o.status = "accepted"_n;
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), // авторизация взноса имуществом
agenda_hash,
_marketplace,
Marketplace::get_valid_marketplace_action("authcontrib"_n),
"declineacc"_n,
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("")
);
};
@@ -1,45 +0,0 @@
/**
\ingroup public_actions
\brief Авторизация заявления на имущественный паевой взнос советом кооператива.
@details Совет кооператива авторизует заявление на имущественный паевой взнос.
@param coopname Имя кооператива
@param request_hash Хэш заявки
@param authorization Документ авторизации от совета
@note Авторизация требуется от аккаунта: @p _soviet
**/
[[eosio::action]] void marketplace::authcontrib(eosio::name coopname, checksum256 request_hash, document2 authorization) {
require_auth(_soviet);
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 == "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;
}
});
};
@@ -1,45 +0,0 @@
/**
\ingroup public_actions
\brief Авторизация заявления на возврат имущества советом кооператива.
@details Совет кооператива авторизует заявление на возврат имущества.
@param coopname Имя кооператива
@param request_hash Хэш заявки
@param authorization Документ авторизации от совета
@note Авторизация требуется от аккаунта: @p _soviet
**/
[[eosio::action]] void marketplace::authreturn(eosio::name coopname, checksum256 request_hash, document2 authorization) {
require_auth(_soviet);
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 == "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::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;
}
});
};
@@ -1,61 +0,0 @@
/**
\ingroup public_actions
\brief Отмена заявки пользователем.
@details Пользователь может отменить свою заявку с учетом комиссии за отмену.
@param coopname Имя кооператива
@param username Имя пользователя
@param request_hash Хэш заявки
@note Авторизация требуется от аккаунта: @p coopname
**/
[[eosio::action]] void marketplace::cancel(eosio::name coopname, eosio::name username, checksum256 request_hash) {
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.username == username || change.product_contributor == username, "Недостаточно прав доступа");
// Ограничения отмены
eosio::check(change.status != "completed"_n, "Нельзя отменить завершенную заявку");
eosio::check(change.status != "canceled"_n, "Заявка уже отменена");
eosio::check(change.status != "declined"_n, "Нельзя отменить отклоненную заявку");
// Поставщик не может отменить заявку после поставки
if ((change.money_contributor != username && change.product_contributor != username) ||
(change.product_contributor == username && (change.status == "supplied1"_n || change.status == "supplied2"_n ||
change.status == "delivered"_n || change.status == "received1"_n || change.status == "received2"_n))) {
eosio::check(false, "Поставщик не может отменить заявку после поставки");
}
std::string memo = "Отмена заявки №" + std::to_string(change.id);
// Возвращаем средства заказчику с учетом комиссии за отмену
if (username == change.money_contributor) {
eosio::asset return_amount = change.total_cost - change.cancellation_fee_amount;
// Списываем заблокированные средства
Wallet::sub_blocked_funds(_marketplace, coopname, change.money_contributor, change.total_cost, _marketplace_program, memo);
// Возвращаем средства в кошелёк
Wallet::add_available_funds(_marketplace, coopname, change.money_contributor, return_amount, _wallet_program, memo);
// Направляем комиссию в фонд членских взносов
if (change.cancellation_fee_amount.amount > 0) {
// Wallet::add_member_fee(_marketplace, coopname, change.username, _marketplace_program_id, change.cancellation_fee_amount, memo);
}
}
// Удаляем заявку из системы
auto change_itr = requests.find(change.id);
eosio::check(change_itr != requests.end(), "Заявка не найдена для удаления");
requests.erase(change_itr);
};
@@ -1,40 +0,0 @@
/**
\ingroup public_actions
\brief Завершение поставки.
@details После истечения гарантийной задержки происходит завершение поставки.
@param coopname Имя кооператива
@param username Имя пользователя
@param request_hash Хэш заявки
@note Авторизация требуется от аккаунта: @p coopname
**/
[[eosio::action]] void marketplace::complete(eosio::name coopname, eosio::name username, checksum256 request_hash) {
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 == "received"_n, "Завершение возможно только после статуса received");
eosio::check(eosio::time_point_sec(eosio::current_time_point()) >= change.warranty_delay_until, "Гарантийная задержка ещё не истекла");
// Проводки по кошельку
std::string memo = "Завершение поставки для заказа №" + std::to_string(change.id);
// Поставщик - списываем заблокированный баланс и начисляем в кошелёк
Wallet::sub_blocked_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);
// Членские взносы (если больше нуля)
if (change.membership_fee_amount.amount > 0) {
// Wallet::add_member_fee(_marketplace, coopname, change.username, _marketplace_program_id, change.membership_fee_amount, memo);
}
// Удаляем заявку из системы
auto change_itr = requests.find(change.id);
eosio::check(change_itr != requests.end(), "Заявка не найдена для удаления");
requests.erase(change_itr);
};
@@ -1,39 +0,0 @@
/**
\ingroup public_actions
\brief Отклонение заявки.
@details Отклонение заявки с указанием причины.
@param coopname Имя кооператива
@param username Имя пользователя
@param request_hash Хэш заявки
@param meta Причина отклонения
@note Авторизация требуется от аккаунта: @p coopname
**/
[[eosio::action]] void marketplace::decline(eosio::name coopname, eosio::name username, checksum256 request_hash, std::string 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 == "active"_n || change.status == "accepted"_n, "Можно отклонить только активную или принятую заявку");
std::string memo = "Отклонение заявки №" + std::to_string(change.id) + ": " + meta;
// Возвращаем средства заказчику если они были заблокированы
if (change.status == "active"_n && change.money_contributor != ""_n) {
// Списываем заблокированные средства
Wallet::sub_blocked_funds(_marketplace, coopname, change.money_contributor, change.total_cost, _marketplace_program, memo);
// Возвращаем средства в кошелёк
Wallet::add_available_funds(_marketplace, coopname, change.money_contributor, change.total_cost, _wallet_program, memo);
}
// Удаляем заявку из системы
auto change_itr = requests.find(change.id);
eosio::check(change_itr != requests.end(), "Заявка не найдена для удаления");
requests.erase(change_itr);
};
@@ -1,62 +0,0 @@
/**
\ingroup public_actions
\brief Отклонение принятия заявки советом (declineacc).
@details Данный метод вызывается советом когда заявление на конвертацию, возврат или взнос отклоняется.
После рефакторинга работаем только с одной заявкой orderoffer.
@param coopname Имя кооператива
@param request_hash Хэш заявки, которая должна быть отменена
@param reason Причина отклонения
@note Авторизация требуется от аккаунта: @p _soviet
*/
[[eosio::action]] void marketplace::declineacc(eosio::name coopname, checksum256 request_hash, std::string reason) {
require_auth(_soviet);
requests_index requests(_marketplace, coopname.value);
auto change_opt = Marketplace::get_request_by_hash(coopname, request_hash);
// Если заявка не найдена, ничего не делаем (возможно уже удалена)
if (!change_opt.has_value()) {
print("Заявка уже удалена или не найдена. Кооператив: ", coopname, ", причина: ", reason);
return;
}
auto change = change_opt.value();
// Логирование отклонения
print("Отклонение заявления советом. Заявка ID: ", change.id, ", причина: ", reason);
// Отклоняем заявку
decline_request(coopname, change);
}
/**
* @brief Статический метод для отклонения заявки (используется советом)
*/
void marketplace::decline_request(eosio::name coopname, const request& change) {
requests_index requests(_marketplace, coopname.value);
// Проверяем, что заявка все еще существует
auto change_itr = requests.find(change.id);
if (change_itr == requests.end()) {
return; // Заявка уже удалена
}
std::string memo = "Отклонение заявления советом по программе №" + std::to_string(_marketplace_program_id) + " для заявки ID: " + std::to_string(change.id);
// Возвращаем средства заказчику если они были заблокированы
if (change.type == "orderoffer"_n && change.total_cost.amount > 0 && change.money_contributor != ""_n) {
// Списываем заблокированные средства с маркетплейса
Wallet::sub_blocked_funds(_marketplace, coopname, change.money_contributor, change.total_cost, _marketplace_program, memo);
// Возвращаем средства в кошелёк
Wallet::add_available_funds(_marketplace, coopname, change.money_contributor, change.total_cost, _wallet_program, memo);
}
// Удаляем заявку из системы
requests.erase(change_itr);
}
@@ -1,35 +0,0 @@
/**
\ingroup public_actions
\brief Перевод заявки в статус готово к выдаче.
@details Председатель КУ переводит заявку в статус delivered независимо от транспортировки.
Используется когда транспортировка между КУ не нужна или имущество уже находится на складе.
Просто перевод статуса без дополнительных документов.
@param coopname Имя кооператива
@param username Имя пользователя (может быть поставщиком или заказчиком)
@param request_hash Хэш заявки
@note Авторизация требуется от аккаунта: @p coopname
**/
[[eosio::action]] void marketplace::delivered(eosio::name coopname, eosio::name username, checksum256 request_hash) {
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 == "supplied2"_n || change.status == "shiprecvd"_n, "Перевод в статус delivered возможен только после поставки (supplied2) или после получения на склад (shiprecvd)");
// Обновляем заявку - просто перевод статуса
auto change_itr = requests.find(change.id);
eosio::check(change_itr != requests.end(), "Заявка не найдена для обновления");
requests.modify(change_itr, _marketplace, [&](auto &o){
o.status = "delivered"_n;
o.delivered_at = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
// Устанавливаем крайний срок получения (3 дня)
o.deadline_for_receipt = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch() + 3 * 24 * 60 * 60);
});
};
@@ -1,110 +0,0 @@
/**
\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
*/
[[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) {
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(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(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, "Комиссия за отмену не может превышать общую стоимость заявки");
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) {
i.id = request_id;
i.hash = hash;
i.type = "orderoffer"_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;
});
// Конвертируем и блокируем средства заказчика
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);
};
@@ -1,39 +0,0 @@
/**
\ingroup public_actions
\brief Получение товара заказчиком.
@details Заказчик приходит на КУ для получения имущества. Председатель КУ подписывает акт и передаёт имущество заказчику.
@param coopname Имя кооператива
@param username Имя заказчика
@param request_hash Хэш заявки
@param document Акт получения имущества
@note Авторизация требуется от аккаунта: @p coopname
**/
[[eosio::action]] void marketplace::receive(eosio::name coopname, eosio::name username, checksum256 request_hash, document2 document) {
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, "Получение возможно только после статуса delivered");
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);
});
};
@@ -1,51 +0,0 @@
/**
\ingroup public_actions
\brief Подтверждение получения заказчиком.
@details Заказчик подтверждает факт получения имущества второй подписью акта приёма-передачи.
@param coopname Имя кооператива
@param username Имя заказчика
@param request_hash Хэш заявки
@param document Акт подтверждения получения
@note Авторизация требуется от аккаунта: @p coopname
**/
[[eosio::action]] void marketplace::receivecnf(eosio::name coopname, eosio::name username, checksum256 request_hash, document2 document) {
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 == "received1"_n, "Подтверждение возможно только после статуса received1");
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 = "received2"_n;
o.received_at = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
// Устанавливаем время окончания гарантийной задержки
o.warranty_delay_until = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch() + change.warranty_period_secs);
// Добавляем акт подтверждения с именем
Document::add_document(o.documents, DocumentNames::RECEIVE_ACT_CONF, document);
});
// Проводки по кошельку
std::string memo = "Подтверждение получения для заказа №" + std::to_string(change.id);
// Паевой фонд - уменьшаем циркуляцию через ledger2 (TRANSFER SHARE_FUND → SUPPLIER_PAYMENTS)
Ledger2::apply(_marketplace, coopname, operations::marketplace::CONFIRM_RECEIPT, change.base_cost, username, request_hash, memo);
// Заказчик - списываем заблокированный баланс
Wallet::sub_blocked_funds(_marketplace, coopname, change.money_contributor, change.total_cost, _marketplace_program, memo);
};
@@ -1,50 +0,0 @@
/**
\ingroup public_actions
\brief Подтверждение поставки председателем КУ.
@details Председатель кооперативного участка или доверенное им лицо подтверждает факт поставки имущества.
@param coopname Имя кооператива
@param username Имя представителя кооператива
@param request_hash Хэш заявки
@param act Акт подтверждения поставки
@note Авторизация требуется от аккаунта: @p coopname
**/
[[eosio::action]] void marketplace::supplcnf(eosio::name coopname, eosio::name username, checksum256 request_hash, document2 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 == "supplied1"_n, "Поставка должна быть подтверждена после статуса supplied1");
// Проверяем подпись документа
verify_document_or_fail(act);
// Валидируем документ по registry_id (пока что ноль)
Document::validate_registry_id(act, 0);
// Обновляем заявку
auto change_itr = requests.find(change.id);
eosio::check(change_itr != requests.end(), "Заявка не найдена для обновления");
requests.modify(change_itr, _marketplace, [&](auto &o){
o.status = "supplied2"_n;
o.warehouse = change.supplier_braname; // Ставим имущество на склад КУ приёма
o.supplied_at = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
// Добавляем акт подтверждения с именем
Document::add_document(o.documents, DocumentNames::SUPPLY_ACT_CONF, act);
});
// Проводки по кошельку
std::string memo = "Подтверждение поставки для заказа №" + std::to_string(change.id);
// Паевой фонд - увеличиваем циркуляцию через ledger2 (ISSUE в SHARE_FUND)
Ledger2::apply(_marketplace, coopname, operations::marketplace::CONFIRM_SUPPLY, change.total_cost, username, request_hash, memo);
// Поставщик - начисляем и блокируем средства
Wallet::add_available_funds(_marketplace, coopname, change.product_contributor, change.base_cost, _marketplace_program, memo);
Wallet::block_funds(_marketplace, coopname, change.product_contributor, change.base_cost, _marketplace_program, memo);
};
@@ -1,40 +0,0 @@
/**
\ingroup public_actions
\brief Поставка имущества в кооператив.
@details Поставщик поставляет имущество на указанный кооперативный участок и предоставляет подписанный акт приёма-передачи.
@param coopname Имя кооператива
@param username Имя поставщика
@param request_hash Хэш заявки
@param act Акт поставки имущества
@note Авторизация требуется от аккаунта: @p coopname
**/
[[eosio::action]] void marketplace::supply(eosio::name coopname, eosio::name username, checksum256 request_hash, document2 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 == "authorized"_n, "Только авторизованная заявка может быть поставлена");
eosio::check(change.product_contributor == username, "Недостаточно прав доступа");
// Проверяем подпись документа
verify_document_or_fail(act);
// Валидируем документ по registry_id (пока что ноль)
Document::validate_registry_id(act, 0);
// Обновляем заявку
auto change_itr = requests.find(change.id);
eosio::check(change_itr != requests.end(), "Заявка не найдена для обновления");
requests.modify(change_itr, _marketplace, [&](auto &o){
o.status = "supplied1"_n;
o.supplied_at = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
// Добавляем акт поставки с именем
Document::add_document(o.documents, DocumentNames::SUPPLY_ACT, act);
});
};
@@ -1,23 +0,0 @@
[[eosio::action]] void marketplace::delivered(eosio::name coopname, eosio::name username, uint64_t exchange_id) {
require_auth(coopname);
requests_index exchange(_marketplace, coopname.value);
auto change = exchange.find(exchange_id);
eosio::check(change != exchange.end(), "Заявка не найдена");
eosio::check(change -> parent_id > 0, "Только продукт по встречной заявке может быть доставлен");
eosio::check(change -> status == "supplied2"_n, "Продукт может быть поставлен только по заявке в статусе supplied2");
auto soviet = get_board_by_type_or_fail(coopname, "soviet"_n);
auto chairman = soviet.get_chairman();
eosio::check(username == chairman, "Недостаточно прав доступа для подтверждения доставки");
auto program = get_program_or_fail(coopname, change -> program_id);
exchange.modify(change, coopname, [&](auto &ch) {
ch.status = "delivered"_n;
ch.delivered_at = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
ch.deadline_for_receipt = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch() + change -> product_lifecycle_secs / 4);
});
}
@@ -1,113 +0,0 @@
[[eosio::action]] void marketplace::dispute(eosio::name coopname, eosio::name username, uint64_t exchange_id, document2 document){
require_auth(coopname);
requests_index exchange(_marketplace, coopname.value);
auto change = exchange.find(exchange_id);
eosio::check(change -> type == "order"_n, "Спор может быть открыт только по заявке на поставку");
eosio::check(change != exchange.end(), "Заявка не найдена");
eosio::check(change -> username == username, "Только заказчик может открыть спор");
eosio::check(change -> is_warranty_return == false, "Нельзя открыть спор на спор");
auto parent_change = exchange.find(change -> parent_id);
eosio::check(parent_change != exchange.end(), "Родительская заявка не найдена");
eosio::check(change -> status == "recieved2"_n, "Неверный статус для открытия спора");
exchange.modify(change, _marketplace, [&](auto &e){
e.status = "disputed"_n;
e.disputed_at = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
e.warranty_return_id = change -> id;
});
uint64_t new_id = get_global_id(_marketplace, "exchange"_n);
uint64_t new_parent_id = get_global_id(_marketplace, "exchange"_n);
auto cooperative = get_cooperative_or_fail(coopname);
// Проверяем подпись документа
verify_document_or_fail(document);
//открытая заявка от заказчика на поставку имущества поставщику
exchange.emplace(_marketplace, [&](auto &i) {
i.id = new_parent_id;
i.type = "offer"_n;
i.program_id = change -> program_id;
i.username = username;
i.parent_username = ""_n;
i.parent_id = 0;
i.coopname = coopname;
i.status = "published"_n;
i.remain_units = change -> remain_units;
i.unit_cost = change -> unit_cost;
i.supplier_amount = i.unit_cost * i.remain_units;
i.total_cost = i.supplier_amount;
i.membership_fee = asset(0, cooperative.initial.symbol);
i.cancellation_fee_amount = asset(0, cooperative.initial.symbol);
i.product_lifecycle_secs = change -> product_lifecycle_secs;
i.data = change -> data;
i.meta = change -> meta;
i.created_at = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
i.is_warranty_return = true;
i.warranty_return_id = change -> id;
});
action(
permission_level{ _marketplace, "active"_n},
_marketplace,
"newid"_n,
std::make_tuple(new_parent_id, "offer")
).send();
//новая встречная заявка для будущего возврата поставщику
exchange.emplace(_marketplace, [&](auto &i) {
i.id = new_id;
i.parent_id = new_parent_id;
i.type = "order"_n;
i.program_id = parent_change -> program_id;
i.coopname = coopname;
i.username = change -> parent_username;
i.parent_username = change -> username;
i.status = "published"_n;
i.remain_units = change -> remain_units;
i.unit_cost = change -> unit_cost;
i.supplier_amount = change -> supplier_amount;
i.total_cost = change -> supplier_amount;;
i.membership_fee = asset(0, cooperative.initial.symbol); //размер членского взноса должен установить председатель в методе resolve
i.cancellation_fee_amount = asset(0, cooperative.initial.symbol);
i.data = change -> data;
i.created_at = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
i.is_warranty_return = true;
i.warranty_return_id = exchange_id;
i.contribute_product_statement = document;
i.money_contributor = parent_change -> username;
i.product_contributor = change -> username;
i.product_lifecycle_secs = 0; //потому что гарантийного срока в споре нет
});
action(
permission_level{ _marketplace, "active"_n},
_marketplace,
"newid"_n,
std::make_tuple(new_id, "order"_n)
).send();
};
@@ -1,59 +0,0 @@
/**
\ingroup public_actions
\brief Открытие гарантийного спора по заявке.
@details Заказчик может открыть спор после получения товара, если есть проблемы с качеством или соответствием.
Создается претензия, которая сохраняется в документах заявки.
@param coopname Имя кооператива
@param username Имя заказчика, открывающего спор
@param request_hash Хэш заявки, по которой открывается спор
@param document Документ с описанием претензии
@note Авторизация требуется от аккаунта: @p coopname
**/
[[eosio::action]] void marketplace::dispute(eosio::name coopname, eosio::name username, checksum256 request_hash, document2 document){
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 == "orderoffer"_n, "Спор может быть открыт только по заявке orderoffer");
eosio::check(change.username == username, "Только заказчик может открыть спор");
eosio::check(change.is_warranty_return == false, "Нельзя открыть спор на спор");
eosio::check(change.status == "received"_n || change.status == "completed"_n, "Спор можно открыть только после получения товара");
// Проверяем подпись документа
verify_document_or_fail(document);
// Обновляем статус заявки на "disputed" и добавляем документ с претензией
auto change_itr = requests.find(change.id);
eosio::check(change_itr != requests.end(), "Заявка не найдена для обновления");
requests.modify(change_itr, _marketplace, [&](auto &e){
e.status = "disputed"_n;
e.disputed_at = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
e.is_warranty_return = true;
Document::add_document(e.documents, Marketplace::DocumentNames::WDISPUTE, document);
});
// Используем хэш заявки как идентификатор пакета решений
checksum256 agenda_hash = change.hash;
// Отправляем заявление на гарантийный возврат в совет
::Soviet::create_agenda(
_marketplace,
coopname,
username,
get_valid_soviet_action("mpwreturn"_n),
agenda_hash,
_marketplace,
Marketplace::get_valid_marketplace_action("wauthorize"_n),
"wdecline"_n,
document,
std::string("")
);
}
@@ -1,48 +0,0 @@
/**
\ingroup public_actions
\brief Принятие или отказ поставщика от товара в рамках гарантийного возврата
@details Поставщик может принять товар (accept=true) или отказаться от него (accept=false).
При принятии товар передается поставщику и диспут завершается.
При отказе товар остается у кооператива и диспут завершается.
@param coopname Имя кооператива
@param username Имя поставщика
@param request_hash Хэш заявки с диспутом
@param accept Принимает ли поставщик товар (true/false)
@param document Документ с решением поставщика
@note Авторизация требуется от аккаунта: @p coopname
*/
[[eosio::action]] void marketplace::waccept(eosio::name coopname, eosio::name username, checksum256 request_hash, bool accept, document2 document) {
require_auth(coopname);
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 == "woffered"_n, "Товар должен быть предложен поставщику");
eosio::check(change.product_contributor == username, "Только поставщик может принять решение о товаре");
// Проверяем подпись документа
verify_document_or_fail(document);
requests_index requests(_marketplace, coopname.value);
auto change_itr = requests.find(change.id);
eosio::check(change_itr != requests.end(), "Заявка не найдена для обновления");
if (accept) {
// Поставщик принимает товар
requests.modify(change_itr, _marketplace, [&](auto &ch) {
ch.status = "wcompleted"_n;
Document::add_document(ch.documents, Marketplace::DocumentNames::WACCEPT_ACT, document);
});
} else {
// Поставщик отказывается от товара
requests.modify(change_itr, _marketplace, [&](auto &ch) {
ch.status = "wdeclined"_n;
Document::add_document(ch.documents, Marketplace::DocumentNames::WACCEPT_ACT, document);
});
}
}
@@ -1,34 +0,0 @@
/**
\ingroup public_actions
\brief Авторизация гарантийного возврата советом
@details Совет авторизует принятие товара от заказчика и его последующую выдачу поставщику
@param coopname Имя кооператива
@param request_hash Хэш заявки с диспутом
@param wreturn_decision_id Идентификатор решения по принятию товара
@param wreturn_authorization Документ авторизации принятия товара
@param wsupply_decision_id Идентификатор решения по выдаче товара поставщику
@param wsupply_authorization Документ авторизации выдачи товара
@note Авторизация требуется от аккаунта: @p _soviet
*/
[[eosio::action]] void marketplace::wauthorize(eosio::name coopname, checksum256 request_hash, uint64_t wreturn_decision_id, document2 wreturn_authorization, uint64_t wsupply_decision_id, document2 wsupply_authorization) {
require_auth(_soviet);
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 == "disputed"_n, "Заявка должна быть в статусе диспута");
// Обновляем статус заявки и добавляем документы авторизации
requests_index requests(_marketplace, coopname.value);
auto change_itr = requests.find(change.id);
eosio::check(change_itr != requests.end(), "Заявка не найдена для обновления");
requests.modify(change_itr, _soviet, [&](auto &o) {
o.status = "wauthorized"_n;
Document::add_document(o.documents, Marketplace::DocumentNames::WRETURN_AUTH, wreturn_authorization);
Document::add_document(o.documents, Marketplace::DocumentNames::WSUPPLY_AUTH, wsupply_authorization);
});
};
@@ -1,40 +0,0 @@
/**
\ingroup public_actions
\brief Предложение товара поставщику в рамках гарантийного возврата
@details Кооператив предлагает поставщику забрать товар, возвращенный заказчиком.
Создается предложение с актом передачи.
@param coopname Имя кооператива
@param username Имя председателя, предлагающего товар
@param request_hash Хэш заявки с диспутом
@param document Акт передачи товара поставщику
@note Авторизация требуется от аккаунта: @p coopname
*/
[[eosio::action]] void marketplace::woffer(eosio::name coopname, eosio::name username, checksum256 request_hash, document2 document) {
require_auth(coopname);
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 == "wreturned"_n, "Товар может быть предложен только после возврата в кооператив");
auto soviet = get_board_by_type_or_fail(coopname, "soviet"_n);
auto chairman = soviet.get_chairman();
eosio::check(username == chairman, "Недостаточно прав доступа для предложения товара");
// Проверяем подпись документа
verify_document_or_fail(document);
// Обновляем статус заявки и добавляем акт предложения товара
requests_index requests(_marketplace, coopname.value);
auto change_itr = requests.find(change.id);
eosio::check(change_itr != requests.end(), "Заявка не найдена для обновления");
requests.modify(change_itr, _marketplace, [&](auto &ch) {
ch.status = "woffered"_n;
Document::add_document(ch.documents, Marketplace::DocumentNames::WOFFER_ACT, document);
});
}
@@ -1,40 +0,0 @@
/**
\ingroup public_actions
\brief Возврат товара от заказчика в кооператив
@details Заказчик возвращает товар в кооператив в рамках гарантийного возврата.
Председатель принимает товар и подписывает акт приёма.
@param coopname Имя кооператива
@param username Имя председателя, принимающего товар
@param request_hash Хэш заявки с диспутом
@param document Акт приёма товара от заказчика
@note Авторизация требуется от аккаунта: @p coopname
*/
[[eosio::action]] void marketplace::wreturn(eosio::name coopname, eosio::name username, checksum256 request_hash, document2 document) {
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 == "wauthorized"_n, "Товар может быть возвращен только по авторизованному диспуту");
auto soviet = get_board_by_type_or_fail(coopname, "soviet"_n);
auto chairman = soviet.get_chairman();
eosio::check(username == chairman, "Недостаточно прав доступа для принятия возврата");
// Проверяем подпись документа
verify_document_or_fail(document);
// Обновляем статус заявки и добавляем акт приёма возврата
auto change_itr = requests.find(change.id);
eosio::check(change_itr != requests.end(), "Заявка не найдена для обновления");
requests.modify(change_itr, _marketplace, [&](auto &ch) {
ch.status = "wreturned"_n;
Document::add_document(ch.documents, Marketplace::DocumentNames::WRETURN_ACT, document);
});
}
@@ -1,32 +0,0 @@
/**
\ingroup public_actions
\brief Модерация товара на маркетплейсе.
*
* Данный метод предназначен для модерации товара перед его публикацией на маркетплейсе.
* Метод может быть вызван только администратором маркетплейса.
*
* @param username Имя пользователя-администратора, который вызывает данный метод.
* @param exchange_id Уникальный идентификатор товара, который нужно опубликовать после модерации.
*
* @note Авторизация требуется от аккаунта: @p username
*/
[[eosio::action]] void marketplace::moderate(eosio::name coopname, eosio::name username, uint64_t exchange_id, uint64_t cancellation_fee) {
require_auth(coopname);
requests_index exchange(_marketplace, coopname.value);
auto change = exchange.find(exchange_id);
eosio::check(change != exchange.end(), "Объявление не найдено");
if (change -> status == "moderation"_n || change -> status == "prohibit"_n) {
eosio::check(cancellation_fee >= 0 && cancellation_fee < 100, "Комиссия отмены должна быть от 0 до 100 процентов");
eosio::asset cancellation_fee_amount = change -> total_cost * cancellation_fee / 100;
exchange.modify(change, username, [&](auto &o){
o.status = "published"_n;
o.cancellation_fee = cancellation_fee;
o.cancellation_fee_amount = cancellation_fee_amount;
});
}
};
@@ -1,16 +0,0 @@
/**
\ingroup public_actions
\brief Создать заявку на имущественный паевой взнос.
*
* Данный метод позволяет пользователю создать заявку на имущественный паевой взнос в системе.
*
* @param params Параметры для создания заявки на имущественный паевой взнос.
*
* @note Авторизация требуется от аккаунта: @p params.username
*/
[[eosio::action]] void marketplace::offer (const exchange_params& params) {
require_auth(params.coopname);
marketplace::create("offer"_n, params);
};
@@ -1,16 +0,0 @@
/**
\ingroup public_actions
\brief Создать заявку на денежный паевой взнос.
*
* Данный метод позволяет пользователю создать заявку на денежный паевой взнос в системе.
*
* @param params Параметры для создания заявки на денежный паевой взнос.
*
* @note Авторизация требуется от аккаунта: @p params.username
*/
[[eosio::action]] void marketplace::order (const exchange_params& params) {
require_auth(params.coopname);
marketplace::create("order"_n, params);
};
@@ -0,0 +1,50 @@
/**
* @brief Председатель принимает гарантийный возврат на очном осмотре (Story 7.4, p.mkt.return).
*
* - Ledger2::apply(o.mkt.return, fact_cost, orderer, hash=request.hash)
* — ISSUE w.mkt.member, Дт 10 / Кт 86. Восстановление средств на членском
* «Стола заказов» заказчика и возврат имущества на склад через целевое финансирование.
*
* Compensating forward, не revert (Locked Decision L3 — AR14): новое событие в
* journal с прикладным полем `original_consume_op_id` (заполняется backend'ом
* в submretrn) для трассировки. Исходная o.mkt.consum в журнале НЕ модифицируется.
*
* Status: approved_for_visit → return_accepted (final). Имущество возвращается
* на склад КУ; средства восстанавливаются на w.mkt.member.available заказчика.
*
* Guards:
* - Подписант (`signer`) авторизован для указанного КУ (`braname`).
* - return_request.status == approved_for_visit.
*
* @ingroup public_marketplace_actions
*/
void marketplace::accretrn(eosio::name coopname,
eosio::name signer,
eosio::name braname,
checksum256 request_hash,
document2 decision) {
require_auth(coopname);
auto branch = get_branch_or_fail(coopname, braname);
eosio::check(branch.is_user_authorized(signer),
"Подписант не уполномочен принимать возвраты данного кооперативного участка");
auto r = Marketplace::get_return_request_by_hash_or_fail(coopname, request_hash);
eosio::check(r.status == ReturnStatus::APPROVED_FOR_VISIT,
"Заявление не одобрено для очного осмотра");
if (!is_empty_document(decision)) {
verify_document_or_fail(decision, { signer });
}
// o.mkt.return: ISSUE w.mkt.member, Дт 10 / Кт 86
Ledger2::apply(_marketplace, coopname,
operations::marketplace::RETURN_BY_MEMBER,
r.fact_cost, r.orderer, r.hash,
Marketplace::Memo::get_return_by_member_memo(r.id, r.original_order_id));
Marketplace::update_return_request(coopname, r.id, [&](auto& upd) {
upd.status = ReturnStatus::RETURN_ACCEPTED;
upd.decision_visit = decision;
});
}
@@ -0,0 +1,38 @@
/**
* @brief Председатель удалённо одобряет очный визит (Story 7.2, p.mkt.return).
*
* Без ledger2-операций. Статус return_request: pending_review → approved_for_visit.
* decision document сохраняется (опциональный — может быть пустой, тогда решение
* фиксируется только статусом + actor + blockchain_actions[at]).
*
* Guards:
* - Подписант (`signer`) авторизован для указанного КУ (`braname`):
* председатель / trustee / trusted в `branches[braname]`.
* - return_request.status == pending_review.
*
* @ingroup public_marketplace_actions
*/
void marketplace::aprretrem(eosio::name coopname,
eosio::name signer,
eosio::name braname,
checksum256 request_hash,
document2 decision) {
require_auth(coopname);
auto branch = get_branch_or_fail(coopname, braname);
eosio::check(branch.is_user_authorized(signer),
"Подписант не уполномочен принимать решения по заявлениям данного кооперативного участка");
auto r = Marketplace::get_return_request_by_hash_or_fail(coopname, request_hash);
eosio::check(r.status == ReturnStatus::PENDING_REVIEW,
"Заявление не находится на рассмотрении");
if (!is_empty_document(decision)) {
verify_document_or_fail(decision, { signer });
}
Marketplace::update_return_request(coopname, r.id, [&](auto& upd) {
upd.status = ReturnStatus::APPROVED_FOR_VISIT;
upd.decision_remote = decision;
});
}
@@ -0,0 +1,41 @@
/**
* @brief Председатель удалённо отказывает в гарантийном возврате (Story 7.2, p.mkt.return).
*
* Финальное решение, без ledger2-операций. Статус: pending_review → rejected_remote.
* reason сохраняется в return_request.reason_remote для UI заказчика.
*
* Guards:
* - Подписант (`signer`) авторизован для указанного КУ (`braname`).
* - return_request.status == pending_review.
* - reason.size() > 0.
*
* @ingroup public_marketplace_actions
*/
void marketplace::rejretrem(eosio::name coopname,
eosio::name signer,
eosio::name braname,
checksum256 request_hash,
std::string reason,
document2 decision) {
require_auth(coopname);
eosio::check(reason.size() > 0 && reason.size() <= 500,
"Укажите причину отказа (от 1 до 500 символов)");
auto branch = get_branch_or_fail(coopname, braname);
eosio::check(branch.is_user_authorized(signer),
"Подписант не уполномочен принимать решения по заявлениям данного кооперативного участка");
auto r = Marketplace::get_return_request_by_hash_or_fail(coopname, request_hash);
eosio::check(r.status == ReturnStatus::PENDING_REVIEW,
"Заявление не находится на рассмотрении");
if (!is_empty_document(decision)) {
verify_document_or_fail(decision, { signer });
}
Marketplace::update_return_request(coopname, r.id, [&](auto& upd) {
upd.status = ReturnStatus::REJECTED_REMOTE;
upd.decision_remote = decision;
upd.reason_remote = reason;
});
}
@@ -0,0 +1,41 @@
/**
* @brief Председатель отказывает в гарантийном возврате на очном осмотре (Story 7.3, p.mkt.return).
*
* Финальное решение, без ledger2-операций. Статус: approved_for_visit → rejected_at_ku.
* reason сохраняется в return_request.reason_visit для UI заказчика.
*
* Guards:
* - Подписант (`signer`) авторизован для указанного КУ (`braname`).
* - return_request.status == approved_for_visit.
* - reason.size() > 0.
*
* @ingroup public_marketplace_actions
*/
void marketplace::rejretrn(eosio::name coopname,
eosio::name signer,
eosio::name braname,
checksum256 request_hash,
std::string reason,
document2 decision) {
require_auth(coopname);
eosio::check(reason.size() > 0 && reason.size() <= 500,
"Укажите причину отказа (от 1 до 500 символов)");
auto branch = get_branch_or_fail(coopname, braname);
eosio::check(branch.is_user_authorized(signer),
"Подписант не уполномочен принимать решения по возвратам данного кооперативного участка");
auto r = Marketplace::get_return_request_by_hash_or_fail(coopname, request_hash);
eosio::check(r.status == ReturnStatus::APPROVED_FOR_VISIT,
"Заявление не одобрено для очного осмотра");
if (!is_empty_document(decision)) {
verify_document_or_fail(decision, { signer });
}
Marketplace::update_return_request(coopname, r.id, [&](auto& upd) {
upd.status = ReturnStatus::REJECTED_AT_KU;
upd.decision_visit = decision;
upd.reason_visit = reason;
});
}
@@ -0,0 +1,85 @@
/**
* @brief Пайщик подаёт заявление на гарантийный возврат (Story 7.1, p.mkt.return).
*
* Без ledger2-операций. Создаётся return_request в pending_review;
* order.return_request_id ставится для двусторонней связи. Привязка к
* конкретному КУ не сохраняется — каждое последующее действие
* (aprretrem/rejretrem/accretrn/rejretrn) принимает `braname` параметром
* и валидирует его через `Branch::is_user_authorized`.
*
* Guards (из p.mkt.return.standard.yaml):
* - actor == original_order.orderer.
* - original_order.status == received.
* - original_order.warranty_until > now() (гарантийный срок не истёк, если
* warranty_period_secs > 0; иначе возврат запрещён).
* - photos.size() > 0 (фото приложены).
* - actual_quantity > 0 && <= original_order.actual_quantity.
* - request_hash уникален.
* - Active возврат на этот order ещё не открыт (idempotency через
* order.return_request_id == 0).
*
* @ingroup public_marketplace_actions
*/
void marketplace::submretrn(eosio::name coopname,
eosio::name orderer,
checksum256 request_hash,
checksum256 original_order_hash,
uint64_t actual_quantity,
std::string reason_text,
std::vector<checksum256> photos,
document2 statement) {
require_auth(coopname);
eosio::check(actual_quantity > 0, "Возвращаемое количество должно быть больше нуля");
eosio::check(!photos.empty(), "Приложите хотя бы одну фотографию товара");
eosio::check(reason_text.size() > 0 && reason_text.size() <= 500,
"Опишите причину возврата (от 1 до 500 символов)");
eosio::check(!Marketplace::get_return_request_by_hash(coopname, request_hash).has_value(),
"Заявление с таким идентификатором уже подано");
auto o = Marketplace::get_order_by_hash_or_fail(coopname, original_order_hash);
eosio::check(o.orderer == orderer,
"Вы не заказчик исходного заказа");
eosio::check(o.status == OrderStatus::RECEIVED,
"Возврат возможен только по выданному заказу");
eosio::check(o.return_request_id == 0,
"По этому заказу уже открыто заявление на возврат");
eosio::check(actual_quantity <= o.actual_quantity,
"Нельзя вернуть больше единиц, чем было выдано");
eosio::check(o.warranty_period_secs > 0,
"По этому заказу гарантия не предусмотрена");
const auto now = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
eosio::check(now < o.warranty_until,
"Гарантийный срок по заказу истёк");
const eosio::asset fact_cost = eosio::asset(
static_cast<int64_t>(actual_quantity) * o.unit_price.amount,
_root_govern_symbol);
// Создание return_request entity
return_requests_index requests(_marketplace, coopname.value);
uint64_t request_id = requests.available_primary_key();
requests.emplace(_marketplace, [&](auto& r) {
r.id = request_id;
r.hash = request_hash;
r.coopname = coopname;
r.orderer = orderer;
r.original_order_id = o.id;
r.original_order_hash = o.hash;
// r.original_consume_op_id заполнит backend post-effect через ParserClient
// (подбор по journal с process_hash=order.hash + operation_code=o.mkt.consum).
r.actual_quantity = actual_quantity;
r.fact_cost = fact_cost;
r.reason_text = reason_text;
r.photos = photos;
r.status = ReturnStatus::PENDING_REVIEW;
r.statement = statement;
});
// Двусторонняя связь — order.return_request_id
Marketplace::update_order(coopname, o.id, [&](auto& upd) {
upd.return_request_id = request_id;
});
}
@@ -0,0 +1,32 @@
/**
* @brief Поставщик акцептует один Order (Story 4.5, p.mkt.supply).
*
* Без ledger2-операций — статус active → accepted. Backend проходит циклом
* по orders соответствующего batch'а, вызывая `acceptorder` per Order
* (контракт не принимает векторов order_hash — единичные транзакции
* масштабируются на любой размер batch'а).
*
* После акцепта поставщик считается обязанным доставить партию: отдельная
* подпись «готов отгрузить» (бывший prepship) удалена из процесса —
* следующий шаг сразу signsupp с актом приёмки.
*
* Guards:
* - Order существует и в статусе active.
* - actor == order.offerer.
*
* @ingroup public_marketplace_actions
*/
void marketplace::acceptorder(eosio::name coopname,
eosio::name offerer,
checksum256 order_hash) {
require_auth(coopname);
auto o = Marketplace::get_order_by_hash_or_fail(coopname, order_hash);
eosio::check(o.offerer == offerer, "Вы не поставщик этого заказа");
eosio::check(o.status == OrderStatus::ACTIVE,
"Заказ уже не в активном статусе — акцептовать нельзя");
Marketplace::update_order(coopname, o.id, [&](auto& upd) {
upd.status = OrderStatus::ACCEPTED;
});
}
@@ -0,0 +1,34 @@
/**
* @brief Заказчик отменяет заказ до акцепта поставщиком (Story 4.4, p.mkt.supply).
*
* Триггерит `o.mkt.unlock` на full `order.total_cost` — TRANSFER w.mkt.order →
* w.mkt.member (снятие резерва). Сумма возвращается на членский «Стола заказов»
* `w.mkt.member.available` заказчика — остаётся в программе под следующие заказы.
*
* Guards:
* - Order существует.
* - actor == order.orderer.
* - Order в статусе active (до acceptorder). После acceptorder отмена
* запрещена — поставщик уже взял обязательство.
*
* @ingroup public_marketplace_actions
*/
void marketplace::cancelorder(eosio::name coopname,
eosio::name orderer,
checksum256 order_hash) {
require_auth(coopname);
auto o = Marketplace::get_order_by_hash_or_fail(coopname, order_hash);
eosio::check(o.orderer == orderer, "Вы не заказчик этого заказа");
eosio::check(o.status == OrderStatus::ACTIVE,
"Нельзя отменить заказ: он уже акцептован поставщиком");
Ledger2::apply(_marketplace, coopname,
operations::marketplace::UNLOCK_ORDER,
o.total_cost, orderer, o.hash,
Marketplace::Memo::get_cancel_order_memo(o.id));
Marketplace::update_order(coopname, o.id, [&](auto& upd) {
upd.status = OrderStatus::CANCELLED;
});
}
@@ -0,0 +1,101 @@
/**
* @brief Заказчик размещает заказ на товар (Story 4.1, p.mkt.supply шаг 1).
*
* Одна ledger2-операция:
* - `o.mkt.lock` (TRANSFER w.wal.share → w.mkt.order пайщика на total_cost,
* Дт 80 / Кт 86) — резерв средств заказчика под этот Order. Паевой пайщика
* переходит в целевое финансирование на резерв-кошелёк.
*
* Guards (из p.mkt.supply.standard.yaml + Locked Decision L6):
* - quantity > 0; unit_price > 0 в _root_govern_symbol.
* - Order с таким hash ещё не создан (idempotency).
* - Заказчик — активный пайщик кооператива (`get_participant_or_fail`).
* - `delivery_braname` существует в `branches` (КУ выдачи задаётся пайщиком
* из доступных и неизменен после создания Order'а).
* - w.wal.share.available заказчика >= total_cost; иначе createorder фейлится
* без создания Order'а.
* - Подписка пайщика на оферту ЦПП «Стол заказов» (L2/L3 онбординг) —
* автоматически проверяется в `ledger2::walletop` через
* `assert_program_signed` (cross-contract в `wallet::users.programs[]`)
* при первом TRANSFER на USER_SHARED-кошельке программы (w.mkt.order).
*
* Сообщения проверок — для прямого показа пользователю (UI ловит check'ом).
*
* @ingroup public_marketplace_actions
*/
void marketplace::createorder(eosio::name coopname,
eosio::name orderer,
checksum256 order_hash,
checksum256 offer_hash,
eosio::name offerer,
eosio::name delivery_braname,
uint64_t quantity,
eosio::asset unit_price,
uint32_t warranty_period_secs,
checksum256 batch_hash) {
require_auth(coopname);
// ── Базовая валидация параметров ────────────────────────────────────
eosio::check(quantity > 0, "Количество должно быть больше нуля");
eosio::check(unit_price.is_valid() && unit_price.amount > 0,
"Некорректная цена за единицу");
eosio::check(unit_price.symbol == _root_govern_symbol,
"Некорректный символ валюты в цене");
// Idempotency: Order с таким hash не должен существовать
eosio::check(!Marketplace::get_order_by_hash(coopname, order_hash).has_value(),
"Заказ с таким идентификатором уже создан");
// Заказчик — активный пайщик кооператива (бросает если не найден / blocked)
get_participant_or_fail(coopname, orderer);
// КУ выдачи существует
get_branch_or_fail(coopname, delivery_braname);
// ── Расчёт total_cost ────────────────────────────────────────────────
eosio::asset total_cost = eosio::asset(
static_cast<int64_t>(quantity) * unit_price.amount,
_root_govern_symbol);
eosio::check(total_cost.amount > 0,
"Итоговая сумма заказа должна быть больше нуля");
// ── Достаточность средств: w.wal.share.available >= total_cost ──────
auto bal_share = Marketplace::get_user_wallet_balance(
coopname, ledger2_wallets::SHARE_FUND_PAY, orderer);
eosio::check(bal_share.available >= total_cost,
std::string{"Недостаточно средств для заказа: требуется "} +
total_cost.to_string() + ", доступно " + bal_share.available.to_string());
// ── Создание Order entity (id потребуется для memo) ─────────────────
orders_index orders(_marketplace, coopname.value);
uint64_t new_id = orders.available_primary_key();
orders.emplace(_marketplace, [&](auto& o) {
o.id = new_id;
o.hash = order_hash;
o.coopname = coopname;
o.orderer = orderer;
o.offerer = offerer;
o.offer_hash = offer_hash;
o.delivery_braname = delivery_braname;
o.accept_braname = eosio::name{}; // заполняется на signsupp
o.quantity = quantity;
o.actual_quantity = quantity; // до signiss2 == quantity (Story 6.2/6.3)
o.unit_price = unit_price;
o.total_cost = total_cost;
o.fact_cost = total_cost; // до signiss2 == total_cost
o.warranty_period_secs = warranty_period_secs;
o.status = OrderStatus::ACTIVE;
o.batch_hash = batch_hash;
});
// ── o.mkt.lock: TRANSFER w.wal.share → w.mkt.order (Дт 80 / Кт 86) ───
Ledger2::apply(_marketplace, coopname,
operations::marketplace::LOCK_ORDER,
total_cost, orderer, order_hash,
Marketplace::Memo::get_create_order_block_memo(new_id));
}
@@ -0,0 +1,33 @@
/**
* @brief Поставщик отказывается от одного Order'а до акцепта (Story 4.5, p.mkt.supply).
*
* Per-Order: o.mkt.unlock на total_cost (TRANSFER w.mkt.order → w.mkt.member —
* резерв возвращается на членский «Стола заказов» заказчика) + статус active →
* cancelled. Backend проходит циклом по orders соответствующего batch'а,
* вызывая `declineorder` per Order — векторов order_hash нет.
*
* Guards:
* - Order существует и в статусе active.
* - actor == order.offerer.
*
* @ingroup public_marketplace_actions
*/
void marketplace::declineorder(eosio::name coopname,
eosio::name offerer,
checksum256 order_hash) {
require_auth(coopname);
auto o = Marketplace::get_order_by_hash_or_fail(coopname, order_hash);
eosio::check(o.offerer == offerer, "Вы не поставщик этого заказа");
eosio::check(o.status == OrderStatus::ACTIVE,
"Заказ уже не в активном статусе — отклонить нельзя");
Ledger2::apply(_marketplace, coopname,
operations::marketplace::UNLOCK_ORDER,
o.total_cost, o.orderer, o.hash,
Marketplace::Memo::get_decline_order_memo(o.id));
Marketplace::update_order(coopname, o.id, [&](auto& upd) {
upd.status = OrderStatus::CANCELLED;
});
}
@@ -0,0 +1,38 @@
/**
* @brief Backend закрывает один Order по таймауту цикла отсечки заявок
* (Story 4.3, p.mkt.supply).
*
* Вызывается бэкендом после расчёта по batch'у: если за время цикла Offer'а
* threshold не достигнут, бэкенд проходит циклом по всем active Order'ам
* этого batch'а и для каждого вызывает `expireorder`. Контракт не знает про
* threshold — это вычисление backend'а; on-chain — только закрытие конкретного
* Order'а с возвратом резерва.
*
* Per-Order: o.mkt.unlock на total_cost (TRANSFER w.mkt.order → w.mkt.member — возврат резерва на членский «Стола заказов» заказчика) + статус active → cancelled.
*
* Guards:
* - Order существует и в статусе active (после акцепта поставщика
* expireorder не применим — поставщик уже взял обязательство; такие
* Order'ы должны идти через `signiss2` обычным порядком либо через
* отдельный механизм просрочки доставки).
* - require_auth(coopname) — backend от имени кооператива.
*
* @ingroup public_marketplace_actions
*/
void marketplace::expireorder(eosio::name coopname,
checksum256 order_hash) {
require_auth(coopname);
auto o = Marketplace::get_order_by_hash_or_fail(coopname, order_hash);
eosio::check(o.status == OrderStatus::ACTIVE,
"Закрыть по таймауту можно только активный заказ");
Ledger2::apply(_marketplace, coopname,
operations::marketplace::UNLOCK_ORDER,
o.total_cost, o.orderer, o.hash,
Marketplace::Memo::get_expire_order_memo(o.id));
Marketplace::update_order(coopname, o.id, [&](auto& upd) {
upd.status = OrderStatus::CANCELLED;
});
}
@@ -0,0 +1,39 @@
/**
* @brief Callback от gateway о фактическом подтверждении исходящей выплаты
* поставщику (E11 техдолг 598-16, Locked Decision L12, p.mkt.supply).
*
* Inline-action отправляется контрактом gateway из `gateway::outcomplete`
* после того, как кассир в админке подтвердил реальный банковский перевод.
* Здесь — единственное место, где применяется бухгалтерская проводка
* выплаты:
*
* - Ledger2::apply(o.mkt.payout, total_cost, …, hash=order.hash) — Дт 86 / Кт 51.
*
* `outcome_hash` приходит из gateway и равен `order.hash` (так его задал
* marketplace::payout). Поиск Order'а — по индексу `byhash`.
*
* Guards:
* - require_auth(_gateway) — callback легитимен только от gateway-контракта.
* - Order найден по `outcome_hash`.
* - `payout_status == PENDING` — на NONE/COMPLETED/DECLINED callback не ждём.
*
* @ingroup public_marketplace_actions
*/
void marketplace::payconfirm(eosio::name coopname, checksum256 outcome_hash) {
require_auth(_gateway);
auto o = Marketplace::get_order_by_hash_or_fail(coopname, outcome_hash,
"Order не найден по outcome_hash из callback'а gateway");
eosio::check(o.payout_status == OrderPayoutStatus::PENDING,
"Callback gateway::outcomplete получен на Order не в статусе ожидания выплаты");
Ledger2::apply(_marketplace, coopname,
operations::marketplace::PAY_SUPPLIER,
o.total_cost, o.offerer, o.hash,
Marketplace::Memo::get_pay_supplier_memo(o.id));
Marketplace::update_order(coopname, o.id, [&](auto& upd) {
upd.payout_status = OrderPayoutStatus::COMPLETED;
upd.payout_decline_reason.clear();
});
}
@@ -0,0 +1,33 @@
/**
* @brief Callback от gateway об отклонении исходящей выплаты поставщику
* (E11 техдолг 598-16, Locked Decision L12, p.mkt.supply).
*
* Inline-action отправляется контрактом gateway из `gateway::outdecline` —
* кассир отметил, что банковский перевод не прошёл (нет реквизитов,
* платёж отменён банком, ошибка ввода). Бухгалтерия не двигается —
* обязательство Кт 86 перед поставщиком остаётся открытым. Backend может
* повторно вызвать `marketplace::payout` после исправления реквизитов;
* gateway-запись по этому outcome_hash уже стёрта на outdecline, поэтому
* повторная инициация проходит штатно (см. payout-гард `payout_status ∈ {
* NONE, DECLINED }`).
*
* Guards:
* - require_auth(_gateway) — callback легитимен только от gateway-контракта.
* - Order найден по `outcome_hash`.
* - `payout_status == PENDING`.
*
* @ingroup public_marketplace_actions
*/
void marketplace::paydecline(eosio::name coopname, checksum256 outcome_hash, std::string reason) {
require_auth(_gateway);
auto o = Marketplace::get_order_by_hash_or_fail(coopname, outcome_hash,
"Order не найден по outcome_hash из callback'а gateway");
eosio::check(o.payout_status == OrderPayoutStatus::PENDING,
"Callback gateway::outdecline получен на Order не в статусе ожидания выплаты");
Marketplace::update_order(coopname, o.id, [&](auto& upd) {
upd.payout_status = OrderPayoutStatus::DECLINED;
upd.payout_decline_reason = reason;
});
}
@@ -0,0 +1,50 @@
/**
* @brief Инициация исходящей выплаты поставщику по одному Order'у через gateway
* (E11 техдолг 598-16, Locked Decision L12, p.mkt.supply).
*
* Backend дёргает это действие, когда кассир в админке отметил готовность
* проводить выплату поставщику. Действие НЕ применяет ledger2 — оно лишь
* inline-вызовом регистрирует в gateway::outcomes запись типа «исходящий
* платёж» со статусом pending и привязанным callback'ом на marketplace. Сам
* Дт 86 / Кт 51 произойдёт уже в callback'е `payconfirm` после фактического
* банковского перевода (gateway::outcomplete вызывает кассир через свой
* стол), либо отменится в `paydecline` (gateway::outdecline).
*
* Inline-вызов: `gateway::createoutpay` с `callback_contract = _marketplace`,
* `confirm_callback = "payconfirm"_n`, `decline_callback = "paydecline"_n`,
* `outcome_hash = order.hash` (уникальность гарантирована индексом orders).
*
* Status Order'а не меняется (выплата может идти параллельно шагам выдачи).
* payout_status переходит NONE/DECLINED → PENDING; declined-кейс — повторная
* попытка после исправления реквизитов (gateway-запись была стёрта на outdecline).
*
* Guards:
* - Order существует и приёмка завершена (статус ∈ accepted_to_coop /
* ready_to_receive / received).
* - payout_status ∈ { NONE, DECLINED } — нельзя инициировать выплату поверх
* pending или completed.
*
* @ingroup public_marketplace_actions
*/
void marketplace::payout(eosio::name coopname, checksum256 order_hash) {
require_auth(coopname);
auto o = Marketplace::get_order_by_hash_or_fail(coopname, order_hash);
eosio::check(o.status == OrderStatus::ACCEPTED_TO_COOP ||
o.status == OrderStatus::READY_TO_RECEIVE ||
o.status == OrderStatus::RECEIVED,
"Выплата возможна только после приёмки имущества кооперативом");
eosio::check(o.payout_status == OrderPayoutStatus::NONE ||
o.payout_status == OrderPayoutStatus::DECLINED,
"Выплата уже инициирована либо завершена");
// Регистрация исходящего платежа в gateway. Сам Дт 86 / Кт 51 произойдёт
// в callback'е `payconfirm` от gateway после действия кассира.
Gateway::create_outcome(_marketplace, coopname, o.offerer, o.hash, o.total_cost,
_marketplace, "payconfirm"_n, "paydecline"_n);
Marketplace::update_order(coopname, o.id, [&](auto& upd) {
upd.payout_status = OrderPayoutStatus::PENDING;
upd.payout_decline_reason.clear(); // на случай повторной инициации после DECLINED
});
}
@@ -0,0 +1,78 @@
/**
* @brief Председатель приёмного КУ ставит закрывающую подпись на АПП приёмки
* по одному Order'у (Story 5.3/5.4, p.mkt.supply).
*
* Per-Order — только бухгалтерская приёмка имущества:
* - Ledger2::apply(o.mkt.purch, fact_cost, …, hash=order.hash) — Дт 10 / Кт 86.
*
* Факт приёмки (кол-во и цена за единицу) корректируется оператором при
* открытии приёмки и зашивается в акт, который утверждает поставщик: привезли
* меньше / другого качества → принимаем со скидкой. Кооператив приходует
* поставщику итоговую `fact_cost = actual_quantity × actual_unit_price`, а не
* исходную `o.total_cost`. Резерва пайщика на приёмке нет, поэтому веток
* возврата/доплаты (как в signiss2) здесь не требуется — это просто итоговая
* стоимость к получению поставщиком.
*
* Имущество приходуется на склад приёмного КУ (`accept_braname`); у кооператива
* возникает обязательство Кт 86 перед поставщиком. Фактическая выплата деньгами
* (Дт 86 / Кт 51) — отдельным lazy action'ом `marketplace::payout` после
* подтверждения кассиром реального банковского перевода (Locked Decision L12,
* E11 техдолг 598-16).
*
* Status: supply_prepared → accepted_to_coop. acceptance_act_signchair
* сохраняется. `payout_done` не выставляется — это атрибут payout-действия.
*
* Guards:
* - Order существует и в статусе supply_prepared.
* - Подписант (`signer`) авторизован для приёмного КУ — председатель,
* trustee либо доверенное лицо в `branches[accept_braname].trusted[]`.
* - На акте есть подписи поставщика и подписанта приёмки.
* - actual_quantity > 0; actual_unit_price > 0 и в валюте кооператива.
*
* @ingroup public_marketplace_actions
*/
void marketplace::signchair(eosio::name coopname,
eosio::name signer,
checksum256 order_hash,
uint64_t actual_quantity,
eosio::asset actual_unit_price,
document2 act) {
require_auth(coopname);
eosio::check(actual_quantity > 0, "Фактическое количество должно быть больше нуля");
eosio::check(actual_unit_price.symbol == _root_govern_symbol,
"Фактическая цена за единицу указана в неверной валюте");
eosio::check(actual_unit_price.amount > 0,
"Фактическая цена за единицу должна быть больше нуля");
auto o = Marketplace::get_order_by_hash_or_fail(coopname, order_hash);
eosio::check(o.status == OrderStatus::SUPPLY_PREPARED,
"Заказ не готов к приёмке кооперативом");
// Авторизация подписи: signer должен быть в trusted списке приёмного КУ.
auto branch = get_branch_or_fail(coopname, o.accept_braname);
eosio::check(branch.is_user_authorized(signer),
"Подписант не уполномочен подписывать акты приёмки данного кооперативного участка");
verify_document_or_fail(act, { o.offerer, signer });
// Итоговая стоимость к получению поставщиком — от скорректированного факта.
const eosio::asset fact_cost = eosio::asset(
static_cast<int64_t>(actual_quantity) * actual_unit_price.amount,
_root_govern_symbol);
eosio::check(fact_cost.amount > 0,
"Итоговая фактическая сумма приёмки должна быть больше нуля");
// Только приёмка имущества; payout — отдельный lazy action (L12).
Ledger2::apply(_marketplace, coopname,
operations::marketplace::PURCHASE_FROM_SUPPLIER,
fact_cost, o.offerer, o.hash,
Marketplace::Memo::get_purchase_from_supplier_memo(o.id));
Marketplace::update_order(coopname, o.id, [&](auto& upd) {
upd.status = OrderStatus::ACCEPTED_TO_COOP;
upd.actual_quantity = actual_quantity;
upd.fact_cost = fact_cost;
upd.acceptance_act_signchair = act;
upd.current_warehouse_braname = o.accept_braname; // имущество на приёмном складе
});
}
@@ -0,0 +1,44 @@
/**
* @brief Председатель КУ выдачи открывает выдачу первой подписью АПП-выдачи
* (Story 6.1, signiss1).
*
* Без ledger2-операций. Per-Order: статус accepted_to_coop → ready_to_receive;
* issue_act_signiss1 сохраняется; current_warehouse_braname обновляется на
* delivery_braname (фиксация факта логистической передачи имущества на склад
* выдачи — промежуточные перемещения по заготовочным КУ контрактом не
* подписываются, точка хранения переходит «скачком» в этот момент).
* Нотификация заказчику — post-effect в backend через ParserClient.
*
* Guards:
* - Order существует и в статусе accepted_to_coop.
* - Подписант (`signer`) авторизован для КУ выдачи (`o.delivery_braname`):
* председатель / trustee / trusted в `branches[delivery_braname]`.
* - verify_document_or_fail(act, {signer}).
* - Idempotency: is_empty_document(issue_act_signiss1).
*
* @ingroup public_marketplace_actions
*/
void marketplace::signiss1(eosio::name coopname,
eosio::name signer,
checksum256 order_hash,
document2 act) {
require_auth(coopname);
auto o = Marketplace::get_order_by_hash_or_fail(coopname, order_hash);
eosio::check(o.status == OrderStatus::ACCEPTED_TO_COOP,
"Заказ не готов к открытию выдачи");
eosio::check(is_empty_document(o.issue_act_signiss1),
"Первая подпись акта выдачи уже зафиксирована");
auto branch = get_branch_or_fail(coopname, o.delivery_braname);
eosio::check(branch.is_user_authorized(signer),
"Подписант не уполномочен подписывать акты выдачи данного кооперативного участка");
verify_document_or_fail(act, { signer });
Marketplace::update_order(coopname, o.id, [&](auto& upd) {
upd.status = OrderStatus::READY_TO_RECEIVE;
upd.issue_act_signiss1 = act;
upd.current_warehouse_braname = o.delivery_braname; // имущество готово к выдаче на КУ выдачи
});
}

Some files were not shown because too many files have changed in this diff Show More