Кнопка «Продолжить» проверяла только число слотов совета, а не email и
обязательные поля ФЛ. Добавлена валидация на фронте и в install.interactor,
чтобы частично заполненный состав не уходил на цепь.
Co-authored-by: Cursor <cursoragent@cursor.com>
Fix swapped doc_data placeholders in generator/blagorost templates, align the install widget with Base* components, and use one shared PrivateData hash in factory tests.
Co-authored-by: Cursor <cursoragent@cursor.com>
На prod требуется 3 человека (на dev — 1): кнопка и подсказка, состав редактируется свободно до перехода на следующий шаг.
Co-authored-by: Cursor <cursoragent@cursor.com>
Не показываем заглушку техобслуживания на /install, разрешаем повтор install из maintenance без vars, проверяем минимум членов совета до записи в цепь (3 на prod, 1 на dev).
Co-authored-by: Cursor <cursoragent@cursor.com>
Изменены компоненты, связанные с выходом из кооператива. В меню заменены иконки и пути для поддержки и выхода. Обновлены метаданные и названия для кнопок и страниц, чтобы улучшить пользовательский интерфейс. В ExitButton добавлены параметры для настройки иконки и метки. Упрощена структура MembershipExitPage с акцентом на ключевые шаги выхода. Исправлены комментарии и типы для лучшего понимания кода.
confirmexit теперь обходит сет LEDGER2_EXIT_REFUND_WALLETS (w.reg.minshr +
w.wal.share + w.cap.blago): собирает доступные L3-балансы каждого (>0),
консолидирует на главный паевой (w.reg.minshr→o.reg.mvmin, w.cap.blago→
o.cap.wthcap) и ставит полную сумму на возврат единым платежом. Раньше
хардкодил только minshr+share — паевой в Благоросте оставался висеть.
Сет задан один раз в wallets.hpp (источник истины), генерируется gen:from-cpp
в cooptypes (LEDGER2_EXIT_REFUND_WALLETS / EXIT_REFUND_WALLET_NAMES) и обходится
backend-preview (getReturnPreview) — расчёт на фронте всегда совпадает с тем,
что реально вернёт контракт. Подпись «планируемая сумма / итог фиксирует Совет»
убрана из ExitButton и ExitOverlay (сумма теперь авторитетна).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- статус: вышедший пайщик (participant_account стёрт delpartcpnt + user_account
blocked) больше не показывается «Активный пайщик» — добавлен терминал
«Вышел из кооператива» по user_account.status
- дата вступления: фолбэк participant_account.created_at → user_account.registered_at
(у вышедших пайщик-запись стёрта, дата больше не «отсутствует»)
- убрана кнопка удаления пайщика (таблица + карточка + диалог + машинерия):
пайщика из реестра не удаляют
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Культура денег: входящие — подтверждением, исходящие — чеком. Единый механизм
на уровне ядра (gateway), привязка по payment_hash — один на все исходящие
(возврат паевого/withdrawal/registration-refund/аванс расхода), переиспользуется
расширениями. Контроль мягкий: статус не трогаем, но зеркалим proof_count →
реестр рисует «чек приложен / не приложен».
Backend (core gateway):
- таблица payment_files + бакет gateway:files (@UseBucket), PaymentFilesService
(upload/read-url/list/delete + зеркалирование proof_count в платёж)
- enum PaymentFileKind (PAYMENT_PROOF), DTO, резолвер uploadPaymentProof /
paymentProofs / paymentFile; провайдеры в typeorm.module + gateway.module
SDK: selectors/mutations/queries gateway + zeus regen
Desktop:
- features/Payment/AttachPaymentProof — панель чека по payment-hash
- реестр: панель + индикатор «чек приложен» для ЛЮБОГО исходящего (PAID/COMPLETED)
- конвергенция expense: чек ушёл в ядро, AttachExpenseProof оставляет только
закрывающие документы (DIRECT)
Терминология: «чек об оплате» (не «платёжка»/«первичка»).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
После completexit строка registrator::exits стирается (терминал=erase) →
getMembershipExit возвращал null → оверлей пропадал, кабинет разблокировался,
пайщик видел стол и мог подать выход повторно (гарда не было).
- enum MembershipExitStatus += COMPLETED
- getMembershipExit: терминальная фаза по blocked-аккаунту + персистентному
MEMBERSHIP_EXIT-платежу (сумма возврата + статус 'Оплачено')
- createMembershipExit: гард — заблокированный аккаунт не может выйти повторно
(контракт уже блокирует через get_participant_or_fail; отсекаем раньше)
- ExitOverlay: терминальная фаза 'Вы вышли из кооператива' (сумма + 'Оплачено'
+ ожидайте поступления), она же UX-гард — перекрывает кабинет
- SDK Zeus regen под новый enum
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Инцидент 2026-06-16: коммит d02c0ce (18 мая) добавил program_expense_pool и
program_expense_reserved в global_state ПЕРЕД полем config напрямую, без
binary_extension. После деплоя на прод таблица state перестала читаться:
запись сериализована старым layout (config сразу после
program_membership_cumulative_reward_per_share), а новый ABI ждёт два asset
перед config → unpack натыкается на байты config (get table → "Invalid
symbol ...Y@" = double 100.0 = config.expense_pool_percent). Любой action,
читающий global_state, падал.
Фикс: оба поля перенесены в ХВОСТ struct (после config) и обёрнуты в
eosio::binary_extension<asset>. Старая запись прода читается без изменений
(хвостовые extension опциональны → пусто = 0), новая логика программных
расходов сохранена. Поля в таблицу EOSIO дописываются только в конец и только
binary_extension'ом.
utility-функции State:: (topup/reserve/release/consume/spend) переведены на
optional-семантику через ext_or_zero(); инвариант — оба поля материализуются
вместе, чтобы порядок хвостовых extension был консистентным. Прямого доступа к
полям вне State:: нет. Собрано в CDT-докере: capital.wasm слинкован, в ABI
program_expense_* → asset$ после config.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
По решению пользователя заход долей Благороста — только в активные проекты.
В pending больше не заводим: при переходе pending→active придёт новая дельта
capital::projects со status=active, на ней листенер и зарегистрирует.
- Листенер ProgramShareRegistrationOnProjectDeltaListener: гейт pending|active → только active.
- ProgramShareRegistrationService.findActiveProjects (путь крона и wallet-листенера): фильтр pending|active → только active.
- Тест: pending теперь НЕ реагирует; active — позитивный кейс.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Дефолт promote.sh — текущий HEAD чекаута; второй аргумент по-прежнему задаёт явный
ref (ветка/тег/sha). Типичный путь: cut на dev → promote testnet → promote main,
все три от того же HEAD. testnet/main остаются независимыми FF-указателями.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ссылка из письма открывается часто без сессии кабинета (другой браузер/инкогнито/
после logout). Подтверждение работает по токену без входа, но wallet защищён —
гард молча редиректил на login-redirect, и пайщик не понимал, что произошло.
- есть сессия → 'Перейти в кабинет' → wallet (глобальный ExitOverlay сам покажет
'на рассмотрении Совета')
- нет сессии → 'Войти в кабинет' → signin + пояснение, что войти нужно для слежения
за статусом и суммой возврата
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Доли пайщиков Благороста регистрировались в проект только периодическим
scheduler'ом (regshare) и точечным листенером на дельту кошелька. Новый
проект баланс не меняет → wallet-листенер молчит, и наполнение нового
проекта зависело ТОЛЬКО от крона. Если компонент быстро прогнали
pending→…→result до тика крона, пайщики в него не попадали, а откат
result→active контракт не допускает — доли в компоненте терялись
безвозвратно (voskhod, компонент 011bcd92…, 2026-06-16).
- Новый ProgramShareRegistrationOnProjectDeltaListener на
delta::capital::projects: при появлении проекта в статусе pending|active
сразу регистрирует доли всех активных пайщиков (неблокирующе, fire-and-forget,
переиспользуя ядро syncContributor — тот же путь, что и крон).
- ProgramShareRegistrationService.syncProgramSharesForProject — обход пайщиков
по одному проекту; syncContributor переведён с ProjectDomainEntity[] на
projectHashes: string[].
- Крон ОСТАВЛЕН как reconciliation-бэкстоп: он ещё и ДОобновляет уже
зарегистрированные доли при дрейфе баланса (контракт upsert_contributor_segment
это допускает) и подбирает события, потерянные при downtime контроллера.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Паровоз dev→testnet→main заменён независимыми указателями: каждое окружение —
самостоятельный fast-forward-указатель на выбранный релизный тег dev. Прод и тест
катятся разным темпом и на разные версии одновременно, без диверджа и конфликтов
(оба — FF-указатели на линейную историю dev).
- promote.sh <testnet|main> [ref]: FF выбранного тега (по умолчанию — последний на
origin/dev) на ветку окружения; main больше не зависит от testnet; только вперёд.
- release.yaml: workflow_dispatch получил inputs environment+ref — откат на старую
версию / редеплой без бампа / hotfix в один контур (то, что FF не умеет). Сборка
идёт из вычекнутого ref, окружение — из inputs.environment. Авто-триггер по
изменению lerna.json сохранён.
- RELEASE.md: модель и инварианты переписаны под независимые указатели.
Принципы деплоя сохранены: бамп версии один раз на dev, образы по версии,
BUILD_MODE по окружению, webhooks per env, npm publish/доки только на main.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- заголовок документа из DocumentHtmlReader (3rem) прижат к h2 по канону,
:deep + !important как в ReadStatement.vue
- 'Сумма к возврату' оформлена soft-панелью (label+hint слева, mono-значение
справа) вместо висящего снизу текста
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Суммы к возврату форматируются канонной formatAsset2Digits ("300.0000 RUB"
→ "300,00 RUB") в диалоге заявления и в оверлее — вместо сырого asset.
- Убрана разбивка "целевой + минимальный" (оба паевые, суммируются): показываем
одну строку "Сумма к возврату".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Было две крутилки рядом («Формирование заявления…» + «Расчёт суммы…»).
Теперь заявление и сумма грузятся параллельно (Promise.allSettled) под одним
центрированным лоадером и показываются вместе. Тот же стиль лоадера — на фазе
проверки реквизитов. Сумма некритична: документ подписывается и без предрасчёта.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Пункт меню «Выход из кооператива» перенесён в самый низ (после «Поддержки»),
иконка group_remove вместо logout — чтобы не сливаться с кнопкой «Выйти»
(выход из кабинета, дверь-logout) внизу панели.
- Гейт реквизитов сделан fail-open: блокируем выход ТОЛЬКО при достоверно
пустом списке методов; null/непонятный ответ не блокирует. + реактивный
фолбэк: если бэкенд отклонил подачу из-за отсутствия реквизитов — диалог
переключается на баннер. Авторитетный источник — бэкенд (createMembershipExit).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Счёт регистрационного взноса создаётся со статусом pending сразу при заходе на
шаг оплаты («счёт выставлен», деньги ещё не получены). При возврате на страницу
(перезагрузка/повторный вход) роутинг считал сам факт наличия платежа за «этап
оплаты пройден» и вёл на экран ожидания, где дефолтная ветка показывала «Ваш
платеж принят» — хотя оплаты не было и статус не менялся.
- SignUp: на экран ожидания/отказа ведём только при PAID/COMPLETED либо
терминальном статусе (отказ/отмена/возврат); pending → шаг оплаты (QR + поллинг).
- WaitingRegistration: для pending — отдельная ветка «Ожидаем поступление оплаты»
+ кнопка «Перейти к оплате»; «платёж принят» остаётся только для PAID/COMPLETED.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- ExitOverlay: чип статуса исходящего платежа у суммы (ожидает оплаты/оплачивается/
оплачено/ошибка) — берётся из payment_status, меняется по мере обработки кассиром.
- MembershipExitPage переверстана на AuthCard (иконка-предупреждение + заголовок +
ключевые шаги процедуры + запуск подачи) — единый канон с оверлеем.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
getMembershipExit подтягивает платёж возврата по hash=exit_hash и отдаёт его
payment_status (PaymentStatus enum, nullable). Фронт показывает реальный статус
платежа кассира (ожидает оплаты → оплачено), а не только статус процесса выхода.
Сумма к возврату — зафиксированная советом on-chain (= сумма платежа), не preview.
+ codegen (controller/sdk zeus, селектор MembershipExit).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Контент больше не плавает по пустому экрану: статус выхода — в центрированной
карточке AuthCard (accent-стрип + мягкая тень, канонный hero-контейнер).
Иконка статуса в soft-плитке (primary/pos), заголовок, пояснение, сумма к
возврату на surface-2, действия в футере с разделителем. Всё на токенах --p-*.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- ExitOverlay переверстан на канонный EmptyState (иконка-плитка + заголовок +
приглушённый текст + слот действий) вместо россыпи text-h5/64px-иконки.
- Добавлена кнопка «Выйти из личного кабинета» (logout) на оба экрана выхода:
при активном выходе аккаунт заблокирован, и выйти из сессии было нечем.
После logout gate обнуляется, редирект на signin; при повторном входе экран
выхода снова показывается (статус живёт on-chain).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Перед подписанием заявления о выходе проверяем наличие реквизитов пайщика
(getPaymentMethods). Если их нет — вместо формы показываем баннер «установите
реквизиты для получения возврата паевого взноса» + кнопку перехода на страницу
реквизитов (payment-methods). Бэкенд проверяет то же при подаче (страховка).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Закрываем пробел: при одобрении советом выхода (on-chain confirmexit) кассир
не видел платёж возврата — его никто не создавал в реестре gateway.
- Гейт реквизитов: createMembershipExit отклоняет подачу, если у пайщика нет
ни одного платёжного метода («установите реквизиты для возврата паевого»).
- MembershipExitAuthorizationListener (@OnEvent action::registrator::confirmexit):
по exit_hash берёт из таблицы exits username + сумму возврата, заводит
исходящий платёж MEMBERSHIP_EXIT (PENDING — совет одобрил, сразу кассиру) по
реквизитам пайщика (метод по умолчанию). hash платежа = exit_hash, поэтому
подтверждение кассой через default-ветку processOutgoingPayment вызовет
completeOutcome → registrator::completexit (списание + блокировка).
- getExitByHash в account-порт/адаптер: confirmexit отдаёт только coopname+exit_hash.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
После merge dev zeus-клиенты были взяты из dev (без операций выхода).
Интроспекция живого coopback → schema.gql → generate-client вернула обе
группы операций (createMembershipExit/confirm/cancel + expense-шасси).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Регенерация Zeus-клиента контроллера под GraphQL-операции выхода
(CreateMembershipExitInput, MembershipExitStatus, signed-document inputs).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Маршрут membership-exit/confirm авторизуется токеном из ссылки (мутация
confirmMembershipExit публичная), поэтому навигационный гард не должен
редиректить его на login-redirect. requiresAuth:false исключает страницу из
auth-гейта — ссылку из письма можно открыть без активной сессии.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Черновик заявления сохраняется до отправки письма; если провайдер писем
недоступен, приём заявления не падает (try/catch + error-лог), пайщик может
отменить. Ссылка подтверждения пишется в debug-лог для диагностики.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- ExitButton: диалог теперь показывает сгенерированное заявление (DocumentHtmlReader)
с предупреждением «читайте внимательно, необратимо» + сумма к возврату, затем
«Подписать и подать» (generateApplication → показ → submitSignedApplication).
- ExitOverlay: новое состояние AWAITING_CONFIRMATION — «пройдите по ссылке из письма»
+ кнопка «Отменить выход»; после перехода/отмены overlay переключается на
ончейн-статус (рассмотрение Совета → одобрено → выплаты).
- useExitGate: isAwaitingConfirmation + cancelExit (Mutations.MembershipExit.CancelMembershipExit).
- model: generateApplication / submitSignedApplication / confirmExit вместо единого processMembershipExit.
- MembershipExitConfirmPage + маршрут membership-exit/confirm (цель ссылки из письма):
onMounted confirmExit(token) → loadExitStatus.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Двойное подтверждение необратимого выхода (закрытия аккаунта): заявление
подписывается и принимается сразу, но в блокчейн уходит только после перехода
по ссылке из письма (по аналогии с verify-email/reset-key токенами).
- notifications: воркфлоу MembershipExitConfirmation (письмо со ссылкой confirmationUrl).
- token: тип CONFIRM_EXIT + generateConfirmExitToken (домен + application).
- entity membership_exit_requests (off-chain черновик подписанного заявления; uniq coopname+username; удаляется при confirm/cancel).
- MembershipExitStatus += AWAITING_CONFIRMATION; MembershipExitResult += status.
- service: createMembershipExit теперь сохраняет черновик + шлёт письмо (НЕ цепь);
confirmMembershipExit(token) проверяет токен и шлёт exitcoop в цепь;
cancelMembershipExit удаляет черновик+токен; getMembershipExit отдаёт off-chain фазу.
- resolver: + confirmMembershipExit (публичная, по токену) + cancelMembershipExit (auth+владелец).
Проверено вживую: мутации в схеме, enum/Result обновлены, таблица создана (synchronize).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Заявление о выходе из кооператива подписывается цифровой подписью (signatures[]),
поэтому из документа убрано поле собственноручной подписи (рукописная картинка):
- cooptypes 200: убраны Action.signature/Model.signature, <img src="{{ signature }}">,
signature из exampleData;
- factory Actions/200: убрана логика поиска/сохранения подписи в mongo;
Templates/200: убрано свойство signature из AJV-схемы;
- controller DTO: убрано поле signature (@IsString) из меты заявления.
Это и вызывало "signature must be a string" — ValidationPipe (422, не логируется)
рубил генерацию, т.к. фронт картинку не передаёт.
Также откат самодуманной шапки "ФОРМА УТВЕРЖДЕНА решением Собрания Совета (Протокол №)":
исходная форма заявления её не содержит. Убраны vars.participant_exit_application
(cooptypes IVars + factory VarsSchema + фикстура теста), проверка протокола в фабрике
и переводы APPROVED/approved_by_council/protocol.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Контроллерный tsc --noEmit (CI) падал: spec тестировал adapter.authExp /
adapter.declineExp, которых нет в ExpensesBlockchainAdapter по дизайну.
authexp/declexp исполняет контракт soviet как callbacks решения совета —
backend-адаптер несёт только 6 прямых actions (createexp/payexp/reportexp/
returnexp/overspendexp/closeexp). Зафиксировано в port, mutations-service и
комментарии адаптера. Прав код — приводим тест к дизайну, удаляя два кейса.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CI впервые дошёл до vue-tsc по ветке и вскрыл накопленный долг типов
(локально vue-tsc не гоняем). Чиним точечно, без смены логики:
- uploaded_at — Zeus-скаляр даты типизирован как {}: new Date(String(...))
и date:String(...) в ProgramExpensePage, ExpenseDetailPage и трёх
Payment-panel (Attach/Settlement/Report).
- ExpenseDetailPage: loadPayments options требует sortOrder → 'DESC';
:key платежа допускает null → ключ pay.hash ?? idx.
- PaymentsPage: routeUsername null → undefined под prop :username.
- expenses/model: снят реэкспорт несуществующего IAuthorizeProposalDraft.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
buildZipArchive писал имена в UTF-8, но general-purpose bit flag = 0 → читатели
(включая верификатор на fflate) декодировали кириллицу как latin1 (мохибейк), и
manifest.json (корректный UTF-8) не находил записи → верификатор «в пакете нет
файлов». Ставим bit-11 в local и central заголовках. Архив теперь корректно
открывается любым ZIP-ридером (Explorer/macOS/fflate).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ExpenseDetailPage переведён со сбора платежей по реконструированным хэшам на
один запрос getPayments({ coopname, proposal_hash }) — фильтр из C28-64. Убрана
скопированная серверная формула settlementPaymentHash/sha256Hex из
shared/lib/expenses: фронт больше не дублирует деривацию хэшей расчётных
платёжек. Запрос type-agnostic — ловит все связанные платежи любого типа,
устойчив к добавлению новых видов. Сортировка: выдача/оплата раньше расчётных.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Добавлен фильтр PaymentFiltersInput.proposal_hash — возвращает все платежи,
связанные с расходом (служебной запиской). Связь платёж→расход уже зашита
бэкендом в json-поле blockchain_data.proposal_hash (и у платежа выдачи аванса/
оплаты организации, и у расчётных платёжек возврата/доплаты); фильтр извлекает
его через json-оператор ->> в typeorm-репозитории. Так связанные платежи
достаются одним запросом по расходу, без реконструкции хэшей на фронте.
regen: schema → generate-client → sdk build (zeus-клиент controller+sdk).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
В историю состояний расхода добавлены события платежей: выдача аванса/оплата
организации, возврат недорасхода, доплата перерасхода, отклонения. Берутся из тех
же linkedPayments (C28-61), без отдельного запроса. Тип/иконка/текст по статусу
(исполнен / оплачен кассой / создан / отклонён), сумма и причина отказа — в
описании; сортируются в общую ленту по дате updated_at.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Из детали расхода нельзя было вернуться в реестр расходов. Добавлен canon
back-link под шапкой (как в MeetDetails/DocumentDetails): router.push на
expenses-registry. Виден всегда, даже пока расход грузится.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
В детали расхода (стол совета → реестр расходов → расход) добавлена секция
«Платежи по расходу»: выдача аванса/оплата организации и, если был перерасчёт по
чекам, расчётная платёжка (возврат недорасхода / доплата перерасхода). Реквизиты
«куда уходил платёж», назначение и причина отклонения — внутри PaymentDetails.
Платежи собираются по ДЕТЕРМИНИРОВАННЫМ хэшам, а не по username: у позиции-
организации платёж принадлежит кооперативу, у аванса — пайщику-получателю, и у
каждой позиции свой получатель — единого владельца нет. Хэш выдачи = item_hash,
расчётные = sha256('expense-settlement:coop:item:kind') (новый settlementPaymentHash
в shared/lib/expenses, точная копия серверного generateHashFromString). Точечный
hash-фильтр gateway не требует листать общий реестр и нового бэкенд-поля.
Клик «Открыть в реестре платежей» → реестр кассира, отфильтрованный по владельцу
(:username? в PaymentsPage), с фокусом на платёж (виджет раскрывает его по ?focus=hash).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Кассир пишет причину в диалоге «Отклонить», бэкенд сохраняет её в payment.message
и ставит статус CANCELLED — но PaymentDetails показывал message только при FAILED,
поэтому причина нигде не отображалась. Добавлен canon-баннер (.banner--neg) с
причиной для CANCELLED: видно сразу при раскрытии платежа. Компонент общий —
работает и на столе совета, и на столе пайщика.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
В блоке «Основание расчёта» (C28-58) добавлена кнопка «Открыть платёж выдачи аванса»:
кассир жмёт → реестр раскрывает исходный платёж-аванс (его hash == item_hash расчётной
платёжки) и прокручивает к нему. Видно, сколько выдавалось и что в чеке, без поиска
строки того же пайщика вручную. id навешены на tr/pay-card для scrollIntoView; если
платёж аванса не на текущей странице — подсказка.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Кассир (и пайщик) при раскрытии расчётной платёжки (возврат недорасхода / доплата
перерасхода) видит основание прямо в строке — без поиска исходного аванса среди
сотен строк реестра:
- ссылка на СЗ (№), что оплачивали, выдано авансом, заявлено по чекам, сумма расчёта
- список подтверждающих документов (чеки REPORT_FILE) со ссылками на открытие
Новый feature ExpenseSettlementBasis (self-contained: грузит позицию СЗ + файлы по
proposal_hash/item_hash из blockchain_data платёжки). Подключён в ListOfPaymentsWidget
для EXPENSE_RETURN/EXPENSE_OVERSPEND на обоих столах (desktop+mobile).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Корень: позиция on-chain остаётся PAID до подтверждения расчётной платёжки кассой
(reportexp отложен), поэтому отчёт можно было подать многократно на разные суммы.
Идемпотентность createSettlementPayment — лишь по виду (возврат/доплата), из-за чего
отчёт 400→200 плодил две платёжки, а 500 молча возвращал старую.
Backend (авторитетный фикс):
- reportExpenseItem: guard по report_state платежа аванса — SETTLEMENT_PENDING/CLOSED
→ BadRequestException «Отчёт по этой позиции уже подан»
- markAdvanceReportState сохраняет reported_amount (заявленный факт) рядом со статусом
- тест на guard + обновлён ассерт SETTLEMENT_PENDING
Frontend:
- ReportExpenseAdvancePanel: новые props report-state/reported-amount; форма
(AmountInput+кнопка) доступна только пока отчёт не подан (canReport); после подачи —
«Отчёт подан на N ₽, ждём расчёт», остаётся лишь загрузка доп.документов
- ListOfPaymentsWidget: прокидывает report_state/reported_amount в панели на обоих столах
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- ReportExpenseAdvancePanel: AmountInput в .report-advance__amount (max-width 240px),
чтобы значение не висело во всю ширину панели с разрывом «лейбл … сумма»
- memo возврата: «выданных под аванс под отчёт» → «выданных авансом под отчёт»
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Направление платежа (ListOfPaymentsWidget):
- колонка «Направление» снова показывается на ОБОИХ столах;
- on-chain direction всегда относительно кооператива (INCOMING = в кооператив).
Стол совета (!hideActions) так и видит; на личном столе пайщика (hideActions)
перспектива обратная — direction инвертируется (исходящий из кооператива =
поступление пайщику). Хелперы displayDirection/directionLabel/directionHint,
tooltip с пояснением («В кооператив»/«Из кооператива» vs «Поступление вам»/
«Списание с вас»). colspan/skeleton/min-width поправлены под доп. колонку.
Уведомления о платеже — без привязки к типу:
- workflow payment-refunded (любой исходящий PAID пайщику) больше не «Возврат
взноса выполнен», а универсальное «Платёж выполнен» (тем же каналом идут
аванс под отчёт, доплата по перерасходу и пр. — тип платежа неизвестен).
payment-paid уже был универсальным («Платёж принят» для входящего).
- комментарий в payment-notification.service приведён в соответствие.
ESLint ✓, notifications build ✓ (id platyozh-vypolnen резолвится в каталоге).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Q1 (расхождение 900 vs 50): на странице программных расходов «Доступно»
теперь = остаток кошелька w.cap.pgexp (source_wallet, с которого payexp
реально списывает оплату), а не счётчик state.program_expense_pool. Счётчик
двигался на резерве, кошелёк — на оплате, поэтому кассир видел одну сумму, а
оплата обламывалась на другой. Карточка «Зарезервировано» (счётчик) убрана,
чтобы не смешивать два контура учёта на одной карточке кошелька.
Q2 (нет поля для доп. документов после «Отчёт принят»): FileUploader в
ReportExpenseAdvancePanel вынесен из ветки isAwaitingReport — теперь доступен
и в состоянии REPORTED («Приложите дополнительный документ»), как и обещает
текст «дополнительные документы дополнят его автоматически».
ESLint ✓. Визуальная проверка — по скриншотам.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Диалог «Пополнение пула программных расходов»:
- убрана тёмная плашка под кнопками (footer-bar на --p-canvas) — кнопки на
поверхности диалога через штатный BaseDialog __foot;
- снят двойной паддинг (.form поверх body) и uppercase-eyebrow («прыгающие шрифты»);
- BaseInput → AmountInput с суффиксом валюты в узкой обёртке (не во всю ширину).
Реестр платежей (стол кассира), последовательная подача:
- AttachExpenseProofPanel и ReportExpenseAdvancePanel получили опциональный
проп step (номер+заголовок) — Этап 1 «Подтвердите оплату», Этап 2 «Отчёт
пайщика»; обёрнуты в .expense-flow с хайрлайн-разделителем. Пустых этапов у
DIRECT нет (панель отчёта сама скрывается);
- в отчёте пайщика порядок изменён на «сумма → чек» (сначала AmountInput, видно
недо/перерасход, затем приложить чек, потом кнопка);
- дружелюбные подписи, больше воздуха (gap --p-3/--p-4), снята плотность.
ESLint ✓. Визуальная проверка — по скриншотам пользователя.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Реквизиты получателя, имя и назначение платежа больше не уходят в blockchain
meta документа. Перестроено на паттерн doc_data из marketplace2 (epic5):
приватный payload сохраняется off-chain в DocDataService фабрики, в meta едет
только doc_data_hash; при генерации/регенерации фабрика подмешивает приватную
часть в позиции по number — полнотекстовый документ не страдает.
2010 (СЗ-смета):
- cooptypes: IExpenseItem → публичный (number/description/amount/recipient_type/
mechanics); новые IExpensePrivateItem/PrivateData/IExpenseRenderItem; Action
extends IDocDataRef + doc_data_hash; Model.items = render-позиции.
- factory: Action подгружает PrivateData по doc_data_hash и склеивает по number;
meta строится из публичной data; ExpenseRenderItemSchema для модели рендера.
- controller: DTO расщеплён — вход генерации «богатый» (приватные поля → сервер
кладёт в doc_data), подписываемая meta = публичные позиции + doc_data_hash.
Сервис: saveDocData → Action с публичными items. Десктоп не меняется (подпись
идёт по meta, возвращённой сервером).
2011 (Решение совета): латентный канал закрыт структурно — item-DTO лишён
requisites/payment_purpose/recipient_name; шаблон протокола их и не рендерил.
Публичная ExpenseItemSchema (5 полей) для модели 2011.
cooptypes build ✓, factory build ✓, ESLint ✓, unit 11/11 ✓.
Codegen (schema/client/sdk) — отдельным шагом.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Выборочный перенос переиспользуемого паттерна doc_data из marketplace2
(исходный коммит 10158b5b33, 598-8) — БЕЗ marketplace-документов 1102/1103,
которых нет в dev (их cooptypes-реестры и controller-сервисы тянули конфликты
modify/delete и сломали бы сборку). 1102/1103 придут штатно при мерже marketplace2.
Что такое doc_data: приватные поля документа (реквизиты/ПДн) сохраняются в Mongo
(коллекция doc_private_data) через DocDataService.save(payload, registry_id) →
{hash}; on-chain в meta публикуется только doc_data_hash (sha256). При генерации
фабрика подгружает payload по хэшу и отдаёт шаблону под зарезервированной
переменной {{ doc_data.* }}. Так реквизиты в блокчейн не попадают, но документ
регенерируем и верифицируем по слепку.
Перенесено (идентично marketplace2 — будущий мерж без конфликтов):
- factory: Services/DocData + обвязка Factory/index.ts (saveDocData/getDocData) +
реэкспорт в Services/Databazor + Generator.saveDocData/getDocData в src/index.ts;
- controller: GeneratorPort/GeneratorInfrastructureService/DocumentDomainService —
проброс saveDocData/getDocData;
- cooptypes: IDocDataRef { doc_data_hash } в модели документа.
cooptypes build + factory typecheck/build зелёные. Контур: следующим шагом
перенастроить СЗ-2010 на doc_data для реквизитов (отдельной задачей).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Разводит три независимых акта вокруг аванса под отчёт, которые UI прежде смешивал:
- Платёж (кассир + платёжка PAYMENT_PROOF) — без изменений.
- Отчёт по авансу (факт + чек REPORT_FILE → reportexp) — пайщик ИЛИ кассир «за пайщика».
- Закрывающие документы организации (новый ExpenseFileKind.CLOSING_DOC) — только DIRECT.
Бэкенд:
- ExpenseReportState (AWAITING/SETTLEMENT_PENDING/CLOSED/NOT_REQUIRED) зеркалится в
payment.blockchain_data.report_state: в reportExpenseItem (CLOSED/SETTLEMENT_PENDING)
и в inter-адаптере reportItem в момент on-chain reportexp (CLOSED). AWAITING — дефолт.
- CLOSING_DOC + valuesMap-описания видов файлов; SDK regen.
Desktop:
- Личный стол: убран кассирский значок «Платёжка приложена» (чужая бухгалтерия),
добавлен бейдж статуса отчёта рядом со статусом платежа.
- Стол совета: кассир может отчитаться за пайщика (ReportExpenseAdvancePanel onBehalf,
самоскрытие для DIRECT) + отдельная секция закрывающих документов для оплат организациям.
Авторизация reportExpenseItem уже допускала совет по любой строке — путь «за пайщика»
работает без правок guard'ов. Unit 11/11, ESLint чист, SDK typecheck зелёный.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Правки по тест-прогону недо-/перерасхода:
- Суммы в реестре платежей (таблица+карточки) и инпут факта — formatAsset2Digits
(2 знака), а не сырой on-chain precision=4.
- «Направление» (входящий/исходящий — относительно кооператива) скрыто на личном
столе пайщика (hideActions): на его столе семантика обратная и путала. На столе
совета остаётся (там перспектива кооператива корректна).
- PaymentDetails: убран дамп «Данные блокчейна» (JSON); реквизиты показываются при
любом направлении (входящий возврат — реквизиты кооператива, исходящая
доплата — реквизиты пайщика); «Сумма к переводу» — 2 знака.
- Settlement-платёжка несёт payment_details с реквизитами: возврат → банк
кооператива (куда платит пайщик), доплата → снимок реквизитов пайщика (куда
платит кооператив). Назначение: «Возврат неиспользованных средств, выданных под
аванс под отчёт» / «Доплата по перерасходу аванса под отчёт».
- После отчёта реестр перезагружается и сразу раскрывает заведённую платёжку
расчёта (по settlement_payment_hash) — пайщик видит реквизиты, не догадываясь
нажать «развернуть».
Юнит-тесты расходов зелёные (11/11). GraphQL-схема не менялась — SDK regen не нужен.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Замыкаю контур факт→возврат/доплата для аванса под отчёт. Контракт уже имел
returnexp/overspendexp с двусторонними ledger2-проводками, но фронт/бэкенд их не
звали: отчёт слепо закрывал позицию на actual=аванс без сверки факта (дыра
недорасхода — пайщик мог недоотчитаться и оставить разницу).
Бэкенд-слой:
- 2 типа платежей: EXPENSE_RETURN (входящий возврат недорасхода) и
EXPENSE_OVERSPEND (исходящая доплата перерасхода) + labels + direction-списки.
- inter-порт шасси: returnItem/overspendItem/reportItem (зеркало payItem),
адаптер реализует через returnExp/overspendExp/reportExp.
- gateway-ветки: подтверждение кассиром EXPENSE_RETURN (income) → returnexp +
reportexp; EXPENSE_OVERSPEND (outcome) → overspendexp + reportexp. proposal_hash
и item_hash берутся из blockchain_data (hash платёжки уникальный, не item_hash).
- reportExpenseItem(actual_amount?): факт==аванс → reportexp сразу (CLOSED);
недорасход → входящая платёжка на |разницу| (RETURN_PENDING); перерасход →
исходящая (OVERSPEND_PENDING); reportexp отложен до подтверждения кассиром
(контракт принимает settlement только на PAID-позиции). Дельта в минорных
единицах по precision. Идемпотентность платёжки по детерминированному хэшу.
- Новый дискриминированный результат ExpenseReportResultDTO + enum
ExpenseReportOutcome. Юнит-тесты на все три исхода + идемпотентность.
Контракт expense не изменён. Авторитет на сумму факта — пайщик (по чекам).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Откат удаления из C28-44: страницы воркспейса expenses (MyAdvances/Cashier/AdminApprove) — намеренный рабочий КАРКАС будущего стола кассира, а не мёртвый код. Решено их не удалять, а сохранить: соберём стол кассира из них позже, тогда «Мои авансы» выведем туда. Сейчас воркспейс не привязан к столу → в меню их нет и ничего на них не ведёт (то, что и нужно).
— восстановлены MyAdvancesPage.vue + роут expenses-my-advances + экспорт из barrel.
— в install.ts добавлен комментарий-маркер: это каркас, не удалять, ничего на него не ведёт намеренно; рабочие ссылки — на /:coopname/user/payments.
— ссылку напоминателя об авансах НЕ откатываем: остаётся на личные «Платежи» (/user/payments), как просил пользователь — всё ведёт через страницу платежей/реестр платежей, не на каркасные страницы.
— README приведён в соответствие (каркас, а не «удалена»).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
MyAdvancesPage была смонтирована только в воркспейсе expenses, который ни к одному столу не привязан (меню столов приходит с бэкенда; setRoutes вешает пункт лишь если воркспейс есть в столе). Страница открывалась только по прямому URL — пользователь её не видел ни разу. Авансы пайщика-получателя и так видны на его личной странице «Платежи» (/:coopname/user/payments, ListOfPaymentsWidget + ReportExpenseAdvancePanel) — отдельная страница дублировала и была мёртвой.
— desktop: удалён MyAdvancesPage.vue, его роут expenses-my-advances и экспорт из barrel; импорт из install.ts.
— controller: напоминатель об авансах при нескольких авансах вёл на /expenses/my/advances → перецелен на /:coopname/user/payments (личные «Платежи»); один аванс по-прежнему ведёт на сам расход.
— notifications: комментарий схемы payload обновлён.
— README/E2E: ссылки и таблица страниц обновлены.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Симптом: пайщик создаёт расход с получателем-организацией (не он сам) — и этот исходящий платёж появляется у него в личном кабинете на странице платежей, хотя он лишь инициатор, а деньги идут не к нему.
Корень: expense-payments.listener создавал gateway-платёж по позиции расхода с username = entity.username (создатель СЗ) для org-получателя. Личный реестр платежей фильтрует по username (loadPayments({username: me})), поэтому платёж кооператива организации оказывался на столе инициатора.
Фикс: владелец org-платежа = сам КООПЕРАТИВ (entity.coopname), а не инициатор. В личных реестрах пайщиков (фильтр по username) он больше не виден; кассир видит его в общем реестре платежей кооператива (loadPayments без username). Аванс под отчёт по-прежнему принадлежит пайщику-получателю (item.recipient) — он у него и отображается. На on-chain payexp правка не влияет: проводка берёт coopname/proposal_hash/item_hash, username не участвует (см. gateway.interactor.processOutgoingPayment).
Касается только НОВЫХ платежей; уже созданные org-платежи с username=создатель не переписываются.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Карточки списка показывали сырой total_planned «1000.0000 RUB». ExpenseProposalListRow.total_planned по контракту типа — уже отформатированная строка; страница подавала сырьё. Оборачиваем в formatAsset2Digits в listRows ProgramExpensesPage (как суммы в сводке/детали).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
— ExpenseDetailPage (стол Совета): добавлена секция «История состояний» (ActivityTimeline, group-by-date) — как на странице расхода программы Благорост. Лента собирается из фактов в данных: создание СЗ, подпись заявления, утверждение/отклонение советом, приложенные документы, подача отчёта, закрытие расхода; актор — ФИО создателя. Отдельной журнальной таблицы у шасси нет.
— capital/install.ts: страница расходов программы переименована «Расходы программы» → «Расходы» (title в meta). Доступ только совету — roles ['chairman','member'] уже стояли. Попутно заменена запрещённая каноном FontAwesome-иконка fa-solid fa-receipt → Material receipt_long на обоих роутах (список + деталь).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
По уточнению UX:
- кнопка выхода убрана из шапки кошелька; заведена отдельная страница стола
пайщика «Выход из кооператива» (route membership-exit) с описанием процесса
(добровольно по заявлению; аккаунт закрывается и удаляется; паевой возвращается
в срок по Уставу кооператива — без хардкода срока) + кнопка с переподтверждением;
- глобальный gate: пока активен процесс выхода (registrator::exits через query
membershipExit), ExitOverlay (maximized, без закрытия) блокирует весь кабинет
и показывает только статус заявления (ожидание Совета / одобрено) и планируемую
сумму возврата — элегантно, по канону. Эталон — SelectBranchOverlay;
- useExitGate (статус+сумма, опрос) + watch-exit-overlay (поллинг 15с) +
монтаж в App.vue + регистрация в init-app; после подачи статус подтягивается
сразу;
- текст переподтверждения переформулирован (необратимо, запуск возврата +
решение Совета).
ESLint чисто. Type-check — в CI.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
— симптом: колонка «Пайщик» (ФИО) в реестре расходов рвалась по буквам в столбик.
— причина: глобальный канон components.css форсит `.table{min-width:0!important}`; мой `.table{min-width:…}` без !important проигрывал → колонки схлопывались уже контента и слова ломались посимвольно. Плюс `overflow-wrap: anywhere` на ячейках добивал.
— фикс (эталон — ListOfPaymentsWidget): `.table{table-layout:fixed!important; min-width:…!important}` + `overflow-wrap: break-word` (не anywhere) на текстовых ячейках. При нехватке ширины таблица скроллится в .table-scroll, а не ломает слова. Применено и к таблице позиций в детали (тот же латентный баг).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- feature Membership/ExitFromCoop: кнопка «Выход из кооператива» (BaseButton
danger) на WalletPage в шапке рядом с возвратом паевого; диалог-предупреждение
(BaseDialog+Form+BaseBanner) с предрасчётом суммы возврата
(MembershipExitReturnPreview) и текстом «паевой будет возвращён, аккаунт
заблокирован, возврат невозможен»; submit генерит заявление (200), подписывает
и подаёт createMembershipExit (push exitcoop). Канон-компоненты, Material-иконки;
- feature Membership/GenerateMembershipExitDecision: генерация решения совета (201);
- process-decisions: handler `leavecoop` в реестре повесток — Совет генерирует
решение по выходу так же, как по вступлению/возврату.
ESLint чисто. Type-check — в CI (полный vue-tsc локально не гоняем).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
По скриншот-ревью пользователя.
Реестр расходов (ExpensesRegistryPage):
— убрана шапка PageHead («Шасси расходов» / «Реестр расходов» / подзаголовок) — сразу таблица.
— колонка «Пайщик»: ФИО первой строкой (из сертификата подписанта СЗ — её подписывает создатель, доп. запрос не нужен), ниже имя аккаунта мелким моно с копированием по клику (copyToClipboard).
— добавлена колонка-шеврон справа (chevron_right) — видно, что строка открывается; подсвечивается на hover.
Деталь расхода (ExpenseDetailPage):
— убрана шапка PageHead; статус — чипом вверху справа (канон detail-страниц). Переверстана одноколоночно секциями с заголовками-эйбрау (Сводка / Документы / Строки расходов / Чеки и подтверждения) — как на странице расхода программы.
— Сводка: «Пайщик» = ФИО (из сертификата подписанта), отдельная строка «Аккаунт» (моно, копируемая); «Хеш» (рус. ярлык вместо «Hash») переносится и копируется (DataRow mono, vertical).
— «Строки расходов»: счётчик с правильным русским множественным («1 строка / 2 строки / 5 строк») вместо «1 строк».
— «Чеки и подтверждения»: имя файла — гиперссылка, клик открывает документ в новой вкладке (read_url короткоживущий — запрашиваем свежий по id через новый getExpenseFileReadUrl, как в capital); кто приложил — ФИО (из карты подписей СЗ+протокола), а не имя аккаунта; убраны storage_key/размер.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- interfaces/registrator: IExitcoop/IConfirmexit/ICompletexit/IDeclinexit + IExit
(таблица exits) — синхронно с обновлённым ABI registrator;
- interfaces/soviet: IDelpartcpnt;
- action-зеркало registrator ExitCoop (член-инициируемое действие, как RegisterUser);
внутренние коллбэки confirmexit/completexit/declinexit конструируются каскадом
из soviet/gateway и в зеркалах не нуждаются (как confirmreg/addpartcpnt);
- table-зеркало registrator Exits (scope=coopname) для чтения статуса выхода и
суммы возврата на бэкенде/фронте.
tsc --noEmit cooptypes — чисто.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Жизненный цикл выхода — зеркало вступления (reguser→confirmreg) и возврата
паевого (wallet::createwthd→authwthd→completewthd):
- registrator::exitcoop — пайщик подаёт заявление о выходе (registry 200),
создаётся реестр exits (status=pending) и повестка совета `leavecoop`;
- registrator::confirmexit — совет одобрил: контракт сам считает сумму возврата
по L3-балансам ledger2 (мин. + целевой паевой), консолидирует минимальный на
главный (o.reg.mvmin), резервирует сумму (o.wal.wthreq) и шлёт исходящий
платёж в gateway; нулевой паевой → финализация без платежа;
- registrator::completexit — кассир подтвердил выплату: проводка Дт80/Кт51
(o.wal.wthcpl), пайщик удаляется (soviet::delpartcpnt), аккаунт блокируется;
- registrator::declinexit — отказ совета или платежа: снятие резерва
(o.wal.wthdec, если был), пайщик остаётся в кооперативе.
Новое: таблица registrator::exits, soviet_action `leavecoop`, soviet::delpartcpnt
(стирание пайщика, зеркало addpartcpnt; уменьшает счётчик активных). Новых
ledger2-кодов не вводилось — переиспользованы o.wal.wthreq/wthcpl/wthdec и ранее
добавленный o.reg.mvmin. registrator уже в contracts_whitelist → вправе применять
wallet-операции. Обе сборки (soviet/registrator) проходят в docker.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
— зачем: на столе совета «Расходы» показывал карточки кошельков-пулов и проваливался с редиректом на страницу программы Благорост. Нужен реестр-наблюдение: единая таблица ВСЕХ расходов кооператива по всем пулам без фильтра, с колонкой кошелька-источника; клик по строке → деталь расхода (та же информация, что на странице расхода). Фильтр по конкретному пулу остаётся на странице расходов программы.
— soviet/install.ts: монтирует ExpensesRegistryPage (title «Реестр расходов», маршрут soviet-expenses-registry) вместо ExpenseWalletsPage; редиректа на программу больше нет. Воркспейс expenses активен → generic-роут expenses-detail резолвится глобально.
— ExpensesRegistryPage: + колонки «Назначение» (описание первой позиции) и «Кошелёк (пул)» (резолв кода source_wallet → человеческое имя через listExpenseWallets(), fallback — сам код); суммы план/факт через formatAsset2Digits (2 знака); убрана колонка «Хеш» (шум, есть в детали).
— ExpenseDetailPage (generic, куда ведёт реестр): поднят до канона страницы расхода — документы СЗ/протокол рендерятся каноном ExpenseProposalDocuments (раскрывающийся BaseDocument с подписями) вместо голых хешей; суммы сводки и позиций через formatAsset2Digits; источник средств показан как имя пула; убрано техническое поле «Действие» (operation_code).
— ExpenseWalletsPage остаётся в barrel'е (не смонтирован); listExpenseWallets/registerExpenseWallet теперь служат картой код-кошелька→имя для реестра. README обновлён.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Операция-фундамент для консолидации при выходе из кооператива: переносит
минимальный паевой (w.reg.minshr) на главный паевой кошелёк (w.wal.share)
через TRANSFER без проводки (оба кошелька на счёте 80), чтобы вернуть его
вместе с основным паевым через wallet-withdraw (o.wal.wthcpl, Дт80/Кт51).
- operations.hpp: объявление + запись OPERATION_REGISTRY (static_assert OK,
registrator скомпилирован в docker dicoop/blockchain).
- operations.ts: ручное TS-зеркало (синхронизация обеих сторон).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Паспортные данные физлица выводятся при vars.passport_request=='yes',
как в заявлении на вступление (100).
- Внизу документа «Документ подписан электронной подписью.» вместо
«личная подпись заявителя» (по правке пользователя).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Заявление о выходе из состава пайщиков (200) с условным рендером по трём
ролям (физлицо/ИП/юрлицо) — зеркало 100.ParticipantApplication, текст по
утверждённой форме. Решение совета о выходе (201) — зеркало 501.
- cooptypes: registry 200/201 (Model + context + переводы + exampleData),
поле vars.participant_exit_application для шапки «ФОРМА УТВЕРЖДЕНА».
- factory: Templates/Actions 200/201, регистрация в индексах и factories-map.
- test: генерация 200 в трёх ролях + 201; сидинг participant_exit_application.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
На карточке расхода в статусе «Создан» председатель видит кнопку «Рассмотреть» —
открывается диалог авторизации СЗ (утвердить с протоколом 2011 / отклонить с
причиной). Фиксы диалога: отклонение теперь идёт через declineExpenseReport →
declexp (раньше ошибочно через authexp, который безусловно ставил AUTHORIZED);
убраны ссылки на удалённый operation_code (назначение = перечень позиций).
Инициатор в карточке — ФИО (creator_name). Тексты карточек пулов укорочены.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
В карточке показывался username (ant) — канон требует ФИО/название организации.
Бэкенд резолвит имя через ACCOUNT_DATA_PORT.getDisplayName (батчем по уникальным
creators, при ошибке остаётся username). Codegen: schema + zeus + sdk selector.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Бегущая строка убрана полностью (раздражала и читалась плохо): заголовок и
подпись переносятся максимум на две строки с многоточием, полный текст — в
title-тултипе. Выпилены measure/ResizeObserver/tabindex и keyframes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
On-chain callback.data — vector<char>; парсер десериализует в Uint8Array, который
в jsonb-зеркале становится {} (пустой) или {"0":..}. GraphQL-поле data — String,
сериализация ответа capitalProgramExpenses падала «String cannot represent value: {}»
и UI показывал пустой список при живой строке в зеркале. Нормализуем в hex ещё
в дельта-маппере (на записи), существующая строка поправлена UPDATE'ом.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
extra_reboot (чистая цепь + совет + dev-shortcut онбординга capital — без него
после reboot снова 5 решений совета руками) → ожидание готовности controller →
seed-capital --up-to=04-contributor (программы, проекты, регистрация ant).
Глубина сида настраивается: SEED_UP_TO=08-investments pnpm run reboot:blago.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Канон-класс .wallet__locked-line — white-space:nowrap + inline-flex, рассчитан
на короткий лейбл; длинный «Зарезервировано под активные расходы» не переносился
и вылезал за карточку. Лейбл → «Зарезервировано» (смысл ясен из контекста
страницы), общий компонент не трогаем.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
global_sequence — глобально-уникальный монотонный id действия в истории цепи.
При re-scan/replay redis-стрима то же действие доставляется повторно → INSERT
падал на unique-индексе (23505) → consumer не ACK'ал сообщение и зацикливал
recoverOwnPending/reclaimStalePending (дикий спам duplicate-key в логах). Теперь
дубль трактуется как «уже сохранено»: возвращаем существующую запись, не бросаем.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Парсер не был подписан на контракт `expense` (нет в subscribedContracts) →
дельты `expense::proposals` не эмитились → postgres-зеркало `expense_proposals`
оставалось пустым → список «Программных расходов пока нет», хотя на цепи СЗ и
резерв создавались. Добавил `expense` в subscribedContracts парсера.
Плюс: `coopname` добавлен явным полем в таблицу `proposals` контракта (struct +
EOSLIB_SERIALIZE + проставление в createexp) и в cooptypes IProposal. Таблица
scoped по coopname, но зеркало требует coopname в строке — дублируем полем по
канону (как capital/soviet), а не выводим из scope дельты.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- expense::createexp принимает контракты-инициаторы по contracts_whitelist
(capital@eosio.code не имеет coopname@active — inline падал по authority);
capital::createpgexp шлёт inline от _capital@active.
- capital::onpgexpdone: require_auth(_expense) вместо _capital — callback
шасси шёл с authority expense@active и всегда падал.
- payexp: cap actual<=plan + DIRECT-item сразу REPORTED с пересчётом статуса
proposal — DIRECT-only СЗ навсегда зависал в PARTIALLY_PAID.
- returnexp/overspendexp: settlement-семантика по PRD — статус item остаётся
PAID, отчёт закрывает item штатным reportexp (раньше RETURNED/OVERSPENT
были терминальными тупиками: перерасход блокировал closeexp навсегда).
- declexp: только CREATED/AUTHORIZED — decline после оплат разъезжался
с учётом пула в capital (возвращал весь резерв при ушедших деньгах).
- onpgexpdone CLOSED: перерасход сверх резерва списывается из
program_expense_pool (spend_program_expense_pool).
- names.hpp: redefinition CREATE_PROGRAM_EXPENSE + несуществующий
Names::Capital::Callbacks — контракты не компилировались вовсе.
- verify_document_or_fail(statement, {creator}) в createpgexp; снос
мёртвых set_program_approved/set_program_authorized.
Оба контракта собраны: expense.wasm + capital.wasm.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Найдена реальная причина «белого экрана» на первом SSR-заходе (доказано
DOM-диагностикой + __INITIAL_STATE__ прода: 87 сериализованных "component"
в desktops.workspaces[].routes):
SSR-сервер кладёт RouteRecordRaw вместе с component в Pinia-стор; Quasar
сериализует стейт в __INITIAL_STATE__; Vue-компонент не переживает JSON
(render-функция выпадает). Клиент гидратируется мёртвыми маршрутами,
registerWorkspaceMenus регистрирует их в router (routes=108 сразу),
initExtensions живые не добавляет («маршрут уже есть») → каждая страница
рендерится пустой при работающем layout/меню. F5 «лечит», потому что SW
отдаёт SPA-shell без гидратации (Config.js-путь, routes=13 → живые 108).
Локально не воспроизводилось: dev-режим SPA, без SSR.
Фикс: на клиенте перед loadDesktop зачищаем гидратированные routes из
workspaces — живые добавит useInitExtensionsProcess, ровно как при
SPA-заходе. Диагностика [BOOTRACE] остаётся до подтверждения на проде.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Переходы из установленного PWA на сайт кооператива (vars.website из
публичного getSystemInfo, кэш 5 мин) остаются в окне приложения вместо
выброса в браузер. Заодно явные id и scope для стабильной identity.
Работает в Chrome/Edge 138+ на десктопе; сайт должен отдать
/.well-known/web-app-origin-association с {"https://<домен-лк>/": {"scope": "/"}}.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Глобальный канон .table{min-width:0!important} снимал локальный min-width
таблицы — колонки сжимались уже контента и кнопки «Подтвердить/Отклонить»
ложились поверх бейджа статуса. Возвращён min-width (860/980px) с
!important: при нехватке ширины таблица скроллится в .table-scroll, как
журнал уведомлений, а не схлопывает колонки.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
padding/margin на .q-table__grid-item virtual-scroll игнорирует, поэтому
зазор не появлялся. Отступ повешен на саму .participant-card (margin-bottom
внутри grid-item) — его virtual-scroll учитывает в измеряемой высоте.
Grid-item padding возвращён в 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Отступ между карточками: margin-bottom игнорился virtual-scroll, а
padding:0 убрал дефолтный зазор. Зазор задан через padding grid-item
(входит в измеряемую высоту элемента virtual-scroll).
- Наезд бейджа: длинный бейдж («Ожидает решения совета») переполнял свой
контейнер и налезал на дату/удаление. Мета-строка переведена на flex-wrap
— дата+удаление переносятся на след. строку, когда бейдж не помещается.
- Аккаунт: иконка badge с подсказкой «Имя аккаунта» в начале + кнопка
копирования username. Email: иконка mail в начале. Текст с ellipsis.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- ListOfParticipantsPage: на <768px (grid-режим карточек) убрана внешняя
обрамлённая поверхность .participants-page__card — карточки уже сами в
рамках, обёртка давала «подложку»/двойное обрамление. На десктопе рамка
остаётся (там таблица).
- ParticipantCard: широкий бейдж статуса («Ожидает решения совета») сжимал
имя/аккаунт/email до пары букв. Перестроено: идентификация на всю ширину
в верхней строке, бейдж + дата + удаление — отдельной строкой снизу.
- ParticipantsTable: на мобиле карточки во всю ширину с понятным
вертикальным отступом (убран дефолтный 4px-padding grid-item, задан gap).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Релизный флоу переведён на линейную fast-forward модель — устраняет
регулярные конфликты на 20 package.json при релизе.
Корень проблемы: publish-alpha.sh/publish-prod.sh бампали версию НА КАЖДОЙ
ветке (alpha-N на testnet, чистую на main) через `git merge -X theirs` +
back-merge. Два независимых bump-коммита за цикл + merge'и плодили
расхождение веток → конфликты (особенно при гонке push/pull).
Новая модель:
- Версию бампает lerna ОДИН раз на dev (scripts/cut-release.sh).
- Тот же коммит едет вверх по FF: scripts/promote.sh testnet|main
(server-side fast-forward push, рабочее дерево не трогается).
- testnet/main не несут своих коммитов → ветки не диверджатся → конфликты
структурно невозможны.
release.yaml:
- Триггер: push в testnet/main с изменением lerna.json (вместо тега v*).
Окружение определяет ВЕТКА (main→prod, testnet→staging), не суффикс -alpha.
- Версия читается из закоммиченного lerna.json (едет с коммитом по FF).
- Гейты npm-publish/доки: branch == main (вместо !contains '-alpha').
- Образы/webhook по-прежнему версия-тегированы → playbooks/приёмник деплоя
не затрагиваются.
Удалены publish-alpha.sh/publish-prod.sh и мёртвые дубли тех же merge-X-theirs
скриптов (root sync:main, components/contracts production/testnet/docs-publish).
Документация — scripts/RELEASE.md + CLAUDE.md PR-flow.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Eyebrow + название кооператива висели отдельным блоком над карточкой и
выглядели оторванно. Перенесены в head карточки (отделены линией от сетки
реквизитов); размер h1→h2 под карточный контекст; field-значения
overflow-wrap anywhere→break-word (телефон не рвётся посреди цифр).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Карточка «Минимальный неснижаемый остаток» рисовалась вручную сырыми
canon-классами .wallet в обход WalletCard, поэтому marquee на неё не
распространялся (бежали только программные кошельки). Переведена на
<WalletCard neutral> — DRY + бегущая строка заголовка/подписи бесплатно.
WalletCard: program стал опциональным, добавлен neutral-вариант подсветки
иконки (--p-canvas-2/--p-ink-2) вместо акцента программы.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Журнал уведомлений: на телефоне вместо горизонтального скролла/посимвольного
переноса — компактные карточки, поля по два в ряд (col-xs-6); таблица скрыта
на ≤599px (скрытие через двойной селектор .nj-cards.nj-mobile).
- WalletCard: заголовок и подпись при переполнении становятся «бегущей строкой»
(marquee) вместо обрезки «…» — JS-детект overflow + CSS-анимация, обе строки
бегут одинаково (одна длительность, синхронные паузы). Уважает
prefers-reduced-motion.
- Реквизиты (PaymentMethods): кнопка удаления переведена на канон — icon-only
BaseButton (delete_outline, danger), без текста «удалить»; убраны лишние
fallthrough-атрибуты flat/color.
- BaseCard head: align-items center → flex-start, чтобы угловое действие при
2-строчном заголовке не проваливалось к центру.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Реальная причина белого экрана (предыдущий коммит чинил не то — гонка
чанков по логам не подтвердилась, router.onError не срабатывал):
Вход в той же вкладке (incognito-логин или выход→вход без F5) НЕ
переинициализирует приложение. LoginForm после login() звал только
selectDefaultWorkspace(true) + goToDefaultPage(), но НЕ loadDesktop().
Поэтому currentDesktop оставался АНОНИМНЫМ (загруженным до входа), целевой
стол (chairman/connect) резолвился, но рендерился пустым — данные/гранты
DesktopWorkspace были анонимные. Ручной reload делал init начисто как
авторизованный → стол подгружался → всё рисовалось.
Фикс: добавить `await desktops.loadDesktop()` в LoginForm после ожидания
loadComplete, перед навигацией — зеркало проверенного паттерна в
SignUp.vue / init-app / EnableButton (там стол перезагружают, вход забыли).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Гонка на первом заходе в проде: ленивый чанк маршрута падал на import()
из-за конкуренции за сеть с фоновым precache только что установленного SW,
а перехватить ошибку было некому → пустой router-view до ручной перезагрузки.
- FIX-1: router.onError ловит провал загрузки чанка и делает авто-reload на
целевой путь (с защитой от reload-цикла через sessionStorage).
- FIX-2: регистрацию Service Worker откладываем до window 'load', чтобы
precache-шторм не конкурировал с первой отрисовкой и догрузкой чанков.
- Диагностика: таймстемп-логи [BOOTRACE] по всей boot-цепочке (initApp,
router beforeEach/afterEach/onError, App.onMounted/isLoaded, SW register,
safety-timeout) для грепа порядка инициализации на проде.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Старые клиенты селектят kpp на BankAccountDetails → GraphQL-валидация
падала (Cannot query field "kpp"). Добавлено nullable-поле в DTO + во все
места Zeus-селектора SDK (иначе сборка десктопа падает на MakeAllFieldsRequired).
Помечено TODO «удалить после 1 августа».
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Реестр платежей: мобильные карточки (.payments-cards.pmt-mobile) показывались
и на десктопе поверх таблицы. Причина — .payments-cards{display:flex} имел ту
же специфичность, что одиночный .pmt-mobile{display:none}, и перебивал его по
порядку источника. Скрытие/показ переведены на двойной селектор
.payments-cards.pmt-mobile (специфичнее) — на десктопе только таблица.
- Удостоверение/DataRow: горизонтальная пара label|value на телефоне (≤599px)
стекается в одну колонку — длинный публичный ключ занимает всю ширину
карточки вместо узких ~150px и больше не рвётся в столбик по буквам.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Корень «push отправлен, но не виден»: workboxMode GenerateSW игнорирует
sourceFiles.serviceWorker, поэтому push/notificationclick из
custom-service-worker.ts в прод не попадали — браузер push получал, но SW его
не показывал (бэкенд честно «sent to N subscription(s)»). Доказано: прод
/service-worker.js содержит 0 push-обработчиков.
- desktop: public/push-sw.js (push → showNotification, notificationclick →
focus/openWindow) + importScripts('push-sw.js') в extendGenerateSWOptions
- controller: «Доставлено»/«Канал пропущен» debug→info + сводка
«web-push → user: N подписок» — чтобы доставка была видна в логах без БД
Корень: overflow-wrap:anywhere рвал имена/email по буквам в столбик, а
бейджи статуса воровали ширину у имени рядом.
- BaseBadge: white-space:nowrap + flex-shrink:0 — бейдж не сжимается/не
переносится (фикс глобально для всех экранов).
- components.css мобильный .table: anywhere → break-word (перенос по словам).
- Реестр платежей (pay-card): имя обрезается «…», сумма flex-shrink:0,
тип переносится по словам.
- Реестр пайщиков (ParticipantCard): имя/аккаунт/email обрезаются «…»;
попутно FontAwesome-иконка аватара → Material person.
- Журнал уведомлений: восстановлен задуманный горизонтальный скролл
таблицы на мобиле (перебит глобальный min-width:0!important), 7 колонок
с кнопкой действия больше не схлопываются в буквы-в-столбик.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Нижний padding rail__signout 12→4px, version padding-top 4→0px,
шрифт версии 11→10px — версия идёт сразу под кнопкой, не висит крупной.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Было: q-menu прижат справа фиксированной шириной 360px → правый край резался
(«Прочитать в…», тексты заявок), снизу горизонтальный скролл. На мобиле (≤600px)
раскрываем меню во всю ширину вьюпорта (left/right 8px), панель width:100%.
- тело уведомления приходит с <br>/HTML (шарится с email/push-шаблонами), а
in-app панель рендерит как текст с переносом \n — нормализую HTML→plain в toItem
- push-strip и кнопку «Включить на устройстве» показываем только когда SW реально
активен (hasActiveServiceWorker), иначе клик умирал на «Service Worker не активен»;
в dev/без-PWA strip не рисуем (не путать с «браузер не поддерживает»)
- strip не переносит текст/кнопку на узких экранах (nowrap + flex 0 0 auto)
Корень бага «push приходит только на одно устройство»: autoSubscribe
гейтился store.isSubscribed (= у аккаунта есть подписка на ЛЮБОМ устройстве),
поэтому второе устройство никогда не подписывалось. Введён endpoint текущего
браузера + computed isThisDeviceSubscribed; autoSubscribe теперь идемпотентно
регистрирует каждое устройство. Добавлены resubscribe()/refreshDeviceState()
и строка статуса push с кнопкой «Включить/Переподписать» в виджете колокольчика.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Релей переехал в отдельный репозиторий C9S/email-relay, собирается локально
плейбуком (playbooks/email-relay) — релиз mono его больше не пересобирает.
Прод-образ mono-base (prune --prod) не подходил для standalone-сервиса.
Удалено: components/email-relay, build_service в release.yaml, dev-сервис в
docker-compose. Контроллерный relay-режим (EMAIL_RELAY_URL/TOKEN в
email-channel.adapter + config) ОСТАЁТСЯ — он и активирует отправку через релей.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Прод-образ mono-base делает `pnpm prune --prod` (Dockerfile:74) — devDeps
вырезаются. ts-node был в devDependencies → в проде `sh: ts-node: not found`,
контейнер в restart-loop, /health не отвечал. controller держит ts-node в
dependencies — повторяем паттерн. (Коммит из закрытого PR #115 не попал в dev,
вношу прямо.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
mailpit был только локальным SMTP-перехватчиком для теста — в поставке не нужен.
email-relay сам ходит в реальный SMTP через nodemailer; конфиг (RELAY_TOKEN,
SMTP_*) выносим в components/email-relay/.env по образцу coopback, без отдельного
контейнера и inline-параметров.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Хостинги коопов режут исходящий SMTP → письма controller'а уходят в timeout
(раньше хабом был Novu, его убрали). Новый компонент @coopenomics/email-relay
принимает письма по HTTPS с Bearer-токеном и форвардит по SMTP. Ставится на
сервер с открытыми портами (плейбук в playbooks/email-relay).
- components/email-relay: express + nodemailer, POST /send + GET /health,
Bearer-токен (constant-time), опц. IP-allowlist, конфиг из ENV
- controller: email-channel.adapter получил relay-режим — если задан
EMAIL_RELAY_URL, письмо уходит POST'ом на релей; иначе прямой SMTP как
раньше (opt-in, без регрессии)
- docker-compose: дев-сервисы email-relay + mailpit (SMTP-перехватчик для E2E)
- release.yaml: сборка образа dicoop/email-relay
E2E проверен локально: controller-формат письма → релей → SMTP → mailpit,
тело с <br> и заголовки целы; 401 на отсутствие/неверный токен, 400 на пустое тело.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
В колоколе уведомлений на тёмной теме элементы сливались с фоном панели:
разделители на --p-line (0.06 white) почти невидимы, а непрочитанные
отличались лишь цветом текста + точкой — всё тонуло в --p-surface. На светлой
контраст тёмного текста на белом спасал.
Канон-токенами (обе темы, светлую не ухудшает): панели — рамка --p-line-1
для контура; разделители между уведомлениями — --p-line-1 (заметнее); у
непрочитанных подложка --p-primary-soft + левый акцент-бар --p-primary,
hover/focus — --p-primary-line.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Заменяет ненадёжный триггер от lifecycle service worker'а (не срабатывал
на iOS standalone PWA, при первой установке и когда бинарь SW не менялся)
на сравнение версий: запечённая в бандл package-версия (process.env.APP_VERSION)
сверяется с self-report SSR-ноды (/version). При расхождении — существующий тост.
- quasar.config: APP_VERSION из package.json в build.env; middleware 'version'
- src-ssr/middlewares/version.ts: self-report /version (JSON, no-store)
- entities/AppVersion: стор useUpdateWatch (реестр компонентов, сейчас shell),
опрос /version раз в 5 мин + при возврате на вкладку; SPA-safe (нет ноды -> no-op)
- init-app: старт version-watch рядом с systemMonitoring
- register-service-worker: убран SW-триггер; applyUpdate устойчив (waiting -> SKIP_WAITING, иначе reload)
- LeftDrawerMenu: версия приложения в футере рейла
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
«исчезнет» → «исчезает»; убрано предложение про отзыв заявки на повестке —
кнопка доступна только когда аккаунт уже в цепи (after оплаты), решение совета
тут неприменимо.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Прежний allow-list статусов исключал Registered — но именно туда попадают
отклонённые советом и оплаченные брошенные регистрации (отказ/возврат
оставляют статус registered; failed/refunded users.status никем не
выставляются). То есть чистка не работала для своей же цели «убрать
незавершённые/отклонённые регистрации (тестеры)».
- backend deleteAccount: убран allow-list по статусу, остаётся единственный
гард — НЕ принят в кооператив (нет participant_account).
- frontend isDeletable: !participant_account. Кнопка удаления теперь и у
Registered (оплачен/на рассмотрении совета/отклонён/возврат).
- confirm-диалог: для blockchain-registered (Registered) предупреждение, что
on-chain аккаунт неудаляем (имя в цепи занято навсегда) + про отзыв заявки
на повестке, если ещё на рассмотрении совета.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
На мобиле (и в узком окне десктопа) canon-таблицы выходили за ширину: левая
колонка обрезалась, появлялся внутренний горизонтальный скролл — из-за
фиксированной min-width (780–1000px), которую виджеты ставят локально, а
скелетон inline-стилем.
Централизовал в каноне components.css: `.table { min-width: 0 !important }`
снимает форс-ширину на всех экранах (!important — иначе scoped/inline
перебивают канон), таблица всегда влезает (width:100%) и переносит содержимое
на следующую строку вместо скролла. На узких экранах (≤599px) дополнительно
table-layout:auto + перенос в ячейках + плотнее паддинги, чтобы многоколоночная
таблица читалась переносом, а не клиппингом.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Пользователю в тост летел сырой ассерт контракта, напр.
`walletop TRANSFER: недостаточно средств на кошельке w.wal.share`.
Ему не нужны ни scope-префикс операции, ни служебное имя кошелька — он
нажал конкретную кнопку и так понимает, о каком кошельке речь.
Добавлен универсальный санитайзер sanitizeBlockchainError, встроенный в
единую точку показа ошибок FailAlert:
- снимает ведущий технический scope-префикс (`walletop TRANSFER:`,
`ledger2::apply:`, `o.mkt.supply:`) — распознаёт идентификатор с
`.`/`::` либо слово+ALLCAPS-опкод, кириллицу не трогает;
- убирает служебные имена ledger2-кошельков (`w.wal.share` и т.п.);
- нормализует пробелы и ставит заглавную первую букву.
Результат для примера выше — «Недостаточно средств на кошельке».
Обычные (нетехнические) сообщения проходят без изменений.
Полный технический текст остаётся разработчикам: FailAlert дублирует
сырой message в console.error, бэкенд пишет его в логи контроллера.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Баг: при «Получить возврат» с суммой больше L3-баланса платёж писался в
gateway-БД (AWAITING_AUTHORIZATION) ДО on-chain транзакции. Контракт wallet
отклонял заявку (недостаточно L3-средств), а в разделе «Платежи» оставался
висеть фантомный исходящий платёж со статусом FAILED.
Фикс — порядок операций в WalletInteractor.createWithdraw:
1. prepareWithdraw — все валидации (символ, дубликат hash, платёжный метод)
и сборка записи, БЕЗ записи в БД;
2. on-chain wallet::createwthd — при недостатке средств падает здесь,
ни одной записи о платеже не создаётся;
3. persistWithdraw — фиксация платежа в БД только после успешной заявки.
GatewayInteractor.createWithdraw разбит на prepareWithdraw/persistWithdraw
(старый combined-метод сохранён как обёртка). Порт и оба адаптера обновлены.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Программные оферты (program_id > 0) с Эпика 2 уходят на цепь через
wallet::signagree (commit 21e94ce0ed), а не soviet::sndagreement —
soviet::newagreement по ним не создаётся. Агрегатор пакета документов
(document-package-v1) искал приложения к заявлению только по
soviet::newagreement, поэтому подписанная оферта молча выпадала из links
и не отображалась как приложение к заявлению на повестке совета (при этом
сам документ виден в реестре).
Резолв каждого linkHash вынесен в общий хелпер buildLinkedAggregate;
носитель подписи ищется сначала в soviet::newagreement, затем фолбэком в
wallet::signagree. Унифицированы три дублировавшихся цикла (statement /
decision / acts). Добавлен регрессионный unit-тест.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
In-app тело инбокса рендерится как текст ({{ n.description }}, без v-html —
payload несёт ФИО/заголовки, v-html = XSS-вектор), поэтому HTML-тег <br> из
шаблона показывался строкой «№C53E<br>От: …». Перевёл 2 in-app шаблона
(new-agenda-item, approval-request) с <br> на \n и добавил white-space:
pre-line на .notification-center__item-desc (line-clamp:2 сохранён).
Push не трогаю — его тела уже plain-text без тегов, отображается верно.
Email оставлен на HTML (<br>/<strong>) — рендерится почтовым клиентом.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Временный shim очистки persisted-localStorage от устаревшего «КПП банка».
К 01.09.2026 старые незавершённые черновики регистрации истекут — утилиту
и оба вызова (CreateUser/AddUser model) можно удалить.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Пайщики, начавшие регистрацию ИП/юрлица ДО удаления поля «КПП банка»
(commit 93b94326), имеют это значение в persisted-localStorage стора
регистратора (useRegistratorStore, persist:true). При построении мутации
RegisterAccount/AddParticipant org/entrepreneur объект уходил целиком,
включая bank_account.details.kpp → BankAccountDetailsInput его больше не
принимает → GRAPHQL_VALIDATION_FAILED, форму не отправить.
Защитный strip stripLegacyBankKpp() срезает kpp из bank_account.details
на входе в мутацию (createUser/signup и addUser/председатель). КПП самой
организации (details.kpp) не трогаем — легитимный реквизит юрлица.
Read-путь (старый фронт запрашивает details{kpp}) лечится перезагрузкой
страницы — селекторы SDK уже без kpp.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CI vue-tsc (PR #60) после фикса lockfile дошёл до тайпчека и поймал хвосты:
- src-ssr/middlewares/{injectEnv,generateConfig}.ts: NOVU_APP_ID/BACKEND_URL/
SOCKET_URL слались в объект EnvVars, откуда поля удалены де-Novu → TS2353.
Убрал мёртвые строки.
- NotificationJournalWidget.vue: getName() возвращает string|undefined
(organization_data?.short_name) → .replace на possibly-undefined (TS2532).
Обернул в (getName(account) ?? '').
- NotificationCenter/model/index.ts: n.createdAt — DateTime-скаляр SDK типа
unknown, NotificationItem.date ждёт string|Date (TS2322). Каст as string.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CI vue-tsc (PR #60) после фикса lockfile дошёл до тайпчека и поймал хвосты:
- src-ssr/middlewares/{injectEnv,generateConfig}.ts: NOVU_APP_ID/BACKEND_URL/
SOCKET_URL слались в объект EnvVars, откуда поля удалены де-Novu → TS2353.
Убрал мёртвые строки.
- NotificationJournalWidget.vue: getName() возвращает string|undefined
(organization_data?.short_name) → .replace на possibly-undefined (TS2532).
Обернул в (getName(account) ?? '').
- NotificationCenter/model/index.ts: n.createdAt — DateTime-скаляр SDK типа
unknown, NotificationItem.date ждёт string|Date (TS2322). Каст as string.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Удаление @novu/js из desktop/package.json (b500789dd6) не сопровождалось
пересборкой pnpm-lock.yaml → CI install --frozen-lockfile падал
ERR_PNPM_OUTDATED_LOCKFILE до шага typecheck. Пересобрал lockfile-only.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Получатель без активной push-подписки (не дал разрешения / нет PWA / 410)
давал {delivered:false,error} → worker ретраил 5× → терминальный FAILED:
ложные «Ошибка» в журнале и warn в логах для всех, у кого push не настроен
(в dev SW не регистрируется без ENABLE_PWA_DEV → у всех).
Ввёл исход skipped в ChannelDeliveryResult: канал неприменим к получателю.
Адаптер web-push возвращает skipped при 0 подписок / отсутствии username.
Worker гасит такую строку в canceled — без ретраев, без записи попытки в
журнал, без error-лога (debug); причину кладёт в lastError → тултип
«Отменено» на столе председателя. Гейт на слое отправки (состояние подписки
авторитетно в момент доставки), роутер outbox остаётся чистым.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Получатель в журнале:
- две строки — ФИО (резолв username→account через getName на фронте, кэш
на компонент) + AccountBadge с username и копированием. DTO журнала несёт
только username/subscriber_id, поэтому ФИО тянем getAccount'ом. Нет ФИО
(приватные данные недоступны) → одна строка с username.
Видимость ошибки доставки:
- worker раньше логировал только исключение тика; провалы отправки шли лишь
в БД → при падающем канале в логах пусто и не видно, что worker жив.
Добавлено: лог старта (onModuleInit), debug-heartbeat тика с числом строк,
warn на ретрай и error на терминальный провал — с контекстом
(workflow/канал/получатель/попытка) и текстом ошибки.
- в журнале на ячейке статуса «Ошибка» — q-tooltip с lastError (поле уже
отдаётся DTO/селектором), курсор help.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Футер «Показать все» удалён из личного колокола: журнал — админский
(стол председателя), пайщику туда не нужно; председатель открывает
журнал через пункт меню. Колокол = свежие из инбокса + «Прочитать все».
- Бейдж счётчика вытолкнут за угол кнопки через transform translate(45%,-45%)
— больше не «наезжает» на глиф колокола.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Журнал уведомлений зарегистрирован в реальном меню стола председателя
(extensions/chairman/install routes[0].children) — был воткнут в мёртвый
src/desktops/Chairman/model (не в реестре расширений), потому страница
была недостижима. Дубль из мёртвого манифеста убран.
- Колокол: спиннер заменён канон-скелетоном (.skel), тело фиксированной
высоты (~4 уведомления, max 60vh) — панель больше не «прыгает» при
скелетон → меньше элементов → пусто.
- Футер: вместо неясной пагинации «Все загружены/Показать ещё» —
навигация «Показать все →» в журнал (только для председателя; q-menu
закрывается при переходе).
- Бейдж счётчика смещён в угол колокола (-4/-4) — убран «наезд» на иконку.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Публичный call.element.io перешёл на слот-протокол MatrixRTC: имя LiveKit-комнаты
теперь считает lk-jwt-service как unpaddedBase64(sha256(JSON.stringify([roomId,
"m.call#ROOM"]))), а не старое sha256(roomId+"|m.call#ROOM"). Старая формула
перестала матчить webhook'и → секретарь молча игнорировал все звонки
(«Комната ... не в реестре транскрипции»).
Матчинг теперь сверяет оба формата (текущий + legacy для старых клиентов Element).
Тест на живой паре с прода (комната «Ферма на Паях», созвон 07.06).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Кнопка удаления не показывалась НИ у кого, и сервер отверг бы все удаления.
Причина: гейт опирался на blockchain_account, но on-chain аккаунт заводится рано
и присутствует уже у статуса Created — то есть у всех. Плюс на фронте сравнение
шло со строками в нижнем регистре, а GraphQL отдаёт enum UserStatus с большой
буквы (Created/Joined/...).
- interactor.deleteAccount: убран blockchain_account из guard; различитель —
participant_account (принят) + статус-allow-list (домен lowercase).
- ParticipantsTable.isDeletable: убран blockchain_account; allow-list через
Zeus.UserStatus (Created/Joined/Payed/Failed/Refunded). Registered (после приёма
взноса), Active, Blocked — не удаляются.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Idempotency-guard listener'а пропускал создание возврата при наличии ЛЮБОГО
REGISTRATION_REFUND. После первого цикла отказа возврат cycle1 оставался в БД
(его hash = registration_hash = sha256(username) детерминирован и совпадает с
новым), поэтому на втором цикле возврат не создавался, latest refund оставался
старше нового платежа, и фронт зависал на «платёж принят».
Guard теперь пропускает только возврат ТЕКУЩЕГО цикла — свежее текущего
вступительного платежа. Детекция цикла по дате — как в account.interactor.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Повторная подача после отказа совета:
- createRegistrationPayment больше не переиспользует старый COMPLETED рег-платёж,
если возврат свежее его (цикл закрыт) — заводит новый ордер со свежим QR,
иначе пайщик видел исполненный QR, а в реестре совета платёж не появлялся.
- ReadStatement сбрасывает согласие с Уставом при входе в раздел (как doc-галочки).
- GenerateAccount сбрасывает «Я сохранил ключ» при входе в шаг (q-step не размонтируется).
Назначение платежа (memo) — единый полный источник:
- оговорка «НДС не облагается.» вынесена в константу VAT_EXEMPT_NOTE и добавлена во
все 4 назначения (вступительный, паевой, возврат паевого, возврат рег-взноса);
- qrpay/sberpoll больше НЕ дописывают «. Без НДС.» к Purpose — назначение целиком
в memo, без задвоения.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Два явных шага возврата при отказе совета (по просьбе): пока касса не подтвердила
исходящий платёж — показываем «возврат выполняется» без кнопки; после подтверждения —
«взнос возвращён» + кнопка. Иначе ранний клик упирался в reguser (карточка ещё не
снята) и падал.
- getAccount: registration_payment.status = PROCESSING пока возврат идёт,
REFUNDED по завершении (refundpay снял карточку — повторная подача возможна).
- WaitingRegistration: ветка isCouncilRefundPending (PROCESSING) — баннер + песочные
часы, без кнопки; isCouncilDeclined (REFUNDED) — баннер + «Подать заявку заново».
- fixData: ошибка через extractGraphQLErrorMessages вместо e.message
(раньше показывало «undefined» — у GraphQL-ошибки SDK message лежит глубже).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Вариант 1: после отказа совета и возврата взноса несостоявшийся пайщик может
подать заявку заново на том же аккаунте (а не упереться в тупик).
Контракт:
- reset_account_card: refundpay (и миграц.ветка declinereg) при закрытии кандидата
снимают карточку участника (type="", storages) — иначе повторный reguser падает
«повторное получение невозможно». Аккаунт и ключи сохраняются.
Бэкенд:
- registerBlockchainAccount: CreateAccount шлётся только если eosio-аккаунта ещё нет
(при повторной подаче он уже существует — иначе двойное создание падало бы).
- resetRegistration: ветка «отказ совета» — пропускает pre-payment гарды (аккаунт в
цепи и взнос принят здесь нормальны), требует возврат COMPLETED (карточку снял
refundpay), сбрасывает кандидата + user.status=created. Старые платежи не удаляет.
- getAccount: сигнал REFUNDED только если возврат свежее последнего рег-платежа —
после повторной подачи новый REGISTRATION перекрывает старый отказ.
Повторная подпись соглашений идемпотентна (signagree перезаписывает программу,
sndagreement падает лишь на confirmed — при отказе совета соглашение не confirmed).
Стандарт p.reg.refund: отмечено освобождение аккаунта для повторной подачи.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
На момент релиза двухфазного учёта будут кандидаты в статусе payed, чей платёж
принят по СТАРОМУ пути — без o.reg.inpay, то есть без баланса на w.reg.pend (76).
Новые confirmreg/declinereg делали бы TRANSFER/BURN с пустого суспенса → падение
«недостаточно средств».
Fallback по наличию баланса кандидата на w.reg.pend (helper
get_registration_pending_balance читает ledger2::userwallets cross-contract):
- есть баланс (новый путь): confirmreg → o.reg.setmin/setent (76→80/86),
declinereg → o.reg.refund (BURN 76, Dr 76/Cr 51);
- нет баланса (принят до релиза): confirmreg → o.reg.putmin/payent (прямой
ISSUE Dr 51/Cr 80|86, как раньше), declinereg → без on-chain проводки
(возврат банковским переводом из бэкенда, как было).
Весь блок помечен «снять условие после 30.07.2026»: к этому сроку все зависшие
на момент релиза кандидаты гарантированно получат решение совета.
registrator.wasm пересобран.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Раньше регистрационный взнос вставал на учёт одношагово только на одобрении
совета (confirmreg: Dr 51/Cr 80 + Dr 51/Cr 86), а приём денег кассой
(confirmpay) и отказ совета (declinereg) не делали проводок вообще.
Вводит суспенс-фазу через новый счёт 76 «Расчёты с пайщиками»:
- confirmpay (касса приняла деньги) → o.reg.inpay: Dr 51 / Cr 76, ISSUE w.reg.pend
- confirmreg (совет одобрил) → o.reg.setmin (Dr 76/Cr 80) + o.reg.setent (Dr 76/Cr 86),
TRANSFER w.reg.pend → w.reg.minshr / w.reg.entry
- declinereg (совет отказал) → o.reg.refund: Dr 76 / Cr 51, BURN w.reg.pend
(отдельный процесс p.reg.refund) + удаление кандидата
Новые сущности ledger2:
- счёт 76 PARTICIPANT_SETTLEMENTS (ACTIVE_PASSIVE)
- кошелёк w.reg.pend REGISTRATION_PENDING (USER_SHARED, program_id=0)
- операции o.reg.inpay/setmin/setent (p.reg.accept) + o.reg.refund (p.reg.refund)
confirmreg: проводки под гардами amount>0 (чинит латентный креш при initial==0).
declinereg: добавлен require_auth(_soviet) + возврат + чистка кандидата
(устраняет залипание записи кандидата, блокировавшее повторную подачу).
Синхронизированы оба зеркала (cpp registries + cooptypes TS), обновлён
snapshot реестра кошельков, добавлен p.reg.refund в process-hash-locator
(иначе integrity-check валит controller на старте). Стандарт p.reg.accept
обновлён под двухфазную схему, заведён p.reg.refund.standard.yaml.
Контракты registrator.wasm и ledger2.wasm собраны (static_assert'ы реестров прошли).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
notifyPaymentStatus выбирал workflow только по статусу (PAID → PaymentPaid
«Платёж принят»), игнорируя direction. Исходящий PAID — это возврат взноса
пайщику, и он получал письмо как на приём. Баг общий для всех возвратов
(регистрационный + паевой — оба идут через gateway.setPaymentStatus).
- Новый workflow notifications/payment-refunded «Возврат взноса выполнен»
(зеркало payment-paid, тот же payload).
- payment-notification.service: OUTGOING+PAID → PaymentRefunded, INCOMING+PAID →
PaymentPaid, остальное → PaymentCancelled.
Workflow засинкан в Novu (pnpm sync, 24 workflow, 0 ошибок).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Backend: memo возвратного платежа теперь зеркалит назначение входящего взноса —
«Возврат вступительного и минимального паевого взносов №XXXX. Без НДС.» с тем же
номером (original.hash.slice(0,8), как в QR при приёме) и НДС-оговоркой, что
qrpay/sberpoll добавляют к Purpose. Кассир копирует as-is в платёжку.
Desktop: PaymentDetails показывал «Назначение платежа» с кнопкой копирования
только в ветке OUTGOING+payment_details. Возврат идёт без payment_details и падал
в «нет дополнительной информации». Добавлена ветка: если есть payment.memo —
показываем его через CopyableInput (с копированием).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
В БД registration_hash хранится lowercase (sha256().digest('hex')), а из
цепи checksum256 в declinereg.registration_hash приходит UPPERCASE — exact-match
findOneBy не находил кандидата, возврат взноса пропускался с warn.
Нормализуем входящий хэш .toLowerCase() в findByRegistrationHash.
Проверено на voskhod: candidate czeudubzzuql имеет registration_hash
f25ca396... (lowercase), warn пришёл с F25CA396... (uppercase) — тот же хэш.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1. WaitingRegistration: крутящийся спиннер заменён статичной иконкой
hourglass_top с подписью «Ожидаем решение совета». Анимация загрузки
на процессе до 30 дней сбивала с толку — казалось, страница не дозагрузилась.
2. fixData (возврат к редактированию после отклонённого платежа) теперь зовёт
новый store.resetConsents(): сбрасывает agreements (устав/ПД/…) + подписи +
документы. Раньше галочка «прочитал устав» оставалась проставленной на
повторном проходе ReadStatement.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
hover ставил background: var(--p-surface) (#fff на light) — карта на
--p-canvas (#f4f4f5) осветлялась до белого, выглядело как засвет выбора.
На dark было ок (canvas #0a0a0a → surface #141416 — чуть светлее).
Замена на --p-canvas-2: на light темнее канвы, на dark светлее — hover
идёт в одну сторону с темой в обеих.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Конфликт только в сгенерированном Zeus-клиенте (controller/zeus + sdk/zeus) — разрешён
перегенерацией codegen, не ручной разводкой:
- пересобран cooptypes dist (протух: DeclineRegistration был в src, не в dist → TS2339)
- generate-schema (56 резолверов) + generate-client
- zeus теперь надмножество: dev + fsm-recovery (resetRegistration) + refund (REGISTRATION_REFUND)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Конвенция для Actions/*: независимые от data чтения — в resolveParallel,
зависимые (getMeta/getDecision/getMeetQuestions) — после батча; ловушка TDZ;
системный шрифт вместо base64.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
В предыдущем коммите arial.ttf закоммитился битым: components/controller/.gitattributes
содержит `* text eol=lf`, что применилось и к бинарному шрифту → срезало 105 байт
\r (275572 → 275467), шрифт невалиден. Добавил `*.ttf/otf/woff binary` в
controller/.gitattributes (перекрывает правило выше) и перезаписал blob корректно.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Главное горло генерации PDF: в каждый html инжектился @font-face с Arial через
data:base64 (~270 КБ TTF). weasyprint декодировал и парсил весь шрифт на КАЖДЫЙ
рендер — ~1.3 сек на документ в 754 байта (замер: тот же текст со встроенным
шрифтом 1282–1715 мс, без него 120–289 мс).
Ставим Arial СИСТЕМНО в образ controller (fontconfig) — парсится один раз и
кешируется. Шрифт тот же файл (декод прежнего ArialBase64 → arial.ttf), имя
семейства "Arial", глифы/вёрстка идентичны.
- controller/assets/fonts/arial.ttf — наш Arial (275 КБ)
- controller/Dockerfile — fontconfig + COPY шрифта + fc-cache
- factory Generator — убран @font-face data:base64, оставлен font-family: Arial
- удалён src/Fonts/arial.ts (367 КБ base64, больше не нужен; bundle 1.48 → 0.75 МБ)
Итог по всей серии (warm-pool + parallel-fetch + системный шрифт):
generateProjectOfFreeDecision ~5 сек → ~400 мс (×10).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Сборка данных документа в каждом Action делала серию строго последовательных
await: getTemplate (2 сетевых запроса в explorer) + getMeta(getCurrentBlock) +
mongo-чтения (getUser/getCooperative/getProject/getVars) + udata/request/meet.
Сетевые запросы складывались в задержку ~1.5с поверх рендера PDF.
Добавлен хелпер DocFactory.resolveParallel({...}) — резолвит ВЗАИМНО
НЕЗАВИСИМЫЕ источники через Promise.all. В каждом из 46 Action-факторов
независимый префикс (template + mongo-чтения + независимые meet/request/udata)
сведён в один resolveParallel; зависимые вызовы оставлены последовательными
после батча, с сохранением порядка и аргументов:
- getMeta({title: template.title}) — зависит от template (TDZ в Promise.all);
- getFullName/getCommonUser — от user;
- getDecision/getApprovedDecision — от coop (+meta.created_at);
- getMeetQuestions/getGeneralMeetingDecision — от meet;
- getProgram — от request.
Поведение не меняется — только параллелизуются независимые чтения. Условные
ветки (bank_account по типу юзера, branch по is_branched, signature) и гварды
сохранены на местах. Файлы, где параллелить нечего (только template+meta,
meta зависит от template) — не тронуты.
tsc + eslint + unbuild — зелёные.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
vue-tsc TS2719 в Decision/CreateProject/model: publishProjectOfFreeDecision
объявлен Promise<IAgenda | null>, но поле мутации nullable в схеме —
result включает undefined и не присваивается IAgenda | null.
Fix: return result ?? null.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Генерация PDF спавнила новый процесс `weasyprint` на каждый документ через
`exec`. Медленные 3-5 сек — это холодный старт Python: import всего стека
(cffi/pydyf/tinycss2/Pango/cairo/HarfBuzz) + скан fontconfig. Сам рендер
быстрый (<0.5 сек), но старт платился каждый раз, т.к. процесс умирал сразу.
Держим пул из N долгоживущих Python-процессов горячими (WeasyPool/WeasyWorker):
import платится один раз за жизнь бэкенда, далее каждый документ = чистый
рендер. Размер пула = env WEASY_POOL_SIZE (дефолт min(4, CPU)) — даёт реальный
параллелизм, одновременные генерации не сериализуются к одному процессу.
Воркер живёт внутри процесса controller'а: без докера, миграций и отдельного
сервиса. Питон берётся из /venv (controller/Dockerfile), фоллбэк python3.
Детерминизм сохранён: SOURCE_DATE_EPOCH=0 в env воркера. Заодно выровнял
weasyPrintVersion 62.3 → 67 (синхрон с pip install в controller/Dockerfile —
метадата producer'а документов больше не врёт).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
getAgendaItemByHash сравнивал decision.hash с data.document.doc_hash (хэш только
содержимого), тогда как decision.hash в блокчейне == data.document.hash («общий
хэш» = doc_hash + meta_hash). Из-за этого матч НИКОГДА не срабатывал: publish
впустую опрашивал повестку весь таймаут (~4.5 c) и возвращал null — мгновенный
показ не работал ни разу, вопрос всегда дожидался поллинга страницы.
- Матч по data.document.hash (= decision.hash on-chain).
- Поллинг: проверка сразу (без слепой стартовой паузы), тик 400 мс — ловим
индексацию парсером action'а (~2 c) минимальной задержкой; publish ~2.2 c.
По таймингам: decision на чейне мгновенно, aggregate ~0.4 c; доминирует лаг
индексации парсером (~2 c) — единственный способ убрать его — синтез action,
отвергнут (требует подделки ~12 explorer/SHiP-полей BlockchainActionDTO).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Бэкенд возвращает пункт повестки только когда он полностью собран (decision из
блокчейна + проиндексированные парсером действие и документ-заявление) — то есть
он уже идентичен тому, что отдаёт getAgenda. Значит блокировать голос/утверждение
до следующего поллинга незачем: пользователь может действовать немедленно.
Убрана блокировка: pendingConfirmationIds (store/process-decisions) и
disabledDecisions (страница). Буфер-мердж в agendaStore оставлен как страховка от
«мигания» (вставленный вопрос держится, пока поллинг не вернёт его authoritative).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
После публикации свободного решения повестка показывала «Нет вопросов на
повестке» до следующего поллинга (10+ сек). getAgenda собирается join'ом таблицы
decisions из блокчейна с проиндексированными парсером действием newsubmitted и
документом-заявлением, поэтому только что созданный вопрос появляется не сразу.
Решение: мутация publishProjectOfFreeDecision теперь возвращает созданный пункт
повестки. Бэкенд после публикации делает settle-паузу (parser-индексация) и
извлекает готовый вопрос с повестки по хэшу документа; фронт вставляет его в
таблицу немедленно. Кнопки голосования/утверждения/правки на нём заблокированы,
пока ближайший getAgenda-поллинг не подтвердит вопрос authoritative-данными.
Backend:
- AgendaInteractor.getAgendaItemByHash / AgendaService.getAgendaItemByHash —
сборка одного пункта повестки по хэшу заявления (null, пока не проиндексирован).
- FreeDecisionService.publishProjectOfFreeDecision — settle-пауза + страховочная
петля, возвращает AgendaWithDocuments | null. Резолвер: тип возврата сменён с
Boolean на AgendaWithDocuments (nullable).
- AgendaModule экспортирует AgendaService; DecisionModule импортирует AgendaModule.
SDK: publish-селектор → форма agenda-item (rawAgendaSelector); regen zeus-клиента.
Frontend:
- agendaStore: буфер оптимистично вставленных вопросов + computed-мердж с
authoritative-списком (без «мигания»); pendingConfirmationIds для блокировки.
- CreateProjectButton: вставляет вернувшийся вопрос через insertCreated.
- process-decisions/страница: блокируют кнопки неподтверждённых вопросов.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Дополнена страница «Повестка совета» бизнес-языком: если решение не принято
или истёк его срок, вопрос снимается с повестки — председателем вручную
(отрицательный консенсус) или автоматически по истечении срока. Обновлён
mermaid-граф (ветки «большинство против» и «срок истёк»), добавлен раздел
«Снятие вопроса с повестки», в блок голосования добавлено описание индикатора
отклонения. Без скриншотов.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Голос «против» больше не затирает решение автоматически. Развязаны два пути
отказа:
- по истечению срока — авто (cancelexprd кроном chairman, ключом кооператива);
- по отрицательному консенсусу (большинство «против») — явным действием
председателя declinedec, до истечения срока.
Контракт (soviet):
- voteagainst: только фиксирует голос, без авто decline_and_erase_decision.
- Новый action declinedec (в cancelexprd.cpp): require_auth(coopname), проверяет
votes_against*100 > members*50 (зеркало votefor), переиспользует общую ветку
decline_and_erase_decision. Собран в CDT, declinedec в ABI.
- Стандарты sov.authpkg / sov.decision приведены к развязке.
cooptypes: action Declinedec + интерфейс IDeclinedec.
Контроллер:
- SovietBlockchainPort/adapter: declineDecision + getBoards.
- Мутация declineDecision (resolver/service/interactor + DeclineDecisionInput),
роль chairman.
- Повестка обогащается council_members_count (состав совета `soviet`, дёргается
один раз) — фронту для порога принятия/отклонения.
- chairman: убран флаг cancelApprovedDecisions и кривая подпись; крон по сроку
гасит ВСЕ истёкшие решения.
SDK: селектор council_members_count, мутация DeclineDecision; zeus regen (обе копии).
Desktop:
- BaseButton: сплошной вариант negative.
- VotingButtons: красный крестик при отрицательном консенсусе.
- QuestionCard: у председателя красная «Отклонить» (declinedec) вместо «Утвердить»
когда решение отклонено советом; фича Decision/DeclineDecision.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Баг: при перезаходе/перезагрузке страницы оплаты в момент приёма взноса
заводился второй регистрационный платёж. Дедуп findActivePendingPayment ловит
только status=PENDING — а когда первый платёж уже PAID/COMPLETED (принят,
аккаунт регистрируется), дедуп промахивался и createInitialPayment создавал
платёж-дубль. Его нельзя ни подтвердить (processIncomingPayment →
registerBlockchainAccount → «аккаунт уже зарегистрирован»), ни осмысленно
отклонить — мусорный сирота в реестре.
Фикс: регистрационный платёж одноразовый. Берём последний рег-платёж пайщика
и, если он в живом статусе (PENDING/PROCESSING/PAID/COMPLETED), возвращаем его
вместо создания нового. Новый ордер заводим только если прошлого нет либо он
FAILED/EXPIRED/CANCELLED (легитимная повторная попытка, в т.ч. после
resetRegistration). Авторитетная защита на бэке, не зависит от гонок на фронте.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
По ревью @ant: не хардкодить 'action::registrator::declinereg' строкой.
Добавил action-константу RegistratorContract.Actions.DeclineRegistration
(actionName='declinereg', тип IDeclinereg) в cooptypes — её не было.
Листенер подписывается через
action::${RegistratorContract.contractName.production}::${...DeclineRegistration.actionName}
по образцу signed-document-ingestion (SovietContract).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
При отказе совета в приёме пайщика (отрицательный консенсус >50% — PR #94 —
или истечение срока повестки) on-chain срабатывает registrator::declinereg.
Взнос по фондам на этот момент НЕ разносился (confirmreg не было), но реальные
деньги пайщика уже у кооператива на расчётном счёте — их нужно вернуть.
- Новый PaymentTypeEnum.REGISTRATION_REFUND (исходящий), отдельный от WITHDRAWAL
(возврат паевого), чтобы не путать термины. Лейблы + OUTGOING_PAYMENT_TYPES.
- RegistrationDeclineListener на action::registrator::declinereg: резолвит
кандидата по registration_hash, по исходному REGISTRATION-платежу берёт сумму
и номер (hash8 = тот же № из QR), заводит исходящий платёж PENDING с
назначением «Возврат вступительного и мин.паевого взноса №<hash8>».
Идемпотентно (повторный приход события не плодит возвраты).
- processOutgoingPayment: для REGISTRATION_REFUND подтверждение кассиром —
off-chain no-op (статус → COMPLETED, без completeOutcome). Взнос on-chain не
двигался, откатывать нечего; обратная проводка по расчётному счёту появится
здесь же, когда введём проводки на приёме платежа.
- Реестр платежей: для возврата спрятана кнопка «Отклонить» (отказать в
возврате нельзя — совет уже отказал, деньги обязаны вернуть).
- Codegen: PaymentType.REGISTRATION_REFUND проброшен в Zeus (sdk + controller).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Провайдер QR (Bank) не эмитит success-колбэк, деньги подтверждаются вебхуком
на бэке асинхронно — без поллинга экран висел на форме оплаты до перезагрузки.
Каждые 10с подтягиваем аккаунт; как только вступительный платёж покинул PENDING
(принят/отклонён) — уходим на шаг ожидания решения совета, который сам
показывает «платёж принят» либо причину отказа.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PNG-иконки manifest'а физически отсутствовали (на проде /icons/*.png отдавали
HTML-фоллбэк) → на домашнем экране дефолтная иконка. Генерим полный набор из
logo.svg (квадрат, белый фон, лого по центру, maskable-safe) + в динамическом
manifest основная иконка — масштабируемый /logo.svg (purpose any maskable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Образ desktop generic, кооп задаётся env контейнера (COOP_SHORT_NAME), а
manifest пёкся на сборке и коопа не знал → на домашнем экране всегда дефолтное
'Цифровой Кооператив'. Отдаём manifest динамически SSR-middleware из env по
пути /manifest.webmanifest (статический /manifest.json перехватывается
serveStatic ДО user-middleware, поэтому новый путь + переписываем <link
rel=manifest> в HTML). Плюс рантайм apple-mobile-web-app-title для iOS.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Новый SW больше не активируется в фоне сам (skipWaiting/clientsClaim=false) —
ждёт в waiting, убирая гонку «подвисание между релизами» (старая страница +
новый SW). updated() шлёт window-событие 'sw:update-available'; boot 'pwa-update'
ловит его и показывает канон-тост UpdateAlert в левом нижнем углу (timeout 0,
кнопки «Обновить»/«Позже»). По «Обновить» → window.applyUpdate() → SKIP_WAITING
→ controllerchange → reload. Детерминированно, без фонового свопа версий.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Plan B (recovery + источник истины о шаге на сервере), поверх гарда A.
Backend:
- resetRegistration (self-scoped по токену): откат незавершённой регистрации
к редактированию до отправки в блокчейн. Снимает заморозку профиля/e-mail
(users.status → created), чистит документы кандидата и отпечаток заявления,
удаляет непринятую попытку вступительного платежа. Принятые средства
(PAID/COMPLETED) или уже созданный on-chain аккаунт → отказ (нужна
перерегистрация/возврат — отдельная ветка).
- PaymentRepository.delete.
Frontend:
- SignUp реконструирует шаг с сервера: Joined → шаг оплаты (переживает
перезагрузку и работу в другой вкладке), приоритет у registration_payment.
- WaitingRegistration «Исправить данные» дёргает resetRegistration перед
возвратом к данным (после подписи профиль заморожен, локального отката мало).
- Фича Account/ResetRegistration + Zeus-клиент (codegen).
Существующие статусы (joined/registered/active) не меняем, новых не вводим:
фронтовые active-гейты висят на on-chain user_account.status — другая ось,
не пересекается с mono users.status, который трогает recovery.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
На контракте совета теперь возможно не только заблокировать решение
голосованием, но и принять отрицательное решение: как только против
проголосовало больше половины состава совета, решение отклоняется
немедленно (не дожидаясь истечения срока) — той же веткой отказа, что и
просрочка: decline-callback в исходный процесс + удаление решения.
- Общая ветка отказа вынесена в decline_and_erase_decision (cancelexprd.cpp),
переиспользована в cancelexprd (просрочка) и voteagainst (негативный консенсус).
- Порог зеркален votefor: votes_against*100 > total_members*50, строгое «больше».
- Стандарты sov.authpkg / sov.decision приведены к новому поведению; заодно
убран cancelvote (в коде запрещён: eosio::check(false)).
Контракт soviet собран в CDT — зелёный.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Гард A: на цепь не уходят данные, отличные от подписанных в заявлении.
Каноничный отпечаток удостоверяющих личность полей фиксируется при
подписании заявления (registerParticipant → candidate.meta), сверяется
перед registerBlockchainAccount. Расхождение → BAD_REQUEST, платёж уходит
в FAILED с причиной (видна пайщику через registration_payment из #91).
Пропуск уже идущих регистраций: кандидаты без отпечатка в meta (заявление
подписано до внедрения гарда) не проверяются — grandfather.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Обработка отклонения платежа на этапе регистрации в SignUp:
- Session store: геттер registrationPayment из аккаунта.
- SignUp.vue onMounted: если есть незавершённый вступительный платёж —
восстанавливаем шаг WaitingRegistration после перезагрузки/в любой вкладке
(процесс может тянуться днями).
- WaitingRegistration.vue: ветка «Платёж не принят» с причиной из
registration_payment.message и кнопками «Повторить оплату» / «Исправить
данные» (откат назад по процессу — только по нажатию пайщика). Live-опрос
аккаунта раз в 10с, чтобы экран сам переключился на отклонение/приём.
- Payment/SetStatus: setCancelledStatus(id, message); кнопка «Отклонить»
теперь ставит CANCELLED (триггерит уведомление) с полем причины.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Backend под обработку отклонения регистрационного платежа (этап до создания
аккаунта в блокчейне):
- SetPaymentStatusInput: опциональное поле message — причина смены статуса;
сохраняется в payment.message и показывается пайщику.
- gateway.interactor.setPaymentStatus: персистит message при смене статуса
(уведомление о CANCELLED уже шлётся через PaymentNotificationService).
- PaymentRepository.findLatestByUsernameAndType — последний платёж типа.
- AccountDTO.registration_payment {status,message,hash,quantity,symbol}:
сводка вступительного платежа в getAccount. Источник истины для фронта,
чтобы восстановить шаг регистрации (ожидание/отклонение) после
перезагрузки и в любой вкладке. Заполняется пока нет participant_account.
- Регенерация Zeus-клиента (schema.gql gitignored).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
'maintenance' на фронте включает полноэкранную заглушку «Техническое
обслуживание» (watch-desktop-health), которая накрывала бы саму страницу
установки. Поэтому статус остаётся 'initialized' на всё время install
(фронт показывает страницу установки, как и прежде), а в 'maintenance'
переводим ТОЛЬКО в catch при падении ПОСЛЕ записи в цепь — это блокирует
повторный запуск (guard требует 'initialized') и корректно сигналит, что
коопа в полу-установленном состоянии и требует ручного разбора.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Счётчик сходится к верному значению (5) за один деплой независимо от
порядка выполнения migrate registrator vs soviet. Сверено с цепью:
11 participants = 6 фантомов + 5 реальных (3 founders + 2 новых пайщика
Белоус Нина/Сергей, вступивших после инцидента).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Причина (инцидент 2026-06-04, gorozhane): InstallInteractor.install прогнан 3×
(2 провала + успех). Каждый прогон генерил случайный username и необратимо
регистрировал совет на цепи (adduser), а откат в catch чистил только off-chain.
На цепи осели 6 дублей-аккаунтов 3 учредителей + 1800 RUB фантома в паевом фонде.
Багфикс (install.interactor.ts):
- лок: статус → 'maintenance' ДО любых on-chain операций; повторный install
блокируется guard'ом (требует 'initialized'), что и плодило дубли;
- при ошибке ПОСЛЕ первого adduser off-chain rollback не делаем (сохраняем
привязку личность↔username для разбора), статус остаётся 'maintenance';
- при ошибке ДО on-chain — чистый откат + возврат 'initialized'.
On-chain очистка (вызовется автоматически на деплое, идемпотентна, scope=coopname):
- registrator::migrate — erase 6 фантом-accounts + пересчёт active_participants_count
из soviet::participants;
- soviet::migrate — erase 6 фантом-participants (гард: остаётся ≥1 реальный);
- ledger2::migrate — снять L3-доли фантомов на w.reg.minshr, на снятую сумму
уменьшить L2-кошелёк и бухсчета 80 (Cr, паевой фонд) и 51 (Dr, расчётный).
Off-chain коррекции (mongo individuals и пр.) — вручную, см. ~/fixes.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
КПП — реквизит юрлица, у ИП и физлиц его нет. Общий тип реквизитов
банковского счёта (IBankAccount.details.kpp) требовал КПП у всех держателей
счёта, из-за чего индивидуальный предприниматель не мог пройти регистрацию
(и фронт, и бэкенд заворачивали пустой КПП).
Поле снято во всех слоях:
- cooptypes: тип IBankAccount.details + шаблон ParticipantApplication
(render-аргументы и плейсхолдеры) + mock-данные registry
- controller: BankAccountDetails(Input) DTO, domain-интерфейсы, init-config
- factory: ajv-схема BankAccountSchema (properties + required)
- sdk: zeus-селектор bankAccountSelector + перегенерённый zeus-клиент
- desktop: формы ИП/юрлица/физлица, реквизиты, добавление способа оплаты,
CSV-импорт участников
КПП организации/кооператива (details.kpp, отчётность, qrpay/sberpoll)
НЕ затронут — это легитимный реквизит юрлица.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Story 9.9 «Стол восхода: approve pending packages» — backend-сторона.
GraphQL contract:
- Query.appsCatalogPendingModerations(status?: ModerationStatusEnum, limit?: Int)
→ [ModerationRequestDTO]. По умолчанию SUBMITTED. Chairman-only.
- Mutation.approveModeration(data: ApproveModerationInputDTO!)
→ ApproveModerationResultDTO. Discriminated outcome:
applied / pendingChain (423) / conflict (409) / requiresOverride (403) / failed.
- Mutation.rejectModeration(data: RejectModerationInputDTO!)
→ RejectModerationResultDTO. Discriminated outcome: applied / conflict / failed.
HTTP-service: 3 новых метода (listSubmittedModerations / approveModeration /
rejectModeration) на ca-admin endpoints, добавленных параллельным
apps-catalog PR #7. Mapping snake_case → camelCase; degraded mode на
list — пустой массив, на mutations — failed с явной причиной (чтобы UI
не вводился в заблуждение «approved»).
ReleaseScope передаётся как InputType с union-style полями
(type + subnets?/coopnames?); резолвер валидирует соответствие type →
непустой массив перед отправкой на ca-admin.
Зависимость: apps-catalog PR #7 (GET /v1/admin/moderation) должен быть
смёрджен в feat/E9-demo-app-pilot до того, как этот endpoint можно
использовать в e2e.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Story 10.4 — pipeline установки subgraph-расширения:
1. (опц.) OCI token у CA-auth → docker pull образа;
2. (опц.) docker compose up сервиса;
3. healthcheck poll по url subgraph'а до 200 OK;
4. upsert в subgraph_registry (Apollo Gateway за ≤10с подхватит);
5. на failure — composeDown rollback + discriminated outcome {applied|failed}.
InstallController стал тонкой обёрткой над InstallOrchestratorService.
Внешние зависимости (docker shell / fetch / CA-auth) изолированы за
портами; тесты (7/7 green) гоняют сценарий через ин-мемори моки без
реального docker daemon'а и сети.
Что ОСТАЁТСЯ для отдельной ветки под этим зонтиком:
- wiring approve→install REST из mono controller (mutation approvePackage);
- desktop notification после успешного install'а;
- E2E на реальном compose + chatcoop subgraph (Story 10.8).
Story 9.3.b-rel — мутация выкладки нового релиза ранее зарегистрированного
пакета. Прокидывает manifest + версию + tarball_sha256 на ca-admin
POST /v1/admin/releases, который сам валидирует manifest Zod-схемой
(PackageManifestSchema) и подписывает on-chain apps::setrelease от
имени chairman'а кооператива-оператора каталога.
Backend (apps-catalog-proxy):
- PublishReleaseInputDTO / PublishReleaseResultDTO — GraphQL Input/Object;
manifest как GraphQLJSON (произвольная структура валидируется на
ca-admin'е).
- AppsCatalogHttpService.createRelease(): POST /v1/admin/releases;
discriminated outcome — applied (с опциональным transaction_id) |
invalidManifest (HTTP 422 INVALID_MANIFEST мапится явно, чтобы UI
показывал валидационную ошибку отдельно от network/auth) | failed
для прочих.
- AppsCatalogProxyResolver.publishRelease(): @AuthRoles(['chairman']).
Под архитектуру E10 manifest содержит ссылки на артефакты в Nexus:
- coopenomics.backend.image — docker-image (subgraph расширения);
- coopenomics.frontend.tarball — npm tarball (desktop-bundle).
Сам manifest НЕ валидируется на стороне controller'а — это делает
ca-admin (там Zod-схема и она же будет расширена для package.json
полей в story 10.7).
Frontend (форма Опубликовать релиз) — отдельной story 9.3.b-ui после
codegen'а Zeus.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Story 9.3.b-pub стола разработчика — мутация для воскходовского
chairman'а: координаты пакета (@scope/name + owner + compatible_subnets)
прокидываются на ca-admin POST /v1/admin/package, который сам
подписывает on-chain action apps::regpkg от имени chairman'а
кооператива-оператора каталога.
Backend (apps-catalog-proxy):
- PublishPackageInputDTO / PublishPackageResultDTO — GraphQL InputType +
ObjectType (camelCase для Zeus, без snake-case утечек).
- AppsCatalogHttpService.registerPackage(): POST на /v1/admin/package;
discriminated outcome (applied | conflict | failed) — мапит 409 в
conflict, прочие axios-ошибки в failed; degraded-mode (без
APPS_CATALOG_API_KEY) возвращает failed с явным сообщением, чтобы UI
не вводился в заблуждение «опубликовано».
- AppsCatalogProxyResolver.publishPackage(): @UseGuards(GqlJwtAuthGuard,
RolesGuard) + @AuthRoles(['chairman']) — мутация доступна только
председателю кооператива.
Multipart upload install.js под архитектуру E10 НЕ делаем: расширения
теперь публикуются как npm-package + docker-image в Nexus отдельной
процедурой (`npm publish` / `docker push`) через scoped JWT от
CA-auth. Эта мутация — только on-chain маркер «такой пакет
существует»; релизы с manifest'ом — story 9.3.b-rel.
schema.gql / Zeus client — codegen за пределами этого PR (mono-каноном
pnpm generate-schema + generate-client + sdk build делается отдельным
коммитом, чтобы PR с backend changes не зависел от пересборки SDK
артефактов).
Frontend — отдельной story 9.3.b-ui: DeveloperPublishReleasePage.vue
сейчас заглушка, форму подключим после codegen'а Zeus.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Epic 10 Story 10.2b — минимальный subgraph на extension-sdk доказывает что
Federation v2 driver + JWT module + healthcheck реально работают вместе.
Smoke результат (SUBGRAPH_PORT=33001 JWT_SECRET=test-secret COOPNAME=voskhod):
- GET /_health → {"status":"ok","version":"unknown","uptimeSeconds":N}
- POST /v1/graphql { helloFromExtension(name:"мир") { greeting fromExtension } }
→ {"greeting":"Здравствуй, мир!","fromExtension":"hello-subgraph@0.1.0"}
- POST /v1/graphql { _service { sdl } } → returns federation v2 SDL с
@key/@external/@requires/@shareable директивами
Также в этом коммите:
- extension-sdk переключён с unbuild на обычный tsc (unbuild esbuild не
поддерживает декораторы для target ES2020). main: dist/index.js, CommonJS.
- @types/passport-jwt в devDeps.
Cross-extension entity references (SharedCooperator @key + ResolveField)
живут в chatcoop subgraph'е (story 10.8) — там есть локальная сущность
ChatThread которая ссылается на пайщика. Этот PoC только доказывает
базовую механику.
Epic 10 Story 10.1 — coopback становится первым federation v2 subgraph'ом.
Локальная core-схема компилируется с federation директивами (`autoSchemaFile.federation: 2`).
Endpoint остаётся на /v1/graphql; non-federated клиенты (desktop) не должны
заметить переключения — Apollo Federation добавляет лишь служебные
`_service { sdl }` и `_entities` query.
Gateway-режим (Apollo Gateway + IntrospectAndCompose) зайдёт отдельной
story 10.1b — поверх этого subgraph'а + registry в Postgres. На MVP пока
один subgraph, gateway не нужен.
Зависимости:
- + @apollo/subgraph ^2 (peer для federation 2 в @nestjs/apollo)
@key директивы на core entity (User/Cooperative/Account/Wallet) добавятся
по мере вытаскивания первых extension'ов — отдельными точечными PR.
R4: убрал хардкод "RUB" и "1000.0000 RUB" из feature ProgramExpense.
- submitTopup принимает raw число "10000" → форматирует через formatToAsset
с info.symbols.root_govern_symbol/precision.
- submitProgramExpense форматирует total_amount + planned_amount каждого item
через тот же helper (UX-долг "amount без IMask" закрыт по сумме).
- Placeholder'ы Dialog'ов вычисляются computed'ом из system.info.symbols.
R1 (частично): тип `GenerateStatementInput = Mutations.Expense.GenerateExpense
ProposalStatementDocument.IInput['data']` + деструктуризация результата через
computed `[Mutations.X.Y.name]` — убраны 2 из 4 `as any` каста (input + index).
Оставшиеся (signed-as-any и create-input-as-any) — отдельный refactor PR через
signed-doc adapter в shared/lib/document.
Source-wallet вынесен в локальную map SOURCE_WALLET_BY_OPERATION с throw на
неизвестный код — backend-derivation (R3) отдельной задачей.
Вынес общий PaginationInput → PaginationResult helper из capital-service в
~/application/common/dto/pagination.dto. Consumer'ы inter-port'ов (без своего
Repository.findAndCount) получают канон-сборку без дубля логики totalPages/
currentPage.
Также `paginationInputToOffset(options)` — стандартная конверсия для адаптеров,
работающих offset-form (большинство inter-port read API).
Capital ProgramExpensesManagementService переведён на оба helper'а — ужал
listProgramExpenses до 10 строк.
Why: использовал несуществующие токены/классы — UI бы рендерился со сломанными цветами/иконками. Привёл к канону:
- BaseChip: `:variant` (pos/accent/warn/neg/info/neutral), не `:label/:tone` (нет такого API).
- BaseButton: иконка через `<template #icon-left>`, не `icon='X'` (нет такого пропа).
- CSS-токены: `--p-ink`/`--p-ink-2` (не `--p-text`/`--p-muted`), `--p-fs-meta` (не `--p-fs-body-xs`).
- Утилитарные классы: `t-h2`/`t-sm`/`t-eyebrow`/`t-muted` из components.css, не `t-section`/`t-body-sm`.
Why: send_callback_if_any слал (proposal_hash, status, amount, data), а capital::onpgexpdone ждёт (coopname, expense_hash, status, total_actual, data). Скоуп таблицы progexpenses = coopname, без него инициатор не найдёт запись. Применимо ко всем будущим consumer'ам шасси (marketplace, EMP).
Why: ExpenseMechanics и ExpenseRecipientType — string enums; Number(str) даёт NaN, wharfkit не сериализует. Зеркалит маппинг из expenses-mutations.service.ts:57-63.
Тонкое расширение Capital для программных расходов: чисто consumer над
шасси `expense` (только Капитал-специфика, остальное обслуживает шасси).
Чейн:
- CapitalBlockchainPort.createProgramExpense → capital::createpgexp.
- CapitalBlockchainPort.topupProgramExpense → capital::topupprogexp.
Backend:
- ProgramExpensesManagementService — write через capital, read через
INTER_EXPENSE_CHASSIS (list по owner='capital'+action='onpgexpdone' +
карточка по hash). Никакого собственного TypeORM-mirror.
- ProgramExpensesResolver — chairman/member guards, mutation+query.
- DTO: CreateProgramExpenseInputDTO (items+statement registry 2010),
TopupProgramExpenseInputDTO, ProgramExpenseOutputDTO.
- Регистрация в capital-extension.module.
actual_amount/status в createProgramExpense инициализируются 0/0 серверно
(их C++ дефолтит, но cooptypes требует все поля IItem).
PR2 (Капитал на шасси).
Адаптация blagorost-cooptypes под архитектуру «capital = инициатор + callback,
шасси expense обслуживает весь flow». Из blagorost-E2 берётся минимальный набор
интерфейсов под программный расход, без debt/role/duplicate-action типов.
Изменения:
- ICreatepgexp: amount/description упрощены, добавлены operation_code+items[]
(передаются inline в шасси expense::createexp).
- ITopupprogexp — без изменений (пополнение пула).
- IOnpgexpdone — НОВЫЙ callback от шасси, payload (hash, status, total_actual, data).
- IApprvpgexp/IAuthpgexp/IPgexppay/IDeclpgexp — УДАЛЕНЫ (шасси expense замещает).
- IProgramExpense table-interface — сохранён.
- import IItem из expense interface для типизации items[].
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
capital теперь только инициатор+получатель результата для программного расхода —
весь flow обслуживает шасси expense. Раньше blagorost-ветка дублировала
approve/auth/pay/decline на стороне capital.
Изменения capital:
- удалены apprvpgexp/authpgexp/pgexppay/declpgexp (дубль шасси).
- createpgexp переписан: вместо самообслуживания резервирует пул и шлёт inline
expense::createexp с callback={capital, onpgexpdone}.
- сигнатура createpgexp принимает operation_code+items[] (передаётся в шасси).
- добавлен onpgexpdone(coopname, hash, status, total_actual, data) — callback от
шасси на CLOSED/DECLINED, обновляет program_expense_pool и удаляет progexpense.
Изменения expense (шасси):
- callback payload расширен полем status (ExpenseDomain::ProposalStatus) — инициатор
отличает CLOSED от DECLINED.
- declexp теперь шлёт callback тоже (раньше только closeexp).
Изменения names.hpp:
- удалён CAPITAL_RESOLVE_PROGRAM_EXPENSE / AUTHORIZE_PROGRAM_EXPENSE /
DECLINE_PROGRAM_EXPENSE / CONFIRM_PROGRAM_EXPENSE_PAYMENT.
- добавлен ON_PROGRAM_EXPENSE_DONE.
- добавлен External::CREATE_EXPENSE_PROPOSAL для inline в шасси.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Story 1.5 эпика «Благорост» (blago issue 51F-17).
Полный цикл целевого расхода программы «Благорост» (не привязанного к проекту):
от заявки председателя до подтверждения оплаты gateway. Источник средств —
program_expense_pool (Story 1.6), резерв снимается окончательно в pgexppay.
PRD-B1: «createprogexp/approveprogex/authprogexp/progexppay/declprogexp». Имена
действий сокращены под 12-символьный лимит EOSIO — семантика та же.
Изменения:
- expenses.hpp: struct program_expense + таблица progexpenses (scope coopname)
+ helper'ы create/get/set_program_approved/set_program_authorized/delete
- 5 новых cpp actions в expense_managment/program_expenses/:
- createpgexp — резервирует в State::reserve_program_expense, создаёт запись
- apprvpgexp — председатель → soviet::create_agenda с CAPITAL_RESOLVE_PROGRAM_EXPENSE
- authpgexp — совет → ::Gateway::create_outpay (через Action::send), статус AUTHORIZED
- pgexppay — gateway callback → consume_program_expense + Ledger2::apply PAY_EXPENSE + delete
- declpgexp — отказ от coopname/_soviet/_gateway → release_program_expense + delete
- names.hpp: CREATE_PROGRAM_EXPENSE, AUTHORIZE_PROGRAM_EXPENSE, DECLINE_PROGRAM_EXPENSE,
CONFIRM_PROGRAM_EXPENSE_PAYMENT, SovietActions::CAPITAL_RESOLVE_PROGRAM_EXPENSE
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Story 1.6 эпика «Благорост» (blago issue 51F-18).
Программные расходы (Story 1.5) списываются из дедикейтнутого пула, а не из
аллокаций проектов. До этого коммита поля для такого пула в global_state не было.
Изменения:
- global_state.program_expense_pool — доступная сумма программы под расходы
- global_state.program_expense_reserved — зарезервированная под approved/authorized
программные расходы (Story 1.5: createprogexp резервирует, declprogexp возвращает,
exppaycnfrm списывает окончательно)
- State::topup_program_expense_pool / reserve_program_expense /
release_program_expense / consume_program_expense — utility-функции для Story 1.5
- topupprogexp(coopname, amount) action — председатель переводит amount из
global_available_invest_pool в program_expense_pool; деньги остаются на
ledger2-кошельке BLAGOROST_FUND, меняется только разбивка по назначению
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Новый push в PR отменяет ещё бегущий typecheck той же ветки —
прежде 30-минутные прогоны копились очередью.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ExpensesAdminApprovePage — таблица proposals в status=CREATED (in-memory filter после ExpenseProposalsByCooperative).
ExpensesAdminAuthorizePage — таблица proposals в status=REPORT_SUBMITTED (тот же подход).
Обе по канону: PageHead + TableSkeleton + .table-wrap + .table-foot + EmptyState + переход в /expenses/:hash.
Mutation-действия (одобрить/авторизовать) появятся после расшивки Эпика 2 signature-pipeline на document2.
4/7 страниц #104 расшито.
8-я mutation симметрично authorizeExpenseReport: NotImplementedException
со ссылкой на statement_doc (type=2010) от signature-pipeline UI Эпика 2.
Регистрирует GraphQL-сигнатуру end-to-end, чтобы SDK + UI видели mutation
до подключения document2.
jest 8/8 passed.
7 тестов проверяют что каждое из 6 действий expense-контракта уходит в
BlockchainService.transact с правильным форматом:
account = ExpenseContract.contractName.production
name = ExpenseContract.Actions.<X>.actionName
authorization = [{actor: coopname, permission: 'active'}]
Защита от дрейфа cooptypes actionName → adapter имя. Также verify
HttpApiError 502 при отсутствии WIF в vault'е (initialize/transact не зовутся).
jest 7/7 passed.
Шасси расходов подключено к init-installed-extensions:
- expenses → expensesInstall в extensionsRegistry
- install.ts: убран TODO-комментарий о блокировке C28-31
Workspace 'expenses' (icon: receipt_long) с 6 routes (registry/admin/cashier/my)
становится доступен ролям chairman + user из конфигурации кооператива.
Stub-страницы внутри ждут расшивки через SDK Zeus после generate-client.
mono controller:
- AppsCatalogHttpService.fetchInstallScript(scope, name) — fetch text
install.js из ca-admin /v1/public/packages/:scope/:name/install.js;
degraded mode (null) при отсутствии env-config'а apps-catalog'а.
- Новый REST controller AppsCatalogInstallScriptController
`GET /v1/apps-catalog/install/:scope/:name` под HttpJwtAuthGuard:
валидирует scope/name regex'ом, проксирует на ca-admin, отдаёт
text/javascript + Cache-Control: no-store. Admin-API key остаётся
на backend'е.
- AppsCatalogProxyModule: добавлен controller в module.controllers[].
mono desktop:
- remote-loader.ts: реальный fetch + CJS-eval. fetchInstalledRemotePackages
читает Queries.Extensions.AppsCatalogRemotePackages, парсит packageId
на (scope, name). loadRemoteExtension качает install.js по REST через
axios responseType:'text' с JWT desktop'а, оборачивает в
`new Function('module','exports','require', code)`, вызывает
`module.exports.install()` → IWorkspaceConfig[].
- Try/catch per item (NFR15-стиль): падение одного пакета не валит
остальные. require() сразу throw — пакеты должны быть CJS без
внешних зависимостей (sandbox — Story 11.x).
- sha256 пока не проверяем (release-таблица будет в 9.4.c).
Demo path: voskhod publishes @voskhod/demo-app → ca-admin отдаёт
examples/demo-app/install.js → controller-proxy → desktop boot тянет +
eval'ит + получает workspace «Demo» с роутом demo-hello.
Покрывает 5 кейсов: REPORT_SUBMITTED/AUTHORIZED/DECLINED → no-op;
CLOSED без total_actual → warn; CLOSED + total_actual → log с
TODO-ссылкой на Эпик 0. Защищает контракт триггера на будущее, когда
skeleton будет заменён на реальный capital::createrid через
WriteMutationPool.
GraphQL surface: authorizeExpenseReport (председатель утверждает,
триггерит capital-trigger → capitalization Благороста) +
declineExpenseReport (с reason, переход в DECLINED, capitalization
НЕ запускается). Skeleton — NotImplementedException, chain-submit
после Эпика 0 (cooptypes regen для expense::authexp / declineexp).
При nullable=true декоратор @Field NestJS GraphQL не умеет вывести тип из
TS-reflection-метаданных (TS пишет в metadata-ключ Object). Без явного
type fn (() => String) — UndefinedTypeError на старте.
Фикс:
- @Field({ nullable: true }) → @Field(() => String, { nullable: true })
для lastActiveVersion: string | null.
- .env.example добавлены APPS_CATALOG_URL и APPS_CATALOG_API_KEY с
готовыми значениями для dev (URL = DNS-имя ca-admin в mono-shared
сети).
Verify: coopback стартует чисто (Nest application successfully started),
DTO виден через introspection (__type AppsCatalogRemotePackageDTO →
поля packageId/publisher/title/description/rubPerMonth/lastActiveVersion/
compatibleSubnets), guard срабатывает (без JWT → 401 Unauthorized).
Story 9.5.b mono часть: добавляет модуль расширения
`apps-catalog-proxy` с GraphQL Query.appsCatalogRemotePackages, который
проксирует запрос на apps-catalog/ca-admin /v1/public/packages.
Архитектурно — выбран альтернатива C (см. apps-catalog
docs/epics/E9-demo-app-pilot.md, V2 follow-ups): браузер ходит в каталог
через mono controller, а не напрямую в ca-admin. Поэтому:
- admin-secret apps-catalog'а живёт в env controller'а, не в SPA;
- нет CORS — desktop запрос на свой же origin;
- единый канон mono GraphQL (никаких raw HTTP в desktop).
Компоненты:
- AppsCatalogRemotePackageDTO (@ObjectType): packageId/publisher/
compatibleSubnets/lastActiveVersion + UI-поля title/description/
rubPerMonth (в V1 description и rubPerMonth — заглушки; в V2 будут из
package manifest и apps-catalog pricing).
- AppsCatalogHttpService: axios-клиент к ca-admin с Authorization: Bearer
admin-key из APPS_CATALOG_API_KEY. Degraded mode (пустой список без
ошибки), если env пустой — чтобы dev/CI без apps-catalog не валили
desktop boot.
- AppsCatalogProxyResolver: @Query, @UseGuards(GqlJwtAuthGuard) — каталог
видят только авторизованные пайщики.
- Регистрация в ExtensionsModule.
Config:
- Новые env APPS_CATALOG_URL и APPS_CATALOG_API_KEY (оба optional).
- Зеркальные поля в config.apps_catalog.{url, api_key}.
TODO (требует ручного запуска на остановленном контейнере, см. CLAUDE.md:
'pnpm generate-schema / generate-client — не запускать локально'):
- pnpm -F controller generate-schema → пересоздать schema.gql.
- pnpm -F controller generate-client → graphql-zeus в SDK.
- pnpm -F sdk build → unbuild dist.
Без codegen фронт ещё не сможет дёрнуть резолвер через Zeus.
ExpenseProposalSyncService override handleSyncDelta → emit
`entitysynced::expense::proposals` после save (per-contract pubsub,
canon controller/CLAUDE.md). ExpensesCapitalTriggerService подписан,
проверяет переход в CLOSED + total_actual → log skeleton. Прямой
DI на CapitalExtensionModule ждёт Эпика 0 (cooptypes regen +
WriteMutationPool для capital::createrid-эквивалента).
Секция «Удалённые пакеты» с карточкой @voskhod/demoapp (publisher,
price 1000 ₽/мес) + dialog «Подписаться» (V1 — заглушка).
Why: Анна видит реальный второй раздел, отделённый от bundle-расширений,
и проходит UX-сценарий клика «Подписаться» — без backend-резолверов.
Реальный список remoteExtensions через Query.appsCatalogRemotePackages
+ Mutation.appsCatalogSubscribe — отложено в 9.5.b (см. mono#64
follow-ups).
Стили — токены MONO Design System (--p-*), никаких magic hex.
V1 минимум для UI ExtensionStore:
- Информационный banner сверху grid'а карточек, объясняющий что bundle-
extensions ниже бесплатные, а remote-пакеты с подпиской появятся
на следующем шаге пилота.
- Реальная карточка remote-пакета с кнопкой «Подписаться» / «Уже
подписан до DD.MM» — story 9.5.b (требует расширения extStore
на kind/subscribed/pricingRubPerMonth + backend-резолверы coopback'а).
Refs: docs/integration/native-to-catalog-research.md §9 story 9.5.
9.3 — components/desktop/extensions/developer/:
- install.ts: workspace `developer`, defaultRoute=developer-my-packages,
4 страницы (packages, publish, subscribers, pricing), все under
meta.roles=['chairman']. Регистрация в extensions-registry.ts.
- pages/: V1 Vue-заглушки с описанием TODO (без реальных мутаций).
Mutation flow (POST /v1/admin/package, /releases, /pricing с подписью
chairman'а) — story 9.3.b.
9.4 — processes/init-installed-extensions/remote-loader.ts:
- Контракт fetch/load для remote-пакетов с активной подпиской: тип
RemoteExtensionDescriptor, функции fetchInstalledRemotePackages,
loadRemoteExtension, installRemoteExtensions.
- V1 — заглушка возвращающая []. Не падает, логирует пропуск.
- index.ts: после bundle-pass — remote pass через installRemoteExtensions;
try/catch чтобы падение remote-flow не валило bundle extensions.
- TODO 9.4.b: реальный fetch к coopback + tar-x + dynamic require
+ sha256-check + (V2 sandbox).
Видимость стола: пока через meta.roles в install.ts. Полноценная
авторизация через DesktopWorkspace.grants с флагом is_operator_coop —
9.3.b/9.5.
Refs: /home/admin/mono-ai-5/docs/integration/native-to-catalog-research.md §9.
Финальные решения по V1 пилоту (без миграции native):
- demo-app = пустой Hello-стол, @voskhod/demo-app
- Pricing 1000 RUB/мес (60 сек на dev), trial 15 мин
- partner1 как покупатель через pnpm boot:extra (на той же цепи)
- Self-subscription bypass: воскод как оператор подписан бесплатно
- Без кэша tarball'ов — каждый desktop boot качает заново
- Стол Разработчика = новый bundle-extension developer, виден chairman оператора
- partner1 desktop+coopback в отдельном docker overlay из apps-catalog
Epic 9 раскладка на 8 stories с зависимостями. Реализация — следующими
коммитами в зонтичной ветке apps-catalog mono + feat/E9-demo-app-pilot
в apps-catalog репо.
html2pdf.js на верхнем уровне модуля трогает `self` → при SSR-бандле Quasar
падает [Quasar] boot error: ReferenceError: self is not defined. Экспорт PDF
нужен только на клиенте по действию пользователя — переносим импорт в
dynamic import() внутри exportFormToPdf, сервер модуль не вычисляет.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Были перекрещены относительно реестра документов: draft 3=PrivacyPolicy,
4=UserAgreement (и тексты on-chain drafts, и cooptypes Registry, и protocol-vars
совета это подтверждают). Только make_base_coagreements крестил user↔privacy.
Затрагивает вновь создаваемые коопы; существующим нужна отдельная миграция.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
getNotifications/getNotification/resendNotification сверяют coopname
(фильтра и найденной строки) с config.coopname → ForbiddenException на чужой.
Канон-паттерн контроллера (free-decision/branch interactor). Federation-hardening.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Документ остаётся в скроллящемся body BaseDialog, кнопка вынесена в слот
footer (.base-dialog__foot, вне overflow) — устраняет залипание, когда до
кнопки нельзя домотать (стек maximized-диалогов RequireAgreements).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Code-review нашёл: --p-elev-2 нет в tokens.css → box-shadow не отрисовывался.
Канон-тень overlay (поповер) — --p-shadow-pop.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
E2E.md в expenses/:
- Journey A (DIRECT-оплата Yandex Cloud из BLAGOROST_POOL): 10 шагов + matrix ledger2/document2 assertions
- Journey B (ADVANCE master+svetlana 2 items): 9 шагов + matrix
- Helpers-skeleton (loginAs, seedBlagorostPool, mockCouncilAuthorize, fixture*, assertLedgerOp/Document2)
- Performance gates ≤ 3 мин на сценарий
- Прежде чем писать — спросить про Cypress vs Playwright (канон-naming-soglasovyvat)
- Зависимости + дорожная карта расшивки
E2E framework в desktop отсутствует; реальные spec'и пишутся после C28-28..C28-33 и согласования инфраструктуры.
INTEGRATIONS.md в expenses/:
- BLAGOROST_POOL flow через существующий capital::createprogexp (никаких новых contract-actions)
- Каталог 9 универсальных событий шасси для Notification Center
- Backward-compat редиректы /programs/blagorost/expenses → /expenses?source=BLAGOROST_POOL
- Cleanup-чеклист legacy program-expense: на ветке пусто (re-grep ProgramExpense → 0 файлов; PR #59 не залит)
- Шаблоны 1010/1011 — не существуют, удалять нечего; роль закрывают 2010/2011 из C28-30
- Расшивка после закрытия C28-29/C28-31/C28-32
Не трогает рабочие сервисы (только новый .md в extensions/expenses/, не зарегистрировано в extensions-registry).
C28-31 (backend extension шасси расходов) логически блокируется C28-29 (контракт expenses) и C28-28 (P0 коды ledger2). Чтобы зонтичный PR не простаивал и следующий цикл шёл механически — кладём README с планируемой структурой файлов, MinIO-спекой бакета `expenses:files` и черновой GraphQL-схемой.
Когда расшивается C28-28 → C28-29 → этот README раскрывается в код по списку.
Реализация Эпика 2 шасси системы расходов (волна 6 проекта 14).
cooptypes/cooperative/registry/:
- 2010.ExpenseProposalStatement — СЗ-смета с массивом items (description, amount,
recipient_type SELF/MEMBER/ORG, mechanics ADVANCE/DIRECT, recipient_name, requisites),
заголовком proposal (description/total_amount/items_count/source_wallet). LiquidJS
context с таблицей позиций, переводы ru.
- 2011.ExpenseProposalDecision — протокол-1 утверждения/отказа председателем.
Поля decision {kind: approve|decline, reason?, protocol_number?, protocol_date?},
переиспользование IExpenseItem/IExpenseProposalHeader из 2010 через cross-import.
factory/src/:
- Schema/ExpenseItemSchema — JSONSchemaType для ExpenseItem, ExpenseProposalHeader,
ExpenseProposalDecisionBody.
- Templates/2010+2011 — обёртки по образцу 1010 ExpenseStatement / 1011 ExpenseDecision,
но с массивом items в Schema.
- Actions/2010+2011 — генераторы pdf через DocFactory с маппингом meta+coop+user+vars
+proposal+items[+decision].
factory/test/documents-1000-plus.test.ts:
- 3 golden-pdf теста (2010 СЗ, 2011 approve, 2011 decline + reason).
Legacy 1010/1011 НЕ удалены — будут выпилены в C28-33 после миграции backend/UI
на новые ID (чтобы не порвать PR #55 на марафоне рефакторинга).
Решение 2026-06-02: только 2 шаблона document2 в MVP (2010, 2011); остальные слоты
(payment_proof, report_files, return_proof, протокол-2) — файлы в MinIO/expense_files
без factory-цепочки. Тяжёлая document2-цепочка нужна только для юридически-весомых
актов СЗ-смета + протокол совета; первичные документы внешнего происхождения и
events приёмки идут отдельным механизмом.
Refs: C28-30, PRD f8 раздел 8.16 + override 2026-06-02, образцы 1010.ExpenseStatement
+ 1011.ExpenseDecision.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Эпик 6.5: страница «Журнал уведомлений» (Chairman desktop, /notifications-journal).
Канон-таблица .table-wrap (дата/получатель/тип/канал/статус/попытки), FilterBar по
каналу и статусу, BaseBadge-статусы, кнопка «Переотправить» на строке (resend →
новая строка в очереди, перечитываем). TableSkeleton/EmptyState/«Загрузить ещё».
Имя типа уведомления — из каталога Workflows. Только канон-компоненты + токены.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Эпик 6.4: виджет NotificationCenter переписан с Novu UI на доменный канон-компонент
shared/ui/domain/NotificationCenter (колокол .icon-btn + BaseBadge-счётчик + q-menu).
Pinia-стор: лента инбокса/unreadCount/markRead/markAllRead через SDK, оптимистичные
отметки, поллинг непрочитанных 30с (live-подписки нет — опрос), тост на новое.
Категория группировки выводится из workflowId. Удалён @novu/js и NOVU_* из env.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Эпик 6.6 (backend-часть): личный инбокс канала In-app поверх notification_inbox.
getInboxNotifications (лента по сессии, пагинация канон), getUnreadNotificationsCount
(бейдж колокола), markNotificationRead/markAllNotificationsRead. Скоуп строго по
subscriber_id из JWT — пайщик видит только свой инбокс (ownership на markRead).
Live-подписка onNotification отложена: graphql-ws/PubSub в репо нет — фронт poll'ит.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop-in переключение ~28 call-sites в 15 сервисах с
NOVU_WORKFLOW_PORT.triggerWorkflow на NOTIFICATION_PORT.notify (Центр
уведомлений DC v3). Listener'ы/события не тронуты — меняется только тело
вызова: name→workflowId, coopname обязателен (action.coopname/config.coopname),
recipient дополнен username (адресация in-app/web-push).
NotificationCenterModule помечен @Global — порт cross-cutting, консьюмеры
инжектят без импорта модуля (как NOTIFICATION_SUBSCRIPTION_PORT).
Cross-plugin InterCoopCalendarEventNotificationPort (chatcoop) сохранён,
мигрировано лишь тело. Из application NotificationSenderService убраны мёртвые
Novu-измы (broadcast/cancel/getStatus) — не использовались.
tsc: 0 новых ошибок (17 = baseline). NOVU_WORKFLOW_PORT-биндинг остаётся
мёртвым до удаления в эпике 5.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
OutboxWorkerService @Interval(3000): поллит notification_outbox по индексу
(status, scheduledAt), шлёт через порт канала, исход пишет в журнал
notification_deliveries. at-least-once: backoff-ретрай (5s→30s→2m→10m→1h,
maxAttempts), после исчерпания — терминальный failed (переотправка на столе
председателя). Реклейм зависших SENDING после рестарта. Строка = один канал →
независимые попытки по каналам. Статусы — enum. ScheduleModule.forRoot()
подключён в app.module. Параметры worker'а из env с дефолтами.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
InAppChannelAdapter за InAppChannelPort: доставка in-app = запись
отрендеренного из каталога уведомления в notification_inbox. Новая
сущность notification_inbox (isRead/readAt, индексы под ленту + unreadCount).
ChannelMessage расширен outboxId + actorSubscriberId (worker даёт из outbox).
Live-подписка/unreadCount/mark-read — read-side инбокса, в эпик 6.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
EmailChannelAdapter (nodemailer, SMTP из config.email; disabled при пустом
SMTP_HOST, наружу не стучится — NFR11). WebPushChannelAdapter: контроллер
доставляет сам своим подписчикам напрямую через WebPushService на endpoint'ы
из web_push_subscriptions по username (фикс пушей — точки доставки, не ключи);
410 → деактивация подписки. Шаблоны рендерятся из каталога @coopenomics/
notifications (controlValues.subject/body), mustache-интерполяция {{payload.x}}
без liquidjs (лишняя зависимость для MVP). Порты EMAIL_CHANNEL_PORT/
WEB_PUSH_CHANNEL_PORT в NotificationCenterModule.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Контракты EmailChannelPort/InAppChannelPort/WebPushChannelPort + ChannelMessage/
ChannelDeliveryResult + DI-токены. Только интерфейсы — реализации в эпике 2;
worker эпика 3 зависит от абстракций, не от провайдеров. Имена без I-префикса
(канон портов репо).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
NotificationService implements NotificationPort: резолвит каналы по типу из
каталога @coopenomics/notifications (steps[].type, только email/in_app/push),
для каждой пары получатель×канал пишет строку в notification_outbox идемпотентно
(ON CONFLICT DO NOTHING по unique idempotencyKey). Опциональный EntityManager —
запись в транзакции вызывающего (story 1.3). idempotencyKey = sha256(coopname|
workflowId|subscriberId|channel|stable(payload)) с детерминированной сериализацией
payload (story 1.5). email-канал — только при подтверждённом email (gate).
Модуль зарегистрирован в app.module, экспортирует NOTIFICATION_PORT.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Сущности notification_outbox (очередь recipient×channel, hot-path индекс
(status, scheduledAt), unique idempotency_key) и notification_deliveries
(append-журнал попыток, индексы по coopname/recipient для стола председателя
и личного инбокса). Статусы — enum, не строки. Фундамент для роутера (1.3),
worker'а (эпик 3) и стола председателя (эпик 6).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Порт notify(NotifyInput) + типы входа (coopname обязателен как federation-инвариант,
workflowId ссылается на каталог @coopenomics/notifications, NotificationChannel enum
email/in_app/push). Старый NovuWorkflowPort сохраняется до миграции call-sites (эпик 4),
удаляется в эпике 5. Часть истории 1.1 эпика 1 (C28-22).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Дефолт реестра совета был newresolved → status=Resolved → виден только
утверждённый документооборот, submitted-документы (в т.ч. свежеподанные
заявления) пропадали. Переключатель вкладок на странице скрыт
(showFilter=false), сменить вид было нельзя. Выравниваем с реестром пайщика:
newsubmitted → status не ограничивается → совет видит ВСЕ документы кооператива.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
freedecision эмитит newresolved и newdecision по одному package в одной
транзакции — оба @OnEvent-хендлера срабатывают одновременно. safeReassemble
(newdecision/newact/newlink) пере-upsert'ил все строки пакета ПОЛНЫМ набором
колонок со сталым row.status, прочитанным до коммита newresolved → затирал
resolved обратно в submitted. Решение совета застревало в submitted и пропадало
из реестра совета (дефолт «Только утверждённые» = status=Resolved).
Пересборка теперь обновляет ТОЛЬКО агрегат-производные колонки
(document_aggregate/full_title/content_text/signers_text) точечным UPDATE.
status/block_num/source_action_data принадлежат статус-событиям и пересборкой
не трогаются → конкурентный newresolved не откатывается. Новый метод
SignedDocumentRepository.updateAggregate + SignedDocumentAggregateUpdate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Кнопка «Утвердить» совета слала authorize+exec напрямую из браузера ключом
председателя — при HTTP 504 от ноды ошибка терялась в браузере, бэкенд её не
видел и не логировал. Переводим действие на проведение через контроллер ключом
кооператива, как остальные soviet-мутации (sendAgreement) — теперь ошибки связи
с блокчейном попадают в логи бэкенда.
Контракт soviet: authorize/exec require_auth(chairman/executer) → require_auth(coopname).
Согласие председателя не теряется — закреплено его личной подписью на document2
(verify_document_or_fail) + проверкой is_valid_chairman.
- contracts: authorize.cpp/exec.cpp — require_auth(coopname)
- controller: новая мутация authorizeDecision (DecisionAuthorizeModule) + порт/адаптер,
пушит authorize+exec одной транзакцией ключом кооператива из vault
- cooptypes: authorizations actor chairman/username → coopname
- sdk: Mutations.Decisions.AuthorizeDecision + регенерация schema.gql/zeus
- desktop: фича AuthorizeAndExecDecision и process-decisions зовут бэкенд-мутацию
вместо useGlobalStore().transact; подпись документа остаётся клиентской
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Что сделано:
- Перенёс canon-стили card-label/card-value/section-title локально в scoped
блоки каждого реального потребителя (26 файлов).
- 7 файлов которые казались USE-ONLY оказались false-positive substring
совпадений (.resource-info-card / .meet-info-card / .membership-info-card /
.tariff-card-container / .cmdk-empty-state / .import-wizard__section-title /
.result-section-title) — глобальные классы info-card/card-container/
empty-state/section-title для них уже не нужны.
- Все 8 живых классов из legacy-stylers.scss (.card-label/.card-value/
.section-title/.card-container/.info-card/.empty-state/.section-header/
.title-container) теперь имеют ноль use-only потребителей: каждый файл
имеет либо local scoped def, либо использует другой класс с похожим именем.
- Удалил `src/css/legacy-stylers.scss` целиком.
- Убрал `'legacy-stylers.scss'` из css array в `quasar.config.cjs`.
Метрики:
- 26 файлов получили scoped-блоки (~6-10 строк canon-токенов в каждом).
- 1 файл удалён (legacy-stylers.scss, 134 строки).
- 1 импорт убран (quasar.config.cjs).
Sanity:
- Все 26 scoped scss-блоков прокомпилированы через sass.compileString — OK.
- false-positives откатил через git checkout HEAD.
Файл legacy-stylers.scss больше не существует.
Долг wave1/2/3 закрыт.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Что сделано:
- Снёс 8 классов с 0 потребителей: info-card-hover, page-main-card,
card-title, card-action-btn, balance-card{,-primary,-warning},
info-warning-card, section-card-warning (со всеми вложенными
.balance-label/.balance-value/.warning-info/.section-content/...).
- Снёс глобальный .profile-section — единственный потребитель
(MicroWallet) определяет его локально в <style scoped>.
- Перевёл живые классы на canon-токены var(--p-*):
• title-container — layout-only, токенов не требует.
• card-container — radius/border/bg через --p-r-lg/--p-line-1/--p-surface.
• info-card — surface-2/line-1/r-md/p-4/p-3.
• section-header + section-title — h2 (--p-fs-h2), color --p-ink,
section-icon margin --p-3, header margin-bottom --p-6.
• card-label/card-value — --p-fs-body/--p-ink-2 vs 16/500/--p-ink.
• empty-state — p-8/p-5/p-4/p-2/h2/body/ink-1/ink-2.
- Убрал ручные dark-overrides (.body--dark &, .q-dark &) —
canon-токены сами переключаются через [data-theme="dark"] в tokens.css.
- Media @max-width:768px ужал до одного правила (мобильный h3 для секций).
Метрика: 433 → 56 строк (-377/+56), CSS-выхлоп 2.1 КБ.
Не тронуто:
- Все 22-23 потребителя card-value/card-label — большинство определяет
стили локально (LOCAL), 5 USE-ONLY компонентов наследуют глобальный.
- 13 потребителей section-title — 6 LOCAL, 7 USE-ONLY.
Долг: переписать USE-ONLY компоненты на canon-виджеты (ColorCard / BaseCard),
после чего глобальные card-* и section-* можно полностью снести
вместе с этим файлом.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 13:57:49 +00:00
849 changed files with 41946 additions and 19753 deletions
**Базовая ветка mono-ai-1 — `dev`.** Прямые коммиты в `dev` для мелких фиксов разрешены и не требуют feature-ветки/PR (отменено 2026-05-22 пользователем). Для крупных задач — feature-ветка (`feat/...` или `fix/...`) от dev и PR в dev; merge делает пользователь на GitHub. Push в `main` / `mvp` — по-прежнему только через PR.
**Релиз/деплой — FF-промоушн `dev → testnet → main`** (см. `scripts/RELEASE.md`). Версию бампает `lerna` ОДИН раз на dev (`scripts/cut-release.sh`), тот же коммит едет вверх по fast-forward (`scripts/promote.sh testnet|main`). `testnet`/`main` не несут своих коммитов — только FF-указатели, поэтому merge-конфликтов нет. **В `testnet`/`main` прямых коммитов и повторных бампов версии быть не должно** — это ломает FF. Деплой триггерит push в testnet/main с изменением `lerna.json`; окружение определяет ветка (main → prod). Старые `publish-alpha.sh`/`publish-prod.sh` (merge `-X theirs` + per-branch бамп + back-merge) удалены — они и плодили 20-package.json конфликты.
**Не 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)
**ПРИ ЛЮБОЙ ВЁРСТКЕ В `components/desktop/` НЕ ВЫДУМЫВАТЬ СТИЛИ.** Всегда сначала свериться с каноном. Не строй гипотез о цветах/радиусах/типографике/паттернах из памяти — иди и читай.
**Два источника правды (именно в этом порядке):**
**Полная спецификация канона — в skill `/mono-desktop-canon`** (правила обёрток, цвета/токены, иконки, структура страницы, stop-signals). Здесь — короткая выжимка; при расхождении побеждает skill.
1.**HTML-канон дизайн-системы** — единственный SoT для визуала (цвета, токены, радиусы, тени, типографика, патерны компонентов):
**Источник истины — в самом репозитории** (НЕ внешние HTML/прототипы):
| **Живой эталон** (`/_dev/ui` в dev-сборке) | `components/desktop/src/pages/_dev/ui/index.vue` |
Это самодостаточный HTML с всеми CSS-токенами `--p-*`, образцами компонентов (`.base-button`, `.base-card`, `.toast`, `.chip`, `.banner` и т.д.), цветовой палитрой и demo-блоками. **При сомнении в каноне — открыть этот файл и читать.** Не путать с`auth-prototype/index.html`, не путать с`ux-design-specification.md` — они НЕ канон.
2.**Живая реализация канона** в самом репозитории — `/_dev/ui` страница:
`components/desktop/src/pages/_dev/ui/index.vue`
Это уже заверстанная по канону witness-страница со всеми base-/widget-компонентами в действии (BaseButton, BaseInput, BaseCard, BaseChip, EmptyState, Toasts, AppHeader/AppDrawer фрагменты, WalletCard, IdentityPanel, DocumentRow, SignatureCard, ActivityTimeline, KpiCard, VerticalStepper и т.д.). **Когда делаешь новый компонент — сначала посмотри как уже сверстаны соседние в этой странице, используй те же токены / классы / структуру.** Эту страницу можно открыть в браузере (`/_dev/ui` в dev-сборке) — она же служит и для визуальной верификации.
**При сомнении — открыть `tokens.css` и `_dev/ui/index.vue`, смотреть как сделано там.** Внешний `shared/MONO Design System.html` и `auth-prototype/` каноном НЕ являются — это устаревшие прототипы.
**Запреты:**
-Никаких magic-цветов в hex/rgb прямо в `<style>` Vue-файла — только токены `var(--p-*)` (см. `components/desktop/src/css/mono-platform/*.css`).
-Никаких локальных переопределений`.q-btn`/`.q-card`/`.q-notification` в feature-файлах — все Quasar-overrides централизованно в `components/desktop/src/css/mono-platform/quasar-canon.css`.
-Никаких ад-хок размеров — отступы/радиусы/typescale из токенов (`--p-1`..`--p-12`, `--p-r-sm/md/lg`, `--p-fs-h1..body-sm`).
-Экран собирается из готовых компонентов: `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-эквивалент попутно.
- Никаких локальных переопределений `.q-btn`/`.q-card`/`.q-notification` в feature-файлах — Quasar-overrides централизованы в `quasar-canon.css`.
**Кейс 2026-05-28:** после компакта забыл, что shared/MONO Design System.html — основной SoT, и нашёл «канон» в auth-prototype, который НЕ канон; перевёрстал тосты под нейтральный фон вместо тёмного, пришлось переделывать.
**Кейс 2026-05-28:** при сомнении в каноне пошёл искать его во внешнем HTML/auth-prototype вместо репозитория — перевёрстал не туда, переделывал. SoT — репо (`tokens.css` + `_dev/ui`) и skill `/mono-desktop-canon`, не внешние файлы.
inlineconstexpreosio::namePUT_MINSHARE="o.reg.putmin"_n;///< Внесение минимального паевого при регистрации (Dr 51 / Cr 80, ISSUE MIN_SHARE_FUND). Одношаговый путь adduser (без совета).
// Двухфазный путь через совет (reguser → confirmpay → confirmreg/declinereg):
inlineconstexpreosio::nameRECEIVE_PAYMENT="o.reg.inpay"_n;///< Приём регистрационного взноса кассой в ожидание решения совета (Dr 51 / Cr 76, ISSUE REGISTRATION_PENDING).
inlineconstexpreosio::nameSETTLE_MINSHARE="o.reg.setmin"_n;///< Зачисление минимального паевого по решению совета (Dr 76 / Cr 80, TRANSFER REGISTRATION_PENDING → MIN_SHARE_FUND).
inlineconstexpreosio::nameSETTLE_ENTRANCE="o.reg.setent"_n;///< Зачисление вступительного по решению совета (Dr 76 / Cr 86, TRANSFER REGISTRATION_PENDING → ENTRANCE_FEES).
inlineconstexpreosio::nameREFUND="o.reg.refund"_n;///< Возврат регистрационного взноса при отказе совета (Dr 76 / Cr 51, BURN REGISTRATION_PENDING — деньги уходят из системы банковским переводом кандидату).
inlineconstexpreosio::nameMOVE_MINSHARE="o.reg.mvmin"_n;///< Перенос минимального паевого на главный паевой при выходе из кооператива (TRANSFER MIN_SHARE_FUND → SHARE_FUND_PAY, без Dr/Cr — оба кошелька на счёте 80). Готовит полный паевой к возврату.
}
// wallet
@@ -80,6 +86,7 @@ namespace operations {
inlineconstexpreosio::nameWITHDRAW_FROM_CAPITAL="o.cap.wthcap"_n;///< Возврат паевого из ЦПП «Благорост» в кошелёк пайщика (TRANSFER BLAGOROST_FUND → SHARE_FUND_PAY, без Dr/Cr).
inlineconstexpreosio::nameCONVERT_TO_SHARE="o.cap.cnvshr"_n;///< Конвертация сегмента: РИД → главный кошелёк (TRANSFER GENERATOR_FUND → SHARE_FUND_PAY, без Dr/Cr — бухпроводка уже была сделана в ACCEPT_RID).
inlineconstexpreosio::nameCONVERT_TO_BLAGO="o.cap.cnvbl"_n;///< Конвертация сегмента: РИД → ЦПП «Благорост» (TRANSFER GENERATOR_FUND → BLAGOROST_FUND, без Dr/Cr — бухпроводка уже была сделана в ACCEPT_RID).
inlineconstexpreosio::namePROGRAM_EXPENSE_TOPUP="o.cap.pgtop"_n;///< Пополнение пула программных расходов из инвестиций программы (ISSUE PROGRAM_EXPENSE_POOL, без Dr/Cr — деньги уже на 51, выделяется кооперативный резерв под расходы; паевые L3-кошельки пайщиков не трогаются).
}
// marketplace
@@ -93,6 +100,27 @@ namespace operations {
inlineconstexpreosio::nameCONVERT_AXN="o.sov.axncnv"_n;///< Трансляция паевого взноса в членский (Dr 80 / Cr 86, TRANSFER SHARE_FUND_PAY → DELEGATE_FEES).
}
// expense — шасси расходов (MVP: только Благорост; хозрасходы из членских — отдельный эпик).
//
// Принципы (см. components/desktop/extensions/expenses/NAMING-C28-28.md):
// - Контракт `expense` универсальный: operation_code передаётся в payload.
// - Источник оплат — КООПЕРАТИВНЫЙ пул расходов (PROGRAM_EXPENSE_POOL),
// - При расходе из Благороста паевой фонд (80) НЕ трогается: меняется только форма
// актива 51 → 08 (Дт 08 / Кт 51 для обеих механик).
// - ADVANCE-отчёт (`o.exp.advrpt`) НЕ создаёт новой бухпроводки: проводка уже
// сделана на `o.exp.blgadv` при выдаче.
// - Callback на финализацию — переменная (`callback{contract, action, data}`),
// заполняется при `expense::createexp`; expense ничего не знает про capital.
namespaceexpense{
inlineconstexpreosio::nameBLAGO_ADVANCE="o.exp.blgadv"_n;///< Выдача подотчётных из пула расходов (TRANSFER PROGRAM_EXPENSE_POOL → ADVANCE_HOLD, Dr 08 / Cr 51).
inlineconstexpreosio::nameBLAGO_DIRECT="o.exp.blgdir"_n;///< Прямая оплата из пула расходов (BURN PROGRAM_EXPENSE_POOL, Dr 08 / Cr 51).
inlineconstexpreosio::nameADVANCE_REPORT="o.exp.advrpt"_n;///< Закрытие подотчёта пайщика (BURN ADVANCE_HOLD, без бухпроводки — canal 08/51 уже сделан на blgadv).
inlineconstexpreosio::nameOVERSPEND="o.exp.over"_n;///< Доплата сверх подотчёта (TRANSFER PROGRAM_EXPENSE_POOL → ADVANCE_HOLD, Dr 08 / Cr 51); сразу за ней expense вызывает ADVANCE_REPORT.
}
// migration (только из migrate.cpp)
//
// В OPERATION_REGISTRY включены **только** те транзиты, которые проводятся
inlineconstexpreosio::nameACCEPT="p.reg.accept"_n;///< Приём пайщика в кооператив (o.reg.payent + o.reg.putmin).
inlineconstexpreosio::nameACCEPT="p.reg.accept"_n;///< Приём пайщика в кооператив (o.reg.payent + o.reg.putmin; для потока через совет — o.reg.inpay + o.reg.setmin + o.reg.setent).
inlineconstexpreosio::nameREFUND="p.reg.refund"_n;///< Возврат регистрационного взноса при отказе совета (o.reg.refund). Отдельный процесс: приём взноса прерывается, начинается возврат.
inlineconstexpreosio::namePREIMP="p.cap.preimp"_n;///< Первичный учёт РИД-взноса до перехода на электронный учёт (одноактовый, anchor = preimp register hash).
inlineconstexpreosio::nameWTHCAP="p.cap.wthcap"_n;///< Возврат паевого из ЦПП «Благорост» в кошелёк пайщика (одноактовый).
inlineconstexpreosio::namePGEXP="p.cap.pgexp"_n;///< Пул программных расходов: пополнение из инвестиций программы (o.cap.pgtop, одноактовый).
}
// marketplace
@@ -60,6 +62,11 @@ namespace processes {
inlineconstexpreosio::nameREQUEST="p.mkt.reqst"_n;///< Цикл запроса маркетплейса (o.mkt.supply + o.mkt.recv).
staticconstexpreosio::nameWITHDRAWALS_SINK="w.wal.wthdrw"_n;///< DEPRECATED 2026-05-21: исторический sink возвратов. Оставлен в реестре для исторических L2-балансов (накопленные возвраты до перехода). Не использовать в новых операциях.
staticconstexpreosio::nameWITHDRAW_PENDING="w.wal.wpend"_n;///< Резерв паевого под заявку на возврат (COOPERATIVE-пул). o.wal.wthreq переводит сюда с w.wal.share, o.wal.wthdec возвращает обратно, o.wal.wthcpl сжигает отсюда. Заменил механику blocked/BLOCK/UNBLOCK 2026-05-24.
staticconstexpreosio::nameREGISTRATION_PENDING="w.reg.pend"_n;///< Регистрационный взнос в ожидании решения совета (суспенс счёта 76, USER_SHARED, без сверки соглашений — кандидат ещё не член). o.reg.inpay → сюда (Dr 51/Cr 76); o.reg.setmin/setent уносят на 80/86; o.reg.refund сжигает (Dr 76/Cr 51).
staticconstexpreosio::nameINFRA_FEES="w.sov.infra"_n;///< Членские взносы за инфраструктуру кооп. платформы (COOPERATIVE)
@@ -64,9 +65,16 @@ struct ledger2_wallets {
staticconstexpreosio::nameBLAGOROST_FUND="w.cap.blago"_n;///< Благорост — единый агрегированный кошелёк программы (USER_SHARED; ADR-009)
staticconstexpreosio::nameGENERATOR_FUND="w.cap.gen"_n;///< Генератор — единый агрегированный кошелёк программы (COOPERATIVE — кооперативный пул, без L3-разреза по пайщику; L3-разрез из ADR-009 отменён из-за несовместимости с CRPS-перераспределением, см. wallets.hpp:107)
staticconstexpreosio::namePREIMP_FUND="w.cap.preimp"_n;///< Первичный учёт РИД-взносов до перехода на электронный учёт (USER_SHARED; o.cap.preimp / o.cap.drppre)
staticconstexpreosio::namePROGRAM_EXPENSE_POOL="w.cap.pgexp"_n;///< Пул программных расходов ЦПП «Благорост» (COOPERATIVE) — кооперативный кошелёк, из которого шасси expense оплачивает СЗ; пополняется topupprogexp (o.cap.pgtop), паевые L3-кошельки пайщиков (w.cap.blago) при расходах не трогаются
'развитию информационной экосистемы взаимодействия физических и юридических лиц, на основе международных кооперативных принципов и законодательства Российской Федерации в отношении потребительских обществ (кооперативов) под названием “Кооперативная Экономика”',
eoap_definition:
'информационная экосистема, интегрируемая в социально-экономическую среду Российской Федерации, состоящая из комплекса программных продуктов на базе технологии распределенного реестра, обеспечивающих широкое экономическое и социальное взаимодействие физических и юридических лиц, включая нерезидентов различных юрисдикций и организационно-правовых форм, на основе международных кооперативных принципов и законодательства Российской Федерации в отношении потребительских кооперативов (обществ) под названием “Кооперативная Экономика”',
generator_task_goal:
'центра привлечения и интеграции передовых инновационных цифровых разработок, а также экономических и социальных методов и решений',
idea_unit_cost:'50',
idea_unit_cost_words:'пятьдесят',
blagorost_goal_expansion:
'вследствие увеличения количества Участников информационной кооперативной экосистемы - ЕОАП - для расширения и повышения социальной эффективности их экономического взаимодействия в некоммерческом формате',
blagorost_task_expansion:
'расширение участников ЕОАП - информационной кооперативной экосистемы как центра экономического взаимодействия в некоммерческом формате',
blagorost_task_development:
'развитие ЕОАП как центра привлечения и интеграции инновационных цифровых разработок, а также экономических и социальных методов и решений',
return_source_description:
'аппаратно-программная сеть узлов распределенного реестра в формате «СМЭВ+SWIFT», построенная на принципах самоорганизации и самофинансирования деятельности технологической инфраструктуры ЕОАП, обеспечивающей консенсус ее распределенных узлов по формированию базового продукта ЕОАП - полного цикла документооборота по синхронному взаимодействию пайщиков и кооперативов - участников экосистемы ЕОАП - через использование цифровых контрактов, с одновременным выполнением функций нотариата и учета финансовых и юридических событий',
return_additional_source:
'взносы пользователей ЕОАП, его отдельных программных продуктов и приложений, переданных Обществу или создаваемых в рамках Общества, которые интегрируются в ЕОАП',
'сводка по вступительному (регистрационному) платежу пайщика. Позволяет восстановить шаг регистрации (ожидание решения совета или отклонение платежа) после перезагрузки страницы и в любой вкладке.',
'Удалить аккаунт пайщика из системы учёта провайдера. Доступно только для незавершённых регистрационных статусов (черновик, неоплачен/отклонён). Активный, заблокированный и любой зарегистрированный в блокчейне аккаунт удалить нельзя. Используется для очистки реестра и освобождения e-mail под перерегистрацию.',
})
@UseGuards(GqlJwtAuthGuard,RolesGuard)
@AuthRoles(['chairman'])
asyncdeleteAccount(
@Args('data',{type:()=>DeleteAccountInputDTO})
data: DeleteAccountInputDTO
):Promise<boolean>{
awaitthis.accountService.deleteAccount(data);
// return true;
// }
returntrue;
}
@Mutation(()=>AccountDTO,{
name:'resetRegistration',
description:
'Откатить собственную незавершённую регистрацию к редактированию данных: снимает заморозку профиля и e-mail, сбрасывает подписанное заявление и непринятую попытку вступительного платежа. Доступно только до отправки регистрации в блокчейн; если взнос уже принят — требуется возврат средств.',
@@ -88,16 +88,25 @@ export class BlockchainDecisionDTO implements SovietContract.Tables.Decisions.ID
@Field(()=>String,{nullable: true})
hash!:string;
@Field(()=>Number,{
description:
'Текущее число членов совета (всего, как считает контракт). Нужно фронту для вычисления порога принятия/отклонения: за/против * 100 > council_members_count * 50',
description:'Опубликовать предложенную повестку и проект решения для дальнейшего голосования совета по нему',
nullable: true,
description:
'Опубликовать предложенную повестку и проект решения для голосования совета. Возвращает созданный пункт повестки (или null, если он ещё не проиндексирован) для немедленного отображения на фронте.',
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.