Compare commits
79 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4b16a18716 | |||
| 67121d51cd | |||
| 489492695b | |||
| 757c785ee6 | |||
| a819b27fbe | |||
| 2b63f54117 | |||
| 8d0e837bb9 | |||
| 70f2e8e38b | |||
| 5e58c173ee | |||
| c1ce7a095d | |||
| 995f65f524 | |||
| 5298e55a7b | |||
| db28ba1137 | |||
| 6a05e88ba1 | |||
| 09eaa6192c | |||
| 81df662cfa | |||
| 845c13605d | |||
| f521d65a51 | |||
| 76b4988da8 | |||
| 3db80a30e9 | |||
| 1b457b8f87 | |||
| ba6d3d1310 | |||
| 8e01373179 | |||
| 31940461c6 | |||
| 37b0227735 | |||
| 2cfb37c5d7 | |||
| 1c605dd2a5 | |||
| b1b1775277 | |||
| 5f4281ac01 | |||
| d35e88d951 | |||
| 5360daa471 | |||
| 652f7c722e | |||
| a2b132b457 | |||
| a6cc71d6a3 | |||
| b67581ce80 | |||
| 024d46ff64 | |||
| 9030d28c6b | |||
| 6e0e68c1e7 | |||
| c079785019 | |||
| bbc6453b24 | |||
| b5eb0ea51f | |||
| a072364d00 | |||
| ed9b0879d5 | |||
| c94043e77f | |||
| c1a8f3e637 | |||
| 15fdf35dd3 | |||
| 79b89695d1 | |||
| 7b27d0805c | |||
| e089dbd3fb | |||
| 5230fab7c6 | |||
| 345aadbd6a | |||
| 33f1ac1a15 | |||
| 5de7e7c65b | |||
| bfbcd2d57f | |||
| ff5df68bce | |||
| a948f07a1b | |||
| 85bd044cd6 | |||
| 45feef2676 | |||
| 02ce9400a2 | |||
| bfa2830650 | |||
| c7824545cf | |||
| b9107e9ee9 | |||
| 4d151befe0 | |||
| eb16d85abc | |||
| 710763f7db | |||
| 9ca51c13c8 | |||
| 4c86c911c6 | |||
| 7e5ef8e928 | |||
| 572984e32c | |||
| 6257730ccb | |||
| 641eaaad6d | |||
| 3b81f821c7 | |||
| 112d13114c | |||
| e1405a3b3d | |||
| 6d4809e8b6 | |||
| edc339b673 | |||
| 9b26834033 | |||
| ecf8d2b5de | |||
| 14170e6d93 |
@@ -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) через
|
||||
|
||||
Vendored
-1
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"cSpell.enabled": false,
|
||||
"editor.bracketPairColorization.enabled": true,
|
||||
"editor.guides.bracketPairs": true,
|
||||
"editor.tabCompletion": "onlySnippets",
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
@@ -35,7 +35,14 @@ RUN npm install -g pnpm@10.33.0
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN pnpm install --frozen-lockfile --filter "@coopenomics/boot..."
|
||||
# --ignore-scripts + точечный rebuild: --filter ограничивает только то, какие
|
||||
# проекты получают node_modules, но НЕ то, для каких пакетов общего lockfile
|
||||
# store выполняет lifecycle-скрипты при --frozen-lockfile. Без --ignore-scripts
|
||||
# сюда тянутся electron/puppeteer (из @coopenomics/desktop/notifications,
|
||||
# boot их не использует) — тяжёлые сетевые загрузки, изредка валящие билд по
|
||||
# TLS-таймауту. rebuild --filter уже честно скопирован на подграф boot.
|
||||
RUN pnpm install --frozen-lockfile --filter "@coopenomics/boot..." --ignore-scripts \
|
||||
&& pnpm rebuild --filter "@coopenomics/boot..."
|
||||
|
||||
# Билд графа: cooptypes/factory собирают свои dist/, boot собирает
|
||||
# свой bundle с inlined workspace-deps.
|
||||
|
||||
@@ -1,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": "",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "@coopenomics/contracts",
|
||||
"version": "2026.6.16-1",
|
||||
"version": "2026.7.23-2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
+1
@@ -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;
|
||||
},
|
||||
};
|
||||
Generated
+2
-2
@@ -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,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… (5–30 с без вывода — норма, не прерывайте)');
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
-3
@@ -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; ///< доступен ли участок текущему пайщику для выбора (публичный или он в белом списке)
|
||||
};
|
||||
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
import { BranchContract } from 'cooptypes';
|
||||
|
||||
export type DeleteBranchWhitelistDomainInterface = BranchContract.Actions.DelWhite.IDelWhite;
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
import { BranchContract } from 'cooptypes';
|
||||
|
||||
export type SetBranchPrivateDomainInterface = BranchContract.Actions.SetPrivate.ISetPrivate;
|
||||
+17
-9
@@ -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;
|
||||
}
|
||||
|
||||
+13
@@ -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>;
|
||||
}
|
||||
|
||||
+1
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+6
@@ -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;
|
||||
|
||||
|
||||
+10
-1
@@ -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({
|
||||
|
||||
+17
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+6
-7
@@ -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'
|
||||
);
|
||||
|
||||
+73
-20
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+5
-4
@@ -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;
|
||||
|
||||
// Проверяем допуск к родителю — каскадно вниз по иерархии проектов:
|
||||
// если есть допуск к корневому проекту, пользователь видит артефакты всех его компонентов.
|
||||
|
||||
+52
-7
@@ -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';
|
||||
|
||||
+1
-1
@@ -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>(
|
||||
|
||||
+4
-2
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
+8
-11
@@ -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 завершён последним шагом, эмиттим событие'
|
||||
);
|
||||
|
||||
+19
-15
@@ -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
Reference in New Issue
Block a user