Compare commits

...

41 Commits

Author SHA1 Message Date
coopops 81df662cfa chore(release): publish
Build bootstrap container / build (push) Successful in 46s
Release / trigger-coopenomics-docs (push) Successful in 2s
Release / release (push) Successful in 34m17s
Release / trigger-mono-docs (push) Successful in 2s
Release / publish-packages (push) Failing after 13m36s
2026-07-21 08:50:38 +00:00
coopops 845c13605d fix(reports): разрешить 10-значный рег.номер СФР наравне с XXX-XXX-XXXXXX
XSD-схема ЕФС-1 (ТипРегНомерОбщ) допускает регистрационный номер
страхователя в двух форматах: действующий XXX-XXX-XXXXXX (12 цифр,
3-3-6) или прежний — 10 цифр без разделителей. Валидация на бэке и
фронте (форма реквизитов кооператива + редактор нулевого отчёта
ЕФС-1) принимала только 12-значный формат с тире, из-за чего
кооперативы со старым 10-значным номером ФСС не могли его сохранить.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 08:43:46 +00:00
Alex Ant 76b4988da8 migrate 2026-07-20 11:34:49 +03:00
coopops 3db80a30e9 chore(release): publish
Release / release (push) Failing after 2m16s
Release / publish-packages (push) Has been skipped
Release / trigger-coopenomics-docs (push) Has been skipped
Release / trigger-mono-docs (push) Has been skipped
Build bootstrap container / build (push) Successful in 49s
2026-07-19 17:01:42 +00:00
coopops 1b457b8f87 fix(controller): stop migration failures being masked as success
Backfill-миграция V2.3.2 падала на testnet TS-ошибкой компиляции
(mongo.db возможно undefined, strictNullChecks), но deploy-скрипт
(blue-green-deploy.sh) рапортовал успех — 2.3.2 не появилась в
таблице migrations, extensions.capital_program_doc_data_hash осталась
пустой, а Semaphore-лог показывал "Database migrations completed
successfully".

Причина ложноположительного результата — не баг деплой-скрипта, а
Sentry: в production Sentry.init() (enabled: config.env==='production')
ставит process.on('unhandledRejection', ...) с дефолтным mode:'warn',
который перехватывает необработанный reject из bootstrap()'а --migrate
ветки и не роняет процесс — в отличие от штатного поведения голого
Node. В dev (Sentry выключен) тот же баг воспроизводимо валит процесс
exit 1; на testnet — exit 0. Подтверждено локальным воспроизведением
обеих версий на идентичном коде.

Фикс на двух уровнях:
1. index.ts: явный try/catch + process.exit(1) вокруг --migrate и
   --migrations-only веток. Не полагаемся на ambient unhandled-rejection
   поведение (Sentry/Node/версия) — exit code теперь детерминирован
   независимо от окружения.
2. V2.3.2: mongo.db действительно типизирован как возможно undefined —
   явная runtime-проверка с throw вместо каста.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-19 16:57:59 +00:00
coopops ba6d3d1310 chore(release): publish
Build bootstrap container / build (push) Successful in 1m10s
Release / publish-packages (push) Has been skipped
Release / trigger-mono-docs (push) Has been skipped
Release / release (push) Successful in 28m13s
Release / trigger-coopenomics-docs (push) Has been skipped
2026-07-19 15:20:08 +00:00
ant 8e01373179 Merge pull request 'feat(capital): CPP document templates via PrivateData' (#154) from feature/capital-cpp-private-data-templates into dev
Build bootstrap container / build (push) Successful in 3m30s
Reviewed-on: #154
Reviewed-by: Алексей Муравьев <chairman.voskhod@gmail.com>
2026-07-19 15:14:39 +00:00
coopops 31940461c6 fix(desktop): remove electron dependency, fix latent typecheck errors
Typecheck / desktop (pull_request) Successful in 15m11s
Typecheck / controller (pull_request) Successful in 13m55s
electron был не нужен вообще — тянулся транзитивно только через
неиспользуемую devDependency @vue/devtools (нет ни одного импорта в
коде). Убрана из desktop/package.json и из onlyBuiltDependencies
корня; lockfile пересобран. Это устраняет флап CI: pnpm install
изредка валился на сетевой ошибке при скачивании ~200MB бинарника
electron (run #745 и run #732 на этом PR), из-за чего typecheck
падал до первой TS-проверки.

Так как install перестал падать раньше срока, vue-tsc теперь
реально доходит до конца и вскрывает предсуществующие баги:

- useCapitalProgramDocParams.ts, CapitalProgramDocumentParametersWidget.vue:
  импорт `CapitalProgramPrivateData` напрямую из 'cooptypes' никогда не
  работал — тип лежит в Cooperative.Registry, не на верхнем уровне
  (остальной код capital-расширения уже импортирует так же). Каст
  результата Object.fromEntries (индексная сигнатура) в интерфейс с
  именованными полями TS отклоняет как insufficient overlap; вместо
  unknown-каста — каст в Record<EditableFieldKey, string> (тот же
  паттерн, что и в соседней createEmptyForm), который затем обычной
  структурной проверкой присваивается в CapitalProgramPrivateData,
  т.к. набор из 11 полей совпадает один в один.
- CapitalProgramDocumentParametersWidget.vue: legacy draft.activeTab
  стал optional при переходе на wizardStepKey и больше не
  записывается читалкой драфта — добавлен фолбэк на дефолт 994,
  как и в самой readCapitalProgramDocParamsDraft.
- CapitalProgramInlineDocumentPreview.vue: querySelectorAll с
  составным CSS-селектором типизируется как NodeListOf<Element>;
  cleanupEditors использовал узел как HTMLTextAreaElement без
  сужения (соседняя syncInlineEditors уже делает `as
  HTMLTextAreaElement` для той же ситуации).
- ExtensionInstall.vue: prop schema был типизирован как unknown,
  хотя передаётся напрямую в ZodForm, которому нужен
  IExtensionConfigSchema — типизирован точно под фактическое
  использование.
- sdk/queries/paymentMethods/getPaymentMethods.ts: `const name`
  был без export вопреки конвенции всех остальных query-модулей
  (Queries.X.Y.name используется для чтения ключа GraphQL-ответа).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-19 14:25:47 +00:00
coopops 37b0227735 fix(ci): skip electron binary download in typecheck workflow
Typecheck / desktop (pull_request) Failing after 9m12s
Typecheck / controller (pull_request) Has been cancelled
electron тянется транзитивно через @vue/devtools-electron (dev-only,
приложение devtools вне браузера) и попадает в onlyBuiltDependencies —
pnpm install качает ~200MB бинарник с github releases, который здесь
никогда не запускается. Изредка скачивание валит install сетевым
таймаутом/TLS-обрывом до того, как дело доходит до vue-tsc/tsc.

Run #745 (PR #154): desktop job упал именно на этом шаге, не на
типах — коммит 2cfb37c5d7 тут ни при чём, чистая инфраструктурная
нестабильность CI-раннера.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-19 14:07:54 +00:00
coopops 2cfb37c5d7 fix(agenda): resolve program-agreement links via soviet::newresolved, not wallet::signagree
Typecheck / desktop (pull_request) Failing after 6m47s
Typecheck / controller (pull_request) Has been cancelled
Прошлый фикс менял receiver=coopname на data.coopname прямо в explorer-запросе
к wallet::signagree — рабочий, но не объясняющий, почему стандартный
receiver-фильтр не подходил именно здесь: wallet::signagree не делает
require_recipient(coopname) сам по себе, поэтому explorer индексирует его
только под receiver='wallet'.

На самом деле уведомление уже существует и не требует правки контракта:
Soviet::make_complete_document (lib/core/soviet/soviet.hpp) на КАЖДЫЙ вызов —
из soviet::sndagreement И из wallet::signagree одинаково — шлёт inline
soviet::newresolved/newsubmitted, а require_recipient(coopname/username)
зашит централизованно внутри самих newresolved.cpp/newsubmitted.cpp
(soviet/src/doc/*.cpp). Проверено на живом хэше оферты Благорост: newresolved
уже приходит с receiver=voskhod, receiver=veszmgpjteqs, receiver=soviet — без
единой правки на контрактах.

Правильный fallback для findLinkedAgreementDocument — искать soviet::newresolved
(с обычным receiver=coopname, как везде), а не действие wallet::signagree
напрямую (у которого никакого receiver-уведомления и не предполагалось).
Расширять notification-поверхность wallet::signagree не нужно — единственное
место для этого уже newresolved.cpp/newsubmitted.cpp.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 13:48:57 +00:00
coopops 1c605dd2a5 fix(agenda): resolve wallet::signagree linked documents by data.coopname, not receiver
Typecheck / desktop (pull_request) Failing after 9m39s
Typecheck / controller (pull_request) Has been cancelled
findLinkedAgreementDocument искал wallet::signagree по фильтру receiver=coopname,
но wallet::signagree не делает require_recipient(coopname) (в отличие от
Soviet::make_complete_document) — explorer индексирует это действие только с
receiver='wallet'. Фильтр receiver=coopname всегда давал 0 строк, поэтому
buildLinkedAggregate молча возвращал null, а программные оферты (capital:
Благорост/Генератор, program_id>0, подписываются через wallet::signagree, не
soviet::sndagreement) пропадали из links повестки совета — платформенные
соглашения (wallet/signature/privacy/user, идут через sndagreement) отображались,
а оферта ЦПП — нет, хотя её hash был на месте в meta.links и сам документ был
в реестре.

Воспроизведено и проверено на живом кандидате voskhod: 5 хэшей в meta.links,
но до фикса только 4 резолвились в документ-агрегаты. Скоуп по кооперативу
берём из payload действия (data.coopname), а не из индекса receiver.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 13:36:58 +00:00
coopops b1b1775277 fix(contracts): migrate voskhod programs.program_type back to 'capital'
Typecheck / desktop (pull_request) Failing after 8m46s
Typecheck / controller (pull_request) Successful in 13m44s
Автоматическая CI/CD-миграция (soviet::migrate) раньше переименовывала
programs.program_type[id=4] voskhod в 'blagorost', но coagreements.type для
той же программы никогда не переименовывался и остаётся 'capital' — отсюда
расхождение двух таблиц на одном и том же боевом кооперативе, которое видно
любым прямым запросом к цепочке и вносит путаницу.

'capital' — каноничное значение по всей цепочке решений в этой сессии: это
то, что реально в coagreements.type, то, на что рассчитывает _capital_program
в lib/consts.hpp, и то, что теперь везде в controller/boot. Новый блок в
migrate() приводит programs.program_type к нему же — идемпотентно, сработает
автоматически на следующем деплое soviet, без ручных действий на проде.
soviet и capital пересобраны локально — компилируются чисто.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 12:35:37 +00:00
coopops 5f4281ac01 revert(contracts): _capital_program back to 'capital', drop stray blagorost migration
Typecheck / desktop (pull_request) Failing after 8m52s
Typecheck / controller (pull_request) Failing after 6m44s
Единственный источник правды — продовая цепочка voskhod: coagreements.type
для program_id=4 буквально 'capital' (проверено прямым запросом к
185.70.105.87), это боевое неизменяемое значение. Contract-константа
_capital_program в lib/consts.hpp была переименована в 'blagorost' коммитом
e1538bb5ab (2026-01-26) и никогда не доезжала до прода — из-за этого dev-цепочки
собирали программу с другим именем и падали на 'Недопустимый тип программы'
при попытке засеять её как 'capital'.

Заодно убран автоматический CI/CD-migrate в soviet::migrate(), который на
каждом деплое насильно переименовывал programs.program_type[id=4] voskhod в
'blagorost' — он и был причиной, что programs.program_type на проде уже
'blagorost', а coagreements.type там же остался 'capital' (миграция трогала
только одну из двух таблиц). Раз откатываем контракт на 'capital' насовсем —
эта миграция больше не нужна и мешает: на следующем деплое она снова
рассинхронила бы данные.

Boot-скрипты и BLAGOROST_AGREEMENT_TYPE в controller уже используют 'capital'
(предыдущий коммит) — теперь везде единое значение, dev-цепочка при
pnpm run reboot соберёт programs+coagreements так же, как боевой voskhod, без
какой-либо миграции на проде.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 11:58:50 +00:00
coopops d35e88d951 revert(capital): keep BLAGOROST_AGREEMENT_TYPE='capital', fix boot seed drift instead
Typecheck / controller (pull_request) Has been cancelled
Typecheck / desktop (pull_request) Has been cancelled
Прямой запрос к продовой цепочке voskhod (185.70.105.87) показал реальный
on-chain тип программы Благорост — 'capital' (program_id=4), не 'blagorost'.
Прод годами работает с этим значением, ошибок в логах нет. Предыдущий коммит
(5360daa471) поменял константу на 'blagorost' по данным ТОЛЬКО локальной
dev-цепочки mono-ai-2 — для прода это было бы регрессией. Откатываю.

Настоящий источник дрейфа — boot-сидинг: components/boot/src/init/cooperative.ts
createPrograms() и src/scripts/seed-capital/phases/01-programs.ts заводили
новый кооператив (createprog) с type='blagorost', расходясь с давно
задеплоенным продовым значением. Правлю сидинг под реальность, а не наоборот
(та же логика, что 'дрейф стандарт↔код — прав контракт': on-chain состояние
неизменяемо и авторитетно, код подстраивается). Заодно поправлены прочие
литералы 'blagorost' в тестах boot (capital.test.ts, capital-import.test.ts,
08-investments.ts, tests/capital/consts.ts), которые создавали/подписывали
программу под тем же неверным именем.

> monocoop@ reboot /home/admin/mono-ai-2
> cd components/boot/scripts && bash reboot.sh

Останавливаем и удаляем контейнеры с volumes...
Останавливаем blockchain контейнер...
Удаляем blockchain data...
Пересоздаем и запускаем базы данных...
Ждем готовности MongoDB...
MongoDB готов!
Ждем готовности PostgreSQL...
PostgreSQL готов!
Запускаем boot процесс...

> @coopenomics/boot@2026.6.24 boot /home/admin/mono-ai-2/components/boot
> esno src/index.ts boot

Node is healthy. Continue. {
  server_version: 'ee002f93',
  chain_id: 'db79c8409645082749ca50640d6f4ee511575acf26c4e2c8e4748e6bf6a01ed4',
  head_block_num: 10,
  last_irreversible_block_num: 9,
  last_irreversible_block_id: '000000093c561104c62b9058c82eab12559686eba92fdbe4bcf4df9bf2600131',
  head_block_id: '0000000ae4bffe027ec2a2c89ca92832422a84ec052649f1a80436fff5c61327',
  head_block_time: '2026-07-19T11:48:57.500',
  head_block_producer: 'eosio',
  virtual_block_cpu_limit: 1008036,
  virtual_block_net_limit: 1058054,
  block_cpu_limit: 1000000,
  block_net_limit: 1048576,
  server_version_string: 'v6.0.0',
  fork_db_head_block_num: 10,
  fork_db_head_block_id: '0000000ae4bffe027ec2a2c89ca92832422a84ec052649f1a80436fff5c61327',
  server_full_version_string: 'v6.0.0-ee002f9372237aa07b05879df772a95f9d54867f-dirty',
  total_cpu_weight: 0,
  total_net_weight: 0,
  earliest_available_block_num: 1,
  last_irreversible_block_time: '2026-07-19T11:48:57.000'
}
Created account: eosio.token
Created account: eosio.bpay
Created account: eosio.vpay
Created account: eosio.msig
Created account: eosio.wrap
Created account: eosio.power
Created account: eosio.saving
Created account: registrator
Created account: capital
Created account: loan
Created account: ledger
Created account: ledger2
Created account: test
Created account: meet
Created account: wallet
Created account: soviet
Created account: branch
Created account: marketplace
Created account: draft
Created account: gateway
Created account: fund
Created account: contributor
Created account: apps
Created account: expense
Created account: voskhod
ok -> init activation:  { result: 'ok' }
on set:  {
  name: 'eosio.boot',
  path: '/home/admin/mono-ai-2/components/contracts/build/contracts/system/contracts/eosio.boot',
  target: 'eosio'
}
wasm_path /home/admin/mono-ai-2/components/contracts/build/contracts/system/contracts/eosio.boot/eosio.boot.wasm
abi_path /home/admin/mono-ai-2/components/contracts/build/contracts/system/contracts/eosio.boot/eosio.boot.abi
contract setted:  eosio
Фича активирована:  ACTION_RETURN_VALUE
Фича активирована:  CONFIGURABLE_WASM_LIMITS2
Фича активирована:  BLOCKCHAIN_PARAMETERS
Фича активирована:  GET_SENDER
Фича активирована:  FORWARD_SETCODE
Фича активирована:  ONLY_BILL_FIRST_AUTHORIZER
Фича активирована:  RESTRICT_ACTION_TO_SELF
Фича активирована:  DISALLOW_EMPTY_PRODUCER_SCHEDULE
Фича активирована:  FIX_LINKAUTH_RESTRICTION
Фича активирована:  REPLACE_DEFERRED
Фича активирована:  NO_DUPLICATE_DEFERRED_ID
Фича активирована:  ONLY_LINK_TO_EXISTING_PERMISSION
Фича активирована:  RAM_RESTRICTIONS
Фича активирована:  WEBAUTHN_KEY
Фича активирована:  WTMSIG_BLOCK_SIGNATURES
Фича активирована:  GET_CODE_HASH
Фича активирована:  GET_BLOCK_NUM
Фича активирована:  CRYPTO_PRIMITIVES
on set:  {
  name: 'eosio.system',
  path: '/home/admin/mono-ai-2/components/contracts/build/contracts/system/contracts/eosio.system',
  target: 'eosio'
}
wasm_path /home/admin/mono-ai-2/components/contracts/build/contracts/system/contracts/eosio.system/eosio.system.wasm
abi_path /home/admin/mono-ai-2/components/contracts/build/contracts/system/contracts/eosio.system/eosio.system.abi
contract setted:  eosio
on set:  {
  name: 'eosio.token',
  path: '/home/admin/mono-ai-2/components/contracts/build/contracts/system/contracts/eosio.token',
  target: 'eosio.token'
}
wasm_path /home/admin/mono-ai-2/components/contracts/build/contracts/system/contracts/eosio.token/eosio.token.wasm
abi_path /home/admin/mono-ai-2/components/contracts/build/contracts/system/contracts/eosio.token/eosio.token.abi
contract setted:  eosio.token
on set:  {
  name: 'eosio.msig',
  path: '/home/admin/mono-ai-2/components/contracts/build/contracts/system/contracts/eosio.msig',
  target: 'eosio.msig'
}
wasm_path /home/admin/mono-ai-2/components/contracts/build/contracts/system/contracts/eosio.msig/eosio.msig.wasm
abi_path /home/admin/mono-ai-2/components/contracts/build/contracts/system/contracts/eosio.msig/eosio.msig.abi
contract setted:  eosio.msig
on set:  {
  name: 'eosio.wrap',
  path: '/home/admin/mono-ai-2/components/contracts/build/contracts/system/contracts/eosio.wrap',
  target: 'eosio.wrap'
}
wasm_path /home/admin/mono-ai-2/components/contracts/build/contracts/system/contracts/eosio.wrap/eosio.wrap.wasm
abi_path /home/admin/mono-ai-2/components/contracts/build/contracts/system/contracts/eosio.wrap/eosio.wrap.abi
contract setted:  eosio.wrap
on set:  {
  name: 'registrator',
  path: '/home/admin/mono-ai-2/components/contracts/build/contracts/registrator',
  target: 'registrator'
}
wasm_path /home/admin/mono-ai-2/components/contracts/build/contracts/registrator/registrator.wasm
abi_path /home/admin/mono-ai-2/components/contracts/build/contracts/registrator/registrator.abi
contract setted:  registrator
on set:  {
  name: 'soviet',
  path: '/home/admin/mono-ai-2/components/contracts/build/contracts/soviet',
  target: 'soviet'
}
wasm_path /home/admin/mono-ai-2/components/contracts/build/contracts/soviet/soviet.wasm
abi_path /home/admin/mono-ai-2/components/contracts/build/contracts/soviet/soviet.abi
contract setted:  soviet
on set:  {
  name: 'marketplace',
  path: '/home/admin/mono-ai-2/components/contracts/build/contracts/marketplace',
  target: 'marketplace'
}
wasm_path /home/admin/mono-ai-2/components/contracts/build/contracts/marketplace/marketplace.wasm
abi_path /home/admin/mono-ai-2/components/contracts/build/contracts/marketplace/marketplace.abi
contract setted:  marketplace
on set:  {
  name: 'draft',
  path: '/home/admin/mono-ai-2/components/contracts/build/contracts/draft',
  target: 'draft'
}
wasm_path /home/admin/mono-ai-2/components/contracts/build/contracts/draft/draft.wasm
abi_path /home/admin/mono-ai-2/components/contracts/build/contracts/draft/draft.abi
contract setted:  draft
on set:  {
  name: 'branch',
  path: '/home/admin/mono-ai-2/components/contracts/build/contracts/branch',
  target: 'branch'
}
wasm_path /home/admin/mono-ai-2/components/contracts/build/contracts/branch/branch.wasm
abi_path /home/admin/mono-ai-2/components/contracts/build/contracts/branch/branch.abi
contract setted:  branch
on set:  {
  name: 'gateway',
  path: '/home/admin/mono-ai-2/components/contracts/build/contracts/gateway',
  target: 'gateway'
}
wasm_path /home/admin/mono-ai-2/components/contracts/build/contracts/gateway/gateway.wasm
abi_path /home/admin/mono-ai-2/components/contracts/build/contracts/gateway/gateway.abi
contract setted:  gateway
on set:  {
  name: 'fund',
  path: '/home/admin/mono-ai-2/components/contracts/build/contracts/fund',
  target: 'fund'
}
wasm_path /home/admin/mono-ai-2/components/contracts/build/contracts/fund/fund.wasm
abi_path /home/admin/mono-ai-2/components/contracts/build/contracts/fund/fund.abi
contract setted:  fund
on set:  {
  name: 'ledger',
  path: '/home/admin/mono-ai-2/components/contracts/build/contracts/ledger',
  target: 'ledger'
}
wasm_path /home/admin/mono-ai-2/components/contracts/build/contracts/ledger/ledger.wasm
abi_path /home/admin/mono-ai-2/components/contracts/build/contracts/ledger/ledger.abi
contract setted:  ledger
on set:  {
  name: 'ledger2',
  path: '/home/admin/mono-ai-2/components/contracts/build/contracts/ledger2',
  target: 'ledger2'
}
wasm_path /home/admin/mono-ai-2/components/contracts/build/contracts/ledger2/ledger2.wasm
abi_path /home/admin/mono-ai-2/components/contracts/build/contracts/ledger2/ledger2.abi
contract setted:  ledger2
on set:  {
  name: 'test',
  path: '/home/admin/mono-ai-2/components/contracts/build/contracts/test',
  target: 'test'
}
wasm_path /home/admin/mono-ai-2/components/contracts/build/contracts/test/test.wasm
abi_path /home/admin/mono-ai-2/components/contracts/build/contracts/test/test.abi
contract setted:  test
on set:  {
  name: 'contributor',
  path: '/home/admin/mono-ai-2/components/contracts/build/contracts/contributor',
  target: 'contributor'
}
wasm_path /home/admin/mono-ai-2/components/contracts/build/contracts/contributor/contributor.wasm
abi_path /home/admin/mono-ai-2/components/contracts/build/contracts/contributor/contributor.abi
contract setted:  contributor
on set:  {
  name: 'capital',
  path: '/home/admin/mono-ai-2/components/contracts/build/contracts/capital',
  target: 'capital'
}
wasm_path /home/admin/mono-ai-2/components/contracts/build/contracts/capital/capital.wasm
abi_path /home/admin/mono-ai-2/components/contracts/build/contracts/capital/capital.abi
contract setted:  capital
on set:  {
  name: 'wallet',
  path: '/home/admin/mono-ai-2/components/contracts/build/contracts/wallet',
  target: 'wallet'
}
wasm_path /home/admin/mono-ai-2/components/contracts/build/contracts/wallet/wallet.wasm
abi_path /home/admin/mono-ai-2/components/contracts/build/contracts/wallet/wallet.abi
contract setted:  wallet
on set:  {
  name: 'loan',
  path: '/home/admin/mono-ai-2/components/contracts/build/contracts/loan',
  target: 'loan'
}
wasm_path /home/admin/mono-ai-2/components/contracts/build/contracts/loan/loan.wasm
abi_path /home/admin/mono-ai-2/components/contracts/build/contracts/loan/loan.abi
contract setted:  loan
on set:  {
  name: 'meet',
  path: '/home/admin/mono-ai-2/components/contracts/build/contracts/meet',
  target: 'meet'
}
wasm_path /home/admin/mono-ai-2/components/contracts/build/contracts/meet/meet.wasm
abi_path /home/admin/mono-ai-2/components/contracts/build/contracts/meet/meet.abi
contract setted:  meet
on set:  {
  name: 'apps',
  path: '/home/admin/mono-ai-2/components/contracts/build/contracts/apps',
  target: 'apps'
}
wasm_path /home/admin/mono-ai-2/components/contracts/build/contracts/apps/apps.wasm
abi_path /home/admin/mono-ai-2/components/contracts/build/contracts/apps/apps.abi
contract setted:  apps
on set:  {
  name: 'expense',
  path: '/home/admin/mono-ai-2/components/contracts/build/contracts/expense',
  target: 'expense'
}
wasm_path /home/admin/mono-ai-2/components/contracts/build/contracts/expense/expense.wasm
abi_path /home/admin/mono-ai-2/components/contracts/build/contracts/expense/expense.abi
contract setted:  expense
создаём токен
Токен создан:  { issuer: 'eosio', maximum_supply: '1000000000.0000 AXON' }
выпускаем токены
Токен выпущен:  { to: 'eosio', quantity: '100000.0000 AXON', memo: '' }
Updated account permissions: eosio.saving -> eosio.saving
Updated account permissions: registrator -> registrator
Updated account permissions: capital -> capital
Updated account permissions: loan -> loan
Updated account permissions: ledger -> ledger
Updated account permissions: ledger2 -> ledger2
Updated account permissions: test -> test
Updated account permissions: meet -> meet
Updated account permissions: wallet -> wallet
Updated account permissions: soviet -> soviet
Updated account permissions: branch -> branch
Updated account permissions: marketplace -> marketplace
Updated account permissions: draft -> draft
Updated account permissions: gateway -> gateway
Updated account permissions: fund -> fund
Updated account permissions: contributor -> contributor
Updated account permissions: apps -> apps
Updated account permissions: expense -> expense
Updated account permissions: voskhod -> registrator
Системный контракт инициализирован { version: 0, core: '4,AXON' }
Эмиссия инициализирована {
  init_supply: '1000.0000 AXON',
  tact_duration: 86400,
  emission_factor: 0.618
}
Система аренды инициализирована { args: { powerup_days: 1, min_powerup_fee: '0.0001 AXON' } }
Шаблон создан:  1
Шаблон создан:  2
Шаблон создан:  3
Шаблон создан:  4
Шаблон создан:  50
Шаблон создан:  51
Шаблон создан:  100
Шаблон создан:  101
Шаблон создан:  200
Шаблон создан:  201
Шаблон создан:  300
Шаблон создан:  301
Шаблон создан:  302
Шаблон создан:  303
Шаблон создан:  304
Шаблон создан:  501
Шаблон создан:  599
Шаблон создан:  600
Шаблон создан:  699
Шаблон создан:  700
Шаблон создан:  701
Шаблон создан:  702
Шаблон создан:  800
Шаблон создан:  801
Шаблон создан:  802
Шаблон создан:  900
Шаблон создан:  901
Шаблон создан:  994
Шаблон создан:  995
Шаблон создан:  996
Шаблон создан:  997
Шаблон создан:  998
Шаблон создан:  999
Шаблон создан:  1000
Шаблон создан:  1001
Шаблон создан:  1002
Шаблон создан:  1003
Шаблон создан:  1004
Шаблон создан:  1005
Шаблон создан:  1006
Шаблон создан:  1007
Шаблон создан:  1010
Шаблон создан:  1011
Шаблон создан:  1020
Шаблон создан:  1025
Шаблон создан:  1030
Шаблон создан:  1040
Шаблон создан:  1041
Шаблон создан:  1042
Шаблон создан:  1050
Шаблон создан:  1051
Шаблон создан:  1060
Шаблон создан:  1061
Шаблон создан:  1062
Шаблон создан:  1070
Шаблон создан:  1071
Шаблон создан:  1072
Шаблон создан:  1080
Шаблон создан:  1090
Шаблон создан:  2010
Шаблон создан:  2011
Арендуем ресурсы провайдеру
Аренда ресурсов:  {
  payer: 'eosio',
  receiver: 'voskhod',
  days: 1,
  payment: '10000.0000 AXON',
  transfer: true
}
{ from: 'eosio', to: 'voskhod', quantity: '1000.0000 AXON', memo: '' }
Перевод токенов:  { from: 'eosio', to: 'voskhod', quantity: '1000.0000 AXON', memo: '' }
Базовая инфраструктура установлена
Провайдер добавлен:  {
  username: 'voskhod',
  type: 'coop',
  short_name: 'ПК "Восход"',
  full_name: 'Потребительский Кооператив "ВОСХОД"',
  represented_by: {
    first_name: 'Иван',
    last_name: 'Иванов',
    middle_name: 'Иванович',
    position: 'Председатель',
    based_on: 'Решение общего собрания №1'
  },
  country: 'Российская Федерация',
  city: 'Москва',
  fact_address: '117593 г. Москва, муниципальный округ Ясенево, проезд Соловьиный, дом 1, помещение 1/1',
  full_address: '117593 г. Москва, муниципальный округ Ясенево, проезд Соловьиный, дом 1, помещение 1/1',
  email: 'copenomics@yandex.ru',
  phone: '+71234567890',
  details: { inn: '9728130611', ogrn: '1247700283346', kpp: '772801001' }
}
Vars сохранены с указанным _id и _created_at
Все документы удалены из коллекции sync
Все документы удалены из коллекции actions
Все документы удалены из коллекции deltas
Добавляем пайщика ant
{
  coopname: 'voskhod',
  referer: '',
  username: 'ant',
  type: 'individual',
  created_at: '2025-01-15T10:00:00',
  initial: '100.0000 RUB',
  minimum: '200.0000 RUB',
  spread_initial: true,
  meta: 'Основатель кооператива ВОСХОД',
  registration_hash: '1e87d2a7d35f1be95da5a676ae799262e970647d0ba69cd19c0eaff0afd5b488'
}
Добавлен пользователь:  ant
Устанавливаем дефолтный публичный ключ для ant
{
  coopname: 'voskhod',
  changer: 'voskhod',
  username: 'ant',
  public_key: 'EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV'
}
Изменён ключ для пользователя:  ant
Инициализируем пользователей в PostgreSQL
Подключение к PostgreSQL установлено для инициализации пользователей
Инициализировано 1 пользователей в PostgreSQL
Инициализируем vault в PostgreSQL
Подключение к PostgreSQL установлено для инициализации vault
Vault инициализирован в PostgreSQL
Создаём совет
{
  coopname: 'voskhod',
  username: 'ant',
  type: 'soviet',
  members: [
    {
      username: 'ant',
      is_voting: true,
      position_title: 'Председатель совета',
      position: 'chairman'
    }
  ],
  name: 'Совет',
  description: 'Совет кооператива ВОСХОД'
}
Создан совет:  {
  coopname: 'voskhod',
  username: 'ant',
  type: 'soviet',
  members: [
    {
      username: 'ant',
      is_voting: true,
      position_title: 'Председатель совета',
      position: 'chairman'
    }
  ],
  name: 'Совет',
  description: 'Совет кооператива ВОСХОД'
}
Создаём программы и соглашения
params {
  coopname: 'voskhod',
  username: 'voskhod',
  type: 'marketplace',
  title: 'КООПЛЕЙС',
  announce: '',
  description: '',
  preview: '',
  images: '',
  calculation_type: 'relative',
  fixed_membership_contribution: '0.0000 RUB',
  membership_percent_fee: 10000,
  meta: '',
  is_can_coop_spend_share_contributions: false
}
Программа установлена:  {
  coopname: 'voskhod',
  username: 'voskhod',
  type: 'marketplace',
  title: 'КООПЛЕЙС',
  announce: '',
  description: '',
  preview: '',
  images: '',
  calculation_type: 'relative',
  fixed_membership_contribution: '0.0000 RUB',
  membership_percent_fee: 10000,
  meta: '',
  is_can_coop_spend_share_contributions: false
}
params {
  coopname: 'voskhod',
  username: 'voskhod',
  type: 'generator',
  title: 'Целевая потребительская программа "Генератор"',
  announce: '',
  description: '',
  preview: '',
  images: '',
  calculation_type: 'free',
  fixed_membership_contribution: '0.0000 RUB',
  membership_percent_fee: 0,
  meta: '',
  is_can_coop_spend_share_contributions: true
}
Программа установлена:  {
  coopname: 'voskhod',
  username: 'voskhod',
  type: 'generator',
  title: 'Целевая потребительская программа "Генератор"',
  announce: '',
  description: '',
  preview: '',
  images: '',
  calculation_type: 'free',
  fixed_membership_contribution: '0.0000 RUB',
  membership_percent_fee: 0,
  meta: '',
  is_can_coop_spend_share_contributions: true
}
params {
  coopname: 'voskhod',
  username: 'voskhod',
  type: 'capital',
  title: 'Целевая потребительская программа "Благорост"',
  announce: '',
  description: '',
  preview: '',
  images: '',
  calculation_type: 'free',
  fixed_membership_contribution: '0.0000 RUB',
  membership_percent_fee: 0,
  meta: '',
  is_can_coop_spend_share_contributions: true
}
 ELIFECYCLE  Command failed with exit code 1.
Запускаем parser...
Запускаем контроллер...
Перезапуск завершен! пересоздаёт blockchain-data с нуля и гоняет
> monocoop@ boot /home/admin/mono-ai-2
> lerna run boot --scope @coopenomics/boot

> @coopenomics/boot:boot

> @coopenomics/boot@2026.6.24 boot /home/admin/mono-ai-2/components/boot
> esno src/index.ts boot
Node is healthy. Continue. {
  server_version: 'ee002f93',
  chain_id: 'db79c8409645082749ca50640d6f4ee511575acf26c4e2c8e4748e6bf6a01ed4',
  head_block_num: 123,
  last_irreversible_block_num: 122,
  last_irreversible_block_id: '0000007adc2e2215250e7ffbd9c7961faa675f57c3134be365b336292fda7e72',
  head_block_id: '0000007bf4fa5276948ff1ea2dce877a2032ac28be3a753271efd198ed5f79d5',
  head_block_time: '2026-07-19T11:49:54.000',
  head_block_producer: 'eosio',
  virtual_block_cpu_limit: 1128638,
  virtual_block_net_limit: 1184640,
  block_cpu_limit: 1000000,
  block_net_limit: 1048576,
  server_version_string: 'v6.0.0',
  fork_db_head_block_num: 123,
  fork_db_head_block_id: '0000007bf4fa5276948ff1ea2dce877a2032ac28be3a753271efd198ed5f79d5',
  server_full_version_string: 'v6.0.0-ee002f9372237aa07b05879df772a95f9d54867f-dirty',
  total_cpu_weight: 999999997,
  total_net_weight: 999999997,
  earliest_available_block_num: 1,
  last_irreversible_block_time: '2026-07-19T11:49:53.500'
}
error -> init activation:  AxiosError: Request failed with status code 500
    at settle (file:///home/admin/mono-ai-2/node_modules/.pnpm/axios@1.13.6/node_modules/axios/lib/core/settle.js:20:7)
    at IncomingMessage.handleStreamEnd (file:///home/admin/mono-ai-2/node_modules/.pnpm/axios@1.13.6/node_modules/axios/lib/adapters/http.js:844:13)
    at IncomingMessage.emit (node:events:531:35)
    at endReadableNT (node:internal/streams/readable:1698:12)
    at process.processTicksAndRejections (node:internal/process/task_queues:89:21)
    at Axios.request (file:///home/admin/mono-ai-2/node_modules/.pnpm/axios@1.13.6/node_modules/axios/lib/core/Axios.js:46:41)
    at process.processTicksAndRejections (node:internal/process/task_queues:103:5)
    at async startInfra (/home/admin/mono-ai-2/components/boot/src/init/infra.ts:45:22)
    at async boot (/home/admin/mono-ai-2/components/boot/src/init/booter.ts:6:22)
    at async Command.<anonymous> (/home/admin/mono-ai-2/components/boot/src/index.ts:144:7) {
  isAxiosError: true,
  code: 'ERR_BAD_RESPONSE',
  config: {
    transitional: {
      silentJSONParsing: true,
      forcedJSONParsing: true,
      clarifyTimeoutError: false,
      legacyInterceptorReqResOrdering: true
    },
    adapter: [ 'xhr', 'http', 'fetch' ],
    transformRequest: [ [Function: transformRequest] ],
    transformResponse: [ [Function: transformResponse] ],
    timeout: 0,
    xsrfCookieName: 'XSRF-TOKEN',
    xsrfHeaderName: 'X-XSRF-TOKEN',
    maxContentLength: -1,
    maxBodyLength: -1,
    env: { FormData: [Function], Blob: [class Blob] },
    validateStatus: [Function: validateStatus],
    headers: Object [AxiosHeaders] {
      Accept: 'application/json, text/plain, */*',
      'Content-Type': 'application/json',
      'User-Agent': 'axios/1.13.6',
      'Content-Length': '102',
      'Accept-Encoding': 'gzip, compress, deflate, br'
    },
    method: 'post',
    url: 'http://127.0.0.1:8898/v1/producer/schedule_protocol_feature_activations',
    data: '{"protocol_features_to_activate":["0ec7e080177b2c02b278d5088611686b49d739925a92d9bfcacd7fc6b74053bd"]}',
    allowAbsoluteUrls: true
  },
  request: <ref *1> ClientRequest {
    _events: [Object: null prototype] {
      abort: [Function (anonymous)],
      aborted: [Function (anonymous)],
      connect: [Function (anonymous)],
      error: [Function (anonymous)],
      socket: [Function (anonymous)],
      timeout: [Function (anonymous)],
      finish: [Function: requestOnFinish]
    },
    _eventsCount: 7,
    _maxListeners: undefined,
    outputData: [],
    outputSize: 0,
    writable: true,
    destroyed: true,
    _last: false,
    chunkedEncoding: false,
    shouldKeepAlive: true,
    maxRequestsOnConnectionReached: false,
    _defaultKeepAlive: true,
    useChunkedEncodingByDefault: true,
    sendDate: false,
    _removedConnection: false,
    _removedContLen: false,
    _removedTE: false,
    strictContentLength: false,
    _contentLength: 102,
    _hasBody: true,
    _trailer: '',
    finished: true,
    _headerSent: true,
    _closed: true,
    _header: 'POST /v1/producer/schedule_protocol_feature_activations HTTP/1.1\r\n' +
      'Accept: application/json, text/plain, */*\r\n' +
      'Content-Type: application/json\r\n' +
      'User-Agent: axios/1.13.6\r\n' +
      'Content-Length: 102\r\n' +
      'Accept-Encoding: gzip, compress, deflate, br\r\n' +
      'Host: 127.0.0.1:8898\r\n' +
      'Connection: keep-alive\r\n' +
      '\r\n',
    _keepAliveTimeout: 0,
    _onPendingData: [Function: nop],
    agent: Agent {
      _events: [Object: null prototype],
      _eventsCount: 2,
      _maxListeners: undefined,
      options: [Object: null prototype],
      defaultPort: 80,
      protocol: 'http:',
      requests: [Object: null prototype] {},
      sockets: [Object: null prototype] {},
      freeSockets: [Object: null prototype],
      keepAliveMsecs: 1000,
      keepAlive: true,
      maxSockets: Infinity,
      maxFreeSockets: 256,
      scheduling: 'lifo',
      maxTotalSockets: Infinity,
      totalSocketCount: 1,
      agentKeepAliveTimeoutBuffer: 1000,
      [Symbol(shapeMode)]: false,
      [Symbol(kCapture)]: false
    },
    socketPath: undefined,
    method: 'POST',
    maxHeaderSize: undefined,
    insecureHTTPParser: undefined,
    joinDuplicateHeaders: undefined,
    path: '/v1/producer/schedule_protocol_feature_activations',
    _ended: true,
    res: IncomingMessage {
      _events: [Object],
      _readableState: [ReadableState],
      _maxListeners: undefined,
      socket: null,
      httpVersionMajor: 1,
      httpVersionMinor: 1,
      httpVersion: '1.1',
      complete: true,
      rawHeaders: [Array],
      rawTrailers: [],
      joinDuplicateHeaders: undefined,
      aborted: false,
      upgrade: false,
      url: '',
      method: null,
      statusCode: 500,
      statusMessage: 'Internal Server Error',
      client: [Socket],
      _consuming: false,
      _dumped: false,
      req: [Circular *1],
      _eventsCount: 4,
      responseUrl: 'http://127.0.0.1:8898/v1/producer/schedule_protocol_feature_activations',
      redirects: [],
      [Symbol(shapeMode)]: true,
      [Symbol(kCapture)]: false,
      [Symbol(kHeaders)]: [Object],
      [Symbol(kHeadersCount)]: 8,
      [Symbol(kTrailers)]: null,
      [Symbol(kTrailersCount)]: 0
    },
    aborted: false,
    timeoutCb: null,
    upgradeOrConnect: false,
    parser: null,
    maxHeadersCount: null,
    reusedSocket: true,
    host: '127.0.0.1',
    protocol: 'http:',
    _redirectable: Writable {
      _events: [Object],
      _writableState: [WritableState],
      _maxListeners: undefined,
      _options: [Object],
      _ended: true,
      _ending: true,
      _redirectCount: 0,
      _redirects: [],
      _requestBodyLength: 102,
      _requestBodyBuffers: [],
      _eventsCount: 3,
      _onNativeResponse: [Function (anonymous)],
      _currentRequest: [Circular *1],
      _currentUrl: 'http://127.0.0.1:8898/v1/producer/schedule_protocol_feature_activations',
      _timeout: null,
      [Symbol(shapeMode)]: true,
      [Symbol(kCapture)]: false
    },
    [Symbol(shapeMode)]: false,
    [Symbol(kCapture)]: false,
    [Symbol(kBytesWritten)]: 0,
    [Symbol(kNeedDrain)]: false,
    [Symbol(corked)]: 0,
    [Symbol(kChunkedBuffer)]: [],
    [Symbol(kChunkedLength)]: 0,
    [Symbol(kSocket)]: Socket {
      connecting: false,
      _hadError: false,
      _parent: null,
      _host: null,
      _closeAfterHandlingError: false,
      _events: [Object],
      _readableState: [ReadableState],
      _writableState: [WritableState],
      allowHalfOpen: false,
      _maxListeners: undefined,
      _eventsCount: 6,
      _sockname: null,
      _pendingData: null,
      _pendingEncoding: '',
      server: null,
      _server: null,
      timeout: 5000,
      parser: null,
      _httpMessage: null,
      [Symbol(async_id_symbol)]: -1,
      [Symbol(kHandle)]: [TCP],
      [Symbol(lastWriteQueueSize)]: 0,
      [Symbol(timeout)]: Timeout {
        _idleTimeout: 5000,
        _idlePrev: [TimersList],
        _idleNext: [TimersList],
        _idleStart: 11165,
        _onTimeout: [Function: bound ],
        _timerArgs: undefined,
        _repeat: null,
        _destroyed: false,
        [Symbol(refed)]: false,
        [Symbol(kHasPrimitive)]: false,
        [Symbol(asyncId)]: 3415,
        [Symbol(triggerId)]: 3413,
        [Symbol(kAsyncContextFrame)]: undefined
      },
      [Symbol(kBuffer)]: null,
      [Symbol(kBufferCb)]: null,
      [Symbol(kBufferGen)]: null,
      [Symbol(shapeMode)]: true,
      [Symbol(kCapture)]: false,
      [Symbol(kSetNoDelay)]: true,
      [Symbol(kSetKeepAlive)]: true,
      [Symbol(kSetKeepAliveInitialDelay)]: 1,
      [Symbol(kBytesRead)]: 0,
      [Symbol(kBytesWritten)]: 0
    },
    [Symbol(kOutHeaders)]: [Object: null prototype] {
      accept: [Array],
      'content-type': [Array],
      'user-agent': [Array],
      'content-length': [Array],
      'accept-encoding': [Array],
      host: [Array]
    },
    [Symbol(errored)]: null,
    [Symbol(kHighWaterMark)]: 65536,
    [Symbol(kRejectNonStandardBodyWrites)]: false,
    [Symbol(kUniqueHeaders)]: null
  },
  response: {
    status: 500,
    statusText: 'Internal Server Error',
    headers: Object [AxiosHeaders] {
      server: 'nodeos/v6.0.0',
      'access-control-allow-origin': '*',
      'content-type': 'application/json',
      'content-length': '371'
    },
    config: {
      transitional: [Object],
      adapter: [Array],
      transformRequest: [Array],
      transformResponse: [Array],
      timeout: 0,
      xsrfCookieName: 'XSRF-TOKEN',
      xsrfHeaderName: 'X-XSRF-TOKEN',
      maxContentLength: -1,
      maxBodyLength: -1,
      env: [Object],
      validateStatus: [Function: validateStatus],
      headers: [Object [AxiosHeaders]],
      method: 'post',
      url: 'http://127.0.0.1:8898/v1/producer/schedule_protocol_feature_activations',
      data: '{"protocol_features_to_activate":["0ec7e080177b2c02b278d5088611686b49d739925a92d9bfcacd7fc6b74053bd"]}',
      allowAbsoluteUrls: true
    },
    request: <ref *1> ClientRequest {
      _events: [Object: null prototype],
      _eventsCount: 7,
      _maxListeners: undefined,
      outputData: [],
      outputSize: 0,
      writable: true,
      destroyed: true,
      _last: false,
      chunkedEncoding: false,
      shouldKeepAlive: true,
      maxRequestsOnConnectionReached: false,
      _defaultKeepAlive: true,
      useChunkedEncodingByDefault: true,
      sendDate: false,
      _removedConnection: false,
      _removedContLen: false,
      _removedTE: false,
      strictContentLength: false,
      _contentLength: 102,
      _hasBody: true,
      _trailer: '',
      finished: true,
      _headerSent: true,
      _closed: true,
      _header: 'POST /v1/producer/schedule_protocol_feature_activations HTTP/1.1\r\n' +
        'Accept: application/json, text/plain, */*\r\n' +
        'Content-Type: application/json\r\n' +
        'User-Agent: axios/1.13.6\r\n' +
        'Content-Length: 102\r\n' +
        'Accept-Encoding: gzip, compress, deflate, br\r\n' +
        'Host: 127.0.0.1:8898\r\n' +
        'Connection: keep-alive\r\n' +
        '\r\n',
      _keepAliveTimeout: 0,
      _onPendingData: [Function: nop],
      agent: [Agent],
      socketPath: undefined,
      method: 'POST',
      maxHeaderSize: undefined,
      insecureHTTPParser: undefined,
      joinDuplicateHeaders: undefined,
      path: '/v1/producer/schedule_protocol_feature_activations',
      _ended: true,
      res: [IncomingMessage],
      aborted: false,
      timeoutCb: null,
      upgradeOrConnect: false,
      parser: null,
      maxHeadersCount: null,
      reusedSocket: true,
      host: '127.0.0.1',
      protocol: 'http:',
      _redirectable: [Writable],
      [Symbol(shapeMode)]: false,
      [Symbol(kCapture)]: false,
      [Symbol(kBytesWritten)]: 0,
      [Symbol(kNeedDrain)]: false,
      [Symbol(corked)]: 0,
      [Symbol(kChunkedBuffer)]: [],
      [Symbol(kChunkedLength)]: 0,
      [Symbol(kSocket)]: [Socket],
      [Symbol(kOutHeaders)]: [Object: null prototype],
      [Symbol(errored)]: null,
      [Symbol(kHighWaterMark)]: 65536,
      [Symbol(kRejectNonStandardBodyWrites)]: false,
      [Symbol(kUniqueHeaders)]: null
    },
    data: { code: 500, message: 'Internal Service Error', error: [Object] }
  },
  status: 500
}
on set:  {
  name: 'eosio.boot',
  path: '/home/admin/mono-ai-2/components/contracts/build/contracts/system/contracts/eosio.boot',
  target: 'eosio'
}
wasm_path /home/admin/mono-ai-2/components/contracts/build/contracts/system/contracts/eosio.boot/eosio.boot.wasm
abi_path /home/admin/mono-ai-2/components/contracts/build/contracts/system/contracts/eosio.boot/eosio.boot.abi
contract setted:  eosio
on set:  {
  name: 'eosio.system',
  path: '/home/admin/mono-ai-2/components/contracts/build/contracts/system/contracts/eosio.system',
  target: 'eosio'
}
wasm_path /home/admin/mono-ai-2/components/contracts/build/contracts/system/contracts/eosio.system/eosio.system.wasm
abi_path /home/admin/mono-ai-2/components/contracts/build/contracts/system/contracts/eosio.system/eosio.system.abi
contract setted:  eosio
on set:  {
  name: 'eosio.token',
  path: '/home/admin/mono-ai-2/components/contracts/build/contracts/system/contracts/eosio.token',
  target: 'eosio.token'
}
wasm_path /home/admin/mono-ai-2/components/contracts/build/contracts/system/contracts/eosio.token/eosio.token.wasm
abi_path /home/admin/mono-ai-2/components/contracts/build/contracts/system/contracts/eosio.token/eosio.token.abi
 ELIFECYCLE  Command failed with exit code 1.

 Lerna (powered by Nx)   Running target boot for project @coopenomics/boot failed

