Compare commits

...

929 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 d243bf5621 [chatcoop-hide-matrix-room-id][@ant] fix(security): не отдавать matrixRoomId на фронт — комнаты секретаря и транскрипции по opaque id
Typecheck / desktop (pull_request) Successful in 13m48s
Typecheck / controller (pull_request) Successful in 13m9s
matrixRoomId — это одновременно идентичность и capability: зная его, можно
попытаться войти в комнату. Раздавая его на фронт (страница «Комнаты секретаря»
и список транскрипций отдавали его в каждой строке), мы по сути публиковали
handle для входа в приватные комнаты любому, кто видит раздел. Приватность тогда
теряет смысл.

Комнаты секретаря:
- ChatcoopSecretaryRoom и RemoveSecretaryRoomInput переведены с matrixRoomId на
  внутренний opaque id реестра (DB-ключ, по нему войти в Matrix-комнату нельзя);
- удаление резолвится по id → matrixRoomId на бэкенде; мутация больше не
  возвращает matrixRoomId;
- фронт: row-key/удаление по id, убран показ matrixRoomId в таблице;
- попутно фикс латентного бага из PR #34: parseKind не знал про 'secretary' и
  мапил такие комнаты в capital_project — из-за чего editable=false и удаление
  падало с «удалять можно только комнаты секретаря».

Транскрипции:
- CallTranscription больше не отдаёт matrixRoomId (на фронте он не отображался,
  но уходил в payload по всем комнатам, включая приватные). Остаётся roomId
  (LiveKit room name — внутренний, не Matrix-handle);
- input-фильтр GetTranscriptionsInput.matrixRoomId сохранён: это вход, не утечка,
  и им пользуется blago-cli;
- blago-cli: рендер транскрипций больше не зависит от tr.matrixRoomId (строка
  «Matrix room» убрана, иначе pull и restore рассогласовались бы — restore не
  имеет контекста комнаты). Заодно поправлен fallback курсоров.

Вне scope намеренно: chatcoopListNonProjectCommunicationRooms и per-room запросы
оставлены с matrixRoomId — их зовёт только blago-cli (доверенный инструмент
председателя), на фронт они не уходят, и matrixRoomId там — операционный ключ
синхронизации.

logger.ts: при CONTROLLER_SCHEMA_GEN не подключаем файловые транспорты winston
(codegen рядом с работающим контейнером падал на EACCES в root-овый logs/).

schema.gql + zeus (sdk и controller) перегенерированы и закоммичены — иначе CI
typecheck собирает sdk против старого zeus и падает.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 07:59:34 +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
ant 4c4c611a3d [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-26 19:04:05 +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 d8c24ada78 [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-26 18:09:00 +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 61d53e7ea0 Merge remote-tracking branch 'origin/main' into dev
Build bootstrap container / build (push) Failing after 2m13s
2026-05-25 10:46:47 +00:00
coopops 25a917866c ci(release): вернуть webhook второй документации + переименовать токен в DOCS_DISPATCH_TOKEN
При удалении publish-docs я снёс не только мёртвый gh-pages-push, но и реальную
публикацию ВТОРОЙ документации (доки mono) через DOCS_DEPLOY_WEBHOOK_URL —
она отдельная от coopenomics и должна уезжать синхронно. Возвращаю её отдельным
lean-джобом trigger-mono-docs (только webhook, без gh-pages и mkdocs-сборки —
приёмник деплоит сам).

Токен GITEA_DISPATCH_TOKEN → DOCS_DISPATCH_TOKEN: Gitea резервирует префикс
GITEA_ для имён секретов, такой секрет не создать.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 10:40:30 +00:00
coopops c73e9b9e80 ci(release): дёргать coopenomics на ветке master (дефолтная), не main
Дефолтная ветка C9S/coopenomics — master; workflow_dispatch надо слать на
ref=master, иначе Gitea вернёт 404 по ветке.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 10:28:24 +00:00
coopops d7ad0d7251 ci(release): убрать publish-docs (gh-pages), триггер coopenomics через Gitea workflow_dispatch
После переезда на Gitea два downstream-джоба были наследием GitHub:
- publish-docs пушил site/ в gh-pages github.com под secrets.GITHUB_TOKEN
  (на Gitea — гитейный токен, невалиден) → Authentication failed. Pages
  устарел, доки теперь деплоит C9S/coopenomics. Джоб удалён целиком.
- trigger-coopenomics-docs слал repository_dispatch на github.com через
  peter-evans (COOPENOMICS_PAT пуст). Репо переехало в C9S/coopenomics на
  Gitea, а Gitea не имеет API для repository_dispatch — только
  workflow_dispatch. Заменено на curl к Gitea API workflow_dispatch с
  secret GITEA_DISPATCH_TOKEN; целевой workflow получит входы mono_sha/mono_ref.

publish-packages не трогаю — там нужен лишь NPM_TOKEN в секретах (E404 на
scoped-пакетах = отсутствует npm-авторизация).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 10:25:13 +00:00
ant 3f5859eb3e Merge pull request 'feat(chatcoop): комнаты секретаря + синхронизация непроектных комнат в blago' (#34) from feat/chatcoop-secretary-rooms into dev
Reviewed-on: #34
Reviewed-by: Алексей Муравьев <chairman.voskhod@gmail.com>
2026-05-25 09:53:32 +00: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
claude 393ea148f0 chore(compose): run dev-сервисы от uid 1000, чтобы codegen не плодил root-файлы
coopback/cooparser/desktop монтируют ./:/app, а node:22-slim без USER пишет
от root — codegen (generate-client, schema.gql, logs, quasar-кэш) кладёт
root-owned файлы в дерево и ломает git checkout (git работает от ant без sudo).
Добавлен user: "1000:1000" + HOME=/tmp, чтобы вывод контейнеров принадлежал
хостовому ant. node_modules/.pnpm-store уже ant:ant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 12:54:31 +05:00
Alex Ant 9029fa1f9d chore(release): publish
Build bootstrap container / build (push) Successful in 3m17s
Release / release (push) Successful in 25m39s
Release / publish-docs (push) Failing after 9m10s
Release / trigger-coopenomics-docs (push) Failing after 5s
Release / publish-packages (push) Failing after 9m23s
2026-05-25 12:41:40 +05:00
coopops e640b6add6 chore(controller): синхронизировать controller/zeus с регенерацией
Typecheck / desktop (pull_request) Successful in 13m19s
Typecheck / controller (pull_request) Successful in 12m49s
generate-client пишет копию zeus и в components/controller/zeus — обновляю вместе
со schema.gql, чтобы не оставлять дрифт сгенерированных артефактов.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 06:29:06 +00:00
coopops b2c56651f9 chore(controller,sdk): регенерация schema.gql + zeus под комнаты секретаря
Typecheck / desktop (pull_request) Successful in 13m29s
Typecheck / controller (pull_request) Successful in 13m0s
generate-schema + generate-client после добавления резолверов SecretaryRooms и
chatcoopListNonProjectCommunicationRooms. Без этого @coopenomics/sdk:build падал
в CI на отсутствующих в zeus типах (ChatcoopSecretaryRoom / CreateSecretaryRoomInput
/ RemoveSecretaryRoomInput / ChatcoopNonProjectCommunicationRoom) — общий build-шаг
обеих джоб typecheck. Локально sdk build (prebuild tsc --noEmit) зелёный.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 06:28: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
coopops 5c6062420f feat(desktop/chatcoop): страница «Комнаты секретаря» на столе связи
Typecheck / desktop (pull_request) Failing after 7m26s
Typecheck / controller (pull_request) Failing after 6m28s
- entity SecretaryRoom (api/model/store) поверх SDK chatcoopListSecretaryRooms /
  chatcoopCreateSecretaryRoom / chatcoopRemoveSecretaryRoom
- SecretaryRoomsPage: список комнат реестра (тип, наличие секретаря, шифрование),
  создание комнаты (публичная/приватная) и удаление комнат секретаря
- маршрут chatcoop-secretary-rooms, доступ роли chairman+member
- системные/проектные комнаты — read-only (удаление только у kind secretary)

Затипизируется после generate-schema/generate-client (новые SDK-операции).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 05:56:57 +00:00
coopops d1ce3924d5 feat(sdk,blago-cli): SDK-операции комнат секретаря + pull непроектных транскрипций
SDK (рукописные обёртки над Zeus — затипизируются после generate-schema/client):
- queries: ListNonProjectCommunicationRooms, ListSecretaryRooms
- mutations: CreateSecretaryRoom, RemoveSecretaryRoom
- селекторы ChatcoopNonProjectCommunicationRoom / ChatcoopSecretaryRoom

blago-cli:
- pullNonProjectCommunicationArtifacts: переписка + транскрипции + memo комнат
  пайщиков/совета/секретаря в отдельную верхнюю папку rooms/<folder>/{messages,meetings}/
- курсор transcriptionLastEndedExclusiveByRoom (по matrixRoomId)
- системные комнаты — стабильные папки komnata-paishchikov / komnata-soveta;
  комнаты секретаря — slug + хвост хеша matrixRoomId

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 05:49:06 +00:00
coopops d7c2c90290 feat(chatcoop): комнаты секретаря + синхронизация непроектных комнат в blago
Задача 1 (синхронизация в blago):
- roomKind 'secretary' в реестре управляемых Matrix-комнат
- репозиторий: findAll + findNonProjectCommunicationRooms (всё кроме capital_project)
- inter-порт listNonProjectCommunicationRooms + тип InterNonProjectCommunicationRoomRef (kind)
- query chatcoopListNonProjectCommunicationRooms (members/council/secretary) для blago-cli

Задача 2 (комнаты секретаря, backend):
- SecretaryRoomManagementService: создание комнаты (public/private, всегда plaintext,
  force-join только секретаря, создатель — модератор) и удаление (kick секретаря + дерегистрация)
- SecretaryRoomsResolver: chatcoopListSecretaryRooms / chatcoopCreateSecretaryRoom /
  chatcoopRemoveSecretaryRoom; доступ chairman+member
- matrix-api: inviteUser / kickUser
- конфиг SECRETARY_ROOM_MATRIX

Принцип: секретарь присутствует только в backend-созданных комнатах; force-join в
произвольную чужую комнату запрещён (общий Synapse на все кооперативы).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 05:46:00 +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
coopops 5eb03c74c4 ci(release): retry на docker push (3 попытки) — гасит транзиентный DNS
registry-1.docker.io на runner'е изредка не резолвится (DNS-таймаут к
127.0.0.53), из-за чего push валит весь релиз уже после успешной сборки
образов. Обёртка dpush() повторяет push до 3 раз с паузой 10с во всех трёх
push-шагах (контракты, base, сервисы).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 04:18:33 +00:00
Alex Ant 64bb8a0be8 chore(release): publish
Build bootstrap container / build (push) Successful in 3m20s
Release / release (push) Successful in 25m42s
Release / publish-packages (push) Has been skipped
Release / publish-docs (push) Has been skipped
Release / trigger-coopenomics-docs (push) Has been skipped
2026-05-25 08:55:05 +05:00
Alex Ant a7c8f8cd17 Merge branch 'dev' of ssh://git.coopenomics.world:222/C9S/mono into dev 2026-05-25 08:54:48 +05:00
coopops da00407a7c ci(release): доставить рантайм-либы CDT (libz3-4 и др.) при установке из .deb
clang-9 падал "error while loading shared libraries: libz3.so.4". Пакет cdt
объявляет только libcurl4-gnutls-dev, но бинари тулчейна (objdump -p NEEDED)
требуют libz3.so.4/libtinfo.so.6/libxml2.so.2/libz.so.1. В образе
dicoop/blockchain они были из сборки исходников, из .deb не тянутся —
ставим libz3-4 libtinfo6 libxml2 zlib1g явно.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 03:54:39 +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
Alex Ant cfb9398ec6 chore(release): publish
Build bootstrap container / build (push) Successful in 3m25s
Release / release (push) Failing after 2m29s
Release / publish-packages (push) Has been skipped
Release / publish-docs (push) Has been skipped
Release / trigger-coopenomics-docs (push) Has been skipped
2026-05-25 08:44:36 +05:00
coopops 966dcc45f2 ci(release): собирать контракты из cdt .deb прямо в job, без вложенного docker
Шаг компиляции контрактов в release.yaml падал под Gitea act_runner:
build-all.sh монтирует $(pwd):/project в sibling-контейнер, но job сам
исполняется в контейнере → хостовый демон не видит путь, /project пуст,
cmake падает "no CMakeLists.txt". Релиз не собирался зелёным (runs #144, #158).

- build_contracts_cdt.sh: чистый cmake/make без docker (общий источник флагов).
- build-all.sh: тонкая docker-обёртка над ним для локальной сборки.
- release.yaml: вместо pull образа + build-all.sh — установка cdt v4.2.0 .deb
  в окружение job'а + симлинк /cdt/build → /usr/opt/cdt/4.2.0 (сводит хардкод
  toolchain-путь CMakeLists без его правки) + вызов build_contracts_cdt.sh.
  Компиляция в самом job-контейнере, без вложенного docker. sudo-агностично —
  одинаково на GitHub-VM и Gitea-контейнере.
- Убран избыточный typecheck-гейт из release; typecheck.yaml остаётся PR-гейтом.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 03:42:34 +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
Alex Ant 25f10e6782 Merge branch 'dev' of ssh://git.coopenomics.world:222/C9S/mono into dev
Build bootstrap container / build (push) Successful in 3m10s
2026-05-24 23:29:58 +05:00
Alex Ant 3d52d816a4 chore(release): publish
Build bootstrap container / build (push) Successful in 3m22s
Release / typecheck (push) Successful in 23m58s
Release / release (push) Failing after 2m27s
Release / publish-packages (push) Has been skipped
Release / publish-docs (push) Has been skipped
Release / trigger-coopenomics-docs (push) Has been skipped
2026-05-24 23:29:42 +05:00
claude 665c22fc87 fix(boot): унифицировать extra_reboot.sh и clean_reboot.sh с reboot.sh
Build bootstrap container / build (push) Successful in 3m14s
Все три reboot-скрипта теперь идентичны, кроме строки pnpm run boot[:extra|:clean]:
- wipe blockchain-data контейнером (docker run alpine rm) вместо sudo rm —
  работает без sudo на любой ноде независимо от владельца данных (на Pi нет
  passwordless sudo; на проде nodeos пишет данные под root). Единый способ с reboot.sh.
- monoredis в down -v и up -d — без него coopback падает на старте
  (getaddrinfo EAI_AGAIN monoredis → nodemon crash), и провайдер не получает
  org-данные partner1.
- coopback через up -d --force-recreate (перечитывает env), как в reboot.sh.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 23:26:40 +05:00
claude b897627d72 fix(boot): partner1 провижининг под EXTRA_RENT, enrollment в ЦПП Кошелька, идемпотентная activateFeature
Build bootstrap container / build (push) Successful in 3m10s
- booter.ts: installExtraData (регистрация partner1 с auto-approve → триггер аренды
  провайдером) выполняется только под EXTRA_RENT=1. boot:extra используется и для
  других задач (пересев совета/чейна), где аренда VM не нужна.
- infra.ts: partner1 подписывает wallet-соглашение (wallet::signagree, program_id=1)
  при регистрации. Без членства в ЦПП Кошелька provider.performInitialTransfers
  (150 AXON на partner1 перед ACTIVE) падает ассертом eosio.token::is_can_transfer
  «Получатель не является участником целевой потребительской программы кошелька»,
  и инстанс навсегда висит в INSTALL.
- blockchain/index.ts: activateFeature не роняет boot при protocol_feature_exception
  (фича уже активна на не-обнулённом чейне) — ловим и продолжаем.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 23:13:39 +05:00
ant 72cec26c14 Merge pull request 'Убрать механику blocked из L3-кошельков (резерв возврата → отдельный кошелёк w.wal.wpend)' (#31) from feat/remove-l3-blocked into dev
Build bootstrap container / build (push) Successful in 3m21s
Reviewed-on: #31
Reviewed-by: Алексей Муравьев <chairman.voskhod@gmail.com>
2026-05-24 17:32:03 +00:00
coopops 7a163389c6 chore(controller,sdk): полная регенерация schema.gql и zeus (generate-schema + generate-client)
Typecheck / desktop (pull_request) Successful in 13m28s
Typecheck / controller (pull_request) Successful in 13m9s
Штатный синк вместо точечной правки: schema.gql приведён в соответствие с
актуальными DTO контроллера (был протухший — накопленный дрейф полей,
не только blocked). zeus перегенерирован из новой схемы; оба
controller/zeus и sdk/src/zeus идентичны.

Результирующие изменения относительно L3-blocked-ветки:
- удалён blocked из Ledger2Wallet/ProgramWallet (цель PR);
- синхронизированы ранее не закоммиченные дрейфы: is_server_init в SystemInit,
  enum-значение AWAITING_AUTHORIZATION, актуализация описаний/ролей @Field.
- blocked сохранён в ChartOfAccountsItem (legacy chart-of-accounts).

Проверено локально: sdk typecheck и controller typecheck зелёные.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 17:11:32 +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
coopops 5c494db262 fix(sdk): синхронизировать schema.gql и zeus с удалением blocked из L3-кошельков
Typecheck / desktop (pull_request) Successful in 13m29s
Typecheck / controller (pull_request) Successful in 13m8s
CI Typecheck падал на @coopenomics/sdk:build (tsc): селекторы
programWalletSelector/ledger2WalletSelector и зависимые capital-селекторы
больше не содержат blocked, но zeus-типы (MakeAllFieldsRequired) всё ещё
требовали его как обязательное поле.

- schema.gql: убран `blocked: String!` из типов Ledger2Wallet и ProgramWallet
  (точечно, без полной регенерации — она тянет несвязанный дрейф схемы).
- zeus (controller/zeus + sdk/src/zeus): убран blocked из Ledger2Wallet и
  ProgramWallet во всех 4 секциях (ValueTypes/ResolverInputTypes/ModelTypes/
  GraphQLTypes). blocked в ChartOfAccountsItem (legacy chart-of-accounts) и
  статус-комментарии (accepted|blocked) сохранены.
- CoopCard.vue: удалён мёртвый закомментированный блок с wallet.blocked.

Проверено локально: sdk typecheck и controller typecheck зелёные.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 16:43:07 +00:00
coopops 0969505a71 feat(ledger2): убрать механику blocked из L3-кошельков, перевести возврат на отдельный кошелёк-резерв
Typecheck / desktop (pull_request) Failing after 6m36s
Typecheck / controller (pull_request) Failing after 6m30s
Упраздняем субсчёт "заблокировано" на кошельках. Резерв средств под заявку
на возврат паевого теперь выражается переводом на отдельный COOPERATIVE-кошелёк
w.wal.wpend, а не блокировкой внутри w.wal.share. Все движения — только переводы
и сжигание, без BLOCK/UNBLOCK.

Контракты (ledger2/wallet/capital):
- wallets.hpp: новый кошелёк WITHDRAW_PENDING (w.wal.wpend, COOPERATIVE).
- operations.hpp: из WalletOp удалены BLOCK/UNBLOCK/BURN_BLOCKED (числовые
  значения ISSUE/TRANSFER/BURN/NONE сохранены для совместимости истории).
  Переключены 3 операции возврата: o.wal.wthreq -> TRANSFER share->wpend;
  o.wal.wthdec -> TRANSFER wpend->share; o.wal.wthcpl -> BURN wpend.
- walletop.cpp/revert.cpp: удалены кейсы и валидации блокировки.
- migrate(): свёртка blocked->available по ВСЕМ коопам (выполняется автоматически
  при деплое; сигнатура без аргументов). Поле blocked в структурах таблиц
  оставлено deprecated — физическое удаление = смена layout таблицы на живых
  коопах, отдельный cleanup-деплой.
- capital balances/importcontr: больше не читают blocked.
- p.wal.wthdrw.standard.yaml: стандарт приведён к модели "резерв на кошельке".

cooptypes/SDK:
- operations.ts: WalletOp без BLOCK/UNBLOCK/BURN_BLOCKED; 3 операции возврата
  переключены; wallets.generated.ts регенерён (добавлен w.wal.wpend).
- selectors: убран blocked из programWalletSelector/ledger2WalletSelector.

Backend (controller): убрано поле blocked из GraphQL DTO ProgramWallet и
Ledger2Wallet. TypeORM-колонка/внутренние интерфейсы оставлены deprecated
(без DB-миграции).

Frontend (desktop): убраны все поля "Заблокировано" — WalletProgramWidget,
WalletWidget, ParticipantWalletsPage, CoopWalletsPage, CapitalWalletsCardsWidget,
ContributorsListWidget, optimistic blocked_delta.

Границы (не в этом PR): legacy soviet::progwallets blockbal/unblockbal и
donor-marketplace; legacy ledger v1 chart-of-accounts "Заблокированные средства";
полное удаление поля blocked из C++-структур таблиц.

Требует CI-шага: regen GraphQL schema + zeus client (generate-schema/-client)
для согласования schema.gql и zeus-типов с удалённым полем blocked.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 16:07:11 +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
Alex Ant 970e107261 DEV setup on macos 2026-05-24 12:09:31 +05:00
claude a0e169e9b4 fix(blago-cli): pull не плодит ложные merge-конфликты при bump updated_at
Корневая причина: при pull конфликт объявлялся по условию
`dirty && remoteUpdatedAt !== prev.remote_updated_at`, где `dirty`
определяется сравнением raw-байтового sha файла с content_etag_local.
Серверные метки времени (updated_at/created_at) входят и во frontmatter
файла, и в etag. Когда сервер бьёт _updated_at родителя при дочерней
мутации (создание/удаление issue/story), etag в индексе расходится с
файлом ровно на строку updated_at — файл считается «грязным», а при
изменившемся remote_updated_at pull пишет маркеры слияния на весь файл,
хотя содержательно текст идентичен. Подтверждено: подстановка
remote_updated_at в файл воспроизводит etag байт-в-байт (58 записей на
проде voskhod).

Фикс: в syncEntityFile перед записью маркеров сравниваем локальный и
серверный тексты в каноне без updated_at/created_at. Если совпадают —
это не конфликт: принимаем серверную версию и лечим etag, без маркеров.
Реальная правка тела/заголовка по-прежнему даёт маркеры.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 12:00:43 +05: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 a93022343c Merge pull request 'test(ci): dry-run typecheck workflow on PR — duration measurement' (#30) from ci-trial-typecheck-2026-05-23 into dev
Reviewed-on: #30
2026-05-23 13:22:54 +00:00
claude 81fca02e39 Merge remote-tracking branch 'origin/dev' into ci-trial-typecheck-2026-05-23
Typecheck / desktop (pull_request) Successful in 13m8s
Typecheck / controller (pull_request) Successful in 12m54s
2026-05-23 16:10:24 +05:00
claude d1977e3da2 test(ci): retrigger after workflow fix (lerna run build)
Typecheck / desktop (pull_request) Successful in 12m22s
Typecheck / controller (pull_request) Failing after 6m31s
2026-05-23 15:49:12 +05:00
claude 69d0e223ed ci(typecheck): полный lerna run build вместо --scope cooptypes+factory(+sdk)
Trial run #148 (PR #30) упал в job controller на 37 TS2307 «Cannot find
module» для @coopenomics/{sdk,inter,notifications}. Локально маскируется
тем, что controller стартует через ts-node, который резолвит .ts напрямую
через tsconfig-paths/pnpm-симлинки и не зависит от dist/. tsc же ищет
types из package.json целевого пакета — а оно показывает на dist/index.d.ts
из unbuild.

Замена локального scoped-build на полный `pnpm lerna run build` (как в
корневом Dockerfile builder-стадии) собирает граф целиком и устойчиво
к добавлению новых workspace-пакетов. Добавляет ~2-3 мин ко времени job'а,
end-to-end оценочно ~21 мин (был 18 на сломанной версии).
2026-05-23 15:48:50 +05: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
claude b1b0776d68 test(ci): dry-run typecheck workflow on PR — measure end-to-end duration
Typecheck / desktop (pull_request) Successful in 12m35s
Typecheck / controller (pull_request) Failing after 6m31s
2026-05-23 10:57:59 +05: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
claude e4a1a33a53 ci: typecheck (vue-tsc desktop + tsc controller) — PR-гейт на dev и needs у release
build образов в release.yaml не ловит TS-ошибки: quasar build идёт через
esbuild с выключенным vueTsc в vite-plugin-checker (см. quasar.config.cjs),
а у controller'а build-скрипта вообще нет — `lerna run build` его молча
пропускает, в проде ts-node стартует и валится на типах только в рантайме.
Поэтому битый тэг мог уехать в DockerHub (кейс PR #392 / rename 1080→1020
в cooptypes — TS2551 в distribution-management проскочил именно так).

Новый reusable workflow .github/workflows/typecheck.yaml:
 - desktop: build cooptypes/factory/sdk → quasar prepare → vue-tsc --noEmit --skipLibCheck
 - controller: build cooptypes/factory → pnpm typecheck (tsc --noEmit)
 - триггер pull_request на dev + workflow_call

В release.yaml добавлен job typecheck (uses: ./.github/workflows/typecheck.yaml),
release-job получил needs: typecheck. Downstream publish-packages /
publish-docs / trigger-coopenomics-docs через release автоматически в цепочке.

Push в dev/testnet/main НЕ триггерит — PR-гейта достаточно, тэги покрыты
через workflow_call. Если vue-tsc упрётся в OOM/таймаут на ubuntu-latest,
fallback на `pnpm --filter @coopenomics/desktop run typecheck` (без SFC).
2026-05-23 10:48:53 +05: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
ant 9cbed2edf5 Merge pull request 'fix(controller): откатить transpileOnly — ломает TypeORM (post-PR#27 hotfix)' (#29) from fix/controller-revert-transpileonly into dev
Reviewed-on: #29
2026-05-23 05:29:52 +00:00
claude f27eb3a102 fix(controller): откатить "ts-node":{"transpileOnly":true} — ломает TypeORM
PR #27 включил transpileOnly через "ts-node" блок в tsconfig.json
для ускорения cold-start dev. Это сломало TypeORM на старте:

  DataTypeNotSupportedError: Data type "Object" in "TokenEntity.type"
  is not supported by "postgres" database.

Корень: transpileOnly режим ts-node использует ts.transpileModule,
один файл за раз без TypeChecker. Cross-file type aliases — типа
`import type { TokenType } from '~/types/token.types'` где
`TokenType = (typeof tokenTypes)[keyof typeof tokenTypes]` — без
type-checker'а **не резолвятся**. design:type metadata
для `@Column() type!: TokenType` записывается как `Object` вместо
`String`. TypeORM пытается создать колонку Object → unsupported.

Проблема структурная: множество TypeORM Entity в controller'е
используют `import type {SomeAlias}` + `@Column() field: SomeAlias`,
полагаясь на полный type-resolve в metadata-emit. Без явного `type:`
в каждом @Column переход на transpileOnly / SWC невозможен.

Возвращаемся к полному ts-node (cold-start 60+ сек, как было до
PR #27). @swc/core / @swc-node/register остаются в devDeps —
безвредны, не используются. Smoke-suite tests/unit/_swc-readiness/
остаётся как regression-net для будущих попыток (тестирует
emitDecoratorMetadata инвариант).

Будущий путь к ускорению (отдельная задача):
1. Пройтись по всем TypeORM Entity, добавить explicit type: в
   каждый @Column — снимет зависимость от cross-file metadata.
2. ИЛИ перевести TokenType-подобные type-aliases в enum (value-
   import) — runtime binding позволит metadata эмититься корректно.
3. Тогда transpileOnly / SWC заработают без regressions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 10:27:49 +05:00
ant e3f4071b16 Merge pull request 'perf(controller): cold-start dev в ~25x (transpileOnly) + Node 22 + SWC-ready' (#27) from feat/controller-swc-builder into dev
Reviewed-on: #27
2026-05-23 05:19:33 +00:00
ant 3c87043d12 Merge pull request 'chore(infra): mongo standalone локально, без --replSet rs0' (#28) from chore/mongo-standalone-local into dev
Build bootstrap container / build (push) Successful in 2m25s
Reviewed-on: #28
Reviewed-by: Алексей Муравьев <chairman.voskhod@gmail.com>
2026-05-23 05:19:12 +00:00
claude 1a6f326aa4 chore(infra): mongo standalone локально, без --replSet rs0
Локально гоняем mongo как standalone. Раньше был --replSet rs0 +
rs.initiate в entrypoint, но проект не использует ни transactions
(нет startSession), ни change streams (нет .watch()), ни
readConcern:'majority' — replica-set серверно избыточен. Клиенты
(mongoose в controller, MongoClient в parser, notifications) уже
подключаются по URL без replicaSet=/directConnection= параметров,
для них переход прозрачен.

Бонусы:
- pnpm run reboot больше не висит на «MongoDB еще не готов
  (нет PRIMARY)»: на arm64 sleep 5 в entrypoint не успевал поднять
  mongod до того, как rs.initiate пытался выполниться, и rs.status()
  внутри try/catch ловил неправильную ошибку — replica config не
  применялся, oplog.rs не создавался, PRIMARY никогда не наступал
  (инцидент 2026-05-23).
- Старт mongo на 5-10 сек быстрее.
- Меньше состояния в volume — нет oplog/replica-config.

reboot.sh: ждать db.adminCommand({ping:1}) вместо db.hello().isWritablePrimary.

Прод-конфигурация (k8s/swarm) этого файла не использует — там
своя replica-схема для HA, не затронута.

Если в будущем потребуются transactions — вернуть --replSet rs0
+ rs.initiate в entrypoint обратно. Триггер: появление в коде
session = await mongoose.startSession() или .watch().

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 10:16:25 +05:00
coopops 712080ff8f Merge branch 'dev' into worktree-design-wave1
Подтянуть свежие изменения dev в дизайн-ветку перед продолжением миграции стола пайщика.
2026-05-23 05:09:17 +00:00
ant 5fc2d90a85 Merge pull request 'ci: объединить релизные workflow и убрать Telegram-уведомления' (#26) from chore/ci-unify-release-workflows into dev
Build bootstrap container / build (push) Successful in 3m26s
Reviewed-on: #26
2026-05-23 04:55:20 +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
Alex Ant bc3a315790 ci: объединить релизные workflow в один, убрать Telegram-уведомления
Слияние в release.yaml через jobs+needs (последовательно):
- release (контракты+контейнеры+webhook, как раньше)
- publish-packages (если не -alpha) — был publish-packages.yaml
- publish-docs (не -alpha + main) — был publish-docs.yaml
- trigger-coopenomics-docs (не -alpha + main) — был build-contracts-docs.yaml

Удалено:
- build-contracts.yaml (ручной workflow_dispatch, сознательно вынесен из
  релиз-пути после инцидента 2026-05-13, пользователем подтверждено удаление)
- publish-packages.yaml, publish-docs.yaml, build-contracts-docs.yaml
  (содержимое перенесено в release.yaml как зависимые jobs)

Гейты унифицированы на !contains(github.ref, '-alpha') во всех publish-*
и trigger-* (раньше publish-docs/build-contracts-docs резали ещё
-beta/-rc/-test). IS_PROD в release-job остаётся на (alpha|beta|rc|test)
намеренно — webhook продакшна и тэг :latest должны быть строже.

Telegram-уведомления удалены из release.yaml и build-bootstrap.yaml.
Полагаемся на дефолтные email-уведомления Gitea-инстанса (mailer ENABLED=true).
Секреты TELEGRAM_BOT_TOKEN/TELEGRAM_CHAT_ID после merge можно удалить из репо.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 09:39:53 +05: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
Alex Ant 4a244c05e4 fix(controller): откат "ts-node":{"swc":true} → "transpileOnly":true
Полный SWC через ts-node наткнулся на TDZ-ловушку при загрузке
src/extensions/1ccoop/oneccoop-extension.module.ts:

  ReferenceError: Cannot access 'OneCoopPlugin' before initialization
    at Object.get OneCoopPlugin
    at oneccoop-secret-key.guard.ts:83 (line in transpiled SWC output)

Корень: circular import между oneccoop-extension.module.ts и
guard.ts (guard импортирует OneCoopPlugin для @Inject(forwardRef(...))
+ type annotation; module импортирует guard для providers). SWC
эмитит `Reflect.metadata("design:paramtypes", [OneCoopPlugin])`
при class-declaration `@Injectable()` — это runtime-обращение к
OneCoopPlugin до того, как oneccoop-extension.module.ts закончил
инициализацию. tsc/CommonJS-loader прощает (hoisted exports +
Object.defineProperty(get) для late-binding), SWC по строгой
ES-семантике — нет.

Масштаб системный, не локальный: 32 файла в src/ используют
forwardRef(() => Class), много из них в pattern @Inject + type
annotation в конструкторе. Под SWC каждый такой файл —
потенциальный TDZ. Чинить по одному (import type + lazy require
в forwardRef) — десятки правок с риском уронить тип-safety.

Решение: остаёмся на ts-node --transpileOnly. Тот же 25x cold-start
(~1 сек на пробе), tsc-семантика прощает циклы, никаких code-changes.
@swc/core и @swc-node/register оставляем в devDependencies —
готовы к будущему полному SWC, когда отдельной задачей разорвём
forwardRef-циклы (заменить class-on-class @Inject через string/Symbol
токены, либо вынести типы в отдельные файлы без cycle).

SWC-options через .swcrc как module.lazy:true проверены — не лечат,
проблема в decoratorMetadata, не в импортах.

Smoke-suite tests/unit/_swc-readiness/ — 4/4 PASS на transpileOnly,
emitDecoratorMetadata через barrel-import сохраняется.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 09:31:25 +05: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
Alex Ant 79e333db44 perf(controller): полный SWC через @swc-node/register + Node 22
Финал миграции: вместо ts-node --transpileOnly теперь полный SWC
(Rust-транспайлер) через ts-node "swc" mode. Установлены
@swc-node/register@^1.11.1 и @swc/core@^1.15.33 в controller.
tsconfig "ts-node" → { "swc": true, "files": true } (вместо
transpileOnly). Все 14 ts-node-вызовов очищены от --transpileOnly
(избыточно при swc).

Замер:
- ts-node + tsc (baseline): 26+ сек cold-start на простом скрипте
- ts-node --transpileOnly (Node 20): ~1.0 сек
- ts-node + swc (Node 22):           ~1.0 сек (та же скорость, но
  без потери типов — SWC всё ещё транспайлит, просто на Rust)

На полном controller-проекте dev-cold-start раньше был 60+ сек,
теперь — secunda-уровень (точно замерить можно только перезапустив
coopback в контейнере).

Node 22:
- nvm alias default 22 (Node 22.22.3 — требование Quasar 2.5.2,
  заодно убрал quasar-prepare warning при pnpm install).
- libxmljs2 native binary теперь совместим с runtime (на Node 20
  падал tests/unit/reports на NODE_MODULE_VERSION mismatch).

.npmrc в корне (`store-dir=./.pnpm-store`) — фиксирует store в
монорепе. Корень проблемы был: docker-coopback запущен от root и
писал в `/app/.pnpm-store` (= host's /home/ant/mono/.pnpm-store),
host-pnpm дефолтно искал в ~/.local/share/pnpm/store — отсюда
ERR_PNPM_UNEXPECTED_STORE. Relative-path в .npmrc устраняет
mismatch навсегда: и хост, и контейнер видят store через тот же
relative-путь от repo root.

Smoke-suite tests/unit/_swc-readiness/ — 4/4 PASS на SWC, что
формально подтверждает: emitDecoratorMetadata через barrel-import
сохраняется (Nest DI, class-validator, @ValidateNested + @Type все
получают правильные design:type/paramtypes из SWC).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 09:20:05 +05: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
Alex Ant 40be8fb60c test(cooptypes,controller): починить два протухших теста
cooptypes/test/wallets-registry.snapshot — `human_name` для
w.wal.wthdrw в src/ledger2/wallets.generated.ts получил суффикс
"(deprecated, не используется в новых операциях)" (видимо, при
свежей регенерации из C++), но snapshot не обновили. Updating.

controller/tests/unit/process-registry — apply-anchor data поле
переименовано action_code → operation_code в самом сервисе
(src/domain/process-registry/services/process-registry.service.ts),
вместе с константой ACTION_CODE_TO_PROCESS_TYPE → OPERATION_CODE_TO_PROCESS_TYPE.
В тесте оставались старые имена → 1 из 6 кейсов падал на mismatch
regex'а сообщения ошибки. Переименовываю action_code → operation_code
во всём файле и обновляю regex (f2).

Что НЕ починено в этом коммите (pre-existing, не моё):
- 5 case'ов (a..e) в том же файле всё ещё падают: они используют
  operation_codes 'sov.axncnv' / 'cap.act2shr' / 'cap.act2ln' /
  'wall.depcpl' / 'reg.entrfee' / 'reg.minshare' / 'mig.opncash',
  которых нет в Ledger2.LEDGER2_OPERATION_REGISTRY (cooptypes).
  Test fixtures устарели относительно текущей канонической
  ledger2-онтологии (canonical имена — o.cap.crtnma, o.cap.dbtwrf,
  o.cap.lend, o.cap.repay, o.reg.payent, o.reg.putmin и т.п.).
  Этот тест-suite надо переписать под актуальный operation registry
  отдельным PR (требует знания canonical naming kanon).
  Падает и на dev (без моих правок) — это pre-existing tech debt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 01:47:33 +05:00
Alex Ant 9e022443ca perf(controller): ts-node --transpileOnly во всех dev-скриптах
Cold-start controller dev (`pnpm dev`) сейчас 60+ секунд из-за полной
TypeScript-компиляции в ts-node на каждый рестарт nodemon. Решение —
добавить --transpileOnly во все 14 ts-node-скриптов и зафиксировать
это в tsconfig "ts-node" блоке. Транспайл-онли пропускает type-check,
оставляет только TS→JS transform (с сохранением emitDecoratorMetadata).

Замер на standalone-пробе с barrel-импортом + Reflect.getMetadata
(tests/unit/_swc-readiness/transpile-only-probe.ts):
- baseline (полный ts-node): 26+ сек (показал тест ts-jest)
- ts-node --transpileOnly:    ~1 сек

Ускорение ~25x на cold-start. dev / start / migration:* / init:* /
analyze:modules / generate-schema — все на --transpileOnly.

Типы продолжаем проверять отдельно: `pnpm typecheck` (tsc --noEmit
по этому же tsconfig.json) — это и так стоит делать перед коммитом
по правилам проекта.

Safety-net — smoke-suite tests/unit/_swc-readiness/swc-readiness.test.ts:
ловит главный риск SWC/transpile-only регрессий — потерю metadata на
barrel-импортах (поведение тех же кодпутей, что и в реальных Nest-
сервисах с injection через @Inject и class-validator DTO).
- ServiceB DI через barrel: design:paramtypes = [ServiceA] (не Object)
- @ValidateNested + @Type через barrel: nested design:type = NestedPayload
- Nest Test.createTestingModule резолвит сервисы через barrel-import
- class-validator/class-transformer не теряют type info

Если позже понадобится **полный SWC** (Rust-транспайлер, ещё в разы
быстрее) — добавить @swc/core + @swc-node/register в devDependencies
и переключить ts-node на swc-режим:
1. `pnpm add -D @swc/core @swc-node/register -F @coopenomics/controller`
2. В tsconfig.json "ts-node" → добавить `"swc": true` либо
   использовать `node --import @swc-node/register/esm-register` в скриптах.
Этот шаг не делается сейчас, так как install требует sudo на shared
pnpm-store (root-owned /home/ant/mono/.pnpm-store/v10) — пользователь
утром может сделать руками.

tsc --noEmit зелёный за 32 сек, smoke-suite + 1 case onboarding-ttl + 1 case
onboarding-steps-registry — все зелёные на baseline и после изменения
tsconfig.json.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 01:36:36 +05:00
Alex Ant 5fd5db0604 perf(desktop): отключить vueTsc в vite-plugin-checker dev
В dev-режиме vue-tsc через vite-plugin-checker удерживал постоянную
100% загрузку CPU и 2–4 GB RAM на больших Vue 3 + Quasar проектах
(Milkdown / BPMN-js / VueFlow / Mermaid / OpenLayers). Каждое
сохранение запускало полный re-typecheck в фоне, что в долгих
сессиях выглядело как утечка памяти и вешало машину.

Типы продолжаем гонять отдельно: `pnpm typecheck` (tsc --noEmit
--skipLibCheck) и через Volar в IDE. eslint в checker'е остаётся —
он лёгкий и полезен для overlay'я.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 01:20:25 +05:00
Alex Ant adc5926fa7 Merge branch 'main' into dev
Build bootstrap container / build (push) Failing after 4m59s
2026-05-23 00:55:56 +05:00
Alex Ant 655a73e828 chore(release): publish
Build bootstrap container / build (push) Failing after 7m45s
Trigger Contracts Docs Deploy / gate (push) Successful in 2m3s
Publish Docs / gate (push) Successful in 2m4s
Publish Packages / build-and-publish (push) Failing after 9m24s
Release / release (push) Failing after 4m33s
Trigger Contracts Docs Deploy / trigger-coopenomics (push) Failing after 3s
Publish Docs / build-and-publish-docs (push) Failing after 1h29m50s
2026-05-23 00:55:52 +05:00
Alex Ant 02418af616 Merge branch 'main' into dev
Build bootstrap container / build (push) Failing after 4m58s
# Conflicts:
#	components/blago-cli/package.json
#	components/boot/package.json
#	components/cleos/package.json
#	components/contracts/package.json
#	components/controller/package-lock.json
#	components/controller/package.json
#	components/cooptypes/package.json
#	components/desktop/package.json
#	components/docs-harness/package-lock.json
#	components/docs-harness/package.json
#	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/setup/package.json
#	lerna.json
2026-05-23 00:54:46 +05:00
Alex Ant 3d4f75bff0 chore(release): publish
Build bootstrap container / build (push) Failing after 7m29s
Trigger Contracts Docs Deploy / gate (push) Successful in 2m4s
Publish Docs / gate (push) Successful in 2m4s
Publish Packages / build-and-publish (push) Failing after 6m39s
Release / release (push) Failing after 4m27s
Trigger Contracts Docs Deploy / trigger-coopenomics (push) Failing after 2s
Publish Docs / build-and-publish-docs (push) Failing after 8m22s
2026-05-23 00:52:06 +05:00
Alex Ant 7b9828ae87 chore(release): publish
Build bootstrap container / build (push) Failing after 7m34s
Trigger Contracts Docs Deploy / gate (push) Successful in 2m17s
Publish Docs / gate (push) Successful in 2m5s
Publish Packages / build-and-publish (push) Failing after 9m49s
Release / release (push) Failing after 4m29s
Trigger Contracts Docs Deploy / trigger-coopenomics (push) Failing after 5s
Publish Docs / build-and-publish-docs (push) Failing after 8m30s
2026-05-23 00:50:01 +05:00
Alex Ant fb7ea468fa chore(release): publish
Build bootstrap container / build (push) Failing after 2m50s
2026-05-23 00:39:08 +05:00
Alex Ant 745438ded3 chore(release): publish 2026-05-23 00:39:08 +05:00
Alex Ant 82d58c29f6 chore(release): publish 2026-05-23 00:39:08 +05:00
Alex Ant aa5c180be0 chore(release): publish 2026-05-23 00:39:08 +05:00
Alex Ant e745c73cb5 chore(release): publish 2026-05-23 00:39:08 +05:00
Alex Ant f531182541 chore(release): publish 2026-05-23 00:39:08 +05:00
Alex Ant 2eadc79ee3 chore(release): publish 2026-05-23 00:39:08 +05:00
Alex Ant 52d423ef8a chore(release): publish 2026-05-23 00:39:08 +05:00
Alex Ant 2b529d5a15 chore(release): publish
Trigger Contracts Docs Deploy / gate (push) Successful in 2m12s
Build bootstrap container / build (push) Failing after 7m29s
Publish Docs / gate (push) Successful in 2m29s
Publish Packages / build-and-publish (push) Has been skipped
Release / release (push) Failing after 4m45s
Trigger Contracts Docs Deploy / trigger-coopenomics (push) Has been skipped
Publish Docs / build-and-publish-docs (push) Has been skipped
2026-05-23 00:38:30 +05:00
Alex Ant 5a17bcdfaf Merge branch 'dev' into testnet 2026-05-23 00:38:14 +05:00
Alex Ant fd59678944 local update
Build bootstrap container / build (push) Failing after 6m54s
2026-05-23 00:37:36 +05: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
Alex Ant 23605d2b81 chore(release): publish
Build bootstrap container / build (push) Failing after 7m29s
Trigger Contracts Docs Deploy / gate (push) Successful in 2m5s
Publish Docs / gate (push) Successful in 2m6s
Publish Packages / build-and-publish (push) Has been skipped
Release / release (push) Failing after 4m51s
Trigger Contracts Docs Deploy / trigger-coopenomics (push) Has been skipped
Publish Docs / build-and-publish-docs (push) Has been skipped
2026-05-22 17:34:24 +05:00
Alex Ant efce39a4c9 Merge branch 'dev' of ssh://git.coopenomics.world:222/C9S/mono into dev 2026-05-22 17:29:29 +05:00
coopops ba335432a2 docs(CLAUDE.md): разрешить прямые коммиты в dev mono-ai-1 для мелких фиксов 2026-05-22 12:29:01 +00:00
coopops 1b9cb27030 fix(controller): AWAITING_AUTHORIZATION в statusLabels PaymentDomainEntity
getStatusLabel() требует Record<PaymentStatusEnum, string>, AWAITING_AUTHORIZATION
был добавлен в enum, но забыт в локальном маппинге → ts-node краш в dev.
2026-05-22 12:28:58 +00:00
Alex Ant e000ff9c8f Merge branch 'dev' of ssh://git.coopenomics.world:222/C9S/mono into dev 2026-05-22 16:44:06 +05:00
coopops fdd09d5501 fix(boot): не тянуть vitest в production-цепочке init/
Build bootstrap container / build (push) Failing after 7m23s
`init/cooperative.ts` и `init/participant.ts` импортировали
`signProgramAgreement` из `tests/wallet/`, который на верхнем уровне делал
`import { expect } from 'vitest'`. При запуске `esno src/index.ts boot`
vitest падал с «Vitest failed to access its internal state», потому что
исполнялся вне vitest-воркера.

- Перенёс реализацию в `init/sign-program-agreement.ts`,
  `expect(...).toBeDefined()` заменил на обычные `throw new Error(...)`.
- `getCoopProgramWallet` инлайнил (через `walletUtils` тащился второй
  module-level `import expect from 'vitest'`).
- `tests/wallet/signProgramAgreement.ts` теперь реэкспортирует из init/,
  чтобы существующие тесты продолжали работать.
- Заодно убрал дохлый `import { describe, expect, it } from 'vitest'` из
  `init/participant.ts` (символы в файле не использовались) + неиспользуемые
  axios/Registry/sendPost.../sleep/GOVERN_SYMBOL/SYMBOL.
2026-05-22 11:43:19 +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
Alex Ant 0504c46fbd chore(release): publish
Build bootstrap container / build (push) Failing after 4m15s
Trigger Contracts Docs Deploy / gate (push) Successful in 2m4s
Publish Docs / gate (push) Successful in 2m3s
Publish Packages / build-and-publish (push) Failing after 8m4s
Release / release (push) Failing after 4m31s
Trigger Contracts Docs Deploy / trigger-coopenomics (push) Failing after 2s
Publish Docs / build-and-publish-docs (push) Failing after 8m16s
2026-05-22 14:44:59 +05:00
Alex Ant 6270ae14bf chore(release): publish
Build bootstrap container / build (push) Failing after 2m27s
Trigger Contracts Docs Deploy / gate (push) Successful in 2m15s
Publish Packages / build-and-publish (push) Has been skipped
Publish Docs / gate (push) Successful in 7m15s
Release / release (push) Failing after 9m33s
Trigger Contracts Docs Deploy / trigger-coopenomics (push) Has been skipped
Publish Docs / build-and-publish-docs (push) Has been skipped
2026-05-22 14:43:43 +05:00
Alex Ant 2a296ef2ef Merge branch 'dev' of ssh://git.coopenomics.world:222/C9S/mono into dev 2026-05-22 14:42:19 +05: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 a8f2b31494 Merge pull request 'fix(boot): test-helper'ы подписания программных соглашений — wallet::signagree вместо soviet::sndagreement' (#19) from fix/boot-tests-wallet-signagree-v2 into dev
Build bootstrap container / build (push) Failing after 2m25s
Reviewed-on: #19
Reviewed-by: Алексей Муравьев <chairman.voskhod@gmail.com>
2026-05-22 07:07:29 +00:00
ant 52a5871330 Merge pull request 'fix(ledger2,wallet): BURN_BLOCKED для возврата паевого + удаление approvewthd' (#20) from feat/ledger2-burn-blocked-withdraw into dev
Reviewed-on: #20
Reviewed-by: Алексей Муравьев <chairman.voskhod@gmail.com>
2026-05-22 07:06:39 +00:00
ant 1f10a3c092 Merge pull request 'fix(gateway): исходящий платёж скрыт от кассира до решения совета' (#21) from fix/wallet-withdraw-gateway-timing into dev
Reviewed-on: #21
Reviewed-by: Алексей Муравьев <chairman.voskhod@gmail.com>
2026-05-22 07:03:52 +00:00
ant 9579ae0612 Merge pull request 'fix(mono-base): install ca-certificates in runtime stage' (#22) from fix/mono-base-ca-certificates into dev
Reviewed-on: #22
2026-05-22 07:02:58 +00:00
ant a436527248 Merge pull request 'feat(chatcoop): транскрипция — снять лимит memo 4000, скилл blago transcription memo' (#23) from feat/chatcoop-transcription-memo into dev
Reviewed-on: #23
Reviewed-by: Алексей Муравьев <chairman.voskhod@gmail.com>
2026-05-22 06:59:29 +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 94e44df124 fix(chatcoop): TranscriptionMemoEditor — карточка с рамкой, заголовок секции, кнопка снизу
- WrappedEditor (q-field+Editor) на странице транскрипции выглядел сломанным: floating-label «Заметка о звонке» падал поверх первой строки memo, кнопка «Сохранить» торчала сбоку без выравнивания, рамки редактора не было.
- Перешёл на голый Editor (Milkdown через src/shared/ui/Editor) в собственной карточке: border-radius:10px, рамка по тону --tr-border, padded=true. Кнопка «Сохранить» — в нижней панели карточки (border-top, flex-end). Hint «доступно председателю и членам совета» — отдельной строкой под карточкой.
- TranscriptionDetailPage: добавлен h2 «Заметка о звонке» как у секции «Текст», memo больше не висит без подписи.
2026-05-22 06:33:37 +00:00
coopops dfed3774b7 feat(chatcoop): TranscriptionsPage — полноширинная q-table как в CalendarPage
Список транскрипций был зажат max-width: 720px в центре страницы и собран из самописных rows. Заменил на тот же паттерн, что использует CalendarPage / CalendarEventsTable — q-page(padding) + q-table с колонками: Звонок (название + превью memo), Начало, Длительность, Участники, Статус. Клик по строке открывает детальную страницу транскрипции.
2026-05-22 06:25:23 +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 3950aa75db feat(chatcoop): memo через WrappedEditor + расширить TranscriptionDetailPage до 1040px
- TranscriptionMemoEditor: q-input type=textarea заменён на WrappedEditor (Milkdown через src/shared/ui/WrappedEditor) — теперь memo отображается с markdown-рендерингом (заголовки/списки/жирный/ссылки), а не как plain текст. Кнопка «Сохранить» вынесена в отдельную панель под редактором (фокус-кольцо и поле растут естественно). Минимальная высота 180px, без скролла — растёт по содержимому.
- TranscriptionDetailPage: max-width 720 → 1040px, padding 16/20 → 16/32 — страница перестала быть сильно зауженной, гармонично для desktop.
- скилл manage-transcription-memo: добавлен раздел «Формат содержимого .memo.md» — первая строка строго одно предложение ≤150 символов без markdown (о чём был звонок), затем пустая строка и сжатая суть; этикет/повторы/разогрев убираются.
2026-05-22 06:05:56 +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
coopops db25c1dfa9 fix(blago-cli): blago pull бэкфилит .memo.md для всех COMPLETED транскрипций (не только новых)
Курсор transcriptionLastEndedExclusiveByProject фильтровал ВСЕ артефакты транскрипции — поэтому
для уже скачанных meeting.md sibling-файлы .memo.md не появлялись. Разделил циклы:

  1. meeting.md — только endedAt > lowerBoundExclusive (тяжёлый GetTranscription с сегментами).
  2. .memo.md  — для всех COMPLETED транскрипций каждый pull (поле memo приходит уже в лёгком
     GetTranscriptions, повторных запросов не делаем).

Проверено: blago pull → 4/4 sibling .memo.md появились пустыми в проекте 33-platforma-otchetov-dlya-fnsfss; затем blago transcription memo опубликовал 476 символов в крайней транскрипции 7116fb31-3b8c-4a63-9c0c-11da26aba075, повторный pull сохранил содержимое без конфликта.
2026-05-22 06:00:58 +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 e38762deaa feat(blago-cli): blago pull всегда создаёт sibling .memo.md, baseline/merge без warnings
- pull-communication: для каждой COMPLETED-транскрипции вызывает syncTranscriptionMemoFile, который ВСЕГДА обеспечивает файл meetings/<stem>.memo.md (пустой, если на сервере memo пуст). Файл сразу индексируется (entity_type=call_transcription_memo) — редактирование→blago transcription memo идёт без шагов «создать файл».
- Конфликты при отсутствии prev-индекса разруливаются явно: server пуст → принять локальный draft как baseline; оба непустые и разные → git-style merge-markers; совпало → проиндексировать как есть.
- update-transcription-memo: текст ошибки про отсутствующий sibling указывает на blago pull (создаст sibling сам).
- скилл manage-transcription-memo: процедура переписана под «sibling уже есть, просто открой и редактируй».
2026-05-22 05:52:53 +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
coopops d2c1d425fc feat(blago-cli): blago pull тянет memo транскрипций в meetings/<stem>.memo.md
- новый entity_type call_transcription_memo в index-store (тип файла .memo.md, parsing/sync через стандартный syncEntityFile)
- pull-communication: для каждой COMPLETED-транскрипции с непустым tr.memo пишет sibling-файл meetings/<stem>.memo.md (hash = "<uuid>:memo"); при наличии локального draft, не индексированного в .blago/index.json, серверный memo не записывается — выводится warning
- update-transcription-memo: после успешной мутации сохраняет sibling и заносит запись в индекс (etag локального файла), чтобы следующий pull шёл штатно через syncEntityFile вместо warning'а
- скилл manage-transcription-memo: добавлены замечания про pull-синк, git-style маркеры конфликта и поведение неиндексированных черновиков
2026-05-22 05:44:46 +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
coopops ef993baa25 feat(blago-cli): команда transcription memo + скилл manage-transcription-memo
- `blago transcription memo <pathOrId> [--file <path>] [--text <inline>]` — публикация краткого содержания транскрипции через chatcoopUpdateTranscriptionMemo
- pathOrId: путь к meetings/<stem>.md (id из .blago/index.json, entity_type=call_transcription) или UUID
- default-источник memo — sibling-файл meetings/<stem>.memo.md рядом с meeting (pull-only, в push не уходит)
- скилл ai/commands/manage-transcription-memo.md описывает workflow «прочитать meeting → собрать .memo.md → согласовать → опубликовать»
2026-05-22 05:40:30 +00:00
coopops adf36b28cb feat(chatcoop): убрать лимит 4000 на memo транскрипции, оставить редактирование только chairman/member
- backend: убран @MaxLength(4000) с UpdateCallTranscriptionMemoInputDTO.memo и описание лимита из @Field
- backend: chatcoopUpdateTranscriptionMemo больше не доступен роли user, только chairman/member (read-методы — без изменений)
- desktop: TranscriptionMemoEditor больше не задаёт maxlength и counter в q-input
2026-05-22 05:40:20 +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
coopops 223dd00f91 fix(mono-base): install ca-certificates in runtime stage
@livekit/rtc-node (Rust + rustls + rustls-native-certs) reads root CAs
only from the system trust store. node:22-slim ships without
ca-certificates, so without /etc/ssl/certs/ca-certificates.crt every TLS
handshake from native bindings fails with
"invalid peer certificate: UnknownIssuer" — even for a valid LE chain.
Node.js itself is unaffected (own embedded CA bundle).

Incident 2026-05-21: secretary in dicoop/coopback:v2026.5.21-2 could not
connect to wss://chatcooprtc.coopenomics.world (controller logs full of
SecretaryAgentService UnknownIssuer; nginx on api-prod never saw the
handshake — broken before HTTP upgrade). Hot-fix on
voskhod-coopback-blue (docker cp ca-certificates.crt + restart)
restored secretary connectivity; this commit makes the fix permanent
in the base image so every consumer of dicoop/mono-base
(coopback / desktop / cooparser / notifications / boot) inherits it
on the next release tag.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 13:21:26 +00:00
coopops fd2f716e61 fix(gateway): исходящий платёж скрыт от кассира до решения совета
Pre-mature payment row: при wallet.interactor.ts::createWithdraw платёж
сразу появлялся в gateway PG со status=PENDING — кассир видел заявку
как готовую к выплате ДО того как совет её утвердил.

Минимальный фикс без переписывания create/sync-цепочки:

- PaymentStatusEnum += AWAITING_AUTHORIZATION (с лейблом «Ожидает
  решения совета»). Initial status в gateway.interactor.createWithdraw
  переключён с PENDING на AWAITING_AUTHORIZATION.
- Новый WithdrawAuthorizationListener в gateway.module:
  - on-chain `wallet::authwthd` (совет авторизовал)
    → AWAITING_AUTHORIZATION → PENDING (кассир увидит и сможет подтвердить).
  - on-chain `wallet::declinewthd` (совет/Gateway отказал)
    → AWAITING_AUTHORIZATION → CANCELLED.

UI кассира (desktop) должен в отдельном PR фильтровать AWAITING_AUTHORIZATION
из списка платежей (текущие компоненты, скорее всего, и так не показывают
неизвестный статус). PAYMENT_STATUS_LABELS подхватится автоматически.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 10:34:50 +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
coopops 33a414c7e7 fix(ledger2,wallet): возврат паевого взноса — BURN_BLOCKED из заблокированной суммы
Завершение возврата паевого (o.wal.wthcpl) переключено с TRANSFER
(SHARE_FUND_PAY → WITHDRAWALS_SINK) на новый WalletOp::BURN_BLOCKED:
заблокированная при createwthd сумма сжигается на кошельке пайщика,
получателя на цепи нет — деньги уходят из системы банковским переводом.

Чинит assertion 'walletop TRANSFER: недостаточно L3-средств' на
completewthd: после REQUEST_WITHDRAW (BLOCK) сумма лежит в L3.blocked,
а TRANSFER проверял L3.available.

Изменения:
- ledger2: WalletOp::BURN_BLOCKED=6 + case в walletop.cpp (списание из
  L2/L3 blocked). OPERATION_REGISTRY: o.wal.wthcpl с BURN_BLOCKED, без
  wallet_to. burn_pattern_correct обобщён на оба BURN-варианта.
- cooptypes: WalletOp += 'BURN_BLOCKED'; o.wal.wthcpl wallet_to=null.
- wallets.hpp: WITHDRAWALS_SINK (w.wal.wthdrw) помечен DEPRECATED —
  остаётся в реестре для исторических L2-балансов.
- standard p.wal.wthdrw: приведён к коду — одностадийный flow
  (pending → authorized → completed), без approvewthd.
- wallet::approvewthd удалён как dead code: action не в whitelist
  callback'ов, из createwthd создаётся повестка сразу с callback=authwthd.
  Удалены: .cpp, declaration в wallet.hpp, include в wallet.cpp, struct
  и action в abi_json/wallet.json.
- sov.authpkg.standard.yaml: упоминание approvewthd → authwthd.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 10:24:44 +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
coopops d128f245b7 fix(boot): test-helper'ы подписания программных соглашений — wallet::signagree вместо soviet::sndagreement
После Эпика 2 / компонента 48 `soviet::sndagreement` отказывается на
`program_id > 0`, программные соглашения переехали в контракт wallet
(`wallet::signagree`, ADR-008). Соответственно boot test helper'ы
(`signWalletAgreement`/`signCapitalAgreement`/`signGenerationContract`)
и две точки `init/*` (`participant.addUser`, `cooperative.createCooperative`),
которые ещё дёргали `soviet::sndagreement` через `signAgreement`/
`blockchain.sendAgreement` с `agreement_type ∈ {wallet, blagorost, generator}`,
падали в beforeAll тестов на ассерте контракта.

Делаем:
- Новый общий helper `tests/wallet/signProgramAgreement.ts` —
  обёртка над `wallet::signagree` с auth=coopname@active. Проверяет, что
  в `wallet::users[username].programs[]` появилась запись с нужным
  `program_id`. Возвращает `{wallet, program, txId}` — той же формы,
  что прежние helper'ы (downstream-тесты остаются без изменений).
- Три исторических helper'а становятся тонкими обёртками над ним:
  `signWalletAgreement` → (program_id=1, draft_id=1),
  `signCapitalAgreement` → (program_id=4, draft_id=1000),
  `signGenerationContract` → (program_id=3, draft_id=0).
- `tests/capital/consts.ts` — добавлены `walletDraftId`/`sourceDraftId`/
  `capitalDraftId`, соответствующие мапе `program_map` в
  `lib/core/programs.hpp` (источник правды).
- `init/participant.ts` (`addUser`) и `init/cooperative.ts`
  (`createCooperative`) — `blockchain.sendAgreement(...agreement_type='wallet')`
  заменён на `signProgramAgreement(...program_id=1, draft_id=1)`.

`tests/soviet/signAgreement.ts` оставлен как есть — он по-прежнему нужен
для не-программных типов соглашений (`signature`/`user`/`privacy`,
program_id=0), которые остались за `soviet::sndagreement`.

Не входит:
- Замена `getUserProgramWallet` (читает legacy `soviet::progwallets`)
  на чтение `wallet::users` в downstream-тестах — отдельный технический
  долг, не блокирует beforeAll.
2026-05-21 08:16:27 +00:00
ant 8cd011e9a7 Merge remote-tracking branch 'origin/marketplace2' into chore/trustees-ownership 2026-05-21 08:15:07 +00:00
Alex Ant 1c7a3afc81 chore(release): publish
Publish Docs / gate (push) Successful in 2m4s
Trigger Contracts Docs Deploy / gate (push) Successful in 2m5s
Publish Packages / build-and-publish (push) Failing after 8m48s
Release / release (push) Failing after 7m22s
Trigger Contracts Docs Deploy / trigger-coopenomics (push) Failing after 6s
Publish Docs / build-and-publish-docs (push) Failing after 8m57s
2026-05-21 13:05:02 +05:00
Alex Ant c0627b53e3 chore(release): publish
Trigger Contracts Docs Deploy / gate (push) Successful in 2m6s
Publish Docs / gate (push) Successful in 2m9s
Publish Packages / build-and-publish (push) Has been skipped
Release / release (push) Failing after 4m28s
Trigger Contracts Docs Deploy / trigger-coopenomics (push) Has been skipped
Publish Docs / build-and-publish-docs (push) Has been skipped
2026-05-21 13:01:24 +05:00
Alex Ant ea8695d1fc Merge branch 'dev' of github.com:coopenomics/mono into dev 2026-05-21 13:00:41 +05:00
ant bf3f9ca5ac Merge pull request 'fix(capital): w.cap.gen → COOPERATIVE — снять L3-проверку, чинит convertsegm при CRPS' (#18) from feat/capital-w-cap-gen-cooperative into dev
Reviewed-on: #18
2026-05-21 07:47:51 +00:00
coopops f023d3a8c8 fix(capital): w.cap.gen на COOPERATIVE — снять L3-проверку, чинит convertsegm при CRPS-перераспределении
На voskhod walletop `convertsegm` падал «недостаточно L3-средств у
пайщика» в проектах, где CRPS-перераспределение увеличивало долю
contributor'а. Причина: `w.cap.gen` был USER_SHARED, а CRPS в
`approvecmmt` не делал per-user компенсирующих TRANSFER между сегментами:
инвариант `Σ COMMIT_RID == Σ ACCEPT_RID` соблюдался только на проекте,
не на сегменте, и L3-проверка walletop ломала convertsegm у пайщиков,
чья доля выросла через CRPS.

Фикс — переключение `w.cap.gen` с USER_SHARED на COOPERATIVE (ADR-002):
генератор становится единым кооперативным пулом без L3, walletop
проверяет только L2-баланс пула. `convertsegm` любого пайщика берёт из
общего котла ровно `segment.available_for_program` без проверки
персонального остатка.

Изменения:
- wallets.hpp: GENERATOR_FUND kind USER_SHARED → COOPERATIVE.
- signact2.cpp: коммент-инвариант — закрытие 08 на программе, не на
  сегменте (L3-разрез по пайщику снят).
- ledger2::migrate(): переписан на cleanup осиротевших userwallets
  [voskhod, w.cap.gen, *] прямым erase. Старая legacy → ledger2
  миграция убрана (meta.migrated=true в проде), сигнатура сжата до
  пустых скобок. migrate теперь — универсальная точка расширения
  для разовых исправлений состояния ledger2, по аналогии с
  capital::migrate.

migrate3 и capital::migrate сознательно не трогаются: migrate3 —
низкоуровневая per-record миграция L3 со своим назначением (не для
очистки чужой таблицы); capital::migrate остаётся пустой точкой
расширения капитала.

L2 `wallets2[w.cap.gen]` = Σ старых userwallets — синхронен на момент
перехода (проверено по фактическим данным voskhod).

Не входит в этот PR: балансовая корректировка для уже застрявших
пайщиков на voskhod — отдельной миграцией-скриптом после деплоя.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 07:45:50 +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
Alex Ant 5de2d2bba1 feat(boot:extra): засев partner1 как coop-пайщика в воскход для E2E Эпика 0 (#410)
Build bootstrap container / build (push) Failing after 3m12s
* 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>

* fix(desktop+harness): SPA mode dev + q-checkbox селектор

- desktop dev по умолчанию в SPA — SSR грузит сервер на 70%+ CPU и ломает
  reverse-proxy HTTP/2 multiplex'ом параллельных модулей /src/*. Старый
  режим доступен как `dev:ssr`. docker-compose зовёт `dev` — без правок.
- harness 09: Quasar q-checkbox рендерит label в div, не <label>; кликаем
  по `.q-checkbox:has-text(...)`.

* feat(boot:extra): засев partner1 как coop-пайщика в воскход + announce=domain

В installExtraData (reboot:extra) теперь регистрируется partner1 как
готовый coop-пайщик воскхода со status=active, чтобы dev-стенд после
reboot:extra сразу позволял запустить полный E2E прогон Эпика 0
(provider order_instance → Hostkey rent → playbook → coopback healthy
→ provider initSystem → chairman wizard).

Flow засева:
  newaccount → reguser(type=organization) → regcoop → stcoopstatus(active)
+ Mongo: organization + paymentMethod (bank_account)
+ Postgres: users.partner1{type:organization, role:user, status:joined}

В CooperativeClass — `announce` теперь хранит доменное имя
('voskhod-dev.coopenomics.world'), не текст. Provider читает announce
как domain для DomainDelegationPollingService (regex→is_valid,
health-check L7-Proxy 8880→is_delegated). См. memory
reference_provider_announce_domain.

Без этих правок:
- provider не подхватывает partner1 в свою DB после reboot:extra
- harness 08 после initSystem видит «Unsupported account type:
  undefined» — partner1 organization_data отсутствуют в воскход'е
- announce='Тестовый кооператив' не парсится как domain → is_valid=false

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(boot:extra): partner1 reuse WIF из docs-harness фикстуры

В installExtraData boot:extra на каждом запуске генерировал случайный
WIF для partner1 через generateKeypair(undefined). harness 08
(chairman wizard /partner1/install) брал WIF из docs-harness/state/
cooperatives/partner1.json (засеян однократно в репозиторий) — после
reboot:extra on-chain active key уже другой, startInstall mutation на
partner-coopback'е не принимает ключ, wizard зависает на шаге 1 без
видимой ошибки. ACTIVE+initialized провайдером проходит, но
installSystem (создание совета + рассылка инвайт-токенов) не
наступает → harness 09 не может получить WIF chairman'а.

Фикс: если фикстура есть, читаем wif (опц. publicKey, иначе деривим
ecc.privateToPublic) и передаём как seededKeys в generateKeypair —
получается детерминированный partner1 active key, синхронный с
harness'ом. Иначе fallback на старое поведение (рандомный ключ).

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>
Co-authored-by: coopops <coopops@coopenomics.world>
2026-05-19 22:03:54 +05:00
coopops 9e545329fc chore(contracts): починить .gitignore для собранных контрактов
После переезда контрактов в cpp/ (коммит 2b6b56a, март 2025) паттерны
в components/contracts/.gitignore ссылались на старые пути
(soviet/soviet.wasm и т.п.) и не матчились. В итоге собранные
.wasm/.abi уходили в репозиторий.

— переписаны паттерны на cpp/**/*.{wasm,abi}
— тестовые фикстуры в cpp/tests/test_contracts/** оставлены трекаться
— git rm --cached для soviet.{wasm,abi} и starter.{wasm,abi}
2026-05-19 17:01:46 +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 7ee4cd633f chore: add CLAUDE.md with project-scoped agent notes
Перенёс из общей auto-memory: stack, worktree policy (mono-ai-1 vs mono-ai-4), PR-flow, локальные тесты, GraphQL/desktop каноны, платформенные паттерны (роли, agreements, трёхуровневый онбординг), SDK login, vault & SERVER_SECRET, Capital фиксы, Стол Заказов MVP. Часть реорганизации памяти агента по проектам; в mono-ai-2..5 файл подцепляется локальными симлинками без отдельного коммита (попадёт в feature-ветки при следующем merge от dev).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 16:57:29 +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 b973baef03 chore(release): publish
Build bootstrap container / build (push) Failing after 2m59s
2026-05-19 15:59:48 +05:00
Alex Ant e10e049bdd chore(release): publish
Build bootstrap container / build (push) Failing after 2m54s
2026-05-19 14:41:11 +05:00
Alex Ant 8f5fcafecb Merge branch 'dev' of github.com:coopenomics/mono into dev 2026-05-19 14:40:18 +05:00
Alex Ant bef5df8a06 migration complete 2026-05-19 14:40:02 +05:00
Alex Ant a46a6c173a 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>
2026-05-19 13:51:45 +05: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
Alex Ant 34c4dadec3 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>
2026-05-19 11:40:37 +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 a24f5d4fc1 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>
2026-05-18 23:41:17 +05:00
Alex Ant 402783d8ae 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>
2026-05-18 23:31:54 +05:00
Alex Ant 2479cce94d fix(capital/convertsegm): регистрировать convert_statement в реестре документов как completed (#403)
Build bootstrap container / build (push) Failing after 2m54s
Финальная фаза процесса 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>
2026-05-18 22:47:38 +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 bd0bd21ae7 Merge branch 'dev' of github.com:coopenomics/mono into dev 2026-05-18 22:03:29 +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 abcd34f23d 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>
2026-05-18 13:00:45 +05:00
Alex Ant 83cac58fca 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>
2026-05-18 12:40:57 +05:00
Alex Ant dbf46d88d0 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>
2026-05-18 12:29:32 +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 a0cb930b77 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>
2026-05-18 11:29:12 +05:00
Alex Ant 4a4145c4ed 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>
2026-05-18 10:40:49 +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 d35ec2680a chore(release): publish
Build bootstrap container / build (push) Failing after 2m51s
2026-05-15 22:28:10 +05:00
Alex Ant 8523ab0c08 Merge branch 'testnet' 2026-05-15 22:27:55 +05:00
Alex Ant 74e3e88853 chore(release): publish
Build bootstrap container / build (push) Failing after 3m5s
2026-05-15 22:23:44 +05:00
Alex Ant 36441437db Merge branch 'dev' into testnet 2026-05-15 22:22:59 +05:00
Alex Ant 00860b3b18 fix commit process
Build bootstrap container / build (push) Failing after 2m52s
2026-05-15 22:21:35 +05:00
Alex Ant 8fe9509e97 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>
2026-05-15 22:02:36 +05:00
Alex Ant 43a9e05ccb chore(release): publish
Build bootstrap container / build (push) Failing after 2m55s
2026-05-15 17:20:43 +05:00
Alex Ant d4de7a3028 Merge branch 'testnet' 2026-05-15 16:48:30 +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 e2584fb898 chore(release): publish
Build contracts container / build (push) Failing after 2m54s
Build bootstrap container / build (push) Failing after 2m53s
Trigger Contracts Docs Deploy / trigger-coopenomics (push) Failing after 7s
Publish Docs / build-and-publish-docs (push) Failing after 18m43s
2026-05-15 16:39:03 +05:00
Alex Ant 3785c55af1 Merge branch 'dev' of github.com:coopenomics/mono into dev 2026-05-15 16:38:35 +05:00
coopops 848566bd69 fix(file-storage): MinIO стартует только при заданном MINIO_ENDPOINT
Trigger Contracts Docs Deploy / trigger-coopenomics (push) Failing after 11s
Publish Docs / build-and-publish-docs (push) Failing after 18m20s
Раньше 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>
2026-05-15 11:30:48 +00:00
Alex Ant 61255da5f4 Merge branch 'main' of github.com:coopenomics/mono 2026-05-15 16:26:18 +05:00
Alex Ant 1a63638f73 chore(release): publish 2026-05-15 15:45:18 +05:00
Alex Ant c8bc5c782d fix commit process 2026-05-15 15:44:46 +05:00
Alex Ant 4e2fbac340 Merge branch 'dev' of github.com:coopenomics/mono into dev 2026-05-15 15:44:28 +05:00
coopops aeb4efe2b6 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>
2026-05-15 10:43:12 +00: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 8aeee3ee56 ci: tag-only триггеры для build-contracts и docs (#367)
Build bootstrap container / build (push) Failing after 2m56s
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>
2026-05-14 14:58:24 +05:00
Alex Ant 70446c300d chore(release): publish 2026-05-14 13:59:19 +05:00
Alex Ant 007a4dbfb8 Merge branch 'main' of github.com:coopenomics/mono 2026-05-14 13:59:05 +05:00
Alex Ant 582a2825be chore(release): publish
Build bootstrap container / build (push) Failing after 2m45s
Trigger Contracts Docs Deploy / trigger-coopenomics (push) Failing after 4s
Publish Docs / build-and-publish-docs (push) Failing after 14m42s
2026-05-14 13:58:25 +05:00
Alex Ant b2f13dc093 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>
2026-05-14 13:55:04 +05:00
Alex Ant c41aba12f1 chore(release): publish
Build bootstrap container / build (push) Failing after 2m58s
Trigger Contracts Docs Deploy / trigger-coopenomics (push) Failing after 6s
Build contracts container / build (push) Failing after 9m28s
Publish Docs / build-and-publish-docs (push) Failing after 16m1s
2026-05-14 10:53:46 +05:00
Alex Ant 5a01aee69f chore(release): publish
Build bootstrap container / build (push) Failing after 2m55s
Trigger Contracts Docs Deploy / trigger-coopenomics (push) Failing after 6s
Publish Docs / build-and-publish-docs (push) Failing after 15m39s
Build contracts container / build (push) Failing after 9m4s
2026-05-13 22:40:40 +05:00
1926 changed files with 112919 additions and 51018 deletions
+3
View File
@@ -0,0 +1,3 @@
Trial run of Typecheck workflow on PR → dev.
Created 2026-05-23 to measure end-to-end duration of vue-tsc (desktop) and tsc (controller).
Do not merge; close after measurement.
-15
View File
@@ -18,7 +18,6 @@ name: Build bootstrap container
#
# Секреты:
# DOCKERHUB_USERNAME / DOCKERHUB_TOKEN
# TELEGRAM_BOT_TOKEN / TELEGRAM_CHAT_ID
on:
push:
@@ -85,17 +84,3 @@ jobs:
SHORT_SHA="${GITHUB_SHA::7}"
docker tag "$IMAGE:${{ env.DOCKER_TAG }}" "$IMAGE:${{ env.DOCKER_TAG }}-$SHORT_SHA"
docker push "$IMAGE:${{ env.DOCKER_TAG }}-$SHORT_SHA"
- name: Telegram notify success
if: ${{ success() }}
run: |
curl -s -X POST https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage \
-d chat_id=${{ secrets.TELEGRAM_CHAT_ID }} \
-d text="✅ [GITHUB MONO] dicoop/bootcoop:${{ env.DOCKER_TAG }} (sha=${GITHUB_SHA::7})"
- name: Telegram notify failure
if: ${{ failure() }}
run: |
curl -s -X POST https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage \
-d chat_id=${{ secrets.TELEGRAM_CHAT_ID }} \
-d text="❌ [GITHUB MONO] Ошибка сборки dicoop/bootcoop на ${{ github.ref_name }} (sha=${GITHUB_SHA::7})"
-223
View File
@@ -1,223 +0,0 @@
name: Build Docker Images
# Триггерится по успешному завершению `Build contracts container`. Это
# гарантирует, что к моменту webhook'а на тестнет/прод образ
# `dicoop/contracts:<branch>` уже обновлён в DockerHub.
#
# Раньше workflow висел на `push: tags`, и при релизе (`chore(release):
# publish` + git tag) запускался параллельно с build-contracts. Поскольку
# build-containers (~8 мин) короче build-contracts (~11 мин), webhook
# отлетал раньше окончания build-contracts на 2-3 минуты — ансибл на
# тестнете подтягивал ПРЕДЫДУЩИЙ образ контрактов и перетирал чейн
# старым wasm. Инцидент 2026-05-13 (ledger2 фикс walletop sweep'а
# откатился ансиблом).
#
# Теперь: build-contracts триггерится и по push веток, и по push тэгов
# (см. tags в build-contracts.yaml). После его success этот workflow
# резолвит — есть ли тэг на коммите, и если есть — собирает контейнеры
# и шлёт webhook. Если тэга нет (обычный push в ветку без релиза) —
# делаем no-op.
on:
workflow_run:
workflows: ["Build contracts container"]
types: [completed]
# Ручной fallback: запустить сборку контейнеров и деплой для конкретного
# тэга, если автоматический workflow_run не сработал (типичный кейс —
# миграционное окно: на default-ветке (main) старая версия build-containers
# с `push: tags`, а на dev/testnet уже новая с `workflow_run`. До мержа
# в main новая логика не активна).
workflow_dispatch:
inputs:
tag:
description: 'Git-тэг для сборки и деплоя (например v2026.5.13-alpha-3)'
required: true
jobs:
build:
runs-on: ubuntu-latest
# Job запускается, если: (а) workflow_run от build-contracts успешен,
# либо (б) ручной запуск через workflow_dispatch.
if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }}
steps:
- name: Checkout repository at triggering SHA
uses: actions/checkout@v3
with:
ref: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || github.event.workflow_run.head_sha }}
fetch-depth: 0
- name: Resolve tag for triggering commit
id: resolve_tag
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
TAG_NAME="${{ github.event.inputs.tag }}"
HEAD_SHA=$(git rev-parse "$TAG_NAME^{commit}")
echo "Ручной запуск: tag=$TAG_NAME sha=$HEAD_SHA"
else
HEAD_SHA="${{ env.HEAD_SHA }}"
# `git describe --exact-match` отдаёт имя тэга, лежащего ровно
# на этом коммите; код 128 если тэга нет — тогда выходим без
# сборки контейнеров (push в ветку без релизного тэга).
TAG_NAME=$(git describe --tags --exact-match "$HEAD_SHA" 2>/dev/null || true)
if [ -z "$TAG_NAME" ]; then
echo "На коммите $HEAD_SHA тэга нет — релиза не было, контейнеры не собираем."
echo "has_tag=false" >> $GITHUB_OUTPUT
exit 0
fi
fi
echo "Найден тэг: $TAG_NAME"
echo "has_tag=true" >> $GITHUB_OUTPUT
echo "DOCKER_TAG=$TAG_NAME" >> $GITHUB_ENV
echo "HEAD_SHA=$HEAD_SHA" >> $GITHUB_ENV
# Проверяем, является ли тег продакшн-тегом (не содержит alpha, beta, rc и т.д.)
if [[ ! $TAG_NAME =~ -(alpha|beta|rc|test) ]]; then
echo "IS_PRODUCTION_TAG=true" >> $GITHUB_ENV
echo "Это продакшн тег, будем добавлять latest"
else
echo "IS_PRODUCTION_TAG=false" >> $GITHUB_ENV
echo "Это не продакшн тег, latest не добавляем"
fi
- name: Debug info
if: steps.resolve_tag.outputs.has_tag == 'true'
run: |
echo "Триггер: ${{ github.event_name }}"
echo "Триггерящий коммит: ${{ env.HEAD_SHA }}"
echo "Резолвенный тэг: ${{ env.DOCKER_TAG }}"
echo "Последние коммиты:"
git log -n 3 --oneline
echo "Проверяем файлы в директории components/desktop/src-ssr:"
ls -la components/desktop/src-ssr/ || echo "Директория не найдена!"
echo "Проверяем файлы в middlewares:"
ls -la components/desktop/src-ssr/middlewares/ || echo "Директория middlewares не найдена!"
- name: Login to DockerHub
if: steps.resolve_tag.outputs.has_tag == 'true'
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# Сначала собираем базовый образ с runtime
- name: Build base image
if: steps.resolve_tag.outputs.has_tag == 'true'
run: |
docker build --target runtime -t dicoop/mono-base:${{ env.DOCKER_TAG }} .
docker push dicoop/mono-base:${{ env.DOCKER_TAG }}
# Если это продакшн тег, добавляем latest
if [[ "${{ env.IS_PRODUCTION_TAG }}" == "true" ]]; then
docker tag dicoop/mono-base:${{ env.DOCKER_TAG }} dicoop/mono-base:latest
docker push dicoop/mono-base:latest
fi
# Создаем сервисные образы на основе базового
- name: Build desktop image
if: steps.resolve_tag.outputs.has_tag == 'true'
run: |
echo "FROM dicoop/mono-base:${{ env.DOCKER_TAG }}" > Dockerfile.desktop
echo "CMD [\"pnpm\", \"-F\", \"@coopenomics/desktop\", \"run\", \"start\"]" >> Dockerfile.desktop
docker build -t dicoop/desktop:${{ env.DOCKER_TAG }} -f Dockerfile.desktop .
docker push dicoop/desktop:${{ env.DOCKER_TAG }}
if [[ "${{ env.IS_PRODUCTION_TAG }}" == "true" ]]; then
docker tag dicoop/desktop:${{ env.DOCKER_TAG }} dicoop/desktop:latest
docker push dicoop/desktop:latest
fi
- name: Build controller image
if: steps.resolve_tag.outputs.has_tag == 'true'
run: |
echo "FROM dicoop/mono-base:${{ env.DOCKER_TAG }}" > Dockerfile.coopback
echo "CMD [\"pnpm\", \"-F\", \"@coopenomics/controller\", \"run\", \"start\"]" >> Dockerfile.coopback
docker build -t dicoop/coopback:${{ env.DOCKER_TAG }} -f Dockerfile.coopback .
docker push dicoop/coopback:${{ env.DOCKER_TAG }}
if [[ "${{ env.IS_PRODUCTION_TAG }}" == "true" ]]; then
docker tag dicoop/coopback:${{ env.DOCKER_TAG }} dicoop/coopback:latest
docker push dicoop/coopback:latest
fi
# TEMP: парсер используется существующим деплоем как dicoop/cooparser,
# пока не мигрировали на dicoop/parser из отдельного coopenomics/parser repo.
# Оставляем сборку и пуш образа до завершения миграции потребителей.
- name: Build parser image
if: steps.resolve_tag.outputs.has_tag == 'true'
run: |
echo "FROM dicoop/mono-base:${{ env.DOCKER_TAG }}" > Dockerfile.cooparser
echo "CMD [\"pnpm\", \"-F\", \"@coopenomics/parser\", \"run\", \"start\"]" >> Dockerfile.cooparser
docker build -t dicoop/cooparser:${{ env.DOCKER_TAG }} -f Dockerfile.cooparser .
docker push dicoop/cooparser:${{ env.DOCKER_TAG }}
if [[ "${{ env.IS_PRODUCTION_TAG }}" == "true" ]]; then
docker tag dicoop/cooparser:${{ env.DOCKER_TAG }} dicoop/cooparser:latest
docker push dicoop/cooparser:latest
fi
- name: Build notificator image
if: steps.resolve_tag.outputs.has_tag == 'true'
run: |
echo "FROM dicoop/mono-base:${{ env.DOCKER_TAG }}" > Dockerfile.notificator
echo "CMD [\"pnpm\", \"-F\", \"coop-notificator\", \"run\", \"start\"]" >> Dockerfile.notificator
docker build -t dicoop/notificator:${{ env.DOCKER_TAG }} -f Dockerfile.notificator .
docker push dicoop/notificator:${{ env.DOCKER_TAG }}
if [[ "${{ env.IS_PRODUCTION_TAG }}" == "true" ]]; then
docker tag dicoop/notificator:${{ env.DOCKER_TAG }} dicoop/notificator:latest
docker push dicoop/notificator:latest
fi
- name: Build notifications image
if: steps.resolve_tag.outputs.has_tag == 'true'
run: |
echo "FROM dicoop/mono-base:${{ env.DOCKER_TAG }}" > Dockerfile.notifications
echo "CMD [\"pnpm\", \"-F\", \"@coopenomics/notifications\", \"run\", \"sync\"]" >> Dockerfile.notifications
docker build -t dicoop/notifications:${{ env.DOCKER_TAG }} -f Dockerfile.notifications .
docker push dicoop/notifications:${{ env.DOCKER_TAG }}
if [[ "${{ env.IS_PRODUCTION_TAG }}" == "true" ]]; then
docker tag dicoop/notifications:${{ env.DOCKER_TAG }} dicoop/notifications:latest
docker push dicoop/notifications:latest
fi
# Отправка хука для деплоя. К этому моменту build-contracts.yaml
# уже завершился успешно (мы и есть его workflow_run-зависимый
# потребитель), значит образ `dicoop/contracts:<branch>` гарантированно
# обновлён в DockerHub — гонки 2026-05-13 больше не воспроизвести.
- name: Trigger deployment webhook
if: success() && steps.resolve_tag.outputs.has_tag == 'true'
run: |
if [[ "${{ env.DOCKER_TAG }}" == *alpha* ]]; then
# Хук для тестнета (alpha теги)
curl -X POST ${{ vars.TESTNET_WEBHOOK_URL }} \
-H 'Content-Type: application/json' \
-d '${{ env.DOCKER_TAG }}'
else
# Хук для продакшена (остальные теги)
curl -X POST ${{ vars.PRODUCTION_WEBHOOK_URL }} \
-H 'Content-Type: application/json' \
-d '${{ env.DOCKER_TAG }}'
fi
# Уведомление в Telegram об успехе
- name: Telegram notify success
if: success() && steps.resolve_tag.outputs.has_tag == 'true'
run: |
if [[ "${{ env.IS_PRODUCTION_TAG }}" == "true" ]]; then
ADDITIONAL_INFO=" (с тегом latest)"
else
ADDITIONAL_INFO=""
fi
curl -s -X POST https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage \
-d chat_id=${{ secrets.TELEGRAM_CHAT_ID }} \
-d text="✅ [GITHUB MONO] Успешная сборка контейнеров: $GITHUB_REPOSITORY (sha=${{ env.HEAD_SHA }}) [${{ env.DOCKER_TAG }}]$ADDITIONAL_INFO"
# Уведомление в Telegram об ошибке (только если до этого был
# резолвен тэг — иначе no-op запуск без сборки и без алертов).
- name: Telegram notify failure
if: failure() && steps.resolve_tag.outputs.has_tag == 'true'
run: |
curl -s -X POST https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage \
-d chat_id=${{ secrets.TELEGRAM_CHAT_ID }} \
-d text="❌ [GITHUB MONO] Ошибка при сборке контейнеров: $GITHUB_REPOSITORY (sha=${{ env.HEAD_SHA }}) [${{ env.DOCKER_TAG }}]"
@@ -1,18 +0,0 @@
# .github/workflows/trigger-coopenomics.yml
name: Trigger Contracts Docs Deploy
on:
push:
branches: [dev, testnet, main, capital] # или когда нужно триггерить
jobs:
trigger-coopenomics:
runs-on: ubuntu-latest
steps:
- name: Trigger Coopenomics deployment
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.COOPENOMICS_PAT }}
repository: coopenomics/coopenomics # укажи правильный owner/repo
event-type: deploy_from_mono
client-payload: '{"repository": "${{ github.repository }}", "sha": "${{ github.sha }}", "ref": "${{ github.ref }}", "actor": "${{ github.actor }}"}'
-240
View File
@@ -1,240 +0,0 @@
name: Build contracts container
# Сборка и публикация docker-образа `dicoop/contracts`, который содержит
# wasm/abi всех контрактов кооперативной экономики (apps, marketplace,
# ledger, meet, loan, wallet, capital, branch, contributor, fund, draft,
# soviet, registrator, gateway, system, test). Образ потребляется
# ke-bootstrap'ом (отдельный артефакт), который при старте ноды деплоит
# контракты через `cleos set contract`.
#
# Триггер — push в одну из веток-окружений (dev / testnet / main).
# Маппинг:
# dev → IS_TESTNET=ON, tag=dev
# testnet → IS_TESTNET=ON, tag=testnet
# main → IS_TESTNET=OFF, tag=main + latest
#
# Сам compile идёт через CDT-образ `dicoop/blockchain_v5.1.1:dev`
# (тот же, что использует `components/contracts/build.sh|build-all.sh`).
# Артефакты копируются в slim alpine-образ.
#
# Секреты:
# DOCKERHUB_USERNAME / DOCKERHUB_TOKEN — публикация в dicoop/*
# TELEGRAM_BOT_TOKEN / TELEGRAM_CHAT_ID — уведомления
on:
push:
branches: [dev, testnet, main]
paths:
- 'components/contracts/cpp/**'
- 'components/contracts/CMakeLists.txt'
- 'components/contracts/build.sh'
- 'components/contracts/build-all.sh'
- 'components/contracts/docker/**'
- '.github/workflows/build-contracts.yaml'
# Дополнительно ловим все push'и тэгов: build-containers.yaml зависит от
# успеха этой сборки через workflow_run, и без unconditional запуска по
# тегу build-containers не получит триггер для коммитов, где .cpp не
# менялся (например `chore(release): publish` правит только версии
# пакетов). См. историю гонки 2026-05-13: webhook деплоя сработал на
# 2:34 быстрее обновления `dicoop/contracts:testnet`, на чейн уехал
# старый wasm.
tags:
- '*'
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
# Полная история нужна, чтобы при push'е тэга резолвить ветку
# через `git branch -r --contains $SHA` (см. следующий step).
fetch-depth: 0
- name: Determine build mode and docker tag
# Источник режима — ВЕТКА, не имя тэга. Сначала пытаемся взять
# `github.ref_name` как ветку (push веток); если не подошло — это
# push тэга, и мы ищем какая из dev/testnet/main содержит коммит.
# Тэг такого окружения нести семантику не должен (например,
# `v2026.5.13-alpha-2` → ветка testnet, BUILD_MODE=test).
run: |
REF="${{ github.ref_name }}"
REF_TYPE="${{ github.ref_type }}"
BRANCH=""
if [ "$REF_TYPE" = "branch" ]; then
BRANCH="$REF"
else
SHA="${{ github.sha }}"
for candidate in main testnet dev; do
if git branch -r --contains "$SHA" 2>/dev/null | grep -qE "^[[:space:]]*origin/${candidate}$"; then
BRANCH="$candidate"
break
fi
done
fi
case "$BRANCH" in
main)
echo "BUILD_MODE=prod" >> $GITHUB_ENV
echo "DOCKER_TAG=main" >> $GITHUB_ENV
echo "EXTRA_TAG=latest" >> $GITHUB_ENV
;;
testnet)
echo "BUILD_MODE=test" >> $GITHUB_ENV
echo "DOCKER_TAG=testnet" >> $GITHUB_ENV
echo "EXTRA_TAG=" >> $GITHUB_ENV
;;
dev)
echo "BUILD_MODE=test" >> $GITHUB_ENV
echo "DOCKER_TAG=dev" >> $GITHUB_ENV
echo "EXTRA_TAG=" >> $GITHUB_ENV
;;
*)
echo "Unsupported ref: ref_name=$REF ref_type=$REF_TYPE resolved_branch='$BRANCH'" >&2
exit 1
;;
esac
echo "Resolved BRANCH=$BRANCH (ref_name=$REF ref_type=$REF_TYPE)"
- name: Pull CDT toolchain image
run: docker pull dicoop/blockchain_v5.1.1:dev
- name: Compile all contracts
working-directory: components/contracts
run: |
./build-all.sh "$BUILD_MODE"
echo "--- build/contracts ---"
ls -la build/contracts/
- name: Stage docker context
working-directory: components/contracts
run: |
ROOT="docker/.context"
rm -rf "$ROOT"
mkdir -p "$ROOT/contracts"
# Имена контрактов берём из CMakeLists (`add_contract_build(<name>)`)
# — единая точка истины. Любое изменение списка там автоматически
# подтянется в образ.
# Имена контрактов = аргументы add_contract_build(...) только в
# незакомменченных строках (CMake-комментарий начинается с `#`).
NAMES=$(grep -E '^[[:space:]]*add_contract_build\(' CMakeLists.txt \
| sed -E 's/^[[:space:]]*add_contract_build\(([^)]*)\).*/\1/')
echo "Contracts to package:"
echo "$NAMES"
MANIFEST="$ROOT/contracts/manifest.json"
# Хелпер: упаковывает одну пару (wasm, abi) в /contracts/<name>/
# и добавляет JSON-запись в STDOUT (caller склеивает в массив).
pack_one() {
local NAME="$1" WASM="$2" ABI="$3"
if [ ! -f "$WASM" ] || [ ! -f "$ABI" ]; then
echo "::warning::Skipping $NAME: artifacts missing ($WASM / $ABI)" >&2
return 1
fi
mkdir -p "$ROOT/contracts/$NAME"
cp "$WASM" "$ROOT/contracts/$NAME/$NAME.wasm"
cp "$ABI" "$ROOT/contracts/$NAME/$NAME.abi"
local SHA_WASM=$(sha256sum "$WASM" | awk '{print $1}')
local SHA_ABI=$(sha256sum "$ABI" | awk '{print $1}')
printf ' {"name":"%s","sha256_wasm":"%s","sha256_abi":"%s"}' \
"$NAME" "$SHA_WASM" "$SHA_ABI"
}
{
printf '{\n'
printf ' "build_mode": "%s",\n' "$BUILD_MODE"
printf ' "git_sha": "%s",\n' "$GITHUB_SHA"
printf ' "ref": "%s",\n' "$GITHUB_REF_NAME"
printf ' "built_at": "%s",\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
printf ' "contracts": [\n'
FIRST=1
emit() {
entry=$(pack_one "$@") || return 0
[ $FIRST -eq 0 ] && printf ',\n'
FIRST=0
printf '%s' "$entry"
}
for c in $NAMES; do
if [ "$c" = "system" ]; then
# У system многоконтрактная сборка: разносим под-контракты
# eosio.* как полноценные элементы каталога, а сам "system"
# как имя пропускаем. test_contracts/* не упаковываем —
# это тестовые stub'ы, не нужны на проде.
for sub in build/contracts/system/contracts/eosio.*/; do
[ -d "$sub" ] || continue
sub_name=$(basename "$sub")
emit "$sub_name" "$sub/$sub_name.wasm" "$sub/$sub_name.abi"
done
else
emit "$c" "build/contracts/$c/$c.wasm" "build/contracts/$c/$c.abi"
fi
done
printf '\n ]\n'
printf '}\n'
} > "$MANIFEST"
cp docker/Dockerfile "$ROOT/Dockerfile"
cp docker/entrypoint.sh "$ROOT/entrypoint.sh"
chmod +x "$ROOT/entrypoint.sh"
echo "--- manifest.json ---"
cat "$MANIFEST"
echo "--- context tree ---"
find "$ROOT" -maxdepth 3 -type f | sort
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push image
working-directory: components/contracts
run: |
IMAGE="dicoop/contracts"
docker build \
--label "org.opencontainers.image.revision=${{ github.sha }}" \
--label "org.opencontainers.image.created=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--label "build.mode=$BUILD_MODE" \
-t "$IMAGE:$DOCKER_TAG" \
./docker/.context
docker push "$IMAGE:$DOCKER_TAG"
if [ -n "$EXTRA_TAG" ]; then
docker tag "$IMAGE:$DOCKER_TAG" "$IMAGE:$EXTRA_TAG"
docker push "$IMAGE:$EXTRA_TAG"
fi
# SHA-tag для воспроизводимости (если нужно запинить конкретный
# коммит, а не двигающийся branch-tag).
SHORT_SHA="${GITHUB_SHA::7}"
docker tag "$IMAGE:$DOCKER_TAG" "$IMAGE:$DOCKER_TAG-$SHORT_SHA"
docker push "$IMAGE:$DOCKER_TAG-$SHORT_SHA"
- name: Verify pushed image
run: |
docker run --rm dicoop/contracts:$DOCKER_TAG list
echo "---"
docker run --rm dicoop/contracts:$DOCKER_TAG sha256
- name: Telegram notify success
if: ${{ success() }}
run: |
COUNT=$(docker run --rm dicoop/contracts:$DOCKER_TAG list | wc -l)
curl -s -X POST https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage \
-d chat_id=${{ secrets.TELEGRAM_CHAT_ID }} \
-d text="✅ [GITHUB MONO] dicoop/contracts:$DOCKER_TAG ($COUNT контрактов, mode=$BUILD_MODE, sha=${GITHUB_SHA::7})"
- name: Telegram notify failure
if: ${{ failure() }}
run: |
curl -s -X POST https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage \
-d chat_id=${{ secrets.TELEGRAM_CHAT_ID }} \
-d text="❌ [GITHUB MONO] Ошибка сборки dicoop/contracts на ${{ github.ref_name }} (sha=${GITHUB_SHA::7})"
-135
View File
@@ -1,135 +0,0 @@
name: Publish Docs
on:
push:
branches:
- main
- testnet
- dev
- reports
- marketplace2
jobs:
build-and-publish-docs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
# Версия pnpm берётся из `packageManager` корневого package.json,
# синхронно с publish-packages.yaml и Dockerfile'ами.
- name: Set up pnpm
uses: pnpm/action-setup@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install Python requirements
run: |
python -m venv venv
source venv/bin/activate
pip install mkdocs-material mkdocs-macros-plugin mkdocs-section-index pymdown-extensions
working-directory: ./components/docs
- name: Install Node.js dependencies
run: pnpm install
working-directory: ./components/docs
- name: Patch spectaql-config.yml for CI
run: |
sed -i.bak "0,/url:.*/s|url:.*|url: 'https://testnet.coopenomics.world/backend/v1/graphql'|" spectaql-config.yml
working-directory: ./components/controller
- name: Show patched spectaql-config.yml
run: cat spectaql-config.yml
working-directory: ./components/controller
- name: Build cooptypes
run: pnpm run build
working-directory: ./components/cooptypes
- name: Generate controller docs
run: pnpm run docs
working-directory: ./components/controller
- name: Copy controller docs
run: |
mkdir -p ./components/docs/docs/graphql
cp -r ./components/controller/docs/* ./components/docs/docs/graphql/
- name: Generate sdk docs
run: pnpm run docs
working-directory: ./components/sdk
- name: Copy sdk docs
run: |
mkdir -p ./components/docs/docs/sdk
cp -r ./components/sdk/docs/* ./components/docs/docs/sdk/
- name: Generate cooptypes docs
run: pnpm run docs
working-directory: ./components/cooptypes
- name: Copy cooptypes docs
run: |
mkdir -p ./components/docs/docs/cooptypes
cp -r ./components/cooptypes/docs/* ./components/docs/docs/cooptypes/
# ── standards-site → /standards/ на docs.цифровой-кооператив.рф ──
# Отдельный Vue-сайт с BPMN-графом кооперативных стандартов,
# публикуется как поддиректория на том же домене. Vite собирает
# с base='/standards/' (см. vite.config.ts), относительные пути
# внутри dist/ и hash-router работают корректно.
# dist кладём в docs/standards/ ДО mkdocs build — так же, как
# graphql/sdk/cooptypes; mkdocs сам включит его в итоговый site/.
- name: Build standards-site
run: pnpm run build
working-directory: ./components/contracts/standards-site
- name: Copy standards-site into docs/standards
run: |
mkdir -p ./components/docs/docs/standards
cp -r ./components/contracts/standards-site/dist/* ./components/docs/docs/standards/
- name: Build docs (mkdocs)
run: |
source venv/bin/activate
mkdocs build
working-directory: ./components/docs
- name: Remove specific large file before publishing
run: |
# Удаляем конкретный большой файл sdk/typedoc.json
rm -f ./components/docs/site/sdk/typedoc.json
# Проверяем, что файл удален
if [ -f "./components/docs/site/sdk/typedoc.json" ]; then
echo "ERROR: typedoc.json still exists!"
exit 1
else
echo "SUCCESS: typedoc.json removed successfully"
fi
- name: Publish to GitHub Pages
run: npx gh-pages --nojekyll -d site --repo https://x-access-token:${GITHUB_TOKEN}@github.com/coopenomics/mono.git
working-directory: ./components/docs
env:
GIT_AUTHOR_NAME: github-actions
GIT_AUTHOR_EMAIL: github-actions@github.com
GIT_COMMITTER_NAME: github-actions
GIT_COMMITTER_EMAIL: github-actions@github.com
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Trigger docs deployment webhook
if: ${{ success() }}
run: |
curl -X POST "${{ vars.DOCS_DEPLOY_WEBHOOK_URL }}" \
-H 'Content-Type: application/json' \
-d '{"ref":"${{ github.ref }}","sha":"${{ github.sha }}","branch":"${{ github.ref_name }}"}'
-37
View File
@@ -1,37 +0,0 @@
name: Publish Packages
on:
push:
tags:
- 'v*'
permissions:
contents: read
jobs:
build-and-publish:
if: |
startsWith(github.ref, 'refs/tags/v') &&
!contains(github.ref, '-alpha')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Жёстко прибиваем версию pnpm, которой сгенерён lockfile (lockfileVersion 9.0 = pnpm 9/10).
# Иначе action-setup может подтянуть другую версию и переписать pnpm-lock.yaml → Lerna EUNCOMMIT.
- uses: pnpm/action-setup@v4
with:
version: 10.33.0
- uses: actions/setup-node@v4
with:
node-version: 24
registry-url: https://registry.npmjs.org
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm lerna run build
- run: pnpm lerna publish from-package --yes
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+406
View File
@@ -0,0 +1,406 @@
name: Release
# Единый релизный workflow по push'у тэга `v*`.
#
# Порядок (через jobs.needs):
# 1) release — контракты + контейнеры + webhook деплоя (атомарно)
# 2) publish-packages — npm publish через lerna (если не -alpha)
# 3) trigger-coopenomics — workflow_dispatch сборки сайта C9S/coopenomics на
# Gitea (если не -alpha + ветка main); он сам тянет
# mono и пересобирает доки.
#
# publish-docs (gh-pages на github.com) удалён 2026-05-25 — после переезда на
# Gitea Pages-публикация невалидна; доки деплоятся через C9S/coopenomics.
#
# Зачем последовательно: пакеты/доки/внешний триггер не должны уезжать,
# если релиз контрактов или контейнеров провалился. Раньше четыре workflow'а
# стартовали параллельно по одному тэгу — отсюда гонка из инцидента 2026-05-13
# с ledger2 walletop sweep'ом (см. историю release.yaml до объединения).
#
# Гейт `-alpha` унифицирован: пакеты + доки + триггер docs пропускают только
# не-alpha тэги. -beta/-rc/-test в проекте не используются (см. CLAUDE.md),
# поэтому проще иметь один паттерн `!contains(github.ref, '-alpha')`.
# IS_PROD в release-job остаётся на `(alpha|beta|rc|test)` — он определяет
# деплой-webhook (testnet vs production) и тэг `:latest` у образов;
# намеренно строже, чтобы случайный -rc-тэг не уехал на прод.
#
# Резолв ветки. Тэг семантики окружения не несёт (например `v2026.5.13-alpha-2`
# может лежать на testnet или dev — определяет именно ветка, на которую сделан
# merge перед тэгом). Поэтому ищем первое совпадение SHA с remote-ветками
# main/testnet/dev. branch экспортируется как outputs.branch для downstream jobs.
on:
push:
tags:
- 'v*'
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
outputs:
tag_name: ${{ steps.resolve.outputs.tag_name }}
branch: ${{ steps.resolve.outputs.branch }}
is_prod: ${{ steps.resolve.outputs.is_prod }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Resolve branch, build mode and tags
id: resolve
run: |
TAG_NAME="${{ github.ref_name }}"
SHA="${{ github.sha }}"
BRANCH=""
for candidate in main testnet dev; do
if git branch -r --contains "$SHA" 2>/dev/null | grep -qE "^[[:space:]]*origin/${candidate}$"; then
BRANCH="$candidate"
break
fi
done
if [ -z "$BRANCH" ]; then
echo "::error::Тэг $TAG_NAME ($SHA) не лежит ни на одной из dev/testnet/main"
exit 1
fi
case "$BRANCH" in
main)
BUILD_MODE=prod
CONTRACTS_TAG=main
CONTRACTS_EXTRA=latest
;;
testnet)
BUILD_MODE=test
CONTRACTS_TAG=testnet
CONTRACTS_EXTRA=
;;
dev)
BUILD_MODE=test
CONTRACTS_TAG=dev
CONTRACTS_EXTRA=
;;
esac
# IS_PROD определяет webhook деплоя (PRODUCTION vs TESTNET) и тэг :latest.
# Сознательно строже гейта publish-* — случайный -rc/-beta тэг не должен
# уехать на прод-вебхук, даже если в npm/docs его опубликовали.
if [[ "$TAG_NAME" =~ -(alpha|beta|rc|test) ]]; then
IS_PROD=false
WEBHOOK_URL='${{ vars.TESTNET_WEBHOOK_URL }}'
else
IS_PROD=true
WEBHOOK_URL='${{ vars.PRODUCTION_WEBHOOK_URL }}'
fi
{
echo "TAG_NAME=$TAG_NAME"
echo "SHA=$SHA"
echo "BRANCH=$BRANCH"
echo "BUILD_MODE=$BUILD_MODE"
echo "CONTRACTS_TAG=$CONTRACTS_TAG"
echo "CONTRACTS_EXTRA=$CONTRACTS_EXTRA"
echo "IS_PROD=$IS_PROD"
echo "WEBHOOK_URL=$WEBHOOK_URL"
} >> "$GITHUB_ENV"
{
echo "tag_name=$TAG_NAME"
echo "branch=$BRANCH"
echo "is_prod=$IS_PROD"
} >> "$GITHUB_OUTPUT"
echo "Release $TAG_NAME → branch=$BRANCH, build_mode=$BUILD_MODE, contracts:$CONTRACTS_TAG, prod=$IS_PROD"
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# === Этап 1: контракты ===
# CDT ставим из .deb (C9S/cdt v4.2.0) прямо в окружение job'а и
# компилируем напрямую — БЕЗ вложенного docker. Почему не build-all.sh:
# тот монтирует $(pwd):/project в sibling-контейнер, а под Gitea
# act_runner сам job исполняется в контейнере → хостовый демон не видит
# этот путь, /project пуст, cmake падает "no CMakeLists.txt".
# build-all.sh остаётся для локальной сборки (оборачивает тот же
# build_contracts_cdt.sh в docker).
# CMakeLists хардкодит toolchain /cdt/build/...; .deb кладёт CDT в
# /usr/opt/cdt/4.2.0, а сам CDTWasmToolchain.cmake указывает на /usr
# абсолютно — поэтому симлинк /cdt/build → /usr/opt/cdt/4.2.0 сводит пути
# без правки CMakeLists.
- name: Install CDT 4.2.0 toolchain (.deb)
run: |
SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo"
$SUDO apt-get update
# build-essential — для host-компилятора (project() в CMakeLists);
# libz3-4/libtinfo6/libxml2/zlib1g — рантайм бинарей CDT (clang-9,
# ld.lld и т.д.). Пакет cdt объявляет только libcurl4-gnutls-dev,
# поэтому остальные .so ставим явно — в образе dicoop/blockchain они
# были из сборки исходников, из .deb не тянутся.
$SUDO apt-get install -y --no-install-recommends \
curl ca-certificates cmake make build-essential \
libz3-4 libtinfo6 libxml2 zlib1g
curl -fsSL -o /tmp/cdt.deb \
https://git.coopenomics.world/C9S/cdt/releases/download/v4.2.0/cdt_4.2.0-1_amd64.deb
$SUDO apt-get install -y /tmp/cdt.deb
$SUDO mkdir -p /cdt
$SUDO ln -sfn /usr/opt/cdt/4.2.0 /cdt/build
cdt-cpp --version || true
- name: Compile contracts
working-directory: components/contracts
run: |
./build_contracts_cdt.sh "$BUILD_MODE"
echo "--- build/contracts ---"
ls -la build/contracts/
- name: Stage contracts docker context
working-directory: components/contracts
run: |
ROOT="docker/.context"
rm -rf "$ROOT"
mkdir -p "$ROOT/contracts"
# Имена контрактов = аргументы add_contract_build(...) только в
# незакомменченных строках (CMake-комментарий начинается с `#`).
NAMES=$(grep -E '^[[:space:]]*add_contract_build\(' CMakeLists.txt \
| sed -E 's/^[[:space:]]*add_contract_build\(([^)]*)\).*/\1/')
echo "Contracts to package:"
echo "$NAMES"
MANIFEST="$ROOT/contracts/manifest.json"
pack_one() {
local NAME="$1" WASM="$2" ABI="$3"
if [ ! -f "$WASM" ] || [ ! -f "$ABI" ]; then
echo "::warning::Skipping $NAME: artifacts missing ($WASM / $ABI)" >&2
return 1
fi
mkdir -p "$ROOT/contracts/$NAME"
cp "$WASM" "$ROOT/contracts/$NAME/$NAME.wasm"
cp "$ABI" "$ROOT/contracts/$NAME/$NAME.abi"
local SHA_WASM=$(sha256sum "$WASM" | awk '{print $1}')
local SHA_ABI=$(sha256sum "$ABI" | awk '{print $1}')
printf ' {"name":"%s","sha256_wasm":"%s","sha256_abi":"%s"}' \
"$NAME" "$SHA_WASM" "$SHA_ABI"
}
{
printf '{\n'
printf ' "build_mode": "%s",\n' "$BUILD_MODE"
printf ' "git_sha": "%s",\n' "$SHA"
printf ' "tag": "%s",\n' "$TAG_NAME"
printf ' "branch": "%s",\n' "$BRANCH"
printf ' "built_at": "%s",\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
printf ' "contracts": [\n'
FIRST=1
emit() {
entry=$(pack_one "$@") || return 0
[ $FIRST -eq 0 ] && printf ',\n'
FIRST=0
printf '%s' "$entry"
}
for c in $NAMES; do
if [ "$c" = "system" ]; then
# У system многоконтрактная сборка: разносим под-контракты
# eosio.* как полноценные элементы каталога, сам "system"
# как имя пропускаем.
for sub in build/contracts/system/contracts/eosio.*/; do
[ -d "$sub" ] || continue
sub_name=$(basename "$sub")
emit "$sub_name" "$sub/$sub_name.wasm" "$sub/$sub_name.abi"
done
else
emit "$c" "build/contracts/$c/$c.wasm" "build/contracts/$c/$c.abi"
fi
done
printf '\n ]\n'
printf '}\n'
} > "$MANIFEST"
cp docker/Dockerfile "$ROOT/Dockerfile"
cp docker/entrypoint.sh "$ROOT/entrypoint.sh"
chmod +x "$ROOT/entrypoint.sh"
echo "--- manifest.json ---"
cat "$MANIFEST"
- name: Build and push contracts image
working-directory: components/contracts
run: |
# Ретрай push'а: резолвинг registry-1.docker.io на runner'е изредка
# моргает (DNS-таймаут к 127.0.0.53) — 3 попытки с паузой.
dpush() {
local ref="$1" n=1
until docker push "$ref"; do
[ "$n" -ge 3 ] && { echo "::error::docker push $ref не удался после $n попыток"; return 1; }
echo "::warning::docker push $ref упал (попытка $n/3) — повтор через 10с"
n=$((n+1)); sleep 10
done
}
IMAGE="dicoop/contracts"
SHORT_SHA="${SHA::7}"
docker build \
--label "org.opencontainers.image.revision=$SHA" \
--label "org.opencontainers.image.created=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--label "build.mode=$BUILD_MODE" \
-t "$IMAGE:$CONTRACTS_TAG" \
./docker/.context
dpush "$IMAGE:$CONTRACTS_TAG"
docker tag "$IMAGE:$CONTRACTS_TAG" "$IMAGE:$CONTRACTS_TAG-$SHORT_SHA"
dpush "$IMAGE:$CONTRACTS_TAG-$SHORT_SHA"
if [ -n "$CONTRACTS_EXTRA" ]; then
docker tag "$IMAGE:$CONTRACTS_TAG" "$IMAGE:$CONTRACTS_EXTRA"
dpush "$IMAGE:$CONTRACTS_EXTRA"
fi
- name: Verify pushed contracts image
run: |
docker run --rm "dicoop/contracts:$CONTRACTS_TAG" list
echo "---"
docker run --rm "dicoop/contracts:$CONTRACTS_TAG" sha256
# === Этап 2: контейнеры приложений ===
- name: Build and push base image
run: |
dpush() {
local ref="$1" n=1
until docker push "$ref"; do
[ "$n" -ge 3 ] && { echo "::error::docker push $ref не удался после $n попыток"; return 1; }
echo "::warning::docker push $ref упал (попытка $n/3) — повтор через 10с"
n=$((n+1)); sleep 10
done
}
docker build --target runtime -t "dicoop/mono-base:$TAG_NAME" .
dpush "dicoop/mono-base:$TAG_NAME"
if [ "$IS_PROD" = "true" ]; then
docker tag "dicoop/mono-base:$TAG_NAME" dicoop/mono-base:latest
dpush dicoop/mono-base:latest
fi
- name: Build and push service images
run: |
dpush() {
local ref="$1" n=1
until docker push "$ref"; do
[ "$n" -ge 3 ] && { echo "::error::docker push $ref не удался после $n попыток"; return 1; }
echo "::warning::docker push $ref упал (попытка $n/3) — повтор через 10с"
n=$((n+1)); sleep 10
done
}
build_service() {
local SVC="$1" PKG="$2" CMD="$3"
local DOCKERFILE="Dockerfile.$SVC"
{
echo "FROM dicoop/mono-base:$TAG_NAME"
echo "CMD [\"pnpm\", \"-F\", \"$PKG\", \"run\", \"$CMD\"]"
} > "$DOCKERFILE"
docker build -t "dicoop/$SVC:$TAG_NAME" -f "$DOCKERFILE" .
dpush "dicoop/$SVC:$TAG_NAME"
if [ "$IS_PROD" = "true" ]; then
docker tag "dicoop/$SVC:$TAG_NAME" "dicoop/$SVC:latest"
dpush "dicoop/$SVC:latest"
fi
}
build_service desktop '@coopenomics/desktop' start
build_service coopback '@coopenomics/controller' start
# TEMP: cooparser — пока не мигрировали потребителей на dicoop/parser
# из отдельного coopenomics/parser repo.
build_service cooparser '@coopenomics/parser' start
build_service notificator 'coop-notificator' start
build_service notifications '@coopenomics/notifications' sync
# === Этап 3: webhook деплоя ===
- name: Trigger deployment webhook
run: |
curl -X POST "$WEBHOOK_URL" \
-H 'Content-Type: application/json' \
-d "$TAG_NAME"
# ============================================================================
# publish-packages — npm publish (бывший publish-packages.yaml).
# Гейт: не-alpha. Не зависит от ветки.
# Жёстко прибиваем версию pnpm, которой сгенерён lockfile (lockfileVersion 9.0
# = pnpm 9/10). Иначе action-setup может подтянуть другую версию и переписать
# pnpm-lock.yaml → Lerna EUNCOMMIT.
# ============================================================================
publish-packages:
needs: release
if: ${{ !contains(github.ref, '-alpha') }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10.33.0
- uses: actions/setup-node@v4
with:
node-version: 24
registry-url: https://registry.npmjs.org
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm lerna run build
- run: pnpm lerna publish from-package --yes
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
# ============================================================================
# trigger-coopenomics-docs — пересборка сайта C9S/coopenomics.
# Гейт: не-alpha + ветка main.
#
# coopenomics переехал на Gitea (C9S/coopenomics); его publish-docs.yaml
# клонирует mono и пересобирает сайт. Gitea НЕ имеет API для
# repository_dispatch — только workflow_dispatch, поэтому дёргаем целевой
# workflow через Gitea API с PAT (secret DOCS_DISPATCH_TOKEN — префикс GITEA_
# у секретов зарезервирован Gitea, нельзя; права write на C9S/coopenomics).
# Целевой workflow слушает workflow_dispatch с входами
# mono_sha/mono_ref.
# ============================================================================
trigger-coopenomics-docs:
needs: release
if: ${{ !contains(github.ref, '-alpha') && needs.release.outputs.branch == 'main' }}
runs-on: ubuntu-latest
steps:
- name: Trigger coopenomics website rebuild (Gitea workflow_dispatch)
run: |
curl -fsSL -X POST \
-H "Authorization: token ${{ secrets.DOCS_DISPATCH_TOKEN }}" \
-H "Content-Type: application/json" \
-d '{"ref":"master","inputs":{"mono_sha":"${{ github.sha }}","mono_ref":"${{ github.ref }}"}}' \
"${{ github.server_url }}/api/v1/repos/C9S/coopenomics/actions/workflows/publish-docs.yaml/dispatches"
# ============================================================================
# trigger-mono-docs — деплой ВТОРОЙ документации (сайт доков mono) через
# webhook DOCS_DEPLOY_WEBHOOK_URL. Отдельная от coopenomics публикация —
# обе доки уезжают синхронно по релизу. Извлечён из бывшего publish-docs:
# gh-pages на github.com выпилен как мёртвый, остался реальный deploy-webhook
# (приёмник деплоит доки на своей стороне). Гейт: не-alpha + ветка main.
# ============================================================================
trigger-mono-docs:
needs: release
if: ${{ !contains(github.ref, '-alpha') && needs.release.outputs.branch == 'main' }}
runs-on: ubuntu-latest
steps:
- name: Trigger docs deployment webhook
run: |
curl -fsSL -X POST "${{ vars.DOCS_DEPLOY_WEBHOOK_URL }}" \
-H 'Content-Type: application/json' \
-d '{"ref":"${{ github.ref }}","sha":"${{ github.sha }}","branch":"${{ github.ref_name }}"}'
+96
View File
@@ -0,0 +1,96 @@
name: Typecheck
# Валидация TypeScript-типов для desktop (Vue 3 + Quasar) и controller (NestJS).
#
# Зачем это отдельный workflow:
# 1. `quasar build --mode ssr` в release.yaml использует Vite/esbuild и
# vue-tsc отключён через vite-plugin-checker (см. quasar.config.cjs):
# TS-ошибки в .vue/.ts НЕ ловятся при docker build образа desktop.
# 2. У `@coopenomics/controller` нет build-скрипта вовсе — `lerna run build`
# его молча пропускает; в проде `ts-node` стартует и валится на типах
# только в рантайме при загрузке модуля. Кейс PR #392 (rename 1080→1020
# в cooptypes) проскочил именно так.
#
# Триггеры:
# - pull_request на dev — гейт перед мерджем в основную ветку;
# - workflow_call — оставлен для переиспользования, но release.yaml его
# больше НЕ вызывает (гейт из релиза убран как избыточный — типы
# проверяются на PR в dev до того, как код доедет до тэга).
#
# Push в dev/testnet/main НЕ триггерит — намеренно (PR-гейт на dev достаточен).
on:
pull_request:
branches: [dev]
workflow_call:
jobs:
desktop:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10.33.0
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- name: Install workspace
run: pnpm install --frozen-lockfile
# Полный lerna build по всему workspace — как в корневом Dockerfile.
# Не --scope, потому что workspace-пакеты unbuild-типа (sdk, inter,
# notifications, factory, cooptypes, …) экспортируют типы только из
# сгенерированного dist/. Без dist/ tsc/vue-tsc валятся на TS2307
# «Cannot find module». Локально это маскируется ts-node-ом, но в
# CI пути резолвятся через package.json → types → dist/.
# См. Graphiti / run #148 root cause 2026-05-23.
- name: Build all workspace packages
run: pnpm lerna run build
# quasar prepare генерирует .quasar/* (auto-imports d.ts, типы для
# quasar плагинов). Без него vue-tsc ругается на отсутствующие
# глобальные компоненты QBtn/QCard/etc.
- name: Quasar prepare
run: pnpm --filter @coopenomics/desktop run prepare
# vue-tsc проверяет и .ts, и <script lang="ts"> в .vue.
# --skipLibCheck — иначе валится на типах из транзитивных deps
# (Quasar/Vue/Vite сами по себе чистые, но в графе есть legacy).
# Из памяти: на dev-сервере висло ~час из-за конкуренции с quasar dev;
# на чистом ubuntu-latest без живого стека ожидаемо в разы быстрее.
# Если упрётся в OOM/таймаут — fallback на `pnpm typecheck`
# (tsc --noEmit --skipLibCheck без vue-tsc, без SFC шаблонов).
- name: vue-tsc (desktop)
run: pnpm --filter @coopenomics/desktop exec vue-tsc --noEmit --skipLibCheck
controller:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10.33.0
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- name: Install workspace
run: pnpm install --frozen-lockfile
# Полный lerna build по всему workspace — controller импортирует
# @coopenomics/sdk, @coopenomics/inter, @coopenomics/notifications
# помимо cooptypes/factory; их dist/ генерируется unbuild и без
# этого шага tsc валится на 37 TS2307. См. root cause выше.
- name: Build all workspace packages
run: pnpm lerna run build
- name: tsc --noEmit (controller)
run: pnpm --filter @coopenomics/controller run typecheck
+4
View File
@@ -1,4 +1,5 @@
node_modules/
node_modules
lerna-debug.log
components/controller/graph.png
blockchain-data/
@@ -21,3 +22,6 @@ components/reports-standarts/**/СФР_1118018397_*.xml
components/contracts/cpp/ledger2/scripts/out/
.env.testnet
.pnpm-store
schema.gql
.claude
+7
View File
@@ -0,0 +1,7 @@
store-dir=./.pnpm-store
supported-architectures.os[]=darwin
supported-architectures.os[]=linux
supported-architectures.cpu[]=x64
supported-architectures.cpu[]=arm64
supported-architectures.libc[]=glibc
+360
View File
@@ -0,0 +1,360 @@
# mono (coopenomics/mono) — рабочие заметки
Этот файл — общая память агента для пяти чекаутов: `~/mono-ai-1`..`~/mono-ai-5`. Реальный файл лежит в `mono-ai-1/CLAUDE.md`, остальные четыре — симлинки сюда; правки коммитим из mono-ai-1 в ветку `dev`.
## Стек монорепы
| Слой | Где | Технология |
|------|-----|------------|
| Backend (coopback) | `components/controller/` | NestJS 10, TypeScript, TypeORM, GraphQL, EventEmitter2 |
| Frontend (UI пайщика) | **`components/desktop/`** | **Vue 3 + Quasar** в SSR-режиме (`quasar dev --mode ssr`) |
| Контракты | `components/contracts/` | EOSIO/CDT, C++ |
| SDK для frontend | `components/sdk/` | TypeScript, авто-генерится из controller GraphQL schema |
| Parser blockchain | `components/parser/` | TypeScript |
| Boot/orchestration | `components/boot/` | TypeScript |
**Frontend (`components/desktop/`) — FSD структура:** `src/{app,pages,processes,widgets,features,entities,shared,stores,desktops,boot}/`.
Когда план говорит «UI компонент» / «frontend integration» — путь `components/desktop/src/{features|widgets|pages|processes}/<name>/`, расширение `.vue` (composition API + `<script setup lang="ts">`), стили Quasar (QChip/QBtn/QCard/QDialog…), GraphQL через Apollo Client + сгенерированные типы из `components/sdk/`. Dev — `pnpm --filter @coopenomics/desktop run dev` или `devnet` (без SSR).
**НЕ путать:** НЕТ `components/app-cooperative/` — не предлагать. Все frontend-сессии работают в той же монорепе, что и backend. В стеке **никакого React нигде нет**.
## Worktree-политика
**Для mono-ai-4 (базовая ветка `marketplace2`, Стол заказов):** **worktree приветствуется** — изоляция работ + параллельные ветки. Если worktree пуст от `node_modules` и `.env` (pnpm их не дублирует):
```bash
WT=/home/admin/mono-ai-4/.claude/worktrees/<name>
ln -s /home/admin/mono-ai-4/node_modules $WT/node_modules
ln -s /home/admin/mono-ai-4/components/controller/node_modules $WT/components/controller/node_modules
ln -s /home/admin/mono-ai-4/components/controller/.env $WT/components/controller/.env
```
Аналогично для других пакетов, чьи тесты будут запускаться (`components/desktop/node_modules`, `components/sdk/node_modules`). `jest` из bin: `cd $WT/components/controller && ./node_modules/.bin/jest -i <test>` — корректно резолвит ts-jest и подхватывает `.env`.
**Подвох cooptypes:** когда `controller/node_modules` — симлинк на main, пакет внутри `node_modules/cooptypes -> ../../cooptypes` раскрывается **относительно main checkout**. `import { MarketContract } from 'cooptypes'` тянет d.ts из main, не из worktree. Если worktree обновлён, а main позади — TSC падает на отсутствующих типах. **Фикс:** перед TSC в worktree controller'а`git pull` в main checkout до того же коммита (или хотя бы где cooptypes/src синхронен) + `pnpm build` в `main/components/cooptypes/`.
## PR-flow
**Базовая ветка mono-ai-1 — `dev`.** Прямые коммиты в `dev` для мелких фиксов разрешены и не требуют feature-ветки/PR (отменено 2026-05-22 пользователем). Для крупных задач — feature-ветка (`feat/...` или `fix/...`) от dev и PR в dev; merge делает пользователь на GitHub. Push в `main` / `mvp` — по-прежнему только через PR.
**Не stash'ить `-u` при unstaged WIP пользователя.** Это создаёт окно для потери при drop/конфликте. Если нужно временно убрать unstaged — `git stash push -- <конкретные-paths>` либо коммит-в-feature, и только свои файлы. Кейс 2026-05-18: после `git stash -u`+`drop` при конфликте потерял WIP пользователя (infra.ts/config.ts/quasar.config.cjs и др.).
### Story-by-story для проекта «Стол заказов» (mono-ai-4 на marketplace2)
Каждая story из MVP-эпиков «Стол заказов» (`coopenomics/mono`, базовая ветка `marketplace2`, чекаут `~/mono-ai-4`, BMad-spec'и `_blago/.../components/3-minimalnyy-produkt/_bmad-output/`) идёт через PR.
Workflow:
1. Worktree от `marketplace2` на feature-ветке `feat/<E>-<S>-<slug>`.
2. Edits + unit-тесты; tsc + jest должны быть зелёные.
3. Commit + push feature-ветку.
4. `gh pr create --base marketplace2 --head feat/<E>-<S>-<slug>`.
5. Merge **пользователем на GitHub** (не вызывать `gh pr merge` без явной просьбы).
6. После merge — fetch + checkout marketplace2 в основной чекаут, удалить feature-ветку + worktree, запустить e2e / blockchain тесты против обновлённого marketplace2.
### Umbrella-PR на эпик vs цепочка PR
При работе story-by-story в одной feature-area **не плодить отдельные PR на каждую story с одинаковой целевой веткой**. Каждый последующий PR показывает кумулятивный diff (всё, что в head минус то, что уже мёрджнуто в base). Если предыдущие PR ещё не смерджены — в diff вылазят дубликаты файлов всех предыдущих stories.
**Default — umbrella-PR на эпик** (для крупных эпиков с >3 stories): одна `feat/<E>-epic` ветка + один PR; каждая story — отдельный коммит. Worktree последовательно, push после каждой story, PR обновляется. Так закрыт Эпик 1 Стола заказов (umbrella #380).
**Альтернатива — stacked-PRs с `base:<prev-feat>`:** каждый PR таргетится на предыдущую feature-ветку. После merge нижнего GitHub автоматически перетарджетит верхние. Требует дисциплины и тулинга.
**Анти-паттерн:** 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.
В mono-ai-4 **запрещён параллельный режим jest** (worker-pool по умолчанию) — это вешает сервер. Если нужен unit-тест — точечный с `--runInBand`:
```bash
pnpm jest tests/unit/marketplace/marketplace-onboarding-service.test.ts --runInBand
```
`pnpm generate-schema` / `pnpm generate-client`**МОЖНО и НУЖНО запускать когда требуется** (разрешено пользователем явно). Любое изменение GraphQL-сигнатуры резолвера или типов контрактов обязано сопровождаться полным циклом регенерации (generate-schema → generate-client → sdk build, при правках cooptypes — ещё и cooptypes build), чтобы desktop получил актуальные типы. Не оставлять заглушку «до regen».
Перед коммитом достаточно `tsc --noEmit` (быстрый, не блокирует).
## SDK login canon
`@coopenomics/sdk` экспортирует `Client.create({api_url, chain_url, chain_id})` + метод `client.login(email, wif)`. Он сам:
1. Генерит `now` (ISO timestamp).
2. Подписывает приватным ключом (WIF) через eosjs.
3. Зовёт `Mutations.Auth.Login.mutation` с `{email, now, signature}`.
4. Возвращает `{tokens: {access: {token}, refresh: {token}}, account: {username}}`.
**Не дёргать `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
- **User** — обычный пайщик. Базовые потребительские права (заказывать, публиковать оферту, видеть свои данные).
- **Member** — **член совета** (не «член кооператива»!). User-права + read-only admin (видит склад, поток заказов, повестку — но не модерирует и не подписывает финальные действия).
- **Chairman** — председатель. User + admin (модерация, KU/whitelist/витрины, closing signature АПП-приёмки/выдачи, повестка совета на write).
Маппинг core-роли на extension-роль явно: User → orderer + опционально offerer/operator; Chairman → admin (полный write); Member → read-only admin (board_readonly). Пайщик может быть одновременно в нескольких extension-ролях — массив, не enum. Guard'ы в расширениях — локальные сейчас, в Phase 2 переключатся на платформенный CASL.
### `agreements` ссылается на существующий document registry_id
Когда расширение controller'а (marketplace, blagorost, любое следующее) хранит факт подписи документа пайщиком в глобальной on-chain таблице `agreements`, ссылка идёт через **существующий `registry_id` из платформенного реестра документов**, не через отдельный type-string типа `marketplace.cpp.stol-zakazov-v1`.
Поле `document_id` в `agreements` — FK на существующий platform registry. Extension-таблицы `*_onboarding_requirement``document_registry_id` ссылается на существующий ID. API запросов вида «какие документы подписаны member'ом» — `agreementsByMember(member_id, document_id_filter=[...])`.
**Технический долг платформы:** «Договор УХД сейчас не проходит через `agreements`» — отдельная задача core controller'а, вне scope конкретных расширений.
### Трёхуровневый онбординг расширений
Платформенный паттерн, обязателен для всех новых расширений controller'а.
**L1 — Кооператив (one-time):** председатель/совет принимает решение совета о подключении ЦПП, принимается положение ЦПП (статический документ из platform registry, через document factory с подстановкой параметров кооператива), оферта регистрируется в `coop_registration_offers_registry`.
**L2 — Пайщик при вступлении (per-membership):** в registration-flow появляется **выбор** ЦПП. Пайщик отмечает интересующие, document factory рендерит оферту с `{cooperative_params, member_params, agreement_date}`. Подпись пишется в глобальную on-chain `agreements`. Эта подпись **нивелирует** gate на столе расширения.
**L3 — Пайщик на рабочем столе (per-extension first visit):** backend проверяет через `agreementsByMember` — подписана ли оферта ЦПП. ДА → gate не показывается. НЕТ → gate показывается как explicit consent.
При проектировании любого нового расширения — обязательно три истории под три уровня. На MVP допустимо упростить: одинаковый `document_registry_id` для положения и оферты (физически разные документы могут быть в Phase 2).
### Marketplace asset через DI
В сервисах `components/controller/src/extensions/marketplace/**/*.service.ts` запрещён хардкод вида `const ASSET_DECIMALS = 4` / `const ASSET_SYMBOL = 'RUB'`. Decimals и symbol — через DI `MARKETPLACE_ASSET_CONFIG`:
```ts
@Inject(MARKETPLACE_ASSET_CONFIG) private readonly assetConfig: MarketplaceAssetConfig
// assetConfig.symbol, assetConfig.decimals
```
Канон-пример: `marketplace-order-create.service.ts`. См. `marketplace-asset.config.provider.ts` — мапит `config.blockchain.root_govern_symbol` / `root_govern_precision`. Разные среды (mainnet/testnet/dev) имеют разный symbol+precision.
### registry_id=800 (ReturnByAssetStatement) — клиринг, не членские взносы
Marketplace в монорепе живёт в **двух контурах**:
1. **Система клиринга** (старый, не используется) — registry_id=800. Не использовать в новых фичах.
2. **Система членских взносов** (текущий MVP) — документы лежат **рядом** с актами приёма-передачи и ТТН; другая группа registry_id.
Для нового документа Marketplace-членские-взносы: завести новый registry_id рядом с актами/ТТН и проложить цепочку `cooptypes → factory → controller → desktop`:
1. `@coopenomics/cooptypes`: новый тип документа + регистрация в registry.
2. `@coopenomics/factory`: generator (preview-структура + meta).
3. `@coopenomics/controller`: signed-document DTO + verify в сервисе.
4. `@coopenomics/desktop`: подпись через `Classes.Document` с новым registry_id.
## GraphQL каноны (controller + desktop)
### Описания @Field — бизнес-языком
В `@Field({ description })`, `@InputType`, `@ObjectType` нельзя писать «Story 4.1», «Эпик 3», «FR11a», «composite-entity», «dispatch pipeline», «tx_snapshot», «backend deterministic order_hash». Только пользовательский язык: «Идентификатор заказа», «ПВЗ получения», «Кол-во единиц товара». Story-ссылки и инвариант-комменты — только в inline-комментариях внутри сервиса.
### Enum вместо строковых литералов
Запрещены `if (offer.cycle_type === 'volume_based')`, `status: 'ACTIVE'`, `'PENDING_MODERATION'`. Любое поле с фиксированным набором (cycle_type, status, type, kind, role) — TypeScript `enum`, при необходимости `registerEnumType` для GraphQL. В тестах константы — тоже из enum, не дублирующие строки. Перед добавлением сравнения по строке — `enum MarketplaceOfferCycleType` / `MarketplaceOrderStatus` в `domain/entities/*.types.ts`.
### Пагинация — единый паттерн
В controller-resolver'ах пагинация делается единым каноническим паттерном:
- Вход: `@Args('options', { nullable: true }) options?: PaginationInputDTO` (импорт из `~/application/common/dto/pagination.dto.ts`, поля page/limit/sortBy/sortOrder).
- Выход: `createPaginationResult(ItemDTO, 'PaginatedXxx')` + сигнатура `Promise<PaginationResult<T>>` (items / totalCount / totalPages / currentPage).
- Repository принимает `PaginationInputDTO`, сам считает offset/limit/sort через TypeORM `findAndCount`.
Канон: `time-tracker.resolver.ts`, `expenses-management.resolver.ts`, `generation.resolver.ts`. Никаких локальных `{ limit, offset }`.
### Никаких raw GraphQL-строк в desktop
В `components/desktop/src/pages/**/api/index.ts` и аналогах запрещены конструкции:
```ts
const QUERY = `query Foo { ... }`;
await sendPOST('/v1/graphql', { query: QUERY, variables });
```
Даже с комментарием «техдолг до Zeus regen».
Обязательная процедура перед UI-кодом под новую GraphQL-операцию:
1. Добавить/изменить DTO/resolver в `components/controller/src/...` (code-first).
2. `pnpm run generate-schema` в `components/controller/` → пересоздать `controller/schema.gql`.
3. `pnpm run generate-client` в `components/controller/` → graphql-zeus кладёт клиент в `components/sdk/src/zeus/`.
4. `pnpm run build` в `components/sdk/` → unbuild собирает `dist/`.
5. В desktop импортировать `Mutations.<Domain>.<Name>` / `Queries.<Domain>.<Name>` из `@coopenomics/sdk` — типизировано end-to-end.
Если на шаге 1 не хватает поля — добавить и запустить весь цикл; не оставлять заглушку «пока».
### Строгая типизация desktop API из SDK через IInput['data']
Каждый вызов `client.Mutation` / `client.Query` в `components/desktop/src/**/api/index.ts`:
1. Принимает аргументом объект `data: IXxxInput`, где `IXxxInput = Mutations.<Domain>.<Action>.IInput['data']` (или `Queries...`) — тип берётся **прямо из @coopenomics/sdk**, не переописывается.
2. Передаёт в `variables` объект `data` целиком: `{ variables: { data } }`. Запрещено разворачивать поля: `{ variables: { data: { a, b, c } } }`.
Канон — `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 коде **все имена идентификаторов — английские**:
- Имена Vue-компонентов: `BaseInput`, `BaseDialog`, `WalletCard`, `IdentityPanel`.
- Имена файлов и директорий: `shared/ui/BaseInput/BaseInput.vue`.
- CSS-классы: `.base-card`, `.wallet-card__icon`, `.id-panel__hero`.
- SCSS-переменные: `$primary`, `$prog-blagorost`, `$separator-color`.
- TS-типы, интерфейсы, переменные, функции: `interface WalletCardProps`, `function useWalletData()`.
**Why:** Unicode-имена ломают тулчейн (Vite/Webpack резолверы и aliases часто на ASCII-only regex; TypeScript symbol-resolution на не-ASCII нестабильно; ESLint `vue/component-name-in-template-casing` ждёт PascalCase ASCII; импорты `import БазоваяКнопка from '@/shared/ui/БазоваяКнопка'` невыносимы при review).
**Заголовки в .vue, label-ы кнопок, тексты в UI — по-русски** (это user-facing strings). Не путать с правилом «онтологические class_id по-русски» — то про EMP/ТЭМ и blago-документы, не про frontend.
**Кейс 2026-05-18:** при подготовке UX-спецификации для components/desktop ошибочно применил правило русских имён к Vue-компонентам (`БазоваяКнопка`, `БазовоеПолеВвода`); пользователь поправил.
## Стандарты процессов (.standard.yaml) — бизнес-языком
`components/contracts/cpp/**/*.standard.yaml` — документация для методолога/бухгалтера, не для разработчиков контракта. В `purpose`, `description`, `note`, `human` запрещены технические термины:
- никаких «callback», «soviet::exec», «soviet::createagenda», «AUTHORIZE_CALLBACK_SIGNATURE», «type-string», «registry N», «proposed расширение enum'а»;
- никаких «backend formирует», «controller вызывает», «contract отдаёт» — пишем кто что делает на уровне бизнеса (председатель / совет / заказчик / поставщик);
- технические `marketplace::propwroff`-имена в полях `action`/`name`/`triggered_by` оставляем как identifier'ы, но всё человеко-читаемое в `human`/`purpose`/`description` — на бизнес-словаре;
- бухгалтерские проводки `Дт 91 / Кт 10` — допустимы (бухгалтер их понимает);
- если процесс встроен в более общий — ссылаемся на стандарт по имени-человеку («типовой процесс решения совета»), не на техническую реализацию повестки.
Эталоны: `p.mkt.return.standard.yaml`, `p.cap.rid.standard.yaml`, `reg.coop.standard.yaml`. Антипример — `p.mkt.wroff.standard.yaml` в PR #399 review 2026-05-18 (был забит callback-описаниями и «type=mktwroff»).
## Vault & SERVER_SECRET
WIF админ-аккаунта (например `voskhod`) хранится в `vaults` PostgreSQL зашифрованным AES-256-CBC с ключом `sha256(SERVER_SECRET)`. Если `SERVER_SECRET` потом меняли — **старые записи разрушаются**, `decipher.final()` бросает `error:1C800064:Provider routines::bad decrypt`.
**Симптомы:**
- В логе coopback: `[VaultDomainService] Ошибка при получении WIF ключа для пользователя voskhod: bad decrypt`.
- В UI каскад `SignAgreementDialog` не закрывается; SPA отправляет `sendAgreement` корректно, но `wallet::signagree` on-chain не происходит.
- Следствие — `wallet::users[<coop>]` пуст, и любой последующий `is_can_transfer`/трансфер AXON падает на `Отправитель не является участником ЦПП кошелька`.
**Фикс:**
1. Достать живой WIF (для voskhod в dev — `5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3` из boot config.ini, signature-provider).
2. Шифрануть им current SERVER_SECRET (см. `controller/src/utils/aes.ts` — sha256(SECRET) → AES-256-CBC, IV 16 байт, формат `ivHex:cipherHex`).
3. `UPDATE vaults SET wif='...', updated_at=now() WHERE username='voskhod' AND permission='active';`.
4. Перезапуск coopback **не нужен** — он читает каждый раз.
**Дефолт всех mono-репозиториев — `SECRET`.** Если в каком-то `.env` стоит другое — девиация, не норма. Перед re-encrypt'ом vault'а ВСЕГДА сначала смотреть `~/mono-ai-1/components/controller/.env` (или соседнего) — это origin truth для SERVER_SECRET.
**Кейс 2026-05-18 (Эпик 0):** в `mono-ai-5/components/controller/.env` стоял `e2e-fixture-secret-DO-NOT-USE-IN-PROD`, а WIF voskhod в vaults был зашифрован оригиналом — `SECRET`. Фикс — откатить SERVER_SECRET к `SECRET` и подровнять provider'а.
## Capital — фиксы билетов времени (PR #387 merged 2026-05-15)
В `components/controller/src/extensions/capital/` была серия багов в распределении билетов времени, закрытая PR #387 в dev. Три бага:
1. **`recalcDoneEstimatesForContributorProject` / `applyExplicitEstimateToTimeEntries`** раздавал «общий остаток пула» `(estimate total_committed) / N` всем creators, включая того, кто уже закоммитил. Фикс: личная доля `max(0, estimate/N own_committed_estimate)` через общий helper `redistributeIssueEstimateEntries`.
2. **`commitTime` partial split** создавал committed-запись без `entry_type` и `estimate_snapshot` — БД по default'у писала `entry_type='hourly'`, что ломало последующий recalc (фильтрует только `entry_type='estimate'`). Фикс: явно копировать оба поля.
3. **`declineCommit` / `handleDeclineCommit`** меняли только `commit.status='declined'`, но не возвращали `time-entries` в `is_committed=false`. Часы оставались в `total_committed_hours`. Фикс: `revertEntriesForDeclinedCommit` через новые `findCommittedByCommitHash` / `revertCommittedEntriesByCommitHash`.
**При симптомах** «25 ч подтверждено непонятно откуда» / «доступные часы не вернулись после decline» / «парные коммиты на одну работу» — первый чек: `grep -c redistributeIssueEstimateEntries time-tracking.interactor.ts` в контейнере > 5. Если нет — деплой устарел. Если есть, но баг — посмотреть `entry_type` у committed-записей по issue: legacy записи из БАГ #2 могут до сих пор быть с `entry_type='hourly'` для split-результатов от estimate (видны по `commit_hash IS NOT NULL AND entry_type='hourly' AND estimate_snapshot IS NULL` на DONE-задаче с estimate>0). Чинятся UPDATE'ом `entry_type → estimate` с правильным `estimate_snapshot`.
**Legacy data caveat:** `capital_time_entries` с `entry_type='hourly'` + `commit_hash != NULL` могут быть как (а) настоящей hourly работой до установки estimate, так и (б) split-наследием БАГ #2. Различить: если у задачи `estimate>0` и `DONE`, и записи hourly от тех же creators что в estimate-долях — это (б), чинить.
**Edge-case дробных остатков:** при `estimate=2.5` и одном creator, после commit'а 2 ч (`Math.floor < 1` для остатка) остаётся 0.5 ч uncommitted estimate, который никогда не закоммитится. Либо ручной DELETE uncommitted остатка, либо изменить estimate на целое (но estimate в `capital_issues` — on-chain, через UI mutation, не DELETE'ом в БД).
## Стол Заказов MVP — текущее состояние
Проект `1-prilozhenie-stol-zakazov` в blago (coopname voskhod, hash `feabc749…3841f73`). Кооперативная закупка/распределение имущества участка (продукты, товары, услуги); пилот — Красногорск; цель 6 мес: 10 кооперативов / 1200+ пайщиков.
**Локация:** `~/blago/production/1-prilozhenie-stol-zakazov/`.
**Структура артефактов:**
- `_bmad-output/planning-artifacts/` (project-уровень): `prd.md`, `prd-validation-report.md`, `ux-design-specification.md`, `architecture.md` (status:complete 2026-05-12).
- **`components/3-minimalnyy-produkt/_bmad-output/planning-artifacts/epics.md`** (SoT для MVP) — `stepsCompleted=[1,2,3,4]`, `status: complete 2026-05-12`, 65 FRs → 11 эпиков → 57 stories. **НЕ создавать дубль на верхнем уровне `_bmad-output/planning-artifacts/`** — MVP-компонент имеет собственный bmad-output.
- `requirements/` (5 файлов): `04-brif`, `0b-protsessy`, `0f-prd`, `7e-uxui`, `d6-arkhitektura` — дубли _bmad-output (намеренно, см. правило о дублях BMad-артефактов в global memory).
**Locked Decisions L1L9:**
- **L1:** порядок подписей АПП (приёмка поставщик→председатель финально / выдача председатель→заказчик финально).
- **L2:** `o.mkt.payout` baseline `Дт 86 / Кт 51` без счёта 60.
- **L3:** реверты исключены — compensating forward с собственным `operation_id`.
- **L4:** геокарта КУ обязательна на трёх столах; lat/lng в `PlaceDomainEntity`.
- **L5 / L9:** «Системный конструктор ЦПП» → реализован как трёхуровневый онбординг расширений (см. выше).
- **L6:** без отрицательного баланса в MVP (отменяет NFR-R5 PRD).
- **L7:** enum 10 кооп-категорий (овощи/фрукты, молочные, мясо, рыба, хлеб, бакалея, напитки, готовая еда, услуги, прочие).
- **L8:** двухслойная схема подписания — глобальная on-chain `agreements` (SoT) + локальные `*_onboarding_requirement`/`*_onboarding_state` per-extension.
- **L10/L11/L13:** consolidated request backend-only / cycle_type backend variation / pull-модель отчётности.
**Прогресс по эпикам (на 2026-05-15):**
- **Эпик 1** — MERGED в `marketplace2`: PR #368/#370/#371/umbrella #380.
- **Эпик 2 «Сеть ПВЗ»** — MERGED PR #381 (после rebase). Workspace `market-pvz`, KU details + Yandex geocoder, Zeus SDK.
- **Эпик 11 Story 11.1 (Ledger2 canonical actions)** — MERGED PR #375 на C++ стороне. **TS-сторона cooptypes НЕ закрыта**: `cooptypes/src/contracts/marketplace/actions/index.ts` экспортирует только LEGACY клиринговые actions, `interfaces/marketplace.ts` auto-generated из устаревшего ABI. **Блокер Эпика 4** — нужен отдельный pre-эпик PR `feat/S11-1-cooptypes-canonical`: ABI regen через `eosio-abi2ts` из новой `marketplace.abi.json` после `coopcontracts` build, либо ручное добавление 18 canonical actions + canonical tables + canonical interfaces.
- **Эпик 3 «Витрина»** — IN REVIEW. PR #382 (`feat/E3-vitrina`) OPEN MERGEABLE. 6 коммитов (Stories 3.13.5 + BC-sync seam), 67 unit-тестов. Stories 3.4 (CAS counters) — callback-target для Эпика 4 + scaffolding `marketplace-order-sync.service.ts` (skeleton, throw NOT_IMPLEMENTED).
- **Эпики 4-10** — BACKLOG. Issues 598-7 … 598-13 разложены; story-requirements в `598-N-…-requirements/`.
**Открытые фоллоуапы Эпика 1:**
- L3 mutation `marketplaceSignOnboardingOffer` (write-mutation pool + `sndagreement`).
- Source-маркер `'registration_flow'` vs `'extension_gate'` в DTO.
- `marketplaceAcceptCpp` валидация повестки совета — после Эпика 8 (FR40).
- Финальный юридический текст оферты — `todo-tspp-templates.md` (блокирует Stories 1.7/1.9/1.11 в продовой подаче).
- Story 1.5 интеграционный тест против тестовой ноды ЦК — после Эпика 11.
**Реальное состояние brownfield (на marketplace2 @ d98041d):**
| Слой | Готово (membership) | Donor-клиринг (нужно решить судьбу) |
|---|---|---|
| ledger2 C++/TS реестры | 12 операций `o.mkt.*`, счета 10/91, кошельки `w.mkt.member`/`w.mkt.payout`/`w.wal.member`, WalletOp `REVOKE` | — (legacy `o.mkt.supply/recv` удалены) |
| YAML-стандарты `p.mkt.{supply,return,wroff}` | canonical имена actions | — |
| C++ actions (`marketplace.hpp` + 30 .cpp) | **ничего** из canonical | `orderoffer/accept/authcontrib/authreturn/supply/...` ~1660 LOC; legacy Wallet, не ledger2 |
| process-registry в controller | заглушки под membership (`p.mkt.reqst` убран) | — |
| cooptypes/contracts/marketplace TS-actions | нет canonical | 24 клиринговых обёртки |
| controller/application + domain/marketplace | нет canonical | 23 DTO + interactor + resolver + 6 interfaces |
| controller/extensions/marketplace | — | для категорий Ozon (clean arch, 7 TypeORM entities); может пригодиться для каталога Offer'ов |
| controller/extensions/marketplace-cards | — | минимальный каркас карточек |
| desktop/pages/Marketplace | нет canonical | 11 страниц под клиринг; `desktop/extensions/market` пуст |
**Решение 2026-05-12:** стратегии миграции не делаем — donor уже не работает по старой модели; собираем новую membership-модель в существующем контуре, donor-код переписывается / удаляется напрямую без переходников.
В `/bmad-create-architecture` MVP **не вводить adapter-слой и не описывать миграционные пути**. Прямо фиксировать: какие C++ actions/DTO/Vue-страницы из donor-листа удаляются, какие переписываются под canonical (`signsupp/signchair/signiss1/signiss2/acceptbatch/declinebatch/expirecycle/prepship/createorder(новая сигнатура)/cancelorder`), какие сохраняются (shipment/coopstock — если попадают в MVP scope, отдельно проверить).
**Обязательная enforcement-база для backend**`mono-ai-4/components/controller/CLAUDE.md` (Composite-Entity `db/bc/derived`, Write-mutation pool с placeholder/sync_key dedup, ParserClient + Redis Streams, ForkRegistry, ADR-002/008/009/011/012; параметры через `config/blockchain.config.ts`, не magic numbers).
+139
View File
@@ -0,0 +1,139 @@
# Запуск mono dev-стека на macOS
Полная пошаговая инструкция, как поднять backend (`coopback`) + parser (`cooparser`) + блокчейн-ноду + БД на macOS через Docker Desktop. Прошёл — отметь, ниже разобраны типичные грабли.
## TL;DR
```bash
cd ~/dacom-code/foundation/monocoop
docker compose up -d # 1. поднять базу
./scripts/dev-setup-macos.sh # 2. одной командой (см. ниже скрипт)
```
Если скрипта ещё нет — выполняй шаги ниже руками.
## Шаги
### 1. Поднять контейнеры
```bash
docker compose up -d
```
Поднимутся: `node` (NodeOS), `mongo`, `monoredis`, `postgres`, `coopback`, `cooparser`. **MinIO** входит в дефолт. **OpenSearch** — нет (тяжёлый, см. опц. сервисы).
### 2. Проверить чтоб .env'ы указывали на service-имена, а не localhost
`components/controller/.env`:
- `MONGODB_URL=mongodb://mongo:27017/cooperative-x`
- `REDIS_HOST=monoredis`
- `POSTGRES_HOST=postgres`, `POSTGRES_PASSWORD=postgres!23!23`
- `BLOCKCHAIN_RPC=http://node:8888`
- `SIMPLE_EXPLORER_API=http://cooparser:4000`
- `MINIO_ENDPOINT=minio:9000`
- `CHAIN_ID=<реальный chain_id, см. ниже>`
`components/parser/.env`:
- `MONGO_EXPLORER_URI=mongodb://mongo:27017/cooperative-x`
- `REDIS_HOST=monoredis`
- `API=http://node:8888`
- `SHIP=ws://node:8080`
**Почему service-имена:** все контейнеры в bridge-сети `monocoop_default`. Внутри контейнера `127.0.0.1` = сам контейнер, не host. На macOS `network_mode: host` в Docker Desktop работает плохо — используем bridge + service names.
### 3. Узнать живой chain_id
```bash
curl -s http://localhost:8888/v1/chain/get_info | jq -r .chain_id
```
И вписать в `components/controller/.env``CHAIN_ID=...`. **Если chain_id в .env не совпадает с живым — on-chain транзакции упадут на verify, хотя приложение запустится.**
### 4. Если правил .env — пересоздать контейнер (не restart!)
```bash
docker compose up -d --force-recreate --no-deps coopback cooparser
```
`docker restart` **не перечитывает** `env_file`. `--no-deps` — чтобы не пересоздавать БД (потеряются данные).
### 5. Native binding libxmljs2 — пересборка под Linux
На свежем чекауте `pnpm install` запускается на macOS, и `libxmljs2` собирает Mach-O бинарник. В Linux-контейнере он падает:
```
Error: .../xmljs.node: invalid ELF header ERR_DLOPEN_FAILED
```
**Фикс** (один раз после `pnpm install`):
```bash
docker exec monocoop-coopback-1 sh -c '
cd /app/node_modules/.pnpm/libxmljs2@0.37.0/node_modules/libxmljs2 \
&& rm -rf build \
&& PATH="$PWD/node_modules/.bin:$PATH" npm run install
'
docker restart monocoop-coopback-1
```
Проверка — magic bytes Linux ELF:
```bash
docker exec monocoop-coopback-1 sh -c \
'head -c 4 /app/node_modules/.pnpm/libxmljs2@0.37.0/node_modules/libxmljs2/build/Release/xmljs.node | od -c -N 4'
# Должно быть: 177 E L F
# Если cf fa ed fe — это macOS Mach-O, фикс не применился.
```
### 6. Дождаться cold-start coopback (~56 минут)
`ts-node` без cache + DI tree NestJS = долго. Первые логи появляются спустя ~5 минут после старта контейнера.
```bash
docker logs -f monocoop-coopback-1 | grep -E 'NestJS app|Nest application|Error'
```
Готово, когда видишь:
```
Nest application successfully started
NestJS app with Express routes running on port 2998
```
Проверка:
```bash
curl -s -X POST -H 'Content-Type: application/json' \
-d '{"query":"{__typename}"}' http://localhost:2998/v1/graphql
# {"data":{"__typename":"Query"}}
```
## Опциональные сервисы
### OpenSearch (поиск)
```bash
docker compose --profile search up -d opensearch
```
Затем в `components/controller/.env`:
```
OPENSEARCH_ENABLED=true
```
И пересоздать coopback:
```bash
docker compose up -d --force-recreate --no-deps coopback
```
## Грабли (если опять провозился пол-дня)
| Симптом | Причина | Фикс |
|---|---|---|
| `invalid ELF header` | host'овый `pnpm install` положил Mach-O | См. п. 5 — пересборка libxmljs2 в контейнере |
| `MongooseServerSelectionError: ECONNREFUSED 127.0.0.1:27017` | .env на localhost вместо service-имени | См. п. 2 |
| `password authentication failed for user "postgres"` | в .env `POSTGRES_PASSWORD=postgres` вместо `postgres!23!23` | См. п. 2 |
| Coopback ушёл в restart-loop, но рядом есть другой `monocoop-coopback-1` Up | Случайно запустился старый `components/controller/docker-compose.yml` (network_mode: host, без bind-mount, без CMD) | `docker stop coopback && docker rm coopback` |
| `docker restart` не подхватил новые переменные .env | `restart` не перечитывает `env_file` | `docker compose up -d --force-recreate --no-deps <svc>` |
| Coopback зависает на 5+ минут, лога нет | Это нормально для cold-start ts-node | Подождать. CPU должен крутиться ≥50%. Если CPU = 0% — другая проблема |
| `[VaultDomainService] Ошибка при получении WIF... bad decrypt` | SERVER_SECRET не совпадает с тем, чем шифровали WIF в БД | См. CLAUDE.md → раздел Vault & SERVER_SECRET. Дефолт — `SECRET` |
| Транзакции падают на on-chain verify | CHAIN_ID в .env не совпадает с живым | См. п. 3 |
| `Custom endpoint \`minio://9000\` was not a valid URI` на bootstrap | `MINIO_ENDPOINT` без схемы | `MINIO_ENDPOINT=http://minio:9000` (со схемой!) |
| nodeos в restart-loop с `Database dirty flag set` после sleep/crash Docker Desktop | unclean shutdown chain state | `docker stop monocoop-node-1`; `mv blockchain-data/state-history{,.broken-$(date +%s)}`; `docker compose up -d node` — nodeos сам сделает replay |
| Cold-start coopback 10+ минут (вместо обычных 3-5) | ts-node без cache + bind-mount через osxfs + параллельная нагрузка (nodeos replay) | Подождать; в будущем — `tsx` / `ts-node --swc` для dev-режима |
+12 -1
View File
@@ -83,12 +83,23 @@ WORKDIR /app
# ссылка на системный интерпретатор; без него `weasyprint` падает с
# `python: not found`. `procps`/`wget` — для дебага/healthcheck'ов
# из docker-compose.
#
# `ca-certificates` обязателен: node:22-slim его не содержит, а
# native-биндинги типа @livekit/rtc-node (Rust + rustls + rustls-native-certs)
# читают корни ТОЛЬКО из системного store. Без файла
# /etc/ssl/certs/ca-certificates.crt любой TLS handshake из такого
# биндинга падает с "invalid peer certificate: UnknownIssuer" —
# даже для валидной LE-цепочки (Node-стек при этом работает,
# у него свои вшитые корни). Инцидент 2026-05-21: secretary в
# coopback не мог подключиться к wss://chatcooprtc.coopenomics.world.
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
python3 \
libpango-1.0-0 libpangoft2-1.0-0 libpangocairo-1.0-0 libcairo2 \
libffi8 libjpeg62-turbo libopenjp2-7 zlib1g shared-mime-info \
procps wget \
&& rm -rf /var/lib/apt/lists/*
&& rm -rf /var/lib/apt/lists/* \
&& update-ca-certificates
# Python venv с WeasyPrint, готовый к использованию.
COPY --from=builder /venv /venv
@@ -0,0 +1,55 @@
### Blago Manage Transcription Memo
Назначение скилла — собрать краткое содержание (memo) встречи по транскрипции звонка, согласовать его с пользователем и опубликовать в backend через `blago` так, чтобы оно появилось в desktop UI на странице транскрипции звонка (поле «Заметка о звонке»).
Право редактировать memo на сервере есть только у председателя и членов совета — убедись, что активная сессия `blago login` принадлежит подходящей роли.
Перед началом прочитай: `~/.claude/config/blago/config.yaml`. Запомни оттуда `workspace_base`, `active_workspace_env` и путь из `workspaces`, соответствующий `active_workspace_env` — назовём его `workspace_path`.
#### Входной артефакт
Транскрипции звонков лежат в `<workspace_path>/<project-slug>/meetings/<stem>.md`. Имя файла `<stem>` — UTC-метка окончания звонка (`YYYY-MM-DD_HHMMSS_mmm`). Этот файл — pull-only: его текст приходит с сервера и в `blago push` не уходит.
Связь meeting-файла с UUID транскрипции хранится в `.blago/index.json` рабочей копии: запись с `entity_type=call_transcription`, `relative_path` равен пути к meeting-файлу, `entity_hash` — UUID транскрипции. Сам `blago transcription memo` этот UUID резолвит автоматически по пути.
`blago pull` всегда кладёт рядом с meeting-файлом `meetings/<stem>.memo.md` (тип `call_transcription_memo`, `entity_hash = "<uuid>:memo"`). Если на сервере memo пустой — файл создаётся пустым. Это сделано специально, чтобы редактирование и публикация шли в одно действие: открыть существующий sibling, дописать, опубликовать. Никаких отдельных шагов «создать файл» делать не нужно.
#### Формат содержимого `.memo.md`
Жёсткие требования к телу memo (применяй всегда, даже без напоминания пользователя):
1. **Первая строка — резюме одним предложением, длиной ≤150 символов.** Это «о чём вообще был звонок». Без markdown-форматирования (без `#`, `*`, `-`, ссылок, цитат), без переноса строки внутри. Считай длину в символах (Unicode codepoints), а не в словах. Если не помещается — переформулируй до сути, не сокращай прилагательными.
2. Следующая пустая строка-разделитель, затем — развёртка по сути. Сжимай до минимума:
- убирай этикет, разогрев, повторы, «слышно? — слышно», частицы;
- объединяй похожие реплики в один пункт;
- оставляй: о чём договорились, что обсудили, какие решения, какие открытые вопросы и кто за что взялся;
- короткие маркированные списки лучше абзацев; имена/числа/даты сохраняй точно.
3. Markdown отображается в desktop UI редактором (Milkdown через `WrappedEditor`). Можно: заголовки `##`/`###`, списки `-`, инлайн `**жирный**`, ссылки. Не используй декоративный markdown, который ничего не добавляет к смыслу.
#### Процедура
1. Получи от пользователя путь к meeting-файлу (или скажи ему: «дай путь к нужному meeting-файлу»). Прочитай meeting-файл целиком.
2. Открой sibling-файл `meetings/<stem>.memo.md` (он уже создан `blago pull`, возможно пустой). Если файла нет — попроси пользователя выполнить `blago pull` для нужного проекта и повтори.
3. Собери summary по правилам выше: первая строка — одно предложение до 150 символов, дальше пустая строка и сжатая суть. По прямому промпту пользователя адаптируй акценты (что выделить, какие выводы). Если промпта нет — попроси его. Запиши результат в существующий sibling.
4. Покажи пользователю обновлённое содержимое `.memo.md` целиком (или дай явную ссылку на файл). Проверь сам: первая строка — одно предложение, ≤150 символов, без markdown. Если нет — переписывай, пока не выполнится. Спроси, всё ли устраивает. Не публикуй без явного «ок / публикуй / поехали».
5. После подтверждения опубликуй memo на сервер:
```bash
blago transcription memo <workspace_path>/<project-slug>/meetings/<stem>.md
```
По умолчанию команда читает `.memo.md` рядом с указанным meeting-файлом. Альтернативы:
- `blago transcription memo <stem>.md --file path/to/draft.md` — взять текст из произвольного файла;
- `blago transcription memo <UUID> --text "<строка>"` — передать текст строкой и адресовать транскрипцию напрямую по UUID.
6. Команда вернёт строку вида `Memo обновлено на сервере: транскрипция <uuid> (символов: N; источник: …)`. Если сервер ответил ошибкой (например, прав не хватает или сессия истекла) — останови работу, сообщи пользователю текст ошибки и предложи `blago login`.
#### Замечания
- `blago pull` всегда создаёт `meetings/<stem>.memo.md` рядом с каждой meeting-транскрипцией — пустой, если на сервере memo пустой. Файл сразу проиндексирован (`entity_type=call_transcription_memo`), поэтому редактирование→`blago transcription memo` идёт без warning'ов.
- Файл `.memo.md` в `blago push` не уходит (артефакты `meetings/` исключены из staging) — это «локальный канал» вокруг серверного поля `memo`.
- Если ты редактировал sibling локально и не публиковал, а на сервере появилась новая версия — повторный `blago pull` запишет git-style маркеры конфликта (`<<<<<<< blago/local``>>>>>>> blago/remote`); разрулите вручную и опубликуй через `blago transcription memo`.
- Если на сервере memo всё ещё пустой, а у тебя локальный черновик не публиковался — `blago pull` сохранит локальный текст и проиндексирует его как baseline; никакие правки не пропадают.
- Чтобы перезаписать опубликованный memo — повтори ту же команду; сервер хранит одно текущее значение поля `memo`.
- Ограничения на длину больше нет (ни на фронте, ни на бэкенде) — можно публиковать развёрнутые саммари с выводами и пунктами.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@coopenomics/blago-cli",
"version": "2026.5.13-alpha-3",
"version": "2026.5.30-alpha-2",
"description": "CLI синхронизации артефактов Благорост с бэкендом через @coopenomics/sdk",
"type": "module",
"private": true,
+45 -2
View File
@@ -41,6 +41,7 @@ import { runPush } from '../sync/push.js'
import { runClearStaging, runRemove } from '../sync/remove.js'
import { restoreAllFromServer, RESTORE_ALL_PATH_SENTINELS, runRestore } from '../sync/restore.js'
import { runStatus } from '../sync/status.js'
import { runUpdateTranscriptionMemo } from '../sync/update-transcription-memo.js'
import { writeWorkspaceIndexMarkdown } from '../sync/workspace-index.js'
import { error, formatThrownValue, info, success, warn } from '../ui/output.js'
@@ -237,6 +238,48 @@ export async function runCli(argv: string[]): Promise<void> {
},
)
const transcriptionCmd = program
.command('transcription')
.description(
'Управление транскрипциями звонков: публикация краткого содержания (memo) на сервер (отображается в desktop на странице транскрипции).',
)
transcriptionCmd
.command('memo')
.description(
'Опубликовать краткое содержание (memo) для транскрипции звонка через GraphQL-мутацию chatcoopUpdateTranscriptionMemo. <pathOrId>: путь к `meetings/<stem>.md` (id берётся из .blago/index.json, entityType=call_transcription) или UUID транскрипции. Без --text/--file читает sibling-файл `<stem>.memo.md` рядом с meeting. Доступно председателю и членам совета.',
)
.argument(
'<pathOrId>',
'путь к meeting-файлу или UUID транскрипции',
)
.option('--file <path>', 'путь к файлу с текстом memo (markdown)')
.option('--text <inline>', 'текст memo строкой (приоритет над --file и sibling)')
.action(
async (
pathOrId: string,
opts: { file?: string, text?: string },
) => {
const root = requireRoot()
const cfg = await loadConfig(root)
const ctx = await ensureAuthenticatedContext(root, cfg)
const result = await runUpdateTranscriptionMemo(
ctx,
pathOrId,
{ inlineText: opts.text, filePath: opts.file },
startDir(),
)
const sourceLabel = result.memoSource === 'inline'
? '--text'
: result.memoSource === 'file'
? `--file ${result.memoFile ?? ''}`
: `sibling ${result.memoFile ?? ''}`
success(
`Memo обновлено на сервере: транскрипция ${result.transcriptionId} (символов: ${result.memoLength}; источник: ${sourceLabel}).`,
)
},
)
const delCmd = program
.command('del')
.alias('delete')
@@ -319,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 }
}
@@ -9,12 +9,15 @@ export interface CommunicationCursorsFile {
messageLastTsByRoom: Record<string, number>
/** project_hash → ISO instant: транскрипции с endedAt ≤ этого момента уже выгружены */
transcriptionLastEndedExclusiveByProject: Record<string, string>
/** matrixRoomId → ISO instant: транскрипции непроектной комнаты с endedAt ≤ этого момента уже выгружены */
transcriptionLastEndedExclusiveByRoom: Record<string, string>
}
function empty(): CommunicationCursorsFile {
return {
messageLastTsByRoom: {},
transcriptionLastEndedExclusiveByProject: {},
transcriptionLastEndedExclusiveByRoom: {},
}
}
@@ -32,6 +35,11 @@ export async function loadCommunicationCursors(root: string): Promise<Communicat
&& typeof parsed.transcriptionLastEndedExclusiveByProject === 'object'
? { ...parsed.transcriptionLastEndedExclusiveByProject }
: {},
transcriptionLastEndedExclusiveByRoom:
parsed.transcriptionLastEndedExclusiveByRoom !== undefined
&& typeof parsed.transcriptionLastEndedExclusiveByRoom === 'object'
? { ...parsed.transcriptionLastEndedExclusiveByRoom }
: {},
}
}
catch {
@@ -43,7 +43,6 @@ export function projectCommunicationDayToMarkdown(
}
export interface TranscriptionHeaderModel {
matrixRoomId: string
roomId: string
startedAt: Date | string
endedAt: Date | string | null | undefined
@@ -71,7 +70,6 @@ export function renderCallTranscriptionMarkdown(
const header = [
`# Транскрипция звонка`,
``,
`- Matrix room: \`${transcription.matrixRoomId}\``,
`- LiveKit room: \`${transcription.roomId}\``,
`- Начало: ${start.toISOString()}`,
`- Окончание: ${end ? end.toISOString() : '—'}`,
@@ -14,6 +14,8 @@ export type BlagoEntityType =
| 'room_message_day'
/** Файл `meetings/*.md` (транскрипция звонка), только pull с сервера */
| 'call_transcription'
/** Файл `meetings/<stem>.memo.md` — краткое содержание звонка; pull с сервера + публикация через `blago transcription memo` */
| 'call_transcription_memo'
export interface IndexEntry {
entity_type: BlagoEntityType
@@ -3,11 +3,16 @@
import type { AuthenticatedContext } from '../session/index.js'
import type { IndexFile } from './index-store.js'
import * as fs from 'node:fs/promises'
import * as path from 'node:path'
import { createHash } from 'node:crypto'
import { Queries, Zeus } from '@coopenomics/sdk'
import { sha256Hex } from '../lib/hash.js'
import { formatThrownValue, warn } from '../ui/output.js'
import { findByHash, normalizeRelativePath, upsertEntry } from './index-store.js'
import {
loadCommunicationCursors,
@@ -20,7 +25,7 @@ import {
transcriptionMeetingFileStemUtc,
type CommunicationDayLine,
} from './communication-markdown.js'
import { workspaceBasePath, type ProjectPathModel } from './layout.js'
import { generateSlug, workspaceBasePath, type ProjectPathModel } from './layout.js'
import { syncEntityFile } from './sync-entity-file.js'
interface ProjectRowLite {
@@ -42,6 +47,121 @@ function messageDayEntityHash(projectHash: string, utcDate: string): string {
return createHash('sha256').update(`${projectHash}:${utcDate}`, 'utf8').digest('hex')
}
function transcriptionMemoEntityHash(transcriptionId: string): string {
return `${transcriptionId.toLowerCase()}:memo`
}
async function readFileIfExists(abs: string): Promise<string | null> {
try {
return await fs.readFile(abs, 'utf8')
}
catch {
return null
}
}
function normalizeMemoContent(memo: string): string {
if (memo.length === 0) {
return ''
}
return memo.endsWith('\n') ? memo : `${memo}\n`
}
/**
* Гарантирует наличие файла `meetings/<stem>.memo.md` после pull.
*
* Файл создаётся **всегда** (даже если на сервере memo пуст), чтобы пользователь мог редактировать
* sibling «в одно касание», а команда `blago transcription memo` всегда находила его. Конфликты с
* локальными правками разруливаются:
*
* - индекс есть → штатный `syncEntityFile` (он умеет merge-markers «<<<<<<< blago/local …»);
* - индекса нет, файла нет → пишем серверный memo и индексируем (baseline = сервер);
* - индекса нет, файл есть, содержимое совпадает с сервером → просто индексируем (baseline = совпавший контент);
* - индекса нет, файл есть, серверный memo пустой → оставляем локальный черновик и индексируем его (baseline = локальный);
* - индекса нет, файл есть, оба непустые и различаются → пишем merge-markers, индексируем merged.
*/
async function syncTranscriptionMemoFile(params: {
root: string
index: IndexFile
transcriptionId: string
relativePath: string
serverMemo: string
remoteUpdatedAtIso: string
}): Promise<void> {
const { root, index, transcriptionId, relativePath, serverMemo, remoteUpdatedAtIso } = params
const rel = normalizeRelativePath(relativePath)
const abs = path.join(root, rel)
const entityHash = transcriptionMemoEntityHash(transcriptionId)
const serverContent = normalizeMemoContent(serverMemo)
const prev = findByHash(index, 'call_transcription_memo', entityHash)
if (prev) {
await syncEntityFile({
root,
index,
entityType: 'call_transcription_memo',
entityHash,
relativePath: rel,
content: serverContent,
remoteUpdatedAt: remoteUpdatedAtIso,
label: `memo транскрипции ${transcriptionId}`,
})
return
}
await fs.mkdir(path.dirname(abs), { recursive: true })
const local = await readFileIfExists(abs)
if (local === null) {
await fs.writeFile(abs, serverContent, 'utf8')
upsertEntry(index, {
entity_type: 'call_transcription_memo',
entity_hash: entityHash,
relative_path: rel,
remote_updated_at: remoteUpdatedAtIso,
content_etag_local: sha256Hex(serverContent),
})
return
}
if (local === serverContent) {
upsertEntry(index, {
entity_type: 'call_transcription_memo',
entity_hash: entityHash,
relative_path: rel,
remote_updated_at: remoteUpdatedAtIso,
content_etag_local: sha256Hex(local),
})
return
}
if (serverContent.trim().length === 0) {
// Сервер ничего не знает — берём локальный черновик как baseline. Опубликовать его можно
// через `blago transcription memo <meeting-path>`.
upsertEntry(index, {
entity_type: 'call_transcription_memo',
entity_hash: entityHash,
relative_path: rel,
remote_updated_at: remoteUpdatedAtIso,
content_etag_local: sha256Hex(local),
})
return
}
const merged = `<<<<<<< blago/local\n${local}\n=======\n${serverContent}\n>>>>>>> blago/remote\n`
await fs.writeFile(abs, merged, 'utf8')
upsertEntry(index, {
entity_type: 'call_transcription_memo',
entity_hash: entityHash,
relative_path: rel,
remote_updated_at: remoteUpdatedAtIso,
content_etag_local: sha256Hex(merged),
})
warn(
`Конфликт memo транскрипции ${transcriptionId}: локальный черновик и серверная версия различаются. В «${rel}» записаны маркеры слияния («<<<<<<< blago/local» … «>>>>>>> blago/remote»). Оставьте одну версию текста и опубликуйте через «blago transcription memo».`,
)
}
function dateFromUnknown(value: unknown): Date | undefined {
if (value === null || value === undefined) {
return undefined
@@ -63,6 +183,19 @@ async function listRooms(ctx: AuthenticatedContext, projectHash: string) {
return q[Queries.ChatCoop.ListProjectCommunicationRooms.name] ?? []
}
/** Стабильная папка непроектной комнаты в `rooms/`. Системные — фиксированные, комнаты секретаря — slug + хвост id (уникальность). */
function nonProjectRoomFolder(kind: string, matrixRoomId: string, displayLabel: string): string {
if (kind === 'MEMBERS') {
return 'komnata-paishchikov'
}
if (kind === 'COUNCIL') {
return 'komnata-soveta'
}
const slug = generateSlug(displayLabel) || 'komnata'
const shortId = createHash('sha256').update(matrixRoomId, 'utf8').digest('hex').slice(0, 6)
return `${slug}-${shortId}`
}
export async function pullProjectCommunicationArtifacts(
ctx: AuthenticatedContext,
index: IndexFile,
@@ -78,6 +211,7 @@ export async function pullProjectCommunicationArtifacts(
cursors = {
messageLastTsByRoom: {},
transcriptionLastEndedExclusiveByProject: {},
transcriptionLastEndedExclusiveByRoom: {},
}
}
@@ -184,13 +318,16 @@ export async function pullProjectCommunicationArtifacts(
try {
const tKey = row.project_hash
const tExIso = cursors.transcriptionLastEndedExclusiveByProject[tKey]
// Как сообщения с after=0: без курсора — полная выгрузка завершённых транскрипций в meetings/.
// (GitHub-синк при первом запуске только ставит курсор без файлов — для локального зеркала так не делаем.)
// Курсор `transcriptionLastEndedExclusiveByProject` влияет ТОЛЬКО на скачивание meeting.md
// (тяжёлый GetTranscription с сегментами). Sibling-файл `.memo.md` синхронизируется для всех
// COMPLETED-транскрипций каждый pull — поле `memo` приходит в лёгком GetTranscriptions.
const lowerBoundExclusive = tExIso === undefined ? new Date(0) : new Date(tExIso)
interface TranscriptionCandidate {
id: string
endedAt: Date
memo: string
updatedAt: Date | undefined
}
const byId = new Map<string, TranscriptionCandidate>()
for (const roomId of matrixIds) {
@@ -203,18 +340,230 @@ export async function pullProjectCommunicationArtifacts(
if (t.status !== Zeus.TranscriptionStatus.COMPLETED || !end) {
continue
}
if (!(end.getTime() > lowerBoundExclusive.getTime())) {
continue
}
const prev = byId.get(t.id)
if (!prev || end > prev.endedAt) {
byId.set(t.id, { id: t.id, endedAt: end })
byId.set(t.id, {
id: t.id,
endedAt: end,
memo: typeof t.memo === 'string' ? t.memo : '',
updatedAt: dateFromUnknown(t.updatedAt),
})
}
}
}
const candidates = [...byId.values()].sort((a, b) => a.endedAt.getTime() - b.endedAt.getTime())
const allCompleted = [...byId.values()].sort((a, b) => a.endedAt.getTime() - b.endedAt.getTime())
// 1) meeting.md — только новые после курсора (тяжёлый GetTranscription с сегментами).
const newMeetings = allCompleted.filter(c => c.endedAt.getTime() > lowerBoundExclusive.getTime())
let maxEnded: Date | null = null
for (const c of candidates) {
for (const c of newMeetings) {
const packQ = await ctx.client.Query(Queries.ChatCoop.GetTranscription.query, {
variables: { data: { id: c.id } },
})
const pack = packQ[Queries.ChatCoop.GetTranscription.name]
if (!pack?.transcription || pack.transcription.status !== Zeus.TranscriptionStatus.COMPLETED) {
continue
}
const tr = pack.transcription
const startedAt: Date | string = dateFromUnknown(tr.startedAt) ?? (tr.startedAt as Date | string)
const endedAtTr: Date | string | null | undefined
= dateFromUnknown(tr.endedAt) ?? (tr.endedAt as Date | string | null | undefined)
const md = renderCallTranscriptionMarkdown(
{
roomId: String(tr.roomId),
startedAt,
endedAt: endedAtTr,
},
pack.segments.map(s => ({
speakerName: s.speakerName,
text: s.text,
startOffset: s.startOffset,
endOffset: s.endOffset,
})),
)
const stem = transcriptionMeetingFileStemUtc(c.endedAt)
const rel = `${basePath}/meetings/${stem}.md`
const entityHash = c.id.toLowerCase()
await syncEntityFile({
root: ctx.root,
index,
entityType: 'call_transcription',
entityHash,
relativePath: rel,
content: md,
remoteUpdatedAt: toUpdatedIso(c.endedAt),
label: `транскрипция ${c.id}`,
})
if (!maxEnded || c.endedAt > maxEnded) {
maxEnded = c.endedAt
}
}
// 2) sibling .memo.md — для ВСЕХ COMPLETED-транскрипций (бэкфил независимо от курсора).
// Поле tr.memo пришло в лёгком GetTranscriptions, повторных запросов не делаем.
for (const c of allCompleted) {
const stem = transcriptionMeetingFileStemUtc(c.endedAt)
const memoRel = `${basePath}/meetings/${stem}.memo.md`
const memoRemoteUpdatedAt = toUpdatedIso(c.updatedAt ?? c.endedAt)
await syncTranscriptionMemoFile({
root: ctx.root,
index,
transcriptionId: c.id,
relativePath: memoRel,
serverMemo: c.memo,
remoteUpdatedAtIso: memoRemoteUpdatedAt,
})
}
if (maxEnded) {
cursors.transcriptionLastEndedExclusiveByProject[tKey] = maxEnded.toISOString()
}
else if (tExIso === undefined) {
cursors.transcriptionLastEndedExclusiveByProject[tKey] = new Date().toISOString()
}
}
catch (e) {
warn(`Транскрипции звонков, проект ${row.project_hash}: ${formatThrownValue(e)}`)
}
}
try {
await saveCommunicationCursors(ctx.root, cursors)
}
catch (e) {
warn(`Не удалось сохранить курсоры переписки: ${formatThrownValue(e)}`)
}
}
/**
* Pull переписки и транскрипций из комнат ВНЕ проектов Capital (пайщики, совет, комнаты секретаря).
* Раскладка — отдельная верхняя папка `rooms/<folder>/{messages,meetings}/`, чтобы не смешивать с
* проектными `meetings/`. Логика идентична проектной, но bucket = одна комната, курсор транскрипций — по matrixRoomId.
*/
export async function pullNonProjectCommunicationArtifacts(
ctx: AuthenticatedContext,
index: IndexFile,
): Promise<void> {
let rooms: { matrixRoomId: string, displayLabel: string, kind: string }[]
try {
const q = await ctx.client.Query(Queries.ChatCoop.ListNonProjectCommunicationRooms.query, {})
rooms = (q[Queries.ChatCoop.ListNonProjectCommunicationRooms.name] ?? []) as typeof rooms
}
catch (e) {
warn(`Список непроектных комнат (chatcoopListNonProjectCommunicationRooms): ${formatThrownValue(e)}`)
return
}
if (rooms.length === 0) {
return
}
let cursors: CommunicationCursorsFile
try {
cursors = await loadCommunicationCursors(ctx.root)
}
catch (e) {
warn(`Курсоры переписки (комнаты): не удалось прочитать, начинаем с пустых: ${formatThrownValue(e)}`)
cursors = {
messageLastTsByRoom: {},
transcriptionLastEndedExclusiveByProject: {},
transcriptionLastEndedExclusiveByRoom: {},
}
}
for (const room of rooms) {
const folder = nonProjectRoomFolder(room.kind, room.matrixRoomId, room.displayLabel)
const basePath = `rooms/${folder}`
const roomTitle = room.displayLabel || room.matrixRoomId
// Сообщения комнаты — по календарным суткам UTC новее курсора.
try {
const last = cursors.messageLastTsByRoom[room.matrixRoomId]
const afterTs = last ?? 0
const datesQ = await ctx.client.Query(Queries.ChatCoop.ListUtcDatesWithNewRoomMessages.query, {
variables: { data: { matrixRoomId: room.matrixRoomId, afterOriginServerTsExclusive: afterTs } },
})
const dates = (datesQ[Queries.ChatCoop.ListUtcDatesWithNewRoomMessages.name] ?? []).sort()
for (const utcDate of dates) {
const mq = await ctx.client.Query(Queries.ChatCoop.GetRoomMessagesForUtcDate.query, {
variables: { data: { matrixRoomId: room.matrixRoomId, utcDate } },
})
const linesRaw = mq[Queries.ChatCoop.GetRoomMessagesForUtcDate.name] ?? []
const lines: CommunicationDayLine[] = linesRaw.map(m => ({
originServerTs: m.originServerTs,
authorLabel: m.authorLabel,
coopUsername: m.coopUsername,
kind: String(m.kind),
bodyText: m.bodyText,
}))
if (lines.length === 0) {
continue
}
const content = projectCommunicationDayToMarkdown(roomTitle, room.matrixRoomId, utcDate, [
{ displayLabel: room.displayLabel, matrixRoomId: room.matrixRoomId, lines },
])
const rel = `${basePath}/messages/${utcDate}.md`
const entityHash = messageDayEntityHash(room.matrixRoomId, utcDate)
await syncEntityFile({
root: ctx.root,
index,
entityType: 'room_message_day',
entityHash,
relativePath: rel,
content,
remoteUpdatedAt: `${utcDate}T23:59:59.999Z`,
label: `переписка ${utcDate} (${room.matrixRoomId})`,
})
}
const maxQ = await ctx.client.Query(Queries.ChatCoop.GetMaxOriginServerTsForRoom.query, {
variables: { data: { matrixRoomId: room.matrixRoomId } },
})
const maxTs = maxQ[Queries.ChatCoop.GetMaxOriginServerTsForRoom.name] as number | null | undefined
if (maxTs !== undefined && maxTs !== null && Number.isFinite(maxTs)) {
cursors.messageLastTsByRoom[room.matrixRoomId] = maxTs
}
}
catch (e) {
warn(`Переписка Matrix, комната ${room.matrixRoomId} (${roomTitle}): ${formatThrownValue(e)}`)
}
// Транскрипции звонков комнаты + sibling memo.
try {
const tExIso = cursors.transcriptionLastEndedExclusiveByRoom[room.matrixRoomId]
const lowerBoundExclusive = tExIso === undefined ? new Date(0) : new Date(tExIso)
interface TranscriptionCandidate {
id: string
endedAt: Date
memo: string
updatedAt: Date | undefined
}
const byId = new Map<string, TranscriptionCandidate>()
const tq = await ctx.client.Query(Queries.ChatCoop.GetTranscriptions.query, {
variables: { data: { matrixRoomId: room.matrixRoomId, limit: CHATCOOP_TRANSCRIPTIONS_QUERY_LIMIT, offset: 0 } },
})
const list = tq[Queries.ChatCoop.GetTranscriptions.name] ?? []
for (const t of list) {
const end = dateFromUnknown(t.endedAt)
if (t.status !== Zeus.TranscriptionStatus.COMPLETED || !end) {
continue
}
const prev = byId.get(t.id)
if (!prev || end > prev.endedAt) {
byId.set(t.id, {
id: t.id,
endedAt: end,
memo: typeof t.memo === 'string' ? t.memo : '',
updatedAt: dateFromUnknown(t.updatedAt),
})
}
}
const allCompleted = [...byId.values()].sort((a, b) => a.endedAt.getTime() - b.endedAt.getTime())
const newMeetings = allCompleted.filter(c => c.endedAt.getTime() > lowerBoundExclusive.getTime())
let maxEnded: Date | null = null
for (const c of newMeetings) {
const packQ = await ctx.client.Query(Queries.ChatCoop.GetTranscription.query, {
variables: { data: { id: c.id } },
})
@@ -228,7 +577,6 @@ export async function pullProjectCommunicationArtifacts(
= dateFromUnknown(tr.endedAt) ?? (tr.endedAt as Date | string | null | undefined)
const md = renderCallTranscriptionMarkdown(
{
matrixRoomId: String(tr.matrixRoomId),
roomId: String(tr.roomId),
startedAt,
endedAt: endedAtTr,
@@ -257,15 +605,30 @@ export async function pullProjectCommunicationArtifacts(
maxEnded = c.endedAt
}
}
for (const c of allCompleted) {
const stem = transcriptionMeetingFileStemUtc(c.endedAt)
const memoRel = `${basePath}/meetings/${stem}.memo.md`
const memoRemoteUpdatedAt = toUpdatedIso(c.updatedAt ?? c.endedAt)
await syncTranscriptionMemoFile({
root: ctx.root,
index,
transcriptionId: c.id,
relativePath: memoRel,
serverMemo: c.memo,
remoteUpdatedAtIso: memoRemoteUpdatedAt,
})
}
if (maxEnded) {
cursors.transcriptionLastEndedExclusiveByProject[tKey] = maxEnded.toISOString()
cursors.transcriptionLastEndedExclusiveByRoom[room.matrixRoomId] = maxEnded.toISOString()
}
else if (tExIso === undefined) {
cursors.transcriptionLastEndedExclusiveByProject[tKey] = new Date().toISOString()
cursors.transcriptionLastEndedExclusiveByRoom[room.matrixRoomId] = new Date().toISOString()
}
}
catch (e) {
warn(`Транскрипции звонков, проект ${row.project_hash}: ${formatThrownValue(e)}`)
warn(`Транскрипции звонков, комната ${room.matrixRoomId}: ${formatThrownValue(e)}`)
}
}
@@ -273,6 +636,6 @@ export async function pullProjectCommunicationArtifacts(
await saveCommunicationCursors(ctx.root, cursors)
}
catch (e) {
warn(`Не удалось сохранить курсоры переписки: ${formatThrownValue(e)}`)
warn(`Не удалось сохранить курсоры переписки (комнаты): ${formatThrownValue(e)}`)
}
}
+3 -1
View File
@@ -28,7 +28,7 @@ import {
storyFileRelativePath,
workspaceBasePath,
} from './layout.js'
import { pullProjectCommunicationArtifacts } from './pull-communication.js'
import { pullNonProjectCommunicationArtifacts, pullProjectCommunicationArtifacts } from './pull-communication.js'
import { scaffoldBmadWorkspacesAfterPull } from './scaffold-bmad-workspace.js'
import { syncEntityFile } from './sync-entity-file.js'
import { writeWorkspaceIndexMarkdown } from './workspace-index.js'
@@ -329,6 +329,8 @@ export async function runPull(ctx: AuthenticatedContext, options: RunPullOptions
await pullProjectCommunicationArtifacts(ctx, index, allProjects, projectByHash)
await pullNonProjectCommunicationArtifacts(ctx, index)
await scaffoldBmadWorkspacesAfterPull(ctx, allProjects, projectByHash)
await saveIndex(ctx.root, index)
+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
View File
@@ -367,7 +367,6 @@ export async function runRestore(ctx: AuthenticatedContext, userPath: string): P
if (entry.entity_type === 'call_transcription') {
interface TranscriptionRestorePack {
transcription: {
matrixRoomId: string
roomId: string
startedAt: Date | string
endedAt: Date | string | null | undefined
@@ -394,7 +393,6 @@ export async function runRestore(ctx: AuthenticatedContext, userPath: string): P
}
const content = renderCallTranscriptionMarkdown(
{
matrixRoomId: tr.matrixRoomId,
roomId: tr.roomId,
startedAt: tr.startedAt,
endedAt: tr.endedAt,
@@ -4,11 +4,34 @@ import type { BlagoEntityType, IndexFile } from './index-store.js'
import * as fs from 'node:fs/promises'
import * as path from 'node:path'
import { parseBlagoMarkdown, serializeBlagoMarkdown } from '../format/index.js'
import { sha256Hex } from '../lib/hash.js'
import { warn } from '../ui/output.js'
import { findByHash, normalizeRelativePath, upsertEntry } from './index-store.js'
/**
* Канонический вид для сравнения «есть ли реальный конфликт», без волатильных
* серверных меток времени. updated_at/created_at сервер бьёт при дочерних мутациях
* (создание/удаление issue/story у родителя), и они попадают и во frontmatter, и в
* content_etag_local. Из-за этого raw-sha файла расходится с etag, локальный файл
* считается «грязным», а pull при изменившемся remote_updated_at пишет маркеры слияния —
* хотя содержательно текст совпадает. Сравнение в каноне (без updated_at/created_at)
* отличает настоящую правку от чистого bump'а времени.
*/
function canonicalForCompare(raw: string): string {
try {
const parsed = parseBlagoMarkdown(raw)
const data = { ...parsed.data }
delete data.updated_at
delete data.created_at
return serializeBlagoMarkdown(data, parsed.body)
}
catch {
return raw
}
}
async function ensureDirForFile(absFile: string): Promise<void> {
await fs.mkdir(path.dirname(absFile), { recursive: true })
}
@@ -139,6 +162,21 @@ export async function syncEntityFile(params: {
const dirty
= current !== null && current !== undefined && sha256Hex(current) !== prev.content_etag_local
if (dirty && remoteUpdatedAt !== prev.remote_updated_at) {
// Реальный конфликт — только если содержимое расходится вне волатильных меток времени.
// Если локальный и серверный тексты совпадают по канону (отличие лишь в updated_at/created_at),
// это не конфликт: принимаем серверную версию и лечим etag, без маркеров слияния.
if (current !== null && current !== undefined && canonicalForCompare(current) === canonicalForCompare(content)) {
await ensureDirForFile(absNew)
await fs.writeFile(absNew, content, 'utf8')
upsertEntry(index, {
entity_type: entityType,
entity_hash: entityHash,
relative_path: rel,
remote_updated_at: remoteUpdatedAt,
content_etag_local: sha256Hex(await fs.readFile(absNew, 'utf8')),
})
return
}
const merged = wrapMergeConflictMarkers(current ?? '', content)
await ensureDirForFile(absNew)
await fs.writeFile(absNew, merged, 'utf8')
@@ -0,0 +1,195 @@
// Публикация поля memo транскрипции на сервер (мутация chatcoopUpdateTranscriptionMemo).
//
// Связь meeting-файла с UUID транскрипции: `.blago/index.json` хранит entity_type=call_transcription
// и entity_hash=<uuid в lower-case> для каждого `meetings/<stem>.md` (см. pull-communication.ts).
//
// Текст memo берётся:
// 1) --text "<inline>" — приоритет;
// 2) --file <path> — читать markdown из файла;
// 3) sibling .memo.md — если <pathOrId> это meeting-файл, берём `<meetings>/<stem>.memo.md` рядом.
import * as fs from 'node:fs/promises'
import * as path from 'node:path'
import { Mutations } from '@coopenomics/sdk'
import type { AuthenticatedContext } from '../session/index.js'
import { sha256Hex } from '../lib/hash.js'
import {
findByRelativePath,
type IndexFile,
loadIndex,
normalizeRelativePath,
saveIndex,
upsertEntry,
} from './index-store.js'
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
export interface UpdateTranscriptionMemoOptions {
readonly inlineText?: string
readonly filePath?: string
}
export interface UpdateTranscriptionMemoResult {
readonly transcriptionId: string
readonly memoLength: number
readonly memoSource: 'inline' | 'file' | 'sibling'
readonly memoFile?: string
}
/** Пара значений: id транскрипции для мутации + опциональный «meeting абсолютный путь» для sibling-резолва. */
interface ResolvedTranscription {
readonly id: string
readonly meetingAbsPath?: string
}
function resolveTranscription(
ctxRoot: string,
cwd: string,
pathOrId: string,
index: IndexFile,
): ResolvedTranscription {
const trimmed = pathOrId.trim()
if (UUID_RE.test(trimmed)) {
return { id: trimmed.toLowerCase() }
}
const abs = path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed)
const rel = path.relative(ctxRoot, abs)
if (rel.startsWith('..') || path.isAbsolute(rel)) {
throw new Error(
`Путь «${pathOrId}» вне рабочей копии blago (${ctxRoot}). Передайте путь относительно корня копии или UUID транскрипции.`,
)
}
const entry = findByRelativePath(index, normalizeRelativePath(rel))
if (!entry) {
throw new Error(
`В .blago/index.json нет записи для «${rel}». Выполните «blago pull», чтобы подтянуть транскрипции.`,
)
}
if (entry.entity_type !== 'call_transcription') {
throw new Error(
`Файл «${rel}» имеет тип «${entry.entity_type}», а не «call_transcription». Передайте путь к meeting-файлу или UUID.`,
)
}
return { id: entry.entity_hash, meetingAbsPath: abs }
}
function buildSiblingMemoPath(meetingAbsPath: string): string {
const dir = path.dirname(meetingAbsPath)
const ext = path.extname(meetingAbsPath)
const stem = path.basename(meetingAbsPath, ext)
return path.join(dir, `${stem}.memo.md`)
}
async function readMemoText(
cwd: string,
resolved: ResolvedTranscription,
options: UpdateTranscriptionMemoOptions,
): Promise<{ text: string, source: UpdateTranscriptionMemoResult['memoSource'], file?: string }> {
if (typeof options.inlineText === 'string') {
return { text: options.inlineText, source: 'inline' }
}
if (options.filePath !== undefined && options.filePath.length > 0) {
const abs = path.isAbsolute(options.filePath) ? options.filePath : path.resolve(cwd, options.filePath)
const text = await fs.readFile(abs, 'utf8')
return { text, source: 'file', file: abs }
}
if (!resolved.meetingAbsPath) {
throw new Error(
'Не указан текст memo. Передайте --text "<строка>" или --file <путь>, либо первым аргументом — путь к meeting-файлу (тогда читается `<stem>.memo.md` рядом).',
)
}
const siblingAbs = buildSiblingMemoPath(resolved.meetingAbsPath)
try {
const text = await fs.readFile(siblingAbs, 'utf8')
return { text, source: 'sibling', file: siblingAbs }
}
catch {
throw new Error(
`Рядом с «${path.basename(resolved.meetingAbsPath)}» нет файла «${path.basename(siblingAbs)}». Выполните «blago pull» (он создаст sibling-файл; пустой, если на сервере memo пустой), затем отредактируйте и повторите команду — либо передайте --file / --text.`,
)
}
}
function toIso(v: unknown, fallback: Date): string {
if (v instanceof Date) {
return v.toISOString()
}
if (typeof v === 'string' || typeof v === 'number') {
const d = new Date(v)
if (!Number.isNaN(d.getTime())) {
return d.toISOString()
}
}
return fallback.toISOString()
}
/**
* После успешной мутации записываем `meetings/<stem>.memo.md` рядом с meeting-файлом и
* заносим запись в `.blago/index.json` с типом `call_transcription_memo`, чтобы:
* - следующий `blago pull` видел индексированный файл и шёл по штатной ветке syncEntityFile;
* - `blago push` его не подхватывал — он внутри `meetings/`, исключённого через `pull-only-paths`.
* Это шаг только при наличии meeting-пути (sibling-резолв из <pathOrId>); UUID-режим без файла — пропускаем.
*/
async function persistSiblingAndIndex(
ctx: AuthenticatedContext,
resolved: ResolvedTranscription,
memoText: string,
remoteUpdatedAtIso: string,
): Promise<{ siblingPath: string } | null> {
if (!resolved.meetingAbsPath) {
return null
}
const ext = path.extname(resolved.meetingAbsPath)
const stem = path.basename(resolved.meetingAbsPath, ext)
const siblingAbs = path.join(path.dirname(resolved.meetingAbsPath), `${stem}.memo.md`)
const content = memoText.endsWith('\n') ? memoText : `${memoText}\n`
await fs.mkdir(path.dirname(siblingAbs), { recursive: true })
await fs.writeFile(siblingAbs, content, 'utf8')
const rel = path.relative(ctx.root, siblingAbs)
if (rel.startsWith('..') || path.isAbsolute(rel)) {
return { siblingPath: siblingAbs }
}
const index = await loadIndex(ctx.root)
upsertEntry(index, {
entity_type: 'call_transcription_memo',
entity_hash: `${resolved.id}:memo`,
relative_path: normalizeRelativePath(rel),
remote_updated_at: remoteUpdatedAtIso,
content_etag_local: sha256Hex(content),
})
await saveIndex(ctx.root, index)
return { siblingPath: siblingAbs }
}
export async function runUpdateTranscriptionMemo(
ctx: AuthenticatedContext,
pathOrId: string,
options: UpdateTranscriptionMemoOptions,
cwd: string,
): Promise<UpdateTranscriptionMemoResult> {
const index = await loadIndex(ctx.root)
const resolved = resolveTranscription(ctx.root, cwd, pathOrId, index)
const memo = await readMemoText(cwd, resolved, options)
const mutationName = Mutations.ChatCoop.UpdateTranscriptionMemo.name
const response = await ctx.client.Mutation(Mutations.ChatCoop.UpdateTranscriptionMemo.mutation, {
variables: { data: { id: resolved.id, memo: memo.text } },
})
const row = response[mutationName] as { updatedAt?: unknown } | undefined
if (!row) {
throw new Error('Сервер вернул пустой ответ на chatcoopUpdateTranscriptionMemo.')
}
const remoteUpdatedAtIso = toIso(row.updatedAt, new Date())
const persisted = await persistSiblingAndIndex(ctx, resolved, memo.text, remoteUpdatedAtIso)
return {
transcriptionId: resolved.id,
memoLength: memo.text.length,
memoSource: memo.source,
memoFile: persisted?.siblingPath ?? memo.file,
}
}
+2 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@coopenomics/boot",
"type": "module",
"version": "2026.5.13-alpha-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",
+22 -13
View File
@@ -1,6 +1,7 @@
#!/bin/bash
# Загружаем per-instance конфиг из корня репо.
# Загружаем per-instance конфиг из корня репо (для CHAIN_URL/MONGODB_URL/API_URL,
# которые читают TS-код boot и шелл-скрипты networks.sh/preactivate.sh).
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
if [ -f "$ROOT_DIR/.env" ]; then
set -a
@@ -9,26 +10,34 @@ if [ -f "$ROOT_DIR/.env" ]; then
set +a
fi
# Останавливаем и удаляем контейнеры вместе с volumes
echo "Останавливаем и удаляем контейнеры с volumes..."
docker compose down -v mongo postgres cooparser || 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
# Удаляем тома баз данных для чистого старта (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
sudo rm -rf ../blockchain-data/
docker run --rm -v "$(cd .. && pwd)/blockchain-data:/d" alpine sh -c 'rm -rf /d/* /d/.[!.]* 2>/dev/null || true'
# Пересоздаем и запускаем базы данных
# Пересоздаем и запускаем базы данных + Redis (monoredis).
# monoredis ОБЯЗАТЕЛЕН: без него coopback падает на старте с
# `getaddrinfo EAI_AGAIN monoredis` → MaxRetriesPerRequestError → nodemon crash.
echo "Пересоздаем и запускаем базы данных..."
docker compose up -d mongo postgres
docker compose up -d mongo postgres monoredis
# Ждем готовности MongoDB
# Ждем готовности MongoDB (standalone, ping вместо ожидания PRIMARY).
echo "Ждем готовности MongoDB..."
until docker compose exec -T mongo mongosh --eval "db.adminCommand('ping')" --quiet > /dev/null 2>&1; do
until docker compose exec -T mongo mongosh --quiet --eval "db.adminCommand({ping:1}).ok" > /dev/null 2>&1; do
echo "MongoDB еще не готов, ждем..."
sleep 2
done
@@ -42,7 +51,7 @@ until docker compose exec -T postgres pg_isready -U postgres -d voskhod > /dev/n
done
echo "PostgreSQL готов!"
# Запускаем boot процесс
# Запускаем boot процесс (clean: только программы Благорост/маркетплейс)
echo "Запускаем boot процесс..."
pnpm run boot:clean
@@ -51,6 +60,6 @@ echo "Запускаем parser..."
docker compose up -d cooparser
echo "Запускаем контроллер..."
docker compose restart coopback || true
docker compose up -d --force-recreate coopback || true
echo "Перезапуск завершен!"
+37 -14
View File
@@ -1,6 +1,7 @@
#!/bin/bash
# Загружаем per-instance конфиг из корня репо.
# Загружаем per-instance конфиг из корня репо (для CHAIN_URL/MONGODB_URL/API_URL,
# которые читают TS-код boot и шелл-скрипты networks.sh/preactivate.sh).
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
if [ -f "$ROOT_DIR/.env" ]; then
set -a
@@ -9,30 +10,32 @@ if [ -f "$ROOT_DIR/.env" ]; then
set +a
fi
# Останавливаем контроллер перед очисткой данных
echo "Останавливаем контроллер..."
docker compose down coopback || true
# Останавливаем и удаляем контейнеры вместе с volumes
echo "Останавливаем и удаляем контейнеры с volumes..."
docker compose down -v mongo postgres cooparser || true
docker compose down -v mongo postgres monoredis cooparser coopback || true
# Останавливаем blockchain контейнер перед удалением данных
echo "Останавливаем blockchain контейнер..."
docker compose stop node || true
# Удаляем blockchain data
# Удаляем blockchain data.
# Контейнерный wipe (alpine под root) стирает данные независимо от их владельца —
# без sudo на любой ноде (на Pi нет passwordless sudo; на проде nodeos пишет
# данные под root). Единый способ с reboot.sh / clean_reboot.sh.
echo "Удаляем blockchain data..."
# sudo chmod -R 755 ../blockchain-data/ 2>/dev/null || true
sudo rm -rf ../blockchain-data/
docker run --rm -v "$(cd .. && pwd)/blockchain-data:/d" alpine sh -c 'rm -rf /d/* /d/.[!.]* 2>/dev/null || true'
# Пересоздаем и запускаем базы данных
# Пересоздаем и запускаем базы данных + Redis (monoredis).
# monoredis ОБЯЗАТЕЛЕН: без него coopback падает на старте с
# `getaddrinfo EAI_AGAIN monoredis` → MaxRetriesPerRequestError → nodemon crash,
# и провайдер не может взять org-данные partner1 (PROVIDER_URL=coopback:2998).
echo "Пересоздаем и запускаем базы данных..."
docker compose up -d mongo postgres
docker compose up -d mongo postgres monoredis
# Ждем готовности MongoDB
# Ждем готовности MongoDB (standalone, ping вместо ожидания PRIMARY).
echo "Ждем готовности MongoDB..."
until docker compose exec -T mongo mongosh --eval "db.adminCommand('ping')" --quiet > /dev/null 2>&1; do
until docker compose exec -T mongo mongosh --quiet --eval "db.adminCommand({ping:1}).ok" > /dev/null 2>&1; do
echo "MongoDB еще не готов, ждем..."
sleep 2
done
@@ -46,7 +49,7 @@ until docker compose exec -T postgres pg_isready -U postgres -d voskhod > /dev/n
done
echo "PostgreSQL готов!"
# Запускаем boot процесс
# Запускаем boot процесс (расширенный: совет + пайщики; partner1 — при EXTRA_RENT=1)
echo "Запускаем boot процесс..."
pnpm run boot:extra
@@ -55,6 +58,26 @@ echo "Запускаем parser..."
docker compose up -d cooparser
echo "Запускаем контроллер..."
docker compose up -d coopback
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 "Перезапуск завершен!"
+3 -3
View File
@@ -21,15 +21,15 @@ docker compose stop node || true
# Удаляем blockchain data
echo "Удаляем blockchain data..."
# sudo chmod -R 755 ../blockchain-data/ 2>/dev/null || true
sudo rm -rf ../blockchain-data/
docker run --rm -v "$(cd .. && pwd)/blockchain-data:/d" alpine sh -c 'rm -rf /d/* /d/.[!.]* 2>/dev/null || true'
# Пересоздаем и запускаем базы данных
echo "Пересоздаем и запускаем базы данных..."
docker compose up -d mongo postgres monoredis
# Ждем готовности MongoDB
# Ждем готовности MongoDB (standalone, ping вместо ожидания PRIMARY).
echo "Ждем готовности MongoDB..."
until docker compose exec -T mongo mongosh --eval "db.adminCommand('ping')" --quiet > /dev/null 2>&1; do
until docker compose exec -T mongo mongosh --quiet --eval "db.adminCommand({ping:1}).ok" > /dev/null 2>&1; do
echo "MongoDB еще не готов, ждем..."
sleep 2
done
+90 -51
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. Контракт может быть
@@ -291,31 +315,46 @@ export default class Blockchain {
async activateFeature(feature: Feature) {
await this.update_pass_instance()
await this.api.transact(
{
actions: [
{
account: 'eosio',
name: 'activate',
authorization: [
{
actor: 'eosio',
permission: 'active',
try {
await this.api.transact(
{
actions: [
{
account: 'eosio',
name: 'activate',
authorization: [
{
actor: 'eosio',
permission: 'active',
},
],
data: {
feature_digest: feature.hash,
},
],
data: {
feature_digest: feature.hash,
},
},
],
},
{
blocksBehind: 3,
expireSeconds: 30,
},
)
],
},
{
blocksBehind: 3,
expireSeconds: 30,
},
)
console.log('Фича активирована: ', feature.name)
console.log('Фича активирована: ', feature.name)
}
catch (e: any) {
// На уже забутстрапленном (не обнулённом) чейне протокол-фича уже активна —
// nodeos отдаёт protocol_feature_exception (code 3250000). Это не повод
// ронять весь boot: фича на месте, нужный результат достигнут, продолжаем.
const errName = e?.json?.error?.name
const errCode = e?.json?.error?.code
const msg = e instanceof Error ? e.message : String(e)
if (errName === 'protocol_feature_exception' || errCode === 3250000 || /already activated/i.test(msg)) {
console.warn(`Фича уже активирована, пропускаю: ${feature.name}`)
return
}
throw e
}
}
async createToken(params: TokenContract.Interfaces.ICreate) {
@@ -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"
}
+19 -9
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,18 +13,29 @@ 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() {
const blockchain = await startInfra()
await installInitialData(blockchain, true) // Создать расширенный совет (устанавливает статус 'active' в MongoDB)
await installExtraData(blockchain) // Добавить дополнительных пайщиков
// installExtraData регистрирует partner1 как coop с auto-approve от провайдера —
// это и есть программная on-chain активация, которая триггерит аренду VM в
// провайдере (PENDING→RENT). boot:extra используется НЕ только для аренды
// сервера (например, просто пересев совета/чейна для других задач), поэтому
// провижининг partner1 включается ТОЛЬКО под флагом EXTRA_RENT=1.
if (process.env.EXTRA_RENT === '1') {
console.log('EXTRA_RENT=1 → installExtraData: провижининг partner1 (триггер аренды)')
await installExtraData(blockchain) // Регистрируем partner1 как coop (active)
}
else {
console.log('EXTRA_RENT не задан → пропускаем провижининг partner1 (аренда не запускается)')
}
console.log('Инициализируем статус системы в PostgreSQL')
await initSystemStatus() // Устанавливает статус 'active' в PostgreSQL
+14 -8
View File
@@ -4,6 +4,8 @@ import config, { GOVERN_SYMBOL, SYMBOL } from '../configs'
import Blockchain from '../blockchain'
import { generateRandomSHA256 } from '../utils/randomHash'
import { processLastDecision } from '../tests/soviet/processLastDecision'
import { signProgramAgreement } from './sign-program-agreement'
import { walletDraftId, walletProgramId } from '../tests/capital/consts'
const test_hash
= '157192b276da23cc84ab078fc8755c051c5f0430bf4802e55718221e6b76c777'
@@ -156,13 +158,17 @@ export class CooperativeClass {
console.log('Отправляем подписанное положение о ЦПП Кошелька оператору')
await this.blockchain.sendAgreement({
coopname: config.provider,
administrator: config.provider,
username: username!,
agreement_type: 'wallet',
// После Эпика 2 / компонента 48 soviet::sndagreement отказывается на
// program_id > 0; программные соглашения подписываются через
// wallet::signagree (auth: coopname@active).
await signProgramAgreement(
this.blockchain,
config.provider,
username!,
walletProgramId,
walletDraftId,
document,
})
)
console.log('Переводим аккаунт в кооператив')
@@ -172,8 +178,8 @@ export class CooperativeClass {
params: {
is_cooperative: true,
coop_type: 'conscoop',
announce: 'Тестовый кооператив',
description: 'Тестовое описание',
announce: 'voskhod-dev.coopenomics.world',
description: 'Восход — тестовый кооператив-оператор (dev-стенд)',
initial: `100.0000 ${config.token.govern_symbol}`,
minimum: `300.0000 ${config.token.govern_symbol}`,
org_initial: `1000.0000 ${config.token.govern_symbol}`,
+195 -4
View File
@@ -1,5 +1,9 @@
import { randomUUID } from 'node:crypto'
import { existsSync, readFileSync } from 'node:fs'
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import axios from 'axios'
import ecc from 'eosjs-ecc'
import { Generator, Registry } from '@coopenomics/factory'
import type { Cooperative } from 'cooptypes'
import { DraftContract } from 'cooptypes'
@@ -11,7 +15,12 @@ import { sleep } from '../utils'
import { generateRandomSHA256 } from '../utils/randomHash'
import { initUsersInPostgres, initVaultInPostgres } from '../postgres-init'
import { CooperativeClass } from './cooperative'
import { generateRandomSHA256 } from '../utils/randomHash'
import { signProgramAgreement } from './sign-program-agreement'
import { fakeDocument } from '../tests/shared/fakeDocument'
import { walletDraftId, walletProgramId } from '../tests/capital/consts'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
export async function startInfra() {
// инициализируем инстанс с ключами
@@ -572,7 +581,189 @@ export async function installInitialData(blockchain: Blockchain, isExtended = fa
}
export async function installExtraData(blockchain: Blockchain) {
// В расширенном режиме пайщики уже добавлены в installInitialData
// Здесь можно добавить дополнительную логику инициализации если потребуется
console.log('Дополнительная инициализация для расширенного режима выполнена')
// Регистрируем partner1 как coop с авто-approve от провайдера (Восхода).
// Без этого реальный UI flow требует ручной активации председателем —
// провайдер не подтянет partner1 в свою DB пока в registrator.coops его
// нет. На dev-стенде хотим иметь готовый partner1 active сразу после
// `reboot:extra`, чтобы прыгать из 0 → 8 (Hostkey rent → install → ACTIVE)
// одним нажатием.
//
// Упрощённый flow (без sendAgreement через soviet — он отказывается на
// program_id > 0 с проверкой «подписывается через wallet::signagree»).
// Минимум для попадания в registrator.coops со status=active:
// newaccount → reguser(type=organization) → regcoop → stcoopstatus(active)
console.log('\n=== installExtraData: регистрируем partner1 как coop (active) ===')
const username = 'partner1'
// Берём WIF из docs-harness/state/cooperatives/partner1.json, если он там есть.
// Без этого boot:extra на каждом запуске генерирует случайный privateKey,
// а harness 08 (chairman install wizard /partner1/install) подаёт WIF из
// фикстуры на шаге 1 — они расходятся, startInstall mutation не принимает
// ключ, wizard зависает на шаге 1 без видимой ошибки. На репозиторий
// фикстура коммит'ится разработчиком вручную; здесь только читаем.
let seededKeys: { privateKey: string; publicKey: string } | undefined
try {
const fixturePath = resolve(
__dirname,
'../../../docs-harness/state/cooperatives/partner1.json',
)
if (existsSync(fixturePath)) {
const fx = JSON.parse(readFileSync(fixturePath, 'utf-8'))
if (fx?.wif) {
// publicKey не хранится в фикстуре — деривим из приватного.
const pub = fx.publicKey || await ecc.privateToPublic(fx.wif)
seededKeys = { privateKey: fx.wif, publicKey: pub }
console.log(`✓ partner1: используем seeded WIF из фикстуры (pub=${pub.slice(0, 12)}…)`)
}
}
} catch (e: any) {
console.warn(`⚠ partner1 fixture read failed (${e.message}) — генерируем новый ключ`)
}
const account = await blockchain.generateKeypair(username, seededKeys, 'Аккаунт partner1')
await blockchain.createAccount({
coopname: config.provider,
referer: '',
username: account.username,
public_key: account.publicKey,
meta: '',
})
const registration_hash = generateRandomSHA256()
await blockchain.registerUser({
coopname: config.provider,
braname: '',
username: account.username,
type: 'organization',
statement: {
hash: registration_hash,
signatures: [],
meta: '{}',
version: '1.0.0',
doc_hash: registration_hash,
meta_hash: registration_hash,
} as any,
registration_hash,
})
await blockchain.registerCooperative({
username: account.username,
coopname: account.username,
params: {
is_cooperative: true,
coop_type: 'conscoop',
announce: 'partner-dev.coopenomics.world',
description: 'Партнёрский кооператив (тестовый dev-стенд)',
initial: `100.0000 ${config.token.govern_symbol}`,
minimum: `300.0000 ${config.token.govern_symbol}`,
org_initial: `1000.0000 ${config.token.govern_symbol}`,
org_minimum: `3000.0000 ${config.token.govern_symbol}`,
},
document: {
hash: registration_hash,
signatures: [],
meta: '{}',
version: '1.0.0',
doc_hash: registration_hash,
meta_hash: registration_hash,
} as any,
})
await blockchain.preInit({
coopname: account.username,
username: config.provider,
status: 'active',
})
// Записываем partner1 в ЦПП Кошелька оператора (voskhod, program_id=1).
// БЕЗ этого провайдерский performInitialTransfers (150 AXON на partner1 ДО
// перехода в ACTIVE, см. provider CLAUDE.md §13) падает ассертом
// eosio.token::is_can_transfer «Получатель не является участником целевой
// потребительской программы кошелька» — и инстанс навсегда застревает в
// INSTALL. wallet::signagree (auth voskhod@active) делает partner1 членом
// ЦПП Кошелька. Та же механика, что для обычных пайщиков в participant.ts.
console.log('Подписываем wallet-соглашение за partner1 (членство в ЦПП Кошелька)')
await signProgramAgreement(blockchain, config.provider, account.username, walletProgramId, walletDraftId, fakeDocument)
// Ресурсы для партнёра: powerup CPU/NET.
await blockchain.powerup({
payer: 'eosio',
receiver: account.username,
days: config.powerup.days,
payment: `100.0000 ${config.token.symbol}`,
transfer: true,
})
// === Засев partner1 в воскход-coopback (Mongo organizations + paymentMethods + Postgres users) ===
//
// Provider при initSystem-инициализации partner1 запрашивает у воскход-coopback'а
// приватные данные организации через GraphQL `getAccount(partner1).private_account`.
// Resolver идёт по ветке `username != config.coopname` → userRepository.findByUsername
// (Postgres users), затем organizationRepository.findByUsername (Mongo organizations).
// Без этих записей resolver вернёт private_account=null → провайдер упадёт
// «Unsupported account type: undefined» при подготовке organization_data.
console.log('Засеваем partner1 как пайщика-organization в воскход-coopback')
const partnerEmail = `chairman.${account.username}@example.com`
const generator = new Generator()
await generator.connect(process.env.MONGO_URI as string)
await generator.save('organization', {
username: account.username,
type: 'coop',
short_name: 'ПК "Партнёр-1"',
full_name: 'Потребительский Кооператив "Партнёр-1"',
represented_by: {
first_name: 'Иван',
last_name: 'Иванов',
middle_name: 'Иванович',
position: 'Председатель',
based_on: 'Решение общего собрания №1',
},
country: 'Российская Федерация',
city: 'Москва',
fact_address: '117593 г. Москва, ул. Тестовая, дом 1',
full_address: '117593 г. Москва, ул. Тестовая, дом 1',
email: partnerEmail,
phone: '+71234567891',
details: {
inn: '7728130612',
ogrn: '1247700283347',
kpp: '772801002',
},
} as any)
await generator.save('paymentMethod', {
is_default: true,
method_id: randomUUID(),
method_type: 'bank_transfer',
username: account.username,
data: {
account_number: '40703810038000110118',
currency: 'RUB',
card_number: '',
bank_name: 'ПАО Сбербанк',
details: {
bik: '044525225',
corr: '30101810400000000225',
kpp: '772801002',
},
},
})
await mongoose.disconnect().catch(() => {})
await initUsersInPostgres([{
username: account.username,
email: partnerEmail,
type: 'organization',
role: 'user',
status: 'joined',
is_registered: true,
}])
console.log(`=== partner1 (${account.username}) засеян в воскход-coopback (Mongo+Postgres) ===\n`)
}
+16 -14
View File
@@ -1,13 +1,11 @@
import axios from 'axios'
import { describe, expect, it } from 'vitest'
import { Registry } from '@coopenomics/factory'
import { RegistratorContract, Cooperative as TCooperative } from 'cooptypes'
import type { Account, Contract, Keys } from '../types'
import config, { GOVERN_SYMBOL, SYMBOL } from '../configs'
import config from '../configs'
import Blockchain from '../blockchain'
import { sendPostToCoopbackWithSecret, sleep } from '../utils'
import { generateRandomSHA256 } from '../utils/randomHash'
import { fakeDocument } from '../tests/shared/fakeDocument'
import { signProgramAgreement } from './sign-program-agreement'
import { walletDraftId, walletProgramId } from '../tests/capital/consts'
import { RegistratorContract } from 'cooptypes'
import type { Keys } from '../types'
export class ParticipantsClass {
public blockchain: Blockchain
@@ -157,13 +155,17 @@ export class ParticipantsClass {
console.log('Отправляем подписанное положение о ЦПП Кошелька оператору')
await this.blockchain.sendAgreement({
coopname: config.provider,
administrator: config.provider,
username: username!,
agreement_type: 'wallet',
document: fakeDocument,
})
// После Эпика 2 / компонента 48 soviet::sndagreement отказывается на
// program_id > 0; программные соглашения подписываются через
// wallet::signagree (auth: coopname@active).
await signProgramAgreement(
this.blockchain,
config.provider,
username!,
walletProgramId,
walletDraftId,
fakeDocument,
)
console.log('создаём кошелёк')
@@ -0,0 +1,99 @@
import { SovietContract, WalletContract } from 'cooptypes'
import { getTotalRamUsage } from '../utils/getTotalRamUsage'
async function getCoopProgramWallet(blockchain: any, coopname: string, program_id: number) {
const program = await blockchain.getTableRows(
SovietContract.contractName.production,
coopname,
'programs',
1000,
program_id.toString(),
program_id.toString(),
)
return program[0]
}
interface UserProgramAgreement {
program_id: number | string
doc_hash: string
version: number | string
draft_id: number | string
signed_at: string
}
interface WalletUserRecord {
username: string
programs: UserProgramAgreement[]
}
// Подписание программного соглашения через `wallet::signagree` (ADR-008, Эпик 2).
// После компонента 48 `soviet::sndagreement` отказывается на program_id > 0 —
// программные соглашения переехали в контракт wallet, источник правды
// «участник ЦПП X» = `wallet::users[username].programs[]`.
//
// Auth: coopname@active (или любой системный контракт из contracts_whitelist
// контракта wallet). В boot-flow ключ кооператива всегда есть в keychain,
// подписываем coopname'ом.
//
// Живёт в init/, а не в tests/, чтобы не тянуть vitest в production-цепочке
// `boot → init/cooperative → init/participant`. Тесты реэкспортируют отсюда.
export async function signProgramAgreement(
blockchain: any,
coopname: string,
username: string,
program_id: number,
draft_id: number,
document: any,
) {
const data: WalletContract.Actions.SignAgreement.ISignAgreement = {
coopname,
username,
program_id,
document,
draft_id,
}
const result = await blockchain.api.transact(
{
actions: [
{
account: WalletContract.contractName.production,
name: WalletContract.Actions.SignAgreement.actionName,
authorization: [{ actor: coopname, permission: 'active' }],
data,
},
],
},
{ blocksBehind: 3, expireSeconds: 30 },
)
getTotalRamUsage(result)
if (!result.transaction_id)
throw new Error('wallet::signagree не вернул transaction_id')
const userRows = await blockchain.getTableRows(
WalletContract.contractName.production,
coopname,
WalletContract.Tables.Users.tableName,
1000,
) as WalletUserRecord[]
const userRecord = userRows.find(r => r.username === username)
if (!userRecord)
throw new Error(`wallet::users[${coopname}, ${username}] должен быть создан после signagree`)
const signedProgram = userRecord.programs.find(p => Number(p.program_id) === program_id)
if (!signedProgram)
throw new Error(`wallet::users[${username}].programs[] должен содержать program_id=${program_id}`)
const program = await getCoopProgramWallet(blockchain, coopname, program_id)
if (!program)
throw new Error(`soviet::programs[${coopname}, ${program_id}] должна существовать`)
return {
txId: result.transaction_id,
wallet: { username, ...signedProgram },
program,
}
}
+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-валидация канона двухподписного АПП приёмки (шаги 47 магистрали 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)
})
@@ -1,6 +1,15 @@
export const sourceProgramId = 3
export const capitalProgramId = 4
export const walletProgramId = 1
// draft_id для программных соглашений — соответствует мапе `program_map` из
// `lib/core/programs.hpp` (wallet→1, source/generator→0, capital/blagorost→1000).
// Используются при `wallet::signagree` (program_id > 0 после Эпика 2 / компонента 48
// ушло из soviet::sndagreement и подписывается через wallet).
export const walletDraftId = 1
export const sourceDraftId = 0
export const capitalDraftId = 1000
export const ratePerHour = '1000.0000 RUB'
export const sourceProgramName = 'generator'
export const capitalProgramName = 'blagorost'
@@ -1,28 +1,20 @@
import { expect } from 'vitest'
import { signAgreement } from '../soviet/signAgreement'
import { getCoopProgramWallet, getUserProgramWallet } from '../wallet/walletUtils'
import { capitalProgramName, sourceProgramId } from './consts'
import { signProgramAgreement } from '../wallet/signProgramAgreement'
import { capitalDraftId, capitalProgramId } from './consts'
// После Эпика 2 / компонента 48 подпись соглашения ЦПП «Благорост»
// (program_id=4) идёт через `wallet::signagree`, а не `soviet::sndagreement`.
export async function signCapitalAgreement(
blockchain: any,
coopname: string,
username: string,
fakeDocument: any,
) {
const txId = await signAgreement(blockchain, coopname, username, capitalProgramName, fakeDocument)
const wallet = await getUserProgramWallet(blockchain, coopname, username, sourceProgramId)
expect(wallet).toEqual(expect.objectContaining({
return signProgramAgreement(
blockchain,
coopname,
username,
available: expect.any(String),
blocked: expect.any(String),
program_id: sourceProgramId,
membership_contribution: expect.any(String),
}))
const program = await getCoopProgramWallet(blockchain, coopname, sourceProgramId)
return { wallet, program, txId }
capitalProgramId,
capitalDraftId,
fakeDocument,
)
}
@@ -1,28 +1,20 @@
import { expect } from 'vitest'
import { signAgreement } from '../soviet/signAgreement'
import { getCoopProgramWallet, getUserProgramWallet } from '../wallet/walletUtils'
import { sourceProgramId, sourceProgramName } from './consts'
import { signProgramAgreement } from '../wallet/signProgramAgreement'
import { sourceDraftId, sourceProgramId } from './consts'
// После Эпика 2 / компонента 48 подпись соглашения ЦПП «Генератор»
// (program_id=3) идёт через `wallet::signagree`, а не `soviet::sndagreement`.
export async function signGenerationContract(
blockchain: any,
coopname: string,
username: string,
fakeDocument: any,
) {
const txId = await signAgreement(blockchain, coopname, username, sourceProgramName, fakeDocument)
const wallet = await getUserProgramWallet(blockchain, coopname, username, sourceProgramId)
expect(wallet).toEqual(expect.objectContaining({
return signProgramAgreement(
blockchain,
coopname,
username,
available: expect.any(String),
blocked: expect.any(String),
program_id: sourceProgramId,
membership_contribution: expect.any(String),
}))
const program = await getCoopProgramWallet(blockchain, coopname, sourceProgramId)
return { wallet, program, txId }
sourceProgramId,
sourceDraftId,
fakeDocument,
)
}
@@ -0,0 +1,5 @@
// Реэкспорт из init/, чтобы существующие импорты в тестах продолжали работать.
// Сама реализация переехала в src/init/sign-program-agreement.ts — иначе
// production-цепочка `boot → init/cooperative → tests/wallet/signProgramAgreement`
// затягивала vitest при загрузке модуля и валила esno-запуск boot'а.
export { signProgramAgreement } from '../../init/sign-program-agreement'
@@ -1,26 +1,20 @@
import { expect } from 'vitest'
import { signAgreement } from '../soviet/signAgreement'
import { getCoopProgramWallet, getUserProgramWallet } from './walletUtils'
import { walletDraftId, walletProgramId } from '../capital/consts'
import { signProgramAgreement } from './signProgramAgreement'
// После Эпика 2 / компонента 48 подпись соглашения ЦПП «Цифровой Кошелёк»
// (program_id=1) идёт через `wallet::signagree`, а не `soviet::sndagreement`.
export async function signWalletAgreement(
blockchain: any,
coopname: string,
username: string,
fakeDocument: any,
) {
const txId = await signAgreement(blockchain, coopname, username, 'wallet', fakeDocument)
const wallet = await getUserProgramWallet(blockchain, coopname, username, 1)
expect(wallet).toEqual(expect.objectContaining({
return signProgramAgreement(
blockchain,
coopname,
username,
available: expect.any(String),
blocked: expect.any(String),
program_id: 1,
membership_contribution: expect.any(String),
}))
const program = await getCoopProgramWallet(blockchain, coopname, 1)
return { wallet, program, txId }
walletProgramId,
walletDraftId,
fakeDocument,
)
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@coopenomics/cleos",
"version": "2026.5.13-alpha-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` (грантовый канон видимости столов).
+4 -6
View File
@@ -1,10 +1,8 @@
soviet/soviet.abi
soviet/soviet.wasm
registrator/registrator.abi
registrator/registrator.wasm
cpp/**/*.wasm
cpp/**/*.abi
!cpp/tests/test_contracts/**/*.wasm
!cpp/tests/test_contracts/**/*.abi
networks.sh
ano/ano.wasm
ano/ano.abi
Testing/
docs/html/*
docs/xml/*
+5 -7
View File
@@ -1,15 +1,13 @@
#!/usr/bin/env bash
# Локальная сборка всех контрактов: оборачивает build_contracts_cdt.sh в
# CDT-образ. Сам cmake/make живёт в build_contracts_cdt.sh — он же
# используется в CI (там CDT ставится из .deb, без docker), поэтому
# флаги сборки в одном месте и не разъезжаются.
mode="${1:-prod}"
if [ "$mode" = "test" ]; then
is_testnet="ON"
else
is_testnet="OFF"
fi
docker run --rm --name cdt \
--volume "$(pwd):/project" \
-w /project \
dicoop/blockchain:latest \
/bin/bash -c "mkdir -p build && cd build && cmake -DBUILD_TARGET= -DTEST_TARGET= -DVERBOSE=ON -DBUILD_TESTS=OFF -DIS_TESTNET=$is_testnet .. && make"
/bin/bash -c "./build_contracts_cdt.sh $mode"
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
# Компиляция всех контрактов кооперативной экономики через CDT.
#
# ВАЖНО: скрипт запускается УЖЕ внутри окружения с установленным CDT —
# тулчейн ожидается по пути /cdt/build/lib/cmake/cdt/CDTWasmToolchain.cmake
# (он захардкожен в CMakeLists.txt). Здесь НЕТ docker — только cmake/make.
#
# Два способа подготовить это окружение:
# - локально: build-all.sh оборачивает скрипт в `docker run dicoop/blockchain:latest`
# (в образе CDT уже лежит в /cdt/build);
# - в CI: CDT ставится из .deb (C9S/cdt), а /cdt/build симлинкуется на
# /usr/opt/cdt/<ver> (см. .github/workflows/release.yaml).
#
# Один источник cmake-флагов для обоих путей — чтобы локальная и CI-сборка
# не разъезжались.
set -euo pipefail
mode="${1:-prod}"
if [ "$mode" = "test" ]; then
is_testnet="ON"
else
is_testnet="OFF"
fi
mkdir -p build
cd build
cmake -DBUILD_TARGET= -DTEST_TARGET= -DVERBOSE=ON -DBUILD_TESTS=OFF -DIS_TESTNET="$is_testnet" ..
make
@@ -72,8 +72,8 @@ void capital::importcontrib(eosio::name coopname, eosio::name username, checksum
auto idx = user_wallets.get_index<"byuserwallet"_n>();
auto preimp_it = idx.find(combine_ids(ledger2_wallets::PREIMP_FUND.value, username.value));
if (preimp_it != idx.end() && preimp_it->available.amount > 0) {
eosio::check(preimp_it->blocked.amount == 0,
"preimp.blocked > 0 не поддерживается при импорте — обратиться в поддержку");
// Поле blocked упразднено (2026-05-24): резерв/блокировка на кошельках
// больше не используется, проверка preimp.blocked == 0 излишня.
Ledger2::apply(
_capital, coopname, operations::capital::DROP_PREIMP,
preimp_it->available, username, contributor_hash,
@@ -34,6 +34,10 @@ void capital::convertsegm(eosio::name coopname, eosio::name username,
document2 convert_statement) {
require_auth(coopname);
// Проверяем подпись заявления о трансляции паевого взноса (шаблон 1080).
// Без этого on-chain принял бы любую сконструированную document2 без валидной подписи.
verify_document_or_fail(convert_statement, {username});
// Получаем сегмент пайщика
auto segment = Capital::Segments::get_segment_or_fail(coopname, project_hash, username,
"Сегмент пайщика не найден");
@@ -108,6 +112,23 @@ void capital::convertsegm(eosio::name coopname, eosio::name username,
// Инкрементируем счётчик сконвертированных сегментов
Capital::Projects::increment_converted_segments(coopname, current_project.id);
// Линкуем заявление о трансляции (1080) к пакету процесса p.cap.rid
// (package = result_hash, ведущий документ пакета — заявление о внесении РИД, 1040).
// Не make_complete_document: тот вытаскивает 1080 на верхний уровень реестра
// как самостоятельный документ. Нужен newlink — тот же канон, что в signact1/signact2:
// off-chain controller добавляет документ в группу пакета по action+package, не как top-level.
// Делаем ДО delete_result, чтобы линковка прошла, пока result_hash ещё анкер процесса.
Action::send<newlink_interface>(
_soviet,
"newlink"_n,
_capital,
coopname,
username,
Names::Capital::CONVERT_SEGMENT,
result_hash,
convert_statement
);
// Удаляем сегмент после конвертации
Capital::Segments::remove_segment(coopname, segment.id);
@@ -48,14 +48,17 @@ void capital::signact2(eosio::name coopname, eosio::name chairman, checksum256 r
// Приём результата интеллектуальной деятельности (РИД) в паевой фонд.
// Схема ревью 2026-04-20 (Ангелина Matrix 2026-04-19): две раздельные проводки
// в разных action'ах по фазам жизненного цикла РИД.
// 1) COMMIT_RID (Dr 08 / Cr 80) — теперь в `approvecmmt` на каждом одобрении
// мастером конкретного коммита, на дельту `available_for_program`.
// «Собираем на 08 частями по мере накопления коммитов».
// 1) COMMIT_RID (Dr 08 / Cr 80) — в `approvecmmt` на каждом одобрении
// мастером конкретного коммита, на полный `commit.amounts.total_contribution`.
// 2) ACCEPT_RID (Dr 04 / Cr 08) — здесь, на полный накопленный
// `segment.available_for_program`. «Переносим с 08 на 04 когда РИД
// собран и подписан акт-2».
// Инвариант: Σ COMMIT_RID (по коммитам сегмента) == ACCEPT_RID → GENERATOR_FUND
// (w.cap.gen) закрывается в ноль, 08-й счёт закрывается в ноль по этому сегменту.
// Инвариант (программный уровень, НЕ сегмент): Σ COMMIT_RID по всем коммитам
// программы == Σ ACCEPT_RID по всем сегментам — `wallets2[w.cap.gen]` (единый
// кооперативный пул, COOPERATIVE) закрывается в ноль после конвертации всех
// сегментов программы. На уровне отдельного сегмента инвариант НЕ выполняется
// (CRPS перераспределяет доли между сегментами без compensating TRANSFER) —
// именно поэтому L3-разрез по пайщику снят (см. wallets.hpp:103).
if (segment.available_for_program.amount > 0) {
Ledger2::apply(_capital, coopname, operations::capital::ACCEPT_RID, segment.available_for_program, result -> username, result_hash, memo);
}
@@ -9,23 +9,26 @@ namespace Capital::Core {
* @brief Совокупный баланс программы Благорост на уровне кооператива.
*
* Источник ledger2 L2 (`wallets[BLAGOROST_FUND]`), агрегированный по всем
* пайщикам. Σ L3.{available, blocked}[w.cap.blago] == L2.{available, blocked}
* (Эпик 3 invariant), поэтому L2 авторитетная сумма для CRPS-знаменателя.
* Если кошелёк ещё не создан (никто не инвестировал) нулевой баланс.
* пайщикам. Σ L3.available[w.cap.blago] == L2.available (Эпик 3 invariant),
* поэтому L2 авторитетная сумма для CRPS-знаменателя. Если кошелёк ещё не
* создан (никто не инвестировал) нулевой баланс.
*
* Поле blocked упразднено (2026-05-24): субсчёт «заблокировано» больше не
* используется, баланс кошелька = available.
*/
eosio::asset get_capital_program_share_balance(eosio::name coopname) {
wallets2_index wallets(_ledger2, coopname.value);
auto it = wallets.find(ledger2_wallets::BLAGOROST_FUND.value);
if (it == wallets.end()) return eosio::asset(0, _root_govern_symbol);
return it->available + it->blocked;
return it->available;
}
/**
* @brief Доля пайщика в Благоросте (available + blocked) из ledger2 L3.
* @brief Доля пайщика в Благоросте (available) из ledger2 L3.
*
* Источник `userwallets[(w.cap.blago, username)]`. Запись существует
* только пока available + blocked > 0 (Эпик 3 §6: auto-delete на нуле),
* поэтому отсутствие = нулевой баланс.
* только пока available > 0 (Эпик 3 §6: auto-delete на нуле), поэтому
* отсутствие = нулевой баланс. Поле blocked упразднено (2026-05-24).
*/
eosio::asset get_capital_program_user_share_balance(eosio::name coopname, eosio::name username) {
userwallets_index user_wallets(_ledger2, coopname.value);
@@ -33,6 +36,6 @@ namespace Capital::Core {
auto key = combine_ids(ledger2_wallets::BLAGOROST_FUND.value, username.value);
auto it = idx.find(key);
if (it == idx.end()) return eosio::asset(0, _root_govern_symbol);
return it->available + it->blocked;
return it->available;
}
}
+10 -12
View File
@@ -117,23 +117,21 @@ public:
std::string memo);
/**
* @brief Миграция остатков с legacy-ledger в ledger2 (курсорный режим).
* @brief Универсальное миграционное действие точка расширения для
* разовых исправлений состояния, которые можно провести автоматически
* после деплоя контракта.
*
* @param from_coop_index начальный индекс в таблице registrator::coops (0 с начала)
* @param limit максимум кооп. за один вызов (UINT64_MAX до конца)
* Тело периодически переписывается под текущую задачу миграции; после
* прогона на проде очищается до пустого `require_auth(get_self())`.
* История прошлых миграций в git-истории `ledger2/src/migrate/migrate.cpp`.
*
* Полный прогон: `migrate(0, UINT64_MAX)`. Продовый прогон порциями:
* `migrate(0, 10)`, `migrate(10, 10)`, ... Мета-таблица хранит
* `last_migrated_coop_index` для возобновления.
*
* Все балансы вносятся через inline `apply(operations::migration::*)` единый путь учёта
* с полной двойной проводкой и audit-trail (пересмотр 2026-04-20:
* детерминированное разнесение на 6 целевых кошельков без транзитного
* счёта 99 и без зеркала CASH_MAIN).
* Текущая задача см. doxygen-блок в migrate.cpp.
*
* @ingroup public_ledger2_actions
*
* @note Авторизация требуется от аккаунта: @p ledger2 (get_self()).
*/
[[eosio::action]] void migrate(uint64_t from_coop_index, uint64_t limit);
[[eosio::action]] void migrate();
/**
* @brief Идемпотентная per-record миграция L3-балансов (Phase 1/2; ADR-008).
@@ -67,12 +67,12 @@ void ledger2::revert(eosio::name coopname,
eosio::check(original_operation_id != 0, "revert: original_operation_id обязателен");
// -------- validate mirror_wallet_op --------
eosio::check(mirror_wallet_op <= 5, "revert: неизвестный mirror_wallet_op");
// Не позволяем откатывать через BLOCK/UNBLOCK — они асимметричны и нет
// адекватного зеркала в одной операции (BLOCK + UNBLOCK — обратные сами по себе).
eosio::check(mirror_wallet_op != static_cast<uint8_t>(WalletOp::BLOCK) &&
mirror_wallet_op != static_cast<uint8_t>(WalletOp::UNBLOCK),
"revert: BLOCK/UNBLOCK не подлежат откату через revert (они симметричны сами себе)");
// Зеркало revert — только TRANSFER (обмен wallet_from/wallet_to) либо BURN
// (зеркало ISSUE: изъятие с wallet_from). Бывшие BLOCK/UNBLOCK упразднены
// (2026-05-24); ISSUE/NONE как зеркало смысла не имеют.
eosio::check(mirror_wallet_op == static_cast<uint8_t>(WalletOp::TRANSFER) ||
mirror_wallet_op == static_cast<uint8_t>(WalletOp::BURN),
"revert: mirror_wallet_op должен быть TRANSFER или BURN");
// -------- validate mirror wallets/accounts --------
if (mirror_wallet_from.value != 0) {
@@ -1,5 +1,5 @@
/**
* @brief Атомарная операция по кошельку (issue/transfer/block/unblock/burn).
* @brief Атомарная операция по кошельку (issue/transfer/burn).
*
* Внутренний action ledger2 вызывается только через inline из apply().
* Auth: только сам ledger2 (require_auth(get_self())).
@@ -53,12 +53,15 @@ void ledger2::walletop(eosio::name coopname,
eosio::check(amount.symbol == _root_govern_symbol,
"walletop: некорректный символ валюты");
eosio::check(memo.size() < 256, "walletop: memo > 255");
// op_code = 5 (NONE) намеренно не допускается: NONE-операции — это только
// бухпроводка без кошелькового движения, apply.cpp не диспатчит для них walletop.
// Прямой вызов с op_code=5 был бы no-op и сбил бы инвариант parity.
// op_code = 6 (REVOKE) — целевое расходование blocked в пустоту; обязательная пара Dr/Cr;
// допускается, обрабатывается отдельным case ниже (введён 2026-05-11 под o.mkt.consum).
eosio::check(op_code <= 6 && op_code != 5, "walletop: неизвестный op_code");
// Допустимы только ISSUE(0), TRANSFER(1), BURN(4). NONE(5) — это только
// бухпроводка без кошелькового движения, apply.cpp для неё walletop не диспатчит;
// прямой вызов был бы no-op и сбил бы parity. Бывшие BLOCK(2)/UNBLOCK(3)/
// BURN_BLOCKED(6) упразднены (2026-05-24): резерв возврата теперь TRANSFER на
// w.wal.wpend, их op_code больше не валиден.
eosio::check(op_code == static_cast<uint8_t>(WalletOp::ISSUE) ||
op_code == static_cast<uint8_t>(WalletOp::TRANSFER) ||
op_code == static_cast<uint8_t>(WalletOp::BURN),
"walletop: недопустимый op_code");
wallets2_index wallets(get_self(), coopname.value);
userwallets_index user_wallets(get_self(), coopname.value);
@@ -171,7 +174,7 @@ void ledger2::walletop(eosio::name coopname,
// миграций 048/049.
//
// walletop по построению применяет одно и то же `amount` к L2 и L3 (см.
// ниже case'ы ISSUE/TRANSFER/BLOCK/UNBLOCK/BURN), а sender-guard на
// ниже case'ы ISSUE/TRANSFER/BURN), а sender-guard на
// строке 46-47 запрещает обход. Поэтому инвариант сохраняется по
// конструкции; полную сверку выполняет бэкенд («стол бухгалтера»),
// вне транзакционного hot path.
@@ -226,60 +229,6 @@ void ledger2::walletop(eosio::name coopname,
cleanup_l3_if_empty(wallet_from);
break;
}
case WalletOp::BLOCK: {
eosio::check(wallet_from.value != 0, "walletop BLOCK: требуется wallet_from");
eosio::check(wallet_to.value == 0, "walletop BLOCK: wallet_to должен быть пустым");
auto it = wallets.find(wallet_from.value);
eosio::check(it != wallets.end() && it->available >= amount,
std::string{"walletop BLOCK: недостаточно available на кошельке "} +
wallet_from.to_string());
wallets.modify(it, payer, [&](auto& w) {
w.available -= amount;
w.blocked += amount;
});
if (is_user_shared_l3(wallet_from)) {
auto uw = find_l3(wallet_from);
eosio::check(uw != user_wallets.get_index<"byuserwallet"_n>().end() &&
uw->available >= amount,
std::string{"walletop BLOCK: недостаточно L3-available у пайщика "} +
username.to_string() + " на " + wallet_from.to_string());
auto uw_pri = user_wallets.find(uw->id);
user_wallets.modify(uw_pri, payer, [&](auto& r) {
r.available -= amount;
r.blocked += amount;
});
}
break;
}
case WalletOp::UNBLOCK: {
eosio::check(wallet_from.value != 0, "walletop UNBLOCK: требуется wallet_from");
eosio::check(wallet_to.value == 0, "walletop UNBLOCK: wallet_to должен быть пустым");
auto it = wallets.find(wallet_from.value);
eosio::check(it != wallets.end() && it->blocked >= amount,
std::string{"walletop UNBLOCK: недостаточно blocked на кошельке "} +
wallet_from.to_string());
wallets.modify(it, payer, [&](auto& w) {
w.blocked -= amount;
w.available += amount;
});
if (is_user_shared_l3(wallet_from)) {
auto uw = find_l3(wallet_from);
eosio::check(uw != user_wallets.get_index<"byuserwallet"_n>().end() &&
uw->blocked >= amount,
std::string{"walletop UNBLOCK: недостаточно L3-blocked у пайщика "} +
username.to_string() + " на " + wallet_from.to_string());
auto uw_pri = user_wallets.find(uw->id);
user_wallets.modify(uw_pri, payer, [&](auto& r) {
r.blocked -= amount;
r.available += amount;
});
}
break;
}
case WalletOp::BURN: {
eosio::check(wallet_from.value != 0, "walletop BURN: требуется wallet_from");
eosio::check(wallet_to.value == 0, "walletop BURN: wallet_to должен быть пустым");
@@ -305,62 +254,9 @@ void ledger2::walletop(eosio::name coopname,
break;
}
case WalletOp::NONE: {
// unreachable: apply.cpp не диспатчит walletop для NONE-операций
// (бухпроводка без кошелькового движения). Pre-check на op_code != 5 выше
// отсекает прямой вызов с этим кодом. case оставлен для исчерпывающего
// switch — иначе компилятор предупредит про unhandled enum value.
eosio::check(false, "walletop NONE: must be unreachable");
break;
}
case WalletOp::REVOKE: {
// Целевое расходование заблокированной суммы: amount списывается с
// wallet_from.blocked в пустоту, без зачисления на wallet_to.
//
// Отличие от BURN:
// - BURN списывает available (свободный остаток). Применяется к штатному
// «сжиганию» свободных средств (например, DROP_PREIMP при переходе на
// электронный учёт РИД-взноса). Dr/Cr пара опциональна.
// - REVOKE списывает blocked (зарезервированный остаток). Применяется на
// финализации заранее зарезервированной операции, когда резерв
// «уходит в потребление», а не возвращается. Обязательная пара Dr/Cr
// на уровне OPERATION_REGISTRY (compile-time validator
// revoke_pattern_correct в operations.hpp).
//
// Применение в членской модели Стола заказов: o.mkt.consum (выдача
// имущества пайщику по АПП выдачи). Пайщик заблокировал сумму под Order
// на createorder (BLOCK на w.mkt.member). При фактической выдаче
// имущества этот blocked-резерв должен «сгореть» как целевое
// потребление с одновременной фиксацией Дт 91 / Кт 10 (выбытие
// имущества со склада на счёт «прочие»). Альтернатива через
// UNBLOCK + BURN — это две операции с искажённой семантикой:
// «снял резерв» (которого по факту не снимал — потребил) +
// «сжёг свободные» (которые в моменте не были свободными). REVOKE
// одной операцией точно описывает целевое расходование blocked.
//
// Не путать с rollback: ledger2 не использует ledger2::revert в
// membership-модели marketplace; компенсирующие проводки делаются
// forward через обратную операцию (см. RETURN_BY_MEMBER + RETURN_TRANSIT_CLOSE).
eosio::check(wallet_from.value != 0, "walletop REVOKE: требуется wallet_from");
eosio::check(wallet_to.value == 0, "walletop REVOKE: wallet_to должен быть пустым");
auto it = wallets.find(wallet_from.value);
eosio::check(it != wallets.end() && it->blocked >= amount,
std::string{"walletop REVOKE: недостаточно blocked на кошельке "} +
wallet_from.to_string());
wallets.modify(it, payer, [&](auto& w) { w.blocked -= amount; });
if (is_user_shared_l3(wallet_from)) {
auto uw = find_l3(wallet_from);
eosio::check(uw != user_wallets.get_index<"byuserwallet"_n>().end() &&
uw->blocked >= amount,
std::string{"walletop REVOKE: недостаточно L3-blocked у пайщика "} +
username.to_string() + " на " + wallet_from.to_string());
auto uw_pri = user_wallets.find(uw->id);
user_wallets.modify(uw_pri, payer, [&](auto& r) { r.blocked -= amount; });
}
cleanup_l2_if_empty(wallet_from);
cleanup_l3_if_empty(wallet_from);
// Недостижимо: проверка op_code != NONE стоит на входе action; этот case
// нужен только чтобы покрыть enum в switch (-Wswitch).
eosio::check(false, "walletop NONE: запрещённый op_code");
break;
}
}
@@ -1,634 +1,72 @@
/**
* @brief Миграция остатков с legacy-ledger на ledger2 (пересмотр 2026-04-20).
* @brief Универсальное миграционное действие контракта ledger2 точка
* расширения для разовых исправлений состояния, которые можно провести
* автоматически после деплоя.
*
* Разделение на два независимых потока:
* Содержимое периодически переписывается под текущую задачу миграции, а
* после её прогона на проде тело очищается до пустого `require_auth(get_self())`
* (как в `capital::migrate`).
*
* A. **Бухгалтерские остатки** из `ledger::accounts` (scope=coopname,
* contract=_ledger) через 4 inline `apply(operations::migration::*)`
* с полной двойной проводкой. Счета: 51 / 80 / 86 / 04.
* Текущая задача (2026-05-24): свёртка `blocked available` по ВСЕМ коопам.
*
* B. **Программные кошельки** из `soviet::progwallets` (scope=coopname,
* contract=_soviet) прямой `wallets2.emplace` в ledger2 БЕЗ
* бух-проводок. Legacy::accounts и soviet::progwallets параллельные
* системы учёта, progwallet.blocked не проводится через 80-й счёт,
* поэтому любая попытка провести его через Dr 51 / Cr 80 вызовет
* двойной учёт на бухуровне. Вместо этого переносим только
* wallet-аналитику (w.cap.blago BLAGOROST_FUND, w.cap.gen GENERATOR_FUND ADR-009).
* Контекст: механика «заблокированного» баланса упразднена (см.
* `lib/core/ledger2/operations.hpp` удалены WalletOp BLOCK/UNBLOCK/BURN_BLOCKED;
* резерв возврата паевого теперь выражается переводом на кошелёк-резерв
* `w.wal.wpend`). Поле `blocked` остаётся в таблицах `wallets2`/`userwallets`
* как deprecated (физическое удаление поля = небезопасная смена layout таблицы
* на живых коопах, выносится в отдельный cleanup-деплой). Перед тем как поле
* перестанет поддерживаться кодом, накопленные `blocked`-остатки нужно вернуть
* в `available`, чтобы средства не «зависли» на упразднённом субсчёте.
*
* Алгоритм на каждый кооператив:
* Действие: пройти всех кооперативов (cooperatives2 в scope registrator) и для
* каждого свернуть `blocked available` на уровнях L2 (`wallets2`) и L3
* (`userwallets`): `available += blocked; blocked = 0`. Сумма средств на кошельке
* не меняется только субсчёт.
*
* 1. Чтение legacy-счетов (laccounts_index, scope=coopname):
* cash_legacy = account[51].available + .blocked
* share_legacy = account[80].available + .blocked
* entry_legacy = account[861].available + .blocked
* Идемпотентно: после свёртки `blocked == 0`, повторный прогон no-op.
*
* 2. Чтение параметров кооператива (cooperative2, scope=_registrator):
* coop.minimum минимальный паевой для individual / entrepreneur
* coop.org_minimum минимальный паевой для organization (binary_extension)
* Сигнатура без аргументов действие вызывается автоматически при деплое
* контракта (как и прочие задачи migrate); проходит по всем кооперативам сам.
*
* 3. Вычисление распределения:
* share_money = cash_legacy entry_legacy
* min_total = Σ participant.minimum_amount по accepted-пайщикам
* (фактический зафиксированный минимум каждого пайщика;
* см. compute_min_total_by_type).
* share_remain = share_money min_total
*
* Инварианты (eosio::check):
* cash_legacy >= entry_legacy
* share_legacy == share_money (legacy 80 без РИД-части иначе abort)
* min_total <= share_money (данные пайщиков сходятся с legacy[80])
*
* 4. Отправка 3 inline apply (ненулевые пропускаются):
* apply(migration::MIN_SHARE, min_total) Dr 51 / Cr 80, MIN_SHARE_FUND (w.reg.minshr)
* apply(migration::SHARE, share_remain) Dr 51 / Cr 80, SHARE_FUND_PAY (w.wal.share)
* apply(migration::ENTRY, entry_legacy) Dr 51 / Cr 86, ENTRANCE_FEES (w.reg.entry)
*
* 5. Прямой emplace в wallets2 для progwallets (БЕЗ бух-проводок):
* Σ progwallet[blagorost].blocked wallets2[BLAGOROST_FUND w.cap.blago]
* Σ progwallet[generator].blocked wallets2[GENERATOR_FUND w.cap.gen]
*
* Если запись wallets2 уже есть (после inline apply сработали)
* aggregate через wallets.modify(available += sum). Иначе emplace.
*
* Курсорный режим: `migrate(from_coop_index, limit)`. Полный прогон
* `migrate(0, UINT64_MAX)`. Мета фиксирует `last_migrated_coop_index`.
*
* Поле `writeoff` legacy-ledger игнорируется целиком (PRD §4.1.2 FR-L-7).
*
* Сборка test mode (IS_TESTNET=1): спец-ветка voskhod ОТКЛЮЧЕНА voskhod
* прогоняется по стандартному арифметическому пути наравне с остальными
* кооперативами. Это нужно, чтобы на тестнете invariant-фейлы (cashentry,
* legacy 80 без РИД-части и т.п.) показывали понятные ошибки, а не маскировались
* заранее заведёнными суммами. В prod-сборке хардкод по фактам сохраняется.
* ПРЕДУСЛОВИЕ (операционное): на момент прогона не должно быть заявок на возврат
* «в полёте» (статусы pending/authorized в `wallet::withdraws`) их `blocked`
* относится к старой механике и при свёртке в `available` вернётся пайщику как
* свободные средства, а последующий `completewthd` (BURN с `w.wal.wpend`) не
* найдёт резерва. Незавершённые заявки нужно довести (complete/decline) ДО
* деплоя с этой миграцией.
*
* @ingroup public_ledger2_actions
*
* @note Авторизация требуется от аккаунта: @p ledger2 (get_self()).
*/
namespace {
struct LegacyBalances {
eosio::asset cash; ///< 51 — Расчётный счёт
eosio::asset share; ///< 80 — Паевой фонд
eosio::asset entry; ///< 861 — Вступительные взносы
};
inline LegacyBalances read_legacy_balances(eosio::name coopname) {
laccounts_index old_accounts(_ledger, coopname.value);
LegacyBalances r{
eosio::asset(0, _root_govern_symbol),
eosio::asset(0, _root_govern_symbol),
eosio::asset(0, _root_govern_symbol),
};
for (auto acc_it = old_accounts.begin(); acc_it != old_accounts.end(); ++acc_it) {
// writeoff игнорируется (FR-L-7).
const eosio::asset total = acc_it->available + acc_it->blocked;
if (total.amount == 0) continue;
eosio::check(total.symbol == _root_govern_symbol,
std::string{"migrate: legacy acc "} + std::to_string(acc_it->id) +
" имеет неожиданный symbol");
switch (acc_it->id) {
case Ledger::accounts::BANK_ACCOUNT: r.cash = total; break; // 51
case Ledger::accounts::SHARE_FUND: r.share = total; break; // 80
case Ledger::accounts::ENTRANCE_FEES: r.entry = total; break; // 861
default:
// Прочие legacy-id (862..867 — RESERVE/INDIVISIBLE/ECONOMIC/MUTUAL/DEVELOPMENT/DELEGATE_FEES)
// в ledger2 как отдельные кошельки не выделены. Намеренно пропускаем —
// не падаем, чтобы не блокировать миграцию кооператива.
break;
}
}
return r;
}
/**
* Суммирует `blocked` по `progwallets` (soviet, scope=coopname) для program_id.
*/
inline eosio::asset sum_progwallet_blocked(eosio::name coopname, uint64_t program_id) {
progwallets_index progwallets(_soviet, coopname.value);
auto byprog = progwallets.get_index<"byprogram"_n>();
eosio::asset total(0, _root_govern_symbol);
for (auto it = byprog.lower_bound(program_id); it != byprog.end() && it->program_id == program_id; ++it) {
if (!it->blocked.has_value()) continue;
const auto& b = it->blocked.value();
if (b.amount == 0) continue;
eosio::check(b.symbol == _root_govern_symbol,
std::string{"migrate: progwallet program_id="} + std::to_string(program_id) +
" имеет неожиданный symbol");
total += b;
}
return total;
}
/**
* Считает суммарный «минимальный паевой» по всем accepted-пайщикам коопа.
*
* Берёт ФАКТИЧЕСКИЙ минимум, ЗАФИКСИРОВАННЫЙ на пайщике
* (`participant.minimum_amount`), а НЕ актуальный coop.minimum/org_minimum.
* coop.minimum мог быть повышен/понижен после вступления, но реальный взнос
* пайщика, попавший в legacy::accounts[80], равен `participant.minimum_amount`.
* Иначе после миграции Σ L3 (по фактическим минимумам) разойдётся с L2,
* который мы построим по этой сумме.
*
* Поле `minimum_amount` binary_extension; для legacy-записей до выставления
* этого поля fallback на coop.minimum / coop.org_minimum.
*/
inline eosio::asset compute_min_total_by_type(eosio::name coopname, const cooperative2& coop) {
eosio::check(coop.minimum.symbol == _root_govern_symbol,
std::string{"migrate: cooperative2.minimum имеет неожиданный symbol на "} +
coopname.to_string());
const eosio::asset org_min_fallback =
coop.org_minimum.has_value() ? coop.org_minimum.value() : coop.minimum;
eosio::check(org_min_fallback.symbol == _root_govern_symbol,
std::string{"migrate: cooperative2.org_minimum имеет неожиданный symbol на "} +
coopname.to_string());
participants_index parts(_soviet, coopname.value);
int64_t total_raw = 0;
for (auto it = parts.begin(); it != parts.end(); ++it) {
if (it->status != "accepted"_n) continue;
if (it->minimum_amount.has_value()) {
const auto& m = it->minimum_amount.value();
eosio::check(m.symbol == _root_govern_symbol,
std::string{"migrate: participant.minimum_amount имеет неожиданный symbol на "} +
coopname.to_string() + "/" + it->username.to_string());
total_raw += m.amount;
} else {
// Legacy-запись без зафиксированного minimum_amount: fallback по типу.
const bool is_org =
it->type.has_value() && it->type.value() == "organization"_n;
total_raw += is_org ? org_min_fallback.amount : coop.minimum.amount;
}
}
return eosio::asset(total_raw, _root_govern_symbol);
}
/**
* Отправляет inline apply для одной миграционной операции, если amount > 0.
* process_hash детерминирован по (coopname, operation_code).
*/
inline void send_transit(eosio::name self_name,
eosio::name coopname,
eosio::name operation_code,
const eosio::asset& amt) {
if (amt.amount == 0) return;
const std::string hash_src =
std::string{"mig::"} + coopname.to_string() + std::string{"::"} + operation_code.to_string();
const eosio::checksum256 proc_hash = hashit(hash_src);
Ledger2::apply(
self_name,
coopname,
operation_code,
amt,
eosio::name{}, // username не применим к транзитной проводке
proc_hash,
std::string{"Транзитная миграция остатков legacy → ledger2"}
);
}
/**
* Прямой emplace/add в wallets2 БЕЗ бух-проводок. Используется для переноса
* соwiet::progwallets wallets2, т.к. progwallets не проводятся через
* ledger::accounts (параллельная система учёта двойная запись не нужна
* и привела бы к двойному учёту на 80).
*/
inline void emplace_wallet_only(eosio::name self_name,
eosio::name coopname,
eosio::name wallet_id,
const eosio::asset& amt) {
if (amt.amount == 0) return;
wallets2_index wallets(self_name, coopname.value);
const auto human_view = ledger2_get_wallet_human_name(wallet_id);
eosio::check(!human_view.empty(),
std::string{"migrate: unknown wallet "} + wallet_id.to_string());
auto it = wallets.find(wallet_id.value);
if (it == wallets.end()) {
wallets.emplace(self_name, [&](auto& w) {
w.id = wallet_id;
w.name = std::string(human_view);
w.available = amt;
w.blocked = eosio::asset(0, amt.symbol);
});
} else {
// Если кошелёк уже создан (например, предыдущим inline apply TRANSIT_*) —
// просто доливаем сумму в available. Не должно случаться при чистой
// миграции (w.cap.blago/w.cap.gen не используются базовыми TRANSIT_*),
// но безопасно.
wallets.modify(it, self_name, [&](auto& w) { w.available += amt; });
}
}
#ifndef IS_TESTNET
/**
* Прямой emplace в wallets2 с произвольным available/blocked. Используется
* только из voskhod-спец-ветки: там L2-агрегаты нужно завести строго по
* фактическому распределению chain (Благорост blocked, не available).
*/
inline void emplace_wallet_balance(eosio::name self_name,
eosio::name coopname,
eosio::name wallet_id,
const eosio::asset& available,
const eosio::asset& blocked) {
if (available.amount == 0 && blocked.amount == 0) return;
wallets2_index wallets(self_name, coopname.value);
const auto human_view = ledger2_get_wallet_human_name(wallet_id);
eosio::check(!human_view.empty(),
std::string{"migrate: unknown wallet "} + wallet_id.to_string());
auto it = wallets.find(wallet_id.value);
eosio::check(it == wallets.end(),
std::string{"migrate: wallet "} + wallet_id.to_string() + " уже создан");
wallets.emplace(self_name, [&](auto& w) {
w.id = wallet_id;
w.name = std::string(human_view);
w.available = available;
w.blocked = blocked;
});
}
/**
* Прямой emplace в accounts2 БЕЗ бух-проводок. Используется только из
* voskhod-спец-ветки: после хардкода в кошельках замыкаем баланс прямой
* записью в accounts2 (отдельные «миграционные» операции под Dr 04 / Dr 08
* усложнили бы общий реестр).
*
* Тип счёта определяется планом (LEDGER2_ACCOUNT_MAP):
* - ACTIVE записываем `balance` в `debit_balance`, `credit_balance = 0`;
* - PASSIVE записываем `balance` в `credit_balance`, `debit_balance = 0`.
*/
inline void emplace_account_balance(eosio::name self_name,
eosio::name coopname,
uint64_t account_id,
const eosio::asset& balance) {
if (balance.amount == 0) return;
const auto* meta = ledger2_find_account_meta(account_id);
eosio::check(meta != nullptr,
std::string{"migrate: unknown account_id "} + std::to_string(account_id));
accounts2_index accounts(self_name, coopname.value);
auto it = accounts.find(account_id);
eosio::check(it == accounts.end(),
std::string{"migrate: account "} + std::to_string(account_id) + " уже создан");
const eosio::asset zero(0, balance.symbol);
const uint8_t type_u8 = static_cast<uint8_t>(meta->type);
accounts.emplace(self_name, [&](auto& a) {
a.id = account_id;
a.name = std::string(meta->name);
a.account_type = type_u8;
if (meta->type == AccountType::PASSIVE) {
a.debit_balance = zero;
a.credit_balance = balance;
} else { // ACTIVE / ACTIVE_PASSIVE
a.debit_balance = balance;
a.credit_balance = zero;
}
a.balance = account2::compute_balance(type_u8, a.debit_balance, a.credit_balance);
});
}
/**
* Прямой emplace L3-записи в userwallets БЕЗ бух-проводок. Только для
* `kind == USER_SHARED`. Записи с (0,0) пропускаются.
*/
inline void emplace_userwallet_only(eosio::name self_name,
eosio::name coopname,
eosio::name wallet_id,
eosio::name username,
const eosio::asset& available,
const eosio::asset& blocked) {
if (available.amount == 0 && blocked.amount == 0) return;
const WalletKind kind = ledger2_get_wallet_kind(wallet_id);
eosio::check(kind == WalletKind::USER_SHARED,
std::string{"migrate: userwallets допустим только для USER_SHARED, wallet="} +
wallet_id.to_string());
userwallets_index user_wallets(self_name, coopname.value);
auto idx = user_wallets.get_index<"byuserwallet"_n>();
const auto key = combine_ids(wallet_id.value, username.value);
auto it = idx.find(key);
eosio::check(it == idx.end(),
std::string{"migrate: userwallet (wallet="} + wallet_id.to_string() +
", user=" + username.to_string() + ") уже существует");
user_wallets.emplace(self_name, [&](auto& uw) {
uw.id = user_wallets.available_primary_key();
uw.wallet_name = wallet_id;
uw.username = username;
uw.available = available;
uw.blocked = blocked;
});
}
/**
* Заводит L3-запись `w.cap.preimp` для пайщика, если он существует в
* `soviet::participants` коопа. Если нет (тестнет/чужой контекст) silently skip.
*
* Использовать ИСКЛЮЧИТЕЛЬНО для воскходных пред-импорт-РИД-учётов (5 пайщиков
* по договору УХД, заведённых до перехода на электронный учёт: ЭГОЛ/Анацко/
* Кочетков/Мильшин/Манакин). Их деньги списаны с `progwallets[pid=1]` cleos-
* командами; здесь фиксируем их как РИД-взносы на отдельном кошельке для
* последующего drppre при importcontr.
*/
inline void emplace_preimp_if_present(eosio::name self_name,
eosio::name coopname,
eosio::name username,
int64_t amount_raw) {
participants_index parts(_soviet, coopname.value);
if (parts.find(username.value) == parts.end()) {
return; // тестнет / иной контекст — пропускаем без падения
}
const eosio::asset amt(amount_raw, _root_govern_symbol);
const eosio::asset zero(0, _root_govern_symbol);
emplace_userwallet_only(self_name, coopname, ledger2_wallets::PREIMP_FUND, username, amt, zero);
}
#endif // !IS_TESTNET
#ifndef IS_TESTNET
/**
* Спец-ветка миграции для voskhod РУЧНОЙ ХАРДКОД балансов (accounts2 +
* wallets2 + L3 для пред-импорт-учётов).
*
* Включена только в prod-сборке (без IS_TESTNET). На тестнете voskhod идёт
* по стандартному арифметическому пути наравне с остальными кооперативами,
* чтобы любые invariant-несостыковки давали внятную ошибку, а не маскировались
* заранее заведёнными суммами. Дополнительная защита внутри ветки
* `participants.find()`-guard в emplace_preimp_if_present, чтобы случайный
* запуск voskhod-веточки в чужом окружении не падал.
*
* Цифры заведены вручную после сверки с фактическими данными mainnet
* (snapshot 2026-05-10) и согласованы с председателем (см. project-memory
* `voskhod migrate2 финальный план миграции`).
*
* Причина хардкода: legacy::accounts и soviet::progwallets рассинхронизированы
* (имущ. Благорост ~57M на legacy 80 без отражения в 04, бумажный шлейф на 51,
* непокрытые хоз.расходы на 86). Арифметической сшивки нет заводим суммы
* напрямую по согласованной с бухгалтером картине.
*
* accounts2
* 51 (BANK_ACCOUNT, А) = 145 000 145 000 деньги
* 04 (INTANGIBLE_ASSETS, А) = 62 353 311 имущ. Благорост 56 903 311 + preimp 5М4
* 08 (NON_CURRENT_INVESTMENTS) = 575 200 свод (балансировка) + 31 800 минП
* 80 (SHARE_FUND, П) = 62 946 011 419 900 ЦК + 57 044 311 Благорост
* + 31 800 минП + 5 450 000 preimp Cr-side
* 86 (TARGET_RECEIPTS, П) = 127 500 хоз.расходы 115К + legacy 861 12 500
*
* Σ Dr = 145 000 + 62 353 311 + 575 200 = 63 073 511
* Σ Cr = 62 946 011 + 127 500 = 63 073 511
*
*
* wallets2 (L2-агрегаты)
* w.sov.mnused available = 31 800 (мин.паевые, ушедшие в 08; COOPERATIVE)
* w.wal.share available = 419 900 (5 пайщиков ЦК-остаток; L3 migrator-049)
* w.cap.blago blocked = 57 044 311 (12 пайщиков pid=4; L3 migrator-049)
* w.cap.preimp available = 5 450 000 (5 пайщиков; L3 ниже, прямой emplace)
* w.sov.expns available = 127 500 (хоз.расходы из числа целевого; COOPERATIVE)
*
*
* w.reg.minshr на voskhod НЕ создаётся: мин.паевые (31 800) уже инвестированы в
* НМА (08), поэтому L2-аналитика лежит на w.sov.mnused (COOPERATIVE, без L3).
* Бухгалтерия (Cr 80 += 31 800; Dr 08 += 31 800) при этом сохранена обязательство
* перед пайщиками на 80 счёте не теряется. Из-за этого migrator-048 (L3 для
* w.reg.minshr) на voskhod не запускается; жёсткий guard в migrate3 отвергает
* запись L3(voskhod, w.reg.minshr).
*
* L3 (userwallets) для USER_SHARED-кошельков заводит migrator (Phase 2 = 049
* для w.wal.share / w.wal.member / w.cap.blago). Здесь же только 5 пред-импорт-
* преимп-записей, выпавших из progwallets после ручных subbal'ов (см. ~/cleos.md,
* 5 пайщиков с РИД-взносами по договорам УХД, не успевшим попасть в электронный
* учёт до миграции).
*/
inline void migrate_voskhod_facts(eosio::name self_name, const cooperative2& coop) {
const eosio::name coopname = coop.username;
const eosio::symbol sym = _root_govern_symbol;
const eosio::asset zero(0, sym);
// ───────── accounts2 ─────────
emplace_account_balance(self_name, coopname, ledger2_accounts::BANK_ACCOUNT,
eosio::asset( 1'450'000'000LL, sym)); // 145 000.0000 RUB
emplace_account_balance(self_name, coopname, ledger2_accounts::INTANGIBLE_ASSETS,
eosio::asset( 623'533'110'000LL, sym)); // 62 353 311.0000 RUB
emplace_account_balance(self_name, coopname, ledger2_accounts::NON_CURRENT_INVESTMENTS,
eosio::asset( 5'752'000'000LL, sym)); // 575 200.0000 RUB
emplace_account_balance(self_name, coopname, ledger2_accounts::SHARE_FUND,
eosio::asset( 629'460'110'000LL, sym)); // 62 946 011.0000 RUB
emplace_account_balance(self_name, coopname, ledger2_accounts::TARGET_RECEIPTS,
eosio::asset( 1'275'000'000LL, sym)); // 127 500.0000 RUB
// ───────── wallets2 (L2) ─────────
// Мин.паевые на voskhod сразу размещены на w.sov.mnused (COOPERATIVE):
// бухгалтерски они уже потрачены через 08 (Dr 08 += 31 800), но обязательство
// Cr 80 += 31 800 сохранено. L3 для w.reg.minshr НЕ создаётся — см. guard в migrate3.
emplace_wallet_balance(self_name, coopname, ledger2_wallets::MIN_SHARE_USED,
eosio::asset( 318'000'000LL, sym), // 31 800.0000 RUB available
zero);
emplace_wallet_balance(self_name, coopname, ledger2_wallets::SHARE_FUND_PAY,
eosio::asset( 4'199'000'000LL, sym), // 419 900.0000 RUB available
zero);
emplace_wallet_balance(self_name, coopname, ledger2_wallets::BLAGOROST_FUND,
zero,
eosio::asset(570'443'110'000LL, sym)); // 57 044 311.0000 RUB blocked
emplace_wallet_balance(self_name, coopname, ledger2_wallets::PREIMP_FUND,
eosio::asset(54'500'000'000LL, sym), // 5 450 000.0000 RUB available
zero);
emplace_wallet_balance(self_name, coopname, ledger2_wallets::SOV_EXPENSES,
eosio::asset( 1'275'000'000LL, sym), // 127 500.0000 RUB available
zero);
// ───────── L3: преимп-учёты (5 пайщиков с РИД-взносами по договорам УХД) ─────────
// Сумма по таблице = 5 450 000 RUB = wallets2[w.cap.preimp].available ✓
emplace_preimp_if_present(self_name, coopname, "honruwpdxtty"_n, 500'000'000LL); // 50 000.0000 ЭГОЛ
emplace_preimp_if_present(self_name, coopname, "zlvsujtoctal"_n, 50'000'000'000LL); // 5 000 000.0000 Анацко
emplace_preimp_if_present(self_name, coopname, "yxkjufikzxri"_n, 1'000'000'000LL); // 100 000.0000 Кочетков
emplace_preimp_if_present(self_name, coopname, "vvqamckynxod"_n, 1'000'000'000LL); // 100 000.0000 Мильшин
emplace_preimp_if_present(self_name, coopname, "hntppjjknmsu"_n, 2'000'000'000LL); // 200 000.0000 Манакин
}
#endif // !IS_TESTNET
/**
* Мигрирует один кооператив. Отправляет до 4 inline apply (бух-проводки)
* + прямой emplace программных кошельков (без проводок).
*
* В prod-сборке для voskhod спец-ветка по фактам (см. migrate_voskhod_facts).
* В test-сборке (IS_TESTNET=1) спец-ветка отключена voskhod проходит
* стандартный арифметический путь, чтобы invariant-фейлы были видимы.
*/
inline void migrate_one_coop(eosio::name self_name, const cooperative2& coop) {
const eosio::name coopname = coop.username;
#ifndef IS_TESTNET
// voskhod — особый кейс в prod: переносим по фактическим суммам, не арифметически.
// В test-сборке этот блок выключен — voskhod идёт по стандартному пути.
if (coopname == "voskhod"_n) {
migrate_voskhod_facts(self_name, coop);
return;
}
#endif
const LegacyBalances b = read_legacy_balances(coopname);
// Программные кошельки читаем всегда (Благорост + Генератор).
const eosio::asset blagorost_invest = sum_progwallet_blocked(coopname, 4); // ЦПП «Благорост»
const eosio::asset generator_commit = sum_progwallet_blocked(coopname, 3); // ЦПП «Генератор»
// Ранний выход: нечего мигрировать (ни legacy, ни программных кошельков).
if (b.cash.amount == 0 && b.share.amount == 0 && b.entry.amount == 0 &&
blagorost_invest.amount == 0 && generator_commit.amount == 0) {
return;
}
// ----- A. Бухгалтерский перенос legacy::accounts через 3 TRANSIT_* -----
if (b.cash.amount > 0 || b.share.amount > 0 || b.entry.amount > 0) {
#ifdef IS_TESTNET
// На тестнете терпим грязные данные: clamp'им вместо abort.
// Тестовые остатки (entry > cash, РИД-часть на 80, Σmin > share) — игнорятся.
const int64_t entry_amt = b.entry.amount < b.cash.amount ? b.entry.amount : b.cash.amount;
const int64_t share_money_amt = b.cash.amount - entry_amt;
const eosio::asset entry_eff(entry_amt, _root_govern_symbol);
const eosio::asset share_money(share_money_amt, _root_govern_symbol);
eosio::asset min_total = compute_min_total_by_type(coopname, coop);
if (min_total.amount > share_money.amount) {
min_total = share_money;
}
const eosio::asset share_remain(
share_money.amount - min_total.amount,
_root_govern_symbol
);
send_transit(self_name, coopname, operations::migration::MIN_SHARE, min_total);
send_transit(self_name, coopname, operations::migration::SHARE, share_remain);
send_transit(self_name, coopname, operations::migration::ENTRY, entry_eff);
#else
// Деньги на паевой фонд: всё на 51 сверх вступительных.
const eosio::asset share_money(
b.cash.amount >= b.entry.amount ? b.cash.amount - b.entry.amount : 0,
_root_govern_symbol
);
eosio::check(b.cash.amount >= b.entry.amount,
std::string{"migrate: entry > cash на кооп "} + coopname.to_string());
// Инвариант: legacy::accounts[80] (паевой фонд) равен money-части (51 - 86).
// РИД-часть на legacy 80 не поддерживается этим путём (ADR-009: РИД-программа
// переезжает в w.cap.blago отдельно); если такой остаток есть — миграция
// отказывается, кооператив должен закрыть РИД-часть до миграции.
eosio::check(b.share.amount == share_money.amount,
std::string{"migrate: legacy 80 содержит РИД-часть на кооп "} + coopname.to_string() +
" — ledger2 не поддерживает РИД-перенос (ADR-009). Закрыть РИД до миграции.");
// Минимальный паевой: Σ p.minimum_amount по accepted-пайщикам.
// По конструкции эта сумма равна реальному взносу на legacy[80] и не может
// превосходить share_money. Если превосходит — данные кооператива
// не консистентны (например, у кого-то на participant.minimum_amount
// выставлено больше, чем фактически попало на 80). Падаем явно — пусть
// оператор разбирается ad-hoc до миграции; clamping мы сознательно НЕ
// делаем, иначе L2 получится меньше Σ L3 и сломаем инвариант.
const eosio::asset min_total = compute_min_total_by_type(coopname, coop);
eosio::check(min_total.amount <= share_money.amount,
std::string{"migrate: Σ participant.minimum_amount > share_money на кооп "} +
coopname.to_string() + " (Σ min=" + min_total.to_string() +
", share=" + share_money.to_string() +
"). Данные кооп-пайщиков не сходятся с legacy[80]; чинить ad-hoc до миграции.");
const eosio::asset share_remain(
share_money.amount - min_total.amount,
_root_govern_symbol
);
send_transit(self_name, coopname, operations::migration::MIN_SHARE, min_total);
send_transit(self_name, coopname, operations::migration::SHARE, share_remain);
send_transit(self_name, coopname, operations::migration::ENTRY, b.entry);
#endif
}
// ----- B. Программные кошельки — прямой emplace, без Dr/Cr -----
// progwallets.blocked НЕ входит в legacy::accounts[80], поэтому
// проводить их через apply(operations::migration::*) вызвало бы двойной учёт.
emplace_wallet_only(self_name, coopname, ledger2_wallets::BLAGOROST_FUND, blagorost_invest);
emplace_wallet_only(self_name, coopname, ledger2_wallets::GENERATOR_FUND, generator_commit);
}
} // namespace
void ledger2::migrate(uint64_t from_coop_index, uint64_t limit) {
void ledger2::migrate() {
require_auth(get_self());
ledger2_meta_index meta_tbl(get_self(), get_self().value);
auto meta_it = meta_tbl.find(0);
// Полный прогон уже завершён — тихий no-op (Decision #D4 / Story 1.15 AC5).
if (meta_it != meta_tbl.end() && meta_it->migrated) {
return;
}
if (meta_it != meta_tbl.end()) {
eosio::check(from_coop_index == meta_it->last_migrated_coop_index,
std::string{"migrate: from_coop_index должен продолжать с last_migrated_coop_index="} +
std::to_string(meta_it->last_migrated_coop_index));
} else {
eosio::check(from_coop_index == 0,
"migrate: первый вызов должен начинаться с from_coop_index=0");
}
eosio::check(limit > 0, "migrate: limit должен быть больше 0");
cooperatives2_index coops(_registrator, _registrator.value);
uint64_t idx = 0;
uint64_t done = 0;
bool reached_end = true;
for (auto c = coops.begin(); c != coops.end(); ++c) {
const eosio::name coopname = c->username;
for (auto it = coops.begin(); it != coops.end(); ++it, ++idx) {
if (idx < from_coop_index) continue;
if (done >= limit) { reached_end = false; break; }
// Мигрируем только активные кооперативы (не отделения, не физ. записи,
// не приостановленные/архивные). apply() всё равно отказывает не-active
// через get_cooperative_or_fail — здесь явно пропускаем, чтобы курсор шёл.
const bool is_active = it->status.has_value() && it->status.value() == "active"_n;
if (it->is_cooperative && is_active) {
migrate_one_coop(get_self(), *it);
// --- L3: userwallets[coopname] ---
// Модифицируем только не-ключевые поля (available/blocked) — итерация по
// первичному индексу с modify безопасна (порядок строк не меняется).
userwallets_index user_wallets(get_self(), coopname.value);
for (auto it = user_wallets.begin(); it != user_wallets.end(); ++it) {
if (it->blocked.amount <= 0) continue;
user_wallets.modify(it, get_self(), [&](auto& r) {
r.available += r.blocked;
r.blocked = eosio::asset(0, r.blocked.symbol);
});
}
++done;
}
if (done == 0 && from_coop_index == 0 && coops.begin() == coops.end()) {
eosio::check(false, "migrate: таблица cooperatives пуста — повторите после регистрации");
}
const uint64_t new_last_index = from_coop_index + done;
const uint64_t coops_total = meta_it == meta_tbl.end() ? done : meta_it->migrated_coops + done;
const auto now = eosio::current_time_point();
if (meta_it == meta_tbl.end()) {
meta_tbl.emplace(get_self(), [&](auto& m) {
m.id = 0;
m.migrated = reached_end;
m.migrated_coops = coops_total;
m.last_migrated_coop_index = new_last_index;
m.migrated_at = now;
});
} else {
meta_tbl.modify(meta_it, get_self(), [&](auto& m) {
m.migrated = reached_end;
m.migrated_coops = coops_total;
m.last_migrated_coop_index = new_last_index;
m.migrated_at = now;
});
// --- L2: wallets2[coopname] ---
wallets2_index wallets(get_self(), coopname.value);
for (auto it = wallets.begin(); it != wallets.end(); ++it) {
if (it->blocked.amount <= 0) continue;
wallets.modify(it, get_self(), [&](auto& w) {
w.available += w.blocked;
w.blocked = eosio::asset(0, w.blocked.symbol);
});
}
}
}
+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
/**
@@ -31,9 +31,9 @@
* - `WalletOp::WALLET_ONLY` удалён (ADR-003): «без бухпроводок» определяется
* парой `(debit_account_id == 0, credit_account_id == 0)` на уровне записи.
* Для `o.cap.invest` теперь TRANSFER без проводок (оба account_id == 0).
* - Добавлен `WalletOp::BURN` (ADR-003): `available -= amount` на `wallet_from`,
* без `wallet_to`. На текущем этапе в `OPERATION_REGISTRY` не используется
* зарезервирован под будущие операции штатного сжигания.
* - `WalletOp::BURN` (ADR-003): `available -= amount` на `wallet_from`,
* без `wallet_to`. Используется в `o.wal.wthcpl` (сжигание резерва возврата
* с `w.wal.wpend`), `o.cap.drppre` и как зеркало ISSUE в `revert`.
* - Единые программные кошельки: `BLAGOROST_FUND` (`w.cap.blago`) и
* `GENERATOR_FUND` (`w.cap.gen`) заменили ранее декомпозированные
* `bginv/bgprop/bgrid/bgmem` и `gncom/gnmem` (ADR-009).
@@ -61,9 +61,9 @@ namespace operations {
// wallet
namespace wallet {
inline constexpr eosio::name COMPLETE_DEPOSIT = "o.wal.depcpl"_n; ///< Завершение внесения паевого взноса (Dr 51 / Cr 80, ISSUE SHARE_FUND_PAY).
inline constexpr eosio::name COMPLETE_WITHDRAW = "o.wal.wthcpl"_n; ///< Завершение возврата паевого взноса (Dr 80 / Cr 51, TRANSFER SHARE_FUND_PAY → WITHDRAWALS_SINK).
inline constexpr eosio::name REQUEST_WITHDRAW = "o.wal.wthreq"_n; ///< Запрос на возврат паевого: BLOCK на SHARE_FUND_PAY (без Dr/Cr).
inline constexpr eosio::name DECLINE_WITHDRAW = "o.wal.wthdec"_n; ///< Отклонение запроса на возврат: UNBLOCK на SHARE_FUND_PAY (без Dr/Cr).
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 в «Столе заказов».
}
@@ -83,26 +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 — членская модель «Стола заказов» (refactor 2026-05-11; старые
// o.mkt.supply / o.mkt.recv — клиринговые — удалены).
//
// Составные бухгалтерские проводки через транзит счёта 91 «Прочие доходы
// и расходы» разбиты на пары операций (часть 1 + часть 2). Контракт
// вызывает обе последовательно в одной транзакции Antelope — атомарность
// обеспечивается транзакцией. Имена с суффиксом «2» — закрытие транзита.
// marketplace — членская модель «Стола заказов».
namespace marketplace {
inline constexpr eosio::name ASSIGN_TO_PROGRAM = "o.mkt.assign"_n; ///< Целевое назначение членского взноса пайщика в программу Marketplace (TRANSFER CK_MEMBER → MARKETPLACE_MEMBER, без проводки — оба кошелька на счёте 86). Conditional-шаг серии createorder.
inline constexpr eosio::name BLOCK_FOR_ORDER = "o.mkt.block"_n; ///< Блокировка членского взноса заказчика под конкретный Order (BLOCK на MARKETPLACE_MEMBER, без Dr/Cr).
inline constexpr eosio::name UNBLOCK_ON_CANCEL = "o.mkt.unblk"_n; ///< Разблокировка членского взноса при отмене Order'а (UNBLOCK на MARKETPLACE_MEMBER, без Dr/Cr). Сумма остаётся на .available и может быть потрачена на следующий заказ в программе.
inline constexpr eosio::name RECALL_TO_UNIVERSAL = "o.mkt.recall"_n; ///< Вывод программного членского взноса в универсальный членский кошелёк пайщика (TRANSFER MARKETPLACE_MEMBER → CK_MEMBER, без проводки — оба кошелька на счёте 86). Явное действие пайщика, не часть авто-flow отмены.
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; ///< Выдача имущества пайщику по АПП выдачи (часть 1 композитной проводки через транзит 91): Dr 91 / Cr 10, REVOKE MARKETPLACE_MEMBER.blocked → 0 — выбытие имущества со склада на «прочие». Закрытие транзита — отдельной операцией CONSUME_TRANSIT_CLOSE, атомарно в той же транзакции signiss2.
inline constexpr eosio::name CONSUME_TRANSIT_CLOSE = "o.mkt.consum2"_n; ///< Выдача имущества пайщику (часть 2 композитной проводки): Dr 86 / Cr 91, NONE — закрытие транзита 91 на счёт ЦФ программы. Срабатывает после CONSUME_BY_MEMBER в той же транзакции.
inline constexpr eosio::name RETURN_BY_MEMBER = "o.mkt.return"_n; ///< Гарантийный возврат имущества пайщиком — compensating forward к CONSUME_BY_MEMBER (часть 1 композитной проводки): Dr 91 / Cr 86, ISSUE ∅ → MARKETPLACE_MEMBER — восстановление «прочих» за счёт ЦФ + восстановление .available заказчика. Реверты ledger2::revert в Столе заказов не используются.
inline constexpr eosio::name RETURN_TRANSIT_CLOSE = "o.mkt.return2"_n; ///< Гарантийный возврат (часть 2 композитной проводки): Dr 10 / Cr 91, NONE — имущество назад на склад через закрытие транзита. Срабатывает после RETURN_BY_MEMBER в той же транзакции decretvisit.
inline constexpr eosio::name WRITE_OFF_PERISHABLE = "o.mkt.wroff"_n; ///< Утилизация скоропорта со склада (часть 1 композитной проводки): Dr 91 / Cr 10, NONE — выбытие со склада на «прочие». По протоколу совета.
inline constexpr eosio::name WRITE_OFF_TRANSIT_CLOSE = "o.mkt.wroff2"_n; ///< Утилизация скоропорта (часть 2 композитной проводки): Dr 86 / Cr 91, NONE — закрытие транзита 91 на счёт ЦФ программы. Срабатывает после WRITE_OFF_PERISHABLE в той же транзакции execwroff.
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
@@ -146,14 +137,23 @@ namespace operations {
* `(debit_account_id == 0, credit_account_id == 0)` на уровне записи реестра.
* Compile-time правило `(debit==0) (credit==0)` ловит смешанные пары.
*/
//
// Удаление BLOCK/UNBLOCK/BURN_BLOCKED (2026-05-24): механика «заблокированного»
// баланса упразднена. Резерв средств под заявку на возврат паевого теперь
// выражается переводом на отдельный кошелёк-резерв `w.wal.wpend` (TRANSFER),
// возврат резерва — обратным TRANSFER, завершение — BURN с резерва. Поле
// `blocked` в таблицах wallets2/userwallets оставлено deprecated (всегда 0
// после ledger2::migrate-свёртки) — физическое удаление поля = небезопасная
// смена layout таблицы на живых коопах, выносится в отдельный cleanup-деплой.
//
// Числовые значения ISSUE/TRANSFER/BURN/NONE СОХРАНЕНЫ (не перенумерованы),
// чтобы исторические op_code в blockchain_actions читались бэкендом без сдвига
// смысла (2 и 3 — бывшие BLOCK/UNBLOCK — больше не выдаются и невалидны на входе).
enum class WalletOp : uint8_t {
ISSUE = 0, ///< первичный вход средств на кошелёк wallet_to (wallet_from = empty)
TRANSFER = 1, ///< перемещение wallet_from → wallet_to (с Dr/Cr ИЛИ без — по парам account_id)
BLOCK = 2, ///< available-=amount, blocked+=amount на wallet_from
UNBLOCK = 3, ///< blocked-=amount, available+=amount на wallet_from
BURN = 4, ///< изъятие amount с wallet_from.available, без wallet_to. Покрывает оба кейса: (a) штатное сжигание как бизнес-операция в OPERATION_REGISTRY; (b) зеркало ISSUE при `ledger2::revert` (различие — через operation_code: `o.adj.rev` для adjustment-mirror).
NONE = 5, ///< только бухпроводка без перемещения средств (wallet_from = empty, wallet_to = empty, debit ≠ 0, credit ≠ 0). Покрывает кейсы внутрибалансовых проводок типа Dr 04 / Cr 08 (приём РИД в НМА), когда кошелёк уже на нужном программном фонде.
REVOKE = 6, ///< изъятие amount с wallet_from.blocked в пустоту, без wallet_to. Обязательная пара Dr/Cr. Отличие от BURN — списание из blocked, а не из available; применяется на целевом расходовании предварительно заблокированной суммы (например, выдача имущества пайщику по АПП выдачи). Введён 2026-05-11 под членскую модель «Стола заказов» (o.mkt.consum).
ISSUE = 0, ///< первичный вход средств на кошелёк wallet_to (wallet_from = empty)
TRANSFER = 1, ///< перемещение wallet_from → wallet_to (с Dr/Cr ИЛИ без — по парам account_id)
BURN = 4, ///< изъятие amount с wallet_from->available, без wallet_to. Покрывает оба кейса: (a) штатное сжигание как бизнес-операция в OPERATION_REGISTRY; (b) зеркало ISSUE при `ledger2::revert` (различие — через operation_code: `o.adj.rev` для adjustment-mirror).
NONE = 5, ///< только бухпроводка без перемещения средств (wallet_from = empty, wallet_to = empty, debit ≠ 0, credit ≠ 0). Покрывает кейсы внутрибалансовых проводок типа Dr 04 / Cr 08 (приём РИД в НМА), когда кошелёк уже на нужном программном фонде.
};
/**
@@ -162,7 +162,7 @@ enum class WalletOp : uint8_t {
* Семантика полей по `wallet_op`:
* - ISSUE: wallet_from = eosio::name{}, wallet_to = required.
* - TRANSFER: wallet_from = required, wallet_to = required ( from).
* - BLOCK / UNBLOCK / BURN: wallet_from = required, wallet_to = eosio::name{}.
* - BURN: wallet_from = required, wallet_to = eosio::name{}.
*
* Семантика бух.проводки:
* - Без проводок: debit_account_id == 0 И credit_account_id == 0.
@@ -174,7 +174,7 @@ struct OperationRegistryEntry {
eosio::name process_type; ///< тип процесса с префиксом `p.<contract>.<noun>`
WalletOp wallet_op;
eosio::name wallet_from; ///< пустое имя для ISSUE
eosio::name wallet_to; ///< пустое имя для BLOCK/UNBLOCK/BURN
eosio::name wallet_to; ///< пустое имя для BURN
uint64_t debit_account_id; ///< 0 если без бухпроводки (тогда credit_account_id тоже == 0)
uint64_t credit_account_id; ///< 0 если без бухпроводки (тогда debit_account_id тоже == 0)
const char* human_name;
@@ -201,9 +201,12 @@ static constexpr OperationRegistryEntry OPERATION_REGISTRY[] = {
ledger2_accounts::BANK_ACCOUNT, ledger2_accounts::SHARE_FUND,
"Внесение пайщиком паевого взноса" },
// 4. Возврат паевого взноса: Dr 80 / Cr 51, TRANSFER SHARE_FUND_PAY → WITHDRAWALS_SINK
{ operations::wallet::COMPLETE_WITHDRAW, processes::wallet::WITHDRAW, WalletOp::TRANSFER,
ledger2_wallets::SHARE_FUND_PAY, ledger2_wallets::WITHDRAWALS_SINK,
// 4. Возврат паевого взноса: Dr 80 / Cr 51, BURN WITHDRAW_PENDING.
// Сжигание из кошелька-резерва (TRANSFER в резерв был на REQUEST_WITHDRAW):
// деньги уходят из системы (банковский перевод пайщику), получателя на цепи нет.
// Бухгалтерия: паевой фонд уменьшается (Дт 80), расчётный счёт уменьшается (Кт 51).
{ operations::wallet::COMPLETE_WITHDRAW, processes::wallet::WITHDRAW, WalletOp::BURN,
ledger2_wallets::WITHDRAW_PENDING, eosio::name{},
ledger2_accounts::SHARE_FUND, ledger2_accounts::BANK_ACCOUNT,
"Возврат паевого взноса пайщику" },
@@ -270,115 +273,82 @@ static constexpr OperationRegistryEntry OPERATION_REGISTRY[] = {
ledger2_accounts::SHARE_FUND, ledger2_accounts::FINANCIAL_INVESTMENTS,
"Возврат беспроцентного займа пайщика по акту-2" },
// ===== Marketplace: членская модель «Стола заказов» (refactor 2026-05-11) =====
// Старые операции o.mkt.supply (CONFIRM_SUPPLY) и o.mkt.recv (CONFIRM_RECEIPT)
// удалены вместе с клиринговой моделью (паевой взнос имуществом от
// поставщика + возврат паевого имуществом заказчику) — она объявлена
// out-of-MVP до изменений в законодательстве.
// 12a. p.mkt.supply: Конвертация цифрового рубля заказчика в универсальный
// членский кошелёк пайщика (Dr 80 / Cr 86, TRANSFER w.wal.share → w.wal.member).
// Conditional-шаг серии createorder — выполняется только при недостаче
// на w.wal.member.available.
{ operations::wallet::CONVERT_TO_MEMBER, processes::marketplace::SUPPLY, WalletOp::TRANSFER,
ledger2_wallets::SHARE_FUND_PAY, ledger2_wallets::CK_MEMBER,
// 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,
"Конвертация цифрового рубля в членский кошелёк пайщика" },
"Резервирование под заказ" },
// 12b. p.mkt.supply: Целевое назначение членского в программу Marketplace
// (TRANSFER w.wal.member → w.mkt.member, без Dr/Cr — оба на 86, аналитика на L2).
// Conditional-шаг серии createorder.
{ operations::marketplace::ASSIGN_TO_PROGRAM, processes::marketplace::SUPPLY, WalletOp::TRANSFER,
ledger2_wallets::CK_MEMBER, ledger2_wallets::MARKETPLACE_MEMBER,
// 12. 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,
"Целевое назначение членского взноса в программу «Стол заказов»" },
"Добор резерва заказа с членского «Стола заказов»" },
// 12c. p.mkt.supply: Блокировка под Order (BLOCK на w.mkt.member, без Dr/Cr).
{ operations::marketplace::BLOCK_FOR_ORDER, processes::marketplace::SUPPLY, WalletOp::BLOCK,
ledger2_wallets::MARKETPLACE_MEMBER, eosio::name{},
// 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,
"Блокировка членского взноса под заказ" },
"Снятие резерва при отмене заказа" },
// 12d. p.mkt.supply: Разблокировка при отмене Order'а (UNBLOCK, без Dr/Cr).
// Срабатывает на cancelorder / expirecycle / declinebatch. Сумма остаётся
// на w.mkt.member.available и может быть потрачена на следующий заказ.
{ operations::marketplace::UNBLOCK_ON_CANCEL, processes::marketplace::SUPPLY, WalletOp::UNBLOCK,
ledger2_wallets::MARKETPLACE_MEMBER, eosio::name{},
0, 0,
"Разблокировка членского взноса при отмене заказа" },
// 12e. p.mkt.supply: Вывод программного членского в универсальный членский
// (TRANSFER w.mkt.member → w.wal.member, без Dr/Cr — оба на 86).
// Явное действие пайщика — не часть авто-flow отмены.
{ operations::marketplace::RECALL_TO_UNIVERSAL, processes::marketplace::SUPPLY, WalletOp::TRANSFER,
ledger2_wallets::MARKETPLACE_MEMBER, ledger2_wallets::CK_MEMBER,
0, 0,
"Вывод членского взноса в универсальный членский кошелёк" },
// 12f. p.mkt.supply: Приёмка имущества кооперативом по АПП приёмки
// 12c. p.mkt.supply: Приёмка имущества кооперативом по АПП приёмки
// (Dr 10 / Cr 86, NONE — только бухпроводка, кошельки не двигаются).
// Имущество — аналитикой по счёту 10 (per-КУ субсчета), без отдельного кошелька.
// Атомарно с PAYOUT на закрывающей подписи председателя АПП приёмки.
// Атомарно с PAY_SUPPLIER на закрывающей подписи председателя АПП приёмки.
{ operations::marketplace::PURCHASE_FROM_SUPPLIER, processes::marketplace::SUPPLY, WalletOp::NONE,
eosio::name{}, eosio::name{},
ledger2_accounts::MATERIALS, ledger2_accounts::TARGET_RECEIPTS,
"Приёмка имущества кооперативом по АПП приёмки" },
// 12g. p.mkt.supply: Оплата поставщику с расчётного счёта
// (Dr 86 / Cr 51, ISSUE ∅ → w.mkt.payout). Атомарно с PURCH.
// 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,
"Оплата поставщику с расчётного счёта по факту приёмки" },
// 12h. p.mkt.supply: Выдача имущества пайщику по АПП выдачи — часть 1.
// REVOKE на w.mkt.member.blocked → 0 (целевой расход членского) +
// Dr 91 / Cr 10 (выбытие имущества со склада на «прочие»).
// Согласовано с Ангелиной 2026-05-11. Атомарность с CONSUME_TRANSIT_CLOSE
// обеспечивается транзакцией Antelope — контракт вызывает обе операции
// последовательно в signiss2.
{ operations::marketplace::CONSUME_BY_MEMBER, processes::marketplace::SUPPLY, WalletOp::REVOKE,
ledger2_wallets::MARKETPLACE_MEMBER, eosio::name{},
ledger2_accounts::OTHER_INCOME_EXPENSES, ledger2_accounts::MATERIALS,
"Выдача имущества пайщику по АПП выдачи — выбытие со склада" },
// 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,
"Выдача имущества пайщику по АПП выдачи" },
// 12h2. p.mkt.supply: Выдача — часть 2. NONE Dr 86 / Cr 91 (закрытие
// транзита 91 на счёт ЦФ программы). Кошельки не двигаются.
{ operations::marketplace::CONSUME_TRANSIT_CLOSE, processes::marketplace::SUPPLY, WalletOp::NONE,
eosio::name{}, eosio::name{},
ledger2_accounts::TARGET_RECEIPTS, ledger2_accounts::OTHER_INCOME_EXPENSES,
"Выдача имущества пайщику — закрытие транзита на ЦФ программы" },
// 12i. p.mkt.return: Гарантийный возврат — часть 1. ISSUE ∅ → w.mkt.member
// (восстановление средств заказчика в программе) + Dr 91 / Cr 86
// (восстановление «прочих» за счёт ЦФ — зеркало 12h2). Compensating
// forward к CONSUM; ledger2::revert в Столе заказов не используется.
// Атомарность с RETURN_TRANSIT_CLOSE — транзакция Antelope в decretvisit.
// 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,
ledger2_accounts::OTHER_INCOME_EXPENSES, ledger2_accounts::TARGET_RECEIPTS,
"Гарантийный возврат — восстановление средств в программе" },
eosio::name{}, ledger2_wallets::MARKETPLACE_MEMBER_FUND,
ledger2_accounts::MATERIALS, ledger2_accounts::TARGET_RECEIPTS,
"Гарантийный возврат — восстановление средств и имущества" },
// 12i2. p.mkt.return: Гарантийный возврат — часть 2. NONE Dr 10 / Cr 91
// (имущество назад на склад через транзит — зеркало 12h).
{ operations::marketplace::RETURN_TRANSIT_CLOSE, processes::marketplace::RETURN, WalletOp::NONE,
eosio::name{}, eosio::name{},
ledger2_accounts::MATERIALS, ledger2_accounts::OTHER_INCOME_EXPENSES,
"Гарантийный возврат — имущество назад на склад через транзит" },
// 12j. p.mkt.wroff: Утилизация скоропорта — часть 1. NONE Dr 91 / Cr 10
// (выбытие со склада на «прочие»). По протоколу совета.
// 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::OTHER_INCOME_EXPENSES, ledger2_accounts::MATERIALS,
"Утилизация скоропорта — выбытие со склада" },
// 12j2. p.mkt.wroff: Утилизация скоропорта — часть 2. NONE Dr 86 / Cr 91
// (закрытие транзита). Атомарно с WROFF в execwroff.
{ operations::marketplace::WRITE_OFF_TRANSIT_CLOSE, processes::marketplace::WRITEOFF, WalletOp::NONE,
eosio::name{}, eosio::name{},
ledger2_accounts::TARGET_RECEIPTS, ledger2_accounts::OTHER_INCOME_EXPENSES,
"Утилизация скоропорта — закрытие транзита на ЦФ программы" },
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,
@@ -386,17 +356,19 @@ static constexpr OperationRegistryEntry OPERATION_REGISTRY[] = {
ledger2_accounts::SHARE_FUND, ledger2_accounts::TARGET_RECEIPTS,
"Трансляция паевого взноса из ЦПП «Цифровой Кошелёк» в членский взнос за пользование инфраструктурой" },
// 15. Запрос на возврат паевого: BLOCK SHARE_FUND_PAY (без Dr/Cr — внутри одного бухсчёта 80).
{ operations::wallet::REQUEST_WITHDRAW, processes::wallet::WITHDRAW, WalletOp::BLOCK,
ledger2_wallets::SHARE_FUND_PAY, eosio::name{},
// 15. Запрос на возврат паевого: TRANSFER SHARE_FUND_PAY → WITHDRAW_PENDING
// (без Dr/Cr — оба кошелька на счёте 80; резерв средств на время рассмотрения).
{ operations::wallet::REQUEST_WITHDRAW, processes::wallet::WITHDRAW, WalletOp::TRANSFER,
ledger2_wallets::SHARE_FUND_PAY, ledger2_wallets::WITHDRAW_PENDING,
0, 0,
"Блокировка паевого под запрос на возврат" },
"Резервирование паевого под запрос на возврат" },
// 16. Отклонение запроса на возврат: UNBLOCK SHARE_FUND_PAY (без Dr/Cr — зеркало REQUEST_WITHDRAW).
{ operations::wallet::DECLINE_WITHDRAW, processes::wallet::WITHDRAW, WalletOp::UNBLOCK,
ledger2_wallets::SHARE_FUND_PAY, eosio::name{},
// 16. Отклонение запроса на возврат: TRANSFER WITHDRAW_PENDING → SHARE_FUND_PAY
// (без Dr/Cr — зеркало REQUEST_WITHDRAW; возврат резерва пайщику).
{ operations::wallet::DECLINE_WITHDRAW, processes::wallet::WITHDRAW, WalletOp::TRANSFER,
ledger2_wallets::WITHDRAW_PENDING, ledger2_wallets::SHARE_FUND_PAY,
0, 0,
"Разблокировка паевого после отклонения запроса на возврат" },
"Снятие резерва паевого после отклонения запроса на возврат" },
// 17. Возврат из ЦПП «Благорост» в Цифровой Кошелёк: TRANSFER BLAGOROST_FUND → SHARE_FUND_PAY (без Dr/Cr — оба счёта 80, зеркало INVEST).
{ operations::capital::WITHDRAW_FROM_CAPITAL, processes::capital::WTHCAP, WalletOp::TRANSFER,
@@ -453,7 +425,7 @@ static constexpr size_t OPERATION_REGISTRY_SIZE = sizeof(OPERATION_REGISTRY) / s
// оба существуют в `LEDGER2_ACCOUNT_MAP`.
// 4. Для TRANSFER: `wallet_from` ≠ `wallet_to`, оба ≠ 0.
// 5. Для ISSUE: `wallet_from` == 0 и `wallet_to` ≠ 0.
// 6. Для BLOCK / UNBLOCK / BURN: `wallet_from` ≠ 0, `wallet_to` == 0.
// 6. Для BURN: `wallet_from` ≠ 0, `wallet_to` == 0.
// 7. Все id кошельков из записей существуют в `LEDGER2_WALLET_REGISTRY`.
namespace ledger2_registry_detail {
constexpr bool operation_codes_unique() {
@@ -508,22 +480,6 @@ namespace ledger2_registry_detail {
return true;
}
// Правило 6b: REVOKE — wallet_from required, wallet_to == 0,
// обязательная пара Dr/Cr ≠ 0 (списание blocked в пустоту требует
// фиксации целевого расхода и выбытия актива). Введён 2026-05-11
// под `o.mkt.consum` (выдача имущества пайщику по АПП выдачи).
constexpr bool revoke_pattern_correct() {
for (size_t i = 0; i < OPERATION_REGISTRY_SIZE; ++i) {
const auto& e = OPERATION_REGISTRY[i];
if (e.wallet_op != WalletOp::REVOKE) continue;
if (e.wallet_from.value == 0) return false;
if (e.wallet_to.value != 0) return false;
if (e.debit_account_id == 0) return false;
if (e.credit_account_id == 0) return false;
}
return true;
}
// Правило 8: NONE — оба wallet пустые, обе проводки обязательны (Dr ≠ 0, Cr ≠ 0).
// Семантика: только бухпроводка, кошельковое движение отсутствует.
constexpr bool none_pattern_correct() {
@@ -570,8 +526,6 @@ static_assert(ledger2_registry_detail::transfer_wallet_from_ne_to(),
"OPERATION_REGISTRY: TRANSFER с wallet_from == wallet_to или одним из них == 0");
static_assert(ledger2_registry_detail::burn_pattern_correct(),
"OPERATION_REGISTRY: BURN требует wallet_from ≠ 0 и wallet_to == 0");
static_assert(ledger2_registry_detail::revoke_pattern_correct(),
"OPERATION_REGISTRY: REVOKE требует wallet_from ≠ 0, wallet_to == 0 и обе проводки Dr/Cr ≠ 0");
static_assert(ledger2_registry_detail::none_pattern_correct(),
"OPERATION_REGISTRY: NONE требует wallet_from == 0, wallet_to == 0 и обе проводки заполненными");
static_assert(ledger2_registry_detail::accounts_exist_in_map(),
@@ -15,11 +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::SUPPLY o.wal.conv + o.mkt.assign + o.mkt.block + o.mkt.unblk +
* o.mkt.recall + o.mkt.purch + o.mkt.payout +
* o.mkt.consum + o.mkt.consum2 (composite через 91)
* - processes::marketplace::RETURN o.mkt.return + o.mkt.return2 (composite через 91)
* - processes::marketplace::WRITEOFF o.mkt.wroff + o.mkt.wroff2 (composite через 91)
* - 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 совпадает с
@@ -61,9 +60,9 @@ namespace processes {
// marketplace
namespace marketplace {
inline constexpr eosio::name SUPPLY = "p.mkt.supply"_n; ///< Прямая поставка-приобретение имущества (9 операций: o.wal.conv + o.mkt.assign + o.mkt.block + o.mkt.unblk + o.mkt.recall + o.mkt.purch + o.mkt.payout + o.mkt.consum + o.mkt.consum2 — последние две композитная проводка через транзит 91). Старый p.mkt.reqst (клиринговый) удалён 2026-05-11.
inline constexpr eosio::name RETURN = "p.mkt.return"_n; ///< Гарантийный возврат имущества пайщиком — compensating forward к o.mkt.consum (o.mkt.return + o.mkt.return2 — композитная проводка через транзит 91).
inline constexpr eosio::name WRITEOFF = "p.mkt.wroff"_n; ///< Утилизация скоропорта со склада КУ (o.mkt.wroff + o.mkt.wroff2 — композитная проводка через транзит 91, по протоколу совета).
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
@@ -27,7 +27,7 @@
* w.mkt.* Маркетплейс (выплаты поставщикам)
*
* Sentinel `eosio::name{}` (пустое имя, value=0) «кошелёк вне системы»
* для ISSUE (нет wallet_from) и для BURN/BLOCK/UNBLOCK (нет wallet_to).
* для ISSUE (нет wallet_from) и для BURN (нет wallet_to).
*
* При первом ISSUE/TRANSFER кошелёк создаётся автоматически по записи
* из WALLET_REGISTRY. При обнулении available+blocked запись удаляется.
@@ -46,7 +46,8 @@ struct ledger2_wallets {
// wallet — паевой фонд + возвраты + ЦК
static constexpr eosio::name SHARE_FUND_PAY = "w.wal.share"_n; ///< Паевой взнос пайщика (USER_SHARED)
static constexpr eosio::name CK_MEMBER = "w.wal.member"_n; ///< ЦК — членская часть пайщика (USER_SHARED)
static constexpr eosio::name WITHDRAWALS_SINK = "w.wal.wthdrw"_n; ///< Возвраты паевых взносов пайщикам (sink TRANSFER, COOPERATIVE)
static constexpr eosio::name WITHDRAWALS_SINK = "w.wal.wthdrw"_n; ///< DEPRECATED 2026-05-21: исторический sink возвратов. Оставлен в реестре для исторических L2-балансов (накопленные возвраты до перехода). Не использовать в новых операциях.
static constexpr eosio::name WITHDRAW_PENDING = "w.wal.wpend"_n; ///< Резерв паевого под заявку на возврат (COOPERATIVE-пул). o.wal.wthreq переводит сюда с w.wal.share, o.wal.wthdec возвращает обратно, o.wal.wthcpl сжигает отсюда. Заменил механику blocked/BLOCK/UNBLOCK 2026-05-24.
// registrator — минимальный паевой + вступительные
static constexpr eosio::name MIN_SHARE_FUND = "w.reg.minshr"_n; ///< Минимальный паевой взнос пайщика (USER_SHARED, без сверки соглашений)
@@ -61,12 +62,13 @@ struct ledger2_wallets {
// capital — единые программные кошельки + займы + пред-импорт
static constexpr eosio::name LOAN_ISSUED = "w.cap.loan"_n; ///< Выданные пайщикам беспроцентные займы (COOPERATIVE; Dr 58 / Cr 51)
static constexpr eosio::name BLAGOROST_FUND = "w.cap.blago"_n; ///< Благорост — единый агрегированный кошелёк программы (USER_SHARED; ADR-009)
static constexpr eosio::name GENERATOR_FUND = "w.cap.gen"_n; ///< Генератор — единый агрегированный кошелёк программы (USER_SHARED; ADR-009)
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 MARKETPLACE_MEMBER = "w.mkt.member"_n; ///< ЦПП «Стол Заказов» — программный членский кошелёк пайщика (USER_SHARED; BLOCK/UNBLOCK/REVOKE под Order)
static constexpr eosio::name SUPPLIER_PAYMENTS = "w.mkt.payout"_n; ///< Выплаты поставщикам (sink PAYOUT, 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,19 +97,25 @@ struct Ledger2WalletMeta {
WalletKind kind;
};
inline constexpr std::array<Ledger2WalletMeta, 15> LEDGER2_WALLET_REGISTRY = {{
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::GENERATOR_FUND, "ЦПП «Генератор» — единый кошелёк программы у пайщика", WalletKind::USER_SHARED },
{ ledger2_wallets::PREIMP_FUND, "Первичный учёт РИД-взносов до перехода на электронный учёт", WalletKind::USER_SHARED },
{ ledger2_wallets::MARKETPLACE_MEMBER, "ЦПП «Стол Заказов» — программный членский у пайщика", WalletKind::USER_SHARED },
{ 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 (8) — единый кооперативный баланс, без L3
// COOPERATIVE (10) — единый кооперативный баланс, без L3
// GENERATOR_FUND переведён сюда из USER_SHARED (см. wallets.hpp:64) —
// CRPS-распределение между сегментами проекта не поддерживает per-user
// компенсирующие TRANSFER на approvecmmt, поэтому L3-проверка walletop
// ломала convertsegm у пайщиков, чья доля выросла через CRPS.
{ ledger2_wallets::GENERATOR_FUND, "ЦПП «Генератор» — единый кошелёк программы", WalletKind::COOPERATIVE },
{ ledger2_wallets::ENTRANCE_FEES, "Вступительные взносы", WalletKind::COOPERATIVE },
{ ledger2_wallets::WITHDRAWALS_SINK, "Возвраты паевых взносов пайщикам", WalletKind::COOPERATIVE },
{ ledger2_wallets::WITHDRAWALS_SINK, "Возвраты паевых взносов пайщикам (deprecated, не используется в новых операциях)", WalletKind::COOPERATIVE },
{ ledger2_wallets::WITHDRAW_PENDING, "Резерв паевого под заявку на возврат", WalletKind::COOPERATIVE },
{ ledger2_wallets::INFRA_FEES, "Членские взносы за инфраструктуру кооп. платформы", WalletKind::COOPERATIVE },
{ ledger2_wallets::DELEGATE_FEES, "Делегатские членские взносы", WalletKind::COOPERATIVE },
{ ledger2_wallets::SOV_EXPENSES, "Хозяйственные расходы из числа целевого финансирования", WalletKind::COOPERATIVE },
@@ -226,14 +234,15 @@ struct Ledger2WalletProgramMapping {
uint64_t required_program_id; // 0 = исключение (без проверки)
};
inline constexpr std::array<Ledger2WalletProgramMapping, 7> 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_MEMBER, 2 /* Marketplace */ },
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 */ },
}};
/**
@@ -110,8 +110,8 @@ inline void update_writeoff_proposal(eosio::name coopname, uint64_t proposal_id,
// ── Cross-contract read: ledger2 wallet/userwallet balances ─────────────
//
// Используется в createorder для guard'а Locked Decision L6 (без отрицательного
// баланса) и для решения о пропуске conditional операций o.wal.conv / o.mkt.assign
// (если на целевом кошельке уже хватает available — соответствующий шаг скипается).
// баланса) — проверка достаточности средств заказчика на паевом кошельке
// перед вызовом o.mkt.lock.
//
// ВАЖНО: контракт marketplace не вызывает ledger2::walletop напрямую, а только
// читает state (RAM-таблицы wallets2 / userwallets через cross-contract scope).
@@ -124,5 +124,6 @@ namespace Names {
constexpr eosio::name CREATE_WITHDRAW_2 = "createwthd2"_n; // акцепт возврата из проекта
constexpr eosio::name CREATE_WITHDRAW_3 = "createwthd3"_n; // акцепт возврата из программы
constexpr eosio::name CREATE_RESULT = "createresult"_n; // акцепт результата
constexpr eosio::name CONVERT_SEGMENT = "convertsegm"_n; // финальная фаза p.cap.rid — трансляция паевого взноса
}
}
@@ -39,16 +39,24 @@ namespace OrderStatus {
}
/**
* @brief Тип цикла отсечки заявок поставщика (атрибут Offer'а Locked Decision L11).
* @brief Состояние выплаты поставщику по Order'у (Locked Decision L12, E11
* техдолг 598-16). Выплата идёт через gateway::createoutpay действие
* кассира callback `marketplace::payconfirm` / `marketplace::paydecline`.
*
* Сохраняется на Order'е, потому что фактический cycle_type фиксируется в момент
* createorder и не меняется при последующем редактировании Offer'а поставщиком.
* Допустимые переходы:
* 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 CycleType {
inline constexpr eosio::name TIME_BASED = "timebased"_n;
inline constexpr eosio::name VOLUME_BASED = "volumebased"_n;
inline constexpr eosio::name OPEN_SUBSCRIPT = "opensubscr"_n;
inline constexpr eosio::name INDIVIDUAL = "individual"_n;
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;
}
/**
@@ -119,7 +127,6 @@ struct [[eosio::table, eosio::contract(MARKETPLACE)]] order {
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)
eosio::name cycle_type = CycleType::TIME_BASED; ///< снимок cycle_type Offer'а на момент createorder
uint32_t warranty_period_secs = 0; ///< из Offer'а — для submretrn гард'а
time_point_sec warranty_until = time_point_sec(0); ///< now() + warranty_period_secs (заполняется в signiss2)
@@ -131,6 +138,9 @@ struct [[eosio::table, eosio::contract(MARKETPLACE)]] order {
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/
@@ -17,18 +17,26 @@ using namespace eosio;
/**
* @brief Статусы проекта решения совета о списании скоропорта (процесс p.mkt.wroff).
*
* Граф: draft executed (final, ledger2-операции применены)
* rejected (final, без ledger2-операций)
* Граф (выровнен под канонический паттерн «решение совета»
* `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 {
// Имя константы PROPOSED (а не DRAFT) — конфликт с макросом
// DRAFT="draft" из lib/consts.hpp; on-chain строка осталась "draft"_n,
// как в p.mkt.wroff.standard.yaml.
inline constexpr eosio::name PROPOSED = "draft"_n;
inline constexpr eosio::name EXECUTED = "executed"_n;
inline constexpr eosio::name REJECTED = "rejected"_n;
// 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;
}
/**
@@ -70,12 +78,11 @@ struct wroff_item {
*
* scope = coopname; primary_key = id; уникальность через `byhash` индекс на
* `proposal.hash` этот hash используется как `process_hash` во всех
* ledger2-операциях процесса (WROFF + WROFF2 по паре per-item).
* ledger2-операциях процесса (o.mkt.wroff per-item).
*
* `items` vector<wroff_item> позиций к списанию; на execwroff контракт
* последовательно вызывает `Ledger2::apply(o.mkt.wroff, item.amount, )` +
* `Ledger2::apply(o.mkt.wroff2, item.amount, )` для каждой позиции в
* одной транзакции Antelope.
* последовательно вызывает `Ledger2::apply(o.mkt.wroff, item.amount, )`
* для каждой позиции в одной транзакции Antelope.
*
* `protocol` document2 решения совета (signed_by: council_members).
* Подпись через стандартный sov.decision-протокол (см. p.mkt.wroff.standard.yaml
@@ -92,10 +99,12 @@ struct [[eosio::table, eosio::contract(MARKETPLACE)]] writeoff_proposal {
eosio::asset total_amount = asset(0, _root_govern_symbol); ///< Σ items.amount (для UI / отчёта)
eosio::name status = WroffStatus::PROPOSED;
document2 protocol; ///< протокол решения совета (для execwroff)
std::string reject_reason; ///< причина отклонения (для declwroff)
document2 protocol; ///< Подписанный советом протокол решения; кладётся в callback onmktwoauth/onmktwodecl
std::string reject_reason; ///< Причина отклонения (можно достать из meta protocol в onmktwodecl)
// Timestamp'ы propwroff/execwroff/declwroff — на бэкенде из blockchain_actions[at].
// Связка с 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; }
@@ -13,6 +13,9 @@
#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"
@@ -23,10 +26,14 @@
#include "src/p.mkt.return/accretrn.cpp"
#include "src/p.mkt.return/rejretrn.cpp"
// ── p.mkt.wroff (3 actions) ───── Stories Эпика 8 ──────────────────────
// ── 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"
#include "src/p.mkt.wroff/declwroff.cpp"
[[eosio::action]] void marketplace::migrate() {
// Donor-таблиц нет (AR30 — donor-actions удалены вместе с requests/segments/
@@ -31,7 +31,16 @@ using namespace Marketplace;
* acceptorder, declineorder, signsupp, signchair, signiss1, signiss2.
* - **p.mkt.return** (5 actions): submretrn, aprretrem, rejretrem, accretrn,
* rejretrn.
* - **p.mkt.wroff** (3 actions): propwroff, execwroff, declwroff.
* - **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'а, объединяя их по
@@ -72,7 +81,7 @@ public:
/**
* @brief Заказчик размещает заказ на товар из каталога (Story 4.1).
* Серия: o.wal.conv (conditional) o.mkt.assign (conditional) o.mkt.block.
* Один шаг ledger2: o.mkt.lock (TRANSFER w.wal.share w.mkt.order).
* @ingroup public_marketplace_actions
*/
[[eosio::action]] void createorder(eosio::name coopname,
@@ -83,12 +92,11 @@ public:
eosio::name delivery_braname,
uint64_t quantity,
eosio::asset unit_price,
eosio::name cycle_type,
uint32_t warranty_period_secs,
checksum256 batch_hash);
/**
* @brief Заказчик отменяет заказ до акцепта (Story 4.4). Триггерит o.mkt.unblk.
* @brief Заказчик отменяет заказ до акцепта (Story 4.4). Триггерит o.mkt.unlock.
* @ingroup public_marketplace_actions
*/
[[eosio::action]] void cancelorder(eosio::name coopname,
@@ -97,7 +105,7 @@ public:
/**
* @brief Backend закрывает Order по таймауту цикла отсечки (Story 4.3).
* Per-Order: o.mkt.unblk + статус active cancelled. Backend вычисляет
* Per-Order: o.mkt.unlock + статус active cancelled. Backend вычисляет
* threshold по batch'у вне контракта; для каждого истёкшего Order'а
* вызывается отдельный `expireorder`.
* @ingroup public_marketplace_actions
@@ -117,7 +125,7 @@ public:
/**
* @brief Поставщик отказывается от одного Order'а до акцепта (Story 4.5).
* Per-Order: o.mkt.unblk на total_cost + статус active cancelled.
* Per-Order: o.mkt.unlock на total_cost + статус active cancelled.
* Backend проходит циклом по orders батча, вызывая `declineorder` per Order.
* @ingroup public_marketplace_actions
*/
@@ -141,17 +149,57 @@ public:
/**
* @brief Председатель приёмного КУ ставит закрывающую подпись на АПП
* приёмки одного Order'а (Story 5.3/5.4). Per-Order: o.mkt.purch +
* o.mkt.payout (атомарно). Авторизация подписи: председатель / trustee /
* trusted branches[o.accept_braname]. Backend проходит циклом по orders
* батча с одинаковым `act`.
* приёмки одного 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. Авторизация:
@@ -166,9 +214,9 @@ public:
/**
* @brief Заказчик ставит финальную подпись АПП-выдачи (Story 6.3).
* Per-Order с поддержкой actual_quantity ordered (Story 6.2).
* Atomic: [o.mkt.unblk на разницу если actual<ordered |
* o.wal.conv+o.mkt.assign+o.mkt.block на разницу если actual>ordered]
* + o.mkt.consum + o.mkt.consum2.
* Atomic: [o.mkt.unlock на разницу если actual<ordered |
* o.mkt.lock на разницу если actual>ordered]
* + o.mkt.consum.
* Подпись акта: orderer + любой авторизованный из branches[o.delivery_braname].
* @ingroup public_marketplace_actions
*/
@@ -176,6 +224,7 @@ public:
eosio::name orderer,
checksum256 order_hash,
uint64_t actual_quantity,
eosio::asset actual_unit_price,
eosio::name delivery_signer,
document2 act);
@@ -220,7 +269,7 @@ public:
/**
* @brief Председатель принимает возврат на очном осмотре (Story 7.4).
* Atomic: o.mkt.return + o.mkt.return2 (compensating forward).
* Один шаг: o.mkt.return (compensating forward к o.mkt.consum).
* Авторизация: подписант branches[braname].
* @ingroup public_marketplace_actions
*/
@@ -245,37 +294,71 @@ public:
// ── p.mkt.wroff ──────────────────────────────────────────────────────
/**
* @brief Backend / админ выносит проект списания на повестку совета (Story 8.1).
* Без ledger2-операций только создание proposal с N позициями.
* @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);
std::vector<wroff_item> items,
document2 statement,
std::string meta);
/**
* @brief Совет исполняет одну позицию проекта списания (Story 8.3).
* Per-item: o.mkt.wroff + o.mkt.wroff2 (атомарно в той же транзакции),
* `items[item_index].executed = true`. Когда все items.executed proposal
* status переходит в EXECUTED. Авторизация: подписант branches[items[item_index].braname].
* Backend проходит циклом по неисполненным items, вызывая `execwroff` per item.
* @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,
document2 protocol);
/**
* @brief Совет отклоняет проект списания целиком (Story 8.3).
* @ingroup public_marketplace_actions
*/
[[eosio::action]] void declwroff(eosio::name coopname,
eosio::name decided_by,
checksum256 proposal_hash,
std::string reason);
uint64_t item_index);
// ── service ──────────────────────────────────────────────────────────
@@ -7,42 +7,34 @@
# исходной выдаче (o.mkt.consum). Откат через ledger2::revert НЕ
# используется (упрощение реализации MVP — реверты исключены из системы).
#
# **Модель кошельков (упрощённая, refinement 2026-05-04):** один программный
# кошелёк w.mkt.member (per-user) — членские взносы пайщика в программу.
# Возврат суммы при гарантии происходит на тот же программный кошелёк
# (.available восстанавливается); пайщик может потратить на следующий заказ
# или вывести в общий членский кошелёк (w.wal.member) через o.mkt.recall.
# **Модель кошельков:** возврат суммы при гарантии происходит на членский
# кошелёк «Стола заказов» пайщика (w.mkt.member.available восстанавливается);
# средства остаются в программе — заказчик направляет их на следующие заказы
# либо отдельным действием выводит в универсальный членский.
#
# Идентификация кошельков — eosio::name с префиксом w.<contract>.<waltype>
# (рефакторинг 2026-04-27 на ветке reports). Sentinel '' (пустая строка) —
# «кошелёк вне системы» для ISSUE/REVOKE/ACCOUNT_ONLY.
# Идентификация кошельков — eosio::name с префиксом w.<contract>.<waltype>.
# Sentinel '' (пустая строка) — «кошелёк вне системы» для ISSUE.
#
# Возврат поставщику и работа с поставщиком по претензиям — out of MVP;
# вернувшееся имущество остаётся на складе КУ как материальный остаток,
# его дальнейшая судьба вне этого процесса.
#
# В процессе участвуют 2 ledger2-операции (compensating-forward пара, зеркало
# выдачи на supply — через транзит счёта 91):
# • o.mkt.return — часть 1: ISSUE ∅ → w.mkt.member (восстановление
# .available заказчика в программе) + Дт 91 / Кт 86
# (восстановление «прочих» за счёт ЦФ)
# • o.mkt.return2 — часть 2: NONE Дт 10 / Кт 91 (имущество назад на склад
# через закрытие транзита). Атомарно с o.mkt.return
# в той же транзакции accretrn.
# В процессе участвует 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/return/ — реализация (предстоит)
# • 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 (programmatic)
# • cpp/lib/core/ledger2/wallets.hpp — w.mkt.member (членский «Стола заказов»)
# • cpp/lib/core/ledger2/accounts.hpp — Целевое финансирование (86),
# Материалы (10),
# Прочие доходы и расходы (91 — NEW,
# транзит при возврате o.mkt.return)
# Материалы (10)
# ─────────────────────────────────────────────────────────────────────────────
# ── Секция 1. Паспорт ───────────────────────────────────────────────────────
@@ -206,7 +198,6 @@ transitions:
ledger_code: p.mkt.return
operations:
- o.mkt.return
- o.mkt.return2
guards:
- Заказчик прибыл на участок с продукцией.
- Председатель очно осмотрел имущество и решил принять гарантийный возврат.
@@ -323,24 +314,20 @@ documents:
# ── Секция 6. Операции (Ledger2) ────────────────────────────────────────────
# Одна ledger2-операция — compensating forward к o.mkt.consum (без
# использования ledger2::revert). Атомарно: восстановление .available
# на программном кошельке пайщика + проводка Дт 10 / Кт 86 (обратная
# к выдаче).
# использования ledger2::revert). Атомарно: восстановление .available на
# членском кошельке «Стола заказов» заказчика + возврат имущества на склад.
operations:
- ledger_code: o.mkt.return
human_name: Гарантийный возврат — восстановление средств в программе
human_name: Гарантийный возврат — восстановление средств и имущества
wallet_op: ISSUE
# L1 — часть 1 композитной проводки через транзит счёта 91 (зеркало
# выдачи): Дт 91 / Кт 86 — восстановление «прочих» за счёт ЦФ.
# Закрытие транзита выполняется отдельной операцией o.mkt.return2
# (Дт 10 / Кт 91), атомарно с o.mkt.return в той же транзакции
# accretrn. Согласовано с Ангелиной 2026-05-11.
debit: 91 # Прочие доходы и расходы (NEW)
# L1 — двойная запись: имущество возвращается на склад через целевое
# финансирование (обратная к выдаче, одной операцией).
debit: 10 # Материалы — имущество на складе
credit: 86 # Целевое финансирование
# L2 — восстановление средств в программе
# L2 — восстановление средств на членском кошельке «Стола заказов» заказчика
wallet_from: null # эмиссия — источника нет
wallet_to: w.mkt.member # ЦПП «Стол Заказов» — программный кошелёк
# L3 — пайщику восстанавливается available на программном кошельке
wallet_to: w.mkt.member # Членский кошелёк «Стола заказов» пайщика
# L3 — пайщику восстанавливается available на членском «Стола заказов»
user_wallet: w.mkt.member
user_ref: return_request.orderer
available_delta: +order.fact_cost
@@ -348,45 +335,12 @@ operations:
amount_ref: order.fact_cost
triggered_by: marketplace::accretrn
description: >
Compensating forward к выдаче имущества (o.mkt.consum), часть 1
композитной бухгалтерской проводки через транзит счёта 91. ISSUE на
пайщикском w.mkt.member — .available +fact_cost (восстановление ранее
списанной суммы); проводка Дт 91 / Кт 86 — восстановление «прочих»
за счёт ЦФ (зеркало 91-стороны от o.mkt.consum). Сразу после этой
операции в той же транзакции accretrn контракт вызывает
o.mkt.return2 (Дт 10 / Кт 91, NONE) — имущество назад на склад через
закрытие транзита. Транзит через 91 симметричен выбытию на выдаче —
согласовано с Ангелиной 2026-05-11. Журнал содержит payload
Compensating forward к выдаче имущества (o.mkt.consum). ISSUE на
членский кошелёк «Стола заказов» заказчика (w.mkt.member) — .available
+fact_cost (восстановление ранее списанной суммы). Двойная запись
Дт 10 / Кт 86 — имущество возвращается на склад за счёт целевого
финансирования (зеркало выдачи). Журнал содержит прикладное поле
original_consume_op_id (ссылка на исходный o.mkt.consum для
трассировки) — это **прикладное поле**, не часть инфраструктуры
revert. Дальнейшая судьба возвращённого имущества — вне этого
процесса.
Пайщик может потратить возвращённую сумму на следующий заказ в Столе
заказов либо вывести в общий членский кошелёк (w.wal.member) через
отдельную операцию o.mkt.recall — там средства универсальны между
программами.
- ledger_code: o.mkt.return2
human_name: Гарантийный возврат — закрытие транзита
wallet_op: NONE
# L1 — часть 2 композитной проводки: Дт 10 / Кт 91 (имущество назад
# на склад через закрытие транзита). Зеркало 91-стороны от o.mkt.csmcls.
debit: 10 # Материалы (NEW) — имущество на складе
credit: 91 # Прочие доходы и расходы (NEW)
# L2 / L3 — без движения по кошелькам (склад — аналитика по счёту 10)
wallet_from: null
wallet_to: null
user_wallet: null
user_ref: null
available_delta: null
blocked_delta: null
amount_ref: order.fact_cost
triggered_by: marketplace::accretrn
description: >
Закрытие транзита счёта 91 после восстановления членского взноса
пайщика. NONE — только бухпроводка Дт 10 / Кт 91, кошельки не
двигаются. Срабатывает атомарно с o.mkt.return в той же транзакции
accretrn. По итогу пары operations нетто-эффект — Дт 10 / Кт 86
(имущество возвращается на склад + восстановление обязательства
по целевой программе), 91 проходит нулевым транзитом.
трассировки) — это не часть инфраструктуры revert. Возвращённая сумма
остаётся в программе «Стол заказов» — на следующие заказы.
@@ -11,55 +11,45 @@
# актах приёма-передачи — приёмки кооперативом от поставщика и выдачи
# заказчику от кооператива.
#
# **Модель кошельков (refinement 2026-05-04, трёхуровневая серия):**
# Членский взнос пайщика проходит через два кошелька:
# • w.wal.member (контракт wallet, USER_SHARED) — универсальный членский
# кошелёк пайщика. Сюда стекаются возвраты из всех
# программ (Стол заказов / Благорост / и т.п.); пайщик
# тратит как «баллы» — может направить на следующий заказ
# в любой программе либо вывести обратно на паевой через
# операцию контракта wallet.
# • w.mkt.member (контракт marketplace, USER_SHARED) — членский в
# программе Marketplace. На нём идут BLOCK/UNBLOCK/REVOKE
# под конкретный Order.
# Платформенные кошельки w.wal.share (Цифровой Кошелёк, паевые деньгами) и
# w.mkt.payout (Выплаты поставщикам) — без изменений. Имущество
# отслеживается бухгалтерской аналитикой по счёту 10 (per-КУ субсчета),
# без отдельного кошелька.
# **Модель кошельков:**
# Средства заказчика движутся напрямую между тремя кошельками пайщика:
# • 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>
# (рефакторинг 2026-04-27 на ветке reports). null для незатронутых уровней;
# sentinel '' — legacy-допуск (рендерится как ∅).
# Идентификация кошельков — eosio::name с префиксом w.<contract>.<waltype>.
# null для незатронутых уровней; sentinel '' — legacy-допуск (рендерится как ∅).
#
# В процессе участвуют 9 ledger2-операций (часть — из контракта wallet):
# • o.wal.conv — конвертация цифрового рубля → общий членский пайщика
# (TRANSFER, w.wal.share → w.wal.member,
# Дт 80 / Кт 86) — conditional, только если на
# w.wal.member.available заказчика не хватает
# • o.mkt.assign — целевое назначение членского взноса в программу
# (TRANSFER, w.wal.member → w.mkt.member, без проводки —
# Дт=Кт по 86 смарт-контракт игнорирует) — conditional,
# только если на w.mkt.member.available не хватает
# • o.mkt.block — блокировка средств под Order на программном кошельке
# (BLOCK)
# • o.mkt.unblk — разблокировка при отмене Order'а
# (UNBLOCK; средства остаются на w.mkt.member.available
# и могут быть потрачены на следующий заказ)
# • o.mkt.recall — вывод из программы в общий членский кошелёк пайщика
# (TRANSFER, w.mkt.member → w.wal.member, без проводки) —
# ОТДЕЛЬНОЕ явное действие пайщика, не часть авто-flow
# отмены/возврата
# • o.mkt.purch — приёмка имущества кооперативом по АПП приёмки
# (ACCOUNT_ONLY, только проводка, Дт 10 / Кт 86)
# • o.mkt.payout — оплата поставщику с расчётного счёта
# (TRANSFER, ∅ → w.mkt.payout, Дт 86 / Кт 51)
# • o.mkt.consum — выдача имущества пайщику по АПП выдачи, часть 1
# (REVOKE, w.mkt.member.blocked → 0, Дт 91 / Кт 10 —
# выбытие имущества со склада на «прочие»)
# • o.mkt.consum2 — выдача имущества пайщику, часть 2 — закрытие транзита
# (NONE, Дт 86 / Кт 91; кошельки не двигаются).
# Атомарно с o.mkt.consum в signiss2 — обе операции
# вызываются последовательно в одной транзакции Antelope.
# В процессе участвуют 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
@@ -68,16 +58,14 @@
# • 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.member (universal member, contract wallet),
# w.mkt.member (programmatic per-user),
# w.wal.share (платформенный),
# • 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 — NEW),
# Прочие доходы и расходы (91 — NEW,
# транзит при выдаче o.mkt.consum)
# Материалы (10)
# ─────────────────────────────────────────────────────────────────────────────
# ── Секция 1. Паспорт ───────────────────────────────────────────────────────
@@ -132,6 +120,7 @@ roles:
- offerer # пайщик-поставщик
- chairman # председатель кооперативного участка (КУ-приёмщик/КУ-выдающий)
- backend # Backend Marketplace (внеблокчейн-логика)
- gateway # Контракт процессинга платежей (источник callback'ов о выплате; не человек, а контракт-инициатор)
# ── Секция 2. Действия контракта (блокчейн-уровень) ─────────────────────────
# Имена actions ≤12 символов eosio::name. Контракт описан в целевом виде —
@@ -217,8 +206,43 @@ actions:
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: Председатель открыл выдачу
@@ -238,8 +262,10 @@ actions:
выдачи забрал свой заказ. Если фактически выданное расходится с
заказом — сумма заранее корректируется до фактического: при
меньшем количестве остаток возвращается на программный членский
кошелёк, при большем — недостающая сумма берётся с паевого
заказчика. Заказ закрыт, открывается гарантийное окно.
кошелёк «Стола заказов», при большем — недостающая сумма вносится
дополнительным паевым взносом, который конвертируется в членский
«Стола заказов» и им добирается доплата (напрямую с паевого не
списываем). Заказ закрыт, открывается гарантийное окно.
# ── Секция 3. Граф состояний ────────────────────────────────────────────────
# Сущность: marketplace::order (таблица orders, scope=coopname).
@@ -297,8 +323,10 @@ states:
human: Поставка принята кооперативом
description: >
Председатель участка завершил приёмку второй подписью. Партия
юридически принята кооперативом, поставщику ушла оплата, имущество
числится на складе участка и ждёт открытия выдачи.
юридически принята кооперативом, имущество числится на складе
участка и ждёт открытия выдачи. Обязательство выплаты поставщику
открыто и закрывается отдельным действием `marketplace::payout` по
факту подтверждения кассиром банковского перевода (Locked Decision L12).
kind: normal
- name: ready_to_receive
@@ -323,14 +351,12 @@ transitions:
actor: orderer
ledger_code: p.mkt.supply
operations:
- o.wal.conv
- o.mkt.assign
- o.mkt.block
- o.mkt.lock
guards:
- Заказчик — активный пайщик кооператива.
- Заказчик подписал Соглашение ЦПП «Стол заказов».
- Стоимость заказа не превышает упрощённый порог приёма имущества (100 000 ₽).
- Совокупных средств заказчика — на паевом и членских кошельках — достаточно для полной стоимости заказа.
- На паевом взносе заказчика (w.wal.share) достаточно средств для полной стоимости заказа.
- Указанный пункт выдачи принадлежит тому же кооперативу.
- from: active
@@ -339,7 +365,7 @@ transitions:
actor: orderer
ledger_code: p.mkt.supply
operations:
- o.mkt.unblk
- o.mkt.unlock
guards:
- Заказ ещё не вошёл в акцептованную поставщиком консолидированную заявку.
@@ -349,7 +375,7 @@ transitions:
actor: backend
ledger_code: p.mkt.supply
operations:
- o.mkt.unblk
- o.mkt.unlock
guards:
- Цикл отсечки заявок поставщика завершился.
- Минимальный порог поставки по количеству заказов не достигнут.
@@ -368,7 +394,7 @@ transitions:
actor: offerer
ledger_code: p.mkt.supply
operations:
- o.mkt.unblk
- o.mkt.unlock
guards:
- Поставщик ещё не акцептовал консолидированную заявку.
@@ -396,11 +422,42 @@ transitions:
ledger_code: p.mkt.supply
operations:
- o.mkt.purch
- o.mkt.payout
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
@@ -415,7 +472,6 @@ transitions:
ledger_code: p.mkt.supply
operations:
- o.mkt.consum
- o.mkt.consum2
guards:
- Заказчик лично присутствует на пункте выдачи участка.
- Председатель сверил фактически выдаваемое имущество с составом заказа.
@@ -517,14 +573,50 @@ scenario:
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: Открытие выдачи
@@ -551,8 +643,10 @@ scenario:
расхождении (например, развесной товар: фактический вес меньше
заказанного) сумма заказа предварительно корректируется до
фактической: при меньшем количестве остаток возвращается на
программный членский кошелёк заказчика, при большем — недостающая
сумма берётся с паевого заказчика. Заказчик ставит финальную
программный членский кошелёк «Стола заказов» заказчика, при большем —
недостающая сумма вносится дополнительным паевым взносом, который
конвертируется в членский «Стола заказов» и им добирается доплата
(напрямую с паевого не списываем). Заказчик ставит финальную
подпись на акте приёма-передачи и забирает заказ.
pre:
- Заказ готов к выдаче.
@@ -591,6 +685,19 @@ scenario:
заказчиков снимается. Договорные санкции при отказе — вне этого
процесса, по регламенту кооператива.
- branch: Кассир отклонил банковский перевод поставщику
at_step: 6b
action: marketplace::paydecline
actor: gateway
description: >
Кассир в столе процессинга отметил, что банковский перевод
поставщику не состоялся — нет реквизитов, ошибка ввода, банк
отклонил. Контракт gateway callback'ом дёргает контракт
marketplace. Бухгалтерия не двигается — обязательство перед
поставщиком остаётся открытым с сохранением причины отказа.
Backend может повторно инициировать выплату после исправления
реквизитов (`marketplace::payout` ещё раз).
- branch: Передача партии через экспедитора
at_step: 5
action: marketplace::signsupp
@@ -659,67 +766,67 @@ documents:
note: "TODO: создать шаблон ТТН в registry. При самовывозе не используется. Подписи проставляются вне блокчейна (на бумажном носителе); фотофиксация председателя по результатам очной сверки сохраняется как основание для дистанционной подписи поставщика."
# ── Секция 6. Операции (Ledger2) ────────────────────────────────────────────
# Восемь ledger2-операций для процесса прямой поставки-приобретения. Имена
# ≤12 символов eosio::name. Кошельки идентифицируются eosio::name-строками
# с префиксом w.<contract>.<waltype> (рефакторинг 2026-04-27, ветка reports):
# • w.wal.member — Универсальный членский кошелёк пайщика (контракт wallet,
# USER_SHARED). Сюда стекаются возвраты из всех программ;
# пайщик расходует «как баллы» в любую программу.
# • w.mkt.member — ЦПП «Стол Заказов» — членский в программе (USER_SHARED).
# На нём идут BLOCK/UNBLOCK/REVOKE под Order.
# • w.wal.share — ЦПП «Цифровой Кошелёк» — паевые взносы деньгами
# [платформенный]
# • w.mkt.payout — Выплаты поставщикам
# [платформенный]
# Пять 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.wal.conv
human_name: Конвертация паевого в членский взнос пайщика
- ledger_code: o.mkt.lock
human_name: Резервирование средств под заказ
wallet_op: TRANSFER
# L1 — двойная запись (паевой → целевой)
# L1 — двойная запись (паевой → целевое финансирование)
debit: 80 # Паевой фонд
credit: 86 # Целевое финансирование (общий членский пайщика)
# L2 — кошельки кооператива (агрегаты)
wallet_from: w.wal.share # ЦПП «Цифровой Кошелёк» — паевые взносы деньгами
wallet_to: w.wal.member # Универсальный членский — общий членский кошелёк пайщика
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.wal.member
- 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: >
Шаг 1 из 3 серии createorder (conditional — выполняется только если на
w.wal.member.available заказчика не хватает суммы заказа). Конвертация
цифрового рубля заказчика из ЦПП «Цифровой Кошелёк» в общий членский
кошелёк пайщика — универсальную точку накопления членских взносов
между программами. Сумма берётся ровно как недостача — если на
w.wal.member уже есть available (например, из прошлого возврата),
конвертируется только разница. Двойная запись Дт 80 / Кт 86 — переход
из складочного капитала в целевое финансирование. (TBD-Standardization:
проводка фиксируется как baseline.)
Единственный обязательный шаг 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.assign
human_name: Целевое назначение членского взноса в программу Marketplace
- ledger_code: o.mkt.unlock
human_name: Снятие резерва при отмене заказа
wallet_op: TRANSFER
# L1 — нет проводки. По плану счетов оба кошелька висят на 86, и Дт 86 /
# Кт 86 даёт нетто-нулевое изменение по счёту — смарт-контракт такие
# «фиктивные» проводки игнорирует. Аналитика «по программе» фиксируется
# на L2 (через имена агрегатных кошельков) — этого достаточно.
# L1 — без проводки (оба кошелька на 86, фиктивная Дт=Кт игнорируется)
debit: null
credit: null
# L2 — реальный перевод между двумя именованными агрегатами кооператива
wallet_from: w.wal.member # Общий членский — пайщик расходует
wallet_to: w.mkt.member # ЦПП «Стол Заказов» — целевое назначение в программу
# L2 — обратный перевод из резерва в членский «Стола заказов» пайщика
wallet_from: w.mkt.order # Резерв под Order
wallet_to: w.mkt.member # Членский кошелёк «Стола заказов» пайщика
# L3 — две стороны движения у одного и того же пайщика
l3:
- user_wallet: w.wal.member
- user_wallet: w.mkt.order
user_ref: order.orderer
available_delta: -order.total_cost
blocked_delta: null
@@ -728,101 +835,79 @@ operations:
available_delta: +order.total_cost
blocked_delta: null
amount_ref: order.total_cost
triggered_by: marketplace::createorder
description: >
Шаг 2 из 3 серии createorder (conditional — выполняется только если на
w.mkt.member.available заказчика не хватает суммы заказа). Перенос
членского взноса пайщика из универсального кошелька (w.wal.member)
в программу Marketplace (w.mkt.member). Сумма берётся ровно как
недостача. По бухгалтерии — без проводки (Дт=Кт по 86 нетто-нулевое
изменение, смарт-контракт такие фиктивные проводки игнорирует);
аналитика «по программе» фиксируется на L2/L3.
- ledger_code: o.mkt.block
human_name: Блокировка членского взноса под заказ
wallet_op: BLOCK
# L1 — операция чисто L3, бухгалтерия не двигается
debit: null
credit: null
# L2 — без перевода между кошельками кооператива
wallet_from: null
wallet_to: null
# L3 — split available → blocked на одном кошельке заказчика
user_wallet: w.mkt.member # ЦПП «Стол Заказов» — членские взносы заказчика
user_ref: order.orderer
available_delta: -order.total_cost
blocked_delta: +order.total_cost
amount_ref: order.total_cost
triggered_by: marketplace::createorder
description: >
Атомарная серия — шаг 2 из 2. Блокировка суммы order.total_cost под
конкретный Order на кошельке заказчика w.mkt.member. Это операция
уровня 3 — split available → blocked на одном кошельке без перевода
между кошельками кооператива и без бухгалтерских проводок.
Блокировка снимается либо при выдаче (o.mkt.consum, REVOKE blocked → 0),
либо при отмене (o.mkt.unblk + o.mkt.recall — UNBLOCK + возврат).
- ledger_code: o.mkt.unblk
human_name: Разблокировка членского взноса при отмене
wallet_op: UNBLOCK
# L1 — без проводок
debit: null
credit: null
# L2 — без перевода
wallet_from: null
wallet_to: null
# L3 — обратное движение blocked → available
user_wallet: w.mkt.member
user_ref: order.orderer
available_delta: +order.total_cost
blocked_delta: -order.total_cost
amount_ref: order.total_cost
triggered_by: marketplace::cancelorder | marketplace::expirecycle | marketplace::declinebatch
description: >
Зеркало o.mkt.block. Снятие блокировки на кошельке заказчика
w.mkt.member при отмене Order'а (заказчик отменяет до акцепта;
цикл истёк без достижения порога; поставщик отказался от партии).
На уровне 3 blocked → available. Без L1, без L2. Сумма остаётся
на w.mkt.member.available заказчика — может быть потрачена на
следующий заказ в Столе заказов. Вывод в общий членский кошелёк
(w.wal.member) — отдельное явное действие пайщика через o.mkt.recall,
не часть авто-flow отмены.
Снятие резерва при отмене Order'а (заказчик отменяет до акцепта;
цикл истёк без достижения порога; поставщик отказался от партии),
а также возврат остатка при недовыдаче (signiss2, факт меньше заказа).
Средства возвращаются с кошелька-резерва (w.mkt.order) на членский
кошелёк «Стола заказов» заказчика (w.mkt.member.available) — остаются
в программе и могут быть направлены на следующие заказы. По бухгалтерии —
без проводки (оба кошелька на счёте 86 — перевод внутри одной аналитики).
- ledger_code: o.mkt.recall
human_name: Вывод членского взноса в общий членский кошелёк
- ledger_code: o.mkt.conv
human_name: Конвертация паевого в членский «Стола заказов» под доплату
wallet_op: TRANSFER
# L1 — нет проводки (Дт=Кт по 86 нетто-нулевое; смарт-контракт игнорирует).
# 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 — реальный перевод между двумя именованными агрегатами кооператива
wallet_from: w.mkt.member # ЦПП «Стол Заказов» — программный членский
wallet_to: w.wal.member # Универсальный членский — общий членский пайщика
# 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: -order.total_cost
available_delta: -diff
blocked_delta: null
- user_wallet: w.wal.member
- user_wallet: w.mkt.order
user_ref: order.orderer
available_delta: +order.total_cost
available_delta: +diff
blocked_delta: null
amount_ref: order.total_cost
triggered_by: TBD
amount_ref: fact_cost - order.total_cost
triggered_by: marketplace::signiss2
description: >
Зеркало o.mkt.assign. Явное действие пайщика — вывести членский взнос
из программы Marketplace в общий членский кошелёк (где средства
универсальны между программами и могут быть направлены, например, на
Благорост или обратно на цифровой паевой через отдельную операцию
контракта wallet). НЕ часть авто-flow отмены/возврата — на cancel/
expire/decline средства остаются на w.mkt.member.available, и
инициатива вывода — у пайщика.
Парный к o.mkt.conv шаг доплаты по факту: сконвертированный членский
взнос «Стола заказов» (w.mkt.member) добирает резерв этого же Order'а
(w.mkt.order) на разницу. Списание под заказ идёт ИМЕННО с членского
программы, не с паевого напрямую. По бухгалтерии — без проводки
(оба кошелька на счёте 86 — перевод внутри одной аналитики).
- ledger_code: o.mkt.purch
human_name: Приёмка имущества кооперативом
wallet_op: ACCOUNT_ONLY
wallet_op: NONE
# L1 — двойная запись приёмки имущества
debit: 10 # Материалы (NEW в LEDGER2_ACCOUNT_REGISTRY)
debit: 10 # Материалы
credit: 86 # Целевое финансирование
# L2 — без движения по кошелькам кооператива (имущество — не кошелёк)
wallet_from: null
@@ -839,20 +924,16 @@ operations:
подпись председателя). Двойная запись Дт 10 / Кт 86 — приобретение
имущества за счёт целевых средств кооператива. Имущество отслеживается
бухгалтерской аналитикой по счёту 10 (per-КУ субсчета), без отдельного
кошелька. Срабатывает атомарно с o.mkt.payout. WalletOp ACCOUNT_ONLY —
proposed расширение enum'а ledger2 для операций, выполняющих только
бухгалтерскую проводку без движения по кошелькам. (TBD-Standardization:
проводка Дт 10 / Кт 86 — baseline; финальное решение — при формализации
стандарта; ACCOUNT_ONLY — proposed.)
кошелька. Срабатывает атомарно с o.mkt.payout.
- ledger_code: o.mkt.payout
human_name: Оплата поставщику с расчётного счёта
wallet_op: TRANSFER
wallet_op: ISSUE
# L1
debit: 86 # Целевое финансирование (TBD-Standardization)
debit: 86 # Целевое финансирование
credit: 51 # Расчётный счёт
# L2 — эмиссия суммы в платформенный кошелёк выплат поставщикам
wallet_from: null # ISSUE-подобная семантика: источника нет
wallet_from: null # ISSUE: источника нет
wallet_to: w.mkt.payout # Выплаты поставщикам (платформенный)
# L3 — w.mkt.payout агрегатный (не USER_SHARED), L3 не заполняется
user_wallet: null
@@ -860,75 +941,46 @@ operations:
available_delta: null
blocked_delta: null
amount_ref: order.total_cost
triggered_by: marketplace::signchair
triggered_by: marketplace::payconfirm
description: >
Оплата поставщику с расчётного счёта кооператива по факту приёмки
имущества. Двойная запись Дт 86 / Кт 51 — расход целевых средств на
оплату поставки (упрощённая модель без счёта 60 «Расчёты с поставщиками»).
Сумма приземляется на платформенный кошелёк w.mkt.payout.
Срабатывает атомарно с o.mkt.purch на финальной подписи председателя
АПП приёмки. (TBD-Standardization: проводка фиксируется как baseline;
альтернативные варианты — Дт 76 / Кт 51 через прочих кредиторов;
финальное решение — при формализации стандарта совместно с Ангелиной.)
Оплата поставщику с расчётного счёта кооператива по факту реального
банковского перевода (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: REVOKE
# L1 — часть 1 композитной проводки через транзит счёта 91:
# Дт 91 / Кт 10 — выбытие имущества со склада на «прочие». Закрытие
# транзита выполняется отдельной операцией o.mkt.consum2 (Дт 86 / Кт 91),
# атомарно с o.mkt.consum в той же транзакции Antelope (вызывается
# сразу после в действии signiss2). Согласовано с Ангелиной 2026-05-11.
debit: 91 # Прочие доходы и расходы (NEW)
credit: 10 # Материалы (NEW)
# L2 — без перевода между кошельками кооператива (REVOKE списывает
# blocked в пустоту, это L3-семантика)
wallet_from: null
wallet_to: null
# L3 — blocked заказчика обнуляется (целевое расходование членского взноса)
user_wallet: w.mkt.member
user_ref: order.orderer
available_delta: null
blocked_delta: -order.fact_cost
amount_ref: order.fact_cost
triggered_by: marketplace::signiss2
description: >
Выдача имущества заказчику-пайщику по АПП выдачи (финальная подпись
заказчика), часть 1 композитной бухгалтерской проводки через транзит
счёта 91 «Прочие доходы и расходы». REVOKE на программном кошельке
пайщика — заблокированная сумма списывается без увеличения куда-либо
(целевое расходование членского взноса). Проводка Дт 91 / Кт 10 —
выбытие имущества со склада на «прочие». Сразу после этой операции
в той же транзакции signiss2 контракт вызывает o.mkt.consum2
(Дт 86 / Кт 91, NONE) — закрытие транзита 91 на счёт ЦФ программы.
Транзит через 91 отделяет момент выбытия имущества от момента
признания целевого расхода — согласовано с Ангелиной 2026-05-11.
При расхождении факт vs заказ предшествуют корректирующие операции
(o.mkt.unblk на остаток если факт меньше; o.wal.conv + o.mkt.assign +
o.mkt.block на разницу если факт больше — первые две conditional).
- ledger_code: o.mkt.consum2
human_name: Выдача имущества пайщику — закрытие транзита
wallet_op: NONE
# L1 — часть 2 композитной проводки: Дт 86 / Кт 91 (закрытие «прочих»
# на счёт ЦФ программы). Зеркало 91-стороны от o.mkt.consum.
human_name: Выдача имущества пайщику по АПП выдачи
wallet_op: BURN
# L1 — двойная запись: целевое финансирование расходуется на выбытие
# имущества со склада (одной операцией, без транзита)
debit: 86 # Целевое финансирование
credit: 91 # Прочие доходы и расходы (NEW)
# L2 / L3 — без движения по кошелькам
wallet_from: null
wallet_to: null
user_wallet: null
user_ref: null
available_delta: null
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: >
Закрытие транзита счёта 91 после выдачи имущества пайщику. NONE —
только бухпроводка Дт 86 / Кт 91, кошельки не двигаются. Срабатывает
атомарно с o.mkt.consum в той же транзакции signiss2 (контракт
выполняет обе операции последовательно). По итогу пары operations
нетто-эффект — Дт 86 / Кт 10 (закрытие обязательства по целевой
программе + выбытие имущества со склада), 91 проходит нулевым
транзитом.
Выдача имущества заказчику-пайщику по АПП выдачи (финальная подпись
заказчика). BURN на кошельке-резерве (w.mkt.order) — зарезервированная
сумма сжигается без получателя (целевое расходование членского взноса).
Двойная запись Дт 86 / Кт 10 — расход целевых средств на выбытие
имущества со склада. При расхождении факт vs заказ предшествуют
корректирующие операции: o.mkt.unlock на остаток → членский «Стола
заказов» (если факт меньше); o.mkt.conv + o.mkt.lockm на разницу
(если факт больше — паевой конвертируется в членский «Стола заказов»
и им добирается резерв, напрямую с паевого не списываем).
@@ -1,43 +1,37 @@
# ─────────────────────────────────────────────────────────────────────────────
# Стандарт «Утилизация скоропорта» — кооперативный процесс периодического
# списания имущества со склада КУ, не выданного первичному заказчику и
# подлежащего безвозвратному изъятию из кооператива (просроченный скоропорт,
# малооценочные позиции и т.п.).
# списания со склада участка имущества, которое физически пропало или стало
# непригодным к выдаче заказчику (просрочка, повреждения, малоценные позиции).
#
# Процесс инициируется кроном на стороне Backend (раз в месяц по умолчанию,
# точная дата согласовывается с бухгалтером), формирует проект решения
# совета, совет принимает или отклоняет проект, при принятии срабатывает
# контрактное действие списания. Списание идёт **через транзит счёта 91
# «Прочие доходы и расходы»** — это фиксирует, что имущество безвозвратно
# исчезло из кооператива (а не передано пайщику).
# Процесс инициируется кооперативом по расписанию (по умолчанию раз в месяц;
# точную дату согласовывает бухгалтер). По итогам опроса складов председатель
# подписывает заявление о списании и вносит его на рассмотрение совета.
# Совет рассматривает заявление через типовой процесс решения совета
# (стандарт sov.decision) и подписывает протокол. По принятому протоколу
# кооператив списывает каждую позицию проекта со склада участка.
#
# **Модель кошельков (упрощённая, refinement 2026-05-04):** процесс не
# двигает кошельки — это чисто бухгалтерское событие через проводки
# счёта 10 (учёт имущества), 91 (транзит) и 86 (ЦФ программы).
# Имущество отслеживается аналитикой по счёту 10 (per-КУ субсчета),
# без отдельного кошелька.
# Списание фиксирует, что имущество безвозвратно исчезло из кооператива
# (а не передано пайщику). По кошелькам заказчиков движений не происходит —
# это чисто кооперативный учётный расход.
#
# Идентификация кошельков (для будущих расширений процесса) — eosio::name
# с префиксом w.<contract>.<waltype> (рефакторинг 2026-04-27 на ветке
# reports). Sentinel '' (пустая строка) — «кошелёк вне системы» для
# операций ACCOUNT_ONLY.
# **Модель кошельков:** процесс не двигает кошельки — это бухгалтерское
# событие через проводки счёта 10 (учёт имущества) и 86 (ЦФ программы).
# Имущество отслеживается аналитикой по счёту 10 (per-КУ субсчета), без
# отдельного кошелька.
#
# В процессе участвуют 2 ledger2-операции (композитная проводка через
# транзит счёта 91 разбита на пару, срабатывают в одной транзакции execwroff):
# • o.mkt.wroff — часть 1: NONE Дт 91 / Кт 10 (выбытие со склада на «прочие»)
# • o.mkt.wroff2 — часть 2: NONE Дт 86 / Кт 91 (закрытие транзита на ЦФ программы)
# В процессе участвует 1 ledger2-операция:
# • o.mkt.wroff — Дт 86 / Кт 10 (списание со склада через ЦФ программы)
#
# Канон формата:
# coopenomics-docs/docs/standards/_spec/canon.md
# Источники правды в коде:
# • cpp/marketplace/marketplace.hpp — actions (status: proposed)
# • cpp/marketplace/src/writeoff/ — реализация (предстоит)
# • 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),
# Прочие доходы и расходы (91 — NEW)
# Материалы (10)
# ─────────────────────────────────────────────────────────────────────────────
# ── Секция 1. Паспорт ───────────────────────────────────────────────────────
@@ -50,208 +44,238 @@ contract: marketplace
purpose: >
Штатный путь корректно отразить в учёте имущество, которое физически
пропало или стало непригодным к выдаче. Применяется, когда товар на
складе участка просрочился, испортился или иначе не может быть
передан заказчику. Без этого процесса такие позиции висели бы на
складе бесконечно — здесь кооператив честно фиксирует свои потери
через решение совета.
складе участка просрочился, испортился или иначе не может быть передан
заказчику. Без этого процесса такие позиции висели бы на складе
бесконечно — здесь кооператив честно фиксирует свои потери через
решение совета.
Процесс состоит из двух шагов. На первом шаге кооператив по расписанию
(по умолчанию раз в месяц; точную дату согласовывает бухгалтер)
собирает со складов участков позиции, удовлетворяющие критериям
списания, формирует проект решения и выносит его на повестку совета.
На втором шаге совет рассматривает проект и принимает решение:
при положительном решении позиции списываются со складов как
безвозвратные потери; при отрицательном — проект отклоняется или
откладывается, а позиции остаются на складах. По кошелькам заказчиков
движений не происходит — это чисто кооперативный учётный расход.
опрашивает склады участков и собирает позиции, удовлетворяющие
критериям списания. Председатель оформляет проект списания, подписывает
заявление и вносит его на рассмотрение совета. На втором шаге совет
по типовому процессу решения совета рассматривает заявление и
подписывает протокол: при положительном решении каждая позиция
списывается со склада как безвозвратные потери; при отрицательном
проект отклоняется и позиции остаются на складе.
По кошелькам заказчиков движений не происходит — это чисто кооперативный
учётный расход на счёт целевого финансирования программы.
roles:
- backend # Backend Marketplace (крон-триггер, формирование проекта)
- chairman # председатель кооператива (вносит проект на повестку совета)
- chairman # председатель кооператива (подписывает заявление, вносит на совет)
- council # совет кооператива (принимает решение по протоколу)
# ── Секция 2. Действия контракта (блокчейн-уровень) ─────────────────────────
# Имена actions ≤12 символов eosio::name.
# Имена actions ≤12 символов eosio::name. Заявление председателя на
# рассмотрение совета вносится по типовому процессу «Решение совета»
# (стандарт sov.decision), поэтому здесь перечислены только специфические
# для списания действия — внесение проекта и исполнение позиций по
# принятому протоколу совета.
actions:
- name: marketplace::propwroff
human: Внести проект списания
actor: backend
human: Внести проект списания на рассмотрение совета
actor: chairman
role: opener
purpose: >
Кооператив по расписанию (по умолчанию раз в месяц) собирает на
складах участков позиции, которые невозможно выдать заказчику
просроченные и не востребованные, повреждённые, малооценные — и
формирует проект решения о списании. Проект выносится на повестку
Председатель подписывает заявление о списании скоропорта — список
позиций, которые невозможно выдать заказчику (просроченные и не
востребованные, повреждённые, малоценные), — и вносит проект на
рассмотрение совета. Дальнейшее движение проекта (повестка совета,
голосование, подписание протокола) — по типовому процессу решения
совета.
- name: marketplace::execwroff
human: Исполнить списание
actor: council
human: Исполнить позицию списания по протоколу совета
actor: chairman
role: closer
purpose: >
Совет принял положительное решение по проекту. Кооператив
списывает позиции со складов участков — имущество уходит из учёта
как безвозвратные потери. По кошелькам заказчиков движений нет.
- name: marketplace::declwroff
human: Отклонить проект списания
actor: council
role: reject
purpose: >
Совет отклонил проект списания либо отложил его рассмотрение.
Позиции остаются на складах участков и попадут в следующий цикл
списания.
После принятия советом положительного решения по каждой позиции
проекта выполняется списание: имущество выбывает со склада участка,
сумма закрывается на счёт целевого финансирования программы. На
каждую позицию — операция o.mkt.wroff. Когда все позиции проекта
исполнены, проект считается завершённым.
# ── Секция 3. Граф состояний ────────────────────────────────────────────────
# Сущность: marketplace::writeoff_proposal (таблица writeoff_proposals,
# scope=coopname). Запись агрегирует список позиций, попавших в один цикл
# утилизации, и проходит свой жизненный цикл (draft → executed | rejected).
# Сущность: marketplace::writeoff_proposal (таблица wroffprops, scope=coopname).
# Запись агрегирует список позиций, попавших в один цикл списания, и проходит
# свой жизненный цикл (proposed → authorized → executed | rejected).
entity: marketplace::writeoff_proposal
entity_human: Проект списания скоропорта
entity_source: cpp/marketplace/src/writeoff/
entity_source: cpp/marketplace/src/p.mkt.wroff/
states:
- name: draft
human: Проект сформирован
- name: proposed
human: На рассмотрении совета
description: >
Кооператив сформировал список позиций к списанию и вынес проект
решения на повестку совета. Ждёт рассмотрения советом.
Председатель подписал заявление о списании и внёс проект на
рассмотрение совета. Ожидается решение совета по типовому процессу
решения совета.
kind: normal
- name: authorized
human: Решение совета принято
description: >
Совет принял положительное решение по проекту и подписал протокол.
По каждой позиции проекта последовательно выполняется списание со
склада участка.
kind: normal
- name: executed
human: Списание исполнено
description: >
Совет принял положительное решение, кооператив списал позиции со
складов участков. Имущество ушло из учёта как безвозвратные
потери. По кошелькам заказчиков движений не было.
Кооператив списал все позиции проекта со складов участков.
Имущество ушло из учёта как безвозвратные потери. По кошелькам
заказчиков движений не было.
kind: final
- name: rejected
human: Проект отклонён
human: Совет отклонил проект
description: >
Совет отклонил проект списания либо отложил его рассмотрение.
Позиции остаются на складах участков и попадут в следующий цикл
списания.
Совет отклонил проект списания. Позиции остаются на складах
участков и могут попасть в следующий цикл списания.
kind: final
transitions:
- from: "∅"
to: draft
to: proposed
action: marketplace::propwroff
actor: backend
actor: chairman
guards:
- Наступил регламентный срок очередного цикла списания.
- На складах участков найдены позиции, удовлетворяющие критериям списания (просроченные, не выданные первичному заказчику, повреждённые, малооценные).
- Наступил регламентный срок очередного цикла списания (или ручной запуск председателем).
- Председатель подписал Заявление о списании скоропорта.
- На складах участков найдены позиции, удовлетворяющие критериям списания.
- from: draft
- from: proposed
to: authorized
actor: council
guards:
- Совет рассмотрел проект и принял положительное решение по типовому процессу решения совета.
- Председатель подписал Протокол совета о списании скоропорта.
- from: authorized
to: executed
action: marketplace::execwroff
actor: council
actor: chairman
ledger_code: p.mkt.wroff
operations:
- o.mkt.wroff
- o.mkt.wroff2
guards:
- Совет принял положительное решение по проекту и оформил его протоколом.
- Решение совета по проекту принято и протокол подписан.
- Все позиции проекта последовательно исполнены.
- from: draft
- from: proposed
to: rejected
action: marketplace::declwroff
actor: council
guards:
- Совет принял отрицательное решение по проекту либо не принял решение в установленный регламентом срок.
- Совет рассмотрел проект и отклонил его по типовому процессу решения совета.
# ── Секция 4. Сценарий ──────────────────────────────────────────────────────
scenario:
steps:
- step: 1
title: Формирование проекта списания
actor: backend
title: Формирование и внесение проекта на рассмотрение совета
actor: chairman
action: marketplace::propwroff
description: >
Кооператив по расписанию (по умолчанию раз в месяц, точная дата
согласовывается с бухгалтером) опрашивает склады участков и
собирает позиции, которые невозможно выдать заказчику:
просроченные и не востребованные, повреждённые, малооценные.
Сформированный список становится проектом решения и выносится
на повестку совета.
Кооператив по расписанию (по умолчанию раз в месяц; точную дату
согласовывает бухгалтер) опрашивает склады участков и собирает
позиции, которые невозможно выдать заказчику: просроченные и не
востребованные, повреждённые, малоценные. Председатель оформляет
проект списания со списком позиций, подписывает заявление и
вносит проект на рассмотрение совета.
pre:
- Наступил регламентный срок очередного цикла списания.
- На складах участков найдены позиции, удовлетворяющие критериям списания.
- Председатель подписал Заявление о списании скоропорта.
post:
- Проект списания сформирован.
- Проект вынесен на повестку совета.
- Проект списания сформирован и подписан председателем.
- Проект внесён на рассмотрение совета.
- step: 2
title: Рассмотрение проекта советом
actor: council
action: marketplace::execwroff
action: null
description: >
Совет рассматривает проект — на очном заседании или в форме
заочного голосования — и принимает решение, оформленное
протоколом. При положительном решении кооператив списывает
перечисленные в проекте позиции со складов участков. Имущество
уходит из учёта как безвозвратные потери. По кошелькам
заказчиков движений не происходит.
Совет рассматривает заявление председателя по типовому процессу
решения совета — на очном заседании или в форме заочного
голосования — и принимает решение. При положительном решении
председатель подписывает протокол о списании. При отрицательном —
проект отклоняется.
pre:
- Проект внесён на рассмотрение совета.
- Совет принял положительное решение по проекту и оформил его протоколом.
post:
- Списание исполнено.
- Списанные позиции ушли со складов участков.
- Совет принял решение по проекту и подписал протокол.
- step: 3
title: Исполнение списания
actor: chairman
action: marketplace::execwroff
description: >
По принятому советом протоколу кооператив последовательно
списывает каждую позицию проекта со склада участка. Имущество
безвозвратно выбывает из учёта; сумма закрывается на счёт
целевого финансирования программы. По кошелькам заказчиков
движений нет.
pre:
- Совет принял положительное решение по проекту.
- Протокол совета о списании подписан.
post:
- Все позиции проекта списаны со складов участков.
- Имущество ушло из учёта как безвозвратные потери.
alternatives:
- branch: Совет отклонил проект
at_step: 2
action: marketplace::declwroff
action: null
actor: council
description: >
Совет отклонил проект списания (например, требуется
дополнительная экспертиза) либо отложил его рассмотрение.
Позиции остаются на складах участков и попадут в следующий цикл
списания.
Совет отклонил проект списания например, требуется
дополнительная экспертиза либо отложил рассмотрение. Позиции
остаются на складах участков и могут попасть в следующий цикл
списания. Движений по имуществу и средствам не происходит.
- branch: Альтернатива — переуступка имущества по сниженной цене
at_step: 1
action: null
actor: backend
actor: chairman
description: >
Альтернативный путь для имущества, ещё пригодного к выдаче
(например, накануне истечения срока годности): кооператив сам
Альтернативный путь для имущества, ещё пригодного к выдаче
например, накануне истечения срока годности: кооператив сам
выступает поставщиком в новом предложении по сниженной цене и
ставит имущество на следующий цикл отсечки заявок. В MVP только
помечен; реализуется отдельным процессом в более поздней фазе.
# ── Секция 5. Документы и подписи ───────────────────────────────────────────
# В процессе подписывается протокол решения совета (authorization), который
# является типовым документом совета и обслуживается через процесс
# sov.decision (повестка → голосование → протокол). Этот процесс
# напрямую не оформляет своих документов — только пункт повестки.
# Заявление председателя — собственный документ процесса (вносится советом
# на рассмотрение). Протокол совета — документ типового процесса решения
# совета (по стандарту sov.decision), к которому привязан проект списания.
documents:
- action: marketplace::execwroff
title: Протокол решения совета о списании скоропорта
registry_id: 0
signed_by: [council_members]
stored_in: "(authorization — параметр действия execwroff, обслуживается через sov.decision)"
note: "TODO: использовать существующий шаблон протокола совета (sov.decision-протокол) с прикреплённым перечнем позиций; либо создать специализированный шаблон в cooptypes/cooperative/registry/<id>.MarketplaceWriteoffProtocol при необходимости отдельной формы."
- 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-операция со СОСТАВНОЙ проводкой: транзит через счёт 91.
# В журнале это одна именованная операция; в бухгалтерии — две проводки
# (или одна составная). Реализация: в .cpp execwroff вызывает ledger2::apply
# дважды с одинаковым operation_id (или один раз с composite-record), так
# чтобы транзит через 91 был атомарным и не нарушал инвариантов
# Дт/Кт-баланса по 91. WalletOp ACCOUNT_ONLY — proposed расширение enum'а
# ledger2 для операций без движения по кошелькам.
# Одна ledger2-операция — списание имущества со склада через целевое
# финансирование программы. Срабатывает по каждой позиции проекта в
# транзакции исполнения списания.
operations:
- ledger_code: o.mkt.wroff
human_name: Утилизация скоропорта — выбытие со склада
wallet_op: NONE # NONE = только бухпроводка без движения по кошелькам
# L1 — часть 1 композитной проводки: Дт 91 / Кт 10 (выбытие имущества
# со склада на «прочие»). Закрытие транзита выполняется отдельной
# операцией o.mkt.wroff2 (Дт 86 / Кт 91), атомарно с o.mkt.wroff
# в той же транзакции execwroff (контракт вызывает обе последовательно).
debit: 91 # Прочие доходы и расходы (NEW)
credit: 10 # Материалы (NEW в LEDGER2_ACCOUNT_REGISTRY)
human_name: Утилизация скоропорта
wallet_op: NONE # только бухпроводка без движения по кошелькам
# L1 — двойная запись: целевое финансирование расходуется на выбытие
# имущества со склада (одной операцией).
debit: 86 # Целевое финансирование
credit: 10 # Материалы
# L2 — без перевода между кошельками кооператива
wallet_from: null
wallet_to: null
@@ -263,35 +287,8 @@ operations:
amount_ref: writeoff_item.cost
triggered_by: marketplace::execwroff
description: >
Списание имущества со склада КУ — часть 1 композитной бухгалтерской
проводки через транзит счёта 91 «Прочие доходы и расходы». NONE —
только бухпроводка Дт 91 / Кт 10, кошельки не двигаются. Сразу
после этой операции в той же транзакции execwroff контракт вызывает
o.mkt.wroff2 (Дт 86 / Кт 91, NONE) — закрытие транзита на счёт ЦФ
программы. Применяется по каждой позиции из списка writeoff_proposal —
может быть несколько последовательных пар в одной транзакции
execwroff. Согласовано с Ангелиной 2026-04-27.
- ledger_code: o.mkt.wroff2
human_name: Утилизация скоропорта — закрытие транзита
wallet_op: NONE
# L1 — часть 2 композитной проводки: Дт 86 / Кт 91 (закрытие «прочих»
# на счёт ЦФ программы).
debit: 86 # Целевое финансирование
credit: 91 # Прочие доходы и расходы (NEW)
# L2 / L3 — без движения по кошелькам
wallet_from: null
wallet_to: null
user_wallet: null
user_ref: null
available_delta: null
blocked_delta: null
amount_ref: writeoff_item.cost
triggered_by: marketplace::execwroff
description: >
Закрытие транзита счёта 91 после выбытия имущества со склада. NONE —
только бухпроводка Дт 86 / Кт 91, кошельки не двигаются. Срабатывает
атомарно с o.mkt.wroff в той же транзакции execwroff. По итогу пары
operations нетто-эффект — Дт 86 / Кт 10 (закрытие ЦФ программы +
выбытие имущества со склада), 91 проходит нулевым транзитом.
Списание имущества со склада участка по протоколу совета. Двойная
запись Дт 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,14 +1,13 @@
/**
* @brief Председатель принимает гарантийный возврат на очном осмотре (Story 7.4, p.mkt.return).
*
* Композитная транзакция (атомарно в одной Antelope tx):
* - Ledger2::apply(o.mkt.return, fact_cost, orderer, hash=request.hash) Дт 91 / Кт 86, ISSUE w.mkt.member.
* - Ledger2::apply(o.mkt.return2, fact_cost, orderer, hash=request.hash) Дт 10 / Кт 91, NONE.
* - 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 / o.mkt.consum2 в
* журнале НЕ модифицируются.
* в submretrn) для трассировки. Исходная o.mkt.consum в журнале НЕ модифицируется.
*
* Status: approved_for_visit return_accepted (final). Имущество возвращается
* на склад КУ; средства восстанавливаются на w.mkt.member.available заказчика.
@@ -38,15 +37,11 @@ void marketplace::accretrn(eosio::name coopname,
verify_document_or_fail(decision, { signer });
}
// Композитная пара return + return2
// 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));
Ledger2::apply(_marketplace, coopname,
operations::marketplace::RETURN_TRANSIT_CLOSE,
r.fact_cost, r.orderer, r.hash,
Marketplace::Memo::get_return_transit_close_memo(r.id, r.original_order_id));
Marketplace::update_return_request(coopname, r.id, [&](auto& upd) {
upd.status = ReturnStatus::RETURN_ACCEPTED;
@@ -1,9 +1,9 @@
/**
* @brief Заказчик отменяет заказ до акцепта поставщиком (Story 4.4, p.mkt.supply).
*
* Триггерит `o.mkt.unblk` на full `order.total_cost`. Сумма остаётся на
* `w.mkt.member.available` заказчика может быть потрачена на следующий
* заказ программы либо выведена явным `o.mkt.recall` (отдельное действие).
* Триггерит `o.mkt.unlock` на full `order.total_cost` TRANSFER w.mkt.order
* w.mkt.member (снятие резерва). Сумма возвращается на членский «Стола заказов»
* `w.mkt.member.available` заказчика остаётся в программе под следующие заказы.
*
* Guards:
* - Order существует.
@@ -24,7 +24,7 @@ void marketplace::cancelorder(eosio::name coopname,
"Нельзя отменить заказ: он уже акцептован поставщиком");
Ledger2::apply(_marketplace, coopname,
operations::marketplace::UNBLOCK_ON_CANCEL,
operations::marketplace::UNLOCK_ORDER,
o.total_cost, orderer, o.hash,
Marketplace::Memo::get_cancel_order_memo(o.id));
@@ -1,33 +1,26 @@
/**
* @brief Заказчик размещает заказ на товар (Story 4.1, p.mkt.supply шаг 1).
*
* Серия операций (атомарно в одной транзакции Antelope):
* 1. `o.wal.conv` (conditional) TRANSFER w.wal.share w.wal.member,
* Дт 80 / Кт 86. Только если на `w.wal.member.available` заказчика и
* на `w.mkt.member.available` суммарно не хватает суммы заказа.
* 2. `o.mkt.assign` (conditional) TRANSFER w.wal.member w.mkt.member,
* без проводки. Только если на `w.mkt.member.available` не хватает.
* 3. `o.mkt.block` (всегда) BLOCK на `w.mkt.member` пайщика на total_cost.
* Одна 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; cycle_type валидный.
* - quantity > 0; unit_price > 0 в _root_govern_symbol.
* - Order с таким hash ещё не создан (idempotency).
* - Заказчик активный пайщик кооператива (`get_participant_or_fail`).
* - `delivery_braname` существует в `branches` (КУ выдачи задаётся пайщиком
* из доступных и неизменен после создания Order'а).
* - Σ available трёх кошельков (w.wal.share + w.wal.member + w.mkt.member)
* >= total_cost; иначе createorder фейлится без создания Order'а.
* - w.wal.share.available заказчика >= total_cost; иначе createorder фейлится
* без создания Order'а.
* - Подписка пайщика на оферту ЦПП «Стол заказов» (L2/L3 онбординг)
* автоматически проверяется в `ledger2::walletop` через
* `assert_program_signed` (cross-contract в `wallet::users.programs[]`)
* при первом ASSIGN/BLOCK на USER_SHARED-кошельке программы.
* при первом TRANSFER на USER_SHARED-кошельке программы (w.mkt.order).
*
* Сообщения проверок для прямого показа пользователю (UI ловит check'ом).
*
* @note process_hash для всех трёх ledger2-операций `order_hash`. Это
* даёт backend'у одну точку группировки операций процесса
* p.mkt.supply через `getProcess(process_hash)` (Story 9.3).
*
* @ingroup public_marketplace_actions
*/
void marketplace::createorder(eosio::name coopname,
@@ -38,7 +31,6 @@ void marketplace::createorder(eosio::name coopname,
eosio::name delivery_braname,
uint64_t quantity,
eosio::asset unit_price,
eosio::name cycle_type,
uint32_t warranty_period_secs,
checksum256 batch_hash) {
require_auth(coopname);
@@ -49,11 +41,6 @@ void marketplace::createorder(eosio::name coopname,
"Некорректная цена за единицу");
eosio::check(unit_price.symbol == _root_govern_symbol,
"Некорректный символ валюты в цене");
eosio::check(cycle_type == CycleType::TIME_BASED ||
cycle_type == CycleType::VOLUME_BASED ||
cycle_type == CycleType::OPEN_SUBSCRIPT ||
cycle_type == CycleType::INDIVIDUAL,
"Неизвестный тип цикла отсечки заявок");
// Idempotency: Order с таким hash не должен существовать
eosio::check(!Marketplace::get_order_by_hash(coopname, order_hash).has_value(),
@@ -72,31 +59,12 @@ void marketplace::createorder(eosio::name coopname,
eosio::check(total_cost.amount > 0,
"Итоговая сумма заказа должна быть больше нуля");
// ── Достаточность средств: Σ available трёх кошельков >= total_cost ─
auto bal_share = Marketplace::get_user_wallet_balance(
// ── Достаточность средств: w.wal.share.available >= total_cost ──────
auto bal_share = Marketplace::get_user_wallet_balance(
coopname, ledger2_wallets::SHARE_FUND_PAY, orderer);
auto bal_member = Marketplace::get_user_wallet_balance(
coopname, ledger2_wallets::CK_MEMBER, orderer);
auto bal_mkt = Marketplace::get_user_wallet_balance(
coopname, ledger2_wallets::MARKETPLACE_MEMBER, orderer);
eosio::asset total_available =
bal_share.available + bal_member.available + bal_mkt.available;
eosio::check(total_available >= total_cost,
eosio::check(bal_share.available >= total_cost,
std::string{"Недостаточно средств для заказа: требуется "} +
total_cost.to_string() + ", доступно " + total_available.to_string());
// ── Расчёт conditional-долей серии ──────────────────────────────────
// Сначала тратим w.mkt.member.available; недостающее берём из w.wal.member
// через assign; недостающее в w.wal.member берём из w.wal.share через conv.
const eosio::asset zero = eosio::asset(0, _root_govern_symbol);
eosio::asset need_to_assign = (bal_mkt.available >= total_cost)
? zero
: (total_cost - bal_mkt.available);
eosio::asset need_to_conv = (bal_member.available >= need_to_assign)
? zero
: (need_to_assign - bal_member.available);
total_cost.to_string() + ", доступно " + bal_share.available.to_string());
// ── Создание Order entity (id потребуется для memo) ─────────────────
orders_index orders(_marketplace, coopname.value);
@@ -119,32 +87,15 @@ void marketplace::createorder(eosio::name coopname,
o.total_cost = total_cost;
o.fact_cost = total_cost; // до signiss2 == total_cost
o.cycle_type = cycle_type;
o.warranty_period_secs = warranty_period_secs;
o.status = OrderStatus::ACTIVE;
o.batch_hash = batch_hash;
});
// ── Шаг 1: o.wal.conv (conditional) ──────────────────────────────────
if (need_to_conv.amount > 0) {
Ledger2::apply(_marketplace, coopname,
operations::wallet::CONVERT_TO_MEMBER,
need_to_conv, orderer, order_hash,
Marketplace::Memo::get_create_order_convert_memo(new_id));
}
// ── Шаг 2: o.mkt.assign (conditional) ────────────────────────────────
if (need_to_assign.amount > 0) {
Ledger2::apply(_marketplace, coopname,
operations::marketplace::ASSIGN_TO_PROGRAM,
need_to_assign, orderer, order_hash,
Marketplace::Memo::get_create_order_assign_memo(new_id));
}
// ── Шаг 3: o.mkt.block (всегда) ──────────────────────────────────────
// ── o.mkt.lock: TRANSFER w.wal.share → w.mkt.order (Дт 80 / Кт 86) ───
Ledger2::apply(_marketplace, coopname,
operations::marketplace::BLOCK_FOR_ORDER,
operations::marketplace::LOCK_ORDER,
total_cost, orderer, order_hash,
Marketplace::Memo::get_create_order_block_memo(new_id));
}
@@ -1,9 +1,10 @@
/**
* @brief Поставщик отказывается от одного Order'а до акцепта (Story 4.5, p.mkt.supply).
*
* Per-Order: o.mkt.unblk на total_cost (резерв возвращается заказчику) +
* статус active cancelled. Backend проходит циклом по orders соответствующего
* batch'а, вызывая `declineorder` per Order векторов order_hash нет.
* 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.
@@ -22,7 +23,7 @@ void marketplace::declineorder(eosio::name coopname,
"Заказ уже не в активном статусе — отклонить нельзя");
Ledger2::apply(_marketplace, coopname,
operations::marketplace::UNBLOCK_ON_CANCEL,
operations::marketplace::UNLOCK_ORDER,
o.total_cost, o.orderer, o.hash,
Marketplace::Memo::get_decline_order_memo(o.id));
@@ -8,7 +8,7 @@
* threshold это вычисление backend'а; on-chain только закрытие конкретного
* Order'а с возвратом резерва.
*
* Per-Order: o.mkt.unblk на total_cost + статус active cancelled.
* Per-Order: o.mkt.unlock на total_cost (TRANSFER w.mkt.order w.mkt.member возврат резерва на членский «Стола заказов» заказчика) + статус active cancelled.
*
* Guards:
* - Order существует и в статусе active (после акцепта поставщика
@@ -28,7 +28,7 @@ void marketplace::expireorder(eosio::name coopname,
"Закрыть по таймауту можно только активный заказ");
Ledger2::apply(_marketplace, coopname,
operations::marketplace::UNBLOCK_ON_CANCEL,
operations::marketplace::UNLOCK_ORDER,
o.total_cost, o.orderer, o.hash,
Marketplace::Memo::get_expire_order_memo(o.id));
@@ -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
});
}
@@ -2,31 +2,47 @@
* @brief Председатель приёмного КУ ставит закрывающую подпись на АПП приёмки
* по одному Order'у (Story 5.3/5.4, p.mkt.supply).
*
* Per-Order композитная транзакция (атомарно в одной Antelope tx):
* - Ledger2::apply(o.mkt.purch, total_cost, , hash=order.hash) Дт 10 / Кт 86.
* - Ledger2::apply(o.mkt.payout, total_cost, , hash=order.hash) Дт 86 / Кт 51.
* Per-Order только бухгалтерская приёмка имущества:
* - Ledger2::apply(o.mkt.purch, fact_cost, , hash=order.hash) Дт 10 / Кт 86.
*
* Имущество приходуется на склад приёмного КУ (`accept_braname`); поставщику
* уходит оплата на w.mkt.payout. Каждый Order отдельная пара (purch +
* payout) с собственным process_hash векторов order_hash нет, backend
* проходит циклом по orders соответствующего batch'а с одним и тем же актом.
* Факт приёмки (кол-во и цена за единицу) корректируется оператором при
* открытии приёмки и зашивается в акт, который утверждает поставщик: привезли
* меньше / другого качества принимаем со скидкой. Кооператив приходует
* поставщику итоговую `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,
@@ -39,18 +55,23 @@ void marketplace::signchair(eosio::name coopname,
verify_document_or_fail(act, { o.offerer, signer });
// Композитная пара purch + payout (атомарно)
// Итоговая стоимость к получению поставщиком — от скорректированного факта.
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,
o.total_cost, o.offerer, o.hash,
fact_cost, o.offerer, o.hash,
Marketplace::Memo::get_purchase_from_supplier_memo(o.id));
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.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; // имущество на приёмном складе
});
@@ -4,20 +4,23 @@
* Per-Order атомарная транзакция с поддержкой actual_quantity ordered (Story 6.2):
*
* 1) actual == ordered:
* Ledger2::apply(o.mkt.consum, fact_cost) Дт 91 / Кт 10, REVOKE на blocked.
* Ledger2::apply(o.mkt.consum2, fact_cost) Дт 86 / Кт 91, NONE.
* Ledger2::apply(o.mkt.consum, fact_cost) BURN w.mkt.order, Дт 86 / Кт 10
* (сжигание резерва заказа и выбытие имущества со склада через целевое
* финансирование).
*
* 2) actual < ordered (выдано меньше остаток на blocked возвращается):
* Ledger2::apply(o.mkt.unblk, ordered_cost - fact_cost) снятие резерва на разницу.
* затем те же consum + consum2 на fact_cost.
* 2) actual < ordered (выдано меньше остаток резерва возвращается):
* Ledger2::apply(o.mkt.unlock, ordered_cost - fact_cost) TRANSFER w.mkt.order
* w.mkt.member на разницу (остаток резерва на членский «Стола заказов»).
* затем consum на fact_cost.
*
* 3) actual > ordered (доплата с паевого):
* 3) actual > ordered (доплата по факту НЕ списываем с паевого напрямую):
* diff = fact_cost - ordered_cost
* проверка достаточности средств для diff (через w.wal.share + w.wal.member).
* Conditional o.wal.conv (если на w.wal.member недостача).
* Conditional o.mkt.assign (если на w.mkt.member недостача).
* o.mkt.block(diff) резервируем доплату на этот же Order.
* затем consum + consum2 на fact_cost.
* проверка достаточности средств для diff на w.wal.share (источник конвертации).
* o.mkt.conv(diff) TRANSFER w.wal.share w.mkt.member, Дт 80 / Кт 86
* дополнительный паевой взнос конвертируется в членский «Стола заказов».
* o.mkt.lockm(diff) TRANSFER w.mkt.member w.mkt.order (без проводки)
* добор резерва на этот же Order ИМЕННО с членского программы.
* Затем consum на fact_cost.
*
* Status: ready_to_receive received. actual_quantity, fact_cost,
* issue_act_signiss2, warranty_until заполняются.
@@ -27,7 +30,9 @@
* - Подписант со стороны кооператива (`delivery_signer`) авторизован для
* КУ выдачи (`o.delivery_braname`).
* - verify_document_or_fail(act, {delivery_signer, orderer}).
* - actual_quantity > 0.
* - actual_quantity > 0; actual_unit_price > 0 и в валюте кооператива.
* fact_cost = actual_quantity × actual_unit_price (цена скорректирована
* оператором при открытии выдачи, заказчик факт не редактирует).
* - При actual > ordered и нехватке средств транзакция фейлится с
* человеческим сообщением, которое UI показывает напрямую.
* - Idempotency: is_empty_document(issue_act_signiss2).
@@ -38,10 +43,15 @@ void marketplace::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) {
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.orderer == orderer, "Вы не заказчик этого заказа");
@@ -56,74 +66,56 @@ void marketplace::signiss2(eosio::name coopname,
verify_document_or_fail(act, { delivery_signer, orderer });
// Факт считается от скорректированной оператором цены (actual_unit_price),
// а не от цены заказа (o.unit_price): оператор мог снизить/поднять цену на
// месте (испорчена упаковка, замена позиции и т. п.).
const eosio::asset fact_cost = eosio::asset(
static_cast<int64_t>(actual_quantity) * o.unit_price.amount,
static_cast<int64_t>(actual_quantity) * actual_unit_price.amount,
_root_govern_symbol);
eosio::check(fact_cost.amount > 0,
"Итоговая фактическая сумма заказа должна быть больше нуля");
// ── Корректирующие операции (если факт ≠ заказ) ─────────────────────
if (fact_cost < o.total_cost) {
// actual < ordered: возвращаем разницу с blocked → available
// actual < ordered: остаток резерва возвращается на членский «Стола заказов»
const eosio::asset diff = o.total_cost - fact_cost;
Ledger2::apply(_marketplace, coopname,
operations::marketplace::UNBLOCK_ON_CANCEL,
operations::marketplace::UNLOCK_ORDER,
diff, orderer, o.hash,
Marketplace::Memo::get_signiss2_correction_less_memo(o.id));
} else if (fact_cost > o.total_cost) {
// actual > ordered: добираем разницу с паевого + assign + block
// actual > ordered: доплату НЕ списываем с паевого напрямую. Дополнительный
// паевой взнос конвертируется в членский «Стола заказов» (o.mkt.conv), и уже
// с членского программы добирается резерв этого же заказа (o.mkt.lockm).
const eosio::asset diff = fact_cost - o.total_cost;
// Проверка доступности diff в трёх кошельках
auto bal_share = Marketplace::get_user_wallet_balance(
// Проверка доступности diff на паевом заказчика — источнике конвертации.
auto bal_share = Marketplace::get_user_wallet_balance(
coopname, ledger2_wallets::SHARE_FUND_PAY, orderer);
auto bal_member = Marketplace::get_user_wallet_balance(
coopname, ledger2_wallets::CK_MEMBER, orderer);
auto bal_mkt = Marketplace::get_user_wallet_balance(
coopname, ledger2_wallets::MARKETPLACE_MEMBER, orderer);
eosio::asset total_avail = bal_share.available + bal_member.available + bal_mkt.available;
eosio::check(total_avail >= diff,
eosio::check(bal_share.available >= diff,
std::string{"Недостаточно средств для дооплаты по факту: требуется "} +
diff.to_string() + ", доступно " + total_avail.to_string());
diff.to_string() + ", доступно " + bal_share.available.to_string());
const eosio::asset zero = eosio::asset(0, _root_govern_symbol);
eosio::asset need_to_assign = (bal_mkt.available >= diff)
? zero
: (diff - bal_mkt.available);
eosio::asset need_to_conv = (bal_member.available >= need_to_assign)
? zero
: (need_to_assign - bal_member.available);
if (need_to_conv.amount > 0) {
Ledger2::apply(_marketplace, coopname,
operations::wallet::CONVERT_TO_MEMBER,
need_to_conv, orderer, o.hash,
Marketplace::Memo::get_signiss2_correction_more_convert_memo(o.id));
}
if (need_to_assign.amount > 0) {
Ledger2::apply(_marketplace, coopname,
operations::marketplace::ASSIGN_TO_PROGRAM,
need_to_assign, orderer, o.hash,
Marketplace::Memo::get_signiss2_correction_more_assign_memo(o.id));
}
// 1) Конвертация паевой → членский «Стола заказов» (Дт 80 / Кт 86).
Ledger2::apply(_marketplace, coopname,
operations::marketplace::BLOCK_FOR_ORDER,
operations::marketplace::CONVERT_TO_MKT_MEMBER,
diff, orderer, o.hash,
Marketplace::Memo::get_signiss2_correction_more_convert_memo(o.id));
// 2) Добор резерва заказа с членского «Стола заказов» (без проводки, оба на 86).
Ledger2::apply(_marketplace, coopname,
operations::marketplace::LOCK_FROM_MEMBER,
diff, orderer, o.hash,
Marketplace::Memo::get_signiss2_correction_more_block_memo(o.id));
}
// fact == ordered — без корректировок
// ── Композитная пара consum + consum2 на fact_cost ──────────────────
// ── o.mkt.consum на fact_cost (BURN w.mkt.order, Дт 86 / Кт 10) ──────
Ledger2::apply(_marketplace, coopname,
operations::marketplace::CONSUME_BY_MEMBER,
fact_cost, orderer, o.hash,
Marketplace::Memo::get_consume_by_member_memo(o.id));
Ledger2::apply(_marketplace, coopname,
operations::marketplace::CONSUME_TRANSIT_CLOSE,
fact_cost, orderer, o.hash,
Marketplace::Memo::get_consume_transit_close_memo(o.id));
// ── Закрытие Order'а ────────────────────────────────────────────────
const auto now = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
@@ -1,39 +0,0 @@
/**
* @brief Совет отклоняет проект списания скоропорта целиком (Story 8.3, p.mkt.wroff).
*
* Без ledger2-операций. Status: PROPOSED REJECTED (final).
* reason сохраняется в proposal.reject_reason. Позиции остаются на складах
* участков и попадут в следующий цикл списания. Отклонить можно только
* непочатый proposal если хотя бы одна позиция уже исполнена через
* execwroff, отклонить целиком запрещено.
*
* Guards:
* - proposal.status == PROPOSED.
* - Ни одна из items не была исполнена (executed == false для всех).
* - reason.size() > 0.
*
* @ingroup public_marketplace_actions
*/
void marketplace::declwroff(eosio::name coopname,
eosio::name decided_by,
checksum256 proposal_hash,
std::string reason) {
require_auth(coopname);
eosio::check(reason.size() > 0 && reason.size() <= 500,
"Укажите причину отклонения проекта списания (от 1 до 500 символов)");
auto p = Marketplace::get_writeoff_proposal_by_hash_or_fail(coopname, proposal_hash);
eosio::check(p.status == WroffStatus::PROPOSED,
"Проект списания не находится на рассмотрении");
for (const auto& it : p.items) {
eosio::check(!it.executed,
"Нельзя отклонить проект — часть позиций уже исполнена");
}
Marketplace::update_writeoff_proposal(coopname, p.id, [&](auto& upd) {
upd.status = WroffStatus::REJECTED;
upd.decided_by = decided_by;
upd.reject_reason = reason;
});
}
@@ -1,44 +1,33 @@
/**
* @brief Совет исполняет одну позицию проекта списания скоропорта (Story 8.3,
* p.mkt.wroff).
* @brief Backend исполняет одну позицию авторизованного советом проекта
* списания скоропорта (Story 8.4, p.mkt.wroff).
*
* Per-item композитная транзакция (атомарно):
* - Ledger2::apply(o.mkt.wroff, item.amount, , hash=proposal.hash) Дт 91 / Кт 10.
* - Ledger2::apply(o.mkt.wroff2, item.amount, , hash=proposal.hash) Дт 86 / Кт 91.
* Per-item операция:
* - Ledger2::apply(o.mkt.wroff, item.amount, , hash=proposal.hash) Дт 86 / Кт 10
* (списание со склада через целевое финансирование).
*
* Backend проходит циклом по неисполненным позициям protocols proposal'а,
* вызывая `execwroff(proposal_hash, item_index, protocol)` per item это
* снимает ограничение на максимальный размер протокола (тысячи позиций
* не помещаются в одну транзакцию Antelope).
* Вызывается ТОЛЬКО после того, как совет авторизовал проект (status =
* AUTHORIZED через callback `onmktwoauth`). Backend проходит циклом по
* неисполненным позициям, вызывая `execwroff(proposal_hash, item_index)`
* per item это снимает ограничение на максимальный размер протокола.
*
* Все операции с одним process_hash = proposal.hash backend через
* `getProcess(proposal.hash)` соберёт полную трассировку процесса.
*
* Status: PROPOSED EXECUTED наступает автоматически после исполнения
* Status: AUTHORIZED EXECUTED наступает автоматически после исполнения
* последней позиции (когда все items[i].executed становятся true).
*
* Guards:
* - proposal.status == PROPOSED.
* - item_index в пределах proposal.items.
* - items[item_index].executed == false (idempotency).
* - Подписант (`signer`) авторизован для КУ-источника списания
* (`items[item_index].braname`) председатель / trustee / trusted.
* - verify_document_or_fail(protocol, {signer}) sanity check на подпись;
* реальная сверка с протоколом совета backend pre-validation через
* soviet::decisions.
* Подписанный советом protocol уже лежит в `proposal.protocol` (положен
* callback'ом `onmktwoauth`), отдельно его передавать не нужно.
*
* @ingroup public_marketplace_actions
*/
void marketplace::execwroff(eosio::name coopname,
eosio::name signer,
checksum256 proposal_hash,
uint64_t item_index,
document2 protocol) {
uint64_t item_index) {
require_auth(coopname);
auto p = Marketplace::get_writeoff_proposal_by_hash_or_fail(coopname, proposal_hash);
eosio::check(p.status == WroffStatus::PROPOSED,
"Проект списания не находится на исполнении");
eosio::check(p.status == WroffStatus::AUTHORIZED,
"Проект списания не авторизован советом");
eosio::check(item_index < p.items.size(),
"Указана несуществующая позиция в проекте списания");
eosio::check(!p.items[item_index].executed,
@@ -50,22 +39,15 @@ void marketplace::execwroff(eosio::name coopname,
eosio::check(branch.is_user_authorized(signer),
"Подписант не уполномочен исполнять списание данного кооперативного участка");
verify_document_or_fail(protocol, { signer });
// Композитная пара wroff + wroff2 для одной позиции
// o.mkt.wroff: Дт 86 / Кт 10
Ledger2::apply(_marketplace, coopname,
operations::marketplace::WRITE_OFF_PERISHABLE,
item.amount, item.braname, p.hash,
Marketplace::Memo::get_writeoff_memo(p.id, item_index));
Ledger2::apply(_marketplace, coopname,
operations::marketplace::WRITE_OFF_TRANSIT_CLOSE,
item.amount, item.braname, p.hash,
Marketplace::Memo::get_writeoff_transit_close_memo(p.id, item_index));
// Помечаем позицию исполненной + финализируем proposal если все позиции готовы
Marketplace::update_writeoff_proposal(coopname, p.id, [&](auto& upd) {
upd.items[item_index].executed = true;
upd.protocol = protocol;
upd.decided_by = signer;
bool all_done = true;
@@ -0,0 +1,34 @@
/**
* @brief Callback от `soviet::exec` после авторизации Протокола совета о
* списании скоропорта (registry 1105) председателем (Story 8.4, p.mkt.wroff).
*
* Соглашение о сигнатуре `(coopname, hash, authorization)` задано в
* `soviet::createagenda::authorize_action_effect`. Контракт `soviet` зовёт
* `marketplace::onmktwoauth` от своего имени, поэтому единственно допустимая
* авторизация `_soviet`.
*
* Эффект:
* - Находит wroffprops по proposal_hash == `hash`.
* - Проверяет, что текущий статус == PROPOSED (запрет повторного callback'а).
* - Записывает подписанный советом protocol2 в `proposal.protocol`.
* - Переводит статус PROPOSED AUTHORIZED.
*
* Дальнейший шаг backend через дельту/мониторинг видит AUTHORIZED и
* проходит циклом execwroff per-item (см. `execwroff.cpp`).
*
* @ingroup public_marketplace_actions
*/
void marketplace::onmktwoauth(eosio::name coopname,
checksum256 hash,
document2 authorization) {
require_auth(_soviet);
auto p = Marketplace::get_writeoff_proposal_by_hash_or_fail(coopname, hash);
eosio::check(p.status == WroffStatus::PROPOSED,
"Проект списания не находится на повестке (callback повторный или поздний)");
Marketplace::update_writeoff_proposal(coopname, p.id, [&](auto& upd) {
upd.status = WroffStatus::AUTHORIZED;
upd.protocol = authorization;
});
}
@@ -0,0 +1,34 @@
/**
* @brief Callback от `soviet` после отказа в Протоколе совета о списании
* скоропорта или истечения срока повестки (Story 8.4, p.mkt.wroff).
*
* Сигнатура `(coopname, hash, reason)` соответствует
* `DECLINE_CALLBACK_SIGNATURE` (см. `cpp/lib/core/soviet/soviet.hpp:19`).
* Контракт `soviet` вызывает action из `cancelexprd` (повестка просрочена)
* или вручную через `decline*` actions от своего имени, поэтому единственно
* допустимая авторизация `_soviet`.
*
* Эффект:
* - Находит wroffprops по proposal_hash == `hash`.
* - Проверяет, что текущий статус == PROPOSED.
* - Записывает `reason` в `proposal.reject_reason`.
* - Переводит статус PROPOSED REJECTED.
*
* Без ledger2-движений.
*
* @ingroup public_marketplace_actions
*/
void marketplace::onmktwodecl(eosio::name coopname,
checksum256 hash,
std::string reason) {
require_auth(_soviet);
auto p = Marketplace::get_writeoff_proposal_by_hash_or_fail(coopname, hash);
eosio::check(p.status == WroffStatus::PROPOSED,
"Проект списания не находится на повестке (callback повторный или поздний)");
Marketplace::update_writeoff_proposal(coopname, p.id, [&](auto& upd) {
upd.status = WroffStatus::REJECTED;
upd.reject_reason = reason;
});
}
@@ -1,15 +1,20 @@
/**
* @brief Backend / админ выносит проект списания скоропорта на повестку
* совета (Story 8.1, p.mkt.wroff).
* @brief Выносит проект списания скоропорта на повестку совета
* (Story 8.1, p.mkt.wroff).
*
* Без ledger2-операций. Создаётся writeoff_proposal в статусе proposed
* (= "draft" в YAML); total_amount = Σ items.amount; все items создаются
* с executed=false. Списание выполняется отдельно `execwroff(proposal_hash,
* item_index, protocol)` исполняет одну позицию за вызов; backend проходит
* циклом по items, что снимает ограничение на размер протокола.
* Без ledger2-операций. Создаётся writeoff_proposal в статусе proposed;
* total_amount = Σ items.amount; все items создаются с executed=false. Тем же
* action'ом контракт ставит повестку: inline-вызов `soviet::createagenda`
* от `permission_level{_marketplace, active}` (marketplace в contracts_whitelist)
* с `type=mktwroff`, `hash=proposal_hash`, `callback_contract=_marketplace`,
* `confirm_callback=onmktwoauth`, `decline_callback=onmktwodecl`,
* `statement` (подписанное Заявление 1106). Мост повестки целиком на контракте
* backend не подписывает createagenda отдельно (кооператив не в whitelist).
* Списание выполняется per-item через `execwroff` только после callback'а
* `onmktwoauth` (status proposed authorized).
*
* Guards:
* - actor backend (auth coopname).
* - actor coopname (require_auth).
* - items.size() > 0.
* - Все items.amount > 0 в _root_govern_symbol.
* - Все items.braname существуют в `branches[coopname]`.
@@ -20,7 +25,9 @@
void marketplace::propwroff(eosio::name coopname,
eosio::name proposed_by,
checksum256 proposal_hash,
std::vector<wroff_item> items) {
std::vector<wroff_item> items,
document2 statement,
std::string meta) {
require_auth(coopname);
eosio::check(!items.empty(), "Список позиций к списанию пуст");
@@ -49,4 +56,21 @@ void marketplace::propwroff(eosio::name coopname,
p.total_amount = total;
p.status = WroffStatus::PROPOSED;
});
// Мост повестки совета: marketplace в contracts_whitelist, поэтому
// createagenda авторизуется от permission_level{_marketplace, active}.
// hash=proposal_hash, чтобы callback onmktwoauth/onmktwodecl нашёл проект.
action(permission_level{_marketplace, "active"_n}, _soviet, "createagenda"_n,
std::make_tuple(
coopname,
proposed_by,
get_valid_soviet_action(_marketplace_writeoff_action),
proposal_hash,
_marketplace,
"onmktwoauth"_n,
"onmktwodecl"_n,
statement,
meta
)
).send();
}
@@ -10,7 +10,7 @@
# ↓
# authorized=true → exec → callback в исходный контракт (confirmreg /
# authmeet / approvereg / authpgprp / authrslt
# / authcontrib / approvewthd / ... — конкретный
# / authcontrib / authwthd / ... — конкретный
# handler заранее зашит в decision.type).
#
# Документ повестки приходит из контракта-инициатора (без registry_id —
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More