Compare commits

..

79 Commits

Author SHA1 Message Date
coopops 4b16a18716 chore(release): publish
Release / trigger-mono-docs (push) Has been cancelled
Build bootstrap container / build (push) Has been cancelled
Release / release (push) Has been cancelled
Release / publish-packages (push) Has been cancelled
Release / trigger-coopenomics-docs (push) Has been cancelled
2026-07-23 17:28:42 +00:00
coopops 67121d51cd ci(release): pnpm@10.33.0 везде + skip docs-dispatch без токена
Build bootstrap container / build (push) Has been cancelled
Corepack в docker-сборке mono-base тянул pnpm@9.9.0 из sdk/docs и
падал на HTTP 504 registry.npmjs.org. Выравниваем packageManager на
корневой 10.33.0 (sdk/docs/boot/parser).

trigger-coopenomics-docs: без DOCS_DISPATCH_TOKEN — skip (GitHub-only);
с токеном бьём в Gitea host, не в github.server_url.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-23 17:23:48 +00:00
coopops 489492695b chore(release): publish
Build bootstrap container / build (push) Successful in 8m20s
Release / release (push) Successful in 1h40m17s
Release / publish-packages (push) Successful in 33m36s
Release / trigger-coopenomics-docs (push) Successful in 5s
Release / trigger-mono-docs (push) Successful in 5s
2026-07-23 16:29:15 +00:00
coopops 757c785ee6 fix(capital): pending_clearance только для appendix status=created
findCreatedByUsernameAndProjectHash снова фильтрует по created —
иначе confirmed-допуск давал «Запрос на рассмотрении» на всех проектах.
Parent lookup для документа компонента: confirmed ?? created (двойной отклик).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-23 16:24:38 +00:00
coopops a819b27fbe chore(release): publish
Build bootstrap container / build (push) Successful in 11m3s
Release / release (push) Successful in 1h33m0s
Release / publish-packages (push) Successful in 19m57s
Release / trigger-coopenomics-docs (push) Successful in 29s
Release / trigger-mono-docs (push) Successful in 4s
2026-07-23 11:49:22 +00:00
coopops 2b63f54117 feat(desktop): страница компонента на канон PageTabs + читаемый диалог отклика
Привести ComponentPage к тому же layout, что у проекта (PageTabs, действия в таббаре, без FAB), и исправить белый текст пути проекта в диалоге «Откликнуться на приглашение».

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-23 11:46:05 +00:00
ant 8d0e837bb9 Merge pull request 'Канон-перевод UI Благороста (capital) — зонтичный PR' (#124) from feat/capital-canon into dev
Reviewed-on: #124
2026-07-23 11:12:41 +00:00
coopops 70f2e8e38b Merge remote-tracking branch 'origin/dev' into feat/capital-canon
Typecheck / desktop (pull_request) Successful in 29m45s
Typecheck / controller (pull_request) Successful in 30m48s
2026-07-23 09:23:23 +00:00
coopops 5e58c173ee chore(release): publish
Build bootstrap container / build (push) Successful in 3m45s
Release / publish-packages (push) Successful in 29m23s
Release / release (push) Successful in 29m7s
Release / trigger-mono-docs (push) Successful in 2s
Release / trigger-coopenomics-docs (push) Successful in 2s
2026-07-21 20:19:26 +00:00
coopops c1ce7a095d [989-10][@ant] fix: убрать поле СФР и сделать поля организации/подписанта в форме нулёвых отчётов read-only
Поле «Регистрационный номер СФР» в форме ЕФС-1 не использовалось генератором вовсе (в документ и в имя файла идёт только pfrRegNumber) — убрано вместе с ложной проверкой в fss4.generator.ts, требовавшей его заполнения.

Остальные поля секций «Организация»/«Подписант» в ZeroReportEditor.vue (ФИО, СНИЛС, ИНН/КПП/ОКВЭД/ОГРН/ОКТМО, рег. номер ПФР, должность, тип подписанта) всегда приходят из Реквизитов/БД кооператива и правка через этот черновик никуда не сохранялась — редактируемые поля создавали иллюзию, что правка что-то меняет. Переведены в read-only (readonly+disable, как уже сделано для орг-полей в SettingsPage) — правка доступна только на странице Реквизитов. Раздел «Отчётный год/период/корректировка» остался редактируемым — он не приходит из Реквизитов, это per-документ данные.
2026-07-21 20:17:24 +00:00
coopops 995f65f524 [989-10][@ant] fix: убрать лишний поясняющий хинт у поля «Рег. номер ПФР» — по замечанию пользователя ни у одного другого поля в реквизитах/форме ЕФС-1 такого хинта нет, заполняющий и так знает, что это за номер 2026-07-21 19:12:09 +00:00
coopops 5298e55a7b [989-10][@ant] fix: ЕФС-1 (4-ФСС) — использовать рег. номер ПФР, а не СФР, в имени файла и <ЕФС8:РегНомер>
Сторонние бухгалтерские системы (СБИС и др.) при приёме ЕФС-1 отклоняли выгруженный XML: "Рег. номер отправителя из наименования файла не соответствует ни рег. номеру ПФР организации, ни рег. номеру СФР". Причина в двух местах:
1) generateGenericFileName для FSS4 хардкодил сегмент отправителя как "0000000000" — реальный номер туда не попадал вообще (тот же класс бага, что чинили в idFile для нулёвок ранее).
2) fss4.generator.ts писал в <ЕФС8:РегНомер> sfrRegNumber (10-значный унифицированный номер СФР) — но внешние системы сверяют этот тег с рег. номером ПФР организации (формат XXX-XXX-XXXXXX), который у них уже настроен через канал ФСС ЭДО, а не с номером СФР.