Failed tasks:

- @coopenomics/boot:boot

Hint: run the command with --verbose for more details.

 ELIFECYCLE  Command failed with exit code 1.
(booter.ts → cooperative.ts createPrograms) — именно так локальная dev-цепочка
получила 'blagorost' вместо 'capital' и воспроизвела баг, которого на проде
никогда не было.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 11:50:04 +00:00
coopops 5360daa471 fix(capital): correct BLAGOROST_AGREEMENT_TYPE to match on-chain 'blagorost'
Typecheck / desktop (pull_request) Failing after 8m53s
Typecheck / controller (pull_request) Has been cancelled
Константа хранила 'capital', но boot/src/init/cooperative.ts createPrograms()
сеет программу с type='blagorost' на генезисе (soviet::createprog), на всех
кооперативах. account.adapter.ts искал coagreement по 'capital' — не находил,
programId откатывался на 0, и вместо wallet::signagree уходил
soviet::sndagreement, который на цепи падает: get_coagreement_or_fail не
находит type='capital' в coagreements — «Соглашение указанного типа не
найдено». Ловилось при приёме платежа регистрации (registerBlockchainAccount
из gateway.interactor при PaymentType.REGISTRATION), падало для КАЖДОГО
участника, зарегистрированного по офферте Благорост.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 11:41:03 +00:00
coopops 652f7c722e fix(cooptypes): add missing space after colon before eoap_definition placeholder
Typecheck / desktop (pull_request) Failing after 8m50s
Typecheck / controller (pull_request) Successful in 13m45s
п.1.1 «а именно:{{ doc_data.eoap_definition }}» без пробела — во всех шести
шаблонах ЦПП (994/995/996/998/999/1000), фраза идентична во всех программах.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 08:46:00 +00:00
coopops a2b132b457 fix(registration): resolve doc_data_hash for extension offers via registry spec
Typecheck / controller (pull_request) Has been cancelled
Typecheck / desktop (pull_request) Has been cancelled
Шаблоны оферт ЦПП (#996/#1000) с c7824545cf требуют PrivateData, но общий
путь регистрации (RegistrationDocumentsService) не передавал doc_data_hash —
регистрация пайщика по GENERATION/CAPITALIZATION падала на генерации превью.

Ядро остаётся слепым к расширениям: AgreementRegistrationSpec получает
опциональный resolve_doc_data_hash, который расширение-владелец прикрепляет
к своим офертам при регистрации в реестре. Capital резолвит hash свежим
чтением своего конфига на каждую генерацию (совет может пересохранить
параметры без перезапуска). Гранулярность — per-оферта: при разделении
параметров по программам ядро менять не потребуется.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 08:41:57 +00:00
coopops a6cc71d6a3 fix(extension): unwrap TypeORM [rows, count] tuple in patchConfig UPDATE...RETURNING
TypeORM возвращает для UPDATE/DELETE кортеж [rows, affectedCount] (в отличие
от INSERT с голым rows-массивом) — без распаковки row оказывался кортежем и
все поля config читались как undefined, роняя buildState онбординга.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 08:41:45 +00:00
coopops b67581ce80 fix(extension): atomic jsonb config patch to stop onboarding flag lost-updates
Typecheck / desktop (pull_request) Failing after 22m0s
Typecheck / controller (pull_request) Successful in 13m37s
extensions.config is a single jsonb blob shared by every flag/hash the L1
onboarding wizards (capital, chairman) write to. Every write site did the
classic read-modify-write: findByName() the whole row, spread+mutate one key
in app memory, then update() the whole config back. Two DecisionTrackedEvent
handlers firing concurrently (two council decisions tracked near-simultaneously)
each read the same stale snapshot and each write their own flag back — whichever
write lands last wins and silently reverts the other one's flag to its old value.
Symptom hit live: onboarding_generator_program_template_done reverted to false
(with its hash still present, proving the decision really was tracked) after
onboarding_generation_contract_template_done raced it to true.

Added ExtensionDomainRepository.patchConfig(name, patch), implemented as a single
`UPDATE extensions SET config = config || patch::jsonb ... RETURNING *` — no
app-side read before the write, Postgres serializes concurrent UPDATEs on the
row so two different-key patches can no longer clobber each other. Replaced
every config read-modify-write in capital's and chairman's onboarding services
(the events handler's flag write, loadPlugin's init/expire timestamps, the
hash writes in completeStep/completeGeneralMeet, saveProgramDocDataHash) with
this atomic patch. update() is untouched for other callers.

Live data recovered manually: onboarding_generator_program_template_done was
patched back to true for voskhod/capital (hash was already present, confirming
the decision had in fact been tracked before the race reverted the flag).
2026-07-19 07:56:00 +00:00
coopops 024d46ff64 fix update 2026-07-19 07:55:49 +00:00
coopops 9030d28c6b fix(cooptypes): remove п.8.1 «Под Сайтом» from BlagorostProgramTemplate, renumber
Typecheck / desktop (pull_request) Failing after 9m37s
Typecheck / controller (pull_request) Successful in 14m3s
По решению: универсал-документ (ЦПП БЛАГОРОСТ_финал_универсал.docx.rtf,
~/blago/production/shared) — новый правильный эталон. В нём раздел 8 «Прочие условия»
содержит только 2 пункта (8.1 «Общество и Участник...», 8.2 «Изменения и дополнения...»),
без пункта про «Под Сайтом... прочие программные продукты» — ни в Положении, ни в
Оферте БЛАГОРОСТ (universal) такого пункта нет.

Убрал п.8.1 из rawContext, сдвинул 8.2→8.1, 8.3→8.2. Ссылка "(п. 7.1.)" в новом 8.2
не требует правки — форс-мажор остаётся разделом 7 независимо от сдвига в разделе 8.
Теперь 998 совпадает с универсалом точь-в-точь (сверено скриптом по номерам пунктов
и по тексту под каждым номером).
2026-07-19 07:31:02 +00:00
coopops 6e0e68c1e7 fix(cooptypes): correct cross-reference in GeneratorProgramTemplate п.12.3
Typecheck / controller (pull_request) Has been cancelled
Typecheck / desktop (pull_request) Has been cancelled
Systematic clause-numbering comparison against Положение_ЦПП_ГЕНЕРАТОР_универсал.docx.rtf
(~/blago/production/shared) found п.12.3 pointing at "(п. 12.1.)" — that's the "Под Сайтом"
definition clause, unrelated. The universal document (and the sentence's own subject —
"изменения условий ЦПП... возможность внесения которых прямо предусмотрена настоящим
Положением") both point to п.11.1, the форс-мажор/legislative-changes clause that actually
grants this right. Fixed the reference in rawContext only; left the orphaned translations
copy (994's dead legacy i18n block, unused by rendering) untouched.
2026-07-19 07:26:44 +00:00
coopops c079785019 fix(cooptypes): remove orphaned п.5.2 in BlagorostProgramTemplate, renumber section 5
Typecheck / desktop (pull_request) Failing after 9m2s
Typecheck / controller (pull_request) Successful in 13m50s
Сверил с ЦПП БЛАГОРОСТ_финал_универсал.docx.rtf (~/blago/production/shared) — в
актуальной версии Положения самостоятельный п.5.2 «Основной экономический эффект
функционирования Платформы достигается за счет членских взносов Участников при
расширении круга пользователей Платформы» отсутствует: его смысл слит в п.5.1
("...экономический эффект от функционирования которого заключается в следующем: ...").
Оферта (999/1000.Blagorost*, уже сверено) была такой всегда — 4.1 (источник) сразу
за которым 4.2 (дополнительный источник), без промежуточного пункта.

Убрал п.5.2 из rawContext, сдвинул 5.3–5.10 на 5.2–5.9, поправил внутреннюю ссылку
"указанными в п. 5.9." → "указанными в п. 5.8." в новом 5.9 (бывший 5.10). Дублирующий
текст в дохлом export const translations (не используется рендером, см. context) не трогал.
2026-07-18 18:55:46 +00:00
coopops bbc6453b24 fix(capital): await onSaved before advancing wizard past БЛАГОРОСТ step
Typecheck / desktop (pull_request) Failing after 8m49s
Typecheck / controller (pull_request) Successful in 13m45s
goNext() checked getFirstAvailableCouncilGroup() (which reads isDocParamsReady,
derived from onboardingState.capital_program_doc_data_hash) synchronously right
after saveParams() resolved. But saveParams() fired options.onSaved(hash) without
awaiting it, and the card's onSaved handler did `void handleDocParamsSaved()` —
an async refetch of onboarding state — so isDocParamsReady was still stale at the
moment goNext() decided which council group to jump to. getFirstAvailableCouncilGroup()
returned null, setWizardStepKey never ran, and the wizard just re-rendered the same
БЛАГОРОСТ step, looking like the just-saved params were never registered.

Fix: onSaved is now awaited inside saveParams(), and the card passes the
handleDocParamsSaved() promise through instead of firing it and forgetting it.
2026-07-18 18:27:57 +00:00
coopops b5eb0ea51f fix(capital): address PR #154 review — proper interface instead of as-cast, IInput['data'] canon
Typecheck / desktop (pull_request) Failing after 23m23s
Typecheck / controller (pull_request) Failing after 21m57s
document-domain.service.ts: replaced the inline `as Cooperative.Document.IGenerate & {...}`
hack with a dedicated GenerateDocumentWithPrivateDataDomainInterface for the doc_data
pre-generation branch (Cooperative.Document.IGenerate already carries a `[key: string]: any`
index signature, so the narrow interface is assignable without any cast at all).

Onboarding api/index.ts: saveProgramDocDataHash now takes
Mutations.Capital.SaveCapitalProgramDocDataHash.IInput['data'] and forwards it as-is to
variables.data, matching the SDK IInput['data'] canon used by completeStep right above it
instead of taking a bare string and rebuilding the payload inline. Updated both call sites.
2026-07-18 15:39:57 +00:00
coopops a072364d00 fix(capital): auto-generate CPP doc preview on step entry, restore section header formatting
Typecheck / desktop (pull_request) Failing after 20m30s
Typecheck / controller (pull_request) Successful in 12m40s
Preview for ГЕНЕРАТОР/БЛАГОРОСТ params no longer requires a manual button click on
first visit — onMounted now calls ensurePreview for the active wizard step. Also
fixes wizardStepOrder typing (was narrower than the runtime string values it holds).

Restores <h3> centered section-title markup in registry templates 994/995/996/998/999/1000
that was lost during the "parameterize CPP templates via private data" refactor, when
translations+context i18n structure was flattened into raw HTML.
2026-07-17 15:21:01 +00:00
coopops ed9b0879d5 chore(release): publish
Build bootstrap container / build (push) Successful in 2m42s
Release / release (push) Successful in 22m23s
Release / publish-packages (push) Successful in 14m53s
Release / trigger-coopenomics-docs (push) Successful in 2s
Release / trigger-mono-docs (push) Successful in 2s
2026-07-17 14:41:21 +00:00
coopops c94043e77f fix(docker): не скачивать бинарники electron/puppeteer в mono-base
Их postinstall тянет тяжёлые сетевые загрузки (Electron zip, Chromium),
не нужные ни для build (unbuild/tsc/vite), ни для runtime — периодически
валил release job TLS-таймаутом, из-за чего publish-packages (needs: release)
не запускался вообще.
2026-07-17 14:40:39 +00:00
coopops c1a8f3e637 fix(boot/Dockerfile): изолировать pnpm install от electron/puppeteer
Build bootstrap container / build (push) Successful in 2m37s
--filter не ограничивает lifecycle-скрипты общего lockfile store — тянулись
и падали по сети чужие тяжёлые постинсталлы (electron/puppeteer из
desktop/notifications), boot их не использует. --ignore-scripts + точечный
pnpm rebuild --filter решают.
2026-07-17 14:13:35 +00:00
coopops 15fdf35dd3 chore(release): publish
Release / trigger-coopenomics-docs (push) Has been skipped
Release / trigger-mono-docs (push) Has been skipped
Build bootstrap container / build (push) Failing after 2m10s
Release / release (push) Failing after 15m13s
Release / publish-packages (push) Has been skipped
2026-07-17 14:02:54 +00:00
coopops 79b89695d1 chore(release): publish 2026-07-17 13:58:44 +00:00
coopops 7b27d0805c chore(release): publish
Release / release (push) Successful in 43m2s
Release / publish-packages (push) Has been cancelled
Release / trigger-coopenomics-docs (push) Has been cancelled
Release / trigger-mono-docs (push) Has been cancelled
Build bootstrap container / build (push) Successful in 46s
2026-07-17 12:54:54 +00:00
coopops e089dbd3fb ci(release): чинить publish-packages — генерировать controller/schema.gql перед publish
С 24 июня (минимум) publish-packages падает на каждом релизе main:
@coopenomics/sdk's prepublishOnly (pnpm run docs → generate-index-comments.ts)
читает components/controller/schema.gql, которого нет в чекауте и никто не
генерирует — ENOENT. lerna publish атомарный, поэтому НИ ОДИН пакет не
публикуется. Итог: main давно на v2026.7.9, а npm видел последний раз
2026.5.30-3 — полтора месяца фиксов (включая убранный kpp из
BankAccountDetailsInput, PR #81) не долетали до потребителей SDK.

Фикс: добавлен шаг generate-schema (изолированный, GraphQLSchemaBuilderModule,
без БД) ПОСЛЕ lerna run build — порядок важен, иначе ts-node падает на
устаревших dist соседних workspace-пакетов (проверено локально: cooptypes →
factory → inter → notifications, каждый был stale и валил компиляцию
controller'а по цепочке).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 12:54:33 +00:00
coopops 5230fab7c6 fix(capital): add blagorost_goal_reason field, stop reusing blagorost_goal_expansion in clause 2.1.1
Typecheck / desktop (pull_request) Failing after 9m28s
Typecheck / controller (pull_request) Successful in 14m6s
Clause 2.1.1 of БЛАГОРОСТ Положение/Оферта needs different text than the
intro paragraph, but both were wired to the same doc_data.blagorost_goal_expansion
variable (confirmed against the original "универсал" RTF sources), so 2.1.1
rendered with the wrong sentence in all three registries (998, 999, 1000).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-17 07:41:24 +00:00
Alex Ant 345aadbd6a update
Typecheck / desktop (pull_request) Failing after 8m25s
Typecheck / controller (pull_request) Successful in 14m12s
2026-07-14 22:31:19 +03:00
Alex Ant 33f1ac1a15 fix(capital): restore CPP templates from RTF sources and improve parameter constructor
Typecheck / desktop (pull_request) Failing after 18m32s
Typecheck / controller (pull_request) Failing after 6m52s
Align cooptypes/factory templates 994–1000 with original document wording,
correct doc_data substitution points, update voskhod backfill values, and
add onboarding UI with descriptive field labels and doc data hash saving.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-14 14:27:27 +03:00
Alex Ant 5de7e7c65b update 2026-07-13 20:53:21 +03:00
coopops bfbcd2d57f fix(desktop): починить «Продолжить» в оверлее выбора КУ
Кнопка снова submit формы из footer, BranchSelector синхронизирует selectedBranch, ошибки генерации документа показываются пользователю.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-09 16:02:19 +00:00
coopops ff5df68bce chore(release): publish
Release / publish-packages (push) Failing after 21m11s
Release / release (push) Successful in 1h35m7s
Release / trigger-coopenomics-docs (push) Successful in 3s
Release / trigger-mono-docs (push) Successful in 3s
Build bootstrap container / build (push) Successful in 10m29s
2026-07-09 05:49:14 +00:00
coopops a948f07a1b fix(desktop): закрепить кнопки оверлея выбора КУ на мобильных
Кнопки «Продолжить» и «подписать» вынесены в footer BaseDialog, чтобы они не уезжали за пределы экрана при прокрутке контента.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-09 05:48:16 +00:00
coopops 85bd044cd6 fix(desktop): перезагрузка списка КУ при показе оверлея выбора
Оверлей выбора кооперативного участка теперь подгружает список при переходе
в мажоритарный режим, показывает лоадер и блокирует «Продолжить» без выбора.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 20:46:55 +00:00
107 changed files with 2331 additions and 1305 deletions
+12
View File
@@ -390,6 +390,18 @@ jobs:
- run: pnpm install --frozen-lockfile
- run: pnpm lerna run build
# schema.gql не коммитится и не создаётся ничем в этом job'е, а
# @coopenomics/sdk's prepublishOnly (pnpm run docs → generate-index-comments.ts)
# читает components/controller/schema.gql и падает ENOENT без него — валя весь
# lerna publish (атомарный) молча с 30 мая. Скрипт изолированный (GraphQLSchemaBuilderModule,
# без TypeORM/Mongo/Redis) — не требует БД, но типизация controller'а резолвится через
# dist соседних workspace-пакетов (cooptypes/factory/notifications/inter) — ОБЯЗАТЕЛЬНО
# после `lerna run build`, иначе ts-node падает на их устаревших dist-типах.
- name: Generate controller schema.gql (required by sdk prepublishOnly)
working-directory: components/controller
run: pnpm run generate-schema
- run: pnpm lerna publish from-package --yes
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+8
View File
@@ -45,6 +45,14 @@ RUN corepack enable && npm install -g lerna --no-fund --no-audit
# выкидывает node_modules/dist/.git, поэтому `COPY .` лёгкий.
COPY . .
# electron (devDep desktop → @vue/devtools) и puppeteer нигде в build/runtime
# не запускаются (unbuild/tsc/vite их не трогают) — только их JS-обёртка нужна
# для типов. Их postinstall тянет тяжёлые бинарники (Electron zip с GitHub
# Releases, Chromium) по сети — периодически валит билд TLS-таймаутом.
# Отключаем сами загрузки, пакеты всё равно ставятся.
ENV ELECTRON_SKIP_BINARY_DOWNLOAD=1 \
PUPPETEER_SKIP_DOWNLOAD=true
# Используем существующий lockfile. Если он не совпадает с workspace —
# падать сразу, не дрейфовать незаметно.
RUN pnpm install --frozen-lockfile
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@coopenomics/blago-cli",
"version": "2026.6.24",
"version": "2026.7.21",
"description": "CLI синхронизации артефактов Благорост с бэкендом через @coopenomics/sdk",
"type": "module",
"private": true,
+8 -1
View File
@@ -35,7 +35,14 @@ RUN npm install -g pnpm@10.33.0
COPY . .
RUN pnpm install --frozen-lockfile --filter "@coopenomics/boot..."
# --ignore-scripts + точечный rebuild: --filter ограничивает только то, какие
# проекты получают node_modules, но НЕ то, для каких пакетов общего lockfile
# store выполняет lifecycle-скрипты при --frozen-lockfile. Без --ignore-scripts
# сюда тянутся electron/puppeteer (из @coopenomics/desktop/notifications,
# boot их не использует) — тяжёлые сетевые загрузки, изредка валящие билд по
# TLS-таймауту. rebuild --filter уже честно скопирован на подграф boot.
RUN pnpm install --frozen-lockfile --filter "@coopenomics/boot..." --ignore-scripts \
&& pnpm rebuild --filter "@coopenomics/boot..."
# Билд графа: cooptypes/factory собирают свои dist/, boot собирает
# свой bundle с inlined workspace-deps.
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@coopenomics/boot",
"type": "module",
"version": "2026.6.24",
"version": "2026.7.21",
"private": true,
"packageManager": "pnpm@9.0.6",
"description": "CLI-утилита инициализации блокчейна и кооператива",
+1 -1
View File
@@ -76,7 +76,7 @@ export class CooperativeClass {
await this.blockchain.createProgram({
coopname,
username: coopname,
type: 'blagorost',
type: 'capital',
title: 'Целевая потребительская программа "Благорост"',
announce: '',
description: '',
@@ -1,6 +1,6 @@
/**
* Фаза 01 — программы УХД (id=3, type='generator') и Благорост
* (id=4, type='blagorost') + capital::setconfig.
* (id=4, type='capital') + capital::setconfig.
*
* Идемпотентно: программы создаются только если их ещё нет в `programs`-таблице,
* setconfig — только если `state` пуст.
@@ -88,7 +88,7 @@ export async function phase01(): Promise<void> {
fixed_membership_contribution: '0.0000 RUB',
membership_percent_fee: '0',
meta: '',
type: 'blagorost',
type: 'capital',
}
await blockchain.api.transact({
actions: [{
@@ -300,8 +300,8 @@ async function ensureAgreementsAndWallet(blockchain: Blockchain, fix: IFixture):
}
// Согласие с ЦПП «Благорост» — для инвестиций в программу/компонент.
// Блокировка идёт по своему пути; для безопасности дёргаем при первом запуске.
await signAgreement(blockchain, fix.username, 'blagorost').catch((e) => {
log(`signAgreement blagorost для ${fix.username} — пропуск (${(e as Error).message?.slice(0, 80)})`)
await signAgreement(blockchain, fix.username, 'capital').catch((e) => {
log(`signAgreement capital для ${fix.username} — пропуск (${(e as Error).message?.slice(0, 80)})`)
})
}
@@ -234,7 +234,7 @@ describe('тест контракта CAPITAL', () => {
fixed_membership_contribution: '0.0000 RUB',
membership_percent_fee: '0',
meta: '',
type: 'blagorost',
type: 'capital',
}
const result = await blockchain.api.transact(
+1 -1
View File
@@ -213,7 +213,7 @@ describe('тест контракта CAPITAL', () => {
fixed_membership_contribution: '0.0000 RUB',
membership_percent_fee: '0',
meta: '',
type: 'blagorost',
type: 'capital',
}
const result = await blockchain.api.transact(
+1 -1
View File
@@ -12,6 +12,6 @@ export const capitalDraftId = 1000
export const ratePerHour = '1000.0000 RUB'
export const sourceProgramName = 'generator'
export const capitalProgramName = 'blagorost'
export const capitalProgramName = 'capital'
// ledger2::accounts.id хранит счёт со смещением *1000 (80 → 80000).
export const circulationAccountId = 80_000
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@coopenomics/cleos",
"version": "2026.6.24",
"version": "2026.7.21",
"private": true,
"description": "Обёртка над кошельком cleos для EOSIO блокчейна",
"scripts": {
+1 -1
View File
@@ -80,7 +80,7 @@ static constexpr eosio::name _wallet_program = "wallet"_n; ///< Главный
static constexpr eosio::name _marketplace_program = "marketplace"_n; ///< Кошелёк программы "Маркетплейс"
static constexpr eosio::name _source_program = "generator"_n; ///< Кошелёк для генерации по договору УХД
static constexpr eosio::name _capital_program = "blagorost"_n; ///< Кошелёк программы "Благорост"
static constexpr eosio::name _capital_program = "capital"_n; ///< Кошелёк программы "Благорост"
static const std::set<eosio::name> soviet_actions = {
@@ -12,14 +12,22 @@
void soviet::migrate() {
require_auth(_soviet); // Проверяем авторизацию
// Миграция program_type для кооператива voskhod, программа id=4 -> blagorost
eosio::name coopname = "voskhod"_n;
programs_index programs(_soviet, coopname.value);
auto program_it = programs.find(4);
if (program_it != programs.end() && program_it->program_type != "blagorost"_n) {
programs.modify(program_it, _soviet, [&](auto &pr) {
pr.program_type = "blagorost"_n;
});
// Откат program_type для кооператива voskhod, программа id=4: blagorost -> capital.
// Предыдущая версия этой миграции переименовывала programs.program_type в
// 'blagorost', но coagreements.type для той же программы (program_id=4) всегда
// оставался 'capital' и никогда не переименовывался — таблицы разъехались.
// 'capital' — каноничное значение (используется coagreements.type, contract-
// константой _capital_program и всем controller-кодом); эта миграция приводит
// programs.program_type к нему же. Идемпотентно.
{
eosio::name coopname = "voskhod"_n;
programs_index programs(_soviet, coopname.value);
auto program_it = programs.find(4);
if (program_it != programs.end() && program_it->program_type != "capital"_n) {
programs.modify(program_it, _soviet, [&](auto &pr) {
pr.program_type = "capital"_n;
});
}
}
// ────────────────────────────────────────────────────────────────────────
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@coopenomics/contracts",
"version": "2026.6.24",
"version": "2026.7.21",
"private": true,
"type": "module",
"scripts": {
@@ -14,34 +14,28 @@ const TARGET_COOPNAME = 'voskhod';
const COLLECTION_NAME = 'doc_private_data';
const capitalProgramPrivateData = {
approval_protocol_number: '1',
approval_protocol_day: '15',
approval_protocol_month: 'января',
approval_protocol_year: '26',
cooperative_name: 'ВОСХОД',
cooperative_short_name: 'ПК',
cooperative_quoted_name: '«ВОСХОД»',
website: 'https://цифровой-кооператив.рф',
chairman_full_name: 'Муравьев Алексей Николаевич',
generator_program_purpose:
'развитию информационной экосистемы взаимодействия физических и юридических лиц, на основе международных кооперативных принципов и законодательства Российской Федерации в отношении потребительских обществ (кооперативов) под названием “Кооперативная Экономика”',
"развитию информационной экосистемы взаимодействия физических и юридических лиц, на основе международных кооперативных принципов и законодательства Российской Федерации в отношении потребительских обществ (кооперативов) под названием “Кооперативная Экономика”, с целью самоорганизации и интеграции в социально-экономическую среду Российской Федерации., на основе международных кооперативных принципов и законодательства Российской Федерации",
eoap_definition:
'информационная экосистема, интегрируемая в социально-экономическую среду Российской Федерации, состоящая из комплекса программных продуктов на базе технологии распределенного реестра, обеспечивающих широкое экономическое и социальное взаимодействие физических и юридических лиц, включая нерезидентов различных юрисдикций и организационно-правовых форм, на основе международных кооперативных принципов и законодательства Российской Федерации в отношении потребительских кооперативов (обществ) под названием “Кооперативная Экономика”',
"информационная экосистема, интегрируемая в социально-экономическую среду Российской Федерации, состоящая из комплекса программных продуктов на базе технологии распределенного реестра, обеспечивающих широкое экономическое и социальное взаимодействие физических и юридических лиц, включая нерезидентов различных юрисдикций и организационно-правовых форм, на основе международных кооперативных принципов и законодательства Российской Федерации в отношении потребительских кооперативов (обществ) под названием “Кооперативная Экономика”",
generator_task_goal:
'центра привлечения и интеграции передовых инновационных цифровых разработок, а также экономических и социальных методов и решений',
idea_unit_cost: '50',
idea_unit_cost_words: 'пятьдесят',
"центра привлечения и интеграции передовых инновационных цифровых разработок, а также экономических и социальных методов и решений",
idea_unit_cost:
"50",
idea_unit_cost_words:
"пятьдесят",
blagorost_goal_expansion:
'вследствие увеличения количества Участников информационной кооперативной экосистемы - ЕОАП - для расширения и повышения социальной эффективности их экономического взаимодействия в некоммерческом формате',
"развитии информационной экосистемы взаимодействия физических и юридических лиц, на основе международных кооперативных принципов и законодательства Российской Федерации в отношении потребительских обществ (кооперативов) – под названием “Кооперативная Экономика”, с целью самоорганизации и интеграции в социально-экономическую среду Российской Федерации",
blagorost_goal_reason:
"вследствие увеличения количества Участников информационной кооперативной экосистемы - ЕОАП - для расширения и повышения социальной эффективности их экономического взаимодействия в некоммерческом формате",
blagorost_task_expansion:
'расширение участников ЕОАП - информационной кооперативной экосистемы как центра экономического взаимодействия в некоммерческом формате',
"расширение участников ЕОАП - информационной кооперативной экосистемы как центра экономического взаимодействия в некоммерческом формате",
blagorost_task_development:
'развитие ЕОАП как центра привлечения и интеграции инновационных цифровых разработок, а также экономических и социальных методов и решений',
"развитие ЕОАП как центра привлечения и интеграции инновационных цифровых разработок, а также экономических и социальных методов и решений",
return_source_description:
'аппаратно-программная сеть узлов распределенного реестра в формате «СМЭВ+SWIFT», построенная на принципах самоорганизации и самофинансирования деятельности технологической инфраструктуры ЕОАП, обеспечивающей консенсус ее распределенных узлов по формированию базового продукта ЕОАП - полного цикла документооборота по синхронному взаимодействию пайщиков и кооперативов - участников экосистемы ЕОАП - через использование цифровых контрактов, с одновременным выполнением функций нотариата и учета финансовых и юридических событий',
"аппаратно-программная сеть узлов распределенного реестра в формате «СМЭВ+SWIFT», построенная на принципах самоорганизации и самофинансирования деятельности технологической инфраструктуры ЕОАП, обеспечивающей консенсус ее распределенных узлов по формированию базового продукта ЕОАП - полного цикла документооборота по синхронному взаимодействию пайщиков и кооперативов - участников экосистемы ЕАОП - через использование цифровых контрактов, с одновременным выполнением функций нотариата и учета финансовых и юридических событий (подробнее на Сайте)",
return_additional_source:
'взносы пользователей ЕОАП, его отдельных программных продуктов и приложений, переданных Обществу или создаваемых в рамках Общества, которые интегрируются в ЕОАП',
offer_template_number: '______',
"взносы пользователей ЕОАП, его отдельных программных продуктов и приложений, переданных Обществу или создаваемых в рамках Общества, которые интегрируются в ЕОАП"
};
function stableStringify(value: unknown): string {
@@ -78,6 +72,9 @@ export default {
try {
mongo = await mongoose.createConnection(config.mongoose.url).asPromise();
const db = mongo.db;
if (!db) {
throw new Error('Подключение к MongoDB установлено, но db не инициализирована');
}
const collection = db.collection(COLLECTION_NAME);
await collection.createIndex({ hash: 1 }, { unique: true });
await collection.updateOne(
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "create-nodejs-express-app",
"version": "2026.6.24",
"version": "2026.7.21",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "create-nodejs-express-app",
"version": "2026.6.24",
"version": "2026.7.21",
"license": "MIT",
"dependencies": {
"@a2seven/yoo-checkout": "^1.1.4",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@coopenomics/controller",
"version": "2026.6.24",
"version": "2026.7.21",
"description": "Бэкенд GraphQL API кооператива на NestJS",
"private": true,
"bin": "bin/createNodejsApp.js",
@@ -14,6 +14,7 @@ const { spawnSync } = require('child_process');
const controllerRoot = path.resolve(__dirname, '..');
const schemaPath = path.join(controllerRoot, 'schema.gql');
const zeusConfigPath = path.join(controllerRoot, 'graphql-zeus.json');
const sdkSrc = path.resolve(controllerRoot, '..', 'sdk', 'src');
const localZeusDir = path.join(controllerRoot, 'zeus');
const sdkZeusDir = path.join(sdkSrc, 'zeus');
@@ -40,6 +41,12 @@ if (!fs.existsSync(sdkSrc)) {
process.exit(1);
}
// graphql-zeus 7.x: yargs кладёт path в argv.path, а CLIClass читает args._[0].
// Без graphql-zeus.json config-maker уходит в inquirer и «зависает» без TTY.
if (!fs.existsSync(zeusConfigPath)) {
fs.writeFileSync(zeusConfigPath, `${JSON.stringify({ urlOrPath: './schema.gql' }, null, 2)}\n`, 'utf8');
}
let zeusBin;
let zeusIsWin;
try {
@@ -51,6 +58,7 @@ try {
const zeusArgs = ['./schema.gql', '../sdk/src', '--subscriptions', 'graphql-ws'];
const startedAt = Date.now();
console.log('generate-client: graphql-zeus… (530 с без вывода — норма, не прерывайте)');
const result = spawnSync(zeusBin, zeusArgs, {
stdio: 'inherit',
cwd: controllerRoot,
@@ -4,15 +4,11 @@ import type { GetDocumentsInputDTO } from '../dto/get-documents-input.dto';
import type { PaginationResultDomainInterface } from '~/domain/common/interfaces/pagination.interface';
import type { DocumentPackageAggregateDomainInterface } from '~/domain/document/interfaces/document-package-aggregate-domain.interface';
import type { GenerateAnyDocumentInputDTO } from '../dto/generate-any-document-input.dto';
import { GeneratorInfrastructureService } from '~/infrastructure/generator/generator.service';
import { DocumentDomainEntity } from '~/domain/document/entity/document-domain.entity';
@Injectable()
export class DocumentService {
constructor(
private readonly documentInteractor: DocumentInteractor,
private readonly generatorService: GeneratorInfrastructureService
) {}
constructor(private readonly documentInteractor: DocumentInteractor) {}
async getDocumentsAggregate(
data: GetDocumentsInputDTO
@@ -34,7 +30,7 @@ export class DocumentService {
}
async generateAnyDocument(input: GenerateAnyDocumentInputDTO): Promise<DocumentDomainEntity> {
return this.generatorService.generateDocument({
return this.documentInteractor.generateDocument({
data: input.data as any,
options: input.options,
});
@@ -6,7 +6,7 @@ import {
UserCertificateDomainService,
USER_CERTIFICATE_DOMAIN_SERVICE,
} from '~/domain/user/services/user-certificate-domain.service';
import { Cooperative, SovietContract, WalletContract } from 'cooptypes';
import { Cooperative, SovietContract } from 'cooptypes';
import { getActions } from '~/utils/getFetch';
import type { DocumentPackageAggregateDomainInterface } from '../interfaces/document-package-aggregate-domain.interface';
import type { StatementDetailAggregateDomainInterface } from '../interfaces/statement-detail-aggregate-domain.interface';
@@ -298,9 +298,17 @@ export class DocumentPackageV1Aggregator {
/**
* Находит on-chain действие-носитель подписанного связанного документа по doc_hash.
* Сначала ищет в реестре соглашений совета (soviet::newagreement), затем
* в программных подписях кошелька (wallet::signagree). Возвращает поле document
* найденного действия либо null.
* Сначала ищет в реестре соглашений совета (soviet::newagreement второе,
* явное уведомление, которое дополнительно шлёт soviet::sndagreement), затем
* в общем уведомлении о завершённом документе (soviet::newresolved), которое
* централизованно шлёт make_complete_document (lib/core/soviet/soviet.hpp) на
* КАЖДЫЙ вызов и из soviet::sndagreement, и из wallet::signagree с
* require_recipient(coopname), заданным один раз внутри newresolved.cpp.
* Программные оферты (capital: Благорост/Генератор, program_id>0) идут через
* wallet::signagree и не порождают newagreement, но newresolved у них есть
* всегда расширять notification-поверхность самого wallet::signagree не
* нужно, единственное место для этого уже newresolved.cpp.
* Возвращает поле document найденного действия либо null.
*/
private async findLinkedAgreementDocument(
linkHash: string
@@ -321,10 +329,10 @@ export class DocumentPackageV1Aggregator {
const sovietDocument = sovietAgreement?.results?.[0]?.data?.document;
if (sovietDocument) return sovietDocument;
const walletAgreement = await getActions(`${process.env.SIMPLE_EXPLORER_API}/get-actions`, {
const resolvedAgreement = await getActions(`${process.env.SIMPLE_EXPLORER_API}/get-actions`, {
filter: JSON.stringify({
account: WalletContract.contractName.production,
name: WalletContract.Actions.SignAgreement.actionName,
account: SovietContract.contractName.production,
name: SovietContract.Actions.Registry.NewResolved.actionName,
receiver: process.env.COOPNAME,
'data.document.doc_hash': docHashFilter,
}),
@@ -332,8 +340,8 @@ export class DocumentPackageV1Aggregator {
limit: 1,
});
const walletDocument = walletAgreement?.results?.[0]?.data?.document;
if (walletDocument) return walletDocument;
const resolvedDocument = resolvedAgreement?.results?.[0]?.data?.document;
if (resolvedDocument) return resolvedDocument;
return null;
}
@@ -0,0 +1,13 @@
import type { Cooperative } from 'cooptypes';
/**
* Узкая задача пред-генерации: документ, у которого часть полей приватна и
* хранится off-chain (`doc_data`) `DocumentDomainService.generateDocument`
* сохраняет `doc_data` через `saveDocData` и публикует дальше только
* `doc_data_hash`. См. `Cooperative.Document.IDocDataRef` в cooptypes
* on-chain-сторона того же контракта.
*/
export interface GenerateDocumentWithPrivateDataDomainInterface extends Cooperative.Document.IGenerate {
doc_data?: Record<string, unknown>;
doc_data_hash?: string;
}
@@ -10,6 +10,7 @@ import { DocumentPackageAggregator } from '../aggregators/document-package.aggre
import { getActions } from '~/utils/getFetch';
import { toDotNotation } from '~/utils/toDotNotation';
import type { ISignedDocumentDomainInterface } from '../interfaces/signed-document-domain.interface';
import type { GenerateDocumentWithPrivateDataDomainInterface } from '../interfaces/generate-document-with-private-data.interface';
@Injectable()
export class DocumentDomainService {
@@ -22,10 +23,7 @@ export class DocumentDomainService {
) {}
public async generateDocument(data: GenerateDocumentDomainInterfaceWithOptions): Promise<DocumentDomainEntity> {
const documentData = data.data as Cooperative.Document.IGenerate & {
doc_data?: Record<string, unknown>;
doc_data_hash?: string;
};
const documentData: GenerateDocumentWithPrivateDataDomainInterface = data.data;
if (documentData.doc_data) {
const { doc_data, ...publicDocumentData } = documentData;
@@ -38,7 +36,7 @@ export class DocumentDomainService {
data: {
...publicDocumentData,
doc_data_hash: hash,
} as Cooperative.Document.IGenerate,
},
});
}
@@ -8,6 +8,13 @@ export interface ExtensionDomainRepository<TConfig = any> {
create(data: Partial<ExtensionDomainEntity<TConfig>>): Promise<ExtensionDomainEntity<TConfig>>;
find(filter?: Partial<ExtensionDomainEntity<TConfig>>): Promise<ExtensionDomainEntity<TConfig>[]>;
update(data: Partial<ExtensionDomainEntity<TConfig>>): Promise<ExtensionDomainEntity<TConfig>>;
/**
* Атомарно мёржит `patch` в jsonb-колонку `config` одним UPDATE (`config || patch`),
* без чтения-изменения-записи всего объекта в памяти приложения. Конкурентные вызовы
* с разными ключами patch не теряют изменения друг друга (в отличие от `update()`,
* который перезаписывает весь config целиком и подвержен lost update при гонке).
*/
patchConfig(name: string, patch: Partial<TConfig>): Promise<ExtensionDomainEntity<TConfig>>;
}
export const EXTENSION_REPOSITORY = Symbol('ExtensionDomainRepository'); // Создаем уникальный токен
@@ -60,6 +60,14 @@ export interface IAgreementConfigItem {
* Порядок отображения на фронтенде (сортировка)
*/
order: number;
/**
* Резолвер hash'а приватных данных документа (doc_data_hash), если шаблон
* оферты на фабрике требует PrivateData. Заполняется только для
* extension-оферт из AgreementRegistrationSpec; платформенные соглашения
* его не имеют. В GraphQL DTO не попадает (поля мапятся декораторами).
*/
resolve_doc_data_hash?: () => Promise<string | undefined>;
}
/**
@@ -55,4 +55,18 @@ export interface AgreementRegistrationSpec {
* реестр удаляет все записи с этим extension_name.
*/
extension_name: string;
/**
* Опциональный резолвер hash'а приватных данных документа (doc_data_hash).
* Если шаблон оферты на фабрике требует PrivateData (например, параметры
* ЦПП, утверждённые советом), расширение предоставляет резолвер ядро
* вызывает его перед каждой генерацией документа этой оферты и передаёт
* результат в фабрику как doc_data_hash.
*
* Резолвер вызывается на каждую генерацию (не кэшируется): источник
* значения конфигурация расширения, которая может обновляться без
* перезапуска (совет пересохранил параметры программ). Ядро не знает,
* откуда берётся hash это контракт расширения с его фабричным шаблоном.
*/
resolve_doc_data_hash?: () => Promise<string | undefined>;
}
@@ -50,6 +50,7 @@ export class AgreementConfigurationService {
link_to_statement: true,
applicable_account_types: spec.applicable_account_types,
order: spec.order,
resolve_doc_data_hash: spec.resolve_doc_data_hash,
};
}
@@ -132,11 +132,17 @@ export class RegistrationDocumentsService {
): Promise<IGeneratedRegistrationDocument> {
this.logger.debug(`Генерация документа: ${config.id} (registry_id=${config.registry_id})`);
// Если шаблон оферты требует PrivateData, расширение-владелец предоставило
// резолвер hash'а в спеке регистрации — ядро не знает ни источника значения,
// ни того, какие registry_id этого требуют.
const doc_data_hash = config.resolve_doc_data_hash ? await config.resolve_doc_data_hash() : undefined;
const document = await this.documentInteractor.generateDocument({
data: {
coopname,
username,
registry_id: config.registry_id,
...(doc_data_hash ? { doc_data_hash } : {}),
},
options: {
skip_save: false, // Сохраняем документ в базу для последующей сверки
@@ -36,6 +36,14 @@ export class CapitalOnboardingStepInputDTO {
decision!: string;
}
@InputType('SaveCapitalProgramDocDataInput')
export class SaveCapitalProgramDocDataInputDTO {
@Field(() => String)
@IsString()
@IsNotEmpty()
doc_data_hash!: string;
}
@ObjectType('CapitalOnboardingState')
export class CapitalOnboardingStateDTO {
@Field(() => Boolean)
@@ -26,10 +26,17 @@ import type { IConfig } from '../../capital-extension.module';
* при завершённом L1 две оферты (generator/blagorost) и две
* программы (generation/capitalization);
* идемпотентность гарантируется AgreementRegistryService.
*
* `resolveDocDataHash` резолвер hash'а PrivateData параметров ЦПП,
* прикрепляется к обеим офертам: их фабричные шаблоны (#996/#1000) требуют
* doc_data. Сейчас все документы ЦПП читают единый набор параметров
* (capital_program_doc_data_hash); если параметры разделятся по программам,
* каждая спека получит собственный резолвер ядро менять не потребуется.
*/
export function registerCapitalInAgreementRegistry(
port: AgreementRegistrationPort,
extensionConfig: IConfig
extensionConfig: IConfig,
resolveDocDataHash?: () => Promise<string | undefined>
): boolean {
const onboardingDone =
extensionConfig.onboarding_generator_program_template_done &&
@@ -52,6 +59,7 @@ export function registerCapitalInAgreementRegistry(
applicable_account_types: [],
order: 6,
extension_name: CAPITAL_EXTENSION_NAME,
resolve_doc_data_hash: resolveDocDataHash,
});
port.registerAgreement({
@@ -70,6 +78,7 @@ export function registerCapitalInAgreementRegistry(
applicable_account_types: [],
order: 5,
extension_name: CAPITAL_EXTENSION_NAME,
resolve_doc_data_hash: resolveDocDataHash,
});
port.registerProgram({
@@ -3,7 +3,11 @@ import { UseGuards } from '@nestjs/common';
import { GqlJwtAuthGuard } from '~/application/auth/guards/graphql-jwt-auth.guard';
import { RolesGuard } from '~/application/auth/guards/roles.guard';
import { AuthRoles } from '~/application/auth/decorators/auth.decorator';
import { CapitalOnboardingStepInputDTO, CapitalOnboardingStateDTO } from '../dto/onboarding.dto';
import {
CapitalOnboardingStepInputDTO,
CapitalOnboardingStateDTO,
SaveCapitalProgramDocDataInputDTO,
} from '../dto/onboarding.dto';
import { CapitalOnboardingService } from '../services/onboarding.service';
import { CurrentUser } from '~/application/auth/decorators/current-user.decorator';
import type { MonoAccountDomainInterface } from '~/domain/account/interfaces/mono-account-domain.interface';
@@ -34,4 +38,16 @@ export class CapitalOnboardingResolver {
): Promise<CapitalOnboardingStateDTO> {
return this.onboardingService.completeStep(data, currentUser?.username);
}
@Mutation(() => CapitalOnboardingStateDTO, {
name: 'saveCapitalProgramDocDataHash',
description: 'Сохранить hash PrivateData параметров документов ЦПП',
})
@UseGuards(GqlJwtAuthGuard, RolesGuard)
@AuthRoles(['chairman'])
async saveProgramDocDataHash(
@Args('data', { type: () => SaveCapitalProgramDocDataInputDTO }) data: SaveCapitalProgramDocDataInputDTO,
): Promise<CapitalOnboardingStateDTO> {
return this.onboardingService.saveProgramDocDataHash(data.doc_data_hash);
}
}
@@ -73,13 +73,12 @@ export class CapitalOnboardingEventsService {
const wasAlreadyDone = !!(plugin.config as any)[flagKey];
// Обновляем флаг завершения шага
const updatedConfig = {
...plugin.config,
// Атомарный частичный UPDATE (config || patch) вместо read-modify-write всего
// config целиком: два DecisionTrackedEvent для разных шагов, обработанные
// конкурентно, иначе теряют флаг друг друга (lost update на общем jsonb-блобе).
const updated = await this.extensionRepository.patchConfig('capital', {
[flagKey]: true,
};
await this.extensionRepository.update({ ...plugin, config: updatedConfig });
} as Partial<IConfig>);
this.logger.info(`Флаг ${flagKey} установлен в true`);
@@ -89,7 +88,7 @@ export class CapitalOnboardingEventsService {
// restartApp('capital'), чтобы initialize(config) увидел свежий config
// и зарегистрировал оферты/программы в платформенном реестре.
// Совет ничего не нажимает (раздел 7.1 плана C28-10, вариант (в)).
if (!wasAlreadyDone && this.isL1Complete(updatedConfig as IConfig)) {
if (!wasAlreadyDone && this.isL1Complete(updated.config as IConfig)) {
this.logger.info(
'[ONBOARDING_COMPLETED] capital L1 завершён последним шагом, эмиттим событие для auto-restart'
);
@@ -127,21 +127,24 @@ export class CapitalOnboardingService {
if (!plugin) throw new Error('Конфигурация расширения capital не найдена');
const pluginConfig: CapitalOnboardingConfig = { ...plugin.config };
let needUpdate = false;
const patch: Partial<CapitalOnboardingConfig> = {};
if (!pluginConfig.onboarding_init_at) {
pluginConfig.onboarding_init_at = new Date().toISOString();
needUpdate = true;
patch.onboarding_init_at = pluginConfig.onboarding_init_at;
}
if (!pluginConfig.onboarding_expire_at) {
const start = new Date(pluginConfig.onboarding_init_at);
pluginConfig.onboarding_expire_at = computeOnboardingExpiresAt(start);
needUpdate = true;
patch.onboarding_expire_at = pluginConfig.onboarding_expire_at;
}
if (needUpdate) {
await this.extensionRepository.update({ ...plugin, config: pluginConfig });
return { ...plugin, config: pluginConfig };
if (Object.keys(patch).length > 0) {
// Точечный merge только изменившихся полей — полная перезапись config
// целиком (как раньше) конкурирует с параллельными записями других шагов
// онбординга (флаги, хэши) и теряет их изменения (lost update).
const updated = await this.extensionRepository.patchConfig('capital', patch);
return { ...plugin, config: updated.config };
}
return { ...plugin, config: pluginConfig };
@@ -170,6 +173,21 @@ export class CapitalOnboardingService {
return this.buildState(plugin.config);
}
public async saveProgramDocDataHash(docDataHash: string): Promise<CapitalOnboardingStateDTO> {
await this.loadPlugin();
const normalizedHash = docDataHash.trim();
if (!normalizedHash) {
throw new Error('Hash PrivateData документов ЦПП не может быть пустым');
}
const updated = await this.extensionRepository.patchConfig('capital', {
capital_program_doc_data_hash: normalizedHash,
} as Partial<CapitalOnboardingConfig>);
return this.buildState(updated.config as CapitalOnboardingConfig);
}
public async completeStep(data: CapitalOnboardingStepInputDTO, username: string): Promise<CapitalOnboardingStateDTO> {
const plugin = await this.loadPlugin();
const flagKey = this.mapStepToFlag(data.step);
@@ -218,12 +236,12 @@ export class CapitalOnboardingService {
document: documentForPublish,
});
// Сохраняем hash в конфиге для отображения на фронтенде
const updatedConfig = {
...plugin.config,
// Сохраняем hash в конфиге для отображения на фронтенде — точечный merge
// (не полная перезапись config), чтобы не потерять конкурентно записанные
// флаги/хэши других шагов онбординга (lost update на общем jsonb-блобе).
const updated = await this.extensionRepository.patchConfig('capital', {
[hashKey]: generatedDoc.hash,
};
await this.extensionRepository.update({ ...plugin, config: updatedConfig });
} as Partial<CapitalOnboardingConfig>);
// Регистрируем правило отслеживания в фабрике
const varsField = this.mapStepToVarsField(data.step);
@@ -239,6 +257,6 @@ export class CapitalOnboardingService {
},
});
return this.buildState(updatedConfig);
return this.buildState(updated.config as CapitalOnboardingConfig);
}
}
@@ -654,7 +654,25 @@ export class CapitalPlugin extends BaseExtModule {
// Если L1 ещё не завершён — реестр остаётся пустым, SignUp не предлагает
// программы capital (раздел 4.1 req 44 проекта 13).
try {
const registered = registerCapitalInAgreementRegistry(this.agreementRegistrationPort, extensionConfig as IConfig);
// Резолвер читает конфиг заново на каждую генерацию: совет может
// пересохранить параметры ЦПП (новый hash) без перезапуска расширения,
// а спека в реестре регистрируется один раз на initialize.
const resolveCapitalProgramDocDataHash = async (): Promise<string | undefined> => {
const ext = await this.extensionRepository.findByName(this.name);
const hash = ext?.config?.capital_program_doc_data_hash?.trim();
if (!hash) {
throw new Error(
'Параметры документов ЦПП не заполнены: отсутствует capital_program_doc_data_hash в конфигурации capital'
);
}
return hash;
};
const registered = registerCapitalInAgreementRegistry(
this.agreementRegistrationPort,
extensionConfig as IConfig,
resolveCapitalProgramDocDataHash
);
if (registered) {
this.logger.log('[CAPITAL.REGISTRY] зарегистрировано 2 оферты + 2 программы capital');
} else {
@@ -23,9 +23,13 @@ export const GENERATOR_OFFER_AGREEMENT_ID = 'generator_offer';
export const BLAGOROST_OFFER_AGREEMENT_ID = 'blagorost_offer';
export const GENERATOR_AGREEMENT_TYPE = 'generator';
// On-chain имя оферты Благорост в `soviet::coagreements` — 'capital' (program_id=4).
// Контроллер обязан слать sndagreement/signagree с этим значением, иначе
// `get_coagreement_or_fail` падает с «Соглашение указанного типа не найдено».
// On-chain имя оферты Благорост в `soviet::coagreements` (_capital_program в
// lib/consts.hpp контракта) — 'capital' (program_id=4). Подтверждено прямым
// запросом к продовой цепочке voskhod (185.70.105.87). Значение здесь ОБЯЗАНО
// совпадать буквально с контрактной константой — иначе account.adapter.ts не
// находит coagreement по type, программный signagree подменяется на
// sndagreement, и `get_coagreement_or_fail` падает с «Соглашение указанного
// типа не найдено».
export const BLAGOROST_AGREEMENT_TYPE = 'capital';
export const GENERATION_PROGRAM_KEY = 'GENERATION';
@@ -74,8 +74,6 @@ export class ChairmanOnboardingEventsService {
return;
}
const cfg = { ...plugin.config };
// Определяем ключ флага для обновления на основе metadata
const flagKey = this.mapStepToFlag(step);
@@ -84,16 +82,15 @@ export class ChairmanOnboardingEventsService {
return;
}
const wasAlreadyDone = !!(cfg as any)[flagKey];
const wasAlreadyDone = !!(plugin.config as any)[flagKey];
// Обновляем флаг выполнения шага
const patch: Partial<IConfig> = {
// Атомарный частичный UPDATE (config || patch) вместо read-modify-write всего
// config целиком: несколько DecisionTrackedEvent (согласия/подписи), обработанные
// конкурентно, иначе теряют флаг друг друга (lost update на общем jsonb-блобе) —
// тот же класс бага, что чинили в capital-онбординге.
const updated = await this.extensionRepository.patchConfig('chairman', {
[flagKey]: true,
};
const updatedConfig = { ...cfg, ...patch };
const updated: ExtensionDomainEntity<IConfig> = { ...plugin, config: updatedConfig };
await this.extensionRepository.update(updated);
} as Partial<IConfig>);
this.logger.info(`Онбординг обновлён: ${flagKey} = true (решение ${result.decision_id})`);
@@ -101,7 +98,7 @@ export class ChairmanOnboardingEventsService {
// завершённым, эмиттим ONBOARDING_COMPLETED_EVENT.
// У chairman нет L2-оферт в реестре сейчас, но событие важно как
// сигнал «L1 кооператива закрыт» — раздел 7.1 C28-10.
if (!wasAlreadyDone && this.isL1Complete(updatedConfig as IConfig)) {
if (!wasAlreadyDone && this.isL1Complete(updated.config as IConfig)) {
this.logger.info(
'[ONBOARDING_COMPLETED] chairman L1 завершён последним шагом, эмиттим событие'
);
@@ -99,22 +99,25 @@ export class ChairmanOnboardingService {
if (!plugin) throw new Error('Конфигурация расширения chairman не найдена');
const config = { ...plugin.config };
let needUpdate = false;
const patch: Partial<IConfig> = {};
if (!config.onboarding_init_at) {
config.onboarding_init_at = new Date().toISOString();
needUpdate = true;
patch.onboarding_init_at = config.onboarding_init_at;
}
if (!config.onboarding_expire_at) {
const start = new Date(config.onboarding_init_at);
const expire = new Date(start.getTime() + 30 * 24 * 60 * 60 * 1000);
config.onboarding_expire_at = expire.toISOString();
needUpdate = true;
patch.onboarding_expire_at = config.onboarding_expire_at;
}
if (needUpdate) {
await this.extensionRepository.update({ ...plugin, config });
return { ...plugin, config };
if (Object.keys(patch).length > 0) {
// Точечный merge только изменившихся полей — полная перезапись config
// целиком конкурирует с параллельными записями флагов/хэшей других шагов
// онбординга и теряет их изменения (lost update на общем jsonb-блобе).
const updated = await this.extensionRepository.patchConfig('chairman', patch);
return { ...plugin, config: updated.config };
}
return plugin;
@@ -198,12 +201,12 @@ export class ChairmanOnboardingService {
document: documentForPublish,
});
// Сохраняем hash в конфиге для отображения на фронтенде
const updatedConfig: IConfig = {
...plugin.config,
// Сохраняем hash в конфиге для отображения на фронтенде — точечный merge
// (не полная перезапись config), чтобы не потерять конкурентно записанные
// флаги/хэши других шагов онбординга (lost update на общем jsonb-блобе).
const updated = await this.extensionRepository.patchConfig('chairman', {
[hashKey]: generatedDoc.hash,
};
await this.extensionRepository.update({ ...plugin, config: updatedConfig });
} as Partial<IConfig>);
// Регистрируем правило отслеживания в фабрике
const varsField = this.mapStepToVarsField(data.step);
@@ -219,7 +222,7 @@ export class ChairmanOnboardingService {
},
});
return this.buildState(updatedConfig);
return this.buildState(updated.config);
}
// Сохраняем hash общего собрания, флаг закроется после newresolved
@@ -233,8 +236,9 @@ export class ChairmanOnboardingService {
// Если hash не пришёл (не должно быть), не затираем существующее значение
const meetHash = proposal_hash || plugin.config.onboarding_general_meet_hash || '';
const updatedConfig: IConfig = { ...plugin.config, onboarding_general_meet_hash: meetHash };
await this.extensionRepository.update({ ...plugin, config: updatedConfig });
const updated = await this.extensionRepository.patchConfig('chairman', {
onboarding_general_meet_hash: meetHash,
} as Partial<IConfig>);
// Регистрируем правило отслеживания общего собрания
await this.decisionTrackingPort.registerTrackingRule({
@@ -247,6 +251,6 @@ export class ChairmanOnboardingService {
},
});
return this.buildState(updatedConfig);
return this.buildState(updated.config);
}
}
@@ -2,9 +2,9 @@
## Возможности
**Удобное общение** - групповые чаты, личные сообщения, голосовые и видео-звонки
🔐 **Безопасность** - сообщения зашифрованы и защищены
👥 **Для всех участников** - каждый член кооператива автоматически получает доступ к чату пайщиков своего кооператива и общему чату кооперативной экономики.
👥 **Для членов совета** - каждый член совета добавляется в зашифрованный чат совета своего кооператива.
📱 **На всех устройствах** - работает на компьютере, телефоне и планшете
**Быстрая установка** - просто установите расширение, и оно заработает
- **Удобное общение** групповые чаты, личные сообщения, голосовые и видео-звонки
- 🔐 **Безопасность** сообщения зашифрованы и защищены
- 👥 **Для всех участников** каждый член кооператива автоматически получает доступ к чату пайщиков своего кооператива и общему чату кооперативной экономики
- 👥 **Для членов совета** каждый член совета добавляется в зашифрованный чат совета своего кооператива
- 📱 **На всех устройствах** работает на компьютере, телефоне и планшете
- **Быстрая установка** просто установите расширение, и оно заработает
@@ -1,6 +1,7 @@
import { ObjectType, Field, InputType, registerEnumType } from '@nestjs/graphql';
import { IsOptional, IsString, IsIn, Matches, MaxLength } from 'class-validator';
import { ReportType } from '../../domain/enums/report-type.enum';
import { SFR_REG_NUMBER_PATTERN } from '../../domain/patterns';
export enum RequisiteSource {
DATABASE = 'database',
@@ -65,7 +66,7 @@ export class ReportRequisitesViewDTO {
* ОКТМО `\d{8}` или `\d{11}`
* ОКПО `\d{8}` или `\d{10}`
* СНИЛС `\d{3}-\d{3}-\d{3} \d{2}` (14 симв.) или 11 цифр подряд
* РегНом СФР `\d{3}-\d{3}-\d{6}` (14 симв. с тире)
* РегНом СФР `\d{3}-\d{3}-\d{6}` (14 симв. с тире) или 10 цифр подряд
*/
@InputType('UpdateReportRequisitesInput')
export class UpdateReportRequisitesInputDTO {
@@ -99,10 +100,10 @@ export class UpdateReportRequisitesInputDTO {
@Matches(/^\d{8}(\d{2})?$/, { message: 'ОКПО — 8 или 10 цифр' })
okpo?: string | null;
@Field(() => String, { nullable: true, description: 'Рег. номер СФР — XXX-XXX-XXXXXX' })
@Field(() => String, { nullable: true, description: 'Рег. номер СФР — XXX-XXX-XXXXXX или 10 цифр' })
@IsOptional()
@IsString()
@Matches(/^\d{3}-\d{3}-\d{6}$/, { message: 'Рег. номер СФР — XXX-XXX-XXXXXX (14 симв.)' })
@Matches(SFR_REG_NUMBER_PATTERN, { message: 'Рег. номер СФР — XXX-XXX-XXXXXX или 10 цифр' })
sfrRegNumber?: string | null;
@Field(() => String, { nullable: true })
@@ -200,7 +200,7 @@ export class ZeroReportSignerEditsInputDTO {
@Field(() => String, { nullable: true })
@IsOptional()
@Matches(SFR_REG_NUMBER_PATTERN, { message: 'Рег.номер СФР — XXX-XXX-XXXXXX' })
@Matches(SFR_REG_NUMBER_PATTERN, { message: 'Рег.номер СФР — XXX-XXX-XXXXXX или 10 цифр' })
sfrRegNumber!: string | null;
@Field(() => String, { nullable: true })
@@ -43,8 +43,9 @@ export const OKPO_PATTERN = /^\d{8}(\d{2})?$/;
// СНИЛС: XXX-XXX-XXX YY (с пробелом/тире) или 11 цифр подряд.
export const SNILS_PATTERN = /^(\d{3}-\d{3}-\d{3} \d{2}|\d{11})$/;
// Регистрационный номер СФР: XXX-XXX-XXXXXX (3-3-6).
export const SFR_REG_NUMBER_PATTERN = /^\d{3}-\d{3}-\d{6}$/;
// Регистрационный номер СФР: действующий XXX-XXX-XXXXXX (3-3-6, 12 цифр)
// или прежний — 10 цифр без разделителей (ТипРегНомерОбщ в XSD ЕФС-1).
export const SFR_REG_NUMBER_PATTERN = /^(\d{3}-\d{3}-\d{6}|\d{10})$/;
/** Дата в формате `DD.MM.YYYY` (XSD `xs:date`-совместимый ФНС-формат). */
export const DATE_DDMMYYYY_PATTERN = /^\d{2}\.\d{2}\.\d{4}$/;
+23 -5
View File
@@ -62,15 +62,33 @@ async function bootstrap() {
// Проверяем, был ли запущен режим миграций
const args = process.argv.slice(2);
if (args.includes('--migrate')) {
await migrateData();
process.exit(0);
try {
await migrateData();
process.exit(0);
} catch (error) {
// migrateData() уже логирует причину; здесь важен только ненулевой
// exit code. В production Sentry.init() ставит свой глобальный
// process.on('unhandledRejection', ...) с дефолтным mode: 'warn' —
// он перехватывает необработанный reject этого await и НЕ роняет
// процесс (в отличие от штатного поведения Node без Sentry). Без
// явного process.exit(1) здесь deploy-скрипт получал ложноположительный
// exit code 0 при реально упавшей миграции (кейс V2.3.2 на testnet
// 2026-07-19 — TS-ошибка компиляции миграции проглатывалась именно так).
logger.error('Процесс миграции завершился с ошибкой, выходим с кодом 1', error);
process.exit(1);
}
}
// Проверяем, был ли запущен режим только миграций (для обратной совместимости)
if (args.includes('--migrations-only')) {
await migrateData();
logger.info('Режим только миграций - миграции выполнены, сервер не будет запущен');
process.exit(0);
try {
await migrateData();
logger.info('Режим только миграций - миграции выполнены, сервер не будет запущен');
process.exit(0);
} catch (error) {
logger.error('Процесс миграции завершился с ошибкой, выходим с кодом 1', error);
process.exit(1);
}
}
// Подключение к MongoDB
@@ -57,4 +57,40 @@ export class TypeOrmExtensionDomainRepository<TConfig = any> implements Extensio
return existingEntity.toDomainEntity();
}
async patchConfig(name: string, patch: Partial<TConfig>): Promise<ExtensionDomainEntity<TConfig>> {
// TypeORM оборачивает .query() для UPDATE/DELETE в кортеж [rows, affectedCount]
// (в отличие от INSERT, где возвращается голый rows-массив) — распаковываем явно,
// иначе row оказывается всем кортежем и все поля читаются как undefined.
const [rows] = (await this.ormRepo.query(
`UPDATE extensions
SET config = COALESCE(config, '{}'::jsonb) || $2::jsonb,
updated_at = NOW()
WHERE name = $1
RETURNING name, enabled, config, schema_version, created_at, updated_at`,
[name, JSON.stringify(patch)]
)) as [
Array<{
name: string;
enabled: boolean;
config: TConfig;
schema_version: number;
created_at: Date;
updated_at: Date;
}>,
number,
];
const row = rows[0];
if (!row) throw new Error('Расширение не найдено в установленных');
return new ExtensionDomainEntity<TConfig>(
row.name,
row.enabled,
row.config,
row.created_at,
row.updated_at,
row.schema_version
);
}
}
+8
View File
@@ -1304,6 +1304,9 @@ export const AllTypesProps: Record<string,any> = {
returnExpenseItem:{
data:"ReturnExpenseItemInput"
},
saveCapitalProgramDocDataHash:{
data:"SaveCapitalProgramDocDataInput"
},
saveReportDraft:{
input:"SaveReportDraftInput"
},
@@ -1959,6 +1962,9 @@ export const AllTypesProps: Record<string,any> = {
},
RoomMessageKind: "enum" as const,
SaveCapitalProgramDocDataInput:{
},
SaveReportDraftInput:{
reportType:"ReportType"
},
@@ -2791,6 +2797,7 @@ export const ReturnTypes: Record<string,any> = {
CapitalOnboardingState:{
blagorost_offer_template_done:"Boolean",
blagorost_provision_done:"Boolean",
capital_program_doc_data_hash:"String",
generation_contract_template_done:"Boolean",
generator_offer_template_done:"Boolean",
generator_program_template_done:"Boolean",
@@ -4051,6 +4058,7 @@ export const ReturnTypes: Record<string,any> = {
resetRegistration:"Account",
restartAnnualGeneralMeet:"MeetAggregate",
returnExpenseItem:"Transaction",
saveCapitalProgramDocDataHash:"CapitalOnboardingState",
saveReportDraft:"ReportDraft",
selectBranch:"Boolean",
sendAgreement:"Transaction",
+33 -3
View File
@@ -3009,6 +3009,7 @@ export type ValueTypes = {
["CapitalOnboardingState"]: AliasType<{
blagorost_offer_template_done?:boolean | `@${string}`,
blagorost_provision_done?:boolean | `@${string}`,
capital_program_doc_data_hash?:boolean | `@${string}`,
generation_contract_template_done?:boolean | `@${string}`,
generator_offer_template_done?:boolean | `@${string}`,
generator_program_template_done?:boolean | `@${string}`,
@@ -7205,6 +7206,7 @@ resetKey?: [{ data: ValueTypes["ResetKeyInput"] | Variable<any, string>},boolean
resetRegistration?:ValueTypes["Account"],
restartAnnualGeneralMeet?: [{ data: ValueTypes["RestartAnnualGeneralMeetInput"] | Variable<any, string>},ValueTypes["MeetAggregate"]],
returnExpenseItem?: [{ data: ValueTypes["ReturnExpenseItemInput"] | Variable<any, string>},ValueTypes["Transaction"]],
saveCapitalProgramDocDataHash?: [{ data: ValueTypes["SaveCapitalProgramDocDataInput"] | Variable<any, string>},ValueTypes["CapitalOnboardingState"]],
saveReportDraft?: [{ input: ValueTypes["SaveReportDraftInput"] | Variable<any, string>},ValueTypes["ReportDraft"]],
selectBranch?: [{ data: ValueTypes["SelectBranchInput"] | Variable<any, string>},boolean | `@${string}`],
sendAgreement?: [{ data: ValueTypes["SendAgreementInput"] | Variable<any, string>},ValueTypes["Transaction"]],
@@ -9459,6 +9461,9 @@ validateReportEdits?: [{ editsJson: string | Variable<any, string>, reportType:
};
/** Тип сообщения в истории комнаты Matrix (текст или расшифрованное аудио) */
["RoomMessageKind"]:RoomMessageKind;
["SaveCapitalProgramDocDataInput"]: {
doc_data_hash: string | Variable<any, string>
};
["SaveReportDraftInput"]: {
editedFields: Array<string> | Variable<any, string>,
editsJson: string | Variable<any, string>,
@@ -12342,6 +12347,7 @@ export type ResolverInputTypes = {
["CapitalOnboardingState"]: AliasType<{
blagorost_offer_template_done?:boolean | `@${string}`,
blagorost_provision_done?:boolean | `@${string}`,
capital_program_doc_data_hash?:boolean | `@${string}`,
generation_contract_template_done?:boolean | `@${string}`,
generator_offer_template_done?:boolean | `@${string}`,
generator_program_template_done?:boolean | `@${string}`,
@@ -16439,6 +16445,7 @@ resetKey?: [{ data: ResolverInputTypes["ResetKeyInput"]},boolean | `@${string}`]
resetRegistration?:ResolverInputTypes["Account"],
restartAnnualGeneralMeet?: [{ data: ResolverInputTypes["RestartAnnualGeneralMeetInput"]},ResolverInputTypes["MeetAggregate"]],
returnExpenseItem?: [{ data: ResolverInputTypes["ReturnExpenseItemInput"]},ResolverInputTypes["Transaction"]],
saveCapitalProgramDocDataHash?: [{ data: ResolverInputTypes["SaveCapitalProgramDocDataInput"]},ResolverInputTypes["CapitalOnboardingState"]],
saveReportDraft?: [{ input: ResolverInputTypes["SaveReportDraftInput"]},ResolverInputTypes["ReportDraft"]],
selectBranch?: [{ data: ResolverInputTypes["SelectBranchInput"]},boolean | `@${string}`],
sendAgreement?: [{ data: ResolverInputTypes["SendAgreementInput"]},ResolverInputTypes["Transaction"]],
@@ -18609,6 +18616,9 @@ validateReportEdits?: [{ editsJson: string, reportType: ResolverInputTypes["Repo
};
/** Тип сообщения в истории комнаты Matrix (текст или расшифрованное аудио) */
["RoomMessageKind"]:RoomMessageKind;
["SaveCapitalProgramDocDataInput"]: {
doc_data_hash: string
};
["SaveReportDraftInput"]: {
editedFields: Array<string>,
editsJson: string,
@@ -21418,6 +21428,7 @@ export type ModelTypes = {
["CapitalOnboardingState"]: {
blagorost_offer_template_done: boolean,
blagorost_provision_done: boolean,
capital_program_doc_data_hash?: string | undefined | null,
generation_contract_template_done: boolean,
generator_offer_template_done: boolean,
generator_program_template_done: boolean,
@@ -25716,7 +25727,7 @@ export type ModelTypes = {
Требуемые роли: chairman, member. */
generateExpenseProposalStatementDocument: ModelTypes["GeneratedDocument"],
/** Сгенерировать протокол решения по предложенно повестке
/** Сгенерировать протокол решения по предложенной повестке
Требуемые роли: chairman, member. */
generateFreeDecision: ModelTypes["GeneratedDocument"],
@@ -25870,6 +25881,10 @@ export type ModelTypes = {
Требуемые роли: chairman, member, user. */
returnExpenseItem: ModelTypes["Transaction"],
/** Сохранить hash PrivateData параметров документов ЦПП
Требуемые роли: chairman. */
saveCapitalProgramDocDataHash: ModelTypes["CapitalOnboardingState"],
/** Сохранить/обновить черновик формы отчёта (upsert по owner+type+year+period)
Требуемые роли: chairman. */
@@ -28235,6 +28250,9 @@ export type ModelTypes = {
return_amount: string
};
["RoomMessageKind"]:RoomMessageKind;
["SaveCapitalProgramDocDataInput"]: {
doc_data_hash: string
};
["SaveReportDraftInput"]: {
editedFields: Array<string>,
editsJson: string,
@@ -29210,7 +29228,10 @@ export type ModelTypes = {
}
export type GraphQLTypes = {
["AcceptChildOrderInput"]: {
// ------------------------------------------------------;
// THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY);
// ------------------------------------------------------;
["AcceptChildOrderInput"]: {
/** Имя аккаунта кооператива */
coopname: string,
/** Подписанное заявление на имущественный паевый взнос */
@@ -31113,6 +31134,7 @@ export type GraphQLTypes = {
__typename: "CapitalOnboardingState",
blagorost_offer_template_done: boolean,
blagorost_provision_done: boolean,
capital_program_doc_data_hash?: string | undefined | null,
generation_contract_template_done: boolean,
generator_offer_template_done: boolean,
generator_program_template_done: boolean,
@@ -35632,7 +35654,7 @@ export type GraphQLTypes = {
Требуемые роли: chairman, member. */
generateExpenseProposalStatementDocument: GraphQLTypes["GeneratedDocument"],
/** Сгенерировать протокол решения по предложенно повестке
/** Сгенерировать протокол решения по предложенной повестке
Требуемые роли: chairman, member. */
generateFreeDecision: GraphQLTypes["GeneratedDocument"],
@@ -35786,6 +35808,10 @@ export type GraphQLTypes = {
Требуемые роли: chairman, member, user. */
returnExpenseItem: GraphQLTypes["Transaction"],
/** Сохранить hash PrivateData параметров документов ЦПП
Требуемые роли: chairman. */
saveCapitalProgramDocDataHash: GraphQLTypes["CapitalOnboardingState"],
/** Сохранить/обновить черновик формы отчёта (upsert по owner+type+year+period)
Требуемые роли: chairman. */
@@ -38346,6 +38372,9 @@ export type GraphQLTypes = {
};
/** Тип сообщения в истории комнаты Matrix (текст или расшифрованное аудио) */
["RoomMessageKind"]: RoomMessageKind;
["SaveCapitalProgramDocDataInput"]: {
doc_data_hash: string
};
["SaveReportDraftInput"]: {
editedFields: Array<string>,
editsJson: string,
@@ -40165,6 +40194,7 @@ type ZEUS_VARIABLES = {
["ReturnByMoneySignedMetaDocumentInput"]: ValueTypes["ReturnByMoneySignedMetaDocumentInput"];
["ReturnExpenseItemInput"]: ValueTypes["ReturnExpenseItemInput"];
["RoomMessageKind"]: ValueTypes["RoomMessageKind"];
["SaveCapitalProgramDocDataInput"]: ValueTypes["SaveCapitalProgramDocDataInput"];
["SaveReportDraftInput"]: ValueTypes["SaveReportDraftInput"];
["SbpDataInput"]: ValueTypes["SbpDataInput"];
["SearchDocumentsInput"]: ValueTypes["SearchDocumentsInput"];
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "cooptypes",
"type": "module",
"version": "2026.6.24",
"version": "2026.7.21",
"description": "Shared TypeScript типы и интерфейсы экосистемы кооперативов",
"author": "Alex Ant <dacom.dark.sun@gmail.com>",
"license": "MIT",
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,49 +1,53 @@
export interface CapitalProgramPrivateData {
approval_protocol_number: string
approval_protocol_day: string
approval_protocol_month: string
approval_protocol_year: string
cooperative_name: string
cooperative_short_name: string
cooperative_quoted_name: string
website: string
chairman_full_name: string
generator_program_purpose: string
eoap_definition: string
generator_task_goal: string
idea_unit_cost: string
idea_unit_cost_words: string
blagorost_goal_expansion: string
blagorost_goal_reason: string
blagorost_task_expansion: string
blagorost_task_development: string
return_source_description: string
return_additional_source: string
offer_template_number: string
}
export interface CapitalApprovalHeader {
protocol_number: string
protocol_day_month_year: string
}
export const CAPITAL_APPROVAL_PLACEHOLDER: CapitalApprovalHeader = {
protocol_number: '______',
protocol_day_month_year: '«___» __________ 20__ г.',
}
export const capitalProgramPrivateDataRequiredFields = [
'approval_protocol_number',
'approval_protocol_day',
'approval_protocol_month',
'approval_protocol_year',
'cooperative_name',
'cooperative_short_name',
'cooperative_quoted_name',
'website',
'chairman_full_name',
'generator_program_purpose',
'eoap_definition',
'generator_task_goal',
'idea_unit_cost',
'idea_unit_cost_words',
'blagorost_goal_expansion',
'blagorost_goal_reason',
'blagorost_task_expansion',
'blagorost_task_development',
'return_source_description',
'return_additional_source',
'offer_template_number',
] as const
export function resolveCapitalApprovalHeader(
decision?: { protocol_number?: string; protocol_day_month_year?: string } | null,
): CapitalApprovalHeader {
const protocol_number = decision?.protocol_number?.trim()
const protocol_day_month_year = decision?.protocol_day_month_year?.trim()
return {
protocol_number: protocol_number || CAPITAL_APPROVAL_PLACEHOLDER.protocol_number,
protocol_day_month_year: protocol_day_month_year || CAPITAL_APPROVAL_PLACEHOLDER.protocol_day_month_year,
}
}
export function requireCapitalProgramPrivateData(
payload: CapitalProgramPrivateData | null | undefined,
registry_id: number,
@@ -22,7 +22,20 @@ const completeStep = async (
return state;
};
const saveProgramDocDataHash = async (
data: Mutations.Capital.SaveCapitalProgramDocDataHash.IInput['data'],
) => {
const { [Mutations.Capital.SaveCapitalProgramDocDataHash.name]: state } = await client.Mutation(
Mutations.Capital.SaveCapitalProgramDocDataHash.mutation,
{
variables: { data },
},
);
return state;
};
export const api = {
loadOnboardingState,
completeStep,
saveProgramDocDataHash,
};
@@ -0,0 +1,83 @@
export const DOC_WIZARD_STEP_GENERATOR = 'doc_generator';
export const DOC_WIZARD_STEP_BLAGOROST = 'doc_blagorost';
export const DOC_WIZARD_REGISTRY: Record<string, number> = {
[DOC_WIZARD_STEP_GENERATOR]: 994,
[DOC_WIZARD_STEP_BLAGOROST]: 998,
};
export const DOC_WIZARD_STEP_ORDER = [DOC_WIZARD_STEP_GENERATOR, DOC_WIZARD_STEP_BLAGOROST] as const;
export type DocWizardStepKey = (typeof DOC_WIZARD_STEP_ORDER)[number];
export function isDocWizardStep(key: string): key is DocWizardStepKey {
return (DOC_WIZARD_STEP_ORDER as readonly string[]).includes(key);
}
/** Группа 1: положение ГЕНЕРАТОР, шаблон договора, положение БЛАГОРОСТ — объявляются параллельно. */
export const COUNCIL_GROUP_FOUNDATION = 'council_foundation';
/** Группа 2: оферты ГЕНЕРАТОР и БЛАГОРОСТ — объявляются параллельно, каждая после своего положения. */
export const COUNCIL_GROUP_OFFERS = 'council_offers';
/** @deprecated Миграция черновиков stepper → COUNCIL_GROUP_OFFERS */
export const COUNCIL_GROUP_GENERATOR_OFFER = 'council_generator_offer';
/** @deprecated Миграция черновиков stepper → COUNCIL_GROUP_OFFERS */
export const COUNCIL_GROUP_BLAGOROST_OFFER = 'council_blagorost_offer';
export const COUNCIL_STEP_GROUP_ORDER = [
COUNCIL_GROUP_FOUNDATION,
COUNCIL_GROUP_OFFERS,
] as const;
export type CouncilGroupStepKey = (typeof COUNCIL_STEP_GROUP_ORDER)[number];
export const COUNCIL_STEP_IDS_BY_GROUP: Record<CouncilGroupStepKey, readonly string[]> = {
[COUNCIL_GROUP_FOUNDATION]: [
'generator_program_template',
'generation_contract_template',
'blagorost_program',
],
[COUNCIL_GROUP_OFFERS]: ['generator_offer_template', 'blagorost_offer_template'],
};
export const COUNCIL_WIZARD_STEP_META: Record<CouncilGroupStepKey, { label: string; description: string }> = {
[COUNCIL_GROUP_FOUNDATION]: {
label: 'Утверждение положений и шаблонов ЦПП',
description:
'Положение «ГЕНЕРАТОР», шаблон договора участия в хозяйственной деятельности и положение «БЛАГОРОСТ»',
},
[COUNCIL_GROUP_OFFERS]: {
label: 'Утверждение оферт ЦПП',
description: 'Оферты «ГЕНЕРАТОР» и «БЛАГОРОСТ» для пайщиков',
},
};
export function isCouncilGroupStep(key: string): key is CouncilGroupStepKey {
return (COUNCIL_STEP_GROUP_ORDER as readonly string[]).includes(key);
}
export function resolveCouncilGroupForStepId(stepId: string): CouncilGroupStepKey | null {
for (const groupKey of COUNCIL_STEP_GROUP_ORDER) {
if (COUNCIL_STEP_IDS_BY_GROUP[groupKey].includes(stepId)) {
return groupKey;
}
}
return null;
}
export function normalizeWizardStepKey(key: string): string {
if (isDocWizardStep(key)) {
return key;
}
if (key === COUNCIL_GROUP_GENERATOR_OFFER || key === COUNCIL_GROUP_BLAGOROST_OFFER) {
return COUNCIL_GROUP_OFFERS;
}
if (isCouncilGroupStep(key)) {
return key;
}
return resolveCouncilGroupForStepId(key) ?? key;
}
export function getCouncilStepIdsForGroup(groupKey: CouncilGroupStepKey): readonly string[] {
return COUNCIL_STEP_IDS_BY_GROUP[groupKey];
}
@@ -0,0 +1,71 @@
export type EditableFieldKey =
| 'generator_program_purpose'
| 'eoap_definition'
| 'generator_task_goal'
| 'idea_unit_cost'
| 'idea_unit_cost_words'
| 'blagorost_goal_expansion'
| 'blagorost_goal_reason'
| 'blagorost_task_expansion'
| 'blagorost_task_development'
| 'return_source_description'
| 'return_additional_source';
export const PREVIEW_PLACEHOLDER = '______';
export const FIELD_LABELS: Record<EditableFieldKey, string> = {
generator_program_purpose:
'Направление ЦПП: какую экосистему или сферу деятельности развивают пайщики (название, правовая основа, цель интеграции в экономику)',
eoap_definition:
'Определение Платформы: что это, из чего состоит, кто участники, на какой технологии и принципах работает',
generator_task_goal:
'Главная задача ЦПП: чем Платформа должна стать для участников (центр привлечения чего и для чего)',
idea_unit_cost: 'Стоимость одной Идеи в рублях — только число',
idea_unit_cost_words: 'Та же стоимость Идеи прописью',
blagorost_goal_expansion:
'Зачем расширять участие в Платформе: какая потребность участников закрывается при росте сообщества',
blagorost_goal_reason:
'Причина роста числа участников: за счёт чего расширяется сообщество и на что это влияет',
blagorost_task_expansion:
'Задача расширения: как описывается рост числа участников Платформы и их взаимодействия',
blagorost_task_development:
'Задача развития: как описывается привлечение инноваций, разработок и решений в Платформу',
return_source_description:
'Основной источник возврата взносов: что создаёт экономический эффект (инфраструктура, продукт, механизм)',
return_additional_source:
'Дополнительные источники возврата: взносы пользователей, продукты и приложения, передаваемые или создаваемые в Обществе',
};
export const GENERATOR_DOC_FIELDS: EditableFieldKey[] = [
'generator_program_purpose',
'eoap_definition',
'generator_task_goal',
'idea_unit_cost',
'idea_unit_cost_words',
];
export const BLAGOROST_DOC_FIELDS: EditableFieldKey[] = [
'blagorost_goal_expansion',
'blagorost_goal_reason',
'blagorost_task_expansion',
'blagorost_task_development',
'return_source_description',
'return_additional_source',
'eoap_definition',
];
export const ALL_DOC_FIELDS: EditableFieldKey[] = [
...GENERATOR_DOC_FIELDS,
...BLAGOROST_DOC_FIELDS.filter((key) => !GENERATOR_DOC_FIELDS.includes(key)),
];
export const DOCUMENT_SECTIONS = [
{
registryId: 994,
tabLabel: 'Положение ГЕНЕРАТОР',
},
{
registryId: 998,
tabLabel: 'Положение БЛАГОРОСТ',
},
] as const;
@@ -15,16 +15,16 @@ interface GeneratedDocument {
type CapitalOnboardingStepId = Mutations.Capital.CompleteOnboardingStep.IInput['data']['step'];
const onboardingState = ref<CapitalOnboardingState | null>(null);
const loading = ref(false);
const submitting = ref(false);
const generatingDocument = ref(false);
const currentGeneratedDoc = ref<GeneratedDocument | null>(null);
export const useCapitalOnboarding = () => {
const systemStore = useSystemStore();
const sessionStore = useSessionStore();
const onboardingState = ref<CapitalOnboardingState | null>(null);
const loading = ref(false);
const submitting = ref(false);
const generatingDocument = ref(false);
const currentGeneratedDoc = ref<GeneratedDocument | null>(null);
// Маппинг шагов на registry_id
const stepToRegistryId: Record<CapitalOnboardingStepId, number> = {
'generator_program_template': 994,
@@ -50,7 +50,7 @@ export const useCapitalOnboarding = () => {
const registry_id = stepToRegistryId[step.id];
const docDataHash = onboardingState.value?.capital_program_doc_data_hash;
if (capitalProgramDocDataRegistryIds.has(registry_id) && !docDataHash) {
throw new Error('Параметры документов ЦПП не заполнены: переустановите или обновите настройки расширения capital');
throw new Error('Сначала заполните параметры документов ЦПП и сформируйте предпросмотр');
}
const generateDocInput: Mutations.Documents.GenerateDocument.IInput = {
@@ -158,10 +158,16 @@ export const useCapitalOnboarding = () => {
});
const isOnboardingCompleted = computed(() => {
console.log(stepsConfig.value);
return stepsConfig.value.every(step => step.status === 'completed');
});
const hasPendingCouncilDecisions = computed(() =>
stepsConfig.value.some((step) => step.status === 'in_progress'),
);
const docDataHash = computed(() => onboardingState.value?.capital_program_doc_data_hash || null);
const isDocParamsReady = computed(() => Boolean(docDataHash.value));
const config = computed<ICouncilOnboardingConfig>(() => ({
steps: stepsConfig.value,
expireAt: expireAt.value,
@@ -169,18 +175,27 @@ export const useCapitalOnboarding = () => {
completionMessage: 'Все необходимые документы для работы с программой утверждены.',
}));
const loadState = async () => {
const loadState = async (options?: { silent?: boolean }) => {
const silent = options?.silent ?? false;
try {
loading.value = true;
if (!silent) {
loading.value = true;
}
await systemStore.loadSystemInfo();
onboardingState.value = await api.loadOnboardingState();
} catch (error) {
FailAlert(error);
if (!silent) {
FailAlert(error);
}
} finally {
loading.value = false;
if (!silent) {
loading.value = false;
}
}
};
const refreshState = () => loadState({ silent: true });
const handleStepClick = async (step: ICouncilOnboardingStep) => {
try {
const doc = await generateDocument(step);
@@ -225,6 +240,10 @@ export const useCapitalOnboarding = () => {
}
};
const handleDocParamsSaved = async () => {
onboardingState.value = await api.loadOnboardingState();
};
return {
config,
loading,
@@ -232,8 +251,13 @@ export const useCapitalOnboarding = () => {
generatingDocument,
currentGeneratedDoc,
isOnboardingCompleted,
hasPendingCouncilDecisions,
docDataHash,
isDocParamsReady,
loadState,
refreshState,
handleStepClick,
handleStepSubmit,
handleDocParamsSaved,
};
};
@@ -1 +1,8 @@
export { useCapitalOnboarding } from './composable';
export { useCapitalProgramDocParams, getRegistryIdForDocWizardStep } from './useCapitalProgramDocParams';
export {
DOC_WIZARD_STEP_BLAGOROST,
DOC_WIZARD_STEP_GENERATOR,
DOC_WIZARD_STEP_ORDER,
isDocWizardStep,
} from './capitalOnboardingWizard';
@@ -0,0 +1,226 @@
import { computed, reactive, ref } from 'vue';
import { useQuasar } from 'quasar';
import type { Cooperative } from 'cooptypes';
import { useSystemStore } from 'src/entities/System/model';
import { useSessionStore } from 'src/entities/Session';
import { api as documentApi } from 'src/shared/lib/document/api';
import { api } from '../api';
import {
ALL_DOC_FIELDS,
BLAGOROST_DOC_FIELDS,
FIELD_LABELS,
GENERATOR_DOC_FIELDS,
PREVIEW_PLACEHOLDER,
type EditableFieldKey,
} from './capitalProgramDocFields';
import {
DOC_WIZARD_STEP_BLAGOROST,
DOC_WIZARD_STEP_GENERATOR,
} from './capitalOnboardingWizard';
import {
getCapitalProgramDocParamsDraftKey,
readCapitalProgramDocParamsDraft,
writeCapitalProgramDocParamsDraft,
} from './useCapitalProgramDocParamsDraft';
type PreviewState = {
html: string;
docDataHash: string;
};
const DOC_FIELDS_BY_REGISTRY: Record<number, EditableFieldKey[]> = {
994: GENERATOR_DOC_FIELDS,
998: BLAGOROST_DOC_FIELDS,
};
function createEmptyForm(): Record<EditableFieldKey, string> {
return Object.fromEntries(ALL_DOC_FIELDS.map((key) => [key, ''])) as Record<EditableFieldKey, string>;
}
export function useCapitalProgramDocParams(options?: { onSaved?: (hash: string) => void | Promise<void> }) {
const $q = useQuasar();
const systemStore = useSystemStore();
const sessionStore = useSessionStore();
const saving = ref(false);
const savedHash = ref('');
const loadingRegistryId = ref<number | null>(null);
const wizardStepKey = ref<string>(DOC_WIZARD_STEP_GENERATOR);
const previews = reactive<Partial<Record<number, PreviewState>>>({});
const form = reactive<Record<EditableFieldKey, string>>(createEmptyForm());
const draftStorageKey = computed(() =>
getCapitalProgramDocParamsDraftKey(systemStore.info?.coopname ?? '', sessionStore.username ?? ''),
);
const editableFields = computed(() =>
ALL_DOC_FIELDS.map((key) => ({
key,
label: FIELD_LABELS[key],
})),
);
function persistDraftToStorage() {
writeCapitalProgramDocParamsDraft(draftStorageKey.value, {
form: { ...form },
wizardStepKey: wizardStepKey.value,
savedHash: savedHash.value,
previews: { ...previews },
});
}
function restoreDraftFromStorage() {
const draft = readCapitalProgramDocParamsDraft(draftStorageKey.value);
if (!draft) return;
Object.assign(form, draft.form);
savedHash.value = draft.savedHash;
Object.assign(previews, draft.previews);
wizardStepKey.value = draft.wizardStepKey ?? DOC_WIZARD_STEP_GENERATOR;
}
function getPreviewPayload(): Cooperative.Registry.CapitalProgramPrivateData {
return Object.fromEntries(
ALL_DOC_FIELDS.map((key) => [key, String(form[key] ?? '').trim() || PREVIEW_PLACEHOLDER]),
) as Record<EditableFieldKey, string>;
}
function getMissingCount(registryId: number): number {
const fields = DOC_FIELDS_BY_REGISTRY[registryId] ?? [];
return fields.filter((key) => !String(form[key] ?? '').trim()).length;
}
function validateSection(registryId: number): string | null {
const fields = DOC_FIELDS_BY_REGISTRY[registryId] ?? [];
const missing = fields
.filter((key) => !String(form[key] ?? '').trim())
.map((key) => FIELD_LABELS[key]);
if (missing.length) {
return `Заполните все поля документа: ${missing.join(', ')}`;
}
return null;
}
function validateAll(): string | null {
const missing = editableFields.value
.filter(({ key }) => !String(form[key] ?? '').trim())
.map(({ label }) => label);
if (missing.length) {
return `Заполните все параметры в документах: ${missing.join(', ')}`;
}
return null;
}
function getDocDataHashFromResult(result: Awaited<ReturnType<typeof documentApi.generateDocument>>): string {
const meta = result.meta as { doc_data_hash?: string } | undefined;
return meta?.doc_data_hash ?? '';
}
async function loadPreview(registryId: number) {
loadingRegistryId.value = registryId;
try {
const result = await documentApi.generateDocument({
coopname: systemStore.info?.coopname ?? '',
username: sessionStore.username ?? '',
registry_id: registryId,
doc_data: getPreviewPayload(),
});
previews[registryId] = {
html: result.html,
docDataHash: getDocDataHashFromResult(result),
};
persistDraftToStorage();
return result;
} catch (error) {
$q.notify({
type: 'negative',
message: error instanceof Error ? error.message : 'Не удалось сформировать предпросмотр',
});
throw error;
} finally {
loadingRegistryId.value = null;
}
}
async function saveParams() {
const validationError = validateAll();
if (validationError) {
$q.notify({ type: 'warning', message: validationError });
return false;
}
saving.value = true;
try {
await loadPreview(994);
await loadPreview(998);
const hash = previews[994]?.docDataHash ?? previews[998]?.docDataHash;
if (!hash) {
throw new Error('Не удалось получить хеш параметров документов');
}
await api.saveProgramDocDataHash({ doc_data_hash: hash });
savedHash.value = hash;
persistDraftToStorage();
await options?.onSaved?.(hash);
$q.notify({
type: 'positive',
message: 'Параметры документов ЦПП сохранены',
});
return true;
} catch (error) {
$q.notify({
type: 'negative',
message: error instanceof Error ? error.message : 'Не удалось сохранить параметры',
});
return false;
} finally {
saving.value = false;
}
}
async function ensurePreview(registryId: number) {
if (previews[registryId]?.html) return;
await loadPreview(registryId);
}
function setWizardStepKey(key: string) {
wizardStepKey.value = key;
persistDraftToStorage();
}
return {
form,
previews,
savedHash,
saving,
loadingRegistryId,
wizardStepKey,
restoreDraftFromStorage,
persistDraftToStorage,
getMissingCount,
validateSection,
loadPreview,
ensurePreview,
saveParams,
setWizardStepKey,
};
}
export function getRegistryIdForDocWizardStep(stepKey: string): number | null {
if (stepKey === DOC_WIZARD_STEP_GENERATOR) return 994;
if (stepKey === DOC_WIZARD_STEP_BLAGOROST) return 998;
return null;
}
@@ -0,0 +1,105 @@
import { ALL_DOC_FIELDS, type EditableFieldKey } from './capitalProgramDocFields';
const STORAGE_PREFIX = 'capital-cpp-doc-params-draft';
export type CapitalProgramDocParamsPreviewState = {
html: string;
docDataHash: string;
};
export type CapitalProgramDocParamsDraft = {
form: Record<EditableFieldKey, string>;
/** @deprecated используйте wizardStepKey */
activeTab?: number;
wizardStepKey?: string;
savedHash: string;
previews: Partial<Record<number, CapitalProgramDocParamsPreviewState>>;
};
function isBrowser(): boolean {
return typeof window !== 'undefined' && typeof window.localStorage !== 'undefined';
}
export function getCapitalProgramDocParamsDraftKey(coopname: string, username: string): string {
return `${STORAGE_PREFIX}:${coopname || 'unknown'}:${username || 'unknown'}`;
}
function isEditableFieldKey(key: string): key is EditableFieldKey {
return (ALL_DOC_FIELDS as readonly string[]).includes(key);
}
function createEmptyForm(): Record<EditableFieldKey, string> {
return Object.fromEntries(ALL_DOC_FIELDS.map((key) => [key, ''])) as Record<EditableFieldKey, string>;
}
function normalizeForm(raw: unknown): Record<EditableFieldKey, string> | null {
if (!raw || typeof raw !== 'object') return null;
const form = createEmptyForm();
for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {
if (isEditableFieldKey(key) && typeof value === 'string') {
form[key] = value;
}
}
return form;
}
function normalizePreviews(raw: unknown): Partial<Record<number, CapitalProgramDocParamsPreviewState>> {
const previews: Partial<Record<number, CapitalProgramDocParamsPreviewState>> = {};
if (!raw || typeof raw !== 'object') return previews;
for (const [id, preview] of Object.entries(raw as Record<string, unknown>)) {
const registryId = Number(id);
if (!Number.isFinite(registryId) || !preview || typeof preview !== 'object') continue;
const html = (preview as CapitalProgramDocParamsPreviewState).html;
const docDataHash = (preview as CapitalProgramDocParamsPreviewState).docDataHash;
if (typeof html === 'string' && typeof docDataHash === 'string') {
previews[registryId] = { html, docDataHash };
}
}
return previews;
}
export function readCapitalProgramDocParamsDraft(storageKey: string): CapitalProgramDocParamsDraft | null {
if (!isBrowser()) return null;
try {
const raw = localStorage.getItem(storageKey);
if (!raw) return null;
const parsed = JSON.parse(raw) as Partial<CapitalProgramDocParamsDraft>;
const form = normalizeForm(parsed.form);
if (!form) return null;
const legacyTab = typeof parsed.activeTab === 'number' ? parsed.activeTab : 994;
const wizardStepKey =
typeof parsed.wizardStepKey === 'string'
? parsed.wizardStepKey
: legacyTab === 998
? 'doc_blagorost'
: 'doc_generator';
return {
form,
wizardStepKey,
savedHash: typeof parsed.savedHash === 'string' ? parsed.savedHash : '',
previews: normalizePreviews(parsed.previews),
};
} catch {
localStorage.removeItem(storageKey);
return null;
}
}
export function writeCapitalProgramDocParamsDraft(storageKey: string, draft: CapitalProgramDocParamsDraft): void {
if (!isBrowser()) return;
try {
localStorage.setItem(storageKey, JSON.stringify(draft));
} catch (error) {
console.warn('Failed to persist capital CPP doc params draft:', error);
}
}
@@ -0,0 +1,91 @@
<template lang="pug">
.capital-council-group
.capital-council-group__item(
v-for='(step, index) in steps'
:key='step.id'
)
.capital-council-group__head
.capital-council-group__title
span.capital-council-group__index {{ index + 1 }}.
span {{ step.title }}
.capital-council-group__status
BaseChip(
v-if='step.status === "completed"'
variant='pos'
size='sm'
) Утверждено советом
BaseChip(
v-else-if='step.status === "in_progress"'
variant='warn'
size='sm'
) Ожидаем решение совета
p.capital-council-group__desc.t-body2 {{ step.description }}
BaseButton.capital-council-group__action(
v-if='canDeclare(step)'
variant='primary'
size='sm'
:disable='busy'
@click='emit("declare", step.id)'
) Объявить собрание совета
q-separator.capital-council-group__sep(v-if='index < steps.length - 1')
</template>
<script setup lang="ts">
import { BaseButton, BaseChip } from 'src/shared/ui/base';
import type { ICouncilOnboardingStep } from 'src/shared/ui/CouncilOnboarding';
defineProps<{
steps: ICouncilOnboardingStep[];
busy?: boolean;
canDeclare: (step: ICouncilOnboardingStep) => boolean;
}>();
const emit = defineEmits<{
declare: [stepId: string];
}>();
</script>
<style scoped lang="scss">
.capital-council-group__item {
padding: var(--p-2) 0;
}
.capital-council-group__head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: var(--p-3);
}
.capital-council-group__title {
display: flex;
gap: var(--p-2);
font-weight: 600;
color: var(--p-ink);
}
.capital-council-group__index {
flex: 0 0 auto;
color: var(--p-ink-2);
}
.capital-council-group__status {
flex: 0 0 auto;
}
.capital-council-group__desc {
margin: var(--p-2) 0 0;
color: var(--p-ink-2);
}
.capital-council-group__action {
margin-top: var(--p-3);
}
.capital-council-group__sep {
margin-top: var(--p-4);
}
</style>
@@ -1,93 +1,124 @@
<template lang="pug">
div
// Показываем лоадер пока данные загружаются
div.capital-onboarding.q-pa-md
WindowLoader(
v-if="loading"
text="Загрузка данных онбординга..."
v-if='loading'
text='Загрузка данных онбординга...'
)
// Показываем шаги если онбординг не завершен и данные загружены
q-card(v-else-if="!isOnboardingCompleted" flat)
q-card-section.row.items-center.justify-between
div
div.text-h5 Адаптация к работе с программой "Благорост"
template(v-else-if='!isOnboardingCompleted')
.capital-onboarding__shell
BaseBanner.capital-onboarding__banner(variant='info')
template(#icon)
q-icon(name='info' size='20px')
| Сначала заполните параметры положений «ГЕНЕРАТОР» и «БЛАГОРОСТ», затем объявите собрания
| совета для утверждения документов ЦПП.
q-separator
q-card-section
q-list(separator)
q-item(v-for="(step, index) in config?.steps || []" :key="step.id")
q-item-section
div.row.items-center.q-gutter-sm
q-icon(
:name="getIcon(step)"
:color="getIconColor(step)"
size="22px"
)
div.text-subtitle1 {{ index + 1 }}. {{ step.title }}
div.text-caption.text-grey-7 {{ step.description }}
q-chip(
v-if="step.status === 'in_progress'"
dense
color="amber"
text-color="black"
icon="hourglass_top"
class="q-mt-xs"
) Ожидаем решение совета
q-item-section(side)
q-btn(
v-if="showAction(index)"
:disable="submitting || generatingDocument"
:loading="generatingDocument && currentStepId === step.id"
color="primary"
label="Объявить собрание совета"
@click="() => openDialog(step)"
)
BaseDialog(
v-model="dialogOpen",
title="Предложение повестки",
size="lg",
:close-on-backdrop="false",
:close-on-escape="false",
@update:model-value="(v) => !v && closeDialog()"
)
Loader(
v-if="generatingDocument"
text="Генерация документа..."
VerticalStepper.capital-onboarding__stepper(
:steps='wizardSteps'
:active-key='activeWizardStep'
:completed='completedWizardSteps'
:clickable-completed='true'
@change='onStepperChange'
)
q-card-section(v-else-if="currentStep && currentGeneratedDoc && !generatingDocument")
template(#active='{ step }')
CapitalProgramDocumentStepPanel(
v-if='isDocWizardStep(step.key)'
v-model='form'
:preview-html='getDocPreviewHtml(step.key)'
:loading='isDocStepLoading(step.key)'
@refresh='refreshDocStep(step.key)'
)
CapitalCouncilStepGroup(
v-else-if='isCouncilGroupStep(step.key)'
:steps='getCouncilStepsForGroup(step.key)'
:busy='submitting || generatingDocument'
:can-declare='canDeclareCouncilStep'
@declare='openCouncilDialog'
)
.capital-onboarding__footer(v-if='showFooter')
BaseButton(
v-if='showBackButton'
variant='ghost'
:disable='footerBusy'
@click='goBack'
) Назад
q-space(v-if='showBackButton && showContinueButton')
BaseButton(
v-if='showContinueButton'
variant='primary'
:class='{ "capital-onboarding__continue--solo": !showBackButton }'
:loading='footerBusy'
@click='goNext'
) {{ continueLabel }}
BaseDialog(
v-model='dialogOpen'
title='Предложение повестки'
size='lg'
:close-on-backdrop='false'
:close-on-escape='false'
@update:model-value='(v) => !v && closeDialog()'
)
Loader(
v-if='generatingDocument'
text='Генерация документа...'
)
template(v-else-if='currentStep && currentGeneratedDoc && !generatingDocument')
.capital-onboarding__dialog-section
div.row.items-center.q-gutter-xs.text-subtitle1.text-weight-medium
q-icon(name="help_outline" size="18px" class="text-primary")
q-icon(name='help_outline' size='18px' color='primary')
span Вопрос на повестке
div.q-mt-sm.q-pa-sm.text-body1.rounded-borders {{ currentStep.question }}
div.q-mt-sm.t-body {{ currentStep.question }}
q-separator.q-my-md
div.row.items-center.q-gutter-xs.text-subtitle1.text-weight-medium
q-icon(name="gavel" size="18px" class="text-primary")
q-icon(name='gavel' size='18px' color='primary')
span Проект решения
div.q-mt-sm.q-pa-sm.rounded-borders
div(v-if="currentStep.decisionPrefix") {{ currentStep.decisionPrefix }}
DocumentHtmlReader(:html="currentGeneratedDoc.html" :sanitize="false")
div.q-mt-sm.text-caption.text-grey-6
div.q-mt-sm
div(v-if='currentStep.decisionPrefix') {{ currentStep.decisionPrefix }}
DocumentHtmlReader(:html='currentGeneratedDoc.html' :sanitize='false')
div.t-caption.text-grey-6.q-mt-sm
strong {{ currentGeneratedDoc.full_title }}
div.q-pb-lg(v-if="currentGeneratedDoc && !generatingDocument")
q-btn(flat label="Отмена" @click="closeDialog" :disable="submitting")
q-btn(
color="primary"
label="Объявить"
:loading="submitting"
@click="submitStep"
)
template(#footer v-if='currentGeneratedDoc && !generatingDocument')
BaseButton(variant='ghost' :disable='submitting' @click='closeDialog') Отмена
BaseButton(variant='primary' :loading='submitting' @click='submitStep') Объявить
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { computed, ref, watch, onMounted, onBeforeUnmount } from 'vue';
import { useQuasar } from 'quasar';
import { DocumentHtmlReader } from 'src/shared/ui/DocumentHtmlReader';
import { BaseBanner, BaseButton } from 'src/shared/ui/base';
import { BaseDialog } from 'src/shared/ui/base/BaseDialog';
import { WindowLoader, Loader } from 'src/shared/ui/Loader';
import { VerticalStepper } from 'src/shared/ui/domain/VerticalStepper';
import type { StepperStep } from 'src/shared/ui/domain/VerticalStepper';
import type { ICouncilOnboardingStep } from 'src/shared/ui/CouncilOnboarding';
import { useDataPoller } from 'src/shared/lib/composables';
import { POLL_INTERVALS } from 'src/shared/lib/consts';
import CapitalProgramDocumentStepPanel from './CapitalProgramDocumentStepPanel.vue';
import CapitalCouncilStepGroup from './CapitalCouncilStepGroup.vue';
import { useCapitalOnboarding } from '../model';
import { useCapitalProgramDocParams, getRegistryIdForDocWizardStep } from '../model/useCapitalProgramDocParams';
import {
COUNCIL_GROUP_FOUNDATION,
COUNCIL_GROUP_OFFERS,
COUNCIL_STEP_GROUP_ORDER,
COUNCIL_WIZARD_STEP_META,
DOC_WIZARD_STEP_BLAGOROST,
DOC_WIZARD_STEP_GENERATOR,
DOC_WIZARD_STEP_ORDER,
getCouncilStepIdsForGroup,
isCouncilGroupStep,
isDocWizardStep,
normalizeWizardStepKey,
type CouncilGroupStepKey,
} from '../model/capitalOnboardingWizard';
const $q = useQuasar();
const {
config,
@@ -96,67 +127,374 @@ const {
generatingDocument,
currentGeneratedDoc,
isOnboardingCompleted,
hasPendingCouncilDecisions,
isDocParamsReady,
loadState,
refreshState,
handleStepClick,
handleStepSubmit,
handleDocParamsSaved,
} = useCapitalOnboarding();
const {
form,
previews,
saving,
loadingRegistryId,
wizardStepKey,
restoreDraftFromStorage,
persistDraftToStorage,
validateSection,
loadPreview,
ensurePreview,
saveParams,
setWizardStepKey,
} = useCapitalProgramDocParams({
onSaved: () => handleDocParamsSaved(),
});
const activeWizardStep = wizardStepKey;
const advancing = ref(false);
const dialogOpen = ref(false);
const currentStep = ref<ICouncilOnboardingStep | null>(null);
const currentStepId = ref<string | null>(null);
const councilSteps = computed(() => config.value?.steps ?? []);
const getIcon = (step: ICouncilOnboardingStep) => {
if (step.status === 'completed') return 'task_alt';
if (step.status === 'in_progress') return 'hourglass_top';
return 'radio_button_unchecked';
};
const wizardSteps = computed<StepperStep[]>(() => [
{
key: DOC_WIZARD_STEP_GENERATOR,
label: 'Параметры «ГЕНЕРАТОР»',
description: 'Переменные положения о целевой программе «ГЕНЕРАТОР»',
},
{
key: DOC_WIZARD_STEP_BLAGOROST,
label: 'Параметры «БЛАГОРОСТ»',
description: 'Переменные положения о целевой программе «БЛАГОРОСТ»',
},
...COUNCIL_STEP_GROUP_ORDER.map((groupKey) => ({
key: groupKey,
label: COUNCIL_WIZARD_STEP_META[groupKey].label,
description: COUNCIL_WIZARD_STEP_META[groupKey].description,
disabled: isCouncilGroupDisabled(groupKey),
})),
]);
const getIconColor = (step: ICouncilOnboardingStep) => {
if (step.status === 'completed') return 'green-6';
if (step.status === 'in_progress') return 'orange-6';
return 'grey-6';
};
const wizardStepOrder = computed<string[]>(() => [
...DOC_WIZARD_STEP_ORDER,
...COUNCIL_STEP_GROUP_ORDER,
]);
const isDependenciesCompleted = (step: ICouncilOnboardingStep) => {
if (!step.depends_on || step.depends_on.length === 0) return true;
if (!config.value?.steps) return true;
const completedWizardSteps = computed(() => {
const completed: string[] = [];
const activeIndex = wizardStepOrder.value.indexOf(activeWizardStep.value);
return step.depends_on.every(depId => {
const depStep = config.value.steps.find(s => s.id === depId);
return depStep && depStep.status === 'completed';
});
};
if (activeIndex > 0 || isDocParamsReady.value) {
completed.push(DOC_WIZARD_STEP_GENERATOR);
}
if (isDocParamsReady.value) {
completed.push(DOC_WIZARD_STEP_BLAGOROST);
}
const showAction = (index: number) => {
if (!config.value?.steps) return false;
const step = config.value.steps[index];
if (!step) return false;
for (const groupKey of COUNCIL_STEP_GROUP_ORDER) {
if (isCouncilGroupCompleted(groupKey)) {
completed.push(groupKey);
}
}
return completed;
});
const footerBusy = computed(() => saving.value || advancing.value || !!loadingRegistryId.value);
const showBackButton = computed(() => {
const index = wizardStepOrder.value.indexOf(activeWizardStep.value);
return index > 0;
});
const showContinueButton = computed(() => isDocWizardStep(activeWizardStep.value));
const showFooter = computed(() => showBackButton.value || showContinueButton.value);
const continueLabel = computed(() =>
activeWizardStep.value === DOC_WIZARD_STEP_BLAGOROST ? 'Сохранить и продолжить' : 'Продолжить',
);
function getCouncilStep(stepId: string): ICouncilOnboardingStep | undefined {
return councilSteps.value.find((step) => step.id === stepId);
}
function getCouncilStepsForGroup(groupKey: CouncilGroupStepKey): ICouncilOnboardingStep[] {
return getCouncilStepIdsForGroup(groupKey)
.map((stepId) => getCouncilStep(stepId))
.filter((step): step is ICouncilOnboardingStep => step != null);
}
function isCouncilGroupCompleted(groupKey: CouncilGroupStepKey): boolean {
return getCouncilStepsForGroup(groupKey).every((step) => step.status === 'completed');
}
function isCouncilGroupDisabled(groupKey: CouncilGroupStepKey): boolean {
if (groupKey === COUNCIL_GROUP_FOUNDATION) {
return !isDocParamsReady.value;
}
if (groupKey === COUNCIL_GROUP_OFFERS) {
return getCouncilStepsForGroup(groupKey).every(
(step) =>
step.status !== 'completed' &&
step.status !== 'in_progress' &&
!canDeclareCouncilStep(step),
);
}
return true;
}
function getFirstAvailableCouncilGroup(): CouncilGroupStepKey | null {
for (const groupKey of COUNCIL_STEP_GROUP_ORDER) {
if (!isCouncilGroupCompleted(groupKey) && !isCouncilGroupDisabled(groupKey)) {
return groupKey;
}
}
return null;
}
function syncWizardStepWithCouncilProgress() {
if (!isDocParamsReady.value || isOnboardingCompleted.value) return;
const firstAvailable = getFirstAvailableCouncilGroup();
if (!firstAvailable) return;
const current = activeWizardStep.value;
if (isDocWizardStep(current)) {
setWizardStepKey(firstAvailable);
return;
}
if (!isCouncilGroupStep(current)) return;
if (isCouncilGroupCompleted(current) && firstAvailable !== current) {
setWizardStepKey(firstAvailable);
}
}
function getDocPreviewHtml(stepKey: string): string {
const registryId = getRegistryIdForDocWizardStep(stepKey);
return registryId ? previews[registryId]?.html ?? '' : '';
}
function isDocStepLoading(stepKey: string): boolean {
const registryId = getRegistryIdForDocWizardStep(stepKey);
return registryId != null && loadingRegistryId.value === registryId;
}
async function refreshDocStep(stepKey: string) {
const registryId = getRegistryIdForDocWizardStep(stepKey);
if (registryId == null) return;
await loadPreview(registryId);
}
function canDeclareCouncilStep(step: ICouncilOnboardingStep): boolean {
if (step.status === 'completed' || step.status === 'in_progress') return false;
return isDependenciesCompleted(step);
};
if (!step.depends_on?.length) return true;
return step.depends_on.every((depId) => getCouncilStep(depId)?.status === 'completed');
}
function onStepperChange(key: string) {
if (isDocWizardStep(key)) {
setWizardStepKey(key);
return;
}
if (isCouncilGroupStep(key) && !isCouncilGroupDisabled(key)) {
setWizardStepKey(key);
}
}
async function goNext() {
if (activeWizardStep.value === DOC_WIZARD_STEP_GENERATOR) {
const validationError = validateSection(994);
if (validationError) {
$q.notify({ type: 'warning', message: validationError });
return;
}
advancing.value = true;
try {
await ensurePreview(994);
setWizardStepKey(DOC_WIZARD_STEP_BLAGOROST);
} finally {
advancing.value = false;
}
return;
}
if (activeWizardStep.value === DOC_WIZARD_STEP_BLAGOROST) {
const validationError = validateSection(998);
if (validationError) {
$q.notify({ type: 'warning', message: validationError });
return;
}
advancing.value = true;
try {
const saved = await saveParams();
if (!saved) return;
const firstCouncilGroup = getFirstAvailableCouncilGroup();
if (firstCouncilGroup) {
setWizardStepKey(firstCouncilGroup);
}
} finally {
advancing.value = false;
}
}
}
function goBack() {
const index = wizardStepOrder.value.indexOf(activeWizardStep.value);
if (index <= 0) return;
setWizardStepKey(wizardStepOrder.value[index - 1]);
}
async function openCouncilDialog(stepId: string) {
const step = getCouncilStep(stepId);
if (!step || !canDeclareCouncilStep(step)) return;
const openDialog = async (step: ICouncilOnboardingStep) => {
currentStep.value = step;
currentStepId.value = step.id;
dialogOpen.value = true;
// Генерируем документ
await handleStepClick(step);
};
}
const closeDialog = () => {
function closeDialog() {
dialogOpen.value = false;
currentStep.value = null;
currentStepId.value = null;
};
}
const submitStep = async () => {
async function submitStep() {
if (!currentStep.value) return;
await handleStepSubmit(currentStep.value);
closeDialog();
};
syncWizardStepWithCouncilProgress();
}
onMounted(loadState);
const shouldPollCouncilState = computed(
() => hasPendingCouncilDecisions.value && !isOnboardingCompleted.value,
);
const { start: startCouncilPoll, stop: stopCouncilPoll } = useDataPoller(
async () => {
await refreshState();
syncWizardStepWithCouncilProgress();
},
{
interval: POLL_INTERVALS.FAST,
immediate: false,
enabled: shouldPollCouncilState,
},
);
function handleVisibilityChange() {
if (document.hidden || !shouldPollCouncilState.value) return;
void refreshState().then(() => syncWizardStepWithCouncilProgress());
}
watch(shouldPollCouncilState, (enabled) => {
if (enabled) {
startCouncilPoll();
} else {
stopCouncilPoll();
}
});
watch(
() => councilSteps.value.map((step) => `${step.id}:${step.status}`).join('|'),
() => {
syncWizardStepWithCouncilProgress();
},
);
watch(
() => activeWizardStep.value,
(stepKey) => {
if (!isDocWizardStep(stepKey)) return;
const registryId = getRegistryIdForDocWizardStep(stepKey);
if (registryId != null) {
void ensurePreview(registryId);
}
},
);
watch(
form,
() => {
persistDraftToStorage();
},
{ deep: true },
);
onMounted(async () => {
await loadState();
restoreDraftFromStorage();
setWizardStepKey(normalizeWizardStepKey(activeWizardStep.value));
syncWizardStepWithCouncilProgress();
if (isDocWizardStep(activeWizardStep.value)) {
const registryId = getRegistryIdForDocWizardStep(activeWizardStep.value);
if (registryId != null) {
void ensurePreview(registryId);
}
}
if (shouldPollCouncilState.value) {
startCouncilPoll();
}
document.addEventListener('visibilitychange', handleVisibilityChange);
});
onBeforeUnmount(() => {
stopCouncilPoll();
document.removeEventListener('visibilitychange', handleVisibilityChange);
});
</script>
<style scoped lang="scss">
.capital-onboarding {
display: flex;
justify-content: center;
min-height: 100%;
}
.capital-onboarding__shell {
width: 100%;
max-width: 920px;
}
.capital-onboarding__banner {
margin-bottom: var(--p-4);
}
.capital-onboarding__stepper {
margin-top: var(--p-2);
}
.capital-onboarding__continue--solo {
margin-left: auto;
}
.capital-onboarding__dialog-section {
padding: var(--p-1) 0;
}
.capital-onboarding__footer {
position: sticky;
bottom: 0;
z-index: 2;
display: flex;
justify-content: space-between;
gap: var(--p-3);
margin-top: var(--p-5);
padding: var(--p-4) 0;
background: var(--p-canvas);
border-top: 1px solid var(--p-line);
}
</style>
@@ -1,387 +1,301 @@
<template lang="pug">
BaseCard(title='Параметры документов ЦПП')
p.capital-doc-params__intro.t-body2
| Заполните поля, которые будут подставлены в положения и оферты программ «ГЕНЕРАТОР» и «БЛАГОРОСТ».
| Перед установкой расширения сформируйте предпросмотр документов: так параметры сохранятся в PrivateData, а в config попадёт только hash.
BaseCard.capital-doc-params(title='Параметры документов ЦПП')
p.t-body-sm.capital-doc-params__intro
| Заполните параметры положений «ГЕНЕРАТОР» и «БЛАГОРОСТ» прямо в тексте документа.
BaseInput(
:model-value='config?.capital_program_doc_data_hash || ""'
label='Hash PrivateData документов'
hint='Появится после формирования предпросмотра'
readonly
mono
)
q-tabs(v-model='activeTab' dense align='left' class='capital-doc-params__tabs')
q-tab(
v-for='section in DOCUMENT_SECTIONS'
:key='section.registryId'
:name='section.registryId'
:label='section.tabLabel'
)
q-badge(v-if='getMissingCount(section.registryId)' color='warning' floating) {{ getMissingCount(section.registryId) }}
BaseBanner(v-if='config?.capital_program_doc_data_hash' variant='pos')
| Параметры сохранены. Hash: {{ config.capital_program_doc_data_hash }}
q-expansion-item(default-opened label='Протокол и кооператив' icon='description')
.row.q-col-gutter-md.q-mt-sm
.col-12.col-md-3(v-for='field in protocolFields' :key='field.key')
BaseInput(
v-model='form[field.key]'
:label='field.label'
:error='fieldErrors[field.key]'
)
.col-12.col-md-6(v-for='field in cooperativeFields' :key='field.key')
BaseInput(
v-model='form[field.key]'
:label='field.label'
:error='fieldErrors[field.key]'
q-tab-panels(v-model='activeTab' animated class='capital-doc-params__panels')
q-tab-panel(
v-for='section in DOCUMENT_SECTIONS'
:key='section.registryId'
:name='section.registryId'
)
.capital-doc-params__toolbar
BaseButton(
color='primary'
outline
:loading='loadingRegistryId === section.registryId'
:disable='!!loadingRegistryId'
@click='loadPreview(section.registryId)'
)
q-icon(name='refresh' class='q-mr-xs')
| {{ previews[section.registryId] ? 'Обновить предпросмотр' : 'Сформировать предпросмотр' }}
q-expansion-item(label='Программа ГЕНЕРАТОР' icon='bolt')
.capital-doc-params__section
BaseInput(
v-for='field in generatorFields'
:key='field.key'
v-model='form[field.key]'
:label='field.label'
:type='field.type || "textarea"'
:autogrow='field.type !== "text"'
:error='fieldErrors[field.key]'
q-inner-loading(:showing='loadingRegistryId === section.registryId')
CapitalProgramInlineDocumentPreview(
v-if='previews[section.registryId]'
v-model='form'
:html='getPreviewHtml(section.registryId)'
:field-labels='FIELD_LABELS'
class='q-mt-md'
)
q-expansion-item(label='Программа БЛАГОРОСТ' icon='eco')
.capital-doc-params__section
BaseInput(
v-for='field in blagorostFields'
:key='field.key'
v-model='form[field.key]'
:label='field.label'
type='textarea'
autogrow
:error='fieldErrors[field.key]'
)
q-expansion-item(label='Возврат, источники и оферты' icon='article')
.capital-doc-params__section
BaseInput(
v-for='field in returnFields'
:key='field.key'
v-model='form[field.key]'
:label='field.label'
:type='field.type || "textarea"'
:autogrow='field.type !== "text"'
:error='fieldErrors[field.key]'
)
p.t-caption.text-grey-6.q-mt-md(v-else)
| Нажмите «Сформировать предпросмотр», чтобы увидеть документ с редактируемыми полями.
.capital-doc-params__footer
p.capital-doc-params__hint.t-caption
| Предпросмотр генерирует документы без публикации и сохраняет PrivateData по hash.
BaseButton(:loading='isGenerating' @click='generatePreview')
| Сформировать предпросмотр
BaseDialog(v-model='previewOpen' title='Предпросмотр положений ЦПП' size='xl')
.capital-doc-params__preview(v-if='previewDocuments.length')
q-tabs(v-model='activePreviewTab' dense align='left' class='capital-doc-params__tabs')
q-tab(
v-for='doc in previewDocuments'
:key='doc.registry_id'
:name='String(doc.registry_id)'
:label='doc.title'
)
q-separator
q-tab-panels(v-model='activePreviewTab' animated)
q-tab-panel(
v-for='doc in previewDocuments'
:key='doc.registry_id'
:name='String(doc.registry_id)'
)
.doc-preview
DocumentHtmlReader(:html='doc.html' :sanitize='false')
template(#footer)
BaseButton(variant='ghost' @click='previewOpen = false')
| Закрыть
BaseButton(
color='primary'
:loading='saving'
:disable='!!loadingRegistryId'
@click='saveParams'
)
| Сохранить параметры
</template>
<script setup lang="ts">
import { computed, reactive, ref, watch } from 'vue';
import { Mutations } from '@coopenomics/sdk';
import { computed, onMounted, reactive, ref, watch } from 'vue';
import { useQuasar } from 'quasar';
import type { Cooperative } from 'cooptypes';
import { useSystemStore } from 'src/entities/System/model';
import { useSessionStore } from 'src/entities/Session';
import { client } from 'src/shared/api/client';
import { FailAlert, SuccessAlert } from 'src/shared/api';
import { BaseBanner, BaseButton, BaseCard, BaseDialog, BaseInput } from 'src/shared/ui/base';
import { DocumentHtmlReader } from 'src/shared/ui/DocumentHtmlReader';
import { BaseButton, BaseCard } from 'src/shared/ui/base';
import { api as documentApi } from 'src/shared/lib/document/api';
import { api } from '../api';
import CapitalProgramInlineDocumentPreview from './CapitalProgramInlineDocumentPreview.vue';
import {
ALL_DOC_FIELDS,
BLAGOROST_DOC_FIELDS,
DOCUMENT_SECTIONS,
FIELD_LABELS,
GENERATOR_DOC_FIELDS,
PREVIEW_PLACEHOLDER,
type EditableFieldKey,
} from '../model/capitalProgramDocFields';
import {
getCapitalProgramDocParamsDraftKey,
readCapitalProgramDocParamsDraft,
writeCapitalProgramDocParamsDraft,
} from '../model/useCapitalProgramDocParamsDraft';
type CapitalProgramPrivateData = {
approval_protocol_number: string;
approval_protocol_day: string;
approval_protocol_month: string;
approval_protocol_year: string;
cooperative_name: string;
cooperative_short_name: string;
cooperative_quoted_name: string;
website: string;
chairman_full_name: string;
generator_program_purpose: string;
eoap_definition: string;
generator_task_goal: string;
idea_unit_cost: string;
idea_unit_cost_words: string;
blagorost_goal_expansion: string;
blagorost_task_expansion: string;
blagorost_task_development: string;
return_source_description: string;
return_additional_source: string;
offer_template_number: string;
};
type FieldDefinition = {
key: keyof CapitalProgramPrivateData;
label: string;
type?: 'text' | 'textarea';
};
type CapitalProgramConfig = Record<string, unknown> & {
capital_program_doc_data_hash?: string | null;
};
type GeneratedPreviewDocument = NonNullable<
Mutations.Documents.GenerateDocument.IOutput[typeof Mutations.Documents.GenerateDocument.name]
>;
interface Props {
config: CapitalProgramConfig;
}
const props = defineProps<Props>();
const emit = defineEmits<{
(event: 'update:config', value: CapitalProgramConfig): void;
saved: [hash: string];
}>();
const system = useSystemStore();
const session = useSessionStore();
const isGenerating = ref(false);
const previewOpen = ref(false);
const activePreviewTab = ref('994');
const previewDocuments = ref<Array<{ registry_id: number; title: string; html: string; hash: string }>>([]);
const fieldErrors = reactive<Partial<Record<keyof CapitalProgramPrivateData, string>>>({});
const $q = useQuasar();
const systemStore = useSystemStore();
const sessionStore = useSessionStore();
const form = reactive<Omit<CapitalProgramPrivateData, 'cooperative_quoted_name'>>({
approval_protocol_number: '',
approval_protocol_day: '',
approval_protocol_month: '',
approval_protocol_year: '',
cooperative_name: '',
cooperative_short_name: '',
website: '',
chairman_full_name: '',
generator_program_purpose: '',
eoap_definition: '',
generator_task_goal: '',
idea_unit_cost: '',
idea_unit_cost_words: '',
blagorost_goal_expansion: '',
blagorost_task_expansion: '',
blagorost_task_development: '',
return_source_description: '',
return_additional_source: '',
offer_template_number: '______',
});
const activeTab = ref<number>(994);
const saving = ref(false);
const savedHash = ref('');
const loadingRegistryId = ref<number | null>(null);
const protocolFields: FieldDefinition[] = [
{ key: 'approval_protocol_number', label: 'Номер протокола' },
{ key: 'approval_protocol_day', label: 'День протокола' },
{ key: 'approval_protocol_month', label: 'Месяц протокола' },
{ key: 'approval_protocol_year', label: 'Год протокола' },
];
type PreviewState = {
html: string;
docDataHash: string;
};
const cooperativeFields: FieldDefinition[] = [
{ key: 'cooperative_name', label: 'Наименование кооператива' },
{ key: 'cooperative_short_name', label: 'Краткое наименование (ПК)' },
{ key: 'website', label: 'Сайт' },
{ key: 'chairman_full_name', label: 'ФИО председателя' },
];
const previews = reactive<Partial<Record<number, PreviewState>>>({});
const generatorFields: FieldDefinition[] = [
{ key: 'generator_program_purpose', label: 'Назначение программы ГЕНЕРАТОР' },
{ key: 'eoap_definition', label: 'Определение ЕОАП' },
{ key: 'generator_task_goal', label: 'Цель задач ГЕНЕРАТОР' },
{ key: 'idea_unit_cost', label: 'Стоимость единицы идеи', type: 'text' },
{ key: 'idea_unit_cost_words', label: 'Стоимость единицы идеи прописью', type: 'text' },
];
const DOC_FIELDS_BY_REGISTRY: Record<number, EditableFieldKey[]> = {
994: GENERATOR_DOC_FIELDS,
998: BLAGOROST_DOC_FIELDS,
};
const blagorostFields: FieldDefinition[] = [
{ key: 'blagorost_goal_expansion', label: 'Расширение цели БЛАГОРОСТ' },
{ key: 'blagorost_task_expansion', label: 'Задача расширения БЛАГОРОСТ' },
{ key: 'blagorost_task_development', label: 'Задача развития БЛАГОРОСТ' },
];
function createEmptyForm(): Record<EditableFieldKey, string> {
return Object.fromEntries(ALL_DOC_FIELDS.map((key) => [key, ''])) as Record<EditableFieldKey, string>;
}
const returnFields: FieldDefinition[] = [
{ key: 'return_source_description', label: 'Источник возврата' },
{ key: 'return_additional_source', label: 'Дополнительный источник возврата' },
{ key: 'offer_template_number', label: 'Номер шаблона оферты', type: 'text' },
];
const form = reactive<Record<EditableFieldKey, string>>(createEmptyForm());
const allFields = computed(() => [
...protocolFields,
...cooperativeFields,
...generatorFields,
...blagorostFields,
...returnFields,
]);
watch(
() => system.info,
(info) => {
if (!info) return;
form.cooperative_name ||= String(info.coopname || '').toUpperCase();
form.cooperative_short_name ||= 'ПК';
form.website ||= info.website || info.contacts?.website || '';
form.chairman_full_name ||= info.chairman?.full_name || info.chairman_full_name || '';
},
{ immediate: true }
const draftStorageKey = computed(() =>
getCapitalProgramDocParamsDraftKey(systemStore.info?.coopname ?? '', sessionStore.username ?? ''),
);
function getPayload(): CapitalProgramPrivateData {
const cooperativeName = String(form.cooperative_name || '').trim();
return {
...Object.fromEntries(
allFields.value.map(({ key }) => [key, String(form[key] || '').trim()])
) as Omit<CapitalProgramPrivateData, 'cooperative_quoted_name'>,
cooperative_quoted_name: cooperativeName ? `«${cooperativeName}»` : '',
};
function restoreDraftFromStorage() {
const draft = readCapitalProgramDocParamsDraft(draftStorageKey.value);
if (!draft) return;
Object.assign(form, draft.form);
savedHash.value = draft.savedHash;
Object.assign(previews, draft.previews);
activeTab.value = draft.activeTab ?? 994;
}
function validatePayload(payload: CapitalProgramPrivateData): string[] {
return allFields.value
.filter(({ key }) => !payload[key])
function persistDraftToStorage() {
writeCapitalProgramDocParamsDraft(draftStorageKey.value, {
form: { ...form },
activeTab: activeTab.value,
savedHash: savedHash.value,
previews: { ...previews },
});
}
const editableFields = computed(() =>
ALL_DOC_FIELDS.map((key) => ({
key,
label: FIELD_LABELS[key],
})),
);
function getPreviewHtml(registryId: number): string {
return previews[registryId]?.html ?? '';
}
function getMissingCount(registryId: number): number {
const fields = DOC_FIELDS_BY_REGISTRY[registryId] ?? [];
return fields.filter((key) => !String(form[key] ?? '').trim()).length;
}
function getPreviewPayload(): Cooperative.Registry.CapitalProgramPrivateData {
return Object.fromEntries(
ALL_DOC_FIELDS.map((key) => [key, String(form[key] ?? '').trim() || PREVIEW_PLACEHOLDER]),
) as Record<EditableFieldKey, string>;
}
function getDocDataHashFromResult(result: Awaited<ReturnType<typeof documentApi.generateDocument>>): string {
const meta = result.meta as { doc_data_hash?: string } | undefined;
return meta?.doc_data_hash ?? '';
}
function validatePayload(): string | null {
const missing = editableFields.value
.filter(({ key }) => !String(form[key] ?? '').trim())
.map(({ label }) => label);
}
function clearFieldErrors(): void {
for (const key of Object.keys(fieldErrors) as Array<keyof CapitalProgramPrivateData>) {
delete fieldErrors[key];
}
}
function setFieldErrors(missingLabels: string[]): void {
clearFieldErrors();
for (const { key, label } of allFields.value) {
if (missingLabels.includes(label)) {
fieldErrors[key] = 'Заполните поле';
}
}
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null;
}
function getDocDataHash(doc: GeneratedPreviewDocument): string | null {
if (!isRecord(doc.meta)) return null;
const hash = doc.meta.doc_data_hash;
return typeof hash === 'string' && hash.length > 0 ? hash : null;
}
async function generateDocument(registry_id: number, payload: CapitalProgramPrivateData): Promise<GeneratedPreviewDocument> {
const { [Mutations.Documents.GenerateDocument.name]: result } = await client.Mutation(
Mutations.Documents.GenerateDocument.mutation,
{
variables: {
input: {
data: {
coopname: system.info?.coopname || '',
username: session.username,
lang: 'ru',
registry_id,
doc_data: payload,
},
options: { skip_save: true },
},
},
}
);
if (!result) {
throw new Error('Документ не был сгенерирован');
}
return result;
}
async function generatePreview() {
const payload = getPayload();
const missing = validatePayload(payload);
if (missing.length) {
setFieldErrors(missing);
FailAlert(`Заполните параметры документов: ${missing.join(', ')}`);
return `Заполните все параметры в документах: ${missing.join(', ')}`;
}
return null;
}
async function loadPreview(registryId: number, options?: { silent?: boolean }) {
if (!options?.silent) {
loadingRegistryId.value = registryId;
}
try {
const result = await documentApi.generateDocument({
coopname: systemStore.info?.coopname ?? '',
username: sessionStore.username ?? '',
registry_id: registryId,
doc_data: getPreviewPayload(),
});
previews[registryId] = {
html: result.html,
docDataHash: getDocDataHashFromResult(result),
};
persistDraftToStorage();
return result;
} catch (error) {
if (!options?.silent) {
$q.notify({
type: 'negative',
message: error instanceof Error ? error.message : 'Не удалось сформировать предпросмотр',
});
}
throw error;
} finally {
if (!options?.silent) {
loadingRegistryId.value = null;
}
}
}
async function saveParams() {
const validationError = validatePayload();
if (validationError) {
$q.notify({ type: 'warning', message: validationError });
return;
}
clearFieldErrors();
saving.value = true;
try {
isGenerating.value = true;
const docs = await Promise.all([
generateDocument(994, payload),
generateDocument(998, payload),
]);
const docDataHash = docs
.map(getDocDataHash)
.find((hash: unknown): hash is string => typeof hash === 'string' && hash.length > 0);
if (!docDataHash) {
throw new Error('Генератор не вернул doc_data_hash');
for (const section of DOCUMENT_SECTIONS) {
await loadPreview(section.registryId, { silent: true });
}
previewDocuments.value = docs.map((doc, index) => ({
registry_id: index === 0 ? 994 : 998,
title: doc.full_title,
html: doc.html,
hash: doc.hash,
}));
activePreviewTab.value = '994';
previewOpen.value = true;
emit('update:config', {
...props.config,
capital_program_doc_data_hash: docDataHash,
const hash = previews[994]?.docDataHash ?? previews[998]?.docDataHash;
if (!hash) {
throw new Error('Не удалось получить хеш параметров документов');
}
await api.saveProgramDocDataHash({ doc_data_hash: hash });
savedHash.value = hash;
emit('saved', hash);
$q.notify({
type: 'positive',
message: 'Параметры документов ЦПП сохранены',
});
SuccessAlert('Параметры документов сохранены в PrivateData');
} catch (error) {
FailAlert(error);
$q.notify({
type: 'negative',
message: error instanceof Error ? error.message : 'Не удалось сохранить параметры',
});
} finally {
isGenerating.value = false;
saving.value = false;
}
}
watch(activeTab, (registryId) => {
persistDraftToStorage();
if (!previews[registryId]) {
void loadPreview(registryId);
}
});
watch(
form,
() => {
persistDraftToStorage();
},
{ deep: true },
);
watch(savedHash, () => {
persistDraftToStorage();
});
onMounted(() => {
restoreDraftFromStorage();
if (!previews[activeTab.value]) {
void loadPreview(activeTab.value);
}
});
</script>
<style scoped lang="scss">
.capital-doc-params__intro {
margin: 0 0 var(--p-4);
color: var(--p-ink-secondary);
color: var(--p-ink-muted);
}
.capital-doc-params__section {
.capital-doc-params__tabs {
margin-bottom: var(--p-2);
}
.capital-doc-params__panels {
background: transparent;
}
.capital-doc-params__toolbar {
display: flex;
flex-direction: column;
gap: var(--p-3);
margin-top: var(--p-2);
align-items: center;
gap: var(--p-2);
}
.capital-doc-params__footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--p-4);
margin-top: var(--p-6);
}
.capital-doc-params__hint {
margin: 0;
color: var(--p-ink-secondary);
}
.capital-doc-params__tabs {
color: var(--p-primary);
}
.doc-preview :deep([data-doc-param]) {
background: var(--p-warn-soft);
border-radius: var(--p-r-sm);
padding: 0 2px;
justify-content: flex-end;
margin-top: var(--p-4);
padding-top: var(--p-4);
border-top: 1px solid var(--p-line);
}
</style>
@@ -0,0 +1,79 @@
<template lang="pug">
.capital-doc-step
.capital-doc-step__preview-shell
.capital-doc-step__toolbar
BaseButton(
variant='ghost'
size='sm'
:disable='loading'
@click='emit("refresh")'
)
q-icon(name='refresh' size='18px')
span.q-ml-xs {{ previewHtml ? 'Обновить предпросмотр' : 'Сформировать предпросмотр' }}
CapitalProgramInlineDocumentPreview(
v-if='previewHtml && !loading'
v-model='form'
:html='previewHtml'
:field-labels='FIELD_LABELS'
)
.capital-doc-step__loading(v-else-if='loading')
q-spinner(size='36px' color='primary')
p.t-body-sm.capital-doc-step__loading-text Формируем предпросмотр документа
EmptyState(
v-else
title='Предпросмотр ещё не сформирован'
body='Нажмите «Сформировать предпросмотр», чтобы увидеть документ с редактируемыми полями.'
)
</template>
<script setup lang="ts">
import { BaseButton, EmptyState } from 'src/shared/ui/base';
import CapitalProgramInlineDocumentPreview from './CapitalProgramInlineDocumentPreview.vue';
import { FIELD_LABELS, type EditableFieldKey } from '../model/capitalProgramDocFields';
defineProps<{
previewHtml: string;
loading: boolean;
}>();
const form = defineModel<Record<EditableFieldKey, string>>({ required: true });
const emit = defineEmits<{
refresh: [];
}>();
</script>
<style scoped lang="scss">
.capital-doc-step__preview-shell {
border: 1px solid var(--p-line);
border-radius: var(--p-r-md);
background: var(--p-surface);
overflow: hidden;
}
.capital-doc-step__toolbar {
display: flex;
justify-content: flex-end;
padding: var(--p-2) var(--p-3);
border-bottom: 1px solid var(--p-line);
background: var(--p-surface-2);
}
.capital-doc-step__loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--p-3);
min-height: 240px;
padding: var(--p-6);
}
.capital-doc-step__loading-text {
margin: 0;
color: var(--p-ink-2);
}
</style>
@@ -0,0 +1,202 @@
<template lang="pug">
.capital-inline-doc.statement(ref='containerRef')
</template>
<script setup lang="ts">
import DOMPurify from 'dompurify';
import { nextTick, onBeforeUnmount, ref, watch } from 'vue';
import type { EditableFieldKey } from '../model/capitalProgramDocFields';
const props = defineProps<{
html: string;
fieldLabels: Partial<Record<EditableFieldKey, string>>;
}>();
const form = defineModel<Record<EditableFieldKey, string>>({ required: true });
const containerRef = ref<HTMLElement | null>(null);
const listeners = new WeakMap<HTMLTextAreaElement, () => void>();
type FocusSnapshot = {
key: EditableFieldKey;
selectionStart: number;
selectionEnd: number;
};
let focusSnapshot: FocusSnapshot | null = null;
function isFieldEmpty(key: EditableFieldKey): boolean {
return !String(form.value[key] ?? '').trim();
}
function applyFieldStyle(textarea: HTMLTextAreaElement, key: EditableFieldKey) {
textarea.classList.toggle('capital-doc-param-inline--empty', isFieldEmpty(key));
}
function captureFocus() {
const active = document.activeElement;
if (!(active instanceof HTMLTextAreaElement) || !active.classList.contains('capital-doc-param-inline')) {
focusSnapshot = null;
return;
}
const key = active.getAttribute('data-field') as EditableFieldKey | null;
if (!key) {
focusSnapshot = null;
return;
}
focusSnapshot = {
key,
selectionStart: active.selectionStart,
selectionEnd: active.selectionEnd,
};
}
function restoreFocus(root: HTMLElement) {
if (!focusSnapshot) return;
const textarea = root.querySelector(`textarea[data-field="${focusSnapshot.key}"]`) as HTMLTextAreaElement | null;
if (!textarea) return;
textarea.focus();
textarea.setSelectionRange(focusSnapshot.selectionStart, focusSnapshot.selectionEnd);
}
function cleanupEditors(root: HTMLElement) {
root.querySelectorAll('textarea.capital-doc-param-inline').forEach((node) => {
const textarea = node as HTMLTextAreaElement;
const handler = listeners.get(textarea);
if (handler) {
textarea.removeEventListener('input', handler);
listeners.delete(textarea);
}
});
}
function mountInlineEditors(root: HTMLElement) {
root.querySelectorAll('[data-doc-param]').forEach((node) => {
const key = node.getAttribute('data-doc-param') as EditableFieldKey | null;
if (!key) return;
const textarea = document.createElement('textarea');
textarea.className = 'capital-doc-param-inline';
textarea.setAttribute('data-field', key);
textarea.rows =
key.includes('description') ||
key.includes('expansion') ||
key.includes('development') ||
key.includes('goal') ||
key.includes('purpose')
? 3
: 2;
textarea.value = form.value[key] ?? '';
textarea.placeholder = props.fieldLabels[key] ?? key;
applyFieldStyle(textarea, key);
const onInput = () => {
form.value[key] = textarea.value;
applyFieldStyle(textarea, key);
};
textarea.addEventListener('input', onInput);
listeners.set(textarea, onInput);
node.replaceWith(textarea);
});
}
function syncInlineEditors(root: HTMLElement) {
root.querySelectorAll('textarea.capital-doc-param-inline').forEach((node) => {
const textarea = node as HTMLTextAreaElement;
const key = textarea.getAttribute('data-field') as EditableFieldKey | null;
if (!key) return;
if (textarea !== document.activeElement && textarea.value !== (form.value[key] ?? '')) {
textarea.value = form.value[key] ?? '';
}
applyFieldStyle(textarea, key);
});
}
async function renderDocument(html: string) {
await nextTick();
const root = containerRef.value;
if (!root) return;
captureFocus();
cleanupEditors(root);
root.innerHTML = DOMPurify.sanitize(html);
mountInlineEditors(root);
restoreFocus(root);
}
watch(
() => props.html,
(html) => {
if (html) void renderDocument(html);
},
{ immediate: true },
);
watch(
form,
() => {
if (containerRef.value) syncInlineEditors(containerRef.value);
},
{ deep: true },
);
onBeforeUnmount(() => {
if (containerRef.value) cleanupEditors(containerRef.value);
});
</script>
<style scoped lang="scss">
.capital-inline-doc {
max-height: 60vh;
overflow: auto;
padding: var(--p-4);
border: 1px solid var(--p-line);
border-radius: var(--p-r-md);
background: var(--p-surface);
}
.capital-inline-doc :deep(.capital-doc-param-inline) {
display: block;
width: 100%;
max-width: 100%;
margin: var(--p-1) 0;
padding: var(--p-2) var(--p-3);
font: inherit;
line-height: inherit;
color: var(--p-ink);
background: color-mix(in srgb, var(--p-primary) 8%, var(--p-surface));
border: 1px dashed var(--p-primary);
border-radius: var(--p-r-sm);
resize: vertical;
box-sizing: border-box;
white-space: pre-wrap;
&:focus {
outline: none;
border-style: solid;
background: color-mix(in srgb, var(--p-primary) 12%, var(--p-surface));
}
&::placeholder {
color: var(--p-ink-muted);
font-size: 0.85em;
}
}
.capital-inline-doc :deep(.capital-doc-param-inline--empty) {
border-color: var(--p-warn);
background: color-mix(in srgb, var(--p-warn) 12%, var(--p-surface));
&:focus {
background: color-mix(in srgb, var(--p-warn) 16%, var(--p-surface));
}
}
</style>
@@ -1,2 +1,4 @@
export { default as CapitalCouncilStepGroup } from './CapitalCouncilStepGroup.vue';
export { default as CapitalOnboardingCard } from './CapitalOnboardingCard.vue';
export { default as CapitalProgramDocumentParametersWidget } from './CapitalProgramDocumentParametersWidget.vue';
export { default as CapitalProgramDocumentStepPanel } from './CapitalProgramDocumentStepPanel.vue';
export { default as CapitalProgramInlineDocumentPreview } from './CapitalProgramInlineDocumentPreview.vue';
@@ -36,9 +36,10 @@ const props = defineProps<{
mask?: string
/** Подставлять плейсхолдеры маски при вводе */
fillMask?: boolean
/** Разрешить только цифры и точку (ОКВЭД: 94.99, 46.73.7). Блокирует
* keypress/paste буквы вообще не появляются в поле. */
digitsDotsOnly?: boolean
/** Разрешить только цифры + перечисленные доп. символы (напр. '.' для
* ОКВЭД: 94.99, 46.73.7; '-' для Рег. номера СФР). Блокирует
* keypress/paste посторонние символы вообще не появляются в поле. */
digitsExtraChars?: string
/** HTML maxlength. */
maxLength?: number
/** Допустимые точные длины (ИЛИ). Напр. `[8, 11]` для ОКТМО. */
@@ -60,12 +61,20 @@ const labelText = computed(() =>
)
const inputModeComputed = computed<'numeric' | 'decimal' | 'text'>(() => {
if (props.digitsDotsOnly) return 'decimal'
// '.' десятичная клавиатура мобильных устройств её показывает; для
// прочих доп.символов (напр. '-') нужна полная клавиатура 'text'.
if (props.digitsExtraChars === '.') return 'decimal'
if (props.digitsExtraChars) return 'text'
// mask из одних `#` = чистая цифровая клавиатура на мобиле.
if (props.mask && /^#+$/.test(props.mask)) return 'numeric'
return 'text'
})
/** Экранирует спецсимволы regex, чтобы вставить строку в символьный класс `[...]`. */
function escapeForCharClass(chars: string): string {
return chars.replace(/[\]\\^-]/g, '\\$&')
}
const NAV_KEYS = new Set([
'Backspace',
'Delete',
@@ -81,19 +90,21 @@ const NAV_KEYS = new Set([
])
// Блокируем keypress не-допустимых символов буквы не попадают в поле даже
// на миллисекунды. Работает только для digitsDotsOnly (для чисто цифровых
// на миллисекунды. Работает только для digitsExtraChars (для чисто цифровых
// полей используем Quasar-mask он нативно блокирует не-# символы).
function onKeydown(e: KeyboardEvent): void {
if (!props.digitsDotsOnly) return
if (!props.digitsExtraChars) return
if (e.ctrlKey || e.metaKey || e.altKey) return
if (NAV_KEYS.has(e.key)) return
if (!/^[\d.]$/.test(e.key)) e.preventDefault()
const re = new RegExp(`^[\\d${escapeForCharClass(props.digitsExtraChars)}]$`)
if (!re.test(e.key)) e.preventDefault()
}
function onPaste(e: ClipboardEvent): void {
if (!props.digitsDotsOnly) return
if (!props.digitsExtraChars) return
const text = e.clipboardData?.getData('text') ?? ''
if (!/^[\d.]*$/.test(text)) e.preventDefault()
const re = new RegExp(`^[\\d${escapeForCharClass(props.digitsExtraChars)}]*$`)
if (!re.test(text)) e.preventDefault()
}
const rules = computed(() => {
@@ -128,7 +139,10 @@ function onInput(raw: string | number | null): void {
// Фоллбек-фильтр на случай programmatic ввода (drop/autofill). Для ручного
// набора актуальна либо mask (q-input сам режет), либо keydown-блокер.
let s = String(raw ?? '')
if (props.digitsDotsOnly) s = s.replace(/[^\d.]+/g, '')
if (props.digitsExtraChars) {
const re = new RegExp(`[^\\d${escapeForCharClass(props.digitsExtraChars)}]+`, 'g')
s = s.replace(re, '')
}
if (props.maxLength && s.length > props.maxLength) s = s.slice(0, props.maxLength)
emit('update:value', s)
}
@@ -37,7 +37,7 @@ q-form.settings-page(@submit.prevent='save' @validation-error='onValidationError
:value='manualInput[f.key]'
:placeholder='f.placeholder'
:mask='f.mask'
:digits-dots-only='f.digitsDotsOnly'
:digits-extra-chars='f.digitsExtraChars'
:max-length='f.maxLength'
:exact-lengths='f.exactLengths'
:pattern='f.pattern'
@@ -60,10 +60,11 @@ q-form.settings-page(@submit.prevent='save' @validation-error='onValidationError
id='field-sfrRegNumber'
label='Рег. номер СФР'
:value='manualInput.sfrRegNumber'
placeholder='XXX-XXX-XXXXXX'
mask='###-###-######'
placeholder='XXX-XXX-XXXXXX или 10 цифр'
digits-extra-chars='-'
:max-length='14'
:pattern='SFR_REG_PATTERN'
pattern-message='Формат: XXX-XXX-XXXXXX (12 цифр)'
pattern-message='Формат: XXX-XXX-XXXXXX (12 цифр) или 10 цифр без разделителей'
required
@update:value='v => (manualInput.sfrRegNumber = v)'
)
@@ -202,7 +203,7 @@ interface ClassifierField {
label: string
placeholder?: string
mask?: string
digitsDotsOnly?: boolean
digitsExtraChars?: string
maxLength?: number
exactLengths?: number[]
pattern?: RegExp
@@ -212,13 +213,13 @@ interface ClassifierField {
// Placeholder'ы правило ввода (кол-во цифр), а не конкретные примеры.
// Для цифровых полей фиксированной/короткой длины используем Quasar-mask
// (`#` = только цифра, буквы блокируются на keypress нативно).
// Для ОКВЭД (цифры + точка переменной длины) digitsDotsOnly с @keydown-блокером.
// Для ОКВЭД (цифры + точка переменной длины) digitsExtraChars='.' с @keydown-блокером.
const classifierFields: ClassifierField[] = [
{
key: 'okved',
label: 'ОКВЭД',
placeholder: 'XX.XX или XX.XX.XX',
digitsDotsOnly: true,
digitsExtraChars: '.',
maxLength: 8,
pattern: /^\d{2}(\.\d{1,2}){0,2}$/,
patternMessage: 'Формат: XX.XX или XX.XX.XX',
@@ -265,7 +266,7 @@ const signerTypeOptions = [
// пользователь прожал пару цифр и нажал «Сохранить»: mask не мешает
// сохранить частичный ввод, паттерн-правило блокирует.
const SNILS_PATTERN = /^\d{3}-\d{3}-\d{3} \d{2}$/
const SFR_REG_PATTERN = /^\d{3}-\d{3}-\d{6}$/
const SFR_REG_PATTERN = /^(\d{3}-\d{3}-\d{6}|\d{10})$/
function getValue(key: keyof IReportRequisitesView): string {
const v = requisites.value?.[key] as any
@@ -162,8 +162,8 @@
:rules='[reportRules.sfrRegNumber()]'
:error='errFor("signer.sfrRegNumber")'
:error-message='msgFor("signer.sfrRegNumber")'
mask='###-###-######'
hint='XXX-XXX-XXXXXX — обязателен для ЕФС-1'
maxlength='14'
hint='XXX-XXX-XXXXXX или 10 цифр — обязателен для ЕФС-1'
dense filled
)
+1 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@coopenomics/desktop",
"version": "2026.6.24",
"version": "2026.7.21",
"description": "Рабочий стол кооператива — Vue 3 + Quasar Framework",
"productName": "Desktop App",
"author": "Alex Ant <dacom.dark.sun@gmail.com>",
@@ -95,7 +95,6 @@
"@types/serviceworker": "^0.0.141",
"@typescript-eslint/eslint-plugin": "^8.57.0",
"@typescript-eslint/parser": "^8.57.0",
"@vue/devtools": "^8.1.1",
"@vue/language-plugin-pug": "~3.2.6",
"autoprefixer": "^10.4.2",
"concurrently": "^8.2.2",
@@ -7,18 +7,19 @@ BaseDialog(
:close-on-backdrop='false',
:close-on-escape='false'
)
div.row.justify-center
div(style="padding-bottom: 100px;").col-md-8.col-col-xs-12
div.row.justify-center.select-branch-overlay__content
div.col-md-8.col-xs-12
div(v-if="step === 1")
p.q-mt-lg
| Кооператив перешёл на двухэтапную систему управления на основании общего собрания уполномоченных председателей кооперативных участков...
Form(
Loader(v-if="branchesLoading" text="Загружаем список кооперативных участков...")
Form#select-branch-form(
v-else
:handler-submit="next"
:is-submitting="isSubmitting"
:showSubmit="!isLoading"
:showSubmit="false"
:showCancel="false"
:button-submit-txt="'Продолжить'"
)
BranchSelector(
v-model:selectedBranch="selectedBranch"
@@ -27,15 +28,29 @@ BaseDialog(
div(v-else-if="step === 2")
Loader(v-if="isLoading" :text="`Формируем документ...`")
DocumentHtmlReader(v-else :html="document.html")
div(v-else)
DocumentHtmlReader(:html="document.html")
q-btn(@click="back" flat) назад
q-btn(@click="sign" color="primary") подписать
template(#footer)
.select-branch-overlay__actions(v-if="step === 1 && !branchesLoading")
BaseButton(
variant='primary',
type='submit',
form='select-branch-form',
:loading='isLoading',
:disabled='!selectedBranch || isLoading',
) Продолжить
.select-branch-overlay__actions(v-else-if="step === 2 && !isLoading")
BaseButton(variant='ghost', @click='back') назад
BaseButton(
variant='primary',
:loading='isSubmitting',
@click='sign'
) подписать
</template>
<script setup lang="ts">
import { BaseDialog } from 'src/shared/ui/base/BaseDialog'
import { BaseButton } from 'src/shared/ui/base/BaseButton'
import { Form } from 'src/shared/ui/Form'
import { Loader } from 'src/shared/ui/Loader'
import { BranchSelector } from 'src/shared/ui/BranchSelector'
@@ -53,12 +68,30 @@ BaseDialog(
document,
isSubmitting,
isLoading,
branchesLoading,
next,
back,
sign
} = useSelectBranchProcess()
</script>
<style scoped lang="scss">
/* Кнопки в слоте footer BaseDialog вне скроллящегося body,
поэтому «Продолжить» / «подписать» всегда видны на мобильных. */
.select-branch-overlay__actions {
width: 100%;
display: flex;
gap: var(--p-2, 8px);
justify-content: flex-end;
flex-wrap: wrap;
padding-bottom: env(safe-area-inset-bottom, 0px);
}
.select-branch-overlay__content {
padding-bottom: var(--p-2, 8px);
}
</style>
<style>
.digital-document .header{
text-align: center;
@@ -236,9 +236,7 @@ const goToCooperativeSite = () => {
flex-direction: column;
gap: 0.75rem;
padding: 1rem;
background: rgba(255, 255, 255, 0.5);
border-radius: 8px;
border: 1px solid rgba(0, 0, 0, 0.05);
}
/* Адаптивность */
@@ -20,31 +20,62 @@ export function useSelectBranchProcess() {
const system = useSystemStore()
const session = useSessionStore()
const branchStore = useBranchStore()
const { selectBranch } = useSelectBranch()
const { isVisible, selectBranch } = useSelectBranch()
const branches = computed(() => branchStore.publicBranches)
const branchesLoading = ref(false)
// Вотчер на авторизацию
const loadBranches = async () => {
if (!session.isAuth || !system.info.coopname) return
try {
branchesLoading.value = true
await branchStore.loadPublicBranches({ coopname: system.info.coopname })
} catch (e: unknown) {
FailAlert(e)
} finally {
branchesLoading.value = false
}
}
// Загрузка при входе
watch(
() => session.isAuth,
(isAuth, wasAuth) => {
if (isAuth && !wasAuth) {
branchStore.loadPublicBranches({ coopname: system.info.coopname })
void loadBranches()
}
},
{ immediate: true }
)
// Перезагрузка при переходе в мажоритарный режим или показе оверлея
watch(
[() => isVisible.value, () => system.info?.cooperator_account?.is_branched],
([visible, branched]) => {
if (visible && branched && session.isAuth) {
void loadBranches()
}
}
)
const next = async () => {
isLoading.value = true
document.value = await digitalDocument.generate<Cooperative.Registry.SelectBranchStatement.Action>({
registry_id: Cooperative.Registry.SelectBranchStatement.registry_id,
coopname: system.info.coopname,
username: session.username,
braname: selectedBranch.value,
})
isLoading.value = false
step.value++
if (!selectedBranch.value || isLoading.value) return
try {
isLoading.value = true
document.value = await digitalDocument.generate<Cooperative.Registry.SelectBranchStatement.Action>({
registry_id: Cooperative.Registry.SelectBranchStatement.registry_id,
coopname: system.info.coopname,
username: session.username,
braname: selectedBranch.value,
})
step.value++
} catch (e: unknown) {
FailAlert(e)
} finally {
isLoading.value = false
}
}
const back = () => {
@@ -80,6 +111,7 @@ export function useSelectBranchProcess() {
document,
isSubmitting,
isLoading,
branchesLoading,
next,
back,
sign,
@@ -9,7 +9,6 @@ div
emit-value
standout="bg-teal text-white"
map-options
@update:model-value="$emit('update:selectedBranch', $event)"
)
template(v-slot:option="scope")
@@ -27,22 +26,13 @@ div
<script lang="ts" setup>
import { type IPublicBranch } from 'src/entities/Branch/model';
import { ref, watch } from 'vue'
const props = defineProps({
defineProps({
branches: {
type: Object as () => IPublicBranch[],
required: true,
},
modelValue: {
type: String,
required: false,
},
})
const selectedBranch = ref(props.modelValue)
watch(() => props.modelValue, (newVal) => {
selectedBranch.value = newVal
})
const selectedBranch = defineModel<string>('selectedBranch', { default: '' })
</script>
@@ -66,21 +66,23 @@ const maxWidthBySize: Record<BaseDialogSize, string> = {
<style scoped>
.base-dialog {
width: 100%;
display: flex;
flex-direction: column;
max-height: calc(100vh - var(--p-6, 48px));
}
/* Maximized: контейнер растягивается на весь экран, контент скроллится. */
.base-dialog--maximized {
width: 100%;
height: 100%;
max-width: none;
display: flex;
flex-direction: column;
max-height: none;
}
.base-dialog--maximized .base-dialog__body {
flex: 1 1 auto;
overflow: auto;
.base-dialog--maximized .base-dialog__foot {
flex-shrink: 0;
}
.base-dialog__head {
display: flex;
flex: 0 0 auto;
align-items: center;
justify-content: space-between;
gap: var(--p-3, 12px);
@@ -94,13 +96,19 @@ const maxWidthBySize: Record<BaseDialogSize, string> = {
}
.base-dialog__body {
display: flex;
flex: 1 1 auto;
flex-direction: column;
min-height: 0;
overflow-y: auto;
padding-bottom: var(--p-2, 8px);
}
.base-dialog__body > :first-child + * {
margin-top: var(--p-4, 16px);
}
.base-dialog__foot {
padding-top: var(--p-2, 8px);
flex: 0 0 auto;
padding: var(--p-3, 12px) var(--p-4, 16px);
border-top: 1px solid var(--p-line);
background: var(--p-surface);
}
</style>
@@ -1,11 +1,6 @@
<template lang="pug">
.extension-install
q-form(:ref='setFormRef')
CapitalProgramDocumentParametersWidget(
v-if='extensionName === "capital"'
:config='config'
@update:config='$emit("update:config", $event)'
)
q-form(ref='formRef')
BaseCard(v-if='schema && !isEmpty', title='Настройки')
ZodForm(
:schema='schema',
@@ -23,7 +18,7 @@ import type { ComponentPublicInstance } from 'vue';
import { ZodForm } from 'src/shared/ui/ZodForm';
import { BaseCard } from 'src/shared/ui/base/BaseCard';
import { isExtensionSchemaEmpty } from 'src/shared/lib/utils';
import { CapitalProgramDocumentParametersWidget } from 'app/extensions/capital/features/Onboarding';
import type { IExtensionConfigSchema } from 'src/entities/Extension/model';
type ExtensionInstallConfig = Record<string, unknown>;
type ExtensionInstallFormRef = {
@@ -32,7 +27,7 @@ type ExtensionInstallFormRef = {
interface Props {
extensionName?: string;
schema?: unknown;
schema?: IExtensionConfigSchema;
config: ExtensionInstallConfig;
formRef?: ExtensionInstallFormRef;
}
@@ -40,12 +35,6 @@ interface Props {
const props = defineProps<Props>();
const isEmpty = computed(() => isExtensionSchemaEmpty(props.schema));
const setFormRef = (element: Element | ComponentPublicInstance | null) => {
if (props.formRef && 'value' in props.formRef) {
props.formRef.value = element;
}
};
</script>
<style scoped lang="scss">
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "@coopenomics/docs-harness",
"version": "2026.6.24",
"version": "2026.7.21",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@coopenomics/docs-harness",
"version": "2026.6.24",
"version": "2026.7.21",
"license": "ISC",
"devDependencies": {
"@playwright/test": "^1.59.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@coopenomics/docs-harness",
"version": "2026.6.24",
"version": "2026.7.21",
"private": true,
"description": "Playwright harness для съёмки скриншотов в документацию (см. bin/shoot.mjs)",
"type": "module",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@coopenomics/docs",
"type": "module",
"version": "2026.6.24",
"version": "2026.7.21",
"private": true,
"packageManager": "pnpm@9.9.0",
"description": "Документация для монорепозитория",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@coopenomics/factory",
"type": "module",
"version": "2026.6.24",
"version": "2026.7.21",
"description": "Фабрика юридических документов кооператива",
"author": "Alex Ant <chairman.voskhod@gmail.com>",
"license": "MIT",
@@ -12,18 +12,24 @@ export class Factory extends DocFactory<BlagorostProgramTemplate.Action> {
}
async generateDocument(data: BlagorostProgramTemplate.Action, options?: IGenerationOptions): Promise<IGeneratedDocument> {
const { template, vars, docData } = await this.resolveParallel({
const { template, vars, coop, docData } = await this.resolveParallel({
template: () => process.env.SOURCE === 'local'
? Promise.resolve(BlagorostProgramTemplate.Template as ITemplate<BlagorostProgramTemplate.Model>)
: this.getTemplate<BlagorostProgramTemplate.Model>(DraftContract.contractName.production, BlagorostProgramTemplate.registry_id, data.block_num),
vars: () => this.getVars(data.coopname),
coop: () => this.getCooperative(data.coopname),
docData: () => this.loadDocData<BlagorostProgramTemplate.Model['doc_data']>(data),
})
const meta: IMetaDocument = await this.getMeta({ title: template.title, ...data })
const doc_data = Cooperative.Registry.requireCapitalProgramPrivateData(docData, BlagorostProgramTemplate.registry_id)
const combinedData: BlagorostProgramTemplate.Model = { meta, vars, doc_data }
const combinedData: BlagorostProgramTemplate.Model = {
meta,
coop,
vars,
doc_data,
}
await this.validate(combinedData, template.model)
@@ -0,0 +1,31 @@
import type { JSONSchemaType } from 'ajv'
import { Cooperative } from 'cooptypes'
export const CapitalApprovalHeaderSchema: JSONSchemaType<Cooperative.Registry.CapitalApprovalHeader> = {
type: 'object',
properties: {
protocol_number: { type: 'string' },
protocol_day_month_year: { type: 'string' },
},
required: ['protocol_number', 'protocol_day_month_year'],
additionalProperties: false,
}
export const CapitalProgramPrivateDataSchema: JSONSchemaType<Cooperative.Registry.CapitalProgramPrivateData> = {
type: 'object',
properties: {
generator_program_purpose: { type: 'string' },
eoap_definition: { type: 'string' },
generator_task_goal: { type: 'string' },
idea_unit_cost: { type: 'string' },
idea_unit_cost_words: { type: 'string' },
blagorost_goal_expansion: { type: 'string' },
blagorost_goal_reason: { type: 'string' },
blagorost_task_expansion: { type: 'string' },
blagorost_task_development: { type: 'string' },
return_source_description: { type: 'string' },
return_additional_source: { type: 'string' },
},
required: [...Cooperative.Registry.capitalProgramPrivateDataRequiredFields],
additionalProperties: false,
}
@@ -1,46 +1,16 @@
import type { JSONSchemaType } from 'ajv'
import { Cooperative } from 'cooptypes'
import type { IMetaDocument, ITemplate } from '../Interfaces'
import type { ITemplate } from '../Interfaces'
import { IMetaJSONSchema } from '../Schema/MetaSchema'
import { CapitalProgramPrivateDataSchema } from '../Schema/CapitalProgramPrivateDataSchema'
import { CommonUserSchema, CooperativeSchema, VarsSchema } from '../Schema'
export const registry_id = Cooperative.Registry.BlagorostOffer.registry_id
// Модель действия для генерации
export type Action = Cooperative.Registry.BlagorostOffer.Action
// Модель данных - используем полную модель с дополнительными полями
export type Model = Cooperative.Registry.BlagorostOffer.Model
const CapitalProgramPrivateDataSchema: JSONSchemaType<Cooperative.Registry.CapitalProgramPrivateData> = {
type: 'object',
properties: {
approval_protocol_number: { type: 'string' },
approval_protocol_day: { type: 'string' },
approval_protocol_month: { type: 'string' },
approval_protocol_year: { type: 'string' },
cooperative_name: { type: 'string' },
cooperative_short_name: { type: 'string' },
cooperative_quoted_name: { type: 'string' },
website: { type: 'string' },
chairman_full_name: { type: 'string' },
generator_program_purpose: { type: 'string' },
eoap_definition: { type: 'string' },
generator_task_goal: { type: 'string' },
idea_unit_cost: { type: 'string' },
idea_unit_cost_words: { type: 'string' },
blagorost_goal_expansion: { type: 'string' },
blagorost_task_expansion: { type: 'string' },
blagorost_task_development: { type: 'string' },
return_source_description: { type: 'string' },
return_additional_source: { type: 'string' },
offer_template_number: { type: 'string' },
},
required: Cooperative.Registry.capitalProgramPrivateDataRequiredFields,
additionalProperties: false,
}
// Схема для сверки - используем расширенную схему для дополнительных данных
export const Schema: JSONSchemaType<Model> = {
type: 'object',
properties: {
@@ -2,6 +2,7 @@ import type { JSONSchemaType } from 'ajv'
import { Cooperative } from 'cooptypes'
import type { ITemplate } from '../Interfaces'
import { IMetaJSONSchema } from '../Schema/MetaSchema'
import { CapitalProgramPrivateDataSchema } from '../Schema/CapitalProgramPrivateDataSchema'
import { CommonUserSchema, CooperativeSchema, VarsSchema } from '../Schema'
export const registry_id = Cooperative.Registry.GeneratorProgramTemplate.registry_id
@@ -12,34 +13,6 @@ export type Action = Cooperative.Registry.GeneratorProgramTemplate.Action
// Модель данных
export type Model = Cooperative.Registry.GeneratorProgramTemplate.Model
const CapitalProgramPrivateDataSchema: JSONSchemaType<Cooperative.Registry.CapitalProgramPrivateData> = {
type: 'object',
properties: {
approval_protocol_number: { type: 'string' },
approval_protocol_day: { type: 'string' },
approval_protocol_month: { type: 'string' },
approval_protocol_year: { type: 'string' },
cooperative_name: { type: 'string' },
cooperative_short_name: { type: 'string' },
cooperative_quoted_name: { type: 'string' },
website: { type: 'string' },
chairman_full_name: { type: 'string' },
generator_program_purpose: { type: 'string' },
eoap_definition: { type: 'string' },
generator_task_goal: { type: 'string' },
idea_unit_cost: { type: 'string' },
idea_unit_cost_words: { type: 'string' },
blagorost_goal_expansion: { type: 'string' },
blagorost_task_expansion: { type: 'string' },
blagorost_task_development: { type: 'string' },
return_source_description: { type: 'string' },
return_additional_source: { type: 'string' },
offer_template_number: { type: 'string' },
},
required: Cooperative.Registry.capitalProgramPrivateDataRequiredFields,
additionalProperties: false,
}
// Схема для сверки
export const Schema: JSONSchemaType<Model> = {
type: 'object',
@@ -2,45 +2,15 @@ import type { JSONSchemaType } from 'ajv'
import { Cooperative } from 'cooptypes'
import type { ITemplate } from '../Interfaces'
import { IMetaJSONSchema } from '../Schema/MetaSchema'
import { CapitalProgramPrivateDataSchema } from '../Schema/CapitalProgramPrivateDataSchema'
import { CommonUserSchema, CooperativeSchema, VarsSchema } from '../Schema'
export const registry_id = Cooperative.Registry.GeneratorOfferTemplate.registry_id
// Модель действия для генерации
export type Action = Cooperative.Registry.GeneratorOfferTemplate.Action
// Модель данных
export type Model = Cooperative.Registry.GeneratorOfferTemplate.Model
const CapitalProgramPrivateDataSchema: JSONSchemaType<Cooperative.Registry.CapitalProgramPrivateData> = {
type: 'object',
properties: {
approval_protocol_number: { type: 'string' },
approval_protocol_day: { type: 'string' },
approval_protocol_month: { type: 'string' },
approval_protocol_year: { type: 'string' },
cooperative_name: { type: 'string' },
cooperative_short_name: { type: 'string' },
cooperative_quoted_name: { type: 'string' },
website: { type: 'string' },
chairman_full_name: { type: 'string' },
generator_program_purpose: { type: 'string' },
eoap_definition: { type: 'string' },
generator_task_goal: { type: 'string' },
idea_unit_cost: { type: 'string' },
idea_unit_cost_words: { type: 'string' },
blagorost_goal_expansion: { type: 'string' },
blagorost_task_expansion: { type: 'string' },
blagorost_task_development: { type: 'string' },
return_source_description: { type: 'string' },
return_additional_source: { type: 'string' },
offer_template_number: { type: 'string' },
},
required: Cooperative.Registry.capitalProgramPrivateDataRequiredFields,
additionalProperties: false,
}
// Схема для сверки
export const Schema: JSONSchemaType<Model> = {
type: 'object',
properties: {
@@ -2,45 +2,15 @@ import type { JSONSchemaType } from 'ajv'
import { Cooperative } from 'cooptypes'
import type { ITemplate } from '../Interfaces'
import { IMetaJSONSchema } from '../Schema/MetaSchema'
import { CapitalProgramPrivateDataSchema } from '../Schema/CapitalProgramPrivateDataSchema'
import { CommonUserSchema, CooperativeSchema, VarsSchema } from '../Schema'
export const registry_id = Cooperative.Registry.GeneratorOffer.registry_id
// Модель действия для генерации
export type Action = Cooperative.Registry.GeneratorOffer.Action
// Модель данных
export type Model = Cooperative.Registry.GeneratorOffer.Model
const CapitalProgramPrivateDataSchema: JSONSchemaType<Cooperative.Registry.CapitalProgramPrivateData> = {
type: 'object',
properties: {
approval_protocol_number: { type: 'string' },
approval_protocol_day: { type: 'string' },
approval_protocol_month: { type: 'string' },
approval_protocol_year: { type: 'string' },
cooperative_name: { type: 'string' },
cooperative_short_name: { type: 'string' },
cooperative_quoted_name: { type: 'string' },
website: { type: 'string' },
chairman_full_name: { type: 'string' },
generator_program_purpose: { type: 'string' },
eoap_definition: { type: 'string' },
generator_task_goal: { type: 'string' },
idea_unit_cost: { type: 'string' },
idea_unit_cost_words: { type: 'string' },
blagorost_goal_expansion: { type: 'string' },
blagorost_task_expansion: { type: 'string' },
blagorost_task_development: { type: 'string' },
return_source_description: { type: 'string' },
return_additional_source: { type: 'string' },
offer_template_number: { type: 'string' },
},
required: Cooperative.Registry.capitalProgramPrivateDataRequiredFields,
additionalProperties: false,
}
// Схема для сверки
export const Schema: JSONSchemaType<Model> = {
type: 'object',
properties: {
@@ -1,60 +1,25 @@
import type { JSONSchemaType } from 'ajv'
import { Cooperative } from 'cooptypes'
import type { IGenerate, IMetaDocument, ITemplate } from '../Interfaces'
import type { IVars } from '../Models'
import type { ITemplate } from '../Interfaces'
import { IMetaJSONSchema } from '../Schema/MetaSchema'
import { VarsSchema } from '../Schema'
import { CapitalProgramPrivateDataSchema } from '../Schema/CapitalProgramPrivateDataSchema'
import { CooperativeSchema, VarsSchema } from '../Schema'
export const registry_id = Cooperative.Registry.BlagorostProgramTemplate.registry_id
// Модель действия для генерации
export interface Action extends IGenerate {
registry_id: number
}
export type Action = Cooperative.Registry.BlagorostProgramTemplate.Action
// Модель данных
export interface Model {
meta: IMetaDocument
vars: IVars
}
export type Model = Cooperative.Registry.BlagorostProgramTemplate.Model
const CapitalProgramPrivateDataSchema: JSONSchemaType<Cooperative.Registry.CapitalProgramPrivateData> = {
type: 'object',
properties: {
approval_protocol_number: { type: 'string' },
approval_protocol_day: { type: 'string' },
approval_protocol_month: { type: 'string' },
approval_protocol_year: { type: 'string' },
cooperative_name: { type: 'string' },
cooperative_short_name: { type: 'string' },
cooperative_quoted_name: { type: 'string' },
website: { type: 'string' },
chairman_full_name: { type: 'string' },
generator_program_purpose: { type: 'string' },
eoap_definition: { type: 'string' },
generator_task_goal: { type: 'string' },
idea_unit_cost: { type: 'string' },
idea_unit_cost_words: { type: 'string' },
blagorost_goal_expansion: { type: 'string' },
blagorost_task_expansion: { type: 'string' },
blagorost_task_development: { type: 'string' },
return_source_description: { type: 'string' },
return_additional_source: { type: 'string' },
offer_template_number: { type: 'string' },
},
required: Cooperative.Registry.capitalProgramPrivateDataRequiredFields,
additionalProperties: false,
}
// Схема для сверки
export const Schema: JSONSchemaType<Model> = {
type: 'object',
properties: {
meta: IMetaJSONSchema,
coop: CooperativeSchema,
vars: VarsSchema,
doc_data: CapitalProgramPrivateDataSchema,
},
required: ['meta', 'vars', 'doc_data'],
required: ['meta', 'coop', 'vars', 'doc_data'],
additionalProperties: true,
}
@@ -2,45 +2,15 @@ import type { JSONSchemaType } from 'ajv'
import { Cooperative } from 'cooptypes'
import type { ITemplate } from '../Interfaces'
import { IMetaJSONSchema } from '../Schema/MetaSchema'
import { CapitalProgramPrivateDataSchema } from '../Schema/CapitalProgramPrivateDataSchema'
import { CooperativeSchema, VarsSchema } from '../Schema'
export const registry_id = Cooperative.Registry.BlagorostOfferTemplate.registry_id
// Модель действия для генерации
export type Action = Cooperative.Registry.BlagorostOfferTemplate.Action
// Модель данных
export type Model = Cooperative.Registry.BlagorostOfferTemplate.Model
const CapitalProgramPrivateDataSchema: JSONSchemaType<Cooperative.Registry.CapitalProgramPrivateData> = {
type: 'object',
properties: {
approval_protocol_number: { type: 'string' },
approval_protocol_day: { type: 'string' },
approval_protocol_month: { type: 'string' },
approval_protocol_year: { type: 'string' },
cooperative_name: { type: 'string' },
cooperative_short_name: { type: 'string' },
cooperative_quoted_name: { type: 'string' },
website: { type: 'string' },
chairman_full_name: { type: 'string' },
generator_program_purpose: { type: 'string' },
eoap_definition: { type: 'string' },
generator_task_goal: { type: 'string' },
idea_unit_cost: { type: 'string' },
idea_unit_cost_words: { type: 'string' },
blagorost_goal_expansion: { type: 'string' },
blagorost_task_expansion: { type: 'string' },
blagorost_task_development: { type: 'string' },
return_source_description: { type: 'string' },
return_additional_source: { type: 'string' },
offer_template_number: { type: 'string' },
},
required: Cooperative.Registry.capitalProgramPrivateDataRequiredFields,
additionalProperties: false,
}
// Схема для сверки
export const Schema: JSONSchemaType<Model> = {
type: 'object',
properties: {
@@ -48,27 +48,29 @@ beforeAll(async () => {
await generator.save('udata', { ...commonUdata, ...data })
}
const capitalProgramPrivateData = {
approval_protocol_number: '1',
approval_protocol_day: '15',
approval_protocol_month: 'января',
approval_protocol_year: '26',
cooperative_name: 'ВОСХОД',
cooperative_short_name: 'ПК',
cooperative_quoted_name: '«ВОСХОД»',
website: 'https://цифровой-кооператив.рф',
chairman_full_name: 'Муравьев Алексей Николаевич',
generator_program_purpose: 'развитию информационной экосистемы взаимодействия физических и юридических лиц, на основе международных кооперативных принципов и законодательства Российской Федерации в отношении потребительских обществ (кооперативов) под названием “Кооперативная Экономика”',
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: 'взносы пользователей ЕОАП, его отдельных программных продуктов и приложений, переданных Обществу или создаваемых в рамках Общества, которые интегрируются в ЕОАП',
offer_template_number: '______',
const capitalProgramPrivateData = {
generator_program_purpose:
"развитию информационной экосистемы взаимодействия физических и юридических лиц, на основе международных кооперативных принципов и законодательства Российской Федерации в отношении потребительских обществ (кооперативов) – под названием “Кооперативная Экономика”, с целью самоорганизации и интеграции в социально-экономическую среду Российской Федерации., на основе международных кооперативных принципов и законодательства Российской Федерации",
eoap_definition:
"информационная экосистема, интегрируемая в социально-экономическую среду Российской Федерации, состоящая из комплекса программных продуктов на базе технологии распределенного реестра, обеспечивающих широкое экономическое и социальное взаимодействие физических и юридических лиц, включая нерезидентов различных юрисдикций и организационно-правовых форм, на основе международных кооперативных принципов и законодательства Российской Федерации в отношении потребительских кооперативов (обществ) под названием “Кооперативная Экономика”",
generator_task_goal:
"центра привлечения и интеграции передовых инновационных цифровых разработок, а также экономических и социальных методов и решений",
idea_unit_cost:
"50",
idea_unit_cost_words:
"пятьдесят",
blagorost_goal_expansion:
"развитии информационной экосистемы взаимодействия физических и юридических лиц, на основе международных кооперативных принципов и законодательства Российской Федерации в отношении потребительских обществ (кооперативов) – под названием “Кооперативная Экономика”, с целью самоорганизации и интеграции в социально-экономическую среду Российской Федерации",
blagorost_goal_reason:
"вследствие увеличения количества Участников информационной кооперативной экосистемы - ЕОАП - для расширения и повышения социальной эффективности их экономического взаимодействия в некоммерческом формате",
blagorost_task_expansion:
"расширение участников ЕОАП - информационной кооперативной экосистемы как центра экономического взаимодействия в некоммерческом формате",
blagorost_task_development:
"развитие ЕОАП как центра привлечения и интеграции инновационных цифровых разработок, а также экономических и социальных методов и решений",
return_source_description:
"аппаратно-программная сеть узлов распределенного реестра в формате «СМЭВ+SWIFT», построенная на принципах самоорганизации и самофинансирования деятельности технологической инфраструктуры ЕОАП, обеспечивающей консенсус ее распределенных узлов по формированию базового продукта ЕОАП - полного цикла документооборота по синхронному взаимодействию пайщиков и кооперативов - участников экосистемы ЕАОП - через использование цифровых контрактов, с одновременным выполнением функций нотариата и учета финансовых и юридических событий (подробнее на Сайте)",
return_additional_source:
"взносы пользователей ЕОАП, его отдельных программных продуктов и приложений, переданных Обществу или создаваемых в рамках Общества, которые интегрируются в ЕОАП"
}
const { hash } = await generator.saveDocData(capitalProgramPrivateData, 994)
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@coopenomics/inter",
"type": "module",
"version": "2026.6.24",
"version": "2026.7.21",
"description": "Контракты и DI-токены для связи расширений контроллера без прямых зависимостей",
"author": "Alex Ant <dacom.dark.sun@gmail.com>",
"license": "MIT",
+2 -2
View File
@@ -1,5 +1,5 @@
{
"local": "049_migrate_phase2_progwallets.ts",
"test": "051_edit_selected_draft.ts",
"prod": "051_edit_selected_draft.ts"
"test": "052_edit_selected_draft.ts",
"prod": "052_edit_selected_draft.ts"
}
@@ -7,7 +7,7 @@ import { api } from '../src/eos'
export class InitialMigration implements Migration {
async run(): Promise {
const ids = [100]
const ids = [994, 995, 996, 998, 999, 1000]
for (const id of ids) {
try {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "migrator",
"version": "2026.6.24",
"version": "2026.7.21",
"description": "Утилита миграции данных между версиями",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@coopenomics/notifications",
"version": "2026.6.24",
"version": "2026.7.21",
"description": "Каталог типобезопасных типов уведомлений и шаблонов Центра уведомлений",
"type": "module",
"private": false,
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "pkg-placeholder",
"version": "2026.6.24",
"version": "2026.7.21",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "pkg-placeholder",
"version": "2026.6.24",
"version": "2026.7.21",
"license": "MIT",
"devDependencies": {
"@antfu/eslint-config": "^2.16.0",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@coopenomics/parser",
"type": "module",
"version": "2026.6.24",
"version": "2026.7.21",
"private": true,
"packageManager": "pnpm@9.0.6",
"description": "Индексатор блокчейна через State History Plugin",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@coopenomics/sdk",
"type": "module",
"version": "2026.6.24",
"version": "2026.7.21",
"private": false,
"packageManager": "pnpm@9.9.0",
"description": "TypeScript SDK для работы с API кооператива",
@@ -4,6 +4,7 @@ export * as CalculateVotes from './calculateVotes'
export * as CloseProject from './closeProject'
export * as CompleteCapitalRegistration from './completeCapitalRegistration'
export * as CompleteOnboardingStep from './completeOnboardingStep'
export * as SaveCapitalProgramDocDataHash from './saveCapitalProgramDocDataHash'
export * as CompleteVoting from './completeVoting'
export * as ConvertSegment from './convertSegment'
export * as CreateCommit from './createCommit'
@@ -0,0 +1,19 @@
import { capitalOnboardingStateSelector } from '../../selectors'
import { $, type GraphQLTypes, type InputType, type ModelTypes, Selector } from '../../zeus'
export const name = 'saveCapitalProgramDocDataHash'
export const mutation = Selector('Mutation')({
[name]: [{ data: $('data', 'SaveCapitalProgramDocDataInput!') }, capitalOnboardingStateSelector],
})
export interface IInput {
/**
* @private
*/
[key: string]: unknown
data: ModelTypes['SaveCapitalProgramDocDataInput']
}
export type IOutput = InputType<GraphQLTypes['Mutation'], typeof mutation>
@@ -2,7 +2,7 @@ import type { GraphQLTypes, InputType, ModelTypes } from '../../zeus/index'
import { rawPaymentMethodPaginationSelector } from '../../selectors/paymentMethods/paginatedPaymentMethodsSelector'
import { $, Selector } from '../../zeus/index'
const name = 'getPaymentMethods'
export const name = 'getPaymentMethods'
/**
* Извлекает методы платежа

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