Добавлено отдельное обязательное поле pfrRegNumber (report_requisites.pfr_reg_number, XXX-XXX-XXXXXX) — не заменяет sfrRegNumber (оставлен, вдруг понадобится), а используется отдельно для генерации ЕФС-1: и в сегменте имени файла, и в теге <ЕФС8:РегНомер>. Заодно сузили маску sfrRegNumber до строго 10 цифр (раньше маска дублировала формат ПФР и два поля выглядели взаимозаменяемыми — по замечанию в ревью). Цепочка regen пройдена (generate-schema → generate-client → sdk build), controller tsc и desktop vue-tsc чистые, добавлены/обновлены unit-тесты Fss4Generator.
2026-07-21 18:45:12 +00:00
coopops db28ba1137 [989-10][@ant] fix: чинить генерацию имени файла нулёвых отчётов (idFile/ИдФайл) — терялся код формы ФНС/СФР
generateGenericFileName брал префикс как первый underscore-сегмент имени XSD (xsd.split('_')[0]), а для всех форм с xsdFile вида 'NO_<Форма>_...' первый сегмент всегда равен 'NO' — код формы (NDFL6.2, RASCHSV, PERSSVFL, USN, UVISCHSUMNAL) терялся из имени файла. Внешние системы (в частности приёмка Сбера) отклоняли выгруженный XML как нераспознанный по имени, а после ручного добавления '_NDFL6.2_' в имя падала уже сверка тега <ИдФайл> с фактическим именем файла — оба генерировались одной и той же испорченной функцией. Исправлено: префикс строится из первых двух сегментов XSD-имени (slice(0, 2)), что совпадает с реальным форматом имени файла ФНС/СФР и с эталонными фикстурами tests/fixtures/reports-references/*.
2026-07-21 16:23:21 +00:00
coopops 6a05e88ba1 [989-10][@ant] fix: убрать stack-раскладку (иконка над текстом) у кнопок панели действий в ReportEditorDialog расширения reports — canon-паддинг q-btn (0 14px, без вертикального зазора) при вертикальной раскладке icon+label прижимал иконку к верхней границе кнопки, из-за чего панель «Действия»/«Скачать»/«Отметить сданным» выглядела недоделанной; иконка возвращена в строку рядом с текстом 2026-07-21 15:56:09 +00:00
coopops 09eaa6192c [reports][@ant] fix: показывать серверные ошибки валидации формы отчёта для полей шапки и строк баланса — счётчик «Ошибок: N» был непрозрачным без единой подсказки
BuhotchEditor/ZeroReportEditor подключали fieldErrors только для части полей
(организация, часть подписанта). Поля шапки (reportYear/period/docDate/
correctionNumber) и все строки баланса (BalanceRowEditor — otch/prev/prePrev)
не имели inline-подсветки вообще, хотя backend (validateReportEdits) исправно
возвращал path+message. В проде это привело к невидимой ошибке
organization.ogrn — 12 vs 13 цифр (опечатка в реквизитах, отдельно исправлена
data-фиксом в organizations, не кодом).

- BalanceRowEditor: добавлены basePath/fieldErrors пропсы, подсветка otch/prev/prePrev.
- BuhotchEditor: прокинул field-errors во все 6 BalanceRowEditor, подключил
  header.reportYear/docDate/correctionNumber.
- ZeroReportEditor: подключил header.reportYear/period.
- ReportEditorDialog: добавлен явный список ошибок (path → человекочитаемое
  сообщение) в панели действий — страховка на случай ещё не подключённых полей.
2026-07-21 15:45:20 +00:00
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
coopops f521d65a51 Merge branch 'dev' into feat/capital-canon
Typecheck / desktop (pull_request) Successful in 14m42s
Typecheck / controller (pull_request) Successful in 14m6s
Подтягиваем свежие изменения dev (в т.ч. капитал: CPP-template mapping,
onboarding widget canon). Единственный конфликт — дублирующее
добавление autogrow в BaseInputProps с обеих сторон, разрешено оставлением
варианта с doc-комментарием.
2026-07-20 15:56:12 +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
Alex Ant 45feef2676 fix(capital): correct CPP template mapping and onboarding widget canon
Typecheck / desktop (pull_request) Failing after 13m22s
Typecheck / controller (pull_request) Failing after 13m26s
Fix swapped doc_data placeholders in generator/blagorost templates, align the install widget with Base* components, and use one shared PrivateData hash in factory tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 21:49:30 +03:00
Alex Ant 02ce9400a2 fix(capital): remove unsafe any from CPP onboarding changes
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-28 11:05:20 +05:00
Alex Ant bfa2830650 feat(capital): collect CPP document parameters in onboarding
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-28 10:44:02 +05:00
Alex Ant c7824545cf feat(capital): parameterize CPP templates via private data 2026-06-28 09:50:03 +05:00
Alex Ant b9107e9ee9 chore(release): publish
Build bootstrap container / build (push) Successful in 1m45s
Release / release (push) Failing after 55m53s
Release / publish-packages (push) Has been skipped
Release / trigger-coopenomics-docs (push) Has been skipped
Release / trigger-mono-docs (push) Has been skipped
2026-06-24 22:04:51 +05:00
Alex Ant 4d151befe0 fix(install): блокировать «Продолжить» до минимума совета, удаление без ограничений.
На prod требуется 3 человека (на dev — 1): кнопка и подсказка, состав редактируется свободно до перехода на следующий шаг.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 22:03:12 +05:00
Alex Ant eb16d85abc fix(install): разблокировать мастер при прерванной установке и валидировать состав совета по окружению.
Не показываем заглушку техобслуживания на /install, разрешаем повтор install из maintenance без vars, проверяем минимум членов совета до записи в цепь (3 на prod, 1 на dev).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 11:35:01 +05:00
coopops 710763f7db fix(desktop): страница проекта — скелетон загрузки, канон-фон и шрифты crepe-редактора
Typecheck / desktop (pull_request) Failing after 8m41s
Typecheck / controller (pull_request) Successful in 13m39s
Пустая страница на время загрузки проекта заменена skel-каркасом
(сайдбар + контент). Crepe-редактор: белый хардкод-фон → transparent
(наследует поверхность страницы), шрифты Noto Serif/Noto Sans/Space
Mono → канонные var(--p-sans)/var(--p-mono) в обеих темах.
ContributorSelector: проп loading подключён к q-select (игнорировался).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 19:46:47 +00:00
coopops 9ca51c13c8 feat(desktop): страница проекта — канонный PageTabs с контекстным действием вкладки, FAB убран
Typecheck / desktop (pull_request) Has been cancelled
Typecheck / controller (pull_request) Has been cancelled
Субменю проекта перенесено из шапки (RouteMenuButton/useHeaderActions)
в канонный PageTabs под заголовком. FAB с четырьмя действиями убран —
действие живёт в слоте actions активной вкладки: Компоненты → «+
Компонент», Артефакты → «+ Артефакт», Участники → «+ Соавтор», План →
«+ Инвестиция»; без допуска — «Принять участие» / чип «Запрос на
рассмотрении». Хоткеи сохранены через refs кнопок.

Попутно: PageTabs научен router-link-режиму (нейтрализация глобальных
стилей ссылок); AddAuthorButton/CreateProjectInvestButton переведены
на BaseButton/BaseForm/AmountInput; PendingClearanceButton — статус-чип
вместо чёрного disabled-fab «жидание»; MakeClearanceButton — primary
вместо accent-заливки вне Fab; новый CreateRequirementButton.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 19:24:35 +00:00
coopops 4c86c911c6 feat(desktop): Мастерская — панель фильтров скрыта со страницы, виджет канонизирован на будущее
Typecheck / desktop (pull_request) Has been cancelled
Typecheck / controller (pull_request) Has been cancelled
Фильтры пока не используются — ProjectsFilterPanel убран из
ProjectsListPage (виджет остаётся в проекте). Попутно панель приведена
к канону: ContributorSelector получил проп outlined (канон-вид как
BaseSelect вместо legacy standout), все четыре инпута панели одинаковые.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 19:07:56 +00:00
coopops 7e5ef8e928 feat(desktop): Мастерская — кнопка «Проект» в шапке вместо Fab, фильтры компактной панелью под шапкой
Typecheck / desktop (pull_request) Has been cancelled
Typecheck / controller (pull_request) Has been cancelled
Fab убран; создание проекта — BaseButton в header-actions (хоткей P
сохранён через колбэк exposeOpen). Диалог фильтров разобран:
ProjectsFilterPanel под шапкой — статусы/приоритеты компактными
multiple-селектами, исполнитель и мастер через ContributorSelector,
применение сразу без кнопки, сброс при активных фильтрах,
восстановление участников из сохранённых фильтров. Страница —
flex-колонка: скролл-зона списка не уезжает за вьюпорт.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 19:00:43 +00:00
coopops 572984e32c fix(desktop): Мастерская — выравнивание пустых аватар-слотов + скелетон при загрузке
Typecheck / desktop (pull_request) Failing after 8m14s
Typecheck / controller (pull_request) Successful in 13m18s
Пустая иконка «назначить исполнителя/мастера» центрировалась внутри
слота и выпадала из вертикали заполненных аватарок — прижата вправо
(margin-left: auto), центр иконки совпадает с центром аватара.

Пока исполнители/мастер грузятся, вместо вводящего в заблуждение
пустого плейсхолдера показывается скелетон-кружок той же геометрии;
при фоновом обновлении уже показанные аватарки не мигают.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 18:07:38 +00:00
coopops 6257730ccb feat(desktop): Мастерская — полоски «Добавить компонент/задачу» вместо плюсиков в строках
Typecheck / desktop (pull_request) Failing after 8m15s
Typecheck / controller (pull_request) Has been cancelled
Кнопки создания убраны из строк проектов/компонентов (hover-reveal
плюсики вываливались из сетки). Вместо них — полнострочные
muted-полоски в конце списков: «Добавить компонент» после всех
компонентов проекта (гейт can_edit_project), «Добавить задачу» после
всех задач компонента. В колонке действий строк остался только
аватар мастера.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 17:26:08 +00:00
coopops 641eaaad6d feat(desktop): Мастерская — аватар мастера на строках проектов/компонентов + единые шрифты
Typecheck / desktop (pull_request) Failing after 8m20s
Typecheck / controller (pull_request) Has been cancelled
Новый SetMasterAvatar (зеркало SetCreatorAvatars): аватар мастера в
правой колонке действий строки проекта и компонента, по клику q-menu
с ContributorSelector в одиночном выборе — мастер может быть только
один. Плейсхолдер manage_accounts при пустом мастере, права —
can_set_master. Шрифты выровнены: чип времени задачи переведён на
mono 12px (как метрики проектов), тайтлы всех уровней — --p-fs-body.
Аватарки исполнителей задач переведены с hex/--q-* на токены --p-*.
2026-06-10 17:07:27 +00:00
coopops 3b81f821c7 feat(desktop): Мастерская — Linear-style строки: плоские ID, фиксированные левые колонки, единый формат времени
Typecheck / desktop (pull_request) Failing after 8m16s
Typecheck / controller (pull_request) Successful in 13m13s
EntityIdBadge получил flat-вариант (muted mono-текст без плашки,
копи-иконка по hover) — серые коробки с двумя иконками убраны из строк.
Левые колонки зафиксированы для всех уровней: [chevron/приоритет 28px |
ID 96px | статус-иконка 22px | title] — заголовки уровня стартуют с
одной координаты. Чип времени задачи прижат к правому краю своей
колонки, формат часов синхронизирован («1ч / 4ч» и у проектов, и у
задач), правый отступ строк выровнен (--p-3 у всех уровней).
2026-06-10 16:44:51 +00:00
coopops 112d13114c feat(desktop): Мастерская — фиксированная колоночная сетка справа и hover-кнопки добавления
Typecheck / desktop (pull_request) Failing after 8m15s
Typecheck / controller (pull_request) Successful in 13m1s
Правая часть строк всех трёх уровней — общая сетка фиксированных
колонок [время 110px | статус/инвестиции 132px | действия 120px]:
часы проектов/компонентов и время задач в одной вертикали, статус
задач и инвестиции — в своей, люди/действия — в своей. Ничего не
плывёт при появлении значений. Безликие плюсики заменены подписанными
ghost-кнопками «+ Компонент» / «+ Задача», которые проявляются при
наведении на строку (на touch-устройствах видны всегда).
2026-06-10 15:54:26 +00:00
coopops e1405a3b3d feat(desktop): Мастерская — время задачи в правый блок строки
Typecheck / desktop (pull_request) Failing after 8m27s
Typecheck / controller (pull_request) Successful in 13m9s
IssueTimeChip переехал из мета-блока (между ID и заголовком, смещал
title) в правый actions-блок перед статусом — время задач теперь в
одной вертикали с метриками часов факт/план проектов и компонентов.
2026-06-10 14:53:58 +00:00
coopops 6d4809e8b6 feat(desktop): Мастерская — правки по ревью: пустые слоты, каскад отступов, метрики строк
Typecheck / desktop (pull_request) Failing after 9m1s
Typecheck / controller (pull_request) Successful in 14m0s
Дефолтный no-data q-table (чёрный треугольник) заменён тихим
канон-состоянием (inbox + muted-текст) во всех трёх списках.
Каскадный отступ вложенности задаёт родительский виджет через
слот-ячейку (--p-7, на мобильных --p-3) — сами виджеты при одиночном
использовании отступа не имеют. На строках проектов и компонентов —
компактные метрики: часы исполнителей факт/план и привлечённые
инвестиции факт/план (компактный формат, скрываются на мобильных,
инвестиции — только если ненулевые).
2026-06-10 12:31:19 +00:00
coopops edc339b673 feat(desktop): Мастерская Благороста на канон MONO Platform v2
Typecheck / controller (pull_request) Has been cancelled
Typecheck / desktop (pull_request) Has been cancelled
Дерево проекты→компоненты→задачи переведено на канон: убраны q-card
обёртки и WindowLoader (скелетон .skel вместо полноэкранного лоадера),
FontAwesome-иконки статусов заменены Material-эквивалентами, цвета
статусов/приоритетов задач и проектов — канон-палитра
(positive/negative/warning/info/primary/grey-6) вместо сырых
green/red/purple, кнопки «Компонент»/«Создать задачу» — BaseButton,
лейблы задач — BaseChip, попапы статуса и времени — токены --p-*
вместо white/grey hex (чинит тёмную тему), hover тайтлов — --p-primary
вместо accent, инлайн-px — классы и токены. Fab — primary вместо
accent-заливки (канон запрещает accent на кнопках).
2026-06-10 12:16:39 +00:00
coopops 9b26834033 feat(desktop): вернуть красный тик затухания энергии в бейдж уровня
Typecheck / desktop (pull_request) Failing after 9m1s
Typecheck / controller (pull_request) Has been cancelled
При канонизации ContributorGamificationWidget потерялся мигающий
красный arrow_left перед процентом энергии — индикатор того, что
энергия затухает со временем и взнос для следующего уровня растёт.
Возвращён внутрь канон-чипа BaseChip.
2026-06-10 11:55:03 +00:00
coopops ecf8d2b5de feat(desktop): профиль Благороста — правки по ревью
Typecheck / desktop (pull_request) Failing after 9m13s
Typecheck / controller (pull_request) Successful in 13m57s
Кошельки Генератор/Благорост — в одну строку по col-md-6 (на мобильных
во всю ширину). Карандаш редактирования — рядом с заголовком поля, а не
оторван справа. Параметры участия и взносы по ролям обогащены:
иконки-плитки на нейтральном фоне, hairline-разделители, выделенная
плашка итога взносов, крупные mono-значения.
2026-06-10 09:00:32 +00:00
coopops 14170e6d93 feat(desktop): профиль Благороста на канон MONO Platform v2
Typecheck / controller (pull_request) Has been cancelled
Typecheck / desktop (pull_request) Has been cancelled
Страница CapitalProfilePage и её виджеты переведены на базовые
компоненты канона: IdentityPanel + бейдж уровня, WalletCard для
кошельков Генератора/Благороста, BaseCard + DataRow для взносов,
редактируемые поля на BaseForm/BaseInput/AmountInput, кнопка
«Инвестировать» — в header actions. BaseInput расширен type=textarea.
Скелетон вместо спиннера, токены --p-* вместо hex/px.
2026-06-10 08:51:16 +00:00
458 changed files with 5785 additions and 26801 deletions
+31 -10
View File
@@ -390,21 +390,34 @@ 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 }}
# ============================================================================
# trigger-coopenomics-docs — пересборка сайта C9S/coopenomics.
# Гейт: ветка main.
# Гейт: ветка main + задан secret DOCS_DISPATCH_TOKEN.
#
# coopenomics переехал на Gitea (C9S/coopenomics); его publish-docs.yaml
# клонирует mono и пересобирает сайт. Gitea НЕ имеет API для
# repository_dispatch — только workflow_dispatch, поэтому дёргаем целевой
# workflow через Gitea API с PAT (secret DOCS_DISPATCH_TOKEN — префикс GITEA_
# у секретов зарезервирован Gitea, нельзя; права write на C9S/coopenomics).
# Целевой workflow слушает workflow_dispatch с входами
# mono_sha/mono_ref.
# Dual-host: Actions могут бежать на GitHub или Gitea. Сайт доков живёт на
# Gitea (C9S/coopenomics), поэтому API всегда бьёт в GITEA_SERVER_URL
# (var, дефолт https://git.coopenomics.world) — НЕ в github.server_url
# (на GitHub Actions тот = github.com → 410).
#
# Без DOCS_DISPATCH_TOKEN job пропускается (GitHub-only режим: токен не
# задаём → релиз не валится). На Gitea задаём PAT (префикс GITEA_ у секретов
# зарезервирован, поэтому имя DOCS_DISPATCH_TOKEN; write на C9S/coopenomics).
# ============================================================================
trigger-coopenomics-docs:
needs: release
@@ -412,12 +425,20 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Trigger coopenomics website rebuild (Gitea workflow_dispatch)
env:
DOCS_DISPATCH_TOKEN: ${{ secrets.DOCS_DISPATCH_TOKEN }}
GITEA_SERVER_URL: ${{ vars.GITEA_SERVER_URL }}
run: |
if [ -z "$DOCS_DISPATCH_TOKEN" ]; then
echo "DOCS_DISPATCH_TOKEN не задан — skip (GitHub-only / без Gitea docs dispatch)"
exit 0
fi
GITEA_SERVER_URL="${GITEA_SERVER_URL:-https://git.coopenomics.world}"
curl -fsSL -X POST \
-H "Authorization: token ${{ secrets.DOCS_DISPATCH_TOKEN }}" \
-H "Authorization: token ${DOCS_DISPATCH_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"ref":"master","inputs":{"mono_sha":"${{ github.sha }}","mono_ref":"${{ github.ref }}"}}' \
"${{ github.server_url }}/api/v1/repos/C9S/coopenomics/actions/workflows/publish-docs.yaml/dispatches"
"${GITEA_SERVER_URL}/api/v1/repos/C9S/coopenomics/actions/workflows/publish-docs.yaml/dispatches"
# ============================================================================
# trigger-mono-docs — деплой ВТОРОЙ документации (сайт доков mono) через
-1
View File
@@ -1,5 +1,4 @@
{
"cSpell.enabled": false,
"editor.bracketPairColorization.enabled": true,
"editor.guides.bracketPairs": true,
"editor.tabCompletion": "onlySnippets",
+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.16-1",
"version": "2026.7.23-2",
"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.
+2 -2
View File
@@ -1,9 +1,9 @@
{
"name": "@coopenomics/boot",
"type": "module",
"version": "2026.6.16-1",
"version": "2026.7.23-2",
"private": true,
"packageManager": "pnpm@9.0.6",
"packageManager": "pnpm@10.33.0",
"description": "CLI-утилита инициализации блокчейна и кооператива",
"author": "Alex Ant <dacom.dark.sun@gmail.com>",
"license": "",
+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.16-1",
"version": "2026.7.23-2",
"private": true,
"description": "Обёртка над кошельком cleos для EOSIO блокчейна",
"scripts": {
@@ -5,25 +5,6 @@
#include "src/deletebranch.cpp"
#include "src/deltrusted.cpp"
#include "src/editbranch.cpp"
#include "src/setprivate.cpp"
#include "src/addwhite.cpp"
#include "src/delwhite.cpp"
#include "src/createdec.cpp"
#include "src/joindec.cpp"
#include "src/startdec.cpp"
#include "src/votedec.cpp"
#include "src/closedec.cpp"
#include "src/exec.cpp"
#include "src/confirmdec.cpp"
#include "src/declinedec.cpp"
#include "src/canceldec.cpp"
#include "src/apprliab.cpp"
#include "src/declliab.cpp"
#include "src/apprauth.cpp"
#include "src/declauth.cpp"
#include "src/reqtrusted.cpp"
#include "src/apprtrusted.cpp"
#include "src/decltrusted.cpp"
using namespace eosio;
+2 -39
View File
@@ -63,43 +63,6 @@ public:
[[eosio::action]] void deletebranch(eosio::name coopname, eosio::name braname);
[[eosio::action]] void addtrusted(eosio::name coopname, eosio::name braname, eosio::name trusted);
[[eosio::action]] void deltrusted(eosio::name coopname, eosio::name braname, eosio::name trusted);
// Приватность кооперативного участка и управление белым списком (председатель совета).
// Приватный участок нельзя выбрать (selectbranch), если аккаунт не в белом списке участка.
[[eosio::action]] void setprivate(eosio::name coopname, eosio::name braname, bool is_private);
[[eosio::action]] void addwhite(eosio::name coopname, eosio::name braname, eosio::name account);
[[eosio::action]] void delwhite(eosio::name coopname, eosio::name braname, eosio::name account);
// Универсальный механизм «собрание → решение» (L1)
[[eosio::action]] void createdec(eosio::name coopname, eosio::checksum256 hash, eosio::name type, eosio::name initiator, document2 proposal, eosio::name braname, std::vector<decision_point> agenda);
[[eosio::action]] void joindec(eosio::name coopname, eosio::checksum256 hash, eosio::name username);
[[eosio::action]] void startdec(eosio::name coopname, eosio::checksum256 hash, eosio::name chairman, std::string address, std::vector<decision_point> agenda);
[[eosio::action]] void votedec(eosio::name coopname, eosio::checksum256 hash, eosio::name username, document2 ballot, std::vector<decision_vote_point> votes);
[[eosio::action]] void closedec(eosio::name coopname, eosio::checksum256 hash, document2 protocol);
// Расширение автоматизируемого решения (L2): создание кооперативного участка через совет.
// Председатель участка подписывает пакетом заявление, договор о полной материальной
// ответственности (liability) и доверенность председателю участка (authority).
[[eosio::action]] void exec(eosio::name coopname, eosio::checksum256 hash, document2 petition, document2 liability, document2 authority);
[[eosio::action]] void confirmdec(eosio::name coopname, eosio::checksum256 hash, document2 authorization);
[[eosio::action]] void declinedec(eosio::name coopname, eosio::checksum256 hash, std::string reason);
[[eosio::action]] void canceldec(eosio::name coopname, eosio::checksum256 hash, std::string reason);
// Договор о полной материальной ответственности председателя КУ: встречная подпись председателя совета
// (callback'и одобрения совета; отклонение заблокировано — решение об учреждении участка уже принято)
[[eosio::action]] void apprliab(eosio::name coopname, eosio::name username, eosio::checksum256 approval_hash, document2 approved_document);
[[eosio::action]] void declliab(eosio::name coopname, eosio::name username, eosio::checksum256 approval_hash, std::string reason);
// Доверенность председателя КУ: встречная подпись председателя совета
// (отдельное одобрение совета рядом с договором матответственности; отклонение заблокировано)
[[eosio::action]] void apprauth(eosio::name coopname, eosio::name username, eosio::checksum256 approval_hash, document2 approved_document);
[[eosio::action]] void declauth(eosio::name coopname, eosio::name username, eosio::checksum256 approval_hash, std::string reason);
// Доверенные лица по заявлению: договор о полной материальной ответственности (application)
// и доверенность доверенному лицу (authority) — оба со встречной подписью председателя участка
[[eosio::action]] void reqtrusted(eosio::name coopname, eosio::name braname, eosio::name username, eosio::checksum256 hash, document2 application, document2 authority);
[[eosio::action]] void apprtrusted(eosio::name coopname, eosio::checksum256 hash, document2 countersigned, document2 countersigned_authority);
[[eosio::action]] void decltrusted(eosio::name coopname, eosio::checksum256 hash, std::string reason);
};
@@ -1,135 +0,0 @@
# ─────────────────────────────────────────────────────────────────────────────
# Стандарт «Собрание пайщиков и решение участка».
#
# Контракт branch ведёт универсальный механизм собраний пайщиков: от объявления
# собрания и присоединения участников до голосования бюллетенями и утверждения
# протокола председателем собрания. Свободное решение фиксируется протоколом и
# на этом завершается. Решение об учреждении кооперативного участка дополнительно
# выносится на рассмотрение совета и по его утверждению создаёт участок.
#
# Источники в коде:
# • cpp/branch/src/createdec.cpp — объявление собрания и повестки
# • cpp/branch/src/joindec.cpp — присоединение участника
# • cpp/branch/src/startdec.cpp — открытие голосования (с назначением председателя)
# • cpp/branch/src/votedec.cpp — бюллетень участника
# • cpp/branch/src/closedec.cpp — утверждение протокола председателем
# • cpp/branch/src/exec.cpp — заявление в совет (для учреждения участка)
# • cpp/branch/src/confirmdec.cpp — учреждение участка по решению совета
# ─────────────────────────────────────────────────────────────────────────────
process_type: brn.decide
id: public_branch_decide_process
title: Собрание пайщиков и решение участка
slug: decide
status: proposed
contract: branch
summary: >
От объявления собрания пайщиков и присоединения участников до голосования
бюллетенями и утверждения протокола председателем. Свободное решение
фиксируется протоколом; решение об учреждении кооперативного участка
выносится на совет и по его утверждению создаёт участок.
purpose: >
«Собрание пайщиков и решение участка» — способ для пайщиков собраться в
любой момент и принять решение по своей повестке. Инициатор объявляет
собрание, желающие присоединяются, председатель собрания открывает
голосование, участники голосуют бюллетенями (за / против / воздержался по
каждому вопросу), председатель утверждает протокол. Свободное решение на
этом завершается как документ. Решение об учреждении кооперативного участка
председатель направляет на рассмотрение совета; по утверждению советом
участок создаётся с указанными реквизитами.
roles:
- participant # Пайщик-инициатор / участник / голосующий
- chairman # Председатель собрания (для учреждения участка — будущий председатель участка)
- soviet # Совет — утверждение учреждения участка
# ── Действия контракта ───────────────────────────────────────────────────────
actions:
- name: branch::createdec
human: Объявить собрание
actor: Пайщик
role: opener
purpose: >
Инициатор формирует повестку дня и подписывает предложение о проведении
собрания. Для учреждения кооперативного участка повестка включает вопросы
об организации участка, адресе привязки и избрании председателя. Собрание
открывается для присоединения участников.
- name: branch::joindec
human: Присоединиться к собранию
actor: Пайщик
role: progress
purpose: >
Пайщик входит в состав участников собрания.
- name: branch::startdec
human: Открыть голосование
actor: Пайщик
role: progress
purpose: >
Организатор собрания — он же председатель собрания по праву его создания —
открывает голосование, указывая избираемого собранием председателя
кооперативного участка из числа присоединившихся участников, а также
адрес привязки участка, определённый собранием. Повестку в этот момент
можно дополнить вопросами, внесёнными участниками на собрании. Открыть
голосование можно только при наличии не менее трёх участников собрания —
иначе кворум недостижим и собрание можно лишь отменить. Окно голосования
отмеряется автоматически и составляет 15 минут — голосование проходит прямо
на собрании.
- name: branch::votedec
human: Подать бюллетень
actor: Пайщик
role: progress
purpose: >
Участник подаёт подписанный бюллетень с волеизъявлением (за / против /
воздержался) по каждому вопросу повестки. Подсчёт голосов ведёт система.
Бюллетень публикуется в реестре документов и привязывается к собранию.
- name: branch::closedec
human: Утвердить протокол
actor: Пайщик
role: closer
purpose: >
Председатель собрания закрывает голосование и утверждает протокол решения
своей подписью. Протокол публикуется в реестре документов и завершает
пакет документов собрания (бюллетени и протокол собираются вместе).
Свободное решение на этом завершается. Решение об учреждении участка
переходит к исполнению.
- name: branch::exec
human: Направить заявление в совет
actor: Пайщик
role: progress
purpose: >
Председатель собрания формирует заявление в совет об учреждении
кооперативного участка; к нему прилагаются протокол собрания и бюллетени.
Вопрос выносится на рассмотрение совета.
links:
- process_type: sov.authpkg
label: Типовой процесс решения совета
- name: branch::confirmdec
human: Учредить участок по решению совета
actor: Совет
role: closer
purpose: >
По утверждению советом кооперативный участок создаётся с указанными
реквизитами (наименование, адрес привязки, избранный председатель).
- name: branch::declinedec
human: Отклонить решением совета
actor: Совет
role: rollback
purpose: >
При отказе совета вопрос об учреждении участка снимается; причина
фиксируется в журнале.
- name: branch::canceldec
human: Отменить собрание
actor: Пайщик
role: rollback
purpose: >
Инициатор сворачивает собрание до вынесения вопроса на совет.
operations: []
@@ -1,61 +0,0 @@
# ─────────────────────────────────────────────────────────────────────────────
# Стандарт «Приём доверенного лица кооперативного участка».
#
# Контракт branch ведёт приём доверенных лиц участка по заявлению: пайщик
# подаёт заявление и договор о полной материальной ответственности, председатель
# участка одобряет встречной подписью, и пайщик становится доверенным лицом.
#
# Источники в коде:
# • cpp/branch/src/reqtrusted.cpp — подача заявки
# • cpp/branch/src/apprtrusted.cpp — одобрение председателем участка
# • cpp/branch/src/decltrusted.cpp — отклонение председателем участка
# ─────────────────────────────────────────────────────────────────────────────
process_type: brn.trust
id: public_branch_trust_process
title: Приём доверенного лица кооперативного участка
slug: trust
status: proposed
contract: branch
summary: >
Пайщик подаёт заявление и договор о полной материальной ответственности;
председатель кооперативного участка одобряет встречной подписью, и пайщик
входит в состав доверенных лиц участка (не более трёх).
purpose: >
«Приём доверенного лица кооперативного участка» позволяет пайщику стать
доверенным лицом участка по заявлению. Заявитель подписывает заявление и
договор о полной материальной ответственности; председатель участка
рассматривает заявку и одобряет её встречной подписью на договоре, после
чего пайщик получает права доверенного лица.
roles:
- participant # Пайщик-заявитель
- chairman # Председатель кооперативного участка
# ── Действия контракта ───────────────────────────────────────────────────────
actions:
- name: branch::reqtrusted
human: Подать заявку доверенного
actor: Пайщик
role: opener
purpose: >
Пайщик подаёт заявку на приём доверенным лицом выбранного участка, прилагая
подписанные заявление и договор о полной материальной ответственности.
- name: branch::apprtrusted
human: Одобрить доверенного
actor: Председатель участка
role: closer
purpose: >
Председатель участка одобряет заявку встречной подписью на договоре
материальной ответственности; пайщик входит в состав доверенных лиц
участка.
- name: branch::decltrusted
human: Отклонить заявку доверенного
actor: Председатель участка
role: rollback
purpose: >
Председатель участка отклоняет заявку; причина фиксируется в журнале.
operations: []
@@ -1,25 +0,0 @@
/**
* @brief Добавление пайщика в белый список кооперативного участка.
* Пайщики из белого списка могут выбрать приватный кооперативный участок при вступлении
* или смене участка. В белый список можно добавить любого пайщика кооператива.
* @param coopname Наименование кооператива
* @param braname Наименование кооперативного участка
* @param account Аккаунт пайщика для добавления
* @ingroup public_actions
* @ingroup public_branch_actions
* @note Авторизация требуется от аккаунта: @p coopname
*/
[[eosio::action]] void branch::addwhite(eosio::name coopname, eosio::name braname, eosio::name account) {
check_auth_or_fail(_branch, coopname, coopname, "addwhite"_n);
branch_index branches(_branch, coopname.value);
auto branch = branches.find(braname.value);
eosio::check(branch != branches.end(), "Кооперативный участок не найден");
get_account_or_fail(account);
branches.modify(branch, coopname, [&](auto &b) {
b.add_account_to_whitelist(account);
});
}
@@ -1,22 +0,0 @@
/**
* @brief Встречная подпись председателя совета на доверенности председателя
* кооперативного участка.
* Callback одобрения совета: председатель совета подписал доверенность второй
* подписью (механизм soviet::confirmapprv проверил его подпись). Фиксируем
* полностью подписанную доверенность в реестре документов по якорю одобрения.
* @param coopname Наименование кооператива
* @param username Председатель кооперативного участка — уполномоченный по доверенности
* @param approval_hash Якорь одобрения (хэш документа доверенности)
* @param approved_document Доверенность с встречной подписью председателя совета
* @ingroup public_actions
* @ingroup public_branch_actions
* @note Авторизация требуется от аккаунта: @p soviet
*/
[[eosio::action]] void branch::apprauth(eosio::name coopname, eosio::name username, eosio::checksum256 approval_hash, document2 approved_document) {
require_auth(_soviet);
// Доверенность подписана обеими сторонами (председатель участка + председатель совета) —
// фиксируем её в реестре документов как принятую.
::Soviet::make_complete_document(_branch, coopname, username, "branchauth"_n, approval_hash, approved_document);
}
@@ -1,22 +0,0 @@
/**
* @brief Встречная подпись председателя совета на договоре о полной материальной
* ответственности председателя кооперативного участка.
* Callback одобрения совета: председатель совета подписал договор второй подписью
* (механизм soviet::confirmapprv проверил его подпись). Фиксируем полностью
* подписанный договор в реестре документов по якорю процесса.
* @param coopname Наименование кооператива
* @param username Председатель кооперативного участка — сторона «Исполнитель»
* @param approval_hash Якорь процесса (хэш одобрения)
* @param approved_document Договор с встречной подписью председателя совета
* @ingroup public_actions
* @ingroup public_branch_actions
* @note Авторизация требуется от аккаунта: @p soviet
*/
[[eosio::action]] void branch::apprliab(eosio::name coopname, eosio::name username, eosio::checksum256 approval_hash, document2 approved_document) {
require_auth(_soviet);
// Договор подписан обеими сторонами (председатель участка + председатель совета) —
// фиксируем его в реестре документов как принятый.
::Soviet::make_complete_document(_branch, coopname, username, "branchliab"_n, approval_hash, approved_document);
}
@@ -1,42 +0,0 @@
/**
* @brief Одобрение заявки доверенного лица председателем участка.
* Председатель кооперативного участка накладывает встречную подпись на договор
* материальной ответственности и доверенность заявителя; оба полностью подписанных
* документа фиксируются в реестре документов, пайщик добавляется в список доверенных
* лиц участка, заявка стирается.
* @param coopname Наименование кооператива
* @param hash Внешний идентификатор заявки
* @param countersigned Договор материальной ответственности со встречной подписью председателя участка
* @param countersigned_authority Доверенность доверенному лицу со встречной подписью председателя участка
* @ingroup public_actions
* @ingroup public_branch_actions
* @note Авторизация требуется от аккаунта: @p coopname
*/
[[eosio::action]] void branch::apprtrusted(eosio::name coopname, eosio::checksum256 hash, document2 countersigned, document2 countersigned_authority) {
check_auth_or_fail(_branch, coopname, coopname, "apprtrusted"_n);
verify_document_or_fail(countersigned);
verify_document_or_fail(countersigned_authority);
auto req = get_trustreq_or_fail(coopname, hash);
auto br = get_branch_or_fail(coopname, req.braname);
eosio::check(!br.is_account_in_trusted(req.username), "Пайщик уже является доверенным лицом участка");
eosio::check(br.trusted.size() < 3, "Достигнут предел доверенных лиц участка (не более трёх)");
branch_index branches(_branch, coopname.value);
auto bitr = branches.find(req.braname.value);
branches.modify(bitr, coopname, [&](auto &row) {
row.add_account_to_trusted(req.username);
});
// Оба документа подписаны обеими сторонами (доверенное лицо + председатель участка) —
// фиксируем их в реестре документов как принятые.
::Soviet::make_complete_document(_branch, coopname, req.username, "trustliab"_n, hash, countersigned);
::Soviet::make_complete_document(_branch, coopname, req.username, "trustauth"_n, hash, countersigned_authority);
trustreq_index trustreqs(_branch, coopname.value);
auto ritr = trustreqs.find(req.id);
trustreqs.erase(ritr);
}
@@ -1,23 +0,0 @@
/**
* @brief Отмена собрания инициатором до вывода на совет.
* Инициатор сворачивает собрание, пока решение не ушло в совет; запись и вопросы
* повестки стираются, причина передаётся аргументом (история — в журнале действий).
* @param coopname Наименование кооператива
* @param hash Якорь процесса
* @param reason Причина отмены
* @ingroup public_actions
* @ingroup public_branch_actions
* @note Авторизация требуется от аккаунта: @p coopname
*/
[[eosio::action]] void branch::canceldec(eosio::name coopname, eosio::checksum256 hash, std::string reason) {
check_auth_or_fail(_branch, coopname, coopname, "canceldec"_n);
auto dec = get_decision_or_fail(coopname, hash);
eosio::check(dec.status != "onapproval"_n, "Решение уже на рассмотрении совета — отмена недоступна");
decision_index decisions(_branch, coopname.value);
auto itr = decisions.find(dec.id);
erase_coodecquests(coopname, dec.id);
decisions.erase(itr);
}
@@ -1,56 +0,0 @@
/**
* @brief Закрытие собрания и утверждение протокола председателем.
* Председатель закрывает голосование и утверждает протокол одной своей подписью
* (секретарь не нужен — подсчёт ведёт контракт). Проверяется кворум и итог
* голосования. Для типа "free" — терминал: запись стирается (история — в журнале).
* Для "createbranch" — решение переходит в статус "approved" в ожидании исполнения
* действием exec (вывод вопроса на совет).
* @param coopname Наименование кооператива
* @param hash Якорь процесса
* @param protocol Подписанный председателем протокол решения
* @ingroup public_actions
* @ingroup public_branch_actions
* @note Авторизация требуется от аккаунта: @p coopname
*/
[[eosio::action]] void branch::closedec(eosio::name coopname, eosio::checksum256 hash, document2 protocol) {
check_auth_or_fail(_branch, coopname, coopname, "closedec"_n);
verify_document_or_fail(protocol);
auto dec = get_decision_or_fail(coopname, hash);
eosio::check(dec.status == "voting"_n, "Решение не находится в стадии голосования");
// Кворум: число поданных бюллетеней не меньше минимального
eosio::check(dec.signed_ballots >= MIN_DECISION_QUORUM, "Не набран минимальный кворум собрания");
// Закрыть может только по истечении окна голосования либо когда проголосовали все участники
auto now = current_time_point();
bool window_passed = now.sec_since_epoch() > dec.close_at.sec_since_epoch();
bool all_voted = dec.signed_ballots >= dec.participants.size();
eosio::check(window_passed || all_voted, "Голосование ещё идёт");
// Протокол собрания пайщиков завершает ПАКЕТ СОБРАНИЯ в реестре — его якорь = хэш
// предложения/повестки (proposal.hash), к нему же привязаны бюллетени (см. votedec).
// Пакет появляется в реестре сразу при закрытии собрания. Отправка в совет (exec) —
// ОТДЕЛЬНЫЙ пакет на хэше процесса (hash), чтобы документы собрания и документы совета
// не сваливались в одну кашу под общим якорем.
Soviet::make_complete_document(_branch, coopname, dec.initiator, get_valid_soviet_action("branchdec"_n), dec.proposal.hash, protocol);
if (dec.type == "free"_n) {
// Свободное решение зафиксировано протоколом и завершается (история — в журнале действий)
decision_index decisions(_branch, coopname.value);
auto itr = decisions.find(dec.id);
erase_coodecquests(coopname, dec.id);
decisions.erase(itr);
return;
}
// Автоматизируемое решение — переходит в ожидание исполнения (exec → совет)
decision_index decisions(_branch, coopname.value);
auto itr = decisions.find(dec.id);
decisions.modify(itr, coopname, [&](auto &d) {
d.status = "approved"_n;
d.protocol = protocol;
});
}
@@ -1,101 +0,0 @@
/**
* @brief Подтверждение решения советом — создание кооперативного участка.
* Callback совета: по утверждению учреждается кооперативный участок с реквизитами
* из решения (аккаунт участка, избранный председатель), после чего запись решения
* стирается (история — в журнале действий).
* @param coopname Наименование кооператива
* @param hash Якорь процесса
* @param authorization Документ решения совета
* @ingroup public_actions
* @ingroup public_branch_actions
* @note Авторизация требуется от аккаунта: @p soviet
*/
[[eosio::action]] void branch::confirmdec(eosio::name coopname, eosio::checksum256 hash, document2 authorization) {
require_auth(_soviet);
auto dec = get_decision_or_fail(coopname, hash);
eosio::check(dec.type == "createbranch"_n, "Решение не относится к созданию участка");
eosio::check(dec.status == "onapproval"_n, "Решение не находится на утверждении совета");
auto coop = get_cooperative_or_fail(coopname);
auto authorizer_account = get_account_or_fail(dec.chairman);
eosio::check(authorizer_account.type == "individual"_n, "Только физическое лицо может быть назначено председателем кооперативного участка");
// Создаём кооперативный участок с избранным председателем
branch_index branches(_branch, coopname.value);
// председатель может возглавлять только один кооперативный участок
auto branches_by_trustee = branches.get_index<"bytrustee"_n>();
eosio::check(branches_by_trustee.find(dec.chairman.value) == branches_by_trustee.end(),
"Избранный председатель уже является председателем другого кооперативного участка");
branches.emplace(coopname, [&](auto &row) {
row.braname = dec.braname;
row.trustee = dec.chairman;
});
action(
permission_level{_branch, "active"_n},
_registrator,
"createbranch"_n,
std::make_tuple(coopname, dec.braname))
.send();
// председатель привязывается к собственному участку и не выбирает его заявлением
action(
permission_level{_branch, "active"_n},
_soviet,
"setbranch"_n,
std::make_tuple(coopname, dec.chairman, dec.braname))
.send();
uint64_t branch_count = add_branch_count(coopname);
if (!coop.is_branched && branch_count >= 3) { // переход на режим кооперативных участков
action(
permission_level{_branch, "active"_n},
_registrator,
"enabranches"_n,
std::make_tuple(coopname))
.send();
}
// Участок учреждён. Договор о полной материальной ответственности председателя участка
// (подписан председателем участка при подаче заявления) уходит на встречную подпись
// председателю совета: единоличное одобрение без повторного голосования. Отклонить нельзя —
// решение об учреждении уже принято (см. branch::declliab).
::Soviet::create_approval(
_branch,
coopname,
dec.chairman,
dec.liability,
"branchliab"_n,
hash,
_branch,
"apprliab"_n,
"declliab"_n,
std::string(""));
// Доверенность председателю участка уходит на встречную подпись председателю совета
// отдельным одобрением рядом с договором. Якорь одобрения — хэш самого документа
// доверенности (hash процесса уже занят одобрением договора). Отклонить нельзя.
::Soviet::create_approval(
_branch,
coopname,
dec.chairman,
dec.authority,
"branchauth"_n,
dec.authority.hash,
_branch,
"apprauth"_n,
"declauth"_n,
std::string(""));
// Решение исполнено — стираем запись и вопросы повестки (история в журнале действий)
decision_index decisions(_branch, coopname.value);
auto itr = decisions.find(dec.id);
erase_coodecquests(coopname, dec.id);
decisions.erase(itr);
}
@@ -20,31 +20,18 @@
auto authorizer_account = get_account_or_fail(trustee);
eosio::check(authorizer_account.type == "individual"_n, "Только физическое лицо может быть назначено председателем кооперативного участка");
// председатель может возглавлять только один кооперативный участок
auto branches_by_trustee = branches.get_index<"bytrustee"_n>();
eosio::check(branches_by_trustee.find(trustee.value) == branches_by_trustee.end(),
"Пайщик уже является председателем другого кооперативного участка");
branches.emplace(coopname, [&](auto &row) {
row.braname = braname;
row.trustee = trustee;
});
action(
permission_level{ _branch, "active"_n},
_registrator,
"createbranch"_n,
std::make_tuple(coopname, braname)
).send();
// председатель привязывается к собственному участку и не выбирает его заявлением
action(
permission_level{ _branch, "active"_n},
_soviet,
"setbranch"_n,
std::make_tuple(coopname, trustee, braname)
).send();
uint64_t branch_count = add_branch_count(coopname);
if (!coop.is_branched && branch_count >= 3) { //регистрируем переход на кооперативные участки
@@ -1,77 +0,0 @@
/**
* @brief Объявление собрания пайщиков с повесткой (создание решения).
* Универсальный механизм: тип "free" — свободное решение (фиксируется протоколом),
* тип "createbranch" — автоматизируемое (после утверждения уходит в совет и создаёт КУ).
* @param coopname Наименование кооператива
* @param hash Якорь процесса (внешний идентификатор решения)
* @param type Тип решения: "free" | "createbranch"
* @param initiator Организатор собрания
* @param proposal Подписанное предложение/повестка
* @param braname Заранее сгенерированный аккаунт будущего КУ (для "createbranch", иначе пустое)
* @param agenda Вопросы повестки дня
* @ingroup public_actions
* @ingroup public_branch_actions
* @note Авторизация требуется от аккаунта: @p coopname
*/
[[eosio::action]] void branch::createdec(eosio::name coopname, eosio::checksum256 hash, eosio::name type, eosio::name initiator, document2 proposal, eosio::name braname, std::vector<decision_point> agenda) {
check_auth_or_fail(_branch, coopname, coopname, "createdec"_n);
verify_document_or_fail(proposal);
eosio::check(type == "free"_n || type == "createbranch"_n, "Недопустимый тип решения");
get_cooperative_or_fail(coopname);
get_participant_or_fail(coopname, initiator);
decision_index decisions(_branch, coopname.value);
auto idx = decisions.get_index<"byhash"_n>();
eosio::check(idx.find(hash) == idx.end(), "Решение с указанным идентификатором уже существует");
eosio::check(!agenda.empty(), "Повестка дня не может быть пустой");
if (type == "createbranch"_n) {
eosio::check(braname != ""_n, "Для создания кооперативного участка требуется аккаунт участка");
}
uint64_t decision_id = get_global_id_in_scope(_branch, coopname, "decisions"_n);
decisions.emplace(coopname, [&](auto &d) {
d.id = decision_id;
d.hash = hash;
d.coopname = coopname;
d.type = type;
d.initiator = initiator;
d.chairman = ""_n; // председатель выбирается организатором из участников при открытии голосования
d.status = "opened"_n;
d.proposal = proposal;
d.signed_ballots = 0;
d.braname = braname;
d.participants = {initiator}; // организатор сразу участник
d.created_at = current_time_point();
});
coodecquest_index questions(_branch, coopname.value);
uint64_t number = 0;
for (const auto &point : agenda) {
eosio::check(!point.title.empty(), "Вопрос должен содержать заголовок");
eosio::check(!point.decision.empty(), "Вопрос должен содержать проект решения");
number++;
eosio::check(number <= 10, "Не больше 10 вопросов на повестке собрания");
questions.emplace(coopname, [&](auto &q) {
q.id = get_global_id_in_scope(_branch, coopname, "decisionq"_n);
q.decision_id = decision_id;
q.number = number;
q.coopname = coopname;
q.title = point.title;
q.decision = point.decision;
q.context = point.context;
q.counter_votes_for = 0;
q.counter_votes_against = 0;
q.counter_votes_abstained = 0;
});
}
}
@@ -1,20 +0,0 @@
/**
* @brief Запрет отклонения доверенности председателя кооперативного участка.
* Callback отклонения совета: к этому моменту решение об учреждении кооперативного
* участка уже принято советом и участок создан, поэтому доверенность председателю
* участка отклонить нельзя — председателю совета остаётся только подписать её
* встречной подписью (см. branch::apprauth). Действие всегда завершается ошибкой.
* @param coopname Наименование кооператива
* @param username Председатель кооперативного участка — уполномоченный по доверенности
* @param approval_hash Якорь одобрения (хэш документа доверенности)
* @param reason Причина отклонения (не используется)
* @ingroup public_actions
* @ingroup public_branch_actions
* @note Авторизация требуется от аккаунта: @p soviet
*/
[[eosio::action]] void branch::declauth(eosio::name coopname, eosio::name username, eosio::checksum256 approval_hash, std::string reason) {
require_auth(_soviet);
eosio::check(false, "Доверенность председателя участка нельзя отклонить: решение совета об учреждении участка уже принято — требуется только встречная подпись председателя совета");
}
@@ -1,24 +0,0 @@
/**
* @brief Отклонение решения советом.
* Callback совета: при отказе совета запись решения и вопросы повестки стираются,
* причина передаётся аргументом (история — в журнале действий).
* @param coopname Наименование кооператива
* @param hash Якорь процесса
* @param reason Причина отклонения
* @ingroup public_actions
* @ingroup public_branch_actions
* @note Авторизация требуется от аккаунта: @p soviet
*/
[[eosio::action]] void branch::declinedec(eosio::name coopname, eosio::checksum256 hash, std::string reason) {
require_auth(_soviet);
decision_index decisions(_branch, coopname.value);
auto idx = decisions.get_index<"byhash"_n>();
auto itr = idx.find(hash);
if (itr != idx.end()) {
erase_coodecquests(coopname, itr->id);
idx.erase(itr);
}
}
@@ -1,21 +0,0 @@
/**
* @brief Запрет отклонения договора о материальной ответственности председателя участка.
* Callback отклонения совета: к этому моменту решение об учреждении кооперативного
* участка уже принято советом и участок создан, поэтому договор о материальной
* ответственности отклонить нельзя — председателю совета остаётся только подписать его
* встречной подписью (см. branch::apprliab). Действие всегда завершается ошибкой, тем
* самым гарантируя, что процесс не уйдёт в неопределённое состояние.
* @param coopname Наименование кооператива
* @param username Председатель кооперативного участка — сторона «Исполнитель»
* @param approval_hash Якорь процесса (хэш одобрения)
* @param reason Причина отклонения (не используется)
* @ingroup public_actions
* @ingroup public_branch_actions
* @note Авторизация требуется от аккаунта: @p soviet
*/
[[eosio::action]] void branch::declliab(eosio::name coopname, eosio::name username, eosio::checksum256 approval_hash, std::string reason) {
require_auth(_soviet);
eosio::check(false, "Договор о материальной ответственности председателя участка нельзя отклонить: решение совета об учреждении участка уже принято — требуется только встречная подпись председателя совета");
}
@@ -1,20 +0,0 @@
/**
* @brief Отклонение заявки доверенного лица председателем участка.
* Заявка стирается, причина передаётся аргументом (история — в журнале действий).
* @param coopname Наименование кооператива
* @param hash Внешний идентификатор заявки
* @param reason Причина отклонения
* @ingroup public_actions
* @ingroup public_branch_actions
* @note Авторизация требуется от аккаунта: @p coopname
*/
[[eosio::action]] void branch::decltrusted(eosio::name coopname, eosio::checksum256 hash, std::string reason) {
check_auth_or_fail(_branch, coopname, coopname, "decltrusted"_n);
auto req = get_trustreq_or_fail(coopname, hash);
trustreq_index trustreqs(_branch, coopname.value);
auto ritr = trustreqs.find(req.id);
trustreqs.erase(ritr);
}
@@ -1,22 +0,0 @@
/**
* @brief Удаление аккаунта из белого списка кооперативного участка.
* Удаляет аккаунт пайщика из белого списка приватного кооперативного участка.
* @param coopname Наименование кооператива
* @param braname Наименование кооперативного участка
* @param account Аккаунт пайщика для удаления
* @ingroup public_actions
* @ingroup public_branch_actions
* @note Авторизация требуется от аккаунта: @p coopname
*/
[[eosio::action]] void branch::delwhite(eosio::name coopname, eosio::name braname, eosio::name account) {
check_auth_or_fail(_branch, coopname, coopname, "delwhite"_n);
branch_index branches(_branch, coopname.value);
auto branch = branches.find(braname.value);
eosio::check(branch != branches.end(), "Кооперативный участок не найден");
branches.modify(branch, coopname, [&](auto &b) {
b.remove_account_from_whitelist(account);
});
}
@@ -20,34 +20,8 @@
auto authorizer_account = get_account_or_fail(trustee);
eosio::check(authorizer_account.type == "individual"_n, "Только физическое лицо может быть назначено председателем кооперативного участка");
eosio::name previous_trustee = branch->trustee;
if (previous_trustee != trustee) {
// председатель может возглавлять только один кооперативный участок
auto branches_by_trustee = branches.get_index<"bytrustee"_n>();
eosio::check(branches_by_trustee.find(trustee.value) == branches_by_trustee.end(),
"Пайщик уже является председателем другого кооперативного участка");
}
branches.modify(branch, coopname, [&](auto &b) {
b.trustee = trustee;
});
if (previous_trustee != trustee) {
// прежний председатель освобождается от привязки и выбирает участок заново заявлением
action(
permission_level{ _branch, "active"_n},
_soviet,
"setbranch"_n,
std::make_tuple(coopname, previous_trustee, ""_n)
).send();
// новый председатель привязывается к собственному участку
action(
permission_level{ _branch, "active"_n},
_soviet,
"setbranch"_n,
std::make_tuple(coopname, trustee, braname)
).send();
}
}
@@ -1,71 +0,0 @@
/**
* @brief Исполнение утверждённого решения о создании кооперативного участка.
* Председатель собрания формирует заявление в совет и выводит вопрос об учреждении
* кооперативного участка на рассмотрение совета. К повестке совета прилагаются
* протокол собрания и бюллетени (по тому же якорю процесса).
* @param coopname Наименование кооператива
* @param hash Якорь процесса
* @param petition Подписанное заявление председателя в совет
* @param liability Подписанный председателем участка договор о полной материальной ответственности
* @param authority Подписанная председателем участка доверенность председателю кооперативного участка
* @ingroup public_actions
* @ingroup public_branch_actions
* @note Авторизация требуется от аккаунта: @p coopname
*/
[[eosio::action]] void branch::exec(eosio::name coopname, eosio::checksum256 hash, document2 petition, document2 liability, document2 authority) {
check_auth_or_fail(_branch, coopname, coopname, "exec"_n);
verify_document_or_fail(petition);
verify_document_or_fail(liability);
verify_document_or_fail(authority);
auto dec = get_decision_or_fail(coopname, hash);
eosio::check(dec.type == "createbranch"_n, "Исполнение доступно только для решения о создании участка");
eosio::check(dec.status == "approved"_n, "Решение не утверждено председателем");
decision_index decisions(_branch, coopname.value);
auto itr = decisions.find(dec.id);
decisions.modify(itr, coopname, [&](auto &d) {
d.status = "onapproval"_n;
d.petition = petition;
d.liability = liability;
d.authority = authority;
});
::Soviet::create_agenda(
_branch,
coopname,
dec.chairman,
get_valid_soviet_action("branchdec"_n),
hash,
_branch,
"confirmdec"_n,
"declinedec"_n,
petition,
std::string(""));
// К пакету повестки совета линкуются договор о материальной ответственности и доверенность
// председателя участка — совет читает их вместе с заявлением до утверждения (по якорю процесса).
Action::send<newlink_interface>(
_soviet,
"newlink"_n,
_branch,
coopname,
dec.chairman,
get_valid_soviet_action("branchliab"_n),
hash,
liability
);
Action::send<newlink_interface>(
_soviet,
"newlink"_n,
_branch,
coopname,
dec.chairman,
get_valid_soviet_action("branchauth"_n),
hash,
authority
);
}
@@ -1,26 +0,0 @@
/**
* @brief Присоединение пайщика к собранию.
* Пайщик фиксируется в списке участников собрания.
* @param coopname Наименование кооператива
* @param hash Якорь процесса
* @param username Имя присоединяющегося пайщика
* @ingroup public_actions
* @ingroup public_branch_actions
* @note Авторизация требуется от аккаунта: @p coopname
*/
[[eosio::action]] void branch::joindec(eosio::name coopname, eosio::checksum256 hash, eosio::name username) {
check_auth_or_fail(_branch, coopname, coopname, "joindec"_n);
get_participant_or_fail(coopname, username);
auto dec = get_decision_or_fail(coopname, hash);
eosio::check(dec.status == "opened"_n, "Присоединиться можно только до начала голосования");
eosio::check(!dec.is_participant(username), "Пайщик уже присоединился к собранию");
decision_index decisions(_branch, coopname.value);
auto itr = decisions.find(dec.id);
decisions.modify(itr, coopname, [&](auto &d) {
d.participants.push_back(username);
});
}
@@ -1,42 +0,0 @@
/**
* @brief Подача заявки на приём доверенным лицом кооперативного участка.
* Пайщик прикладывает подписанные заявление и договор о полной материальной
* ответственности. Заявка ожидает одобрения председателя участка.
* @param coopname Наименование кооператива
* @param braname Кооперативный участок
* @param username Заявитель
* @param hash Внешний идентификатор заявки
* @param application Подписанный заявителем договор о полной материальной ответственности
* @param authority Подписанная заявителем доверенность доверенному лицу/оператору участка
* @ingroup public_actions
* @ingroup public_branch_actions
* @note Авторизация требуется от аккаунта: @p coopname
*/
[[eosio::action]] void branch::reqtrusted(eosio::name coopname, eosio::name braname, eosio::name username, eosio::checksum256 hash, document2 application, document2 authority) {
check_auth_or_fail(_branch, coopname, coopname, "reqtrusted"_n);
verify_document_or_fail(application);
verify_document_or_fail(authority);
get_participant_or_fail(coopname, username);
auto br = get_branch_or_fail(coopname, braname);
eosio::check(br.trustee != username, "Председатель участка не может быть доверенным лицом");
eosio::check(!br.is_account_in_trusted(username), "Пайщик уже является доверенным лицом участка");
eosio::check(br.trusted.size() < 3, "Достигнут предел доверенных лиц участка (не более трёх)");
trustreq_index trustreqs(_branch, coopname.value);
auto idx = trustreqs.get_index<"byhash"_n>();
eosio::check(idx.find(hash) == idx.end(), "Заявка с указанным идентификатором уже существует");
trustreqs.emplace(coopname, [&](auto &r) {
r.id = get_global_id_in_scope(_branch, coopname, "trustreqs"_n);
r.hash = hash;
r.coopname = coopname;
r.braname = braname;
r.username = username;
r.application = application;
r.authority = authority;
});
}
@@ -1,23 +0,0 @@
/**
* @brief Установка признака приватности кооперативного участка.
* Приватный кооперативный участок нельзя выбрать при вступлении или смене участка,
* если аккаунт пайщика не добавлен в белый список этого участка.
* @param coopname Наименование кооператива
* @param braname Наименование кооперативного участка
* @param is_private Признак приватности (true — приватный, false — публичный)
* @ingroup public_actions
* @ingroup public_branch_actions
* @note Авторизация требуется от аккаунта: @p coopname
*/
[[eosio::action]] void branch::setprivate(eosio::name coopname, eosio::name braname, bool is_private) {
check_auth_or_fail(_branch, coopname, coopname, "setprivate"_n);
branch_index branches(_branch, coopname.value);
auto branch = branches.find(braname.value);
eosio::check(branch != branches.end(), "Кооперативный участок не найден");
branches.modify(branch, coopname, [&](auto &b) {
b.set_private(is_private);
});
}
@@ -1,72 +0,0 @@
/**
* @brief Открытие голосования по решению.
* Организатор собрания (он же председатель собрания) открывает голосование,
* указывая избираемого председателя кооперативного участка (из числа
* присоединившихся участников) и — для решения "createbranch" —
* адрес привязки кооперативного участка, определённый собранием.
* Окно голосования отмеряется автоматически (DECISION_VOTING_WINDOW_SECONDS).
* @param coopname Наименование кооператива
* @param hash Якорь процесса
* @param chairman Избираемый председатель кооперативного участка (из участников)
* @param address Адрес привязки кооперативного участка (для "createbranch")
* @param agenda Итоговая повестка собрания: если непуста — заменяет предварительную
* (уточнённые на собрании формулировки и дополнительные вопросы); пустая — повестка без изменений
* @ingroup public_actions
* @ingroup public_branch_actions
* @note Авторизация требуется от аккаунта: @p coopname
*/
[[eosio::action]] void branch::startdec(eosio::name coopname, eosio::checksum256 hash, eosio::name chairman, std::string address, std::vector<decision_point> agenda) {
check_auth_or_fail(_branch, coopname, coopname, "startdec"_n);
auto dec = get_decision_or_fail(coopname, hash);
eosio::check(dec.status == "opened"_n, "Голосование уже начато или собрание закрыто");
eosio::check(dec.participants.size() >= MIN_DECISION_QUORUM,
"Для открытия голосования требуется не менее 3 участников собрания");
eosio::check(dec.is_participant(chairman), "Председатель участка должен быть участником собрания");
if (dec.type == "createbranch"_n) {
eosio::check(!address.empty(), "Для создания кооперативного участка требуется адрес привязки");
}
// Итоговая повестка собрания: непустой список заменяет предварительную
// (формулировки уточняются на собрании — наименование участка, председатель, новые вопросы)
coodecquest_index questions(_branch, coopname.value);
uint64_t number = 0;
if (!agenda.empty()) {
erase_coodecquests(coopname, dec.id);
}
for (const auto &point : agenda) {
eosio::check(!point.title.empty(), "Вопрос должен содержать заголовок");
eosio::check(!point.decision.empty(), "Вопрос должен содержать проект решения");
number++;
eosio::check(number <= 10, "Не больше 10 вопросов на повестке собрания");
questions.emplace(coopname, [&](auto &q) {
q.id = get_global_id_in_scope(_branch, coopname, "decisionq"_n);
q.decision_id = dec.id;
q.number = number;
q.coopname = coopname;
q.title = point.title;
q.decision = point.decision;
q.context = point.context;
q.counter_votes_for = 0;
q.counter_votes_against = 0;
q.counter_votes_abstained = 0;
});
}
auto now = current_time_point();
decision_index decisions(_branch, coopname.value);
auto itr = decisions.find(dec.id);
decisions.modify(itr, coopname, [&](auto &d) {
d.status = "voting"_n;
d.chairman = chairman;
d.address = address;
d.open_at = eosio::time_point_sec(now.sec_since_epoch());
d.close_at = eosio::time_point_sec(now.sec_since_epoch() + DECISION_VOTING_WINDOW_SECONDS);
});
}
@@ -1,95 +0,0 @@
/**
* @brief Голосование по вопросам повестки бюллетенем.
* Участник подаёт бюллетень-заявление с волеизъявлением (за/против/воздержался)
* по каждому вопросу повестки. Подсчёт ведёт контракт. Адаптация meet::vote.
* @param coopname Наименование кооператива
* @param hash Якорь процесса
* @param username Имя голосующего участника
* @param ballot Подписанный бюллетень
* @param votes Массив голосов по вопросам повестки
* @ingroup public_actions
* @ingroup public_branch_actions
* @note Авторизация требуется от аккаунта: @p coopname
*/
[[eosio::action]] void branch::votedec(eosio::name coopname, eosio::checksum256 hash, eosio::name username, document2 ballot, std::vector<decision_vote_point> votes) {
check_auth_or_fail(_branch, coopname, coopname, "votedec"_n);
verify_document_or_fail(ballot);
auto dec = get_decision_or_fail(coopname, hash);
eosio::check(dec.status == "voting"_n, "Голосование не открыто");
eosio::check(dec.is_participant(username), "Голосовать может только участник собрания");
auto now = current_time_point();
eosio::check(now.sec_since_epoch() >= dec.open_at.sec_since_epoch(), "Голосование ещё не началось");
eosio::check(now.sec_since_epoch() <= dec.close_at.sec_since_epoch(), "Голосование завершено");
// Собираем вопросы данного решения
coodecquest_index questions(_branch, coopname.value);
auto by_dec = questions.get_index<"bydecision"_n>();
std::vector<coodecquest> decision_questions;
for (auto itr = by_dec.lower_bound(dec.id); itr != by_dec.end() && itr->decision_id == dec.id; ++itr) {
decision_questions.push_back(*itr);
}
eosio::check(votes.size() == decision_questions.size(), "Бюллетень должен содержать голоса по всем вопросам");
// Бюллетень покрывает все вопросы без дублирования
std::set<uint64_t> question_ids;
for (const auto &q : decision_questions) {
question_ids.insert(q.id);
}
std::set<uint64_t> voted_ids;
for (const auto &v : votes) {
eosio::check(question_ids.find(v.question_id) != question_ids.end(), "В бюллетене указан неизвестный вопрос");
eosio::check(voted_ids.insert(v.question_id).second, "Дублирование голосов по одному вопросу недопустимо");
}
// Участник не голосовал ранее
for (const auto &q : decision_questions) {
auto already = [&](const std::vector<eosio::name> &voters) {
return std::find(voters.begin(), voters.end(), username) != voters.end();
};
eosio::check(!already(q.voters_for) && !already(q.voters_against) && !already(q.voters_abstained), "Ваш голос уже учтён");
}
// Учитываем голоса
for (const auto &v : votes) {
auto qitr = questions.find(v.question_id);
eosio::check(qitr != questions.end(), "Вопрос не найден");
questions.modify(qitr, coopname, [&](auto &q) {
if (v.vote == "for"_n) {
q.counter_votes_for++;
q.voters_for.push_back(username);
} else if (v.vote == "against"_n) {
q.counter_votes_against++;
q.voters_against.push_back(username);
} else if (v.vote == "abstained"_n) {
q.counter_votes_abstained++;
q.voters_abstained.push_back(username);
} else {
eosio::check(false, "Недопустимое значение голоса");
}
});
}
decision_index decisions(_branch, coopname.value);
auto ditr = decisions.find(dec.id);
decisions.modify(ditr, coopname, [&](auto &d) {
d.signed_ballots++;
});
// Бюллетень линкуется к ПАКЕТУ СОБРАНИЯ (якорь = хэш предложения proposal.hash),
// туда же закрывающим документом ляжет протокол (см. closedec). Не на хэш процесса,
// иначе бюллетени попадут в пакет заявления в совет.
Action::send<newlink_interface>(
_soviet,
"newlink"_n,
_branch,
coopname,
username,
get_valid_soviet_action("ballot"_n),
dec.proposal.hash,
ballot
);
}
+1 -5
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 = {
@@ -92,10 +92,6 @@ static const std::set<eosio::name> soviet_actions = {
"completegm"_n, //решение общего собрания пайщиков
"ballot"_n, //бюллетень участника общего собрания пайщиков
"gmnotify"_n, //уведомление участника общего собрания пайщиков
//BRANCH
"branchdec"_n, //решение собрания пайщиков об учреждении кооперативного участка
"branchliab"_n, //договор о полной материальной ответственности председателя участка (линк в пакет совета)
"branchauth"_n, //доверенность председателю кооперативного участка (линк в пакет совета)
//CAPITAL
"capitalinvst"_n, //заявление на инвестиции по договору УХД
"createresult"_n, //клайм прироста благороста из задания
@@ -48,8 +48,6 @@
#include "table_branch_branches.hpp"
#include "table_branch_branchstat.hpp"
#include "table_branch_decisions.hpp"
#include "table_branch_trustreqs.hpp"
// wallet / gateway / ledger / loan / marketplace
#include "table_wallet_deposits.hpp"
@@ -1,7 +1,6 @@
#pragma once
#include <algorithm>
#include <eosio/binary_extension.hpp>
#include <eosio/eosio.hpp>
#include <vector>
@@ -17,12 +16,6 @@ struct [[eosio::table, eosio::contract(BRANCH)]] coobranch {
eosio::name trustee;
std::vector<eosio::name> trusted;
// Поля приватности добавлены как binary_extension: у действующих кооперативов
// в таблице уже могут быть строки, и расширение в конце struct сохраняет
// обратную совместимость десериализации (старые записи читаются как публичные с пустым списком).
eosio::binary_extension<bool> is_private; // приватный участок: выбрать его могут только из белого списка
eosio::binary_extension<std::vector<eosio::name>> whitelist; // белый список аккаунтов, допущенных к выбору приватного участка
uint64_t primary_key() const { return braname.value; }
uint64_t by_trustee() const { return trustee.value; }
@@ -44,38 +37,6 @@ struct [[eosio::table, eosio::contract(BRANCH)]] coobranch {
}
return is_account_in_trusted(username);
}
// признак приватности с учётом отсутствующего расширения у старых записей
// (const-перегрузка value_or() без аргумента возвращает bool() == false, если значения нет)
bool is_branch_private() const { return is_private.value_or(); }
bool is_account_in_whitelist(const eosio::name &account) const {
if (!whitelist.has_value()) {
return false;
}
const auto &wl = whitelist.value();
return std::find(wl.begin(), wl.end(), account) != wl.end();
}
void set_private(bool value) { is_private.emplace(value); }
void add_account_to_whitelist(const eosio::name &account) {
if (!whitelist.has_value()) {
whitelist.emplace();
}
auto &wl = whitelist.value();
eosio::check(std::find(wl.begin(), wl.end(), account) == wl.end(),
"Аккаунт уже добавлен в белый список кооперативного участка");
wl.push_back(account);
}
void remove_account_from_whitelist(const eosio::name &account) {
eosio::check(whitelist.has_value(), "Белый список кооперативного участка пуст");
auto &wl = whitelist.value();
auto it = std::find(wl.begin(), wl.end(), account);
eosio::check(it != wl.end(), "Аккаунт не найден в белом списке кооперативного участка");
wl.erase(it);
}
};
typedef eosio::multi_index<
@@ -1,138 +0,0 @@
#pragma once
#include <algorithm>
#include <eosio/eosio.hpp>
#include <string>
#include <vector>
#include "../consts.hpp"
#include "document_core.hpp"
/**
* @file
* Универсальный механизм «собрание пайщиков → решение» на контракте BRANCH.
* Зеркало Совета: тип решения `free` (свободное — просто фиксируется протоколом)
* либо автоматизируемый (`createbranch` — после утверждения председателем уходит
* в совет действием exec и по решению совета создаёт кооперативный участок).
* Якорь записи — hash (byhash). Голос фиксируется бюллетенем-заявлением;
* протокол утверждает один председатель собрания (секретарь не нужен).
* Жизненный цикл — Chain-RAM: терминал = erase, история — в журнале действий парсера.
*/
/// Минимальный кворум учредительного собрания (число подписанных бюллетеней).
static constexpr uint64_t MIN_DECISION_QUORUM = 3;
/// Длительность окна голосования собрания пайщиков участка (секунд).
/// Голосование короткое — проходит прямо на собрании: организатор открывает
/// его кнопкой, дедлайн отмеряется автоматически.
static constexpr uint32_t DECISION_VOTING_WINDOW_SECONDS = 15 * 60;
/// Точка повестки дня собрания (вход действия createdec).
struct decision_point {
std::string title; ///< Текст вопроса
std::string decision; ///< Проект решения по вопросу
std::string context; ///< Контекст вопроса (может быть пустым)
};
/// Голос участника по одному вопросу повестки (вход действия votedec).
struct decision_vote_point {
uint64_t question_id; ///< Идентификатор вопроса
eosio::name vote; ///< Голос: "for"_n | "against"_n | "abstained"_n
};
/**
* @ingroup public_tables
* @ingroup public_branch_tables
* @par table: decisions
*/
struct [[eosio::table, eosio::contract(BRANCH)]] coodecision {
uint64_t id; ///< Идентификатор решения
eosio::checksum256 hash; ///< Якорь процесса (внешний идентификатор)
eosio::name coopname; ///< Имя кооператива
eosio::name type; ///< Тип решения: "free" | "createbranch"
eosio::name initiator; ///< Организатор собрания
eosio::name chairman; ///< Председатель собрания — выбирается организатором из участников при открытии голосования (для createbranch — он же кандидат в председатели КУ)
eosio::name status; ///< opened | voting | approved | onapproval
document2 proposal; ///< Подписанное предложение/повестка инициатора
document2 protocol; ///< Протокол решения (утверждает председатель)
document2 petition; ///< Заявление председателя в совет (для createbranch)
document2 liability; ///< Договор о полной индивидуальной материальной ответственности председателя КУ (подписан председателем участка, ждёт встречной подписи председателя совета)
document2 authority; ///< Доверенность председателю КУ (подписана председателем участка, ждёт встречной подписи председателя совета)
document2 authorization; ///< Решение совета (callback)
eosio::time_point_sec open_at; ///< Начало голосования
eosio::time_point_sec close_at; ///< Плановое закрытие голосования
uint64_t signed_ballots; ///< Число поданных бюллетеней
eosio::name braname; ///< Заранее сгенерированный аккаунт будущего КУ (createbranch)
std::string address; ///< Адрес привязки КУ (createbranch)
std::vector<eosio::name> participants; ///< Присоединившиеся участники собрания
eosio::time_point_sec created_at; ///< Дата создания
// Место и время проведения собрания НЕ публикуются в блокчейн —
// это приватные данные пайщиков, живут в БД платформы (composite db-слой).
uint64_t primary_key() const { return id; }
eosio::checksum256 by_hash() const { return hash; }
bool is_participant(const eosio::name &username) const {
return std::find(participants.begin(), participants.end(), username) != participants.end();
}
};
typedef eosio::multi_index<
"decisions"_n, coodecision,
eosio::indexed_by<"byhash"_n, eosio::const_mem_fun<coodecision, eosio::checksum256, &coodecision::by_hash>>>
decision_index;
/**
* @ingroup public_tables
* @ingroup public_branch_tables
* @par table: decisionq
*/
struct [[eosio::table, eosio::contract(BRANCH)]] coodecquest {
uint64_t id; ///< Идентификатор вопроса
uint64_t decision_id; ///< Идентификатор решения
uint64_t number; ///< Номер вопроса в повестке
eosio::name coopname; ///< Имя кооператива
std::string title; ///< Текст вопроса
std::string decision; ///< Проект решения по вопросу
std::string context; ///< Контекст вопроса
uint64_t counter_votes_for; ///< Счётчик голосов за
uint64_t counter_votes_against; ///< Счётчик голосов против
uint64_t counter_votes_abstained; ///< Счётчик воздержавшихся
std::vector<eosio::name> voters_for; ///< Проголосовавшие за
std::vector<eosio::name> voters_against; ///< Проголосовавшие против
std::vector<eosio::name> voters_abstained; ///< Воздержавшиеся
uint64_t primary_key() const { return id; }
uint64_t by_decision() const { return decision_id; }
};
typedef eosio::multi_index<
"decisionq"_n, coodecquest,
eosio::indexed_by<"bydecision"_n, eosio::const_mem_fun<coodecquest, uint64_t, &coodecquest::by_decision>>>
coodecquest_index;
/// Получить решение по якорю или упасть.
inline coodecision get_decision_or_fail(eosio::name coopname, eosio::checksum256 hash) {
decision_index decisions(_branch, coopname.value);
auto idx = decisions.get_index<"byhash"_n>();
auto itr = idx.find(hash);
eosio::check(itr != idx.end(), "Решение собрания не найдено");
return *itr;
}
/// Удалить все вопросы повестки данного решения (при erase решения).
inline void erase_coodecquests(eosio::name coopname, uint64_t decision_id) {
coodecquest_index questions(_branch, coopname.value);
auto by_dec = questions.get_index<"bydecision"_n>();
auto itr = by_dec.lower_bound(decision_id);
while (itr != by_dec.end() && itr->decision_id == decision_id) {
itr = by_dec.erase(itr);
}
}
@@ -1,45 +0,0 @@
#pragma once
#include <eosio/eosio.hpp>
#include "../consts.hpp"
#include "document_core.hpp"
/**
* @file
* Заявки на приём доверенным лицом кооперативного участка.
* Pending-таблица: заявитель подаёт подписанные заявление и договор о полной
* материальной ответственности; председатель участка одобряет встречной подписью —
* пайщик добавляется в trusted участка, заявка стирается (история — в журнале действий).
*/
/**
* @ingroup public_tables
* @ingroup public_branch_tables
* @par table: trustreqs
*/
struct [[eosio::table, eosio::contract(BRANCH)]] trustreq {
uint64_t id; ///< Идентификатор заявки
eosio::checksum256 hash; ///< Внешний идентификатор заявки
eosio::name coopname; ///< Имя кооператива
eosio::name braname; ///< Кооперативный участок
eosio::name username; ///< Заявитель
document2 application; ///< Договор о полной материальной ответственности доверенного лица (подписан заявителем)
document2 authority; ///< Доверенность доверенному лицу/оператору участка (подписана заявителем, ждёт встречной подписи председателя участка)
uint64_t primary_key() const { return id; }
eosio::checksum256 by_hash() const { return hash; }
};
typedef eosio::multi_index<
"trustreqs"_n, trustreq,
eosio::indexed_by<"byhash"_n, eosio::const_mem_fun<trustreq, eosio::checksum256, &trustreq::by_hash>>>
trustreq_index;
inline trustreq get_trustreq_or_fail(eosio::name coopname, eosio::checksum256 hash) {
trustreq_index trustreqs(_branch, coopname.value);
auto idx = trustreqs.get_index<"byhash"_n>();
auto itr = idx.find(hash);
eosio::check(itr != idx.end(), "Заявка доверенного не найдена");
return *itr;
}
@@ -64,7 +64,6 @@ transitions:
- Пайщик зарегистрирован в кооперативе (participants).
- Указанный участок существует (branches).
- Заявление подписано ЭЦП.
- Если участок приватный — пайщик должен быть в белом списке участка.
# ── Секция 4. Сценарий ──────────────────────────────────────────────────────
scenario:
@@ -82,7 +81,6 @@ scenario:
- Пайщик присутствует в participants кооператива.
- Участок (branch) с указанным braname существует.
- Заявление подписано ЭЦП.
- Приватный участок доступен к выбору только пайщикам из его белого списка.
post:
- participants.braname = выбранный участок.
- Документ заявления зафиксирован в реестрах документов.
@@ -29,7 +29,6 @@
#include "src/board/updateboard.cpp"
#include "src/branch/deletebranch.cpp"
#include "src/branch/setbranch.cpp"
#include "src/decision/authorize.cpp"
#include "src/decision/freedecision.cpp"
@@ -238,7 +238,6 @@ public:
//branch.cpp
[[eosio::action]] void deletebranch(eosio::name coopname, eosio::name braname);
[[eosio::action]] void setbranch(eosio::name coopname, eosio::name username, eosio::name braname);
};
@@ -1,33 +0,0 @@
/**
* @brief Системная привязка пайщика к кооперативному участку
* Устанавливает или сбрасывает кооперативный участок пайщика без заявления —
* вызывается контрактом участков при назначении или смене председателя участка:
* председатель всегда привязан к собственному участку и не выбирает его заявлением.
* Пустое наименование участка сбрасывает привязку (пайщик выберет участок заново).
* @param coopname Наименование кооператива
* @param username Наименование пайщика
* @param braname Наименование кооперативного участка (пустое — сброс привязки)
* @ingroup public_actions
* @ingroup public_soviet_actions
* @note Авторизация требуется от аккаунта: @p _branch
*/
void soviet::setbranch(eosio::name coopname, eosio::name username, eosio::name braname) {
require_auth(_branch);
participants_index participants(_soviet, coopname.value);
auto participant = participants.find(username.value);
// председатель участка может не быть пайщиком на момент учреждения — не роняем создание участка
if (participant == participants.end()) {
return;
}
participants.modify(participant, get_self(), [&](auto& row) {
if (braname == ""_n) {
row.braname.reset();
} else {
row.braname = braname;
}
});
}
@@ -15,22 +15,7 @@
require_auth(coopname);
verify_document_or_fail(document);
auto target_branch = get_branch_or_fail(coopname, braname);
// приватный кооперативный участок может выбрать только аккаунт из белого списка участка;
// белым списком управляет председатель совета (действия addwhite/delwhite контракта branch)
if (target_branch.is_branch_private()) {
eosio::check(target_branch.is_account_in_whitelist(username),
"Кооперативный участок приватный — выбрать его могут только пайщики из белого списка участка");
}
// председатель кооперативного участка привязан к собственному участку
// и не может сменить его, пока исполняет обязанности
branch_index branches(_branch, coopname.value);
auto branches_by_trustee = branches.get_index<"bytrustee"_n>();
eosio::check(branches_by_trustee.find(username.value) == branches_by_trustee.end(),
"Председатель кооперативного участка не может сменить участок, пока исполняет обязанности председателя");
get_branch_or_fail(coopname, braname);
participants_index participants(_soviet, coopname.value);
auto participant = participants.find(username.value);
eosio::check(participant != participants.end(), "Пайщик не найден");
@@ -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.16-1",
"version": "2026.7.23-2",
"private": true,
"type": "module",
"scripts": {
+1
View File
@@ -1,4 +1,5 @@
{
"cSpell.words": ["subcode"],
"editor.formatOnSave": false,
"typescript.tsdk": "node_modules/typescript/lib",
"editor.defaultFormatter": null,
@@ -0,0 +1,126 @@
import { createHash } from 'node:crypto';
import type { DataSource } from 'typeorm';
import mongoose from 'mongoose';
import config from '~/config/config';
type MigrationLogger = {
info: (message: string) => void;
error: (message: string) => void;
warn: (message: string) => void;
};
const CAPITAL_DOC_REGISTRY_ID = 994;
const TARGET_COOPNAME = 'voskhod';
const COLLECTION_NAME = 'doc_private_data';
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:
"взносы пользователей ЕОАП, его отдельных программных продуктов и приложений, переданных Обществу или создаваемых в рамках Общества, которые интегрируются в ЕОАП"
};
function stableStringify(value: unknown): string {
if (value === null || typeof value !== 'object') return JSON.stringify(value);
if (Array.isArray(value)) return `[${value.map((item) => stableStringify(item)).join(',')}]`;
const entries = Object.keys(value as Record<string, unknown>)
.sort()
.map((key) => {
const v = (value as Record<string, unknown>)[key];
return `${JSON.stringify(key)}:${stableStringify(v)}`;
});
return `{${entries.join(',')}}`;
}
function calculateSha256(value: unknown): string {
return createHash('sha256').update(Buffer.from(stableStringify(value), 'utf8')).digest('hex').toUpperCase();
}
export default {
name: 'Backfill capital_program_doc_data_hash for voskhod',
validUntil: new Date('2027-05-01T00:00:00Z'),
async up({ dataSource, logger }: { dataSource: DataSource; logger: MigrationLogger }): Promise<boolean> {
if (config.coopname !== TARGET_COOPNAME) {
logger.info(`COOPNAME=${config.coopname}: миграция предназначена только для ${TARGET_COOPNAME}, пропускаю.`);
return true;
}
const hash = calculateSha256(capitalProgramPrivateData);
let mongo: mongoose.Connection | null = null;
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(
{ hash },
{
$setOnInsert: {
hash,
registry_id: CAPITAL_DOC_REGISTRY_ID,
payload: capitalProgramPrivateData,
_created_at: new Date(),
},
},
{ upsert: true }
);
const result = await dataSource.query(
`UPDATE extensions
SET config = jsonb_set(COALESCE(config, '{}'::jsonb), '{capital_program_doc_data_hash}', to_jsonb($1::text), true),
updated_at = NOW()
WHERE name = 'capital'`,
[hash]
);
logger.info(`capital_program_doc_data_hash=${hash} сохранён для ${TARGET_COOPNAME}; extensions updated=${result?.[1] ?? 'unknown'}.`);
return true;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error(`Ошибка миграции capital_program_doc_data_hash для ${TARGET_COOPNAME}: ${message}`);
return false;
} finally {
await mongo?.close();
}
},
async down({ dataSource, logger }: { dataSource: DataSource; logger: MigrationLogger }): Promise<boolean> {
if (config.coopname !== TARGET_COOPNAME) {
return true;
}
await dataSource.query(
`UPDATE extensions
SET config = COALESCE(config, '{}'::jsonb) - 'capital_program_doc_data_hash',
updated_at = NOW()
WHERE name = 'capital'`
);
logger.warn('capital_program_doc_data_hash удалён из config capital; запись doc_private_data оставлена как идемпотентный audit trail.');
return true;
},
};
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "create-nodejs-express-app",
"version": "2026.6.16-1",
"version": "2026.7.23-2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "create-nodejs-express-app",
"version": "2026.6.16-1",
"version": "2026.7.23-2",
"license": "MIT",
"dependencies": {
"@a2seven/yoo-checkout": "^1.1.4",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@coopenomics/controller",
"version": "2026.6.16-1",
"version": "2026.7.23-2",
"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,
@@ -22,7 +22,6 @@ import { randomUUID } from 'crypto';
import type { Cooperative } from 'cooptypes';
import { ORGANIZATION_REPOSITORY, OrganizationRepository } from '~/domain/common/repositories/organization.repository';
import { INDIVIDUAL_REPOSITORY, IndividualRepository } from '~/domain/common/repositories/individual.repository';
import type { PassportDataDomainInterface } from '~/domain/common/interfaces/passport-data-domain.interface';
import { ENTREPRENEUR_REPOSITORY, EntrepreneurRepository } from '~/domain/common/repositories/entrepreneur.repository';
import {
SEARCH_PRIVATE_ACCOUNTS_REPOSITORY,
@@ -183,35 +182,6 @@ export class AccountInteractor {
return result;
}
/**
* Self-service: пайщик сохраняет СВОИ паспортные данные в реестр.
*
* Принимается только если паспорт ещё не был установлен ранее существующие
* данные не перезатираются (их изменение отдельный процесс, не здесь).
* Нужно для ролей, которым паспорт обязателен (председатель участка, доверенное
* лицо) в кооперативах, где паспорт при регистрации не собирается.
*/
async saveOwnPassport(username: string, passport: PassportDataDomainInterface): Promise<AccountDomainEntity> {
const existing = await this.individualRepository.findByUsername(username);
if (!existing) {
throw new HttpApiError(httpStatus.BAD_REQUEST, 'Паспортные данные можно сохранить только для физического лица');
}
if (existing.passport) {
throw new HttpApiError(
httpStatus.BAD_REQUEST,
'Паспортные данные уже установлены ранее — их изменение производится отдельным процессом'
);
}
await this.individualRepository.create({ ...existing, passport, username });
const account = await this.getAccount(username);
this.eventsService.emit('account::updated', { username, account });
return new AccountDomainEntity(account);
}
/**
* Удаление аккаунта из системы учёта провайдера (off-chain).
*
@@ -12,7 +12,6 @@ import type { PaginationResultDomainInterface } from '~/domain/common/interfaces
import { RegisterAccountInputDTO } from '../dto/register-account-input.dto';
import { RegisteredAccountDTO } from '../dto/registered-account.dto';
import { UpdateAccountInputDTO } from '../dto/update-account-input.dto';
import { PassportInputDTO } from '../dto/passport-input.dto';
import { DeleteAccountInputDTO } from '../dto/delete-account-input.dto';
import { SearchPrivateAccountsInputDTO } from '../dto/search-private-accounts-input.dto';
import { PrivateAccountSearchResultDTO } from '../dto/search-private-accounts-result.dto';
@@ -112,17 +111,4 @@ export class AccountResolver {
): Promise<AccountDTO> {
return await this.accountService.updateAccount(data);
}
@Mutation(() => AccountDTO, {
name: 'saveMyPassport',
description:
'Сохранить собственные паспортные данные в реестре пайщиков. Применяется, когда паспорт ранее не был указан (например, при подписании договора материальной ответственности председателем кооперативного участка или доверенным лицом). Если паспортные данные уже установлены — они не перезаписываются.',
})
@UseGuards(GqlJwtAuthGuard)
async saveMyPassport(
@Args('passport', { type: () => PassportInputDTO }) passport: PassportInputDTO,
@CurrentUser() currentUser: MonoAccountDomainInterface
): Promise<AccountDTO> {
return await this.accountService.saveOwnPassport(currentUser.username, passport);
}
}
@@ -8,7 +8,6 @@ import type { RegisterAccountInputDTO } from '../dto/register-account-input.dto'
import { RegisteredAccountDTO } from '../dto/registered-account.dto';
import type { DeleteAccountInputDTO } from '../dto/delete-account-input.dto';
import type { UpdateAccountInputDTO } from '../dto/update-account-input.dto';
import type { PassportInputDTO } from '../dto/passport-input.dto';
import type { SearchPrivateAccountsInputDTO } from '../dto/search-private-accounts-input.dto';
import { PrivateAccountSearchResultDTO } from '../dto/search-private-accounts-result.dto';
@@ -21,11 +20,6 @@ export class AccountService {
return new AccountDTO(result);
}
public async saveOwnPassport(username: string, passport: PassportInputDTO): Promise<AccountDTO> {
const result = await this.accountInteractor.saveOwnPassport(username, passport);
return new AccountDTO(result);
}
public async deleteAccount(data: DeleteAccountInputDTO): Promise<void> {
await this.accountInteractor.deleteAccount(data.username_for_delete);
}
@@ -1,14 +0,0 @@
import { Field, InputType } from '@nestjs/graphql';
import type { AddBranchWhitelistDomainInterface } from '~/domain/branch/interfaces/add-branch-whitelist-domain-input.interface';
@InputType('AddBranchWhitelistInput')
export class AddBranchWhitelistGraphQLInput implements AddBranchWhitelistDomainInterface {
@Field(() => String, { description: 'Имя аккаунта кооператива' })
coopname!: string;
@Field(() => String, { description: 'Имя аккаунта кооперативного участка' })
braname!: string;
@Field(() => String, { description: 'Имя аккаунта пайщика, добавляемого в белый список участка' })
account!: string;
}
@@ -1,11 +1,9 @@
import { ObjectType, Field, Int } from '@nestjs/graphql';
import { ObjectType, Field } from '@nestjs/graphql';
import { RepresentedByDTO } from '~/application/common/dto/represented-by.dto';
import type { BranchDomainInterface } from '~/domain/branch/interfaces/branch-domain.interface';
import type { BranchDomainEntity } from '~/domain/branch/entities/branch-domain.entity';
import { OrganizationDetailsDTO } from '~/application/common/dto/organization-details.dto';
import { IndividualDTO } from '~/application/common/dto/individual.dto';
import { IndividualCertificateDTO } from '~/application/common/dto/individual-certificate.dto';
import { AccountType } from '~/application/account/enum/account-type.enum';
import { IsArray, IsJSON, IsString } from 'class-validator';
import { BankPaymentMethodDTO } from '~/application/payment-method/dto/bank-payment-method.dto';
import { AuthRoles } from '~/application/auth/decorators/auth.decorator';
@@ -33,14 +31,6 @@ export class BranchDTO implements BranchDomainInterface {
@IsArray()
public readonly trusted: IndividualDTO[];
// публичная часть (сертификаты — только ФИО и имя аккаунта) доступна любому
// пайщику; полные персональные данные выше остаются под ограничением ролей
@Field(() => IndividualCertificateDTO, { description: 'Сертификат председателя кооперативного участка (ФИО)' })
public readonly trustee_certificate: IndividualCertificateDTO;
@Field(() => [IndividualCertificateDTO], { description: 'Сертификаты доверенных лиц участка (ФИО)' })
public readonly trusted_certificates: IndividualCertificateDTO[];
@Field(() => String, { description: 'Тип организации' })
@IsString()
public readonly type: string;
@@ -89,33 +79,11 @@ export class BranchDTO implements BranchDomainInterface {
@IsJSON()
public readonly details: OrganizationDetailsDTO;
@Field(() => Int, { description: 'Количество пайщиков, состоящих в кооперативном участке' })
public readonly participants_count: number;
@Field(() => Boolean, {
description: 'Приватный кооперативный участок: выбрать его при вступлении или смене могут только пайщики из белого списка',
})
public readonly is_private: boolean;
@Field(() => Boolean, {
description: 'Доступен ли участок текущему пайщику для выбора (публичный участок либо пайщик в белом списке)',
})
public readonly is_available: boolean;
// список ФИО пайщиков белого списка нужен председателю для управления приватным участком
@Field(() => [IndividualCertificateDTO], { description: 'Пайщики в белом списке приватного участка (ФИО)' })
@AuthRoles(['chairman', 'member'])
public readonly whitelist_certificates: IndividualCertificateDTO[];
constructor(entity: BranchDomainEntity) {
this.coopname = entity.coopname;
this.braname = entity.braname;
this.trustee = new IndividualDTO(entity.trustee);
this.trusted = entity.trusted.map((trustedEntity) => new IndividualDTO(trustedEntity));
this.trustee_certificate = new IndividualCertificateDTO({ ...entity.trustee, type: AccountType.individual });
this.trusted_certificates = entity.trusted.map(
(trustedEntity) => new IndividualCertificateDTO({ ...trustedEntity, type: AccountType.individual }),
);
this.type = entity.type;
this.short_name = entity.short_name;
this.full_name = entity.full_name;
@@ -128,11 +96,5 @@ export class BranchDTO implements BranchDomainInterface {
this.email = entity.email;
this.details = new OrganizationDetailsDTO(entity.details);
this.bank_account = new BankPaymentMethodDTO(entity.bank_account);
this.participants_count = entity.participants_count;
this.is_private = entity.is_private;
this.is_available = entity.is_available;
this.whitelist_certificates = entity.whitelist_members.map(
(member) => new IndividualCertificateDTO({ ...member, type: AccountType.individual }),
);
}
}
@@ -1,14 +0,0 @@
import { Field, InputType } from '@nestjs/graphql';
import type { DeleteBranchWhitelistDomainInterface } from '~/domain/branch/interfaces/delete-branch-whitelist-domain-input.interface';
@InputType('DeleteBranchWhitelistInput')
export class DeleteBranchWhitelistGraphQLInput implements DeleteBranchWhitelistDomainInterface {
@Field(() => String, { description: 'Имя аккаунта кооператива' })
coopname!: string;
@Field(() => String, { description: 'Имя аккаунта кооперативного участка' })
braname!: string;
@Field(() => String, { description: 'Имя аккаунта пайщика, удаляемого из белого списка участка' })
account!: string;
}
@@ -1,16 +0,0 @@
import { Field, InputType } from '@nestjs/graphql';
import type { SetBranchPrivateDomainInterface } from '~/domain/branch/interfaces/set-branch-private-domain-input.interface';
@InputType('SetBranchPrivateInput')
export class SetBranchPrivateGraphQLInput implements SetBranchPrivateDomainInterface {
@Field(() => String, { description: 'Имя аккаунта кооператива' })
coopname!: string;
@Field(() => String, { description: 'Имя аккаунта кооперативного участка' })
braname!: string;
@Field(() => Boolean, {
description: 'Признак приватности участка: при включении выбрать участок смогут только пайщики из белого списка',
})
is_private!: boolean;
}
@@ -11,12 +11,7 @@ import { EditBranchGraphQLInput } from '../dto/edit-branch-input.dto';
import { DeleteBranchGraphQLInput } from '../dto/delete-branch-input.dto';
import { AddTrustedAccountGraphQLInput } from '../dto/add-trusted-account-input.dto';
import { DeleteTrustedAccountGraphQLInput } from '../dto/delete-trusted-account-input.dto';
import { SetBranchPrivateGraphQLInput } from '../dto/set-branch-private-input.dto';
import { AddBranchWhitelistGraphQLInput } from '../dto/add-branch-whitelist-input.dto';
import { DeleteBranchWhitelistGraphQLInput } from '../dto/delete-branch-whitelist-input.dto';
import { SelectBranchInputDTO } from '../dto/select-branch-input.dto';
import { CurrentUser } from '~/application/auth/decorators/current-user.decorator';
import type { MonoAccountDomainInterface } from '~/domain/account/interfaces/mono-account-domain.interface';
import { SelectBranchGenerateDocumentInputDTO } from '../../document/documents-dto/select-branch-document.dto';
import { GenerateDocumentOptionsInputDTO } from '~/application/document/dto/generate-document-options-input.dto';
import { GeneratedDocumentDTO } from '~/application/document/dto/generated-document.dto';
@@ -31,11 +26,9 @@ export class BranchResolver {
})
@UseGuards(GqlJwtAuthGuard, RolesGuard)
async getBranches(
@Args('data', { type: () => GetBranchesGraphQLInput }) data: GetBranchesGraphQLInput,
@CurrentUser() currentUser: MonoAccountDomainInterface
@Args('data', { type: () => GetBranchesGraphQLInput }) data: GetBranchesGraphQLInput
): Promise<BranchDTO[]> {
// имя текущего пайщика нужно, чтобы вычислить доступность приватных участков (is_available)
return this.branchService.getBranches(data, currentUser?.username);
return this.branchService.getBranches(data);
}
@Mutation(() => BranchDTO, { name: 'createBranch', description: 'Создать кооперативный участок' })
@@ -87,42 +80,6 @@ export class BranchResolver {
return this.branchService.deleteTrustedAccount(data);
}
@Mutation(() => BranchDTO, {
name: 'setBranchPrivate',
description: 'Установить приватность кооперативного участка (выбор только из белого списка)',
})
@UseGuards(GqlJwtAuthGuard, RolesGuard)
@AuthRoles(['chairman'])
async setBranchPrivate(
@Args('data', { type: () => SetBranchPrivateGraphQLInput }) data: SetBranchPrivateGraphQLInput
): Promise<BranchDTO> {
return this.branchService.setBranchPrivate(data);
}
@Mutation(() => BranchDTO, {
name: 'addBranchWhitelist',
description: 'Добавить пайщика в белый список приватного кооперативного участка',
})
@UseGuards(GqlJwtAuthGuard, RolesGuard)
@AuthRoles(['chairman'])
async addBranchWhitelist(
@Args('data', { type: () => AddBranchWhitelistGraphQLInput }) data: AddBranchWhitelistGraphQLInput
): Promise<BranchDTO> {
return this.branchService.addBranchWhitelist(data);
}
@Mutation(() => BranchDTO, {
name: 'deleteBranchWhitelist',
description: 'Удалить пайщика из белого списка приватного кооперативного участка',
})
@UseGuards(GqlJwtAuthGuard, RolesGuard)
@AuthRoles(['chairman'])
async deleteBranchWhitelist(
@Args('data', { type: () => DeleteBranchWhitelistGraphQLInput }) data: DeleteBranchWhitelistGraphQLInput
): Promise<BranchDTO> {
return this.branchService.deleteBranchWhitelist(data);
}
@Mutation(() => Boolean, { name: 'selectBranch', description: 'Выбрать кооперативный участок' })
@UseGuards(GqlJwtAuthGuard, RolesGuard)
@AuthRoles(['chairman', 'member', 'user'])
@@ -7,9 +7,6 @@ import type { CreateBranchGraphQLInput } from '../dto/create-branch-input.dto';
import type { DeleteBranchGraphQLInput } from '../dto/delete-branch-input.dto';
import type { AddTrustedAccountGraphQLInput } from '../dto/add-trusted-account-input.dto';
import type { DeleteTrustedAccountGraphQLInput } from '../dto/delete-trusted-account-input.dto';
import type { SetBranchPrivateGraphQLInput } from '../dto/set-branch-private-input.dto';
import type { AddBranchWhitelistGraphQLInput } from '../dto/add-branch-whitelist-input.dto';
import type { DeleteBranchWhitelistGraphQLInput } from '../dto/delete-branch-whitelist-input.dto';
import type { SelectBranchInputDTO } from '../dto/select-branch-input.dto';
import type { GenerateDocumentOptionsInputDTO } from '~/application/document/dto/generate-document-options-input.dto';
import type { SelectBranchGenerateDocumentInputDTO } from '../../document/documents-dto/select-branch-document.dto';
@@ -19,8 +16,8 @@ import type { GeneratedDocumentDTO } from '~/application/document/dto/generated-
export class BranchService {
constructor(private readonly branchInteractor: BranchInteractor) {}
public async getBranches(data: GetBranchesGraphQLInput, currentUsername?: string): Promise<BranchDTO[]> {
const branches = await this.branchInteractor.getBranches(data, currentUsername);
public async getBranches(data: GetBranchesGraphQLInput): Promise<BranchDTO[]> {
const branches = await this.branchInteractor.getBranches(data);
const result: BranchDTO[] = [];
@@ -59,21 +56,6 @@ export class BranchService {
return new BranchDTO(branch);
}
public async setBranchPrivate(data: SetBranchPrivateGraphQLInput): Promise<BranchDTO> {
const branch = await this.branchInteractor.setBranchPrivate(data);
return new BranchDTO(branch);
}
public async addBranchWhitelist(data: AddBranchWhitelistGraphQLInput): Promise<BranchDTO> {
const branch = await this.branchInteractor.addBranchWhitelist(data);
return new BranchDTO(branch);
}
public async deleteBranchWhitelist(data: DeleteBranchWhitelistGraphQLInput): Promise<BranchDTO> {
const branch = await this.branchInteractor.deleteBranchWhitelist(data);
return new BranchDTO(branch);
}
public async selectBranch(data: SelectBranchInputDTO): Promise<boolean> {
const selected = await this.branchInteractor.selectBranch(data);
return selected;
@@ -2,7 +2,7 @@ import { BRANCH_BLOCKCHAIN_PORT, BranchBlockchainPort } from '~/domain/branch/in
import type { GetBranchesDomainInput } from '~/domain/branch/interfaces/get-branches-domain-input.interface';
import { BranchDomainEntity } from '~/domain/branch/entities/branch-domain.entity';
import { ORGANIZATION_REPOSITORY, OrganizationRepository } from '~/domain/common/repositories/organization.repository';
import { BadRequestException, Inject, Injectable, Logger } from '@nestjs/common';
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import type { CreateBranchDomainInput } from '~/domain/branch/interfaces/create-branch-domain-input.interface';
import { HttpApiError } from '~/utils/httpApiError';
import httpStatus from 'http-status';
@@ -10,9 +10,6 @@ import type { EditBranchDomainInput } from '~/domain/branch/interfaces/edit-bran
import type { DeleteBranchDomainInput } from '~/domain/branch/interfaces/delete-branch-domain-input';
import type { AddTrustedAccountDomainInterface } from '~/domain/branch/interfaces/add-trusted-account-domain-input.interface';
import type { DeleteTrustedAccountDomainInterface } from '~/domain/branch/interfaces/delete-trusted-account-domain-input.interface';
import type { SetBranchPrivateDomainInterface } from '~/domain/branch/interfaces/set-branch-private-domain-input.interface';
import type { AddBranchWhitelistDomainInterface } from '~/domain/branch/interfaces/add-branch-whitelist-domain-input.interface';
import type { DeleteBranchWhitelistDomainInterface } from '~/domain/branch/interfaces/delete-branch-whitelist-domain-input.interface';
import { INDIVIDUAL_REPOSITORY, IndividualRepository } from '~/domain/common/repositories/individual.repository';
import { IndividualDomainEntity } from '~/domain/branch/entities/individual-domain.entity';
import { OrganizationDomainEntity } from '~/domain/branch/entities/organization-domain.entity';
@@ -29,8 +26,6 @@ import { DocumentDomainEntity } from '~/domain/document/entity/document-domain.e
@Injectable()
export class BranchInteractor {
private readonly logger = new Logger(BranchInteractor.name);
constructor(
@Inject(PAYMENT_METHOD_REPOSITORY) private readonly paymentMethodRepository: PaymentMethodRepository,
@Inject(ORGANIZATION_REPOSITORY) private readonly organizationRepository: OrganizationRepository,
@@ -40,26 +35,7 @@ export class BranchInteractor {
private readonly documentInteractor: DocumentInteractor
) {}
// Количество пайщиков на каждом участке: участники совета (status accepted),
// у которых проставлен braname. Председатель и доверенные участка — тоже пайщики
// и входят в это число. Возвращаем карту braname → количество за одно чтение таблицы.
private async countParticipantsByBranch(coopname: string): Promise<Map<string, number>> {
const participants = await this.branchBlockchainPort.getParticipants(coopname);
const counts = new Map<string, number>();
for (const participant of participants) {
if (participant.status === 'accepted' && participant.braname) {
counts.set(participant.braname, (counts.get(participant.braname) ?? 0) + 1);
}
}
return counts;
}
async getBranch(
coopname: string,
braname: string,
participantsCount?: number,
currentUsername?: string
): Promise<BranchDomainEntity> {
async getBranch(coopname: string, braname: string): Promise<BranchDomainEntity> {
const branch = await this.branchBlockchainPort.getBranch(coopname, braname);
if (!branch) {
@@ -75,19 +51,6 @@ export class BranchInteractor {
trustedData.push(new IndividualDomainEntity(tr));
}
// белый список приватного участка — резолвим ФИО для управления председателем
// (поля is_private/whitelist приходят из binary_extension контракта и могут отсутствовать у старых записей)
const whitelist = branch.whitelist ?? [];
const whitelistMembers: IndividualDomainEntity[] = [];
for (const account of whitelist) {
const member = await this.individualRepository.findByUsername(account);
if (member) whitelistMembers.push(new IndividualDomainEntity(member));
}
// доступность участка для выбора текущим пайщиком: публичный либо пайщик в белом списке
const isPrivate = branch.is_private ?? false;
const isAvailable = !isPrivate || (!!currentUsername && whitelist.includes(currentUsername));
const bankAccount = new BankPaymentMethodDTO(
await this.paymentMethodRepository.get({
username: braname,
@@ -96,50 +59,19 @@ export class BranchInteractor {
})
);
// при одиночном запросе считаем число пайщиков сами; в списке оно приходит готовым
const count = participantsCount ?? (await this.countParticipantsByBranch(coopname)).get(braname) ?? 0;
return new BranchDomainEntity(
coopname,
branch,
databaseData,
trusteeData,
trustedData,
bankAccount,
count,
whitelistMembers,
isAvailable
);
return new BranchDomainEntity(coopname, branch, databaseData, trusteeData, trustedData, bankAccount);
}
async getBranches(data: GetBranchesDomainInput, currentUsername?: string): Promise<BranchDomainEntity[]> {
async getBranches(data: GetBranchesDomainInput): Promise<BranchDomainEntity[]> {
const branches = await this.branchBlockchainPort.getBranches(data.coopname);
// Фильтрация до сборки
const filteredBranches = data.braname ? branches.filter((branch) => branch.braname === data.braname) : branches;
// одно чтение таблицы участников на весь список, далее раздаём по braname
const participantCounts = await this.countParticipantsByBranch(data.coopname);
const result: BranchDomainEntity[] = [];
for (const branch of filteredBranches) {
try {
const branchEntity = await this.getBranch(
data.coopname,
branch.braname,
participantCounts.get(branch.braname) ?? 0,
currentUsername
);
result.push(branchEntity);
} catch (error) {
// Карточка участка (организация/председатель/реквизиты) после одобрения советом
// создаётся обработчиком события confirmdec асинхронно. Пока проекция не догнала
// блокчейн, getBranch бросает «Организация не найдена». Пропускаем не-готовый
// участок, чтобы один такой не ронял весь список — он появится на следующей загрузке.
this.logger.warn(
`getBranches: участок ${branch.braname} ещё не материализован — пропущен (${(error as Error).message})`
);
}
const branchEntity = await this.getBranch(data.coopname, branch.braname);
result.push(branchEntity);
}
return result;
@@ -308,54 +240,6 @@ export class BranchInteractor {
return await this.getBranch(data.coopname, data.braname);
}
async setBranchPrivate(data: SetBranchPrivateDomainInterface): Promise<BranchDomainEntity> {
const existingBranch = await this.branchBlockchainPort.getBranch(data.coopname, data.braname);
if (!existingBranch) {
throw new HttpApiError(httpStatus.BAD_REQUEST, 'Кооперативный участок не найден');
}
await this.branchBlockchainPort.setBranchPrivate({
coopname: data.coopname,
braname: data.braname,
is_private: data.is_private,
});
return await this.getBranch(data.coopname, data.braname);
}
async addBranchWhitelist(data: AddBranchWhitelistDomainInterface): Promise<BranchDomainEntity> {
const existingBranch = await this.branchBlockchainPort.getBranch(data.coopname, data.braname);
if (!existingBranch) {
throw new HttpApiError(httpStatus.BAD_REQUEST, 'Кооперативный участок не найден');
}
await this.branchBlockchainPort.addBranchWhitelist({
coopname: data.coopname,
braname: data.braname,
account: data.account,
});
return await this.getBranch(data.coopname, data.braname);
}
async deleteBranchWhitelist(data: DeleteBranchWhitelistDomainInterface): Promise<BranchDomainEntity> {
const existingBranch = await this.branchBlockchainPort.getBranch(data.coopname, data.braname);
if (!existingBranch) {
throw new HttpApiError(httpStatus.BAD_REQUEST, 'Кооперативный участок не найден');
}
await this.branchBlockchainPort.deleteBranchWhitelist({
coopname: data.coopname,
braname: data.braname,
account: data.account,
});
return await this.getBranch(data.coopname, data.braname);
}
async selectBranch(data: SelectBranchInputDomainInterface): Promise<boolean> {
// TODO move it to separate document domain service for validate
const document = await this.documentRepository.findByHash(data.document.doc_hash);
@@ -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,
});
@@ -39,10 +39,7 @@ export class SettingsDTO implements SettingsDomainInterface {
@Field(() => Date, { description: 'Дата последнего обновления' })
updated_at!: Date;
@Field(() => Boolean, { description: 'Открыта ли регистрация новых пайщиков' })
is_registration_open!: boolean;
constructor(data: SettingsDomainInterface, isRegistrationOpen = false) {
constructor(data: SettingsDomainInterface) {
this.coopname = data.coopname;
this.authorized_default_workspace = data.authorized_default_workspace;
this.authorized_default_route = data.authorized_default_route;
@@ -51,7 +48,6 @@ export class SettingsDTO implements SettingsDomainInterface {
this.provider_name = data.provider_name;
this.created_at = data.created_at;
this.updated_at = data.updated_at;
this.is_registration_open = isRegistrationOpen;
}
}
@@ -13,7 +13,6 @@ import { SymbolsDTO } from './symbols.dto';
import { SettingsDTO } from './settings.dto';
import { BoardMemberDTO } from './board-member.dto';
import { SystemFeaturesDTO } from './system-features.dto';
import { isRegistrationOpen } from '~/domain/system/utils/is-registration-open.util';
@ObjectType('SystemInfo')
export class SystemInfoDTO {
@@ -80,7 +79,7 @@ export class SystemInfoDTO {
this.blockchain_info = new BlockchainInfoDTO(entity.blockchain_info);
this.system_status = entity.system_status || 'install';
this.symbols = entity.symbols;
this.settings = new SettingsDTO(entity.settings, isRegistrationOpen(entity.vars));
this.settings = new SettingsDTO(entity.settings);
this.blockchain_account = entity.blockchain_account;
this.cooperator_account = new CooperativeOperatorAccountDTO({
...entity.cooperator_account,
@@ -25,6 +25,9 @@ import { Workflows } from '@coopenomics/notifications';
import { TokenApplicationService } from '~/application/token/services/token-application.service';
import { normalizeUserEmail } from '~/utils/normalize-user-email';
/** Минимум членов совета — как MIN_SOVIET_MEMBERS_COUNT в контракте soviet (3 prod / 1 dev). */
const MIN_SOVIET_MEMBERS_COUNT = config.min_soviet_members_count;
@Injectable()
export class InstallInteractor {
constructor(
@@ -118,14 +121,30 @@ export class InstallInteractor {
async install(data: InstallInputDomainInterface): Promise<void> {
const status = await this.monoStatusRepository.getStatus();
const savedVars = await this.varsRepository.get();
const isIncompleteInstallMaintenance =
status === SystemStatus.maintenance && !savedVars?.name;
// Разрешаем установку ТОЛЬКО если система в статусе 'initialized' (после initSystem)
if (status !== SystemStatus.initialized) {
// Разрешаем установку если:
// - initialized (нормальный прогон)
// - maintenance без сохранённых vars (прерванная установка, можно повторить)
if (status !== SystemStatus.initialized && !isIncompleteInstallMaintenance) {
throw new BadRequestException(
`Установка невозможна. Текущий статус: ${status}. Требуется статус: ${SystemStatus.initialized}`
);
}
if (data.soviet.length < MIN_SOVIET_MEMBERS_COUNT) {
throw new BadRequestException(
`Количество членов совета должно быть не менее ${MIN_SOVIET_MEMBERS_COUNT}`
);
}
// Повтор после прерванной установки: снимаем maintenance, дальше — обычный прогон.
if (isIncompleteInstallMaintenance) {
await this.monoStatusRepository.setStatus(SystemStatus.initialized);
}
const info = await this.blockchainPort.getInfo();
const coop = await this.blockchainPort.getCooperative(config.coopname);
@@ -36,7 +36,6 @@ import {
PaymentMethodDomainPort,
} from '~/domain/payment-method/ports/payment-method-domain.port';
import { LoadContactsInteractor } from './load-contacts.interactor';
import { isRegistrationOpen } from '~/domain/system/utils/is-registration-open.util';
@Injectable()
export class SystemInteractor {
@@ -308,9 +307,4 @@ export class SystemInteractor {
async updateSettings(updates: UpdateSettingsInputDomainInterface): Promise<SettingsDomainEntity> {
return this.settingsDomainPort.updateSettings(updates);
}
async getRegistrationOpenStatus(): Promise<boolean> {
const vars = await this.varsRepository.get();
return isRegistrationOpen(vars);
}
}
@@ -99,11 +99,8 @@ export class SystemService {
* Обновляет настройки системы
*/
public async updateSettings(data: UpdateSettingsInputDTO): Promise<SettingsDTO> {
const [settings, isRegistrationOpenStatus] = await Promise.all([
this.systemInteractor.updateSettings(data),
this.systemInteractor.getRegistrationOpenStatus(),
]);
return new SettingsDTO(settings, isRegistrationOpenStatus);
const settings = await this.systemInteractor.updateSettings(data);
return new SettingsDTO(settings);
}
/**
@@ -189,6 +189,10 @@ const envVarsSchema = z.object({
.string()
.optional()
.describe('База публичного URL контроллера для read-URL; пусто — берётся BACKEND_URL'),
MIN_SOVIET_MEMBERS_COUNT: z
.string()
.optional()
.describe('Минимум членов совета при install; пусто — 3 на production, 1 на development/test'),
});
const envInput = isSchemaGeneration ? { ...SCHEMA_GEN_ENV_DEFAULTS, ...process.env } : process.env;
@@ -260,6 +264,11 @@ export default {
},
},
coopname: envVars.data.COOPNAME,
min_soviet_members_count: envVars.data.MIN_SOVIET_MEMBERS_COUNT
? parseInt(envVars.data.MIN_SOVIET_MEMBERS_COUNT, 10)
: envVars.data.NODE_ENV === 'production'
? 3
: 1,
graphql_service: envVars.data.GRAPHQL_SERVICE,
provider_base_url: envVars.data.PROVIDER_BASE_URL,
union: {
@@ -33,10 +33,6 @@ export class BranchDomainEntity implements BranchDomainInterface {
ogrn: string;
kpp: string;
};
public readonly participants_count: number;
public readonly is_private: boolean;
public readonly is_available: boolean;
public readonly whitelist_members: IndividualDomainInterface[];
constructor(
coopname: string,
@@ -44,10 +40,7 @@ export class BranchDomainEntity implements BranchDomainInterface {
organizationDatabaseData: OrganizationDomainInterface,
trusteeData: IndividualDomainInterface,
trustedData: IndividualDomainInterface[],
bankAccount: BankPaymentMethodDomainInterface,
participantsCount = 0,
whitelistMembers: IndividualDomainInterface[] = [],
isAvailable = true
bankAccount: BankPaymentMethodDomainInterface
) {
if (branchBlockchainData.braname != organizationDatabaseData.username)
throw new Error(`Неверные данные для агрегата: username и braname кооперативного участка должны совпадать`);
@@ -70,11 +63,5 @@ export class BranchDomainEntity implements BranchDomainInterface {
this.phone = organizationDatabaseData.phone;
this.email = organizationDatabaseData.email;
this.details = organizationDatabaseData.details;
this.participants_count = participantsCount;
// признак приватности приходит из binary_extension контракта: у старых записей отсутствует → публичный
this.is_private = branchBlockchainData.is_private ?? false;
this.whitelist_members = whitelistMembers;
this.is_available = isAvailable;
}
}
@@ -1,3 +0,0 @@
import { BranchContract } from 'cooptypes';
export type AddBranchWhitelistDomainInterface = BranchContract.Actions.AddWhite.IAddWhite;
@@ -6,13 +6,9 @@ export interface BranchBlockchainPort {
editBranch(data: BranchContract.Actions.EditBranch.IEditBranch): Promise<TransactResult>;
getBranches(coopname: string): Promise<BranchContract.Tables.Branches.IBranch[]>;
getBranch(coopname: string, braname: string): Promise<BranchContract.Tables.Branches.IBranch | null>;
getParticipants(coopname: string): Promise<SovietContract.Tables.Participants.IParticipants[]>;
deleteBranch(data: BranchContract.Actions.DeleteBranch.IDeleteBranch): Promise<TransactResult>;
addTrustedAccount(data: BranchContract.Actions.AddTrusted.IAddTrusted): Promise<TransactResult>;
deleteTrustedAccount(data: BranchContract.Actions.DeleteTrusted.IDeleteTrusted): Promise<TransactResult>;
setBranchPrivate(data: BranchContract.Actions.SetPrivate.ISetPrivate): Promise<TransactResult>;
addBranchWhitelist(data: BranchContract.Actions.AddWhite.IAddWhite): Promise<TransactResult>;
deleteBranchWhitelist(data: BranchContract.Actions.DelWhite.IDelWhite): Promise<TransactResult>;
selectBranch(data: SovietContract.Actions.Branches.SelectBranch.ISelectBranch): Promise<TransactResult>;
}
@@ -5,7 +5,4 @@ export type BranchDomainInterface = Omit<OrganizationDomainInterface, 'username'
braname: string; ///< имя аккаунта
trustee: IndividualDomainInterface;
trusted: IndividualDomainInterface[];
participants_count: number; ///< количество пайщиков, состоящих в участке
is_private: boolean; ///< приватный участок: выбрать его можно только из белого списка
is_available: boolean; ///< доступен ли участок текущему пайщику для выбора (публичный или он в белом списке)
};
@@ -1,3 +0,0 @@
import { BranchContract } from 'cooptypes';
export type DeleteBranchWhitelistDomainInterface = BranchContract.Actions.DelWhite.IDelWhite;
@@ -1,3 +0,0 @@
import { BranchContract } from 'cooptypes';
export type SetBranchPrivateDomainInterface = BranchContract.Actions.SetPrivate.ISetPrivate;
@@ -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,6 +23,23 @@ export class DocumentDomainService {
) {}
public async generateDocument(data: GenerateDocumentDomainInterfaceWithOptions): Promise<DocumentDomainEntity> {
const documentData: GenerateDocumentWithPrivateDataDomainInterface = data.data;
if (documentData.doc_data) {
const { doc_data, ...publicDocumentData } = documentData;
const { hash } = documentData.doc_data_hash
? { hash: documentData.doc_data_hash }
: await this.saveDocData(doc_data, documentData.registry_id);
return await this.generatorInfrastructureService.generateDocument({
...data,
data: {
...publicDocumentData,
doc_data_hash: hash,
},
});
}
return await this.generatorInfrastructureService.generateDocument(data);
}
@@ -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, // Сохраняем документ в базу для последующей сверки
@@ -1,35 +0,0 @@
import type { AgreementNumberDomainInterface } from '~/domain/agreement/interfaces/agreement-number.interface';
import type { VarsDomainInterface } from '~/domain/system/interfaces/vars-domain.interface';
const REGISTRATION_REQUIRED_AGREEMENT_VARS = [
'wallet_agreement',
'signature_agreement',
'privacy_agreement',
'user_agreement',
'participant_application',
] as const satisfies ReadonlyArray<keyof VarsDomainInterface>;
function isAgreementVarFilled(agreement?: AgreementNumberDomainInterface | null): boolean {
if (!agreement) {
return false;
}
const protocolNumber = agreement.protocol_number?.trim();
const protocolDate = agreement.protocol_day_month_year?.trim();
return Boolean(protocolNumber) && Boolean(protocolDate);
}
/**
* Регистрация пайщиков доступна, когда заполнены реквизиты протоколов
* базовых соглашений кооператива без них factory не сгенерирует пакет документов.
*/
export function isRegistrationOpen(vars: VarsDomainInterface | null | undefined): boolean {
if (!vars) {
return false;
}
return REGISTRATION_REQUIRED_AGREEMENT_VARS.every((field) =>
isAgreementVarFilled(vars[field] as AgreementNumberDomainInterface | null | undefined)
);
}
@@ -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)
@@ -68,6 +76,9 @@ export class CapitalOnboardingStateDTO {
@Field(() => String, { nullable: true })
onboarding_blagorost_offer_template_hash?: string | null;
@Field(() => String, { nullable: true })
capital_program_doc_data_hash?: string | null;
@Field(() => String)
onboarding_init_at!: string;
@@ -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'
);
@@ -29,6 +29,10 @@ type OnboardingHashKey =
| 'onboarding_blagorost_provision_hash'
| 'onboarding_blagorost_offer_template_hash';
type CapitalOnboardingConfig = IConfig &
Partial<Record<OnboardingFlagKey, boolean>> &
Partial<Record<OnboardingHashKey | 'onboarding_init_at' | 'onboarding_expire_at' | 'capital_program_doc_data_hash', string>>;
@Injectable()
export class CapitalOnboardingService {
constructor(
@@ -88,32 +92,65 @@ export class CapitalOnboardingService {
}
}
private async loadPlugin(): Promise<ExtensionDomainEntity<IConfig & Record<string, any>>> {
private isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null;
}
private getMetaString(meta: unknown, key: string): string | undefined {
if (!this.isRecord(meta)) return undefined;
const value = meta[key];
return typeof value === 'string' ? value : undefined;
}
private isSignatureInfo(value: unknown): value is ISignedDocumentDomainInterface['signatures'][number] {
if (!this.isRecord(value)) return false;
return (
typeof value.id === 'number' &&
typeof value.signed_hash === 'string' &&
typeof value.signer === 'string' &&
typeof value.public_key === 'string' &&
typeof value.signature === 'string' &&
typeof value.signed_at === 'string' &&
typeof value.meta === 'string'
);
}
private getMetaSignatures(meta: unknown): ISignedDocumentDomainInterface['signatures'] {
if (!this.isRecord(meta)) return [];
const value = meta.signatures;
return Array.isArray(value) ? value.filter((item) => this.isSignatureInfo(item)) : [];
}
private async loadPlugin(): Promise<ExtensionDomainEntity<CapitalOnboardingConfig>> {
const plugin = await this.extensionRepository.findByName('capital');
if (!plugin) throw new Error('Конфигурация расширения capital не найдена');
const pluginConfig = { ...plugin.config } as IConfig & Record<string, any>;
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 };
}
private buildState(pluginConfig: IConfig & Record<string, any>): CapitalOnboardingStateDTO {
private buildState(pluginConfig: CapitalOnboardingConfig): CapitalOnboardingStateDTO {
return {
generator_program_template_done: !!pluginConfig.onboarding_generator_program_template_done,
onboarding_generator_program_template_hash: pluginConfig.onboarding_generator_program_template_hash || null,
@@ -125,6 +162,7 @@ export class CapitalOnboardingService {
onboarding_blagorost_provision_hash: pluginConfig.onboarding_blagorost_provision_hash || null,
blagorost_offer_template_done: !!pluginConfig.onboarding_blagorost_offer_template_done,
onboarding_blagorost_offer_template_hash: pluginConfig.onboarding_blagorost_offer_template_hash || null,
capital_program_doc_data_hash: pluginConfig.capital_program_doc_data_hash || null,
onboarding_init_at: pluginConfig.onboarding_init_at || '',
onboarding_expire_at: pluginConfig.onboarding_expire_at || '',
};
@@ -135,13 +173,28 @@ 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);
const hashKey = this.mapStepToHash(data.step);
const normalizedTitle = data.title?.trim().substring(0, 200) || undefined;
if ((plugin.config as any)[flagKey]) {
if (plugin.config[flagKey]) {
return this.buildState(plugin.config);
}
const project_id = uuid();
@@ -168,12 +221,12 @@ export class CapitalOnboardingService {
// Публикуем проект решения в блокчейн сразу после генерации
const documentForPublish: ISignedDocumentDomainInterface = {
version: (generatedDoc.meta as any)?.version || '1.0',
version: this.getMetaString(generatedDoc.meta, 'version') || '1.0',
hash: generatedDoc.hash,
doc_hash: (generatedDoc.meta as any)?.doc_hash || generatedDoc.hash,
meta_hash: (generatedDoc.meta as any)?.meta_hash || generatedDoc.hash,
doc_hash: this.getMetaString(generatedDoc.meta, 'doc_hash') || generatedDoc.hash,
meta_hash: this.getMetaString(generatedDoc.meta, 'meta_hash') || generatedDoc.hash,
meta: generatedDoc.meta,
signatures: (generatedDoc.meta as any)?.signatures || [],
signatures: this.getMetaSignatures(generatedDoc.meta),
};
await this.freeDecisionPort.publishProjectOfFreeDecision({
@@ -183,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);
@@ -204,6 +257,6 @@ export class CapitalOnboardingService {
},
});
return this.buildState(updatedConfig);
return this.buildState(updated.config as CapitalOnboardingConfig);
}
}
@@ -306,10 +306,11 @@ export class PermissionsService {
? await this.isProjectContributor(username, project.coopname, project.project_hash)
: false;
// Проверяем наличие pending clearance
const pending_clearance = project.coopname
? (await this.appendixRepository.findCreatedByUsernameAndProjectHash(username, project.project_hash)) !== null
: false;
// Запрос на рассмотрении — только status=created и только если допуска ещё нет
const pending_clearance =
!has_clearance && project.coopname
? (await this.appendixRepository.findCreatedByUsernameAndProjectHash(username, project.project_hash)) !== null
: false;
// Проверяем допуск к родителю — каскадно вниз по иерархии проектов:
// если есть допуск к корневому проекту, пользователь видит артефакты всех его компонентов.
@@ -51,6 +51,11 @@ import { ProgramKey } from '~/domain/registration/enum';
import type { GenerateCapitalRegistrationDocumentsDomainInput } from '../../domain/actions/generate-capital-registration-documents-domain-input.interface';
import type { GenerateCapitalRegistrationDocumentsDomainOutput } from '../../domain/actions/generate-capital-registration-documents-domain-output.interface';
import type { CompleteCapitalRegistrationDomainInput } from '../../domain/actions/complete-capital-registration-domain-input.interface';
import {
EXTENSION_REPOSITORY,
type ExtensionDomainRepository,
} from '~/domain/extension/repositories/extension-domain.repository';
import type { IConfig } from '../../capital-extension.module';
/**
* Интерактор домена для управления участием в CAPITAL контракте
@@ -76,8 +81,24 @@ export class ParticipationManagementInteractor {
private readonly projectManagementInteractor: ProjectManagementInteractor,
private readonly domainToBlockchainUtils: DomainToBlockchainUtils,
private readonly documentInteractor: DocumentInteractor,
@Inject(EXTENSION_REPOSITORY)
private readonly extensionRepository: ExtensionDomainRepository<IConfig>,
) { }
private async getCapitalProgramDocDataHash(): Promise<string> {
const extension = await this.extensionRepository.findByName('capital');
const hash = (extension?.config as IConfig | undefined)?.capital_program_doc_data_hash?.trim();
if (!hash) {
throw new HttpApiError(
httpStatus.BAD_REQUEST,
'Параметры документов ЦПП не заполнены: отсутствует capital_program_doc_data_hash в конфигурации capital'
);
}
return hash;
}
/**
* Получение отображаемого имени из аккаунта через порт расширения
*/
@@ -313,7 +334,8 @@ export class ParticipationManagementInteractor {
}
// Извлекаем appendix_hash из метаданных документа
const appendix_hash = (document.meta as any).appendix_hash;
const documentMeta: Record<string, unknown> = document.meta;
const appendix_hash = typeof documentMeta.appendix_hash === 'string' ? documentMeta.appendix_hash : undefined;
//TODO: адаптировать или документ или код ниже к parent_appendix_hash
if (!appendix_hash) {
@@ -323,7 +345,20 @@ export class ParticipationManagementInteractor {
);
}
// Проверяем, есть ли уже запрос на рассмотрении для данного пользователя и проекта
// Уже подтверждённый допуск — повторный отклик не нужен
const confirmedAppendix =
await this.appendixRepository.findConfirmedByUsernameAndProjectHash(
data.username,
data.project_hash
);
if (confirmedAppendix) {
throw new HttpApiError(
httpStatus.CONFLICT,
'Вы уже являетесь участником этого проекта'
);
}
// Есть незакрытый запрос на рассмотрении
const existingAppendix =
await this.appendixRepository.findCreatedByUsernameAndProjectHash(
data.username,
@@ -590,11 +625,17 @@ export class ParticipationManagementInteractor {
);
}
// 6. Находим родительское приложение (appendix) к родительскому проекту
const parentAppendix = await this.appendixRepository.findCreatedByUsernameAndProjectHash(
data.username,
parentProject.project_hash
);
// 6. Допуск к родительскому проекту: confirmed (уже участник)
// или created (только что отправили отклик на родителя в том же диалоге)
const parentAppendix =
(await this.appendixRepository.findConfirmedByUsernameAndProjectHash(
data.username,
parentProject.project_hash
)) ??
(await this.appendixRepository.findCreatedByUsernameAndProjectHash(
data.username,
parentProject.project_hash
));
if (!parentAppendix) {
throw new HttpApiError(
@@ -778,6 +819,7 @@ export class ParticipationManagementInteractor {
// Генерируем параметры для соглашения Благорост
await this.udataDocumentParametersService.generateBlagorostAgreementParametersIfNotExist(data.coopname, data.username);
const capitalProgramDocDataHash = await this.getCapitalProgramDocDataHash();
blagorostAgreement = await this.documentInteractor.generateDocument({
data: {
@@ -785,6 +827,7 @@ export class ParticipationManagementInteractor {
username: data.username,
lang,
registry_id: Cooperative.Registry.BlagorostAgreement.registry_id,
doc_data_hash: capitalProgramDocDataHash,
},
options: { skip_save: false },
});
@@ -809,6 +852,7 @@ export class ParticipationManagementInteractor {
// Генерируем параметры для оферты Генератор
await this.udataDocumentParametersService.generateGeneratorOfferParametersIfNotExist(data.coopname, data.username);
const capitalProgramDocDataHash = await this.getCapitalProgramDocDataHash();
generatorOffer = await this.documentInteractor.generateDocument({
data: {
@@ -816,6 +860,7 @@ export class ParticipationManagementInteractor {
username: data.username,
lang,
registry_id: Cooperative.Registry.GeneratorOffer.registry_id,
doc_data_hash: capitalProgramDocDataHash,
},
options: { skip_save: false },
});
@@ -40,6 +40,7 @@ export const defaultConfig = {
onboarding_generator_offer_template_done: false,
onboarding_blagorost_provision_done: false,
onboarding_blagorost_offer_template_done: false,
capital_program_doc_data_hash: '',
/** Ветка для выборки коммитов (FR3 / PRD); URL репозитория — на проекте/компоненте (PRD §6.2.1). */
github_sync_branch: 'dev',
/** Интервал polling GitHub API в минутах (FR2); 0 — периодический опрос отключён. */
@@ -253,6 +254,10 @@ export const Schema = z.object({
.boolean()
.default(defaultConfig.onboarding_blagorost_offer_template_done)
.describe(describeField({ label: 'Шаг предложения Благорост выполнен', visible: false })),
capital_program_doc_data_hash: z
.string()
.default(defaultConfig.capital_program_doc_data_hash)
.describe(describeField({ label: 'PrivateData документов ЦПП', visible: false })),
});
@@ -577,8 +582,10 @@ export class CapitalPlugin extends BaseExtModule {
} else {
this.logger.log('Конфигурация контракта CAPITAL уже актуальна');
}
} catch (error: any) {
this.logger.error(`Не удалось синхронизировать конфигурацию с контрактом CAPITAL: ${error.message}`, error.stack);
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
const stack = error instanceof Error ? error.stack : undefined;
this.logger.error(`Не удалось синхронизировать конфигурацию с контрактом CAPITAL: ${message}`, stack);
// Не бросаем ошибку, чтобы не блокировать запуск модуля
}
@@ -586,10 +593,12 @@ export class CapitalPlugin extends BaseExtModule {
try {
await this.syncInteractor.initializeSync();
this.logger.log('Синхронизация благороста с блокчейном инициализирована');
} catch (error: any) {
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
const stack = error instanceof Error ? error.stack : undefined;
this.logger.error(
`Не удалось инициализировать синхронизацию благороста с блокчейном: ${error.message}`,
error.stack
`Не удалось инициализировать синхронизацию благороста с блокчейном: ${message}`,
stack
);
// Не бросаем ошибку, чтобы не блокировать запуск модуля
}
@@ -645,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';
@@ -873,7 +873,7 @@ export class CapitalBlockchainAdapter implements CapitalBlockchainPort {
): Promise<CapitalContract.Tables.Segments.ISegment | null> {
// Создаем составной ключ для поиска по индексу by_project_user (позиция 3)
const compositeKey = this.domainToBlockchainUtils.combineChecksumAndUsername(projectHash, username);
const keyUInt128 = UInt128.from(compositeKey.toString());
const keyUInt128 = UInt128.from(compositeKey);
// Получаем сегмент из таблицы segments контракта capital
const segment = await this.blockchainService.getSingleRow<CapitalContract.Tables.Segments.ISegment>(
@@ -72,13 +72,15 @@ export class AppendixTypeormRepository
}
/**
* Найти созданное приложение по имени пользователя и хэшу проекта
* Найти приложение со статусом created (запрос на рассмотрении)
* по имени пользователя и хэшу проекта
*/
async findCreatedByUsernameAndProjectHash(username: string, projectHash: string): Promise<AppendixDomainEntity | null> {
const entity = await this.repository.findOne({
where: {
username,
username: username.toLowerCase(),
project_hash: projectHash.toLowerCase(),
status: AppendixStatus.CREATED,
},
});
@@ -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);
}
}

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