[merge-dev-into-marketplace2][@dark.sun] chore: merge origin/dev в marketplace2

Резолв конфликтов:
- ledger2/operations.hpp: оставлен CONVERT_TO_MEMBER (marketplace2) +
  принята новая логика withdraw из dev (WITHDRAW_PENDING вместо BLOCK).
- ledger2/wallets.hpp: реестр расширен до 16 кошельков
  (USER_SHARED=6 с MARKETPLACE_ORDER_LOCK + COOPERATIVE=10 с WITHDRAW_PENDING).
- WalletProgramWidget.vue / WalletWidget.vue: убраны неиспользуемые helper'ы из marketplace2,
  принят рефактор dev.
- schema.gql + zeus/* (controller + sdk): полная регенерация после merge.

Сопутствующие правки:
- Восстановлен marketplace-branch-ownership.service.ts (отсутствовал в origin/marketplace2,
  но был импортирован из marketplace-return-claim.resolver.ts — взят из ветки
  chore/trustees-ownership 46f5269b89).
- Исправлена опечатка MARKETPLACE_KU_CHAIRMEN_SERVICE → MARKETPLACE_KU_CHAIRMAN_SERVICE
  в marketplace-desktop-grants.provider.ts.
- orderSelector.ts: blocked_amount → locked_amount, удалены did_convert/did_assign
  (привести selector к новой схеме MarketplaceOrderCreateTxSnapshot).
- Пересобраны cooptypes и inter (stale dist после merge).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
ant
2026-05-28 17:03:20 +00:00
559 changed files with 25943 additions and 11465 deletions
+3
View File
@@ -0,0 +1,3 @@
Trial run of Typecheck workflow on PR → dev.
Created 2026-05-23 to measure end-to-end duration of vue-tsc (desktop) and tsc (controller).
Do not merge; close after measurement.
+104 -160
View File
@@ -5,8 +5,12 @@ name: Release
# Порядок (через jobs.needs):
# 1) release — контракты + контейнеры + webhook деплоя (атомарно)
# 2) publish-packages — npm publish через lerna (если не -alpha)
# 3) publish-docs — mkdocs + standards-site → gh-pages (если не -alpha + ветка main)
# 4) trigger-coopenomics — repository_dispatch в coopenomics/coopenomics (тот же гейт, что docs)
# 3) trigger-coopenomics — workflow_dispatch сборки сайта C9S/coopenomics на
# Gitea (если не -alpha + ветка main); он сам тянет
# mono и пересобирает доки.
#
# publish-docs (gh-pages на github.com) удалён 2026-05-25 — после переезда на
# Gitea Pages-публикация невалидна; доки деплоятся через C9S/coopenomics.
#
# Зачем последовательно: пакеты/доки/внешний триггер не должны уезжать,
# если релиз контрактов или контейнеров провалился. Раньше четыре workflow'а
@@ -34,16 +38,7 @@ permissions:
contents: write
jobs:
# Гейт TS-типов перед сборкой образов. Reusable workflow `typecheck.yaml`
# запускает vue-tsc на desktop и tsc --noEmit на controller. Нужно потому,
# что `lerna run build` в корневом Dockerfile НЕ ловит TS-ошибки:
# quasar build идёт через esbuild с выключенным vueTsc, а у controller'а
# build-скрипта вообще нет (ts-node в рантайме).
typecheck:
uses: ./.github/workflows/typecheck.yaml
release:
needs: typecheck
runs-on: ubuntu-latest
outputs:
tag_name: ${{ steps.resolve.outputs.tag_name }}
@@ -127,14 +122,41 @@ jobs:
password: ${{ secrets.DOCKERHUB_TOKEN }}
# === Этап 1: контракты ===
# CDT ставим из .deb (C9S/cdt v4.2.0) прямо в окружение job'а и
# компилируем напрямую — БЕЗ вложенного docker. Почему не build-all.sh:
# тот монтирует $(pwd):/project в sibling-контейнер, а под Gitea
# act_runner сам job исполняется в контейнере → хостовый демон не видит
# этот путь, /project пуст, cmake падает "no CMakeLists.txt".
# build-all.sh остаётся для локальной сборки (оборачивает тот же
# build_contracts_cdt.sh в docker).
# CMakeLists хардкодит toolchain /cdt/build/...; .deb кладёт CDT в
# /usr/opt/cdt/4.2.0, а сам CDTWasmToolchain.cmake указывает на /usr
# абсолютно — поэтому симлинк /cdt/build → /usr/opt/cdt/4.2.0 сводит пути
# без правки CMakeLists.
- name: Pull CDT toolchain image
run: docker pull dicoop/blockchain_v5.1.1:dev
- name: Install CDT 4.2.0 toolchain (.deb)
run: |
SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo"
$SUDO apt-get update
# build-essential — для host-компилятора (project() в CMakeLists);
# libz3-4/libtinfo6/libxml2/zlib1g — рантайм бинарей CDT (clang-9,
# ld.lld и т.д.). Пакет cdt объявляет только libcurl4-gnutls-dev,
# поэтому остальные .so ставим явно — в образе dicoop/blockchain они
# были из сборки исходников, из .deb не тянутся.
$SUDO apt-get install -y --no-install-recommends \
curl ca-certificates cmake make build-essential \
libz3-4 libtinfo6 libxml2 zlib1g
curl -fsSL -o /tmp/cdt.deb \
https://git.coopenomics.world/C9S/cdt/releases/download/v4.2.0/cdt_4.2.0-1_amd64.deb
$SUDO apt-get install -y /tmp/cdt.deb
$SUDO mkdir -p /cdt
$SUDO ln -sfn /usr/opt/cdt/4.2.0 /cdt/build
cdt-cpp --version || true
- name: Compile contracts
working-directory: components/contracts
run: |
./build-all.sh "$BUILD_MODE"
./build_contracts_cdt.sh "$BUILD_MODE"
echo "--- build/contracts ---"
ls -la build/contracts/
@@ -215,6 +237,17 @@ jobs:
- name: Build and push contracts image
working-directory: components/contracts
run: |
# Ретрай push'а: резолвинг registry-1.docker.io на runner'е изредка
# моргает (DNS-таймаут к 127.0.0.53) — 3 попытки с паузой.
dpush() {
local ref="$1" n=1
until docker push "$ref"; do
[ "$n" -ge 3 ] && { echo "::error::docker push $ref не удался после $n попыток"; return 1; }
echo "::warning::docker push $ref упал (попытка $n/3) — повтор через 10с"
n=$((n+1)); sleep 10
done
}
IMAGE="dicoop/contracts"
SHORT_SHA="${SHA::7}"
docker build \
@@ -223,14 +256,14 @@ jobs:
--label "build.mode=$BUILD_MODE" \
-t "$IMAGE:$CONTRACTS_TAG" \
./docker/.context
docker push "$IMAGE:$CONTRACTS_TAG"
dpush "$IMAGE:$CONTRACTS_TAG"
docker tag "$IMAGE:$CONTRACTS_TAG" "$IMAGE:$CONTRACTS_TAG-$SHORT_SHA"
docker push "$IMAGE:$CONTRACTS_TAG-$SHORT_SHA"
dpush "$IMAGE:$CONTRACTS_TAG-$SHORT_SHA"
if [ -n "$CONTRACTS_EXTRA" ]; then
docker tag "$IMAGE:$CONTRACTS_TAG" "$IMAGE:$CONTRACTS_EXTRA"
docker push "$IMAGE:$CONTRACTS_EXTRA"
dpush "$IMAGE:$CONTRACTS_EXTRA"
fi
- name: Verify pushed contracts image
@@ -243,15 +276,31 @@ jobs:
- name: Build and push base image
run: |
dpush() {
local ref="$1" n=1
until docker push "$ref"; do
[ "$n" -ge 3 ] && { echo "::error::docker push $ref не удался после $n попыток"; return 1; }
echo "::warning::docker push $ref упал (попытка $n/3) — повтор через 10с"
n=$((n+1)); sleep 10
done
}
docker build --target runtime -t "dicoop/mono-base:$TAG_NAME" .
docker push "dicoop/mono-base:$TAG_NAME"
dpush "dicoop/mono-base:$TAG_NAME"
if [ "$IS_PROD" = "true" ]; then
docker tag "dicoop/mono-base:$TAG_NAME" dicoop/mono-base:latest
docker push dicoop/mono-base:latest
dpush dicoop/mono-base:latest
fi
- name: Build and push service images
run: |
dpush() {
local ref="$1" n=1
until docker push "$ref"; do
[ "$n" -ge 3 ] && { echo "::error::docker push $ref не удался после $n попыток"; return 1; }
echo "::warning::docker push $ref упал (попытка $n/3) — повтор через 10с"
n=$((n+1)); sleep 10
done
}
build_service() {
local SVC="$1" PKG="$2" CMD="$3"
local DOCKERFILE="Dockerfile.$SVC"
@@ -260,10 +309,10 @@ jobs:
echo "CMD [\"pnpm\", \"-F\", \"$PKG\", \"run\", \"$CMD\"]"
} > "$DOCKERFILE"
docker build -t "dicoop/$SVC:$TAG_NAME" -f "$DOCKERFILE" .
docker push "dicoop/$SVC:$TAG_NAME"
dpush "dicoop/$SVC:$TAG_NAME"
if [ "$IS_PROD" = "true" ]; then
docker tag "dicoop/$SVC:$TAG_NAME" "dicoop/$SVC:latest"
docker push "dicoop/$SVC:latest"
dpush "dicoop/$SVC:latest"
fi
}
@@ -314,149 +363,44 @@ jobs:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
# ============================================================================
# publish-docs — mkdocs + standards-site → gh-pages (бывший publish-docs.yaml).
# trigger-coopenomics-docs — пересборка сайта C9S/coopenomics.
# Гейт: не-alpha + ветка main.
# ВНИМАНИЕ: gh-pages step пушит на github.com/coopenomics/mono — это GitHub
# Pages. При переезде на Gitea Actions нужно будет либо подложить PAT для
# внешнего push'а в GitHub, либо переключить публикацию на Gitea Pages.
# ============================================================================
publish-docs:
needs: release
if: ${{ !contains(github.ref, '-alpha') && needs.release.outputs.branch == 'main' }}
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Версия pnpm берётся из `packageManager` корневого package.json,
# синхронно с publish-packages job'ом и Dockerfile'ами.
- name: Set up pnpm
uses: pnpm/action-setup@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install Python requirements
run: |
python -m venv venv
source venv/bin/activate
pip install mkdocs-material mkdocs-macros-plugin mkdocs-section-index pymdown-extensions
working-directory: ./components/docs
- name: Install Node.js dependencies
run: pnpm install
working-directory: ./components/docs
- name: Patch spectaql-config.yml for CI
run: |
sed -i.bak "0,/url:.*/s|url:.*|url: 'https://testnet.coopenomics.world/backend/v1/graphql'|" spectaql-config.yml
working-directory: ./components/controller
- name: Show patched spectaql-config.yml
run: cat spectaql-config.yml
working-directory: ./components/controller
- name: Build cooptypes
run: pnpm run build
working-directory: ./components/cooptypes
- name: Generate controller docs
run: pnpm run docs
working-directory: ./components/controller
- name: Copy controller docs
run: |
mkdir -p ./components/docs/docs/graphql
cp -r ./components/controller/docs/* ./components/docs/docs/graphql/
- name: Generate sdk docs
run: pnpm run docs
working-directory: ./components/sdk
- name: Copy sdk docs
run: |
mkdir -p ./components/docs/docs/sdk
cp -r ./components/sdk/docs/* ./components/docs/docs/sdk/
- name: Generate cooptypes docs
run: pnpm run docs
working-directory: ./components/cooptypes
- name: Copy cooptypes docs
run: |
mkdir -p ./components/docs/docs/cooptypes
cp -r ./components/cooptypes/docs/* ./components/docs/docs/cooptypes/
# ── standards-site → /standards/ на docs.цифровой-кооператив.рф ──
# Отдельный Vue-сайт с BPMN-графом кооперативных стандартов,
# публикуется как поддиректория на том же домене. Vite собирает
# с base='/standards/' (см. vite.config.ts), относительные пути
# внутри dist/ и hash-router работают корректно.
# dist кладём в docs/standards/ ДО mkdocs build — так же, как
# graphql/sdk/cooptypes; mkdocs сам включит его в итоговый site/.
- name: Build standards-site
run: pnpm run build
working-directory: ./components/contracts/standards-site
- name: Copy standards-site into docs/standards
run: |
mkdir -p ./components/docs/docs/standards
cp -r ./components/contracts/standards-site/dist/* ./components/docs/docs/standards/
- name: Build docs (mkdocs)
run: |
source venv/bin/activate
mkdocs build
working-directory: ./components/docs
- name: Remove specific large file before publishing
run: |
rm -f ./components/docs/site/sdk/typedoc.json
if [ -f "./components/docs/site/sdk/typedoc.json" ]; then
echo "ERROR: typedoc.json still exists!"
exit 1
else
echo "SUCCESS: typedoc.json removed successfully"
fi
- name: Publish to GitHub Pages
run: npx gh-pages --nojekyll -d site --repo https://x-access-token:${GITHUB_TOKEN}@github.com/coopenomics/mono.git
working-directory: ./components/docs
env:
GIT_AUTHOR_NAME: github-actions
GIT_AUTHOR_EMAIL: github-actions@github.com
GIT_COMMITTER_NAME: github-actions
GIT_COMMITTER_EMAIL: github-actions@github.com
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Trigger docs deployment webhook
if: ${{ success() }}
run: |
curl -X POST "${{ vars.DOCS_DEPLOY_WEBHOOK_URL }}" \
-H 'Content-Type: application/json' \
-d '{"ref":"${{ github.ref }}","sha":"${{ github.sha }}","branch":"${{ github.ref_name }}"}'
# ============================================================================
# trigger-coopenomics-docs — repository_dispatch в coopenomics/coopenomics
# (бывший build-contracts-docs.yaml). Гейт идентичен publish-docs.
#
# coopenomics переехал на Gitea (C9S/coopenomics); его publish-docs.yaml
# клонирует mono и пересобирает сайт. Gitea НЕ имеет API для
# repository_dispatch — только workflow_dispatch, поэтому дёргаем целевой
# workflow через Gitea API с PAT (secret DOCS_DISPATCH_TOKEN — префикс GITEA_
# у секретов зарезервирован Gitea, нельзя; права write на C9S/coopenomics).
# Целевой workflow слушает workflow_dispatch с входами
# mono_sha/mono_ref.
# ============================================================================
trigger-coopenomics-docs:
needs: release
if: ${{ !contains(github.ref, '-alpha') && needs.release.outputs.branch == 'main' }}
runs-on: ubuntu-latest
steps:
- name: Trigger Coopenomics deployment
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.COOPENOMICS_PAT }}
repository: coopenomics/coopenomics
event-type: deploy_from_mono
client-payload: '{"repository": "${{ github.repository }}", "sha": "${{ github.sha }}", "ref": "${{ github.ref }}", "actor": "${{ github.actor }}"}'
- name: Trigger coopenomics website rebuild (Gitea workflow_dispatch)
run: |
curl -fsSL -X POST \
-H "Authorization: token ${{ secrets.DOCS_DISPATCH_TOKEN }}" \
-H "Content-Type: application/json" \
-d '{"ref":"master","inputs":{"mono_sha":"${{ github.sha }}","mono_ref":"${{ github.ref }}"}}' \
"${{ github.server_url }}/api/v1/repos/C9S/coopenomics/actions/workflows/publish-docs.yaml/dispatches"
# ============================================================================
# trigger-mono-docs — деплой ВТОРОЙ документации (сайт доков mono) через
# webhook DOCS_DEPLOY_WEBHOOK_URL. Отдельная от coopenomics публикация —
# обе доки уезжают синхронно по релизу. Извлечён из бывшего publish-docs:
# gh-pages на github.com выпилен как мёртвый, остался реальный deploy-webhook
# (приёмник деплоит доки на своей стороне). Гейт: не-alpha + ветка main.
# ============================================================================
trigger-mono-docs:
needs: release
if: ${{ !contains(github.ref, '-alpha') && needs.release.outputs.branch == 'main' }}
runs-on: ubuntu-latest
steps:
- name: Trigger docs deployment webhook
run: |
curl -fsSL -X POST "${{ vars.DOCS_DEPLOY_WEBHOOK_URL }}" \
-H 'Content-Type: application/json' \
-d '{"ref":"${{ github.ref }}","sha":"${{ github.sha }}","branch":"${{ github.ref_name }}"}'
+19 -15
View File
@@ -13,11 +13,11 @@ name: Typecheck
#
# Триггеры:
# - pull_request на dev — гейт перед мерджем в основную ветку;
# - workflow_call — переиспользуется из release.yaml как `needs:` у release-job,
# чтобы битый тэг не уехал в DockerHub.
# - workflow_call — оставлен для переиспользования, но release.yaml его
# больше НЕ вызывает (гейт из релиза убран как избыточный — типы
# проверяются на PR в dev до того, как код доедет до тэга).
#
# Push в dev/testnet/main НЕ триггерит — намеренно (PR-гейт достаточен,
# а тэги покрывает workflow_call из release.yaml).
# Push в dev/testnet/main НЕ триггерит — намеренно (PR-гейт на dev достаточен).
on:
pull_request:
@@ -42,13 +42,15 @@ jobs:
- name: Install workspace
run: pnpm install --frozen-lockfile
# cooptypes/factory/sdk собираются здесь, потому что desktop
# импортирует типы из @coopenomics/sdk (zeus-генерация поверх
# controller GraphQL-схемы), а sdk в свою очередь зависит от
# cooptypes. Без `unbuild` dist/ пусто — vue-tsc упадёт на
# «Cannot find module '@coopenomics/sdk'».
- name: Build type dependencies (cooptypes → factory → sdk)
run: pnpm lerna run build --scope cooptypes --scope @coopenomics/factory --scope @coopenomics/sdk
# Полный lerna build по всему workspace — как в корневом Dockerfile.
# Не --scope, потому что workspace-пакеты unbuild-типа (sdk, inter,
# notifications, factory, cooptypes, …) экспортируют типы только из
# сгенерированного dist/. Без dist/ tsc/vue-tsc валятся на TS2307
# «Cannot find module». Локально это маскируется ts-node-ом, но в
# CI пути резолвятся через package.json → types → dist/.
# См. Graphiti / run #148 root cause 2026-05-23.
- name: Build all workspace packages
run: pnpm lerna run build
# quasar prepare генерирует .quasar/* (auto-imports d.ts, типы для
# quasar плагинов). Без него vue-tsc ругается на отсутствующие
@@ -83,10 +85,12 @@ jobs:
- name: Install workspace
run: pnpm install --frozen-lockfile
# controller импортирует cooptypes и factory. Без build dist/
# типы не резолвятся — TS2307 на каждом import.
- name: Build type dependencies (cooptypesfactory)
run: pnpm lerna run build --scope cooptypes --scope @coopenomics/factory
# Полный lerna build по всему workspace — controller импортирует
# @coopenomics/sdk, @coopenomics/inter, @coopenomics/notifications
# помимо cooptypes/factory; их dist/ генерируется unbuild и без
# этого шага tsc валится на 37 TS2307. См. root cause выше.
- name: Build all workspace packages
run: pnpm lerna run build
- name: tsc --noEmit (controller)
run: pnpm --filter @coopenomics/controller run typecheck
+1
View File
@@ -1,4 +1,5 @@
node_modules/
node_modules
lerna-debug.log
components/controller/graph.png
blockchain-data/
+6
View File
@@ -1 +1,7 @@
store-dir=./.pnpm-store
supported-architectures.os[]=darwin
supported-architectures.os[]=linux
supported-architectures.cpu[]=x64
supported-architectures.cpu[]=arm64
supported-architectures.libc[]=glibc
+25
View File
@@ -214,6 +214,31 @@ await sendPOST('/v1/graphql', { query: QUERY, variables });
Канон — `features/Branch/CreateBranch/{api,model}/index.ts`: тип в model `export type IXxxInput = Mutations.X.Y.IInput['data']`; функция в api `function (data: IXxxInput) { ... variables: { data } }`. Не делать `as` cast'ов.
## ⚠️ ДИЗАЙН-КАНОН desktop — ОБЯЗАТЕЛЕН К ИСПОЛЬЗОВАНИЮ
**ПРИ ЛЮБОЙ ВЁРСТКЕ В `components/desktop/` НЕ ВЫДУМЫВАТЬ СТИЛИ.** Всегда сначала свериться с каноном. Не строй гипотез о цветах/радиусах/типографике/паттернах из памяти — иди и читай.
**Два источника правды (именно в этом порядке):**
1. **HTML-канон дизайн-системы** — единственный SoT для визуала (цвета, токены, радиусы, тени, типографика, патерны компонентов):
`/home/admin/blago/production/shared/MONO Design System.html`
Это самодостаточный HTML с всеми CSS-токенами `--p-*`, образцами компонентов (`.base-button`, `.base-card`, `.toast`, `.chip`, `.banner` и т.д.), цветовой палитрой и demo-блоками. **При сомнении в каноне — открыть этот файл и читать.** Не путать с `auth-prototype/index.html`, не путать с `ux-design-specification.md` — они НЕ канон.
2. **Живая реализация канона** в самом репозитории — `/_dev/ui` страница:
`components/desktop/src/pages/_dev/ui/index.vue`
Это уже заверстанная по канону witness-страница со всеми base-/widget-компонентами в действии (BaseButton, BaseInput, BaseCard, BaseChip, EmptyState, Toasts, AppHeader/AppDrawer фрагменты, WalletCard, IdentityPanel, DocumentRow, SignatureCard, ActivityTimeline, KpiCard, VerticalStepper и т.д.). **Когда делаешь новый компонент — сначала посмотри как уже сверстаны соседние в этой странице, используй те же токены / классы / структуру.** Эту страницу можно открыть в браузере (`/_dev/ui` в dev-сборке) — она же служит и для визуальной верификации.
**Запреты:**
- Никаких magic-цветов в hex/rgb прямо в `<style>` Vue-файла — только токены `var(--p-*)` (см. `components/desktop/src/css/mono-platform/*.css`).
- Никаких локальных переопределений `.q-btn`/`.q-card`/`.q-notification` в feature-файлах — все Quasar-overrides централизованно в `components/desktop/src/css/mono-platform/quasar-canon.css`.
- Никаких ад-хок размеров — отступы/радиусы/typescale из токенов (`--p-1`..`--p-12`, `--p-r-sm/md/lg`, `--p-fs-h1..body-sm`).
**Кейс 2026-05-28:** после компакта забыл, что shared/MONO Design System.html — основной SoT, и нашёл «канон» в auth-prototype, который НЕ канон; перевёрстал тосты под нейтральный фон вместо тёмного, пришлось переделывать.
## Frontend desktop — English имена
В `components/desktop/` и любом Vue/TS frontend коде **все имена идентификаторов — английские**:
+139
View File
@@ -0,0 +1,139 @@
# Запуск mono dev-стека на macOS
Полная пошаговая инструкция, как поднять backend (`coopback`) + parser (`cooparser`) + блокчейн-ноду + БД на macOS через Docker Desktop. Прошёл — отметь, ниже разобраны типичные грабли.
## TL;DR
```bash
cd ~/dacom-code/foundation/monocoop
docker compose up -d # 1. поднять базу
./scripts/dev-setup-macos.sh # 2. одной командой (см. ниже скрипт)
```
Если скрипта ещё нет — выполняй шаги ниже руками.
## Шаги
### 1. Поднять контейнеры
```bash
docker compose up -d
```
Поднимутся: `node` (NodeOS), `mongo`, `monoredis`, `postgres`, `coopback`, `cooparser`. **MinIO** входит в дефолт. **OpenSearch** — нет (тяжёлый, см. опц. сервисы).
### 2. Проверить чтоб .env'ы указывали на service-имена, а не localhost
`components/controller/.env`:
- `MONGODB_URL=mongodb://mongo:27017/cooperative-x`
- `REDIS_HOST=monoredis`
- `POSTGRES_HOST=postgres`, `POSTGRES_PASSWORD=postgres!23!23`
- `BLOCKCHAIN_RPC=http://node:8888`
- `SIMPLE_EXPLORER_API=http://cooparser:4000`
- `MINIO_ENDPOINT=minio:9000`
- `CHAIN_ID=<реальный chain_id, см. ниже>`
`components/parser/.env`:
- `MONGO_EXPLORER_URI=mongodb://mongo:27017/cooperative-x`
- `REDIS_HOST=monoredis`
- `API=http://node:8888`
- `SHIP=ws://node:8080`
**Почему service-имена:** все контейнеры в bridge-сети `monocoop_default`. Внутри контейнера `127.0.0.1` = сам контейнер, не host. На macOS `network_mode: host` в Docker Desktop работает плохо — используем bridge + service names.
### 3. Узнать живой chain_id
```bash
curl -s http://localhost:8888/v1/chain/get_info | jq -r .chain_id
```
И вписать в `components/controller/.env``CHAIN_ID=...`. **Если chain_id в .env не совпадает с живым — on-chain транзакции упадут на verify, хотя приложение запустится.**
### 4. Если правил .env — пересоздать контейнер (не restart!)
```bash
docker compose up -d --force-recreate --no-deps coopback cooparser
```
`docker restart` **не перечитывает** `env_file`. `--no-deps` — чтобы не пересоздавать БД (потеряются данные).
### 5. Native binding libxmljs2 — пересборка под Linux
На свежем чекауте `pnpm install` запускается на macOS, и `libxmljs2` собирает Mach-O бинарник. В Linux-контейнере он падает:
```
Error: .../xmljs.node: invalid ELF header ERR_DLOPEN_FAILED
```
**Фикс** (один раз после `pnpm install`):
```bash
docker exec monocoop-coopback-1 sh -c '
cd /app/node_modules/.pnpm/libxmljs2@0.37.0/node_modules/libxmljs2 \
&& rm -rf build \
&& PATH="$PWD/node_modules/.bin:$PATH" npm run install
'
docker restart monocoop-coopback-1
```
Проверка — magic bytes Linux ELF:
```bash
docker exec monocoop-coopback-1 sh -c \
'head -c 4 /app/node_modules/.pnpm/libxmljs2@0.37.0/node_modules/libxmljs2/build/Release/xmljs.node | od -c -N 4'
# Должно быть: 177 E L F
# Если cf fa ed fe — это macOS Mach-O, фикс не применился.
```
### 6. Дождаться cold-start coopback (~56 минут)
`ts-node` без cache + DI tree NestJS = долго. Первые логи появляются спустя ~5 минут после старта контейнера.
```bash
docker logs -f monocoop-coopback-1 | grep -E 'NestJS app|Nest application|Error'
```
Готово, когда видишь:
```
Nest application successfully started
NestJS app with Express routes running on port 2998
```
Проверка:
```bash
curl -s -X POST -H 'Content-Type: application/json' \
-d '{"query":"{__typename}"}' http://localhost:2998/v1/graphql
# {"data":{"__typename":"Query"}}
```
## Опциональные сервисы
### OpenSearch (поиск)
```bash
docker compose --profile search up -d opensearch
```
Затем в `components/controller/.env`:
```
OPENSEARCH_ENABLED=true
```
И пересоздать coopback:
```bash
docker compose up -d --force-recreate --no-deps coopback
```
## Грабли (если опять провозился пол-дня)
| Симптом | Причина | Фикс |
|---|---|---|
| `invalid ELF header` | host'овый `pnpm install` положил Mach-O | См. п. 5 — пересборка libxmljs2 в контейнере |
| `MongooseServerSelectionError: ECONNREFUSED 127.0.0.1:27017` | .env на localhost вместо service-имени | См. п. 2 |
| `password authentication failed for user "postgres"` | в .env `POSTGRES_PASSWORD=postgres` вместо `postgres!23!23` | См. п. 2 |
| Coopback ушёл в restart-loop, но рядом есть другой `monocoop-coopback-1` Up | Случайно запустился старый `components/controller/docker-compose.yml` (network_mode: host, без bind-mount, без CMD) | `docker stop coopback && docker rm coopback` |
| `docker restart` не подхватил новые переменные .env | `restart` не перечитывает `env_file` | `docker compose up -d --force-recreate --no-deps <svc>` |
| Coopback зависает на 5+ минут, лога нет | Это нормально для cold-start ts-node | Подождать. CPU должен крутиться ≥50%. Если CPU = 0% — другая проблема |
| `[VaultDomainService] Ошибка при получении WIF... bad decrypt` | SERVER_SECRET не совпадает с тем, чем шифровали WIF в БД | См. CLAUDE.md → раздел Vault & SERVER_SECRET. Дефолт — `SECRET` |
| Транзакции падают на on-chain verify | CHAIN_ID в .env не совпадает с живым | См. п. 3 |
| `Custom endpoint \`minio://9000\` was not a valid URI` на bootstrap | `MINIO_ENDPOINT` без схемы | `MINIO_ENDPOINT=http://minio:9000` (со схемой!) |
| nodeos в restart-loop с `Database dirty flag set` после sleep/crash Docker Desktop | unclean shutdown chain state | `docker stop monocoop-node-1`; `mv blockchain-data/state-history{,.broken-$(date +%s)}`; `docker compose up -d node` — nodeos сам сделает replay |
| Cold-start coopback 10+ минут (вместо обычных 3-5) | ts-node без cache + bind-mount через osxfs + параллельная нагрузка (nodeos replay) | Подождать; в будущем — `tsx` / `ts-node --swc` для dev-режима |
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@coopenomics/blago-cli",
"version": "2026.5.23-4",
"version": "2026.5.27-2",
"description": "CLI синхронизации артефактов Благорост с бэкендом через @coopenomics/sdk",
"type": "module",
"private": true,
@@ -9,12 +9,15 @@ export interface CommunicationCursorsFile {
messageLastTsByRoom: Record<string, number>
/** project_hash → ISO instant: транскрипции с endedAt ≤ этого момента уже выгружены */
transcriptionLastEndedExclusiveByProject: Record<string, string>
/** matrixRoomId → ISO instant: транскрипции непроектной комнаты с endedAt ≤ этого момента уже выгружены */
transcriptionLastEndedExclusiveByRoom: Record<string, string>
}
function empty(): CommunicationCursorsFile {
return {
messageLastTsByRoom: {},
transcriptionLastEndedExclusiveByProject: {},
transcriptionLastEndedExclusiveByRoom: {},
}
}
@@ -32,6 +35,11 @@ export async function loadCommunicationCursors(root: string): Promise<Communicat
&& typeof parsed.transcriptionLastEndedExclusiveByProject === 'object'
? { ...parsed.transcriptionLastEndedExclusiveByProject }
: {},
transcriptionLastEndedExclusiveByRoom:
parsed.transcriptionLastEndedExclusiveByRoom !== undefined
&& typeof parsed.transcriptionLastEndedExclusiveByRoom === 'object'
? { ...parsed.transcriptionLastEndedExclusiveByRoom }
: {},
}
}
catch {
@@ -43,7 +43,6 @@ export function projectCommunicationDayToMarkdown(
}
export interface TranscriptionHeaderModel {
matrixRoomId: string
roomId: string
startedAt: Date | string
endedAt: Date | string | null | undefined
@@ -71,7 +70,6 @@ export function renderCallTranscriptionMarkdown(
const header = [
`# Транскрипция звонка`,
``,
`- Matrix room: \`${transcription.matrixRoomId}\``,
`- LiveKit room: \`${transcription.roomId}\``,
`- Начало: ${start.toISOString()}`,
`- Окончание: ${end ? end.toISOString() : '—'}`,
@@ -25,7 +25,7 @@ import {
transcriptionMeetingFileStemUtc,
type CommunicationDayLine,
} from './communication-markdown.js'
import { workspaceBasePath, type ProjectPathModel } from './layout.js'
import { generateSlug, workspaceBasePath, type ProjectPathModel } from './layout.js'
import { syncEntityFile } from './sync-entity-file.js'
interface ProjectRowLite {
@@ -183,6 +183,19 @@ async function listRooms(ctx: AuthenticatedContext, projectHash: string) {
return q[Queries.ChatCoop.ListProjectCommunicationRooms.name] ?? []
}
/** Стабильная папка непроектной комнаты в `rooms/`. Системные — фиксированные, комнаты секретаря — slug + хвост id (уникальность). */
function nonProjectRoomFolder(kind: string, matrixRoomId: string, displayLabel: string): string {
if (kind === 'MEMBERS') {
return 'komnata-paishchikov'
}
if (kind === 'COUNCIL') {
return 'komnata-soveta'
}
const slug = generateSlug(displayLabel) || 'komnata'
const shortId = createHash('sha256').update(matrixRoomId, 'utf8').digest('hex').slice(0, 6)
return `${slug}-${shortId}`
}
export async function pullProjectCommunicationArtifacts(
ctx: AuthenticatedContext,
index: IndexFile,
@@ -198,6 +211,7 @@ export async function pullProjectCommunicationArtifacts(
cursors = {
messageLastTsByRoom: {},
transcriptionLastEndedExclusiveByProject: {},
transcriptionLastEndedExclusiveByRoom: {},
}
}
@@ -356,7 +370,6 @@ export async function pullProjectCommunicationArtifacts(
= dateFromUnknown(tr.endedAt) ?? (tr.endedAt as Date | string | null | undefined)
const md = renderCallTranscriptionMarkdown(
{
matrixRoomId: String(tr.matrixRoomId),
roomId: String(tr.roomId),
startedAt,
endedAt: endedAtTr,
@@ -422,3 +435,207 @@ export async function pullProjectCommunicationArtifacts(
warn(`Не удалось сохранить курсоры переписки: ${formatThrownValue(e)}`)
}
}
/**
* Pull переписки и транскрипций из комнат ВНЕ проектов Capital (пайщики, совет, комнаты секретаря).
* Раскладка — отдельная верхняя папка `rooms/<folder>/{messages,meetings}/`, чтобы не смешивать с
* проектными `meetings/`. Логика идентична проектной, но bucket = одна комната, курсор транскрипций — по matrixRoomId.
*/
export async function pullNonProjectCommunicationArtifacts(
ctx: AuthenticatedContext,
index: IndexFile,
): Promise<void> {
let rooms: { matrixRoomId: string, displayLabel: string, kind: string }[]
try {
const q = await ctx.client.Query(Queries.ChatCoop.ListNonProjectCommunicationRooms.query, {})
rooms = (q[Queries.ChatCoop.ListNonProjectCommunicationRooms.name] ?? []) as typeof rooms
}
catch (e) {
warn(`Список непроектных комнат (chatcoopListNonProjectCommunicationRooms): ${formatThrownValue(e)}`)
return
}
if (rooms.length === 0) {
return
}
let cursors: CommunicationCursorsFile
try {
cursors = await loadCommunicationCursors(ctx.root)
}
catch (e) {
warn(`Курсоры переписки (комнаты): не удалось прочитать, начинаем с пустых: ${formatThrownValue(e)}`)
cursors = {
messageLastTsByRoom: {},
transcriptionLastEndedExclusiveByProject: {},
transcriptionLastEndedExclusiveByRoom: {},
}
}
for (const room of rooms) {
const folder = nonProjectRoomFolder(room.kind, room.matrixRoomId, room.displayLabel)
const basePath = `rooms/${folder}`
const roomTitle = room.displayLabel || room.matrixRoomId
// Сообщения комнаты — по календарным суткам UTC новее курсора.
try {
const last = cursors.messageLastTsByRoom[room.matrixRoomId]
const afterTs = last ?? 0
const datesQ = await ctx.client.Query(Queries.ChatCoop.ListUtcDatesWithNewRoomMessages.query, {
variables: { data: { matrixRoomId: room.matrixRoomId, afterOriginServerTsExclusive: afterTs } },
})
const dates = (datesQ[Queries.ChatCoop.ListUtcDatesWithNewRoomMessages.name] ?? []).sort()
for (const utcDate of dates) {
const mq = await ctx.client.Query(Queries.ChatCoop.GetRoomMessagesForUtcDate.query, {
variables: { data: { matrixRoomId: room.matrixRoomId, utcDate } },
})
const linesRaw = mq[Queries.ChatCoop.GetRoomMessagesForUtcDate.name] ?? []
const lines: CommunicationDayLine[] = linesRaw.map(m => ({
originServerTs: m.originServerTs,
authorLabel: m.authorLabel,
coopUsername: m.coopUsername,
kind: String(m.kind),
bodyText: m.bodyText,
}))
if (lines.length === 0) {
continue
}
const content = projectCommunicationDayToMarkdown(roomTitle, room.matrixRoomId, utcDate, [
{ displayLabel: room.displayLabel, matrixRoomId: room.matrixRoomId, lines },
])
const rel = `${basePath}/messages/${utcDate}.md`
const entityHash = messageDayEntityHash(room.matrixRoomId, utcDate)
await syncEntityFile({
root: ctx.root,
index,
entityType: 'room_message_day',
entityHash,
relativePath: rel,
content,
remoteUpdatedAt: `${utcDate}T23:59:59.999Z`,
label: `переписка ${utcDate} (${room.matrixRoomId})`,
})
}
const maxQ = await ctx.client.Query(Queries.ChatCoop.GetMaxOriginServerTsForRoom.query, {
variables: { data: { matrixRoomId: room.matrixRoomId } },
})
const maxTs = maxQ[Queries.ChatCoop.GetMaxOriginServerTsForRoom.name] as number | null | undefined
if (maxTs !== undefined && maxTs !== null && Number.isFinite(maxTs)) {
cursors.messageLastTsByRoom[room.matrixRoomId] = maxTs
}
}
catch (e) {
warn(`Переписка Matrix, комната ${room.matrixRoomId} (${roomTitle}): ${formatThrownValue(e)}`)
}
// Транскрипции звонков комнаты + sibling memo.
try {
const tExIso = cursors.transcriptionLastEndedExclusiveByRoom[room.matrixRoomId]
const lowerBoundExclusive = tExIso === undefined ? new Date(0) : new Date(tExIso)
interface TranscriptionCandidate {
id: string
endedAt: Date
memo: string
updatedAt: Date | undefined
}
const byId = new Map<string, TranscriptionCandidate>()
const tq = await ctx.client.Query(Queries.ChatCoop.GetTranscriptions.query, {
variables: { data: { matrixRoomId: room.matrixRoomId, limit: CHATCOOP_TRANSCRIPTIONS_QUERY_LIMIT, offset: 0 } },
})
const list = tq[Queries.ChatCoop.GetTranscriptions.name] ?? []
for (const t of list) {
const end = dateFromUnknown(t.endedAt)
if (t.status !== Zeus.TranscriptionStatus.COMPLETED || !end) {
continue
}
const prev = byId.get(t.id)
if (!prev || end > prev.endedAt) {
byId.set(t.id, {
id: t.id,
endedAt: end,
memo: typeof t.memo === 'string' ? t.memo : '',
updatedAt: dateFromUnknown(t.updatedAt),
})
}
}
const allCompleted = [...byId.values()].sort((a, b) => a.endedAt.getTime() - b.endedAt.getTime())
const newMeetings = allCompleted.filter(c => c.endedAt.getTime() > lowerBoundExclusive.getTime())
let maxEnded: Date | null = null
for (const c of newMeetings) {
const packQ = await ctx.client.Query(Queries.ChatCoop.GetTranscription.query, {
variables: { data: { id: c.id } },
})
const pack = packQ[Queries.ChatCoop.GetTranscription.name]
if (!pack?.transcription || pack.transcription.status !== Zeus.TranscriptionStatus.COMPLETED) {
continue
}
const tr = pack.transcription
const startedAt: Date | string = dateFromUnknown(tr.startedAt) ?? (tr.startedAt as Date | string)
const endedAtTr: Date | string | null | undefined
= dateFromUnknown(tr.endedAt) ?? (tr.endedAt as Date | string | null | undefined)
const md = renderCallTranscriptionMarkdown(
{
roomId: String(tr.roomId),
startedAt,
endedAt: endedAtTr,
},
pack.segments.map(s => ({
speakerName: s.speakerName,
text: s.text,
startOffset: s.startOffset,
endOffset: s.endOffset,
})),
)
const stem = transcriptionMeetingFileStemUtc(c.endedAt)
const rel = `${basePath}/meetings/${stem}.md`
const entityHash = c.id.toLowerCase()
await syncEntityFile({
root: ctx.root,
index,
entityType: 'call_transcription',
entityHash,
relativePath: rel,
content: md,
remoteUpdatedAt: toUpdatedIso(c.endedAt),
label: `транскрипция ${c.id}`,
})
if (!maxEnded || c.endedAt > maxEnded) {
maxEnded = c.endedAt
}
}
for (const c of allCompleted) {
const stem = transcriptionMeetingFileStemUtc(c.endedAt)
const memoRel = `${basePath}/meetings/${stem}.memo.md`
const memoRemoteUpdatedAt = toUpdatedIso(c.updatedAt ?? c.endedAt)
await syncTranscriptionMemoFile({
root: ctx.root,
index,
transcriptionId: c.id,
relativePath: memoRel,
serverMemo: c.memo,
remoteUpdatedAtIso: memoRemoteUpdatedAt,
})
}
if (maxEnded) {
cursors.transcriptionLastEndedExclusiveByRoom[room.matrixRoomId] = maxEnded.toISOString()
}
else if (tExIso === undefined) {
cursors.transcriptionLastEndedExclusiveByRoom[room.matrixRoomId] = new Date().toISOString()
}
}
catch (e) {
warn(`Транскрипции звонков, комната ${room.matrixRoomId}: ${formatThrownValue(e)}`)
}
}
try {
await saveCommunicationCursors(ctx.root, cursors)
}
catch (e) {
warn(`Не удалось сохранить курсоры переписки (комнаты): ${formatThrownValue(e)}`)
}
}
+3 -1
View File
@@ -28,7 +28,7 @@ import {
storyFileRelativePath,
workspaceBasePath,
} from './layout.js'
import { pullProjectCommunicationArtifacts } from './pull-communication.js'
import { pullNonProjectCommunicationArtifacts, pullProjectCommunicationArtifacts } from './pull-communication.js'
import { scaffoldBmadWorkspacesAfterPull } from './scaffold-bmad-workspace.js'
import { syncEntityFile } from './sync-entity-file.js'
import { writeWorkspaceIndexMarkdown } from './workspace-index.js'
@@ -329,6 +329,8 @@ export async function runPull(ctx: AuthenticatedContext, options: RunPullOptions
await pullProjectCommunicationArtifacts(ctx, index, allProjects, projectByHash)
await pullNonProjectCommunicationArtifacts(ctx, index)
await scaffoldBmadWorkspacesAfterPull(ctx, allProjects, projectByHash)
await saveIndex(ctx.root, index)
-2
View File
@@ -367,7 +367,6 @@ export async function runRestore(ctx: AuthenticatedContext, userPath: string): P
if (entry.entity_type === 'call_transcription') {
interface TranscriptionRestorePack {
transcription: {
matrixRoomId: string
roomId: string
startedAt: Date | string
endedAt: Date | string | null | undefined
@@ -394,7 +393,6 @@ export async function runRestore(ctx: AuthenticatedContext, userPath: string): P
}
const content = renderCallTranscriptionMarkdown(
{
matrixRoomId: tr.matrixRoomId,
roomId: tr.roomId,
startedAt: tr.startedAt,
endedAt: tr.endedAt,
@@ -4,11 +4,34 @@ import type { BlagoEntityType, IndexFile } from './index-store.js'
import * as fs from 'node:fs/promises'
import * as path from 'node:path'
import { parseBlagoMarkdown, serializeBlagoMarkdown } from '../format/index.js'
import { sha256Hex } from '../lib/hash.js'
import { warn } from '../ui/output.js'
import { findByHash, normalizeRelativePath, upsertEntry } from './index-store.js'
/**
* Канонический вид для сравнения «есть ли реальный конфликт», без волатильных
* серверных меток времени. updated_at/created_at сервер бьёт при дочерних мутациях
* (создание/удаление issue/story у родителя), и они попадают и во frontmatter, и в
* content_etag_local. Из-за этого raw-sha файла расходится с etag, локальный файл
* считается «грязным», а pull при изменившемся remote_updated_at пишет маркеры слияния —
* хотя содержательно текст совпадает. Сравнение в каноне (без updated_at/created_at)
* отличает настоящую правку от чистого bump'а времени.
*/
function canonicalForCompare(raw: string): string {
try {
const parsed = parseBlagoMarkdown(raw)
const data = { ...parsed.data }
delete data.updated_at
delete data.created_at
return serializeBlagoMarkdown(data, parsed.body)
}
catch {
return raw
}
}
async function ensureDirForFile(absFile: string): Promise<void> {
await fs.mkdir(path.dirname(absFile), { recursive: true })
}
@@ -139,6 +162,21 @@ export async function syncEntityFile(params: {
const dirty
= current !== null && current !== undefined && sha256Hex(current) !== prev.content_etag_local
if (dirty && remoteUpdatedAt !== prev.remote_updated_at) {
// Реальный конфликт — только если содержимое расходится вне волатильных меток времени.
// Если локальный и серверный тексты совпадают по канону (отличие лишь в updated_at/created_at),
// это не конфликт: принимаем серверную версию и лечим etag, без маркеров слияния.
if (current !== null && current !== undefined && canonicalForCompare(current) === canonicalForCompare(content)) {
await ensureDirForFile(absNew)
await fs.writeFile(absNew, content, 'utf8')
upsertEntry(index, {
entity_type: entityType,
entity_hash: entityHash,
relative_path: rel,
remote_updated_at: remoteUpdatedAt,
content_etag_local: sha256Hex(await fs.readFile(absNew, 'utf8')),
})
return
}
const merged = wrapMergeConflictMarkers(current ?? '', content)
await ensureDirForFile(absNew)
await fs.writeFile(absNew, merged, 'utf8')
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@coopenomics/boot",
"type": "module",
"version": "2026.5.23-4",
"version": "2026.5.27-2",
"private": true,
"packageManager": "pnpm@9.0.6",
"description": "CLI-утилита инициализации блокчейна и кооператива",
+22 -13
View File
@@ -1,6 +1,7 @@
#!/bin/bash
# Загружаем per-instance конфиг из корня репо.
# Загружаем per-instance конфиг из корня репо (для CHAIN_URL/MONGODB_URL/API_URL,
# которые читают TS-код boot и шелл-скрипты networks.sh/preactivate.sh).
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
if [ -f "$ROOT_DIR/.env" ]; then
set -a
@@ -9,26 +10,34 @@ if [ -f "$ROOT_DIR/.env" ]; then
set +a
fi
# Останавливаем и удаляем контейнеры вместе с volumes
echo "Останавливаем и удаляем контейнеры с volumes..."
docker compose down -v mongo postgres cooparser || true
# Останавливаем и удаляем контейнеры (сеть НЕ трогаем — к ней может быть
# подключён внешний контейнер вроде provider-backend; down её снёс бы с ошибкой).
echo "Останавливаем и удаляем контейнеры..."
docker compose rm -fsv mongo postgres monoredis cooparser coopback || true
# Останавливаем blockchain контейнер перед удалением данных
echo "Останавливаем blockchain контейнер..."
docker compose stop node || true
# Удаляем blockchain data
# Удаляем тома баз данных для чистого старта (named volumes rm -v не чистит)
echo "Удаляем тома баз данных..."
PROJECT="${COMPOSE_PROJECT_NAME:-$(basename "$ROOT_DIR")}"
docker volume rm "${PROJECT}_postgres_data" "${PROJECT}_mongo_data" 2>/dev/null || true
# Удаляем blockchain data через контейнер (alpine под root стирает данные
# независимо от владельца — без sudo на любой ноде; на проде nodeos пишет под root).
echo "Удаляем blockchain data..."
# sudo chmod -R 755 ../blockchain-data/ 2>/dev/null || true
sudo rm -rf ../blockchain-data/
docker run --rm -v "$(cd .. && pwd)/blockchain-data:/d" alpine sh -c 'rm -rf /d/* /d/.[!.]* 2>/dev/null || true'
# Пересоздаем и запускаем базы данных
# Пересоздаем и запускаем базы данных + Redis (monoredis).
# monoredis ОБЯЗАТЕЛЕН: без него coopback падает на старте с
# `getaddrinfo EAI_AGAIN monoredis` → MaxRetriesPerRequestError → nodemon crash.
echo "Пересоздаем и запускаем базы данных..."
docker compose up -d mongo postgres
docker compose up -d mongo postgres monoredis
# Ждем готовности MongoDB
# Ждем готовности MongoDB (standalone, ping вместо ожидания PRIMARY).
echo "Ждем готовности MongoDB..."
until docker compose exec -T mongo mongosh --eval "db.adminCommand('ping')" --quiet > /dev/null 2>&1; do
until docker compose exec -T mongo mongosh --quiet --eval "db.adminCommand({ping:1}).ok" > /dev/null 2>&1; do
echo "MongoDB еще не готов, ждем..."
sleep 2
done
@@ -42,7 +51,7 @@ until docker compose exec -T postgres pg_isready -U postgres -d voskhod > /dev/n
done
echo "PostgreSQL готов!"
# Запускаем boot процесс
# Запускаем boot процесс (clean: только программы Благорост/маркетплейс)
echo "Запускаем boot процесс..."
pnpm run boot:clean
@@ -51,6 +60,6 @@ echo "Запускаем parser..."
docker compose up -d cooparser
echo "Запускаем контроллер..."
docker compose restart coopback || true
docker compose up -d --force-recreate coopback || true
echo "Перезапуск завершен!"
+17 -14
View File
@@ -1,6 +1,7 @@
#!/bin/bash
# Загружаем per-instance конфиг из корня репо.
# Загружаем per-instance конфиг из корня репо (для CHAIN_URL/MONGODB_URL/API_URL,
# которые читают TS-код boot и шелл-скрипты networks.sh/preactivate.sh).
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
if [ -f "$ROOT_DIR/.env" ]; then
set -a
@@ -9,30 +10,32 @@ if [ -f "$ROOT_DIR/.env" ]; then
set +a
fi
# Останавливаем контроллер перед очисткой данных
echo "Останавливаем контроллер..."
docker compose down coopback || true
# Останавливаем и удаляем контейнеры вместе с volumes
echo "Останавливаем и удаляем контейнеры с volumes..."
docker compose down -v mongo postgres cooparser || true
docker compose down -v mongo postgres monoredis cooparser coopback || true
# Останавливаем blockchain контейнер перед удалением данных
echo "Останавливаем blockchain контейнер..."
docker compose stop node || true
# Удаляем blockchain data
# Удаляем blockchain data.
# Контейнерный wipe (alpine под root) стирает данные независимо от их владельца —
# без sudo на любой ноде (на Pi нет passwordless sudo; на проде nodeos пишет
# данные под root). Единый способ с reboot.sh / clean_reboot.sh.
echo "Удаляем blockchain data..."
# sudo chmod -R 755 ../blockchain-data/ 2>/dev/null || true
sudo rm -rf ../blockchain-data/
docker run --rm -v "$(cd .. && pwd)/blockchain-data:/d" alpine sh -c 'rm -rf /d/* /d/.[!.]* 2>/dev/null || true'
# Пересоздаем и запускаем базы данных
# Пересоздаем и запускаем базы данных + Redis (monoredis).
# monoredis ОБЯЗАТЕЛЕН: без него coopback падает на старте с
# `getaddrinfo EAI_AGAIN monoredis` → MaxRetriesPerRequestError → nodemon crash,
# и провайдер не может взять org-данные partner1 (PROVIDER_URL=coopback:2998).
echo "Пересоздаем и запускаем базы данных..."
docker compose up -d mongo postgres
docker compose up -d mongo postgres monoredis
# Ждем готовности MongoDB
# Ждем готовности MongoDB (standalone, ping вместо ожидания PRIMARY).
echo "Ждем готовности MongoDB..."
until docker compose exec -T mongo mongosh --eval "db.adminCommand('ping')" --quiet > /dev/null 2>&1; do
until docker compose exec -T mongo mongosh --quiet --eval "db.adminCommand({ping:1}).ok" > /dev/null 2>&1; do
echo "MongoDB еще не готов, ждем..."
sleep 2
done
@@ -46,7 +49,7 @@ until docker compose exec -T postgres pg_isready -U postgres -d voskhod > /dev/n
done
echo "PostgreSQL готов!"
# Запускаем boot процесс
# Запускаем boot процесс (расширенный: совет + пайщики; partner1 — при EXTRA_RENT=1)
echo "Запускаем boot процесс..."
pnpm run boot:extra
@@ -55,7 +58,7 @@ echo "Запускаем parser..."
docker compose up -d cooparser
echo "Запускаем контроллер..."
docker compose up -d coopback
docker compose up -d --force-recreate coopback || true
# Контроллер создаёт marketplace-таблицы через TypeORM synchronize при старте.
# Ждём появления marketplace_ku_details, затем засеваем 3 ПВЗ Подмосковья
+37 -22
View File
@@ -315,31 +315,46 @@ export default class Blockchain {
async activateFeature(feature: Feature) {
await this.update_pass_instance()
await this.api.transact(
{
actions: [
{
account: 'eosio',
name: 'activate',
authorization: [
{
actor: 'eosio',
permission: 'active',
try {
await this.api.transact(
{
actions: [
{
account: 'eosio',
name: 'activate',
authorization: [
{
actor: 'eosio',
permission: 'active',
},
],
data: {
feature_digest: feature.hash,
},
],
data: {
feature_digest: feature.hash,
},
},
],
},
{
blocksBehind: 3,
expireSeconds: 30,
},
)
],
},
{
blocksBehind: 3,
expireSeconds: 30,
},
)
console.log('Фича активирована: ', feature.name)
console.log('Фича активирована: ', feature.name)
}
catch (e: any) {
// На уже забутстрапленном (не обнулённом) чейне протокол-фича уже активна —
// nodeos отдаёт protocol_feature_exception (code 3250000). Это не повод
// ронять весь boot: фича на месте, нужный результат достигнут, продолжаем.
const errName = e?.json?.error?.name
const errCode = e?.json?.error?.code
const msg = e instanceof Error ? e.message : String(e)
if (errName === 'protocol_feature_exception' || errCode === 3250000 || /already activated/i.test(msg)) {
console.warn(`Фича уже активирована, пропускаю: ${feature.name}`)
return
}
throw e
}
}
async createToken(params: TokenContract.Interfaces.ICreate) {
+19 -9
View File
@@ -1,7 +1,6 @@
import config from '../configs'
import { initExtensionsInPostgres, initSystemStatus } from '../postgres-init'
import { installExtraData, installInitialData, startInfra } from './infra'
import { CooperativeClass, startCoop } from './cooperative'
import { startCoop } from './cooperative'
export async function boot() {
const blockchain = await startInfra()
@@ -14,18 +13,29 @@ export async function boot() {
}
export async function bootClean() {
const blockchain = await startInfra()
console.log('Создаём программы (Благорост и маркетплейс)')
const cooperative = new CooperativeClass(blockchain)
await cooperative.createPrograms(config.provider)
// Только инфраструктура: контракты, фичи, токен, системные параметры.
// Ни совета, ни программ — программы (createPrograms) требуют существующий
// совет и создаются в boot/bootExtra внутри installInitialData. В clean их
// не делаем (иначе soviet::createprog падает с «Совет не найден»).
await startInfra()
}
export async function bootExtra() {
const blockchain = await startInfra()
await installInitialData(blockchain, true) // Создать расширенный совет (устанавливает статус 'active' в MongoDB)
await installExtraData(blockchain) // Добавить дополнительных пайщиков
// installExtraData регистрирует partner1 как coop с auto-approve от провайдера —
// это и есть программная on-chain активация, которая триггерит аренду VM в
// провайдере (PENDING→RENT). boot:extra используется НЕ только для аренды
// сервера (например, просто пересев совета/чейна для других задач), поэтому
// провижининг partner1 включается ТОЛЬКО под флагом EXTRA_RENT=1.
if (process.env.EXTRA_RENT === '1') {
console.log('EXTRA_RENT=1 → installExtraData: провижининг partner1 (триггер аренды)')
await installExtraData(blockchain) // Регистрируем partner1 как coop (active)
}
else {
console.log('EXTRA_RENT не задан → пропускаем провижининг partner1 (аренда не запускается)')
}
console.log('Инициализируем статус системы в PostgreSQL')
await initSystemStatus() // Устанавливает статус 'active' в PostgreSQL
+14 -4
View File
@@ -15,6 +15,9 @@ import { sleep } from '../utils'
import { generateRandomSHA256 } from '../utils/randomHash'
import { initUsersInPostgres, initVaultInPostgres } from '../postgres-init'
import { CooperativeClass } from './cooperative'
import { signProgramAgreement } from './sign-program-agreement'
import { fakeDocument } from '../tests/shared/fakeDocument'
import { walletDraftId, walletProgramId } from '../tests/capital/consts'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
@@ -674,10 +677,17 @@ export async function installExtraData(blockchain: Blockchain) {
status: 'active',
})
// Ресурсы для партнёра: powerup CPU/NET. transfer токенов пропускаем —
// eosio.token::transfer падает на проверке membership в wallet program,
// которая для partner1 не настроена. Для provider sync + Hostkey-flow
// токены не нужны: tx от имени partner1 пойдут от soviet/admin.
// Записываем partner1 в ЦПП Кошелька оператора (voskhod, program_id=1).
// БЕЗ этого провайдерский performInitialTransfers (150 AXON на partner1 ДО
// перехода в ACTIVE, см. provider CLAUDE.md §13) падает ассертом
// eosio.token::is_can_transfer «Получатель не является участником целевой
// потребительской программы кошелька» — и инстанс навсегда застревает в
// INSTALL. wallet::signagree (auth voskhod@active) делает partner1 членом
// ЦПП Кошелька. Та же механика, что для обычных пайщиков в participant.ts.
console.log('Подписываем wallet-соглашение за partner1 (членство в ЦПП Кошелька)')
await signProgramAgreement(blockchain, config.provider, account.username, walletProgramId, walletDraftId, fakeDocument)
// Ресурсы для партнёра: powerup CPU/NET.
await blockchain.powerup({
payer: 'eosio',
receiver: account.username,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@coopenomics/cleos",
"version": "2026.5.23-4",
"version": "2026.5.27-2",
"private": true,
"description": "Обёртка над кошельком cleos для EOSIO блокчейна",
"scripts": {
+5 -7
View File
@@ -1,15 +1,13 @@
#!/usr/bin/env bash
# Локальная сборка всех контрактов: оборачивает build_contracts_cdt.sh в
# CDT-образ. Сам cmake/make живёт в build_contracts_cdt.sh — он же
# используется в CI (там CDT ставится из .deb, без docker), поэтому
# флаги сборки в одном месте и не разъезжаются.
mode="${1:-prod}"
if [ "$mode" = "test" ]; then
is_testnet="ON"
else
is_testnet="OFF"
fi
docker run --rm --name cdt \
--volume "$(pwd):/project" \
-w /project \
dicoop/blockchain:latest \
/bin/bash -c "mkdir -p build && cd build && cmake -DBUILD_TARGET= -DTEST_TARGET= -DVERBOSE=ON -DBUILD_TESTS=OFF -DIS_TESTNET=$is_testnet .. && make"
/bin/bash -c "./build_contracts_cdt.sh $mode"
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
# Компиляция всех контрактов кооперативной экономики через CDT.
#
# ВАЖНО: скрипт запускается УЖЕ внутри окружения с установленным CDT —
# тулчейн ожидается по пути /cdt/build/lib/cmake/cdt/CDTWasmToolchain.cmake
# (он захардкожен в CMakeLists.txt). Здесь НЕТ docker — только cmake/make.
#
# Два способа подготовить это окружение:
# - локально: build-all.sh оборачивает скрипт в `docker run dicoop/blockchain:latest`
# (в образе CDT уже лежит в /cdt/build);
# - в CI: CDT ставится из .deb (C9S/cdt), а /cdt/build симлинкуется на
# /usr/opt/cdt/<ver> (см. .github/workflows/release.yaml).
#
# Один источник cmake-флагов для обоих путей — чтобы локальная и CI-сборка
# не разъезжались.
set -euo pipefail
mode="${1:-prod}"
if [ "$mode" = "test" ]; then
is_testnet="ON"
else
is_testnet="OFF"
fi
mkdir -p build
cd build
cmake -DBUILD_TARGET= -DTEST_TARGET= -DVERBOSE=ON -DBUILD_TESTS=OFF -DIS_TESTNET="$is_testnet" ..
make
@@ -72,8 +72,8 @@ void capital::importcontrib(eosio::name coopname, eosio::name username, checksum
auto idx = user_wallets.get_index<"byuserwallet"_n>();
auto preimp_it = idx.find(combine_ids(ledger2_wallets::PREIMP_FUND.value, username.value));
if (preimp_it != idx.end() && preimp_it->available.amount > 0) {
eosio::check(preimp_it->blocked.amount == 0,
"preimp.blocked > 0 не поддерживается при импорте — обратиться в поддержку");
// Поле blocked упразднено (2026-05-24): резерв/блокировка на кошельках
// больше не используется, проверка preimp.blocked == 0 излишня.
Ledger2::apply(
_capital, coopname, operations::capital::DROP_PREIMP,
preimp_it->available, username, contributor_hash,
@@ -9,23 +9,26 @@ namespace Capital::Core {
* @brief Совокупный баланс программы Благорост на уровне кооператива.
*
* Источник — ledger2 L2 (`wallets[BLAGOROST_FUND]`), агрегированный по всем
* пайщикам. Σ L3.{available, blocked}[w.cap.blago] == L2.{available, blocked}
* (Эпик 3 invariant), поэтому L2 — авторитетная сумма для CRPS-знаменателя.
* Если кошелёк ещё не создан (никто не инвестировал) — нулевой баланс.
* пайщикам. Σ L3.available[w.cap.blago] == L2.available (Эпик 3 invariant),
* поэтому L2 — авторитетная сумма для CRPS-знаменателя. Если кошелёк ещё не
* создан (никто не инвестировал) — нулевой баланс.
*
* Поле blocked упразднено (2026-05-24): субсчёт «заблокировано» больше не
* используется, баланс кошелька = available.
*/
eosio::asset get_capital_program_share_balance(eosio::name coopname) {
wallets2_index wallets(_ledger2, coopname.value);
auto it = wallets.find(ledger2_wallets::BLAGOROST_FUND.value);
if (it == wallets.end()) return eosio::asset(0, _root_govern_symbol);
return it->available + it->blocked;
return it->available;
}
/**
* @brief Доля пайщика в Благоросте (available + blocked) из ledger2 L3.
* @brief Доля пайщика в Благоросте (available) из ledger2 L3.
*
* Источник — `userwallets[(w.cap.blago, username)]`. Запись существует
* только пока available + blocked > 0 (Эпик 3 §6: auto-delete на нуле),
* поэтому отсутствие = нулевой баланс.
* только пока available > 0 (Эпик 3 §6: auto-delete на нуле), поэтому
* отсутствие = нулевой баланс. Поле blocked упразднено (2026-05-24).
*/
eosio::asset get_capital_program_user_share_balance(eosio::name coopname, eosio::name username) {
userwallets_index user_wallets(_ledger2, coopname.value);
@@ -33,6 +36,6 @@ namespace Capital::Core {
auto key = combine_ids(ledger2_wallets::BLAGOROST_FUND.value, username.value);
auto it = idx.find(key);
if (it == idx.end()) return eosio::asset(0, _root_govern_symbol);
return it->available + it->blocked;
return it->available;
}
}
@@ -67,12 +67,12 @@ void ledger2::revert(eosio::name coopname,
eosio::check(original_operation_id != 0, "revert: original_operation_id обязателен");
// -------- validate mirror_wallet_op --------
eosio::check(mirror_wallet_op <= 5, "revert: неизвестный mirror_wallet_op");
// Не позволяем откатывать через BLOCK/UNBLOCK — они асимметричны и нет
// адекватного зеркала в одной операции (BLOCK + UNBLOCK — обратные сами по себе).
eosio::check(mirror_wallet_op != static_cast<uint8_t>(WalletOp::BLOCK) &&
mirror_wallet_op != static_cast<uint8_t>(WalletOp::UNBLOCK),
"revert: BLOCK/UNBLOCK не подлежат откату через revert (они симметричны сами себе)");
// Зеркало revert — только TRANSFER (обмен wallet_from/wallet_to) либо BURN
// (зеркало ISSUE: изъятие с wallet_from). Бывшие BLOCK/UNBLOCK упразднены
// (2026-05-24); ISSUE/NONE как зеркало смысла не имеют.
eosio::check(mirror_wallet_op == static_cast<uint8_t>(WalletOp::TRANSFER) ||
mirror_wallet_op == static_cast<uint8_t>(WalletOp::BURN),
"revert: mirror_wallet_op должен быть TRANSFER или BURN");
// -------- validate mirror wallets/accounts --------
if (mirror_wallet_from.value != 0) {
@@ -1,5 +1,5 @@
/**
* @brief Атомарная операция по кошельку (issue/transfer/block/unblock/burn/burn_blocked).
* @brief Атомарная операция по кошельку (issue/transfer/burn).
*
* Внутренний action ledger2 — вызывается только через inline из apply().
* Auth: только сам ledger2 (require_auth(get_self())).
@@ -53,13 +53,15 @@ void ledger2::walletop(eosio::name coopname,
eosio::check(amount.symbol == _root_govern_symbol,
"walletop: некорректный символ валюты");
eosio::check(memo.size() < 256, "walletop: memo > 255");
// op_code = 5 (NONE) намеренно не допускается: NONE-операции — это только
// бухпроводка без кошелькового движения, apply.cpp не диспатчит для них walletop.
// Прямой вызов с op_code=5 был бы no-op и сбил бы инвариант parity.
// Допустимы 0..4 (ISSUE/TRANSFER/BLOCK/UNBLOCK/BURN) и 6 (BURN_BLOCKED).
eosio::check(op_code <= static_cast<uint8_t>(WalletOp::BURN_BLOCKED) &&
op_code != static_cast<uint8_t>(WalletOp::NONE),
"walletop: неизвестный op_code");
// Допустимы только ISSUE(0), TRANSFER(1), BURN(4). NONE(5) — это только
// бухпроводка без кошелькового движения, apply.cpp для неё walletop не диспатчит;
// прямой вызов был бы no-op и сбил бы parity. Бывшие BLOCK(2)/UNBLOCK(3)/
// BURN_BLOCKED(6) упразднены (2026-05-24): резерв возврата теперь TRANSFER на
// w.wal.wpend, их op_code больше не валиден.
eosio::check(op_code == static_cast<uint8_t>(WalletOp::ISSUE) ||
op_code == static_cast<uint8_t>(WalletOp::TRANSFER) ||
op_code == static_cast<uint8_t>(WalletOp::BURN),
"walletop: недопустимый op_code");
wallets2_index wallets(get_self(), coopname.value);
userwallets_index user_wallets(get_self(), coopname.value);
@@ -172,7 +174,7 @@ void ledger2::walletop(eosio::name coopname,
// миграций 048/049.
//
// walletop по построению применяет одно и то же `amount` к L2 и L3 (см.
// ниже case'ы ISSUE/TRANSFER/BLOCK/UNBLOCK/BURN/BURN_BLOCKED), а sender-guard на
// ниже case'ы ISSUE/TRANSFER/BURN), а sender-guard на
// строке 46-47 запрещает обход. Поэтому инвариант сохраняется по
// конструкции; полную сверку выполняет бэкенд («стол бухгалтера»),
// вне транзакционного hot path.
@@ -227,60 +229,6 @@ void ledger2::walletop(eosio::name coopname,
cleanup_l3_if_empty(wallet_from);
break;
}
case WalletOp::BLOCK: {
eosio::check(wallet_from.value != 0, "walletop BLOCK: требуется wallet_from");
eosio::check(wallet_to.value == 0, "walletop BLOCK: wallet_to должен быть пустым");
auto it = wallets.find(wallet_from.value);
eosio::check(it != wallets.end() && it->available >= amount,
std::string{"walletop BLOCK: недостаточно available на кошельке "} +
wallet_from.to_string());
wallets.modify(it, payer, [&](auto& w) {
w.available -= amount;
w.blocked += amount;
});
if (is_user_shared_l3(wallet_from)) {
auto uw = find_l3(wallet_from);
eosio::check(uw != user_wallets.get_index<"byuserwallet"_n>().end() &&
uw->available >= amount,
std::string{"walletop BLOCK: недостаточно L3-available у пайщика "} +
username.to_string() + " на " + wallet_from.to_string());
auto uw_pri = user_wallets.find(uw->id);
user_wallets.modify(uw_pri, payer, [&](auto& r) {
r.available -= amount;
r.blocked += amount;
});
}
break;
}
case WalletOp::UNBLOCK: {
eosio::check(wallet_from.value != 0, "walletop UNBLOCK: требуется wallet_from");
eosio::check(wallet_to.value == 0, "walletop UNBLOCK: wallet_to должен быть пустым");
auto it = wallets.find(wallet_from.value);
eosio::check(it != wallets.end() && it->blocked >= amount,
std::string{"walletop UNBLOCK: недостаточно blocked на кошельке "} +
wallet_from.to_string());
wallets.modify(it, payer, [&](auto& w) {
w.blocked -= amount;
w.available += amount;
});
if (is_user_shared_l3(wallet_from)) {
auto uw = find_l3(wallet_from);
eosio::check(uw != user_wallets.get_index<"byuserwallet"_n>().end() &&
uw->blocked >= amount,
std::string{"walletop UNBLOCK: недостаточно L3-blocked у пайщика "} +
username.to_string() + " на " + wallet_from.to_string());
auto uw_pri = user_wallets.find(uw->id);
user_wallets.modify(uw_pri, payer, [&](auto& r) {
r.blocked -= amount;
r.available += amount;
});
}
break;
}
case WalletOp::BURN: {
eosio::check(wallet_from.value != 0, "walletop BURN: требуется wallet_from");
eosio::check(wallet_to.value == 0, "walletop BURN: wallet_to должен быть пустым");
@@ -311,30 +259,6 @@ void ledger2::walletop(eosio::name coopname,
eosio::check(false, "walletop NONE: запрещённый op_code");
break;
}
case WalletOp::BURN_BLOCKED: {
eosio::check(wallet_from.value != 0, "walletop BURN_BLOCKED: требуется wallet_from");
eosio::check(wallet_to.value == 0, "walletop BURN_BLOCKED: wallet_to должен быть пустым");
auto it = wallets.find(wallet_from.value);
eosio::check(it != wallets.end() && it->blocked >= amount,
std::string{"walletop BURN_BLOCKED: недостаточно blocked на кошельке "} +
wallet_from.to_string());
wallets.modify(it, payer, [&](auto& w) { w.blocked -= amount; });
if (is_user_shared_l3(wallet_from)) {
auto uw = find_l3(wallet_from);
eosio::check(uw != user_wallets.get_index<"byuserwallet"_n>().end() &&
uw->blocked >= amount,
std::string{"walletop BURN_BLOCKED: недостаточно L3-blocked у пайщика "} +
username.to_string() + " на " + wallet_from.to_string());
auto uw_pri = user_wallets.find(uw->id);
user_wallets.modify(uw_pri, payer, [&](auto& r) { r.blocked -= amount; });
}
cleanup_l2_if_empty(wallet_from);
cleanup_l3_if_empty(wallet_from);
break;
}
}
// Post-mutation Σ L3 == L2: см. блок выше (строки 163-175). Снято
@@ -1,5 +1,3 @@
#include <vector>
/**
* @brief Универсальное миграционное действие контракта ledger2 — точка
* расширения для разовых исправлений состояния, которые можно провести
@@ -9,23 +7,33 @@
* после её прогона на проде тело очищается до пустого `require_auth(get_self())`
* (как в `capital::migrate`).
*
* Текущая задача (2026-05-21): чистка осиротевших L3-записей `w.cap.gen`
* на voskhod после смены WalletKind GENERATOR_FUND с USER_SHARED на
* COOPERATIVE (см. `lib/core/ledger2/wallets.hpp`).
* Текущая задача (2026-05-24): свёртка `blocked → available` по ВСЕМ коопам.
*
* Контекст: на voskhod `convertsegm` падал «недостаточно L3-средств у
* пайщика» в проектах, где CRPS перераспределял доли между сегментами.
* w.cap.gen был USER_SHARED, а CRPS в approvecmmt не делал per-user
* компенсирующих TRANSFER между сегментами: `Σ COMMIT_RID == Σ ACCEPT_RID`
* соблюдался только на проекте, не на пайщике.
* Контекст: механика «заблокированного» баланса упразднена (см.
* `lib/core/ledger2/operations.hpp` — удалены WalletOp BLOCK/UNBLOCK/BURN_BLOCKED;
* резерв возврата паевого теперь выражается переводом на кошелёк-резерв
* `w.wal.wpend`). Поле `blocked` остаётся в таблицах `wallets2`/`userwallets`
* как deprecated (физическое удаление поля = небезопасная смена layout таблицы
* на живых коопах, выносится в отдельный cleanup-деплой). Перед тем как поле
* перестанет поддерживаться кодом, накопленные `blocked`-остатки нужно вернуть
* в `available`, чтобы средства не «зависли» на упразднённом субсчёте.
*
* Архитектурный фикс: w.cap.gen — COOPERATIVE-пул без L3. Чтобы UI/бэкенд
* не врали остатками по «личным» w.cap.gen, удаляем осиротевшие L3-записи
* прямым `userwallets.erase`. L2-баланс `wallets2[w.cap.gen]` уже верен
* (синхронен с Σ старых userwallets), его не трогаем.
* Действие: пройти всех кооперативов (cooperatives2 в scope registrator) и для
* каждого свернуть `blocked → available` на уровнях L2 (`wallets2`) и L3
* (`userwallets`): `available += blocked; blocked = 0`. Сумма средств на кошельке
* не меняется — только субсчёт.
*
* Идемпотентно: повторный вызов на чистой БД — no-op (lower_bound пуст).
* Только voskhod: на остальных кооперативах w.cap.gen не использовался.
* Идемпотентно: после свёртки `blocked == 0`, повторный прогон — no-op.
*
* Сигнатура без аргументов — действие вызывается автоматически при деплое
* контракта (как и прочие задачи migrate); проходит по всем кооперативам сам.
*
* ПРЕДУСЛОВИЕ (операционное): на момент прогона не должно быть заявок на возврат
* «в полёте» (статусы pending/authorized в `wallet::withdraws`) — их `blocked`
* относится к старой механике и при свёртке в `available` вернётся пайщику как
* свободные средства, а последующий `completewthd` (BURN с `w.wal.wpend`) не
* найдёт резерва. Незавершённые заявки нужно довести (complete/decline) ДО
* деплоя с этой миграцией.
*
* @ingroup public_ledger2_actions
*
@@ -34,24 +42,31 @@
void ledger2::migrate() {
require_auth(get_self());
const eosio::name target_coop = "voskhod"_n;
cooperatives2_index coops(_registrator, _registrator.value);
userwallets_index user_wallets(get_self(), target_coop.value);
auto idx = user_wallets.get_index<"bywallet"_n>();
for (auto c = coops.begin(); c != coops.end(); ++c) {
const eosio::name coopname = c->username;
// Собираем primary id записей до erase (модификация контейнера при
// итерации через secondary index — небезопасна).
std::vector<uint64_t> ids_to_erase;
for (auto it = idx.lower_bound(ledger2_wallets::GENERATOR_FUND.value);
it != idx.end() && it->wallet_name == ledger2_wallets::GENERATOR_FUND;
++it) {
ids_to_erase.push_back(it->id);
}
// --- L3: userwallets[coopname] ---
// Модифицируем только не-ключевые поля (available/blocked) — итерация по
// первичному индексу с modify безопасна (порядок строк не меняется).
userwallets_index user_wallets(get_self(), coopname.value);
for (auto it = user_wallets.begin(); it != user_wallets.end(); ++it) {
if (it->blocked.amount <= 0) continue;
user_wallets.modify(it, get_self(), [&](auto& r) {
r.available += r.blocked;
r.blocked = eosio::asset(0, r.blocked.symbol);
});
}
for (uint64_t id : ids_to_erase) {
auto pri = user_wallets.find(id);
if (pri != user_wallets.end()) {
user_wallets.erase(pri);
// --- L2: wallets2[coopname] ---
wallets2_index wallets(get_self(), coopname.value);
for (auto it = wallets.begin(); it != wallets.end(); ++it) {
if (it->blocked.amount <= 0) continue;
wallets.modify(it, get_self(), [&](auto& w) {
w.available += w.blocked;
w.blocked = eosio::asset(0, w.blocked.symbol);
});
}
}
}
+1 -1
View File
@@ -241,7 +241,7 @@ static constexpr uint64_t _capital_program_id = 4;
#ifdef IS_TESTNET
static constexpr uint64_t MIN_SOVIET_MEMBERS_COUNT = 1; /*!< минимальное количество членов совета (тест) */
#else
static constexpr uint64_t MIN_SOVIET_MEMBERS_COUNT = 5; /*!< минимальное количество членов совета (прод) */
static constexpr uint64_t MIN_SOVIET_MEMBERS_COUNT = 3; /*!< минимальное количество членов совета (прод) */
#endif
/**
@@ -31,9 +31,9 @@
* - `WalletOp::WALLET_ONLY` удалён (ADR-003): «без бухпроводок» определяется
* парой `(debit_account_id == 0, credit_account_id == 0)` на уровне записи.
* Для `o.cap.invest` теперь TRANSFER без проводок (оба account_id == 0).
* - Добавлен `WalletOp::BURN` (ADR-003): `available -= amount` на `wallet_from`,
* без `wallet_to`. На текущем этапе в `OPERATION_REGISTRY` не используется —
* зарезервирован под будущие операции штатного сжигания.
* - `WalletOp::BURN` (ADR-003): `available -= amount` на `wallet_from`,
* без `wallet_to`. Используется в `o.wal.wthcpl` (сжигание резерва возврата
* с `w.wal.wpend`), `o.cap.drppre` и как зеркало ISSUE в `revert`.
* - Единые программные кошельки: `BLAGOROST_FUND` (`w.cap.blago`) и
* `GENERATOR_FUND` (`w.cap.gen`) — заменили ранее декомпозированные
* `bginv/bgprop/bgrid/bgmem` и `gncom/gnmem` (ADR-009).
@@ -61,9 +61,9 @@ namespace operations {
// wallet
namespace wallet {
inline constexpr eosio::name COMPLETE_DEPOSIT = "o.wal.depcpl"_n; ///< Завершение внесения паевого взноса (Dr 51 / Cr 80, ISSUE SHARE_FUND_PAY).
inline constexpr eosio::name COMPLETE_WITHDRAW = "o.wal.wthcpl"_n; ///< Завершение возврата паевого взноса (Dr 80 / Cr 51, BURN_BLOCKED SHARE_FUND_PAY — деньги уходят из системы, без wallet_to).
inline constexpr eosio::name REQUEST_WITHDRAW = "o.wal.wthreq"_n; ///< Запрос на возврат паевого: BLOCK на SHARE_FUND_PAY (без Dr/Cr).
inline constexpr eosio::name DECLINE_WITHDRAW = "o.wal.wthdec"_n; ///< Отклонение запроса на возврат: UNBLOCK на SHARE_FUND_PAY (без Dr/Cr).
inline constexpr eosio::name COMPLETE_WITHDRAW = "o.wal.wthcpl"_n; ///< Завершение возврата паевого взноса (Dr 80 / Cr 51, BURN с WITHDRAW_PENDING — деньги уходят из системы, без wallet_to).
inline constexpr eosio::name REQUEST_WITHDRAW = "o.wal.wthreq"_n; ///< Запрос на возврат паевого: TRANSFER SHARE_FUND_PAY → WITHDRAW_PENDING (резерв, без Dr/Cr).
inline constexpr eosio::name DECLINE_WITHDRAW = "o.wal.wthdec"_n; ///< Отклонение запроса на возврат: TRANSFER WITHDRAW_PENDING → SHARE_FUND_PAY (снятие резерва, без Dr/Cr).
inline constexpr eosio::name CONVERT_TO_MEMBER = "o.wal.conv"_n; ///< Конвертация цифрового рубля в универсальный членский кошелёк (Dr 80 / Cr 86, TRANSFER SHARE_FUND_PAY → CK_MEMBER). Conditional-шаг серии createorder в «Столе заказов».
}
@@ -135,14 +135,23 @@ namespace operations {
* `(debit_account_id == 0, credit_account_id == 0)` на уровне записи реестра.
* Compile-time правило `(debit==0) ⇔ (credit==0)` ловит смешанные пары.
*/
//
// Удаление BLOCK/UNBLOCK/BURN_BLOCKED (2026-05-24): механика «заблокированного»
// баланса упразднена. Резерв средств под заявку на возврат паевого теперь
// выражается переводом на отдельный кошелёк-резерв `w.wal.wpend` (TRANSFER),
// возврат резерва — обратным TRANSFER, завершение — BURN с резерва. Поле
// `blocked` в таблицах wallets2/userwallets оставлено deprecated (всегда 0
// после ledger2::migrate-свёртки) — физическое удаление поля = небезопасная
// смена layout таблицы на живых коопах, выносится в отдельный cleanup-деплой.
//
// Числовые значения ISSUE/TRANSFER/BURN/NONE СОХРАНЕНЫ (не перенумерованы),
// чтобы исторические op_code в blockchain_actions читались бэкендом без сдвига
// смысла (2 и 3 — бывшие BLOCK/UNBLOCK — больше не выдаются и невалидны на входе).
enum class WalletOp : uint8_t {
ISSUE = 0, ///< первичный вход средств на кошелёк wallet_to (wallet_from = empty)
TRANSFER = 1, ///< перемещение wallet_from → wallet_to (с Dr/Cr ИЛИ без — по парам account_id)
BLOCK = 2, ///< available-=amount, blocked+=amount на wallet_from
UNBLOCK = 3, ///< blocked-=amount, available+=amount на wallet_from
BURN = 4, ///< изъятие amount с wallet_from->available, без wallet_to. Покрывает оба кейса: (a) штатное сжигание как бизнес-операция в OPERATION_REGISTRY; (b) зеркало ISSUE при `ledger2::revert` (различие — через operation_code: `o.adj.rev` для adjustment-mirror).
NONE = 5, ///< только бухпроводка без перемещения средств (wallet_from = empty, wallet_to = empty, debit ≠ 0, credit ≠ 0). Покрывает кейсы внутрибалансовых проводок типа Dr 04 / Cr 08 (приём РИД в НМА), когда кошелёк уже на нужном программном фонде.
BURN_BLOCKED = 6, ///< изъятие amount из wallet_from->blocked, без wallet_to. Используется при завершении возврата паевого (`o.wal.wthcpl`): средства предварительно заблокированы через BLOCK (REQUEST_WITHDRAW), на завершении сжигаются — выходят из системы (получателя на цепи нет). На L3 синхронно: from_uw->blocked -= amount.
};
/**
@@ -151,7 +160,7 @@ enum class WalletOp : uint8_t {
* Семантика полей по `wallet_op`:
* - ISSUE: wallet_from = eosio::name{}, wallet_to = required.
* - TRANSFER: wallet_from = required, wallet_to = required (≠ from).
* - BLOCK / UNBLOCK / BURN / BURN_BLOCKED: wallet_from = required, wallet_to = eosio::name{}.
* - BURN: wallet_from = required, wallet_to = eosio::name{}.
*
* Семантика бух.проводки:
* - Без проводок: debit_account_id == 0 И credit_account_id == 0.
@@ -163,7 +172,7 @@ struct OperationRegistryEntry {
eosio::name process_type; ///< тип процесса с префиксом `p.<contract>.<noun>`
WalletOp wallet_op;
eosio::name wallet_from; ///< пустое имя для ISSUE
eosio::name wallet_to; ///< пустое имя для BLOCK/UNBLOCK/BURN/BURN_BLOCKED
eosio::name wallet_to; ///< пустое имя для BURN
uint64_t debit_account_id; ///< 0 если без бухпроводки (тогда credit_account_id тоже == 0)
uint64_t credit_account_id; ///< 0 если без бухпроводки (тогда debit_account_id тоже == 0)
const char* human_name;
@@ -190,12 +199,12 @@ static constexpr OperationRegistryEntry OPERATION_REGISTRY[] = {
ledger2_accounts::BANK_ACCOUNT, ledger2_accounts::SHARE_FUND,
"Внесение пайщиком паевого взноса" },
// 4. Возврат паевого взноса: Dr 80 / Cr 51, BURN_BLOCKED SHARE_FUND_PAY.
// Сжигание из заблокированной суммы пайщика (BLOCK был на REQUEST_WITHDRAW):
// 4. Возврат паевого взноса: Dr 80 / Cr 51, BURN WITHDRAW_PENDING.
// Сжигание из кошелька-резерва (TRANSFER в резерв был на REQUEST_WITHDRAW):
// деньги уходят из системы (банковский перевод пайщику), получателя на цепи нет.
// Бухгалтерия: паевой фонд уменьшается (Дт 80), расчётный счёт уменьшается (Кт 51).
{ operations::wallet::COMPLETE_WITHDRAW, processes::wallet::WITHDRAW, WalletOp::BURN_BLOCKED,
ledger2_wallets::SHARE_FUND_PAY, eosio::name{},
{ operations::wallet::COMPLETE_WITHDRAW, processes::wallet::WITHDRAW, WalletOp::BURN,
ledger2_wallets::WITHDRAW_PENDING, eosio::name{},
ledger2_accounts::SHARE_FUND, ledger2_accounts::BANK_ACCOUNT,
"Возврат паевого взноса пайщику" },
@@ -328,17 +337,19 @@ static constexpr OperationRegistryEntry OPERATION_REGISTRY[] = {
ledger2_accounts::SHARE_FUND, ledger2_accounts::TARGET_RECEIPTS,
"Трансляция паевого взноса из ЦПП «Цифровой Кошелёк» в членский взнос за пользование инфраструктурой" },
// 15. Запрос на возврат паевого: BLOCK SHARE_FUND_PAY (без Dr/Cr — внутри одного бухсчёта 80).
{ operations::wallet::REQUEST_WITHDRAW, processes::wallet::WITHDRAW, WalletOp::BLOCK,
ledger2_wallets::SHARE_FUND_PAY, eosio::name{},
// 15. Запрос на возврат паевого: TRANSFER SHARE_FUND_PAY → WITHDRAW_PENDING
// (без Dr/Cr — оба кошелька на счёте 80; резерв средств на время рассмотрения).
{ operations::wallet::REQUEST_WITHDRAW, processes::wallet::WITHDRAW, WalletOp::TRANSFER,
ledger2_wallets::SHARE_FUND_PAY, ledger2_wallets::WITHDRAW_PENDING,
0, 0,
"Блокировка паевого под запрос на возврат" },
"Резервирование паевого под запрос на возврат" },
// 16. Отклонение запроса на возврат: UNBLOCK SHARE_FUND_PAY (без Dr/Cr — зеркало REQUEST_WITHDRAW).
{ operations::wallet::DECLINE_WITHDRAW, processes::wallet::WITHDRAW, WalletOp::UNBLOCK,
ledger2_wallets::SHARE_FUND_PAY, eosio::name{},
// 16. Отклонение запроса на возврат: TRANSFER WITHDRAW_PENDING → SHARE_FUND_PAY
// (без Dr/Cr — зеркало REQUEST_WITHDRAW; возврат резерва пайщику).
{ operations::wallet::DECLINE_WITHDRAW, processes::wallet::WITHDRAW, WalletOp::TRANSFER,
ledger2_wallets::WITHDRAW_PENDING, ledger2_wallets::SHARE_FUND_PAY,
0, 0,
"Разблокировка паевого после отклонения запроса на возврат" },
"Снятие резерва паевого после отклонения запроса на возврат" },
// 17. Возврат из ЦПП «Благорост» в Цифровой Кошелёк: TRANSFER BLAGOROST_FUND → SHARE_FUND_PAY (без Dr/Cr — оба счёта 80, зеркало INVEST).
{ operations::capital::WITHDRAW_FROM_CAPITAL, processes::capital::WTHCAP, WalletOp::TRANSFER,
@@ -395,7 +406,7 @@ static constexpr size_t OPERATION_REGISTRY_SIZE = sizeof(OPERATION_REGISTRY) / s
// оба существуют в `LEDGER2_ACCOUNT_MAP`.
// 4. Для TRANSFER: `wallet_from` ≠ `wallet_to`, оба ≠ 0.
// 5. Для ISSUE: `wallet_from` == 0 и `wallet_to` ≠ 0.
// 6. Для BLOCK / UNBLOCK / BURN / BURN_BLOCKED: `wallet_from` ≠ 0, `wallet_to` == 0.
// 6. Для BURN: `wallet_from` ≠ 0, `wallet_to` == 0.
// 7. Все id кошельков из записей существуют в `LEDGER2_WALLET_REGISTRY`.
namespace ledger2_registry_detail {
constexpr bool operation_codes_unique() {
@@ -439,11 +450,11 @@ namespace ledger2_registry_detail {
return true;
}
// Правило 6: BURN / BURN_BLOCKED — wallet_from required, wallet_to == 0 (ADR-003).
// Правило 6: BURN — wallet_from required, wallet_to == 0 (ADR-003).
constexpr bool burn_pattern_correct() {
for (size_t i = 0; i < OPERATION_REGISTRY_SIZE; ++i) {
const auto& e = OPERATION_REGISTRY[i];
if (e.wallet_op != WalletOp::BURN && e.wallet_op != WalletOp::BURN_BLOCKED) continue;
if (e.wallet_op != WalletOp::BURN) continue;
if (e.wallet_from.value == 0) return false;
if (e.wallet_to.value != 0) return false;
}
@@ -495,7 +506,7 @@ static_assert(ledger2_registry_detail::dr_ne_cr_when_posting(),
static_assert(ledger2_registry_detail::transfer_wallet_from_ne_to(),
"OPERATION_REGISTRY: TRANSFER с wallet_from == wallet_to или одним из них == 0");
static_assert(ledger2_registry_detail::burn_pattern_correct(),
"OPERATION_REGISTRY: BURN / BURN_BLOCKED требует wallet_from ≠ 0 и wallet_to == 0");
"OPERATION_REGISTRY: BURN требует wallet_from ≠ 0 и wallet_to == 0");
static_assert(ledger2_registry_detail::none_pattern_correct(),
"OPERATION_REGISTRY: NONE требует wallet_from == 0, wallet_to == 0 и обе проводки заполненными");
static_assert(ledger2_registry_detail::accounts_exist_in_map(),
@@ -27,7 +27,7 @@
* w.mkt.* — Маркетплейс (выплаты поставщикам)
*
* Sentinel `eosio::name{}` (пустое имя, value=0) — «кошелёк вне системы»
* для ISSUE (нет wallet_from) и для BURN/BLOCK/UNBLOCK (нет wallet_to).
* для ISSUE (нет wallet_from) и для BURN (нет wallet_to).
*
* При первом ISSUE/TRANSFER кошелёк создаётся автоматически по записи
* из WALLET_REGISTRY. При обнулении available+blocked запись удаляется.
@@ -46,7 +46,8 @@ struct ledger2_wallets {
// wallet — паевой фонд + возвраты + ЦК
static constexpr eosio::name SHARE_FUND_PAY = "w.wal.share"_n; ///< Паевой взнос пайщика (USER_SHARED)
static constexpr eosio::name CK_MEMBER = "w.wal.member"_n; ///< ЦК — членская часть пайщика (USER_SHARED)
static constexpr eosio::name WITHDRAWALS_SINK = "w.wal.wthdrw"_n; ///< DEPRECATED 2026-05-21: после переключения o.wal.wthcpl с TRANSFER на BURN_BLOCKED кошелёк больше не получает новых средств. Оставлен в реестре для исторических L2-балансов (накопленные возвраты до перехода). Не использовать в новых операциях.
static constexpr eosio::name WITHDRAWALS_SINK = "w.wal.wthdrw"_n; ///< DEPRECATED 2026-05-21: исторический sink возвратов. Оставлен в реестре для исторических L2-балансов (накопленные возвраты до перехода). Не использовать в новых операциях.
static constexpr eosio::name WITHDRAW_PENDING = "w.wal.wpend"_n; ///< Резерв паевого под заявку на возврат (COOPERATIVE-пул). o.wal.wthreq переводит сюда с w.wal.share, o.wal.wthdec возвращает обратно, o.wal.wthcpl сжигает отсюда. Заменил механику blocked/BLOCK/UNBLOCK 2026-05-24.
// registrator — минимальный паевой + вступительные
static constexpr eosio::name MIN_SHARE_FUND = "w.reg.minshr"_n; ///< Минимальный паевой взнос пайщика (USER_SHARED, без сверки соглашений)
@@ -95,7 +96,7 @@ struct Ledger2WalletMeta {
WalletKind kind;
};
inline constexpr std::array<Ledger2WalletMeta, 15> LEDGER2_WALLET_REGISTRY = {{
inline constexpr std::array<Ledger2WalletMeta, 16> LEDGER2_WALLET_REGISTRY = {{
// USER_SHARED (6) — L3-разрез по пайщику
{ ledger2_wallets::MIN_SHARE_FUND, "Минимальный паевой взнос", WalletKind::USER_SHARED },
{ ledger2_wallets::SHARE_FUND_PAY, "Паевой взнос пайщика", WalletKind::USER_SHARED },
@@ -104,7 +105,7 @@ inline constexpr std::array<Ledger2WalletMeta, 15> LEDGER2_WALLET_REGISTRY = {{
{ ledger2_wallets::PREIMP_FUND, "Первичный учёт РИД-взносов до перехода на электронный учёт", WalletKind::USER_SHARED },
{ ledger2_wallets::MARKETPLACE_ORDER_LOCK,"ЦПП «Стол Заказов» — резерв под заказ у пайщика", WalletKind::USER_SHARED },
// COOPERATIVE (9) — единый кооперативный баланс, без L3
// COOPERATIVE (10) — единый кооперативный баланс, без L3
// GENERATOR_FUND переведён сюда из USER_SHARED (см. wallets.hpp:64) —
// CRPS-распределение между сегментами проекта не поддерживает per-user
// компенсирующие TRANSFER на approvecmmt, поэтому L3-проверка walletop
@@ -112,6 +113,7 @@ inline constexpr std::array<Ledger2WalletMeta, 15> LEDGER2_WALLET_REGISTRY = {{
{ ledger2_wallets::GENERATOR_FUND, "ЦПП «Генератор» — единый кошелёк программы", WalletKind::COOPERATIVE },
{ ledger2_wallets::ENTRANCE_FEES, "Вступительные взносы", WalletKind::COOPERATIVE },
{ ledger2_wallets::WITHDRAWALS_SINK, "Возвраты паевых взносов пайщикам (deprecated, не используется в новых операциях)", WalletKind::COOPERATIVE },
{ ledger2_wallets::WITHDRAW_PENDING, "Резерв паевого под заявку на возврат", WalletKind::COOPERATIVE },
{ ledger2_wallets::INFRA_FEES, "Членские взносы за инфраструктуру кооп. платформы", WalletKind::COOPERATIVE },
{ ledger2_wallets::DELEGATE_FEES, "Делегатские членские взносы", WalletKind::COOPERATIVE },
{ ledger2_wallets::SOV_EXPENSES, "Хозяйственные расходы из числа целевого финансирования", WalletKind::COOPERATIVE },
@@ -3,10 +3,11 @@
# программу «Цифровой Кошелёк» (SHARE_FUND_PAY).
#
# Четырёхактовый процесс с одной авторизацией советом. Запись заявки живёт в
# таблице `withdraws` от создания до выплаты или отказа. На закрывающем
# действии completewthd срабатывает одна операция o.wal.wthcpl — сжигание
# заблокированной суммы (BURN_BLOCKED w.wal.share): деньги уходят из системы
# пайщику банковским переводом, получателя на цепи нет.
# таблице `withdraws` от создания до выплаты или отказа. На подаче заявки сумма
# переводится в кошелёк-резерв (WITHDRAW_PENDING, w.wal.wpend). На закрывающем
# действии completewthd срабатывает операция o.wal.wthcpl — сжигание суммы из
# резерва (BURN w.wal.wpend): деньги уходят из системы пайщику банковским
# переводом, получателя на цепи нет.
#
# Источники правды в коде:
# • cpp/wallet/wallet.hpp — actions
@@ -43,8 +44,8 @@ actions:
actor: contributor
role: opener
purpose: >
Пайщик создаёт заявку на возврат паевого взноса. Контракт блокирует
сумму на кошельке SHARE_FUND_PAY (статус available → blocked) и
Пайщик создаёт заявку на возврат паевого взноса. Контракт переводит
сумму с кошелька SHARE_FUND_PAY в кошелёк-резерв WITHDRAW_PENDING и
создаёт повестку в совете о возврате с callback'ом авторизации.
- name: wallet::authwthd
human: Авторизовать выплату
@@ -60,16 +61,17 @@ actions:
role: closer
purpose: >
Gateway подтвердил списание средств в пользу пайщика. Это закрывающее
действие: применяется операция o.wal.wthcpl (BURN_BLOCKED w.wal.share)
заблокированная сумма сжигается, в бухгалтерии проходит обратная
действие: применяется операция o.wal.wthcpl (BURN w.wal.wpend)
— сумма из резерва сжигается, в бухгалтерии проходит обратная
проводка Дт 80 / Кт 51. Запись заявки удаляется.
- name: wallet::declinewthd
human: Отклонить
actor: soviet
role: reject
purpose: >
Отказ на любом из этапов до выплаты. Сумма разблокируется на кошельке
пайщика (UNBLOCK через o.wal.wthdec), запись заявки удаляется.
Отказ на любом из этапов до выплаты. Сумма возвращается из резерва на
кошелёк пайщика (o.wal.wthdec: WITHDRAW_PENDING → SHARE_FUND_PAY),
запись заявки удаляется.
# ── Секция 3. Граф состояний ────────────────────────────────────────────────
entity: wallet::withdraw
@@ -80,8 +82,8 @@ states:
- name: pending
human: Ожидает решения совета
description: >
Заявка создана, сумма заблокирована на кошельке пайщика (available → blocked),
повестка отправлена в совет.
Заявка создана, сумма переведена в кошелёк-резерв (SHARE_FUND_PAY →
WITHDRAW_PENDING), повестка отправлена в совет.
kind: normal
- name: authorized
human: Выплата отправлена
@@ -92,15 +94,15 @@ states:
- name: completed
human: Возврат выплачен
description: >
Gateway подтвердил списание, заблокированная сумма сжигается на кошельке
пайщика (BURN_BLOCKED w.wal.share). В бухгалтерии прошла обратная
Gateway подтвердил списание, сумма из резерва сжигается
(BURN w.wal.wpend). В бухгалтерии прошла обратная
проводка Дт 80 / Кт 51. Запись заявки удалена.
kind: final
- name: removed
human: Отклонено
description: >
Возврат не состоялся: сумма разблокирована (UNBLOCK через o.wal.wthdec),
запись удалена.
Возврат не состоялся: сумма возвращена из резерва на кошелёк пайщика
(o.wal.wthdec: WITHDRAW_PENDING → SHARE_FUND_PAY), запись удалена.
kind: virtual
virtual: true
@@ -172,7 +174,7 @@ scenario:
- Сумма возврата ≤ доступного остатка.
post:
- В таблице withdraws создана запись со статусом `pending`.
- На кошельке пайщика сумма переведена available → blocked.
- Сумма переведена с SHARE_FUND_PAY в кошелёк-резерв WITHDRAW_PENDING.
- Совет получил повестку о возврате.
- step: 2
@@ -197,13 +199,13 @@ scenario:
action: wallet::completewthd
description: >
Gateway подтверждает списание средств в пользу пайщика. Контракт
применяет ledger2-операцию o.wal.wthcpl (BURN_BLOCKED w.wal.share с
применяет ledger2-операцию o.wal.wthcpl (BURN w.wal.wpend с
обратной проводкой Дт 80 / Кт 51) и удаляет запись заявки.
pre:
- Заявка в статусе `authorized`.
- Gateway подтвердил списание.
post:
- Заблокированная сумма сожжена с SHARE_FUND_PAY (w.wal.share).
- Сумма из резерва сожжена (WITHDRAW_PENDING, w.wal.wpend).
- В ledger2 применена операция o.wal.wthcpl.
- Запись заявки удалена.
@@ -214,8 +216,8 @@ scenario:
actor: soviet
description: >
Совет принял отрицательное решение либо Gateway отклонил платёж.
Сумма разблокирована на кошельке пайщика (UNBLOCK через o.wal.wthdec),
запись удалена.
Сумма возвращена из резерва на кошелёк пайщика (o.wal.wthdec:
WITHDRAW_PENDING → SHARE_FUND_PAY), запись удалена.
# ── Секция 5. Документы и подписи ───────────────────────────────────────────
documents:
@@ -234,45 +236,45 @@ documents:
# ── Секция 6. Операции ──────────────────────────────────────────────────────
operations:
- ledger_code: o.wal.wthreq
human_name: Блокировка паевого под запрос на возврат
wallet_op: BLOCK
human_name: Резервирование паевого под запрос на возврат
wallet_op: TRANSFER
wallet_from: w.wal.share
wallet_to: ~
wallet_to: w.wal.wpend
debit: ~
credit: ~
amount_ref: withdraw.quantity
triggered_by: wallet::createwthd
description: >
На создании заявки контракт блокирует сумму на кошельке пайщика
(SHARE_FUND_PAY): available → blocked. Бухгалтерская проводка не
создаётся — движение внутри одного бухсчёта 80.
На создании заявки контракт переводит сумму с кошелька пайщика
(SHARE_FUND_PAY) в кошелёк-резерв (WITHDRAW_PENDING). Бухгалтерская
проводка не создаётся — движение внутри одного бухсчёта 80.
- ledger_code: o.wal.wthdec
human_name: Разблокировка паевого после отклонения запроса на возврат
wallet_op: UNBLOCK
wallet_from: w.wal.share
wallet_to: ~
human_name: Снятие резерва паевого после отклонения запроса на возврат
wallet_op: TRANSFER
wallet_from: w.wal.wpend
wallet_to: w.wal.share
debit: ~
credit: ~
amount_ref: withdraw.quantity
triggered_by: wallet::declinewthd
description: >
Зеркало REQUEST_WITHDRAW: blocked → available на кошельке пайщика.
Бухгалтерская проводка не создаётся.
Зеркало REQUEST_WITHDRAW: сумма возвращается из резерва (WITHDRAW_PENDING)
на кошелёк пайщика (SHARE_FUND_PAY). Бухгалтерская проводка не создаётся.
- ledger_code: o.wal.wthcpl
human_name: Возврат паевого взноса пайщику
wallet_op: BURN_BLOCKED
wallet_from: w.wal.share # ЦПП «Цифровой Кошелёк» — паевые взносы деньгами
wallet_op: BURN
wallet_from: w.wal.wpend # кошелёк-резерв возвратов
wallet_to: ~ # деньги уходят из системы, получателя на цепи нет
debit: 80 # Паевой фонд (складочный капитал)
credit: 51 # Расчётный счёт
amount_ref: withdraw.quantity
triggered_by: wallet::completewthd
description: >
Обратная операция к o.wal.depcpl: заблокированная сумма сжигается с
кошелька пайщика «ЦПП Цифровой Кошелёк» (w.wal.share) — деньги уходят
из системы банковским переводом пайщику. На цепи получателя нет.
Обратная операция к o.wal.depcpl: зарезервированная сумма сжигается с
кошелька-резерва (w.wal.wpend) — деньги уходят из системы банковским
переводом пайщику. На цепи получателя нет.
В бухгалтерии прошла проводка Дт 80 / Кт 51 — паевой фонд уменьшился,
деньги ушли с расчётного счёта пайщику.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@coopenomics/contracts",
"version": "2026.5.23-4",
"version": "2026.5.27-2",
"private": true,
"type": "module",
"scripts": {
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "create-nodejs-express-app",
"version": "2026.5.23-4",
"version": "2026.5.27-2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "create-nodejs-express-app",
"version": "2026.5.23-4",
"version": "2026.5.27-2",
"license": "MIT",
"dependencies": {
"@a2seven/yoo-checkout": "^1.1.4",
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@coopenomics/controller",
"version": "2026.5.23-4",
"version": "2026.5.27-2",
"description": "Бэкенд GraphQL API кооператива на NestJS",
"private": true,
"bin": "bin/createNodejsApp.js",
@@ -193,12 +193,12 @@
"@antfu/eslint-config": "^2.24.1",
"@graphql-codegen/cli": "^5.0.3",
"@graphql-codegen/typescript": "^4.1.1",
"@swc-node/register": "^1.11.1",
"@swc/core": "^1.15.33",
"@graphql-codegen/typescript-apollo-client-helpers": "^3.0.0",
"@graphql-codegen/typescript-operations": "^4.3.1",
"@nestjs/testing": "^11.1.19",
"@redocly/cli": "^1.18.0",
"@swc-node/register": "^1.11.1",
"@swc/core": "^1.15.33",
"@types/express": "^4.17.21",
"@types/node": "^20.11.26",
"@typescript-eslint/eslint-plugin": "^5.10.0",
+118 -23
View File
@@ -1435,7 +1435,6 @@ type CallTranscription {
createdAt: DateTime!
endedAt: DateTime
id: String!
matrixRoomId: String!
"""Пользовательская заметка о содержании звонка"""
memo: String!
@@ -1444,6 +1443,8 @@ type CallTranscription {
Отображаемые имена участников (Synapse displayname); в БД хранятся канонические Matrix user id
"""
participants: [String!]!
"""Внутреннее имя комнаты звонка (LiveKit room name), не Matrix room id"""
roomId: String!
roomName: String!
startedAt: DateTime!
@@ -3554,6 +3555,17 @@ type ChatCoopCalendarRoomOption {
matrixRoomId: String!
}
type ChatcoopNonProjectCommunicationRoom {
"""Подпись для отображения комнаты"""
displayLabel: String!
"""Тип комнаты (пайщики / совет / секретарь)"""
kind: NonProjectRoomKind!
"""Идентификатор комнаты Matrix"""
matrixRoomId: String!
}
type ChatcoopProjectCommunicationRoom {
"""Подпись для отображения (комната / проект Capital)"""
displayLabel: String!
@@ -3577,6 +3589,28 @@ type ChatcoopRoomMessageLine {
originServerTs: Float!
}
type ChatcoopSecretaryRoom {
"""Название комнаты"""
displayLabel: String!
"""Можно ли удалить комнату из интерфейса (только комнаты секретаря)"""
editable: Boolean!
"""Комната зашифрована (E2EE) — секретарь не транскрибирует такие комнаты"""
encrypted: Boolean!
"""
Внутренний идентификатор комнаты в реестре (для операций; это НЕ Matrix room id)
"""
id: String!
"""Тип комнаты"""
kind: ManagedRoomKind!
"""Секретарь присутствует в комнате"""
secretaryInRoom: Boolean!
}
input CheckMatrixUsernameInput {
username: String!
}
@@ -4635,6 +4669,16 @@ input CreateRequestInput {
width: Int
}
input CreateSecretaryRoomInput {
"""Название комнаты"""
displayName: String!
"""
Публичная комната (любой может войти) либо приватная (вход по приглашению создателя)
"""
isPublic: Boolean!
}
input CreateSovietIndividualDataInput {
"""Дата рождения"""
birthdate: String!
@@ -6717,9 +6761,6 @@ type Ledger2Wallet {
"""Доступный баланс"""
available: String!
"""Заблокированный баланс"""
blocked: String!
"""eosio::name-идентификатор кошелька (w.<contract>.<waltype>)"""
id: String!
@@ -6899,6 +6940,14 @@ input MakeClearanceInput {
username: String!
}
"""Тип комнаты: пайщики, совет, проект Capital, комната секретаря"""
enum ManagedRoomKind {
CAPITAL_PROJECT
COUNCIL
MEMBERS
SECRETARY
}
input MarkReportPeriodInput {
mark: ReportSubmissionMark
period: Int
@@ -8082,6 +8131,19 @@ input MarketplaceListOrdersInput {
supplier_account: String
}
input MarketplaceListOutgoingPaymentsAsSupplierFilterInput {
"""Фильтр по статусам выплат. Пусто — показывать все статусы."""
statuses: [MarketplaceOutgoingPaymentRequestStatus!]
}
input MarketplaceListOutgoingPaymentsFilterInput {
"""Фильтр по статусам выплат. Пусто — показывать все статусы."""
statuses: [MarketplaceOutgoingPaymentRequestStatus!]
"""Поставщик-получатель выплаты. Пусто — по всем поставщикам."""
supplier_account: String
}
input MarketplaceListPendingOffersInput {
"""Количество элементов на странице"""
limit: Int! = 10
@@ -8129,7 +8191,7 @@ type MarketplaceMemberWallet {
username: String!
"""
Релевантные стол-заказам USER_SHARED-кошельки пайщика; порядок: share → member → mkt.member
Релевантные стол-заказам USER_SHARED-кошельки пайщика; порядок: share → member → mkt.order
"""
wallets: [MarketplaceWalletEntry!]!
}
@@ -8338,20 +8400,14 @@ type MarketplaceOrder {
}
"""
Снимок транзакции блокировки средств: ссылки на блок, флаги конверсии и сумма.
Снимок транзакции резервирования средств: ссылки на блок и сумма резерва.
"""
type MarketplaceOrderCreateTxSnapshot {
"""Номер блока, в который попала транзакция."""
block_num: Int!
"""Сумма заблокированных средств (строка денежного актива)."""
blocked_amount: String!
"""Был ли выполнен перевод средств между кошельками пайщика."""
did_assign: Boolean!
"""Была ли выполнена конверсия паевого взноса в членский."""
did_convert: Boolean!
"""Сумма зарезервированных средств (строка денежного актива)."""
locked_amount: String!
"""Время подписания заказа (ISO 8601)."""
signed_at: String!
@@ -9246,7 +9302,9 @@ type MarketplaceWalletEntry {
"""Доступный остаток (`userwallets.available`)"""
available: String!
"""Заблокированный остаток (`userwallets.blocked`)"""
"""
Заблокированный остаток (`userwallets.blocked`). Для marketplace-кошельков всегда `0` — резерв выражается через `.available` кошелька w.mkt.order. Поле остаётся для wallet/withdraw flow и legacy данных.
"""
blocked: String!
"""Человекочитаемое название (из cooptypes LEDGER2_WALLET_REGISTRY)"""
@@ -9260,7 +9318,7 @@ type MarketplaceWalletEntry {
"""
label: String!
"""eosio::name кошелька (w.wal.share / w.wal.member / w.mkt.member)"""
"""eosio::name кошелька (w.wal.share / w.wal.member / w.mkt.order)"""
name: String!
"""program_id: 1 — Цифровой Кошелёк, 2 — Стол Заказов"""
@@ -10248,6 +10306,13 @@ type Mutation {
"""
chatcoopCreateCalendarIcsSubscription: ChatCoopCalendarIcsUrlResponse!
"""
Создать комнату с секретарём (публичную или приватную); секретарь подключается сразу
Требуемые роли: chairman, member.
"""
chatcoopCreateSecretaryRoom(data: CreateSecretaryRoomInput!): ChatcoopSecretaryRoom!
"""
Удалить событие календаря
@@ -10255,6 +10320,13 @@ type Mutation {
"""
chatcoopDeleteCalendarEvent(id: String!): Boolean!
"""
Удалить комнату секретаря: вывести секретаря и снять комнату с синхронизации (возвращает идентификатор комнаты в реестре)
Требуемые роли: chairman, member.
"""
chatcoopRemoveSecretaryRoom(data: RemoveSecretaryRoomInput!): String!
"""
Обновить событие календаря
@@ -10984,6 +11056,13 @@ type Mutation {
walmoveWallets(input: WalmoveInput!): Ledger2AdjustmentResult!
}
"""Тип комнаты вне проекта: пайщики, совет, комната секретаря"""
enum NonProjectRoomKind {
COUNCIL
MEMBERS
SECRETARY
}
input NotificationWorkflowRecipientInput {
"""Username получателя"""
username: String!
@@ -12222,9 +12301,6 @@ type ProgramWallet {
"""Номер блока последнего обновления"""
blockNum: Float
"""Заблокированный баланс (формат: "100.0000 RUB")"""
blocked: String!
"""Имя кооператива"""
coopname: String!
@@ -12734,6 +12810,13 @@ type Query {
"""
chatcoopListCalendarRooms: [ChatCoopCalendarRoomOption!]!
"""
Комнаты Matrix вне проектов Capital (пайщики/совет/секретарь) — для синхронизации в blago
Требуемые роли: chairman, member, user.
"""
chatcoopListNonProjectCommunicationRooms: [ChatcoopNonProjectCommunicationRoom!]!
"""
Комнаты Matrix, привязанные к проекту Capital (реестр ChatCoop)
@@ -12741,6 +12824,13 @@ type Query {
"""
chatcoopListProjectCommunicationRooms(data: GetProjectCommunicationRoomsInput!): [ChatcoopProjectCommunicationRoom!]!
"""
Все комнаты реестра ChatCoop (системные/проектные — read-only, комнаты секретаря — удаляемые)
Требуемые роли: chairman, member.
"""
chatcoopListSecretaryRooms: [ChatcoopSecretaryRoom!]!
"""
UTC-даты (YYYY-MM-DD), в которых есть сообщения новее afterOriginServerTsExclusive, для комнаты Matrix
@@ -13205,7 +13295,7 @@ type Query {
marketplaceListConsolidatedRequests(input: MarketplaceListConsolidatedRequestsInput, options: PaginationInput): MarketplaceConsolidatedRequestPaginationResult!
"""
Список наклеек инвентаря КУ: admin/совет видят весь склад кооператива, оператор — только свой участок.
Список наклеек инвентаря КУ — для admin-стола склада и операторских разделов.
"""
marketplaceListInventory(data: MarketplaceListInventoryInput): [MarketplaceInventoryItem!]!
@@ -13241,12 +13331,12 @@ type Query {
"""
Лента выплат поставщикам по всему кооперативу — для совета. Опциональные фильтры: по поставщику-получателю и по статусам.
"""
marketplaceListOutgoingPayments(statuses: [MarketplaceOutgoingPaymentRequestStatus!], supplier_account: String): [MarketplaceOutgoingPaymentRequest!]!
marketplaceListOutgoingPayments(filter: MarketplaceListOutgoingPaymentsFilterInput): [MarketplaceOutgoingPaymentRequest!]!
"""
История выплат поставщику в столе поставщика — статусы по каждому заказу.
"""
marketplaceListOutgoingPaymentsAsSupplier(statuses: [MarketplaceOutgoingPaymentRequestStatus!]): [MarketplaceOutgoingPaymentRequest!]!
marketplaceListOutgoingPaymentsAsSupplier(filter: MarketplaceListOutgoingPaymentsAsSupplierFilterInput): [MarketplaceOutgoingPaymentRequest!]!
"""Список Offer'ов на модерации (admin)"""
marketplaceListPendingOffers(input: MarketplaceListPendingOffersInput): MarketplaceOfferPaginationResult!
@@ -13278,7 +13368,7 @@ type Query {
marketplaceListWriteoffProposals(data: MarketplaceListWriteoffProposalsInput!, options: PaginationInput): PaginatedMarketplaceWriteoffProposals!
"""
Кошельки пайщика, релевантные столу заказов: w.wal.share, w.wal.member, w.mkt.member
Кошельки пайщика, релевантные столу заказов: w.wal.share, w.wal.member, w.mkt.order
"""
marketplaceMemberWallet: MarketplaceMemberWallet!
@@ -13632,6 +13722,11 @@ input RemoveAvailableCategoryTypesInput {
categoryTypes: [CategoryTypeInput!]!
}
input RemoveSecretaryRoomInput {
"""Идентификатор комнаты в реестре, которую нужно удалить"""
id: String!
}
input ReplaceAvailableItemsInput {
"""ID категорий (целые категории)"""
categoryIds: [Int!]! = []
@@ -17,7 +17,4 @@ export class Ledger2WalletDTO {
@Field(() => String, { description: 'Доступный баланс' })
available!: string;
@Field(() => String, { description: 'Заблокированный баланс' })
blocked!: string;
}
@@ -28,9 +28,6 @@ export class ProgramWalletDTO {
@Field(() => String, { description: 'Доступный баланс (формат: "100.0000 RUB")' })
available!: string;
@Field(() => String, { description: 'Заблокированный баланс (формат: "100.0000 RUB")' })
blocked!: string;
@Field(() => String, { description: 'Паевой взнос (формат: "100.0000 RUB")' })
membership_contribution!: string;
@@ -49,7 +46,6 @@ export class ProgramWalletDTO {
dto.agreement_id = entity.agreement_id;
dto.username = entity.username;
dto.available = entity.available;
dto.blocked = entity.blocked;
dto.membership_contribution = entity.membership_contribution;
dto.blockNum = entity.blockNum;
return dto;
+23 -16
View File
@@ -26,22 +26,29 @@ const logger = winston.createLogger({
})
),
transports: [
new DailyRotateFile({
filename: 'logs/application-%DATE%.log',
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '20m',
maxFiles: '30d',
level: 'info',
}),
new DailyRotateFile({
filename: 'logs/error-%DATE%.log',
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '20m',
maxFiles: '30d',
level: 'error',
}),
// При генерации схемы (build-time codegen) файловые транспорты не подключаем:
// процесс запускается из-под пользователя рядом с работающим контейнером, чей logs/
// принадлежит root, — запись туда падает с EACCES. На рантайм не влияет (флаг ставится только в codegen).
...(process.env.CONTROLLER_SCHEMA_GEN
? []
: [
new DailyRotateFile({
filename: 'logs/application-%DATE%.log',
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '20m',
maxFiles: '30d',
level: 'info',
}),
new DailyRotateFile({
filename: 'logs/error-%DATE%.log',
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '20m',
maxFiles: '30d',
level: 'error',
}),
]),
new winston.transports.Console({
stderrLevels: ['error'],
level: config.env === 'development' ? 'debug' : 'info',
@@ -256,12 +256,20 @@ export class DecisionExpiredNotificationService implements OnModuleInit, OnModul
}
}
// Обновляем дату последней проверки (только это поле, чтобы не перезаписать другие параметры конфига)
// Обновляем дату последней проверки. ВАЖНО: read-modify-write по СВЕЖЕМУ
// config из БД, а не по захваченному при initialize() снимку `plugin`.
// Cron-замыкание держит in-memory снимок с момента boot; за время между
// boot и тиком онбординг-флаги (`onboarding_*_done/_hash`) и прочие поля
// могли быть записаны в БД другими сервисами. Перезапись устаревшего
// снимка стёрла бы их (lost update). Поэтому берём актуальный config и
// трогаем только lastCheckDate.
const fresh = await this.extensionRepository.findByName('chairman');
const baseConfig = fresh?.config ?? plugin.config;
const updatedConfig = {
...plugin.config,
...baseConfig,
lastCheckDate: new Date().toISOString(),
};
await this.extensionRepository.update({ ...plugin, config: updatedConfig });
await this.extensionRepository.update({ name: 'chairman', config: updatedConfig });
this.logger.debug('Проверка истекших решений завершена');
} catch (error) {
@@ -0,0 +1,70 @@
/**
* Matrix: комната секретаря — создаётся вручную председателем/советом из desktop.
* Всегда незашифрована (E2EE секретарь транскрибировать не может). Тип (public/private)
* выбирается при создании, поэтому isPrivate здесь не фиксируем — задаёт сервис.
*/
import type { MatrixChatRoomPreset } from './matrix-chat-room-preset.types';
/**
* Те же права, что у комнаты проекта Capital: модераторы (50) почти по всем настройкам;
* 100 только у Matrix-админа для критичных state.
*/
function buildPowerLevels(adminUserId: string): Record<string, unknown> {
return {
users_default: 0,
invite: 50,
kick: 50,
ban: 50,
redact: 50,
state_default: 50,
events_default: 0,
users: {
[adminUserId]: 100,
},
events: {
'm.room.name': 50,
'm.room.topic': 50,
'm.room.avatar': 50,
'm.room.pinned_events': 50,
'm.room.canonical_alias': 50,
'm.room.aliases': 50,
'm.room.power_levels': 100,
'm.room.history_visibility': 50,
'm.room.encryption': 100,
'm.room.tombstone': 100,
'm.room.server_acl': 100,
'im.vector.modular.widgets': 50,
'm.widget': 50,
'org.matrix.msc2876.widgets': 50,
'io.element.widgets.layout': 50,
'io.element.call': 50,
'io.element.call.member': 0,
'org.matrix.msc3401.call': 50,
'org.matrix.msc3401.call.member': 0,
'm.call': 50,
'm.call.invite': 50,
'm.call.answer': 0,
'm.call.hangup': 0,
'm.call.candidates': 0,
'm.call.select_answer': 0,
'm.call.reject': 0,
'm.call.negotiate': 0,
},
};
}
/**
* Базовый пресет комнаты секретаря. `isPrivate` переопределяется сервисом по флагу публичности.
* `encrypt` всегда false — иначе секретарь не сможет читать сообщения и транскрибировать.
*/
export const SECRETARY_ROOM_MATRIX: MatrixChatRoomPreset = {
label: 'Комната секретаря',
isPrivate: true,
encrypt: false,
roomType: undefined,
initialState: [],
buildPowerLevels,
};
@@ -23,6 +23,32 @@ export class ChatcoopProjectCommunicationRoomDTO {
displayLabel!: string;
}
/**
* Тип непроектной комнаты: пайщики, совет или комната секретаря.
*/
export enum NonProjectRoomKindGql {
MEMBERS = 'MEMBERS',
COUNCIL = 'COUNCIL',
SECRETARY = 'SECRETARY',
}
registerEnumType(NonProjectRoomKindGql, {
name: 'NonProjectRoomKind',
description: 'Тип комнаты вне проекта: пайщики, совет, комната секретаря',
});
@ObjectType('ChatcoopNonProjectCommunicationRoom')
export class ChatcoopNonProjectCommunicationRoomDTO {
@Field({ description: 'Идентификатор комнаты Matrix' })
matrixRoomId!: string;
@Field({ description: 'Подпись для отображения комнаты' })
displayLabel!: string;
@Field(() => NonProjectRoomKindGql, { description: 'Тип комнаты (пайщики / совет / секретарь)' })
kind!: NonProjectRoomKindGql;
}
@ObjectType('ChatcoopRoomMessageLine')
export class ChatcoopRoomMessageLineDTO {
@Field(() => Float, { description: 'origin_server_ts из Matrix (мс)' })
@@ -0,0 +1,60 @@
import { Field, InputType, ObjectType, registerEnumType } from '@nestjs/graphql';
import { IsBoolean, IsNotEmpty, IsString, MaxLength } from 'class-validator';
/**
* Тип комнаты в реестре ChatCoop. Редактировать (удалять) можно только `SECRETARY`;
* системные (`MEMBERS`/`COUNCIL`) и проектные (`CAPITAL_PROJECT`) защищены.
*/
export enum ManagedRoomKindGql {
MEMBERS = 'MEMBERS',
COUNCIL = 'COUNCIL',
CAPITAL_PROJECT = 'CAPITAL_PROJECT',
SECRETARY = 'SECRETARY',
}
registerEnumType(ManagedRoomKindGql, {
name: 'ManagedRoomKind',
description: 'Тип комнаты: пайщики, совет, проект Capital, комната секретаря',
});
@ObjectType('ChatcoopSecretaryRoom')
export class ChatcoopSecretaryRoomDTO {
@Field({ description: 'Внутренний идентификатор комнаты в реестре (для операций; это НЕ Matrix room id)' })
id!: string;
@Field({ description: 'Название комнаты' })
displayLabel!: string;
@Field(() => ManagedRoomKindGql, { description: 'Тип комнаты' })
kind!: ManagedRoomKindGql;
@Field({ description: 'Комната зашифрована (E2EE) — секретарь не транскрибирует такие комнаты' })
encrypted!: boolean;
@Field({ description: 'Секретарь присутствует в комнате' })
secretaryInRoom!: boolean;
@Field({ description: 'Можно ли удалить комнату из интерфейса (только комнаты секретаря)' })
editable!: boolean;
}
@InputType('CreateSecretaryRoomInput')
export class CreateSecretaryRoomInputDTO {
@Field({ description: 'Название комнаты' })
@IsString()
@IsNotEmpty()
@MaxLength(240)
displayName!: string;
@Field({ description: 'Публичная комната (любой может войти) либо приватная (вход по приглашению создателя)' })
@IsBoolean()
isPublic!: boolean;
}
@InputType('RemoveSecretaryRoomInput')
export class RemoveSecretaryRoomInputDTO {
@Field({ description: 'Идентификатор комнаты в реестре, которую нужно удалить' })
@IsString()
@IsNotEmpty()
id!: string;
}
@@ -39,12 +39,9 @@ export class CallTranscriptionResponseDTO {
@Field()
id!: string;
@Field()
@Field({ description: 'Внутреннее имя комнаты звонка (LiveKit room name), не Matrix room id' })
roomId!: string;
@Field()
matrixRoomId!: string;
@Field()
roomName!: string;
@@ -11,19 +11,33 @@ import { AuthRoles } from '~/application/auth/decorators/auth.decorator';
import { CurrentUser } from '~/application/auth/decorators/current-user.decorator';
import type { MonoAccountDomainInterface } from '~/domain/account/interfaces/mono-account-domain.interface';
import {
ChatcoopNonProjectCommunicationRoomDTO,
ChatcoopProjectCommunicationRoomDTO,
ChatcoopRoomMessageLineDTO,
GetMaxOriginServerTsForRoomInputDTO,
GetProjectCommunicationRoomsInputDTO,
GetRoomMessagesForUtcDateInputDTO,
ListUtcDatesWithNewRoomMessagesInputDTO,
NonProjectRoomKindGql,
RoomMessageKindGql,
} from '../dto/project-communication.dto';
import type { InterNonProjectRoomKind } from '@coopenomics/inter';
function mapKind(kind: 'text' | 'audio'): RoomMessageKindGql {
return kind === 'text' ? RoomMessageKindGql.TEXT : RoomMessageKindGql.AUDIO;
}
function mapNonProjectKind(kind: InterNonProjectRoomKind): NonProjectRoomKindGql {
switch (kind) {
case 'members':
return NonProjectRoomKindGql.MEMBERS;
case 'council':
return NonProjectRoomKindGql.COUNCIL;
case 'secretary':
return NonProjectRoomKindGql.SECRETARY;
}
}
/**
* Доступ к данным переписки Capital↔Matrix для синхронизации (blago-cli, секретарь).
* Источник — порт `INTER_PROJECT_COMMUNICATION_ARTIFACTS` (пакет inter).
@@ -61,6 +75,24 @@ export class ProjectCommunicationResolver {
}));
}
@Query(() => [ChatcoopNonProjectCommunicationRoomDTO], {
name: 'chatcoopListNonProjectCommunicationRooms',
description: 'Комнаты Matrix вне проектов Capital (пайщики/совет/секретарь) — для синхронизации в blago',
})
@AuthRoles(['chairman', 'member', 'user'])
async listNonProjectCommunicationRooms(
@CurrentUser() user: MonoAccountDomainInterface
): Promise<ChatcoopNonProjectCommunicationRoomDTO[]> {
this.ensureComm();
this.logger.debug(`chatcoopListNonProjectCommunicationRooms user=${user.username}`);
const rooms = await this.comm!.listNonProjectCommunicationRooms();
return rooms.map((r) => ({
matrixRoomId: r.matrixRoomId,
displayLabel: r.displayLabel,
kind: mapNonProjectKind(r.kind),
}));
}
@Query(() => [String], {
name: 'chatcoopListUtcDatesWithNewRoomMessages',
description:
@@ -0,0 +1,98 @@
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { Logger, UseGuards } from '@nestjs/common';
import { ActiveUserStatusGuard } from '~/application/auth/guards/active-user-status.guard';
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 { CurrentUser } from '~/application/auth/decorators/current-user.decorator';
import type { MonoAccountDomainInterface } from '~/domain/account/interfaces/mono-account-domain.interface';
import { SecretaryRoomManagementService } from '../services/secretary-room-management.service';
import {
ChatcoopSecretaryRoomDTO,
CreateSecretaryRoomInputDTO,
ManagedRoomKindGql,
RemoveSecretaryRoomInputDTO,
} from '../dto/secretary-room.dto';
import type { ChatcoopManagedMatrixRoomKind } from '../../domain/entities/managed-matrix-room.entity';
import type { ManagedMatrixRoomDomainEntity } from '../../domain/entities/managed-matrix-room.entity';
function mapManagedKind(kind: ChatcoopManagedMatrixRoomKind): ManagedRoomKindGql {
switch (kind) {
case 'members':
return ManagedRoomKindGql.MEMBERS;
case 'council':
return ManagedRoomKindGql.COUNCIL;
case 'capital_project':
return ManagedRoomKindGql.CAPITAL_PROJECT;
case 'secretary':
return ManagedRoomKindGql.SECRETARY;
}
}
function toDto(room: ManagedMatrixRoomDomainEntity): ChatcoopSecretaryRoomDTO {
return {
id: room.id,
displayLabel: room.displayLabel || 'Без названия',
kind: mapManagedKind(room.kind),
encrypted: room.encrypted,
secretaryInRoom: room.secretaryInRoom,
editable: room.kind === 'secretary',
};
}
/**
* Управление комнатами секретаря (стол связи desktop).
* Доступ — председатель и члены совета: они создают комнаты для звонков с секретарём и удаляют свои.
*/
@Resolver()
@UseGuards(GqlJwtAuthGuard, RolesGuard, ActiveUserStatusGuard)
export class SecretaryRoomsResolver {
private readonly logger = new Logger(SecretaryRoomsResolver.name);
constructor(private readonly service: SecretaryRoomManagementService) {}
@Query(() => [ChatcoopSecretaryRoomDTO], {
name: 'chatcoopListSecretaryRooms',
description: 'Все комнаты реестра ChatCoop (системные/проектные — read-only, комнаты секретаря — удаляемые)',
})
@AuthRoles(['chairman', 'member'])
async listSecretaryRooms(
@CurrentUser() user: MonoAccountDomainInterface
): Promise<ChatcoopSecretaryRoomDTO[]> {
this.logger.debug(`chatcoopListSecretaryRooms user=${user.username}`);
const rooms = await this.service.listRooms();
return rooms.map(toDto);
}
@Mutation(() => ChatcoopSecretaryRoomDTO, {
name: 'chatcoopCreateSecretaryRoom',
description: 'Создать комнату с секретарём (публичную или приватную); секретарь подключается сразу',
})
@AuthRoles(['chairman', 'member'])
async createSecretaryRoom(
@CurrentUser() user: MonoAccountDomainInterface,
@Args('data', { type: () => CreateSecretaryRoomInputDTO }) data: CreateSecretaryRoomInputDTO
): Promise<ChatcoopSecretaryRoomDTO> {
this.logger.log(`chatcoopCreateSecretaryRoom user=${user.username} isPublic=${data.isPublic}`);
const room = await this.service.createSecretaryRoom({
creatorUsername: user.username,
creatorRole: user.role,
displayName: data.displayName,
isPublic: data.isPublic,
});
return toDto(room);
}
@Mutation(() => String, {
name: 'chatcoopRemoveSecretaryRoom',
description: 'Удалить комнату секретаря: вывести секретаря и снять комнату с синхронизации (возвращает идентификатор комнаты в реестре)',
})
@AuthRoles(['chairman', 'member'])
async removeSecretaryRoom(
@CurrentUser() user: MonoAccountDomainInterface,
@Args('data', { type: () => RemoveSecretaryRoomInputDTO }) data: RemoveSecretaryRoomInputDTO
): Promise<string> {
this.logger.log(`chatcoopRemoveSecretaryRoom user=${user.username} room=${data.id}`);
return this.service.removeSecretaryRoom(data.id);
}
}
@@ -54,7 +54,6 @@ export class TranscriptionResolver {
return {
id: domain.id,
roomId: domain.roomId,
matrixRoomId: domain.matrixRoomId,
roomName: domain.roomName,
startedAt: domain.startedAt,
endedAt: domain.endedAt,
@@ -818,6 +818,48 @@ export class MatrixApiService {
}
}
/**
* Приглашает пользователя в комнату (от имени Matrix-админа, который состоит в комнате).
* Пользователь увидит приглашение и войдёт сам — в отличие от {@link joinRoom} (force-join).
*/
async inviteUser(userId: string, roomId: string): Promise<void> {
try {
const adminToken = await this.loginAdmin();
await this.httpClient.post(
`/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/invite`,
{ user_id: userId },
{ headers: { Authorization: `Bearer ${adminToken}` } }
);
this.logger.log(`Пользователь ${userId} приглашён в комнату ${roomId}`);
} catch (error: any) {
this.logger.error(
`Не удалось пригласить пользователя ${userId} в комнату ${roomId}: ${JSON.stringify(error?.response?.data)}`
);
throw new Error('Не удалось пригласить пользователя в комнату');
}
}
/**
* Исключает пользователя из комнаты (от имени Matrix-админа с power 100).
* Используется для вывода секретаря из комнаты по требованию председателя/совета.
*/
async kickUser(userId: string, roomId: string, reason?: string): Promise<void> {
try {
const adminToken = await this.loginAdmin();
await this.httpClient.post(
`/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/kick`,
reason ? { user_id: userId, reason } : { user_id: userId },
{ headers: { Authorization: `Bearer ${adminToken}` } }
);
this.logger.log(`Пользователь ${userId} исключён из комнаты ${roomId}`);
} catch (error: any) {
this.logger.error(
`Не удалось исключить пользователя ${userId} из комнаты ${roomId}: ${JSON.stringify(error?.response?.data)}`
);
throw new Error('Не удалось исключить пользователя из комнаты');
}
}
/**
* Получает текущие права пользователей в комнате
*/
@@ -0,0 +1,152 @@
import { BadRequestException, Inject, Injectable, Logger, NotFoundException } from '@nestjs/common';
import { MatrixApiService } from './matrix-api.service';
import { ChatCoopApplicationService } from './chatcoop-application.service';
import { MatrixUserManagementService } from '../../domain/services/matrix-user-management.service';
import { SECRETARY_ROOM_MATRIX } from '../config/matrix-secretary-room.config';
import {
CHATCOOP_MANAGED_MATRIX_ROOM_REPOSITORY,
type ChatcoopManagedMatrixRoomRepository,
} from '../../domain/repositories/managed-matrix-room.repository';
import {
CHATCOOP_STATE_REPOSITORY,
type ChatcoopStateRepository,
} from '../../domain/repositories/chatcoop-state.repository';
import type { ManagedMatrixRoomDomainEntity } from '../../domain/entities/managed-matrix-room.entity';
export interface CreateSecretaryRoomInput {
/** Логин пайщика-создателя (председатель или член совета) */
creatorUsername: string;
/** Роль создателя в кооперативе — для прав в комнате (chairman / member) */
creatorRole: string;
/** Название комнаты */
displayName: string;
/** true — публичная (любой может войти), false — приватная (вход по приглашению) */
isPublic: boolean;
}
/**
* Управление комнатами секретаря: создание/удаление и список всех комнат реестра.
*
* Принцип безопасности: секретарь присутствует ТОЛЬКО в комнатах, созданных нашим бэкендом
* (kind `secretary`, а также системные members/council и проектные capital_project). Произвольные
* «чужие» Matrix-комнаты сюда не подключаются — Synapse общий на все кооперативы, и force-join в
* чужую комнату был бы дырой.
*/
@Injectable()
export class SecretaryRoomManagementService {
private readonly logger = new Logger(SecretaryRoomManagementService.name);
constructor(
private readonly matrixApi: MatrixApiService,
private readonly chatCoopApplicationService: ChatCoopApplicationService,
private readonly matrixUserManagement: MatrixUserManagementService,
@Inject(CHATCOOP_MANAGED_MATRIX_ROOM_REPOSITORY)
private readonly managedRooms: ChatcoopManagedMatrixRoomRepository,
@Inject(CHATCOOP_STATE_REPOSITORY)
private readonly chatcoopState: ChatcoopStateRepository
) {}
async listRooms(): Promise<ManagedMatrixRoomDomainEntity[]> {
return this.managedRooms.findAll();
}
async createSecretaryRoom(input: CreateSecretaryRoomInput): Promise<ManagedMatrixRoomDomainEntity> {
const st = await this.chatcoopState.getSingleton();
if (!st.isInitialized || !st.spaceId || st.spaceId.trim().length === 0) {
throw new BadRequestException('ChatCoop не инициализирован — нельзя создать комнату секретаря');
}
const secretaryId = st.secretaryMatrixUserId;
if (typeof secretaryId !== 'string' || secretaryId.trim().length === 0) {
throw new BadRequestException('Секретарь не инициализирован — нельзя создать комнату секретаря');
}
const displayName = input.displayName.trim();
if (displayName.length === 0) {
throw new BadRequestException('Название комнаты не может быть пустым');
}
const adminUserId = this.matrixApi.getAdminUserId();
const powerLevels = SECRETARY_ROOM_MATRIX.buildPowerLevels(adminUserId);
const isPrivate = !input.isPublic;
const roomId = await this.matrixApi.createRoom(
displayName.slice(0, 240),
`Комната секретаря — создал ${input.creatorUsername}`,
isPrivate,
SECRETARY_ROOM_MATRIX.roomType,
SECRETARY_ROOM_MATRIX.initialState.length > 0 ? SECRETARY_ROOM_MATRIX.initialState : undefined,
// Комната секретаря всегда незашифрована — иначе секретарь не читает сообщения и не транскрибирует.
false,
powerLevels as Record<string, unknown>
);
await this.matrixApi.addRoomToSpace(st.spaceId.trim(), roomId);
// Force-join только секретарь.
await this.matrixApi.joinRoom(secretaryId.trim(), roomId);
// Создатель сразу входит в свою комнату с правами модератора.
const creatorMatrix = await this.matrixUserManagement.getMatrixUserByCoopUsername(input.creatorUsername);
if (creatorMatrix) {
try {
await this.matrixApi.joinRoom(creatorMatrix.matrixUserId, roomId);
await this.chatCoopApplicationService.applyMembersRoomStylePowerForUser(
creatorMatrix.matrixUserId,
roomId,
input.creatorRole
);
} catch (err) {
this.logger.warn(`Не удалось ввести создателя ${input.creatorUsername} в комнату ${roomId}: ${String(err)}`);
}
} else {
this.logger.warn(`Нет Matrix-аккаунта для создателя ${input.creatorUsername} — он войдёт позже сам`);
}
const room = await this.managedRooms.upsertRoom({
matrixRoomId: roomId,
encrypted: false,
kind: 'secretary',
displayLabel: displayName,
projectHash: null,
secretaryInRoom: true,
});
this.logger.log(
`Создана комната секретаря ${roomId} (${isPrivate ? 'приватная' : 'публичная'}) создателем ${input.creatorUsername}`
);
return room;
}
/**
* Удаляет комнату секретаря: выводит секретаря из Matrix-комнаты и убирает запись из реестра ChatCoop.
* Сама Matrix-комната не уничтожается — её участники сохраняют доступ и историю, но транскрипция/синхронизация прекращаются.
* Разрешено только для kind `secretary` — системные и проектные комнаты так удалять нельзя.
*
* На вход — внутренний идентификатор реестра (`id`), а НЕ Matrix room id: интерфейс не оперирует
* Matrix room id, чтобы не раздавать handle для входа в приватные комнаты.
*/
async removeSecretaryRoom(id: string): Promise<string> {
const room = await this.managedRooms.findById(id);
if (!room) {
throw new NotFoundException('Комната не найдена в реестре ChatCoop');
}
if (room.kind !== 'secretary') {
throw new BadRequestException('Удалять можно только комнаты секретаря (системные и проектные защищены)');
}
const matrixRoomId = room.matrixRoomId;
const st = await this.chatcoopState.getSingleton();
const secretaryId = st.secretaryMatrixUserId;
if (typeof secretaryId === 'string' && secretaryId.trim().length > 0) {
try {
await this.matrixApi.kickUser(secretaryId.trim(), matrixRoomId, 'Комната секретаря удалена');
} catch (err) {
this.logger.warn(`Не удалось вывести секретаря из ${matrixRoomId}: ${String(err)}`);
}
}
await this.managedRooms.setSecretaryInRoom(matrixRoomId, false);
await this.managedRooms.deleteByMatrixRoomId(matrixRoomId);
this.logger.log(`Комната секретаря ${id} удалена из реестра`);
return id;
}
}
@@ -13,6 +13,8 @@ import { ChatCoopCalendarResolver } from './application/resolvers/chatcoop-calen
import { ChatCoopCalendarFeedController } from './application/controllers/chatcoop-calendar-feed.controller';
import { TranscriptionResolver } from './application/resolvers/transcription.resolver';
import { ProjectCommunicationResolver } from './application/resolvers/project-communication.resolver';
import { SecretaryRoomsResolver } from './application/resolvers/secretary-rooms.resolver';
import { SecretaryRoomManagementService } from './application/services/secretary-room-management.service';
import { WinstonLoggerService } from '~/application/logger/logger-app.service';
import { ConfigModule } from '@nestjs/config';
import { z } from 'zod';
@@ -527,6 +529,7 @@ export class ChatCoopPlugin extends BaseExtModule {
// Application Services
ChatCoopApplicationService,
CapitalProjectMatrixSyncService,
SecretaryRoomManagementService,
MatrixApiService,
ChatCoopSecretaryMatrixTokenService,
SecretaryAgentService,
@@ -604,6 +607,7 @@ export class ChatCoopPlugin extends BaseExtModule {
TranscriptionResolver,
ActiveUserStatusGuard,
ProjectCommunicationResolver,
SecretaryRoomsResolver,
],
exports: [
ChatCoopPlugin,
@@ -1,7 +1,7 @@
/**
* Тип комнаты Matrix, зарегистрированной в реестре ChatCoop (централизованное хранение).
*/
export type ChatcoopManagedMatrixRoomKind = 'members' | 'council' | 'capital_project';
export type ChatcoopManagedMatrixRoomKind = 'members' | 'council' | 'capital_project' | 'secretary';
/**
* Запись о Matrix-комнате кооператива: источник правды для LiveKit/секретаря; совет при миграции v3 из legacy-конфига.
@@ -20,10 +20,16 @@ export interface UpsertManagedMatrixRoomInput {
*/
export interface ChatcoopManagedMatrixRoomRepository {
upsertRoom(input: UpsertManagedMatrixRoomInput): Promise<ManagedMatrixRoomDomainEntity>;
/** Поиск по внутреннему идентификатору реестра (не Matrix room id) — для операций из интерфейса. */
findById(id: string): Promise<ManagedMatrixRoomDomainEntity | null>;
findByMatrixRoomId(matrixRoomId: string): Promise<ManagedMatrixRoomDomainEntity | null>;
findByKind(kind: ChatcoopManagedMatrixRoomKind): Promise<ManagedMatrixRoomDomainEntity[]>;
/** Комнаты проекта Capital (kind capital_project, projectHash задан). */
findByProjectHash(projectHash: string): Promise<ManagedMatrixRoomDomainEntity[]>;
/** Все комнаты реестра (для интерфейса управления присутствием секретаря). */
findAll(): Promise<ManagedMatrixRoomDomainEntity[]>;
/** Комнаты, не привязанные к проекту Capital (members/council/secretary) — для синхронизации в blago. */
findNonProjectCommunicationRooms(): Promise<ManagedMatrixRoomDomainEntity[]>;
/** Комнаты, в которых секретарь может участвовать в звонке и писать plaintext в Matrix */
findEligibleForSecretaryTranscription(): Promise<ManagedMatrixRoomDomainEntity[]>;
/** Обновить флаг членства секретаря (после успешного join или проверки Matrix) */
@@ -1,6 +1,8 @@
import { Inject, Injectable } from '@nestjs/common';
import type {
InterCompletedCallTranscriptionHead,
InterNonProjectCommunicationRoomRef,
InterNonProjectRoomKind,
InterProjectCommunicationArtifactsPort,
InterProjectCommunicationRoomRef,
InterRoomMessageLine,
@@ -48,6 +50,19 @@ export class ChatcoopInterProjectCommunicationArtifactsAdapter implements InterP
}));
}
async listNonProjectCommunicationRooms(): Promise<InterNonProjectCommunicationRoomRef[]> {
const rooms = await this.managedRooms.findNonProjectCommunicationRooms();
return rooms
.filter((r): r is typeof r & { kind: InterNonProjectRoomKind } =>
r.kind === 'members' || r.kind === 'council' || r.kind === 'secretary'
)
.map((r) => ({
matrixRoomId: r.matrixRoomId,
displayLabel: r.displayLabel || r.matrixRoomId,
kind: r.kind,
}));
}
async listUtcDatesWithNewMessages(
matrixRoomId: string,
afterOriginServerTsExclusive: number
@@ -3,7 +3,7 @@ import type { ChatcoopManagedMatrixRoomKind } from '../../domain/entities/manage
import { ManagedMatrixRoomTypeormEntity } from '../entities/managed-matrix-room.typeorm-entity';
function parseKind(raw: string): ChatcoopManagedMatrixRoomKind {
if (raw === 'members' || raw === 'council' || raw === 'capital_project') {
if (raw === 'members' || raw === 'council' || raw === 'capital_project' || raw === 'secretary') {
return raw;
}
return 'capital_project';
@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Not, Repository } from 'typeorm';
import type {
ChatcoopManagedMatrixRoomRepository,
UpsertManagedMatrixRoomInput,
@@ -39,6 +39,11 @@ export class ManagedMatrixRoomTypeormRepository implements ChatcoopManagedMatrix
return ManagedMatrixRoomMapper.toDomain(row);
}
async findById(id: string): Promise<ManagedMatrixRoomDomainEntity | null> {
const row = await this.repository.findOne({ where: { id } });
return row ? ManagedMatrixRoomMapper.toDomain(row) : null;
}
async findByMatrixRoomId(matrixRoomId: string): Promise<ManagedMatrixRoomDomainEntity | null> {
const row = await this.repository.findOne({ where: { matrixRoomId } });
return row ? ManagedMatrixRoomMapper.toDomain(row) : null;
@@ -56,6 +61,16 @@ export class ManagedMatrixRoomTypeormRepository implements ChatcoopManagedMatrix
return rows.map(ManagedMatrixRoomMapper.toDomain);
}
async findAll(): Promise<ManagedMatrixRoomDomainEntity[]> {
const rows = await this.repository.find();
return rows.map(ManagedMatrixRoomMapper.toDomain);
}
async findNonProjectCommunicationRooms(): Promise<ManagedMatrixRoomDomainEntity[]> {
const rows = await this.repository.find({ where: { roomKind: Not('capital_project') } });
return rows.map(ManagedMatrixRoomMapper.toDomain);
}
async findEligibleForSecretaryTranscription(): Promise<ManagedMatrixRoomDomainEntity[]> {
const rows = await this.repository.find({ where: { encrypted: false } });
return rows.map(ManagedMatrixRoomMapper.toDomain);
@@ -46,7 +46,7 @@ export class MarketplaceDesktopGrantsProvider
private readonly grantsRegistry: ExtensionGrantsRegistry,
@Inject(MARKETPLACE_WHITELIST_SERVICE)
private readonly whitelistService: MarketplaceWhitelistService,
@Inject(MARKETPLACE_KU_CHAIRMEN_SERVICE)
@Inject(MARKETPLACE_KU_CHAIRMAN_SERVICE)
private readonly kuChairmanService: MarketplaceKuChairmanService,
) {}
@@ -0,0 +1,52 @@
import { ForbiddenException, Inject, Injectable, NotFoundException } from '@nestjs/common';
import {
BRANCH_BLOCKCHAIN_PORT,
type BranchBlockchainPort,
} from '~/domain/branch/interfaces/branch-blockchain.port';
export const MARKETPLACE_BRANCH_OWNERSHIP_SERVICE = Symbol(
'MarketplaceBranchOwnershipService'
);
/**
* Проверяет, что аккаунт правомочен оперировать от имени кооперативного участка.
*
* Правомочны: председатель КУ (`branch.trustee`) либо его доверенные лица
* (`branch.trusted[]`, до 3 individual-аккаунтов; см. `branch::addtrusted`).
*
* Branches не реплицируются в Postgres — chain RPC через port. ADR-011
* не нарушается: read-path вычисляет authorization, а не отдаёт прикладную
* сущность пользователю.
*/
@Injectable()
export class MarketplaceBranchOwnershipService {
constructor(
@Inject(BRANCH_BLOCKCHAIN_PORT)
private readonly branchPort: BranchBlockchainPort
) {}
async canActAsBraname(coopname: string, account: string, braname: string): Promise<boolean> {
const branch = await this.branchPort.getBranch(coopname, braname);
if (!branch) return false;
if (branch.trustee === account) return true;
return branch.trusted?.includes(account) ?? false;
}
async assertCanActAsBraname(
coopname: string,
account: string,
braname: string
): Promise<void> {
const branch = await this.branchPort.getBranch(coopname, braname);
if (!branch) {
throw new NotFoundException(`Кооперативный участок ${braname} не найден.`);
}
const allowed =
branch.trustee === account || (branch.trusted?.includes(account) ?? false);
if (!allowed) {
throw new ForbiddenException(
'Действие доступно только председателю кооперативного участка или его доверенному лицу.'
);
}
}
}
@@ -64,6 +64,29 @@ export class MeetTrackerService {
this.logger.info('MeetTrackerService инициализирован');
}
/**
* Сохраняет состояние трекинга собраний read-modify-write по СВЕЖЕМУ config.
*
* `this.pluginConfig` — снимок, захваченный при initialize() (boot), а
* cron-проверка крутится часами; update() заменяет весь config JSONB целиком.
* Если писать захваченный снимок, затрутся поля, записанные в БД после boot
* другими сервисами (онбординг-флаги и т.п.) — это и есть наблюдавшийся
* сброс онбординга. Поэтому берём актуальный config из БД и накладываем
* только поля, которыми владеет meet-tracker.
*/
private async persistTrackedState(): Promise<void> {
const fresh = await this.extensionRepository.findByName(this.extensionName);
const baseConfig = fresh?.config ?? this.pluginConfig.config;
const nextConfig: IConfig = {
...baseConfig,
trackedMeets: this.pluginConfig.config.trackedMeets,
closedMeetIds: this.pluginConfig.config.closedMeetIds,
lastCheckTimestamp: this.pluginConfig.config.lastCheckTimestamp,
};
await this.extensionRepository.update({ name: this.extensionName, config: nextConfig });
this.pluginConfig = { ...this.pluginConfig, config: nextConfig };
}
// Приватная функция для обновления trackedMeet на основе свежих данных
private getUpdatedTrackedMeet(trackedMeet: TrackedMeet, meetData: any, extendedStatus: string): TrackedMeet {
return {
@@ -145,7 +168,7 @@ export class MeetTrackerService {
this.pluginConfig.config.trackedMeets[trackedMeetIndex].notifications.endNotification = true;
if (!closedMeetIds.includes(meetID)) {
closedMeetIds.push(meetID);
await this.extensionRepository.update(this.pluginConfig);
await this.persistTrackedState();
}
}
this.logger.info(`Удаление закрытого собрания ${meetHash} (№${meetID}) из списка отслеживаемых`);
@@ -310,7 +333,7 @@ export class MeetTrackerService {
this.pluginConfig.config.lastCheckTimestamp = now.toISOString();
// Сохраняем изменения в конфигурации
await this.extensionRepository.update(this.pluginConfig);
await this.persistTrackedState();
} catch (error: any) {
this.logger.error(`Ошибка при проверке собраний: ${error.message}`, error.stack);
}
@@ -215,8 +215,18 @@ export class PowerupPlugin extends BaseExtModule implements OnModuleDestroy {
await this.blockchainPort.powerUp(username, quantity);
this.plugin.config.lastDailyReplenishmentDate = new Date().toISOString();
await this.extensionRepository.update(this.plugin);
// read-modify-write по СВЕЖЕМУ config: daily-cron держит in-memory снимок
// `this.plugin` с момента boot, а update() заменяет весь config JSONB
// целиком. Перезапись устаревшего снимка стёрла бы поля, записанные за
// сутки другими сервисами (онбординг и т.п.). Берём актуальный config и
// трогаем только lastDailyReplenishmentDate.
const fresh = await this.extensionRepository.findByName(this.name);
const nextConfig = {
...(fresh?.config ?? this.plugin.config),
lastDailyReplenishmentDate: new Date().toISOString(),
};
await this.extensionRepository.update({ name: this.name, config: nextConfig });
this.plugin = { ...this.plugin, config: nextConfig };
await this.log({
type: 'daily',
+40 -8
View File
@@ -317,6 +317,9 @@ export const AllTypesProps: Record<string,any> = {
attributes:"RequestAttributeInput",
images:"RequestImageInput",
type:"RequestTypeInput"
},
CreateSecretaryRoomInput:{
},
CreateSovietIndividualDataInput:{
passport:"PassportInput"
@@ -621,6 +624,7 @@ export const AllTypesProps: Record<string,any> = {
MakeClearanceInput:{
document:"SignedDigitalDocumentInput"
},
ManagedRoomKind: "enum" as const,
MarkReportPeriodInput:{
mark:"ReportSubmissionMark",
reportType:"ReportType"
@@ -754,6 +758,12 @@ export const AllTypesProps: Record<string,any> = {
MarketplaceListOrdersInput:{
statuses:"MarketplaceOrderStatus"
},
MarketplaceListOutgoingPaymentsAsSupplierFilterInput:{
statuses:"MarketplaceOutgoingPaymentRequestStatus"
},
MarketplaceListOutgoingPaymentsFilterInput:{
statuses:"MarketplaceOutgoingPaymentRequestStatus"
},
MarketplaceListPendingOffersInput:{
},
@@ -1118,8 +1128,14 @@ export const AllTypesProps: Record<string,any> = {
chatcoopCreateCalendarEvent:{
data:"CreateChatCoopCalendarEventInput"
},
chatcoopCreateSecretaryRoom:{
data:"CreateSecretaryRoomInput"
},
chatcoopDeleteCalendarEvent:{
},
chatcoopRemoveSecretaryRoom:{
data:"RemoveSecretaryRoomInput"
},
chatcoopUpdateCalendarEvent:{
data:"UpdateChatCoopCalendarEventInput"
@@ -1508,6 +1524,7 @@ export const AllTypesProps: Record<string,any> = {
input:"WalmoveInput"
}
},
NonProjectRoomKind: "enum" as const,
NotificationWorkflowRecipientInput:{
},
@@ -1980,10 +1997,10 @@ export const AllTypesProps: Record<string,any> = {
options:"PaginationInput"
},
marketplaceListOutgoingPayments:{
statuses:"MarketplaceOutgoingPaymentRequestStatus"
filter:"MarketplaceListOutgoingPaymentsFilterInput"
},
marketplaceListOutgoingPaymentsAsSupplier:{
statuses:"MarketplaceOutgoingPaymentRequestStatus"
filter:"MarketplaceListOutgoingPaymentsAsSupplierFilterInput"
},
marketplaceListPendingOffers:{
input:"MarketplaceListPendingOffersInput"
@@ -2085,6 +2102,9 @@ export const AllTypesProps: Record<string,any> = {
},
RemoveAvailableCategoryTypesInput:{
categoryTypes:"CategoryTypeInput"
},
RemoveSecretaryRoomInput:{
},
ReplaceAvailableItemsInput:{
categoryTypes:"CategoryTypeInput"
@@ -2705,7 +2725,6 @@ export const ReturnTypes: Record<string,any> = {
createdAt:"DateTime",
endedAt:"DateTime",
id:"String",
matrixRoomId:"String",
memo:"String",
participants:"String",
roomId:"String",
@@ -3384,6 +3403,11 @@ export const ReturnTypes: Record<string,any> = {
displayLabel:"String",
matrixRoomId:"String"
},
ChatcoopNonProjectCommunicationRoom:{
displayLabel:"String",
kind:"NonProjectRoomKind",
matrixRoomId:"String"
},
ChatcoopProjectCommunicationRoom:{
displayLabel:"String",
matrixRoomId:"String"
@@ -3395,6 +3419,14 @@ export const ReturnTypes: Record<string,any> = {
kind:"RoomMessageKind",
originServerTs:"Float"
},
ChatcoopSecretaryRoom:{
displayLabel:"String",
editable:"Boolean",
encrypted:"Boolean",
id:"String",
kind:"ManagedRoomKind",
secretaryInRoom:"Boolean"
},
ContactsDTO:{
chairman:"PublicChairman",
details:"OrganizationDetails",
@@ -3808,7 +3840,6 @@ export const ReturnTypes: Record<string,any> = {
},
Ledger2Wallet:{
available:"String",
blocked:"String",
id:"String",
name:"String"
},
@@ -4165,9 +4196,7 @@ export const ReturnTypes: Record<string,any> = {
},
MarketplaceOrderCreateTxSnapshot:{
block_num:"Int",
blocked_amount:"String",
did_assign:"Boolean",
did_convert:"Boolean",
locked_amount:"String",
signed_at:"String",
tx_hash:"String"
},
@@ -4632,7 +4661,9 @@ export const ReturnTypes: Record<string,any> = {
chatcoopCreateAccount:"Boolean",
chatcoopCreateCalendarEvent:"ChatCoopCalendarEvent",
chatcoopCreateCalendarIcsSubscription:"ChatCoopCalendarIcsUrlResponse",
chatcoopCreateSecretaryRoom:"ChatcoopSecretaryRoom",
chatcoopDeleteCalendarEvent:"Boolean",
chatcoopRemoveSecretaryRoom:"String",
chatcoopUpdateCalendarEvent:"ChatCoopCalendarEvent",
chatcoopUpdateTranscriptionMemo:"CallTranscription",
completeCapitalOnboardingStep:"CapitalOnboardingState",
@@ -5169,7 +5200,6 @@ export const ReturnTypes: Record<string,any> = {
agreement_id:"ID",
available:"String",
blockNum:"Float",
blocked:"String",
coopname:"String",
id:"ID",
membership_contribution:"String",
@@ -5258,7 +5288,9 @@ export const ReturnTypes: Record<string,any> = {
chatcoopGetTranscriptions:"CallTranscription",
chatcoopListCalendarEvents:"ChatCoopCalendarEvent",
chatcoopListCalendarRooms:"ChatCoopCalendarRoomOption",
chatcoopListNonProjectCommunicationRooms:"ChatcoopNonProjectCommunicationRoom",
chatcoopListProjectCommunicationRooms:"ChatcoopProjectCommunicationRoom",
chatcoopListSecretaryRooms:"ChatcoopSecretaryRoom",
chatcoopListUtcDatesWithNewRoomMessages:"String",
checkReportReadiness:"ReportReadinessView",
cooperativeAgreements:"CoopAgreement",
+301 -70
View File
@@ -2163,11 +2163,11 @@ export type ValueTypes = {
createdAt?:boolean | `@${string}`,
endedAt?:boolean | `@${string}`,
id?:boolean | `@${string}`,
matrixRoomId?:boolean | `@${string}`,
/** Пользовательская заметка о содержании звонка */
memo?:boolean | `@${string}`,
/** Отображаемые имена участников (Synapse displayname); в БД хранятся канонические Matrix user id */
participants?:boolean | `@${string}`,
/** Внутреннее имя комнаты звонка (LiveKit room name), не Matrix room id */
roomId?:boolean | `@${string}`,
roomName?:boolean | `@${string}`,
startedAt?:boolean | `@${string}`,
@@ -3689,6 +3689,16 @@ export type ValueTypes = {
matrixRoomId?:boolean | `@${string}`,
__typename?: boolean | `@${string}`,
['...on ChatCoopCalendarRoomOption']?: Omit<ValueTypes["ChatCoopCalendarRoomOption"], "...on ChatCoopCalendarRoomOption">
}>;
["ChatcoopNonProjectCommunicationRoom"]: AliasType<{
/** Подпись для отображения комнаты */
displayLabel?:boolean | `@${string}`,
/** Тип комнаты (пайщики / совет / секретарь) */
kind?:boolean | `@${string}`,
/** Идентификатор комнаты Matrix */
matrixRoomId?:boolean | `@${string}`,
__typename?: boolean | `@${string}`,
['...on ChatcoopNonProjectCommunicationRoom']?: Omit<ValueTypes["ChatcoopNonProjectCommunicationRoom"], "...on ChatcoopNonProjectCommunicationRoom">
}>;
["ChatcoopProjectCommunicationRoom"]: AliasType<{
/** Подпись для отображения (комната / проект Capital) */
@@ -3710,6 +3720,22 @@ export type ValueTypes = {
originServerTs?:boolean | `@${string}`,
__typename?: boolean | `@${string}`,
['...on ChatcoopRoomMessageLine']?: Omit<ValueTypes["ChatcoopRoomMessageLine"], "...on ChatcoopRoomMessageLine">
}>;
["ChatcoopSecretaryRoom"]: AliasType<{
/** Название комнаты */
displayLabel?:boolean | `@${string}`,
/** Можно ли удалить комнату из интерфейса (только комнаты секретаря) */
editable?:boolean | `@${string}`,
/** Комната зашифрована (E2EE) — секретарь не транскрибирует такие комнаты */
encrypted?:boolean | `@${string}`,
/** Внутренний идентификатор комнаты в реестре (для операций; это НЕ Matrix room id) */
id?:boolean | `@${string}`,
/** Тип комнаты */
kind?:boolean | `@${string}`,
/** Секретарь присутствует в комнате */
secretaryInRoom?:boolean | `@${string}`,
__typename?: boolean | `@${string}`,
['...on ChatcoopSecretaryRoom']?: Omit<ValueTypes["ChatcoopSecretaryRoom"], "...on ChatcoopSecretaryRoom">
}>;
["CheckMatrixUsernameInput"]: {
username: string | Variable<any, string>
@@ -4442,6 +4468,12 @@ export type ValueTypes = {
weightUnit?: string | undefined | null | Variable<any, string>,
/** Ширина упаковки */
width?: number | undefined | null | Variable<any, string>
};
["CreateSecretaryRoomInput"]: {
/** Название комнаты */
displayName: string | Variable<any, string>,
/** Публичная комната (любой может войти) либо приватная (вход по приглашению создателя) */
isPublic: boolean | Variable<any, string>
};
["CreateSovietIndividualDataInput"]: {
/** Дата рождения */
@@ -5931,8 +5963,6 @@ export type ValueTypes = {
["Ledger2Wallet"]: AliasType<{
/** Доступный баланс */
available?:boolean | `@${string}`,
/** Заблокированный баланс */
blocked?:boolean | `@${string}`,
/** eosio::name-идентификатор кошелька (w.<contract>.<waltype>) */
id?:boolean | `@${string}`,
/** Название кошелька */
@@ -6028,6 +6058,8 @@ export type ValueTypes = {
/** Имя пользователя */
username: string | Variable<any, string>
};
/** Тип комнаты: пайщики, совет, проект Capital, комната секретаря */
["ManagedRoomKind"]:ManagedRoomKind;
["MarkReportPeriodInput"]: {
mark?: ValueTypes["ReportSubmissionMark"] | undefined | null | Variable<any, string>,
period?: number | undefined | null | Variable<any, string>,
@@ -6868,6 +6900,16 @@ export type ValueTypes = {
statuses?: Array<ValueTypes["MarketplaceOrderStatus"]> | undefined | null | Variable<any, string>,
/** Фильтр по аккаунту поставщика. */
supplier_account?: string | undefined | null | Variable<any, string>
};
["MarketplaceListOutgoingPaymentsAsSupplierFilterInput"]: {
/** Фильтр по статусам выплат. Пусто — показывать все статусы. */
statuses?: Array<ValueTypes["MarketplaceOutgoingPaymentRequestStatus"]> | undefined | null | Variable<any, string>
};
["MarketplaceListOutgoingPaymentsFilterInput"]: {
/** Фильтр по статусам выплат. Пусто — показывать все статусы. */
statuses?: Array<ValueTypes["MarketplaceOutgoingPaymentRequestStatus"]> | undefined | null | Variable<any, string>,
/** Поставщик-получатель выплаты. Пусто — по всем поставщикам. */
supplier_account?: string | undefined | null | Variable<any, string>
};
["MarketplaceListPendingOffersInput"]: {
/** Количество элементов на странице */
@@ -6903,7 +6945,7 @@ export type ValueTypes = {
["MarketplaceMemberWallet"]: AliasType<{
coopname?:boolean | `@${string}`,
username?:boolean | `@${string}`,
/** Релевантные стол-заказам USER_SHARED-кошельки пайщика; порядок: share → member → mkt.member */
/** Релевантные стол-заказам USER_SHARED-кошельки пайщика; порядок: share → member → mkt.order */
wallets?:ValueTypes["MarketplaceWalletEntry"],
__typename?: boolean | `@${string}`,
['...on MarketplaceMemberWallet']?: Omit<ValueTypes["MarketplaceMemberWallet"], "...on MarketplaceMemberWallet">
@@ -7056,16 +7098,12 @@ export type ValueTypes = {
__typename?: boolean | `@${string}`,
['...on MarketplaceOrder']?: Omit<ValueTypes["MarketplaceOrder"], "...on MarketplaceOrder">
}>;
/** Снимок транзакции блокировки средств: ссылки на блок, флаги конверсии и сумма. */
/** Снимок транзакции резервирования средств: ссылки на блок и сумма резерва. */
["MarketplaceOrderCreateTxSnapshot"]: AliasType<{
/** Номер блока, в который попала транзакция. */
block_num?:boolean | `@${string}`,
/** Сумма заблокированных средств (строка денежного актива). */
blocked_amount?:boolean | `@${string}`,
/** Был ли выполнен перевод средств между кошельками пайщика. */
did_assign?:boolean | `@${string}`,
/** Была ли выполнена конверсия паевого взноса в членский. */
did_convert?:boolean | `@${string}`,
/** Сумма зарезервированных средств (строка денежного актива). */
locked_amount?:boolean | `@${string}`,
/** Время подписания заказа (ISO 8601). */
signed_at?:boolean | `@${string}`,
/** Идентификатор транзакции в блокчейне. */
@@ -7683,7 +7721,7 @@ export type ValueTypes = {
["MarketplaceWalletEntry"]: AliasType<{
/** Доступный остаток (`userwallets.available`) */
available?:boolean | `@${string}`,
/** Заблокированный остаток (`userwallets.blocked`) */
/** Заблокированный остаток (`userwallets.blocked`). Для marketplace-кошельков всегда `0` — резерв выражается через `.available` кошелька w.mkt.order. Поле остаётся для wallet/withdraw flow и legacy данных. */
blocked?:boolean | `@${string}`,
/** Человекочитаемое название (из cooptypes LEDGER2_WALLET_REGISTRY) */
human_name?:boolean | `@${string}`,
@@ -7691,7 +7729,7 @@ export type ValueTypes = {
kind?:boolean | `@${string}`,
/** UX-метка кошелька в формате `<тип взноса> | <программа>` (например «Паевой | Цифровой Кошелёк», «Членский | Стол Заказов») */
label?:boolean | `@${string}`,
/** eosio::name кошелька (w.wal.share / w.wal.member / w.mkt.member) */
/** eosio::name кошелька (w.wal.share / w.wal.member / w.mkt.order) */
name?:boolean | `@${string}`,
/** program_id: 1 — Цифровой Кошелёк, 2 — Стол Заказов */
program_id?:boolean | `@${string}`,
@@ -8092,7 +8130,9 @@ chatcoopCreateCalendarEvent?: [{ data: ValueTypes["CreateChatCoopCalendarEventIn
Требуемые роли: chairman, member, user. */
chatcoopCreateCalendarIcsSubscription?:ValueTypes["ChatCoopCalendarIcsUrlResponse"],
chatcoopCreateSecretaryRoom?: [{ data: ValueTypes["CreateSecretaryRoomInput"] | Variable<any, string>},ValueTypes["ChatcoopSecretaryRoom"]],
chatcoopDeleteCalendarEvent?: [{ id: string | Variable<any, string>},boolean | `@${string}`],
chatcoopRemoveSecretaryRoom?: [{ data: ValueTypes["RemoveSecretaryRoomInput"] | Variable<any, string>},boolean | `@${string}`],
chatcoopUpdateCalendarEvent?: [{ data: ValueTypes["UpdateChatCoopCalendarEventInput"] | Variable<any, string>},ValueTypes["ChatCoopCalendarEvent"]],
chatcoopUpdateTranscriptionMemo?: [{ data: ValueTypes["UpdateCallTranscriptionMemoInput"] | Variable<any, string>},ValueTypes["CallTranscription"]],
completeCapitalOnboardingStep?: [{ data: ValueTypes["CapitalOnboardingStepInput"] | Variable<any, string>},ValueTypes["CapitalOnboardingState"]],
@@ -8223,6 +8263,8 @@ walmoveWallets?: [{ input: ValueTypes["WalmoveInput"] | Variable<any, string>},V
__typename?: boolean | `@${string}`,
['...on Mutation']?: Omit<ValueTypes["Mutation"], "...on Mutation">
}>;
/** Тип комнаты вне проекта: пайщики, совет, комната секретаря */
["NonProjectRoomKind"]:NonProjectRoomKind;
["NotificationWorkflowRecipientInput"]: {
/** Username получателя */
username: string | Variable<any, string>
@@ -9198,8 +9240,6 @@ walmoveWallets?: [{ input: ValueTypes["WalmoveInput"] | Variable<any, string>},V
available?:boolean | `@${string}`,
/** Номер блока последнего обновления */
blockNum?:boolean | `@${string}`,
/** Заблокированный баланс (формат: "100.0000 RUB") */
blocked?:boolean | `@${string}`,
/** Имя кооператива */
coopname?:boolean | `@${string}`,
/** Уникальный идентификатор кошелька в блокчейне */
@@ -9453,7 +9493,15 @@ chatcoopGetTranscriptions?: [{ data?: ValueTypes["GetTranscriptionsInput"] | und
Требуемые роли: chairman, member. */
chatcoopListCalendarRooms?:ValueTypes["ChatCoopCalendarRoomOption"],
/** Комнаты Matrix вне проектов Capital (пайщики/совет/секретарь) для синхронизации в blago
Требуемые роли: chairman, member, user. */
chatcoopListNonProjectCommunicationRooms?:ValueTypes["ChatcoopNonProjectCommunicationRoom"],
chatcoopListProjectCommunicationRooms?: [{ data: ValueTypes["GetProjectCommunicationRoomsInput"] | Variable<any, string>},ValueTypes["ChatcoopProjectCommunicationRoom"]],
/** Все комнаты реестра ChatCoop (системные/проектные read-only, комнаты секретаря удаляемые)
Требуемые роли: chairman, member. */
chatcoopListSecretaryRooms?:ValueTypes["ChatcoopSecretaryRoom"],
chatcoopListUtcDatesWithNewRoomMessages?: [{ data: ValueTypes["ListUtcDatesWithNewRoomMessagesInput"] | Variable<any, string>},boolean | `@${string}`],
checkReportReadiness?: [{ reportType: ValueTypes["ReportType"] | Variable<any, string>},ValueTypes["ReportReadinessView"]],
cooperativeAgreements?: [{ coopname: string | Variable<any, string>},ValueTypes["CoopAgreement"]],
@@ -9596,8 +9644,8 @@ marketplaceListMyOrders?: [{ input?: ValueTypes["MarketplaceListOrdersInput"] |
marketplaceListMyReadyToReceive?:ValueTypes["MarketplaceOrder"],
/** Все заявления текущего пайщика на гарантийный возврат — активные и архивные. */
marketplaceListMyReturnClaims?:ValueTypes["MarketplaceReturnClaim"],
marketplaceListOutgoingPayments?: [{ statuses?: Array<ValueTypes["MarketplaceOutgoingPaymentRequestStatus"]> | undefined | null | Variable<any, string>, supplier_account?: string | undefined | null | Variable<any, string>},ValueTypes["MarketplaceOutgoingPaymentRequest"]],
marketplaceListOutgoingPaymentsAsSupplier?: [{ statuses?: Array<ValueTypes["MarketplaceOutgoingPaymentRequestStatus"]> | undefined | null | Variable<any, string>},ValueTypes["MarketplaceOutgoingPaymentRequest"]],
marketplaceListOutgoingPayments?: [{ filter?: ValueTypes["MarketplaceListOutgoingPaymentsFilterInput"] | undefined | null | Variable<any, string>},ValueTypes["MarketplaceOutgoingPaymentRequest"]],
marketplaceListOutgoingPaymentsAsSupplier?: [{ filter?: ValueTypes["MarketplaceListOutgoingPaymentsAsSupplierFilterInput"] | undefined | null | Variable<any, string>},ValueTypes["MarketplaceOutgoingPaymentRequest"]],
marketplaceListPendingOffers?: [{ input?: ValueTypes["MarketplaceListPendingOffersInput"] | undefined | null | Variable<any, string>},ValueTypes["MarketplaceOfferPaginationResult"]],
marketplaceListReturnClaimsByBraname?: [{ data: ValueTypes["MarketplaceListReturnClaimsByBranameInput"] | Variable<any, string>},ValueTypes["MarketplaceReturnClaim"]],
marketplaceListShipments?: [{ data?: ValueTypes["MarketplaceListShipmentsInput"] | undefined | null | Variable<any, string>},ValueTypes["MarketplaceShipment"]],
@@ -9606,7 +9654,7 @@ marketplaceListSupplierOrders?: [{ input?: ValueTypes["MarketplaceListOrdersInpu
/** Список пайщиков-поставщиков, допущенных к публикации оферт */
marketplaceListWhitelist?:ValueTypes["MarketplaceWhitelistEntry"],
marketplaceListWriteoffProposals?: [{ data: ValueTypes["MarketplaceListWriteoffProposalsInput"] | Variable<any, string>, options?: ValueTypes["PaginationInput"] | undefined | null | Variable<any, string>},ValueTypes["PaginatedMarketplaceWriteoffProposals"]],
/** Кошельки пайщика, релевантные столу заказов: w.wal.share, w.wal.member, w.mkt.member */
/** Кошельки пайщика, релевантные столу заказов: w.wal.share, w.wal.member, w.mkt.order */
marketplaceMemberWallet?:ValueTypes["MarketplaceMemberWallet"],
/** Состояние онбординга пайщика в Столе заказов: показывать ли gate или пропускать на стол */
marketplaceOnboardingState?:ValueTypes["MarketplaceOnboardingState"],
@@ -9819,6 +9867,10 @@ validateReportEdits?: [{ editsJson: string | Variable<any, string>, reportType:
["RemoveAvailableCategoryTypesInput"]: {
/** Типы товаров для удаления */
categoryTypes: Array<ValueTypes["CategoryTypeInput"]> | Variable<any, string>
};
["RemoveSecretaryRoomInput"]: {
/** Идентификатор комнаты в реестре, которую нужно удалить */
id: string | Variable<any, string>
};
["ReplaceAvailableItemsInput"]: {
/** ID категорий (целые категории) */
@@ -12239,11 +12291,11 @@ export type ResolverInputTypes = {
createdAt?:boolean | `@${string}`,
endedAt?:boolean | `@${string}`,
id?:boolean | `@${string}`,
matrixRoomId?:boolean | `@${string}`,
/** Пользовательская заметка о содержании звонка */
memo?:boolean | `@${string}`,
/** Отображаемые имена участников (Synapse displayname); в БД хранятся канонические Matrix user id */
participants?:boolean | `@${string}`,
/** Внутреннее имя комнаты звонка (LiveKit room name), не Matrix room id */
roomId?:boolean | `@${string}`,
roomName?:boolean | `@${string}`,
startedAt?:boolean | `@${string}`,
@@ -13723,6 +13775,15 @@ export type ResolverInputTypes = {
displayLabel?:boolean | `@${string}`,
matrixRoomId?:boolean | `@${string}`,
__typename?: boolean | `@${string}`
}>;
["ChatcoopNonProjectCommunicationRoom"]: AliasType<{
/** Подпись для отображения комнаты */
displayLabel?:boolean | `@${string}`,
/** Тип комнаты (пайщики / совет / секретарь) */
kind?:boolean | `@${string}`,
/** Идентификатор комнаты Matrix */
matrixRoomId?:boolean | `@${string}`,
__typename?: boolean | `@${string}`
}>;
["ChatcoopProjectCommunicationRoom"]: AliasType<{
/** Подпись для отображения (комната / проект Capital) */
@@ -13742,6 +13803,21 @@ export type ResolverInputTypes = {
/** origin_server_ts из Matrix (мс) */
originServerTs?:boolean | `@${string}`,
__typename?: boolean | `@${string}`
}>;
["ChatcoopSecretaryRoom"]: AliasType<{
/** Название комнаты */
displayLabel?:boolean | `@${string}`,
/** Можно ли удалить комнату из интерфейса (только комнаты секретаря) */
editable?:boolean | `@${string}`,
/** Комната зашифрована (E2EE) — секретарь не транскрибирует такие комнаты */
encrypted?:boolean | `@${string}`,
/** Внутренний идентификатор комнаты в реестре (для операций; это НЕ Matrix room id) */
id?:boolean | `@${string}`,
/** Тип комнаты */
kind?:boolean | `@${string}`,
/** Секретарь присутствует в комнате */
secretaryInRoom?:boolean | `@${string}`,
__typename?: boolean | `@${string}`
}>;
["CheckMatrixUsernameInput"]: {
username: string
@@ -14469,6 +14545,12 @@ export type ResolverInputTypes = {
weightUnit?: string | undefined | null,
/** Ширина упаковки */
width?: number | undefined | null
};
["CreateSecretaryRoomInput"]: {
/** Название комнаты */
displayName: string,
/** Публичная комната (любой может войти) либо приватная (вход по приглашению создателя) */
isPublic: boolean
};
["CreateSovietIndividualDataInput"]: {
/** Дата рождения */
@@ -15918,8 +16000,6 @@ export type ResolverInputTypes = {
["Ledger2Wallet"]: AliasType<{
/** Доступный баланс */
available?:boolean | `@${string}`,
/** Заблокированный баланс */
blocked?:boolean | `@${string}`,
/** eosio::name-идентификатор кошелька (w.<contract>.<waltype>) */
id?:boolean | `@${string}`,
/** Название кошелька */
@@ -16011,6 +16091,8 @@ export type ResolverInputTypes = {
/** Имя пользователя */
username: string
};
/** Тип комнаты: пайщики, совет, проект Capital, комната секретаря */
["ManagedRoomKind"]:ManagedRoomKind;
["MarkReportPeriodInput"]: {
mark?: ResolverInputTypes["ReportSubmissionMark"] | undefined | null,
period?: number | undefined | null,
@@ -16822,6 +16904,16 @@ export type ResolverInputTypes = {
statuses?: Array<ResolverInputTypes["MarketplaceOrderStatus"]> | undefined | null,
/** Фильтр по аккаунту поставщика. */
supplier_account?: string | undefined | null
};
["MarketplaceListOutgoingPaymentsAsSupplierFilterInput"]: {
/** Фильтр по статусам выплат. Пусто — показывать все статусы. */
statuses?: Array<ResolverInputTypes["MarketplaceOutgoingPaymentRequestStatus"]> | undefined | null
};
["MarketplaceListOutgoingPaymentsFilterInput"]: {
/** Фильтр по статусам выплат. Пусто — показывать все статусы. */
statuses?: Array<ResolverInputTypes["MarketplaceOutgoingPaymentRequestStatus"]> | undefined | null,
/** Поставщик-получатель выплаты. Пусто — по всем поставщикам. */
supplier_account?: string | undefined | null
};
["MarketplaceListPendingOffersInput"]: {
/** Количество элементов на странице */
@@ -16857,7 +16949,7 @@ export type ResolverInputTypes = {
["MarketplaceMemberWallet"]: AliasType<{
coopname?:boolean | `@${string}`,
username?:boolean | `@${string}`,
/** Релевантные стол-заказам USER_SHARED-кошельки пайщика; порядок: share → member → mkt.member */
/** Релевантные стол-заказам USER_SHARED-кошельки пайщика; порядок: share → member → mkt.order */
wallets?:ResolverInputTypes["MarketplaceWalletEntry"],
__typename?: boolean | `@${string}`
}>;
@@ -17004,16 +17096,12 @@ export type ResolverInputTypes = {
warranty_until?:boolean | `@${string}`,
__typename?: boolean | `@${string}`
}>;
/** Снимок транзакции блокировки средств: ссылки на блок, флаги конверсии и сумма. */
/** Снимок транзакции резервирования средств: ссылки на блок и сумма резерва. */
["MarketplaceOrderCreateTxSnapshot"]: AliasType<{
/** Номер блока, в который попала транзакция. */
block_num?:boolean | `@${string}`,
/** Сумма заблокированных средств (строка денежного актива). */
blocked_amount?:boolean | `@${string}`,
/** Был ли выполнен перевод средств между кошельками пайщика. */
did_assign?:boolean | `@${string}`,
/** Была ли выполнена конверсия паевого взноса в членский. */
did_convert?:boolean | `@${string}`,
/** Сумма зарезервированных средств (строка денежного актива). */
locked_amount?:boolean | `@${string}`,
/** Время подписания заказа (ISO 8601). */
signed_at?:boolean | `@${string}`,
/** Идентификатор транзакции в блокчейне. */
@@ -17611,7 +17699,7 @@ export type ResolverInputTypes = {
["MarketplaceWalletEntry"]: AliasType<{
/** Доступный остаток (`userwallets.available`) */
available?:boolean | `@${string}`,
/** Заблокированный остаток (`userwallets.blocked`) */
/** Заблокированный остаток (`userwallets.blocked`). Для marketplace-кошельков всегда `0` — резерв выражается через `.available` кошелька w.mkt.order. Поле остаётся для wallet/withdraw flow и legacy данных. */
blocked?:boolean | `@${string}`,
/** Человекочитаемое название (из cooptypes LEDGER2_WALLET_REGISTRY) */
human_name?:boolean | `@${string}`,
@@ -17619,7 +17707,7 @@ export type ResolverInputTypes = {
kind?:boolean | `@${string}`,
/** UX-метка кошелька в формате `<тип взноса> | <программа>` (например «Паевой | Цифровой Кошелёк», «Членский | Стол Заказов») */
label?:boolean | `@${string}`,
/** eosio::name кошелька (w.wal.share / w.wal.member / w.mkt.member) */
/** eosio::name кошелька (w.wal.share / w.wal.member / w.mkt.order) */
name?:boolean | `@${string}`,
/** program_id: 1 — Цифровой Кошелёк, 2 — Стол Заказов */
program_id?:boolean | `@${string}`,
@@ -18006,7 +18094,9 @@ chatcoopCreateCalendarEvent?: [{ data: ResolverInputTypes["CreateChatCoopCalenda
Требуемые роли: chairman, member, user. */
chatcoopCreateCalendarIcsSubscription?:ResolverInputTypes["ChatCoopCalendarIcsUrlResponse"],
chatcoopCreateSecretaryRoom?: [{ data: ResolverInputTypes["CreateSecretaryRoomInput"]},ResolverInputTypes["ChatcoopSecretaryRoom"]],
chatcoopDeleteCalendarEvent?: [{ id: string},boolean | `@${string}`],
chatcoopRemoveSecretaryRoom?: [{ data: ResolverInputTypes["RemoveSecretaryRoomInput"]},boolean | `@${string}`],
chatcoopUpdateCalendarEvent?: [{ data: ResolverInputTypes["UpdateChatCoopCalendarEventInput"]},ResolverInputTypes["ChatCoopCalendarEvent"]],
chatcoopUpdateTranscriptionMemo?: [{ data: ResolverInputTypes["UpdateCallTranscriptionMemoInput"]},ResolverInputTypes["CallTranscription"]],
completeCapitalOnboardingStep?: [{ data: ResolverInputTypes["CapitalOnboardingStepInput"]},ResolverInputTypes["CapitalOnboardingState"]],
@@ -18136,6 +18226,8 @@ voteOnAnnualGeneralMeet?: [{ data: ResolverInputTypes["VoteOnAnnualGeneralMeetIn
walmoveWallets?: [{ input: ResolverInputTypes["WalmoveInput"]},ResolverInputTypes["Ledger2AdjustmentResult"]],
__typename?: boolean | `@${string}`
}>;
/** Тип комнаты вне проекта: пайщики, совет, комната секретаря */
["NonProjectRoomKind"]:NonProjectRoomKind;
["NotificationWorkflowRecipientInput"]: {
/** Username получателя */
username: string
@@ -19059,8 +19151,6 @@ walmoveWallets?: [{ input: ResolverInputTypes["WalmoveInput"]},ResolverInputType
available?:boolean | `@${string}`,
/** Номер блока последнего обновления */
blockNum?:boolean | `@${string}`,
/** Заблокированный баланс (формат: "100.0000 RUB") */
blocked?:boolean | `@${string}`,
/** Имя кооператива */
coopname?:boolean | `@${string}`,
/** Уникальный идентификатор кошелька в блокчейне */
@@ -19310,7 +19400,15 @@ chatcoopGetTranscriptions?: [{ data?: ResolverInputTypes["GetTranscriptionsInput
Требуемые роли: chairman, member. */
chatcoopListCalendarRooms?:ResolverInputTypes["ChatCoopCalendarRoomOption"],
/** Комнаты Matrix вне проектов Capital (пайщики/совет/секретарь) для синхронизации в blago
Требуемые роли: chairman, member, user. */
chatcoopListNonProjectCommunicationRooms?:ResolverInputTypes["ChatcoopNonProjectCommunicationRoom"],
chatcoopListProjectCommunicationRooms?: [{ data: ResolverInputTypes["GetProjectCommunicationRoomsInput"]},ResolverInputTypes["ChatcoopProjectCommunicationRoom"]],
/** Все комнаты реестра ChatCoop (системные/проектные read-only, комнаты секретаря удаляемые)
Требуемые роли: chairman, member. */
chatcoopListSecretaryRooms?:ResolverInputTypes["ChatcoopSecretaryRoom"],
chatcoopListUtcDatesWithNewRoomMessages?: [{ data: ResolverInputTypes["ListUtcDatesWithNewRoomMessagesInput"]},boolean | `@${string}`],
checkReportReadiness?: [{ reportType: ResolverInputTypes["ReportType"]},ResolverInputTypes["ReportReadinessView"]],
cooperativeAgreements?: [{ coopname: string},ResolverInputTypes["CoopAgreement"]],
@@ -19453,8 +19551,8 @@ marketplaceListMyOrders?: [{ input?: ResolverInputTypes["MarketplaceListOrdersIn
marketplaceListMyReadyToReceive?:ResolverInputTypes["MarketplaceOrder"],
/** Все заявления текущего пайщика на гарантийный возврат — активные и архивные. */
marketplaceListMyReturnClaims?:ResolverInputTypes["MarketplaceReturnClaim"],
marketplaceListOutgoingPayments?: [{ statuses?: Array<ResolverInputTypes["MarketplaceOutgoingPaymentRequestStatus"]> | undefined | null, supplier_account?: string | undefined | null},ResolverInputTypes["MarketplaceOutgoingPaymentRequest"]],
marketplaceListOutgoingPaymentsAsSupplier?: [{ statuses?: Array<ResolverInputTypes["MarketplaceOutgoingPaymentRequestStatus"]> | undefined | null},ResolverInputTypes["MarketplaceOutgoingPaymentRequest"]],
marketplaceListOutgoingPayments?: [{ filter?: ResolverInputTypes["MarketplaceListOutgoingPaymentsFilterInput"] | undefined | null},ResolverInputTypes["MarketplaceOutgoingPaymentRequest"]],
marketplaceListOutgoingPaymentsAsSupplier?: [{ filter?: ResolverInputTypes["MarketplaceListOutgoingPaymentsAsSupplierFilterInput"] | undefined | null},ResolverInputTypes["MarketplaceOutgoingPaymentRequest"]],
marketplaceListPendingOffers?: [{ input?: ResolverInputTypes["MarketplaceListPendingOffersInput"] | undefined | null},ResolverInputTypes["MarketplaceOfferPaginationResult"]],
marketplaceListReturnClaimsByBraname?: [{ data: ResolverInputTypes["MarketplaceListReturnClaimsByBranameInput"]},ResolverInputTypes["MarketplaceReturnClaim"]],
marketplaceListShipments?: [{ data?: ResolverInputTypes["MarketplaceListShipmentsInput"] | undefined | null},ResolverInputTypes["MarketplaceShipment"]],
@@ -19463,7 +19561,7 @@ marketplaceListSupplierOrders?: [{ input?: ResolverInputTypes["MarketplaceListOr
/** Список пайщиков-поставщиков, допущенных к публикации оферт */
marketplaceListWhitelist?:ResolverInputTypes["MarketplaceWhitelistEntry"],
marketplaceListWriteoffProposals?: [{ data: ResolverInputTypes["MarketplaceListWriteoffProposalsInput"], options?: ResolverInputTypes["PaginationInput"] | undefined | null},ResolverInputTypes["PaginatedMarketplaceWriteoffProposals"]],
/** Кошельки пайщика, релевантные столу заказов: w.wal.share, w.wal.member, w.mkt.member */
/** Кошельки пайщика, релевантные столу заказов: w.wal.share, w.wal.member, w.mkt.order */
marketplaceMemberWallet?:ResolverInputTypes["MarketplaceMemberWallet"],
/** Состояние онбординга пайщика в Столе заказов: показывать ли gate или пропускать на стол */
marketplaceOnboardingState?:ResolverInputTypes["MarketplaceOnboardingState"],
@@ -19669,6 +19767,10 @@ validateReportEdits?: [{ editsJson: string, reportType: ResolverInputTypes["Repo
["RemoveAvailableCategoryTypesInput"]: {
/** Типы товаров для удаления */
categoryTypes: Array<ResolverInputTypes["CategoryTypeInput"]>
};
["RemoveSecretaryRoomInput"]: {
/** Идентификатор комнаты в реестре, которую нужно удалить */
id: string
};
["ReplaceAvailableItemsInput"]: {
/** ID категорий (целые категории) */
@@ -22016,11 +22118,11 @@ export type ModelTypes = {
createdAt: ModelTypes["DateTime"],
endedAt?: ModelTypes["DateTime"] | undefined | null,
id: string,
matrixRoomId: string,
/** Пользовательская заметка о содержании звонка */
memo: string,
/** Отображаемые имена участников (Synapse displayname); в БД хранятся канонические Matrix user id */
participants: Array<string>,
/** Внутреннее имя комнаты звонка (LiveKit room name), не Matrix room id */
roomId: string,
roomName: string,
startedAt: ModelTypes["DateTime"],
@@ -23457,6 +23559,14 @@ export type ModelTypes = {
["ChatCoopCalendarRoomOption"]: {
displayLabel: string,
matrixRoomId: string
};
["ChatcoopNonProjectCommunicationRoom"]: {
/** Подпись для отображения комнаты */
displayLabel: string,
/** Тип комнаты (пайщики / совет / секретарь) */
kind: ModelTypes["NonProjectRoomKind"],
/** Идентификатор комнаты Matrix */
matrixRoomId: string
};
["ChatcoopProjectCommunicationRoom"]: {
/** Подпись для отображения (комната / проект Capital) */
@@ -23474,6 +23584,20 @@ export type ModelTypes = {
kind: ModelTypes["RoomMessageKind"],
/** origin_server_ts из Matrix (мс) */
originServerTs: number
};
["ChatcoopSecretaryRoom"]: {
/** Название комнаты */
displayLabel: string,
/** Можно ли удалить комнату из интерфейса (только комнаты секретаря) */
editable: boolean,
/** Комната зашифрована (E2EE) — секретарь не транскрибирует такие комнаты */
encrypted: boolean,
/** Внутренний идентификатор комнаты в реестре (для операций; это НЕ Matrix room id) */
id: string,
/** Тип комнаты */
kind: ModelTypes["ManagedRoomKind"],
/** Секретарь присутствует в комнате */
secretaryInRoom: boolean
};
["CheckMatrixUsernameInput"]: {
username: string
@@ -24193,6 +24317,12 @@ export type ModelTypes = {
weightUnit?: string | undefined | null,
/** Ширина упаковки */
width?: number | undefined | null
};
["CreateSecretaryRoomInput"]: {
/** Название комнаты */
displayName: string,
/** Публичная комната (любой может войти) либо приватная (вход по приглашению создателя) */
isPublic: boolean
};
["CreateSovietIndividualDataInput"]: {
/** Дата рождения */
@@ -25593,8 +25723,6 @@ export type ModelTypes = {
["Ledger2Wallet"]: {
/** Доступный баланс */
available: string,
/** Заблокированный баланс */
blocked: string,
/** eosio::name-идентификатор кошелька (w.<contract>.<waltype>) */
id: string,
/** Название кошелька */
@@ -25680,6 +25808,7 @@ export type ModelTypes = {
/** Имя пользователя */
username: string
};
["ManagedRoomKind"]:ManagedRoomKind;
["MarkReportPeriodInput"]: {
mark?: ModelTypes["ReportSubmissionMark"] | undefined | null,
period?: number | undefined | null,
@@ -26455,6 +26584,16 @@ export type ModelTypes = {
statuses?: Array<ModelTypes["MarketplaceOrderStatus"]> | undefined | null,
/** Фильтр по аккаунту поставщика. */
supplier_account?: string | undefined | null
};
["MarketplaceListOutgoingPaymentsAsSupplierFilterInput"]: {
/** Фильтр по статусам выплат. Пусто — показывать все статусы. */
statuses?: Array<ModelTypes["MarketplaceOutgoingPaymentRequestStatus"]> | undefined | null
};
["MarketplaceListOutgoingPaymentsFilterInput"]: {
/** Фильтр по статусам выплат. Пусто — показывать все статусы. */
statuses?: Array<ModelTypes["MarketplaceOutgoingPaymentRequestStatus"]> | undefined | null,
/** Поставщик-получатель выплаты. Пусто — по всем поставщикам. */
supplier_account?: string | undefined | null
};
["MarketplaceListPendingOffersInput"]: {
/** Количество элементов на странице */
@@ -26490,7 +26629,7 @@ export type ModelTypes = {
["MarketplaceMemberWallet"]: {
coopname: string,
username: string,
/** Релевантные стол-заказам USER_SHARED-кошельки пайщика; порядок: share → member → mkt.member */
/** Релевантные стол-заказам USER_SHARED-кошельки пайщика; порядок: share → member → mkt.order */
wallets: Array<ModelTypes["MarketplaceWalletEntry"]>
};
["MarketplaceModerationLogEntry"]: {
@@ -26631,16 +26770,12 @@ export type ModelTypes = {
/** Дата окончания гарантии. */
warranty_until?: ModelTypes["DateTime"] | undefined | null
};
/** Снимок транзакции блокировки средств: ссылки на блок, флаги конверсии и сумма. */
/** Снимок транзакции резервирования средств: ссылки на блок и сумма резерва. */
["MarketplaceOrderCreateTxSnapshot"]: {
/** Номер блока, в который попала транзакция. */
block_num: number,
/** Сумма заблокированных средств (строка денежного актива). */
blocked_amount: string,
/** Был ли выполнен перевод средств между кошельками пайщика. */
did_assign: boolean,
/** Была ли выполнена конверсия паевого взноса в членский. */
did_convert: boolean,
/** Сумма зарезервированных средств (строка денежного актива). */
locked_amount: string,
/** Время подписания заказа (ISO 8601). */
signed_at: string,
/** Идентификатор транзакции в блокчейне. */
@@ -27209,7 +27344,7 @@ export type ModelTypes = {
["MarketplaceWalletEntry"]: {
/** Доступный остаток (`userwallets.available`) */
available: string,
/** Заблокированный остаток (`userwallets.blocked`) */
/** Заблокированный остаток (`userwallets.blocked`). Для marketplace-кошельков всегда `0` — резерв выражается через `.available` кошелька w.mkt.order. Поле остаётся для wallet/withdraw flow и legacy данных. */
blocked: string,
/** Человекочитаемое название (из cooptypes LEDGER2_WALLET_REGISTRY) */
human_name: string,
@@ -27217,7 +27352,7 @@ export type ModelTypes = {
kind: string,
/** UX-метка кошелька в формате `<тип взноса> | <программа>` (например «Паевой | Цифровой Кошелёк», «Членский | Стол Заказов») */
label: string,
/** eosio::name кошелька (w.wal.share / w.wal.member / w.mkt.member) */
/** eosio::name кошелька (w.wal.share / w.wal.member / w.mkt.order) */
name: string,
/** program_id: 1 — Цифровой Кошелёк, 2 — Стол Заказов */
program_id: number
@@ -27828,10 +27963,18 @@ export type ModelTypes = {
Требуемые роли: chairman, member, user. */
chatcoopCreateCalendarIcsSubscription: ModelTypes["ChatCoopCalendarIcsUrlResponse"],
/** Создать комнату с секретарём (публичную или приватную); секретарь подключается сразу
Требуемые роли: chairman, member. */
chatcoopCreateSecretaryRoom: ModelTypes["ChatcoopSecretaryRoom"],
/** Удалить событие календаря
Требуемые роли: chairman, member. */
chatcoopDeleteCalendarEvent: boolean,
/** Удалить комнату секретаря: вывести секретаря и снять комнату с синхронизации (возвращает идентификатор комнаты в реестре)
Требуемые роли: chairman, member. */
chatcoopRemoveSecretaryRoom: string,
/** Обновить событие календаря
Требуемые роли: chairman, member. */
@@ -28231,6 +28374,7 @@ export type ModelTypes = {
Требуемые роли: chairman. */
walmoveWallets: ModelTypes["Ledger2AdjustmentResult"]
};
["NonProjectRoomKind"]:NonProjectRoomKind;
["NotificationWorkflowRecipientInput"]: {
/** Username получателя */
username: string
@@ -29085,8 +29229,6 @@ export type ModelTypes = {
available: string,
/** Номер блока последнего обновления */
blockNum?: number | undefined | null,
/** Заблокированный баланс (формат: "100.0000 RUB") */
blocked: string,
/** Имя кооператива */
coopname: string,
/** Уникальный идентификатор кошелька в блокчейне */
@@ -29411,10 +29553,18 @@ export type ModelTypes = {
Требуемые роли: chairman, member. */
chatcoopListCalendarRooms: Array<ModelTypes["ChatCoopCalendarRoomOption"]>,
/** Комнаты Matrix вне проектов Capital (пайщики/совет/секретарь) для синхронизации в blago
Требуемые роли: chairman, member, user. */
chatcoopListNonProjectCommunicationRooms: Array<ModelTypes["ChatcoopNonProjectCommunicationRoom"]>,
/** Комнаты Matrix, привязанные к проекту Capital (реестр ChatCoop)
Требуемые роли: chairman, member, user. */
chatcoopListProjectCommunicationRooms: Array<ModelTypes["ChatcoopProjectCommunicationRoom"]>,
/** Все комнаты реестра ChatCoop (системные/проектные read-only, комнаты секретаря удаляемые)
Требуемые роли: chairman, member. */
chatcoopListSecretaryRooms: Array<ModelTypes["ChatcoopSecretaryRoom"]>,
/** UTC-даты (YYYY-MM-DD), в которых есть сообщения новее afterOriginServerTsExclusive, для комнаты Matrix
Требуемые роли: chairman, member, user. */
@@ -29681,7 +29831,7 @@ export type ModelTypes = {
marketplaceListCategories: Array<ModelTypes["MarketplaceCategory"]>,
/** Постраничный список сводных заявок поставщика — для стола поставщика и для прослеживания состояния заказов. */
marketplaceListConsolidatedRequests: ModelTypes["MarketplaceConsolidatedRequestPaginationResult"],
/** Список наклеек инвентаря КУ: admin/совет видят весь склад кооператива, оператор — только свой участок. */
/** Список наклеек инвентаря КУ — для admin-стола склада и операторских разделов. */
marketplaceListInventory: Array<ModelTypes["MarketplaceInventoryItem"]>,
/** Список заказов на кооперативном участке, ожидающих открытия и финальной подписи выдачи (для оператора кооперативного участка). */
marketplaceListIssuancesByBraname: Array<ModelTypes["MarketplaceOrder"]>,
@@ -29717,7 +29867,7 @@ export type ModelTypes = {
marketplaceListWhitelist: Array<ModelTypes["MarketplaceWhitelistEntry"]>,
/** Лента всех проектов списания кооператива с фильтром по статусу. */
marketplaceListWriteoffProposals: ModelTypes["PaginatedMarketplaceWriteoffProposals"],
/** Кошельки пайщика, релевантные столу заказов: w.wal.share, w.wal.member, w.mkt.member */
/** Кошельки пайщика, релевантные столу заказов: w.wal.share, w.wal.member, w.mkt.order */
marketplaceMemberWallet: ModelTypes["MarketplaceMemberWallet"],
/** Состояние онбординга пайщика в Столе заказов: показывать ли gate или пропускать на стол */
marketplaceOnboardingState: ModelTypes["MarketplaceOnboardingState"],
@@ -29939,6 +30089,10 @@ export type ModelTypes = {
["RemoveAvailableCategoryTypesInput"]: {
/** Типы товаров для удаления */
categoryTypes: Array<ModelTypes["CategoryTypeInput"]>
};
["RemoveSecretaryRoomInput"]: {
/** Идентификатор комнаты в реестре, которую нужно удалить */
id: string
};
["ReplaceAvailableItemsInput"]: {
/** ID категорий (целые категории) */
@@ -32300,11 +32454,11 @@ export type GraphQLTypes = {
createdAt: GraphQLTypes["DateTime"],
endedAt?: GraphQLTypes["DateTime"] | undefined | null,
id: string,
matrixRoomId: string,
/** Пользовательская заметка о содержании звонка */
memo: string,
/** Отображаемые имена участников (Synapse displayname); в БД хранятся канонические Matrix user id */
participants: Array<string>,
/** Внутреннее имя комнаты звонка (LiveKit room name), не Matrix room id */
roomId: string,
roomName: string,
startedAt: GraphQLTypes["DateTime"],
@@ -33825,6 +33979,16 @@ export type GraphQLTypes = {
displayLabel: string,
matrixRoomId: string,
['...on ChatCoopCalendarRoomOption']: Omit<GraphQLTypes["ChatCoopCalendarRoomOption"], "...on ChatCoopCalendarRoomOption">
};
["ChatcoopNonProjectCommunicationRoom"]: {
__typename: "ChatcoopNonProjectCommunicationRoom",
/** Подпись для отображения комнаты */
displayLabel: string,
/** Тип комнаты (пайщики / совет / секретарь) */
kind: GraphQLTypes["NonProjectRoomKind"],
/** Идентификатор комнаты Matrix */
matrixRoomId: string,
['...on ChatcoopNonProjectCommunicationRoom']: Omit<GraphQLTypes["ChatcoopNonProjectCommunicationRoom"], "...on ChatcoopNonProjectCommunicationRoom">
};
["ChatcoopProjectCommunicationRoom"]: {
__typename: "ChatcoopProjectCommunicationRoom",
@@ -33846,6 +34010,22 @@ export type GraphQLTypes = {
/** origin_server_ts из Matrix (мс) */
originServerTs: number,
['...on ChatcoopRoomMessageLine']: Omit<GraphQLTypes["ChatcoopRoomMessageLine"], "...on ChatcoopRoomMessageLine">
};
["ChatcoopSecretaryRoom"]: {
__typename: "ChatcoopSecretaryRoom",
/** Название комнаты */
displayLabel: string,
/** Можно ли удалить комнату из интерфейса (только комнаты секретаря) */
editable: boolean,
/** Комната зашифрована (E2EE) — секретарь не транскрибирует такие комнаты */
encrypted: boolean,
/** Внутренний идентификатор комнаты в реестре (для операций; это НЕ Matrix room id) */
id: string,
/** Тип комнаты */
kind: GraphQLTypes["ManagedRoomKind"],
/** Секретарь присутствует в комнате */
secretaryInRoom: boolean,
['...on ChatcoopSecretaryRoom']: Omit<GraphQLTypes["ChatcoopSecretaryRoom"], "...on ChatcoopSecretaryRoom">
};
["CheckMatrixUsernameInput"]: {
username: string
@@ -34578,6 +34758,12 @@ export type GraphQLTypes = {
weightUnit?: string | undefined | null,
/** Ширина упаковки */
width?: number | undefined | null
};
["CreateSecretaryRoomInput"]: {
/** Название комнаты */
displayName: string,
/** Публичная комната (любой может войти) либо приватная (вход по приглашению создателя) */
isPublic: boolean
};
["CreateSovietIndividualDataInput"]: {
/** Дата рождения */
@@ -36068,8 +36254,6 @@ export type GraphQLTypes = {
__typename: "Ledger2Wallet",
/** Доступный баланс */
available: string,
/** Заблокированный баланс */
blocked: string,
/** eosio::name-идентификатор кошелька (w.<contract>.<waltype>) */
id: string,
/** Название кошелька */
@@ -36164,6 +36348,8 @@ export type GraphQLTypes = {
/** Имя пользователя */
username: string
};
/** Тип комнаты: пайщики, совет, проект Capital, комната секретаря */
["ManagedRoomKind"]: ManagedRoomKind;
["MarkReportPeriodInput"]: {
mark?: GraphQLTypes["ReportSubmissionMark"] | undefined | null,
period?: number | undefined | null,
@@ -37004,6 +37190,16 @@ export type GraphQLTypes = {
statuses?: Array<GraphQLTypes["MarketplaceOrderStatus"]> | undefined | null,
/** Фильтр по аккаунту поставщика. */
supplier_account?: string | undefined | null
};
["MarketplaceListOutgoingPaymentsAsSupplierFilterInput"]: {
/** Фильтр по статусам выплат. Пусто — показывать все статусы. */
statuses?: Array<GraphQLTypes["MarketplaceOutgoingPaymentRequestStatus"]> | undefined | null
};
["MarketplaceListOutgoingPaymentsFilterInput"]: {
/** Фильтр по статусам выплат. Пусто — показывать все статусы. */
statuses?: Array<GraphQLTypes["MarketplaceOutgoingPaymentRequestStatus"]> | undefined | null,
/** Поставщик-получатель выплаты. Пусто — по всем поставщикам. */
supplier_account?: string | undefined | null
};
["MarketplaceListPendingOffersInput"]: {
/** Количество элементов на странице */
@@ -37040,7 +37236,7 @@ export type GraphQLTypes = {
__typename: "MarketplaceMemberWallet",
coopname: string,
username: string,
/** Релевантные стол-заказам USER_SHARED-кошельки пайщика; порядок: share → member → mkt.member */
/** Релевантные стол-заказам USER_SHARED-кошельки пайщика; порядок: share → member → mkt.order */
wallets: Array<GraphQLTypes["MarketplaceWalletEntry"]>,
['...on MarketplaceMemberWallet']: Omit<GraphQLTypes["MarketplaceMemberWallet"], "...on MarketplaceMemberWallet">
};
@@ -37192,17 +37388,13 @@ export type GraphQLTypes = {
warranty_until?: GraphQLTypes["DateTime"] | undefined | null,
['...on MarketplaceOrder']: Omit<GraphQLTypes["MarketplaceOrder"], "...on MarketplaceOrder">
};
/** Снимок транзакции блокировки средств: ссылки на блок, флаги конверсии и сумма. */
/** Снимок транзакции резервирования средств: ссылки на блок и сумма резерва. */
["MarketplaceOrderCreateTxSnapshot"]: {
__typename: "MarketplaceOrderCreateTxSnapshot",
/** Номер блока, в который попала транзакция. */
block_num: number,
/** Сумма заблокированных средств (строка денежного актива). */
blocked_amount: string,
/** Был ли выполнен перевод средств между кошельками пайщика. */
did_assign: boolean,
/** Была ли выполнена конверсия паевого взноса в членский. */
did_convert: boolean,
/** Сумма зарезервированных средств (строка денежного актива). */
locked_amount: string,
/** Время подписания заказа (ISO 8601). */
signed_at: string,
/** Идентификатор транзакции в блокчейне. */
@@ -37820,7 +38012,7 @@ export type GraphQLTypes = {
__typename: "MarketplaceWalletEntry",
/** Доступный остаток (`userwallets.available`) */
available: string,
/** Заблокированный остаток (`userwallets.blocked`) */
/** Заблокированный остаток (`userwallets.blocked`). Для marketplace-кошельков всегда `0` — резерв выражается через `.available` кошелька w.mkt.order. Поле остаётся для wallet/withdraw flow и legacy данных. */
blocked: string,
/** Человекочитаемое название (из cooptypes LEDGER2_WALLET_REGISTRY) */
human_name: string,
@@ -37828,7 +38020,7 @@ export type GraphQLTypes = {
kind: string,
/** UX-метка кошелька в формате `<тип взноса> | <программа>` (например «Паевой | Цифровой Кошелёк», «Членский | Стол Заказов») */
label: string,
/** eosio::name кошелька (w.wal.share / w.wal.member / w.mkt.member) */
/** eosio::name кошелька (w.wal.share / w.wal.member / w.mkt.order) */
name: string,
/** program_id: 1 — Цифровой Кошелёк, 2 — Стол Заказов */
program_id: number,
@@ -38469,10 +38661,18 @@ export type GraphQLTypes = {
Требуемые роли: chairman, member, user. */
chatcoopCreateCalendarIcsSubscription: GraphQLTypes["ChatCoopCalendarIcsUrlResponse"],
/** Создать комнату с секретарём (публичную или приватную); секретарь подключается сразу
Требуемые роли: chairman, member. */
chatcoopCreateSecretaryRoom: GraphQLTypes["ChatcoopSecretaryRoom"],
/** Удалить событие календаря
Требуемые роли: chairman, member. */
chatcoopDeleteCalendarEvent: boolean,
/** Удалить комнату секретаря: вывести секретаря и снять комнату с синхронизации (возвращает идентификатор комнаты в реестре)
Требуемые роли: chairman, member. */
chatcoopRemoveSecretaryRoom: string,
/** Обновить событие календаря
Требуемые роли: chairman, member. */
@@ -38873,6 +39073,8 @@ export type GraphQLTypes = {
walmoveWallets: GraphQLTypes["Ledger2AdjustmentResult"],
['...on Mutation']: Omit<GraphQLTypes["Mutation"], "...on Mutation">
};
/** Тип комнаты вне проекта: пайщики, совет, комната секретаря */
["NonProjectRoomKind"]: NonProjectRoomKind;
["NotificationWorkflowRecipientInput"]: {
/** Username получателя */
username: string
@@ -39851,8 +40053,6 @@ export type GraphQLTypes = {
available: string,
/** Номер блока последнего обновления */
blockNum?: number | undefined | null,
/** Заблокированный баланс (формат: "100.0000 RUB") */
blocked: string,
/** Имя кооператива */
coopname: string,
/** Уникальный идентификатор кошелька в блокчейне */
@@ -40186,10 +40386,18 @@ export type GraphQLTypes = {
Требуемые роли: chairman, member. */
chatcoopListCalendarRooms: Array<GraphQLTypes["ChatCoopCalendarRoomOption"]>,
/** Комнаты Matrix вне проектов Capital (пайщики/совет/секретарь) для синхронизации в blago
Требуемые роли: chairman, member, user. */
chatcoopListNonProjectCommunicationRooms: Array<GraphQLTypes["ChatcoopNonProjectCommunicationRoom"]>,
/** Комнаты Matrix, привязанные к проекту Capital (реестр ChatCoop)
Требуемые роли: chairman, member, user. */
chatcoopListProjectCommunicationRooms: Array<GraphQLTypes["ChatcoopProjectCommunicationRoom"]>,
/** Все комнаты реестра ChatCoop (системные/проектные read-only, комнаты секретаря удаляемые)
Требуемые роли: chairman, member. */
chatcoopListSecretaryRooms: Array<GraphQLTypes["ChatcoopSecretaryRoom"]>,
/** UTC-даты (YYYY-MM-DD), в которых есть сообщения новее afterOriginServerTsExclusive, для комнаты Matrix
Требуемые роли: chairman, member, user. */
@@ -40456,7 +40664,7 @@ export type GraphQLTypes = {
marketplaceListCategories: Array<GraphQLTypes["MarketplaceCategory"]>,
/** Постраничный список сводных заявок поставщика — для стола поставщика и для прослеживания состояния заказов. */
marketplaceListConsolidatedRequests: GraphQLTypes["MarketplaceConsolidatedRequestPaginationResult"],
/** Список наклеек инвентаря КУ: admin/совет видят весь склад кооператива, оператор — только свой участок. */
/** Список наклеек инвентаря КУ — для admin-стола склада и операторских разделов. */
marketplaceListInventory: Array<GraphQLTypes["MarketplaceInventoryItem"]>,
/** Список заказов на кооперативном участке, ожидающих открытия и финальной подписи выдачи (для оператора кооперативного участка). */
marketplaceListIssuancesByBraname: Array<GraphQLTypes["MarketplaceOrder"]>,
@@ -40492,7 +40700,7 @@ export type GraphQLTypes = {
marketplaceListWhitelist: Array<GraphQLTypes["MarketplaceWhitelistEntry"]>,
/** Лента всех проектов списания кооператива с фильтром по статусу. */
marketplaceListWriteoffProposals: GraphQLTypes["PaginatedMarketplaceWriteoffProposals"],
/** Кошельки пайщика, релевантные столу заказов: w.wal.share, w.wal.member, w.mkt.member */
/** Кошельки пайщика, релевантные столу заказов: w.wal.share, w.wal.member, w.mkt.order */
marketplaceMemberWallet: GraphQLTypes["MarketplaceMemberWallet"],
/** Состояние онбординга пайщика в Столе заказов: показывать ли gate или пропускать на стол */
marketplaceOnboardingState: GraphQLTypes["MarketplaceOnboardingState"],
@@ -40727,6 +40935,10 @@ export type GraphQLTypes = {
["RemoveAvailableCategoryTypesInput"]: {
/** Типы товаров для удаления */
categoryTypes: Array<GraphQLTypes["CategoryTypeInput"]>
};
["RemoveSecretaryRoomInput"]: {
/** Идентификатор комнаты в реестре, которую нужно удалить */
id: string
};
["ReplaceAvailableItemsInput"]: {
/** ID категорий (целые категории) */
@@ -42348,6 +42560,13 @@ export enum LogEventType {
VOTING_COMPLETED = "VOTING_COMPLETED",
VOTING_STARTED = "VOTING_STARTED"
}
/** Тип комнаты: пайщики, совет, проект Capital, комната секретаря */
export enum ManagedRoomKind {
CAPITAL_PROJECT = "CAPITAL_PROJECT",
COUNCIL = "COUNCIL",
MEMBERS = "MEMBERS",
SECRETARY = "SECRETARY"
}
/** Статус АПП приёмки на КУ. */
export enum MarketplaceAplReceptionStatus {
ACCEPTED_TO_COOP = "ACCEPTED_TO_COOP",
@@ -42477,6 +42696,12 @@ export enum MarketplaceWriteoffProposalTrigger {
CRON = "CRON",
MANUAL = "MANUAL"
}
/** Тип комнаты вне проекта: пайщики, совет, комната секретаря */
export enum NonProjectRoomKind {
COUNCIL = "COUNCIL",
MEMBERS = "MEMBERS",
SECRETARY = "SECRETARY"
}
/** Тип юридического лица */
export enum OrganizationType {
AO = "AO",
@@ -42788,6 +43013,7 @@ type ZEUS_VARIABLES = {
["CreateProjectInvestInput"]: ValueTypes["CreateProjectInvestInput"];
["CreateProjectPropertyInput"]: ValueTypes["CreateProjectPropertyInput"];
["CreateRequestInput"]: ValueTypes["CreateRequestInput"];
["CreateSecretaryRoomInput"]: ValueTypes["CreateSecretaryRoomInput"];
["CreateSovietIndividualDataInput"]: ValueTypes["CreateSovietIndividualDataInput"];
["CreateStoryInput"]: ValueTypes["CreateStoryInput"];
["CreateSubscriptionInput"]: ValueTypes["CreateSubscriptionInput"];
@@ -42895,6 +43121,7 @@ type ZEUS_VARIABLES = {
["LoginInput"]: ValueTypes["LoginInput"];
["LogoutInput"]: ValueTypes["LogoutInput"];
["MakeClearanceInput"]: ValueTypes["MakeClearanceInput"];
["ManagedRoomKind"]: ValueTypes["ManagedRoomKind"];
["MarkReportPeriodInput"]: ValueTypes["MarkReportPeriodInput"];
["MarketplaceAcceptConsolidatedRequestInput"]: ValueTypes["MarketplaceAcceptConsolidatedRequestInput"];
["MarketplaceAcceptCppInput"]: ValueTypes["MarketplaceAcceptCppInput"];
@@ -42941,6 +43168,8 @@ type ZEUS_VARIABLES = {
["MarketplaceListIssuancesByBranameInput"]: ValueTypes["MarketplaceListIssuancesByBranameInput"];
["MarketplaceListMyOffersInput"]: ValueTypes["MarketplaceListMyOffersInput"];
["MarketplaceListOrdersInput"]: ValueTypes["MarketplaceListOrdersInput"];
["MarketplaceListOutgoingPaymentsAsSupplierFilterInput"]: ValueTypes["MarketplaceListOutgoingPaymentsAsSupplierFilterInput"];
["MarketplaceListOutgoingPaymentsFilterInput"]: ValueTypes["MarketplaceListOutgoingPaymentsFilterInput"];
["MarketplaceListPendingOffersInput"]: ValueTypes["MarketplaceListPendingOffersInput"];
["MarketplaceListReturnClaimsByBranameInput"]: ValueTypes["MarketplaceListReturnClaimsByBranameInput"];
["MarketplaceListShipmentsByBranameInput"]: ValueTypes["MarketplaceListShipmentsByBranameInput"];
@@ -42979,6 +43208,7 @@ type ZEUS_VARIABLES = {
["MarketplaceWriteoffProposalTrigger"]: ValueTypes["MarketplaceWriteoffProposalTrigger"];
["MarketplaceWriteoffStatementSignablePayloadInput"]: ValueTypes["MarketplaceWriteoffStatementSignablePayloadInput"];
["MoveCapitalIssueToComponentInput"]: ValueTypes["MoveCapitalIssueToComponentInput"];
["NonProjectRoomKind"]: ValueTypes["NonProjectRoomKind"];
["NotificationWorkflowRecipientInput"]: ValueTypes["NotificationWorkflowRecipientInput"];
["NotifyOnAnnualGeneralMeetInput"]: ValueTypes["NotifyOnAnnualGeneralMeetInput"];
["OpenProjectInput"]: ValueTypes["OpenProjectInput"];
@@ -43025,6 +43255,7 @@ type ZEUS_VARIABLES = {
["RegisterParticipantInput"]: ValueTypes["RegisterParticipantInput"];
["RemoveAvailableCategoriesInput"]: ValueTypes["RemoveAvailableCategoriesInput"];
["RemoveAvailableCategoryTypesInput"]: ValueTypes["RemoveAvailableCategoryTypesInput"];
["RemoveSecretaryRoomInput"]: ValueTypes["RemoveSecretaryRoomInput"];
["ReplaceAvailableItemsInput"]: ValueTypes["ReplaceAvailableItemsInput"];
["ReportHistoryFilterInput"]: ValueTypes["ReportHistoryFilterInput"];
["ReportPreviewInput"]: ValueTypes["ReportPreviewInput"];
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "cooptypes",
"type": "module",
"version": "2026.5.23-4",
"version": "2026.5.27-2",
"description": "Shared TypeScript типы и интерфейсы экосистемы кооперативов",
"author": "Alex Ant <dacom.dark.sun@gmail.com>",
"license": "MIT",
@@ -11,11 +11,11 @@
*
* Идентификаторы кошельков (`wallet_from`/`wallet_to`) eosio::name с
* префиксом `w.<contract>.<waltype>` (см. `./wallets.ts`). Пустая строка
* (`""`) sentinel «кошелёк вне системы» для ISSUE, BURN и BURN_BLOCKED.
* (`""`) sentinel «кошелёк вне системы» для ISSUE и BURN.
*/
import type { IName } from '../interfaces/ledger2'
export type WalletOp = 'ISSUE' | 'TRANSFER' | 'BLOCK' | 'UNBLOCK' | 'BURN' | 'BURN_BLOCKED' | 'NONE'
export type WalletOp = 'ISSUE' | 'TRANSFER' | 'BURN' | 'NONE'
export interface OperationMeta {
/** Машинный идентификатор — eosio::name в контракте. */
@@ -34,7 +34,7 @@ export interface OperationMeta {
wallet_op: WalletOp | null
/** Кошелёк-источник (null для ISSUE и для adjustment-операций). */
wallet_from: IName | null
/** Кошелёк-приёмник (null для BLOCK/UNBLOCK/BURN/BURN_BLOCKED и для adjustment-операций). */
/** Кошелёк-приёмник (null для BURN и для adjustment-операций). */
wallet_to: IName | null
/** Код счёта Дт (null без бухпроводки, ADR-003: ⇔ credit == null). */
debit: number | null
@@ -74,17 +74,17 @@ export const LEDGER2_OPERATION_REGISTRY: readonly OperationMeta[] = [
human_name: 'Внесение пайщиком паевого взноса' },
{ code: 'o.wal.wthreq', process_type: 'p.wal.wthdrw', contract: 'wallet',
name: 'REQUEST_WITHDRAW', wallet_op: 'BLOCK', wallet_from: 'w.wal.share', wallet_to: null,
name: 'REQUEST_WITHDRAW', wallet_op: 'TRANSFER', wallet_from: 'w.wal.share', wallet_to: 'w.wal.wpend',
debit: null, credit: null,
human_name: 'Блокировка паевого под запрос на возврат' },
human_name: 'Резервирование паевого под запрос на возврат' },
{ code: 'o.wal.wthdec', process_type: 'p.wal.wthdrw', contract: 'wallet',
name: 'DECLINE_WITHDRAW', wallet_op: 'UNBLOCK', wallet_from: 'w.wal.share', wallet_to: null,
name: 'DECLINE_WITHDRAW', wallet_op: 'TRANSFER', wallet_from: 'w.wal.wpend', wallet_to: 'w.wal.share',
debit: null, credit: null,
human_name: 'Разблокировка паевого после отклонения запроса на возврат' },
human_name: 'Снятие резерва паевого после отклонения запроса на возврат' },
{ code: 'o.wal.wthcpl', process_type: 'p.wal.wthdrw', contract: 'wallet',
name: 'COMPLETE_WITHDRAW', wallet_op: 'BURN_BLOCKED', wallet_from: 'w.wal.share', wallet_to: null,
name: 'COMPLETE_WITHDRAW', wallet_op: 'BURN', wallet_from: 'w.wal.wpend', wallet_to: null,
debit: 80, credit: 51,
human_name: 'Возврат паевого взноса пайщику' },
@@ -28,6 +28,7 @@ export const LEDGER2_WALLET_REGISTRY: readonly WalletMeta[] = [
{ name: "w.cap.gen", human_name: "ЦПП «Генератор» — единый кошелёк программы", kind: "COOPERATIVE" },
{ name: "w.reg.entry", human_name: "Вступительные взносы", kind: "COOPERATIVE" },
{ name: "w.wal.wthdrw", human_name: "Возвраты паевых взносов пайщикам (deprecated, не используется в новых операциях)", kind: "COOPERATIVE" },
{ name: "w.wal.wpend", human_name: "Резерв паевого под заявку на возврат", kind: "COOPERATIVE" },
{ name: "w.sov.infra", human_name: "Членские взносы за инфраструктуру кооп. платформы", kind: "COOPERATIVE" },
{ name: "w.sov.delgte", human_name: "Делегатские членские взносы", kind: "COOPERATIVE" },
{ name: "w.sov.expns", human_name: "Хозяйственные расходы из числа целевого финансирования", kind: "COOPERATIVE" },
@@ -1,72 +1,77 @@
<template lang="pug">
q-btn(
color='accent',
@click.stop='showDialog = true',
:loading='loading',
label="Коммит",
icon="add",
:size='mini ? "sm" : "md"',
:dense="isMobile"
:disabled='disabled'
)
div
q-btn(
color='accent',
@click.stop='showDialog = true',
:loading='loading',
label="Коммит",
icon="add",
:size='mini ? "sm" : "md"',
:dense="isMobile"
:disabled='disabled'
)
q-dialog(v-model='showDialog', @hide='clear')
ModalBase(:title='"Зафиксировать взнос"')
Form.create-commit-form(
:class='isMobile ? "q-pb-md" : "q-pb-lg"',
:handler-submit='handleCreateCommit',
:is-submitting='isSubmitting',
:button-submit-txt='"Зафиксировать"',
:button-cancel-txt='"Отмена"',
@cancel='clear'
)
.create-commit-dialog-content.column.q-gutter-y-sm
section.create-commit-block
.column.q-gutter-y-sm
.create-commit-kv
.text-caption.text-grey-7 Компонент проекта
.text-body1.text-weight-medium.text-primary {{ projectLabel }}
.create-commit-kv
.text-caption.text-grey-7 Время
.text-body1.text-weight-medium
| {{ formatHours(formData.creator_hours) }}
.create-commit-kv(v-if='commitCostFormatted')
.text-caption.text-grey-7 Себестоимость
.text-body1.text-weight-medium.text-primary {{ commitCostFormatted }}
.text-caption.text-grey-6.q-pl-sm {{ commitCostCaption }}
.create-commit-kv(v-else-if='contributorStore.self')
.text-caption.text-grey-7 Себестоимость
.text-body2.text-grey-6 Ставка в час не задана сумму посчитать нельзя.
template(v-if='commitBreakdown?.tail && commitBreakdown.tail > 1e-6')
q-separator(color='grey-5', style='opacity: 0.35')
.text-caption.text-grey-7
| После фиксации в накоплении останется {{ formatHours(commitBreakdown.tail) }}.
BaseDialog(
v-model='showDialog',
title='Зафиксировать взнос',
size='md',
@update:model-value='(v) => !v && clear()'
)
Form.create-commit-form(
:class='isMobile ? "q-pb-md" : "q-pb-lg"',
:handler-submit='handleCreateCommit',
:is-submitting='isSubmitting',
:button-submit-txt='"Зафиксировать"',
:button-cancel-txt='"Отмена"',
@cancel='clear'
)
.create-commit-dialog-content.column.q-gutter-y-sm
section.create-commit-block
.column.q-gutter-y-sm
.create-commit-kv
.text-caption.text-grey-7 Компонент проекта
.text-body1.text-weight-medium.text-primary {{ projectLabel }}
.create-commit-kv
.text-caption.text-grey-7 Время
.text-body1.text-weight-medium
| {{ formatHours(formData.creator_hours) }}
.create-commit-kv(v-if='commitCostFormatted')
.text-caption.text-grey-7 Себестоимость
.text-body1.text-weight-medium.text-primary {{ commitCostFormatted }}
.text-caption.text-grey-6.q-pl-sm {{ commitCostCaption }}
.create-commit-kv(v-else-if='contributorStore.self')
.text-caption.text-grey-7 Себестоимость
.text-body2.text-grey-6 Ставка в час не задана сумму посчитать нельзя.
template(v-if='commitBreakdown?.tail && commitBreakdown.tail > 1e-6')
q-separator(color='grey-5', style='opacity: 0.35')
.text-caption.text-grey-7
| После фиксации в накоплении останется {{ formatHours(commitBreakdown.tail) }}.
section.create-commit-block
.column.q-gutter-y-md
.row.items-center.q-gutter-x-sm
span.text-caption.text-grey-7 Удовлетворение результатом
span.text-body2.text-weight-medium.text-accent
| {{ satisfactionLabel }}
q-rating(
v-model='formData.satisfaction_stars',
:max='5',
:size='isMobile ? "lg" : "md"',
color-selected='accent'
)
q-input(
v-model='formData.review_text',
type='textarea',
autogrow,
color="accent"
filled,
stack-label,
label='Отзыв (необязательно)',
placeholder='Например, что сработало хорошо или что стоит учесть дальше…',
:maxlength='8000',
counter,
:input-style='{ minHeight: "96px" }'
)
section.create-commit-block
.column.q-gutter-y-md
.row.items-center.q-gutter-x-sm
span.text-caption.text-grey-7 Удовлетворение результатом
span.text-body2.text-weight-medium.text-accent
| {{ satisfactionLabel }}
q-rating(
v-model='formData.satisfaction_stars',
:max='5',
:size='isMobile ? "lg" : "md"',
color-selected='accent'
)
q-input(
v-model='formData.review_text',
type='textarea',
autogrow,
color="accent"
filled,
stack-label,
label='Отзыв (необязательно)',
placeholder='Например, что сработало хорошо или что стоит учесть дальше…',
:maxlength='8000',
counter,
:input-style='{ minHeight: "96px" }'
)
</template>
<script setup lang="ts">
@@ -75,7 +80,7 @@ import { useCreateCommit, type ICreateCommitInput } from '../model';
import { useSystemStore } from 'src/entities/System/model';
import { useSessionStore } from 'src/entities/Session';
import { FailAlert, SuccessAlert } from 'src/shared/api/alerts';
import { ModalBase } from 'src/shared/ui/ModalBase';
import { BaseDialog } from 'src/shared/ui/base/BaseDialog';
import { Form } from 'src/shared/ui/Form';
import { useWindowSize } from 'src/shared/hooks';
import { formatHours } from 'src/shared/lib/utils';
@@ -1,32 +1,37 @@
<template lang="pug">
q-btn(
color='negative',
@click='showDialog = true',
:loading='loading',
label="Отклонить",
:size='mini ? "sm" : "md"',
:dense="isMobile"
)
div
q-btn(
color='negative',
@click='showDialog = true',
:loading='loading',
label="Отклонить",
:size='mini ? "sm" : "md"',
:dense="isMobile"
)
q-dialog(v-model='showDialog', @hide='clear')
ModalBase(:title='"Отклонить коммит"')
Form.q-pa-md(
:handler-submit='handleDeclineCommit',
:is-submitting='isSubmitting',
:button-submit-txt='"Отклонить"',
:button-cancel-txt='"Отмена"',
@cancel='clear'
BaseDialog(
v-model='showDialog',
title='Отклонить коммит',
size='md',
@update:model-value='(v) => !v && clear()'
)
Form.q-pa-md(
:handler-submit='handleDeclineCommit',
:is-submitting='isSubmitting',
:button-submit-txt='"Отклонить"',
:button-cancel-txt='"Отмена"',
@cancel='clear'
)
q-input(
v-model='reason',
outline
label='Причина отклонения',
:rules='[(val) => !!val || "Причина обязательна"]',
autocomplete='off'
placeholder='Укажите причину отклонения...'
type='textarea'
rows='3'
)
q-input(
v-model='reason',
outline
label='Причина отклонения',
:rules='[(val) => !!val || "Причина обязательна"]',
autocomplete='off'
placeholder='Укажите причину отклонения...'
type='textarea'
rows='3'
)
</template>
<script setup lang="ts">
@@ -34,7 +39,7 @@ import { ref } from 'vue';
import { useDeclineCommit } from '../model';
import { useSystemStore } from 'src/entities/System/model';
import { FailAlert, SuccessAlert } from 'src/shared/api/alerts';
import { ModalBase } from 'src/shared/ui/ModalBase';
import { BaseDialog } from 'src/shared/ui/base/BaseDialog';
import { Form } from 'src/shared/ui/Form';
import { useWindowSize } from 'src/shared/hooks';
@@ -1,82 +1,96 @@
<template lang="pug">
q-btn(
color='primary',
@click='showDialog = true',
:loading='loading',
icon='add',
label='Добавить'
)
q-dialog(v-model='showDialog', @hide='clear')
ModalBase(:title='"Импорт участника"')
Form.q-pa-md(
:handler-submit='handleImportContributor',
:is-submitting='isSubmitting',
:button-submit-txt='"Импортировать"',
:button-cancel-txt='"Отмена"',
@cancel='clear'
style="width: 600px; max-width: 100% !important;"
div
//- Canon header-кнопка: на мобильном иконка-only + tooltip.
q-btn(
@click='showDialog = true',
:color='isMobile ? "accent" : "primary"',
:flat='isMobile',
:dense='isMobile',
:size='isMobile ? "sm" : undefined',
:loading='loading',
icon='add',
:label='isMobile ? undefined : "Добавить"',
no-wrap
)
q-tooltip(v-if='isMobile') Добавить участника
BaseDialog(
v-model='showDialog',
title='Импорт участника',
size='md',
@update:model-value='(v) => !v && clear()'
)
Form.q-pa-md(
:handler-submit='handleImportContributor',
:is-submitting='isSubmitting',
:button-submit-txt='"Импортировать"',
:button-cancel-txt='"Отмена"',
@cancel='clear'
style="width: 600px; max-width: 100% !important;"
)
q-input(
v-model='formData.username'
label='Имя пользователя'
:rules='[(val) => !!val || "Имя пользователя обязательно"]'
outlined
)
q-input(
v-model='formData.username'
label='Имя пользователя'
:rules='[(val) => !!val || "Имя пользователя обязательно"]'
outlined
)
q-input(
v-model='formData.contribution_amount'
label='Сумма взноса'
:rules='[(val) => !!val || "Сумма взноса обязательна"]'
outlined
)
template(#append)
span.text-grey-7 {{ info.symbols.root_govern_symbol }}
q-input(
v-model='formData.contribution_amount'
label='Сумма взноса'
:rules='[(val) => !!val || "Сумма взноса обязательна"]'
outlined
)
template(#append)
span.text-grey-7 {{ info.symbols.root_govern_symbol }}
q-input(
v-model='formData.contributor_contract_number'
label='Номер договора'
:rules='[(val) => !!val || "Номер договора обязателен"]'
outlined
)
q-input(
v-model='formData.contributor_contract_number'
label='Номер договора'
:rules='[(val) => !!val || "Номер договора обязателен"]'
outlined
)
q-input(
v-model='formData.contributor_contract_created_at'
label='Дата договора (ДД.ММ.ГГГГ)'
:rules='[(val) => !!val || "Дата договора обязательна"]'
outlined
)
q-input(
v-model='formData.contributor_contract_created_at'
label='Дата договора (ДД.ММ.ГГГГ)'
:rules='[(val) => !!val || "Дата договора обязательна"]'
outlined
)
q-input(
v-model='formData.blagorost_agreement_number'
label='Номер соглашения Благорост'
:rules='[(val) => !!val || "Номер соглашения Благорост обязателен"]'
outlined
)
q-input(
v-model='formData.blagorost_agreement_number'
label='Номер соглашения Благорост'
:rules='[(val) => !!val || "Номер соглашения Благорост обязателен"]'
outlined
)
q-input(
v-model='formData.blagorost_agreement_created_at'
label='Дата соглашения Благорост (ДД.ММ.ГГГГ)'
:rules='[(val) => !!val || "Дата соглашения Благорост обязательна"]'
outlined
)
q-input(
v-model='formData.blagorost_agreement_created_at'
label='Дата соглашения Благорост (ДД.ММ.ГГГГ)'
:rules='[(val) => !!val || "Дата соглашения Благорост обязательна"]'
outlined
)
q-input(
v-model='formData.memo'
label='Примечание'
outlined
)
q-input(
v-model='formData.memo'
label='Примечание'
outlined
)
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useImportContributor } from '../model';
import { FailAlert, SuccessAlert } from 'src/shared/api/alerts';
import { ModalBase } from 'src/shared/ui/ModalBase';
import { BaseDialog } from 'src/shared/ui/base/BaseDialog';
import { Form } from 'src/shared/ui/Form';
import { useSystemStore } from 'src/entities/System/model';
import { useWindowSize } from 'src/shared/hooks';
const { importContributor } = useImportContributor();
const { info } = useSystemStore();
const { isMobile } = useWindowSize();
const showDialog = ref(false);
const isSubmitting = ref(false);
@@ -1,35 +1,41 @@
<template lang="pug">
q-btn(
color='primary',
:loading='isGenerating',
@click='showDialog = true',
label='Инвестировать в проект'
)
q-dialog(v-model='showDialog', @hide='clear')
ModalBase(:title='"Инвестирование в проект"')
Form.q-pa-sm(
:handler-submit='handleInvest',
:is-submitting='isGenerating',
:button-cancel-txt='"Отменить"',
:button-submit-txt='"Инвестировать"',
@cancel='clear'
div
q-btn(
color='primary',
:loading='isGenerating',
@click='showDialog = true',
label='Инвестировать в проект'
)
BaseDialog(
v-model='showDialog',
title='Инвестирование в проект',
size='md',
@update:model-value='(v) => !v && clear()'
)
Form.q-pa-sm(
:handler-submit='handleInvest',
:is-submitting='isGenerating',
:button-cancel-txt='"Отменить"',
:button-submit-txt='"Инвестировать"',
@cancel='clear'
)
q-input(
v-model='quantity',
standout='bg-teal text-white',
placeholder='Введите сумму инвестиций',
type='number',
:min='0',
:rules='[(val) => val > 0 || "Сумма инвестиций должна быть положительной"]'
)
q-input(
v-model='quantity',
standout='bg-teal text-white',
placeholder='Введите сумму инвестиций',
type='number',
:min='0',
:rules='[(val) => val > 0 || "Сумма инвестиций должна быть положительной"]'
)
template(#append)
span.text-overline {{ currency }}
template(#append)
span.text-overline {{ currency }}
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue';
import { Form } from 'src/shared/ui/Form';
import { ModalBase } from 'src/shared/ui/ModalBase';
import { BaseDialog } from 'src/shared/ui/base/BaseDialog';
import { useCreateProjectInvest } from '../model';
import { FailAlert, SuccessAlert } from 'src/shared/api/alerts';
import { useSetPlan } from '../../../Project/SetPlan/model';
@@ -1,50 +1,45 @@
<template lang="pug">
// Рендерим как обычную кнопку, если передан флаг fab
q-btn(
v-if="fab"
color="accent"
:label="fabMainLabel"
icon="attach_money"
@click="openInvest"
fab
).bg-fab-accent-radial
q-dialog(v-model="showDialog", @hide="showDialog = false")
ModalBase(title="Инвестирование в проект" style="width: 400px; max-width: 100%;")
q-card-section.row.items-center
span Инвестиции в проект деньгами или результатами доступны только через их компоненты. Пожалуйста, выберите компонент из списка и инвестируйте во все или в некоторые из них по отдельности.
div.q-mt-md
q-btn(flat label="Понятно" @click="showDialog = false")
q-btn(
color="primary"
label="Перейти к компонентам"
@click="goToComponents"
)
div
// Рендерим как обычную кнопку, если передан флаг fab
q-btn.bg-fab-accent-radial(
v-if="fab"
color="accent"
:label="fabMainLabel"
icon="attach_money"
@click="openInvest"
fab
)
// Иначе рендерим как экшен для FAB
q-fab-action.bg-fab-accent-radial(
v-else
icon="attach_money"
:label="fabActionLabel"
@click="openInvest"
text-color="white"
)
q-dialog(v-model="showDialog", @hide="showDialog = false")
ModalBase(title="Инвестирование в проект" style="width: 400px; max-width: 100%;")
q-card-section.row.items-center
span Инвестиции в проект деньгами или результатами доступны только через их компоненты. Пожалуйста, выберите компонент из списка и инвестируйте во все или в некоторые из них по отдельности.
div.q-mt-md
q-btn(flat label="Понятно" @click="showDialog = false")
q-btn(
color="primary"
label="Перейти к компонентам"
@click="goToComponents"
)
// Иначе рендерим как экшен для FAB
q-fab-action.bg-fab-accent-radial(
v-else
icon="attach_money"
:label="fabActionLabel"
@click="openInvest"
text-color="white"
)
BaseDialog(
v-model="showDialog",
title="Инвестирование в проект",
size="sm",
@update:model-value="(v) => !v && (showDialog = false)"
)
q-card-section.row.items-center
span Инвестиции в проект деньгами или результатами доступны только через их компоненты. Пожалуйста, выберите компонент из списка и инвестируйте во все или в некоторые из них по отдельности.
div.q-mt-md
q-btn(flat label="Понятно" @click="showDialog = false")
q-btn(
color="primary"
label="Перейти к компонентам"
@click="goToComponents"
)
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { ModalBase } from 'src/shared/ui/ModalBase';
import { BaseDialog } from 'src/shared/ui/base/BaseDialog';
import type { IProject } from '../../../../../entities/Project/model';
import { formatCapitalFabLabel } from 'app/extensions/capital/shared/lib';
@@ -1,31 +1,37 @@
<template lang="pug">
q-btn(
v-if='canDelete'
flat
size="sm"
color='negative'
class='full-width q-mt-md'
:label='label'
@click='showDialog = true'
:loading='isSubmitting'
)
q-dialog(v-model='showDialog', @hide='close')
ModalBase(title='Удаление задачи')
Form.q-pa-sm(
:handler-submit='confirmDelete'
:is-submitting='isSubmitting'
:button-cancel-txt='"Отменить"'
:button-submit-txt='"Удалить"'
@cancel='close'
)
div(style='max-width: 360px')
p Вы уверены, что хотите удалить задачу?
div
q-btn(
v-if='canDelete'
flat
size="sm"
color='negative'
class='full-width q-mt-md'
:label='label'
@click='showDialog = true'
:loading='isSubmitting'
)
BaseDialog(
v-model='showDialog',
title='Удаление задачи',
size='sm',
@update:model-value='(v) => !v && close()'
)
Form.q-pa-sm(
:handler-submit='confirmDelete'
:is-submitting='isSubmitting'
:button-cancel-txt='"Отменить"'
:button-submit-txt='"Удалить"'
@cancel='close'
)
div(style='max-width: 360px')
p Вы уверены, что хотите удалить задачу?
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { FailAlert, SuccessAlert } from 'src/shared/api';
import { ModalBase } from 'src/shared/ui/ModalBase';
import { BaseDialog } from 'src/shared/ui/base/BaseDialog';
import { Form } from 'src/shared/ui/Form';
import { useDeleteIssue } from '../model';
@@ -12,37 +12,42 @@
@click="openDialog"
)
.text-caption.q-mt-xs.text-grey-7(v-if="moveDisabledReason") {{ moveDisabledReason }}
q-dialog(v-model="dialogOpen" @hide="resetDialog")
ModalBase(title="Перенос задачи в другой компонент")
Form.q-pa-sm(
:handler-submit="confirmMove"
:is-submitting="isSubmitting"
button-cancel-txt="Отменить"
button-submit-txt="Перенести"
@cancel="close"
)
div(style="max-width: 420px")
p.text-body2.q-mb-sm
| Перенос допустим только в рамках одного проекта. Для перемещения доступны другие компоненты проекта в статусах «ожидание» и «активен». Время по задаче переносится вместе с ней.
q-select(
v-model="selectedHash"
:options="targetOptions"
option-value="project_hash"
option-label="label"
emit-value
map-options
outlined
dense
label="Компонент для переноса"
:loading="optionsLoading"
)
BaseDialog(
v-model="dialogOpen",
title="Перенос задачи в другой компонент",
size="md",
@update:model-value="(v) => !v && resetDialog()"
)
Form.q-pa-sm(
:handler-submit="confirmMove"
:is-submitting="isSubmitting"
button-cancel-txt="Отменить"
button-submit-txt="Перенести"
@cancel="close"
)
div(style="max-width: 420px")
p.text-body2.q-mb-sm
| Перенос допустим только в рамках одного проекта. Для перемещения доступны другие компоненты проекта в статусах «ожидание» и «активен». Время по задаче переносится вместе с ней.
q-select(
v-model="selectedHash"
:options="targetOptions"
option-value="project_hash"
option-label="label"
emit-value
map-options
outlined
dense
label="Компонент для переноса"
:loading="optionsLoading"
)
</template>
<script lang="ts" setup>
import { ref, computed, watch } from 'vue';
import { FailAlert, SuccessAlert } from 'src/shared/api';
import { EMPTY_HASH } from 'src/shared/lib/consts';
import { ModalBase } from 'src/shared/ui/ModalBase';
import { BaseDialog } from 'src/shared/ui/base/BaseDialog';
import { Form } from 'src/shared/ui/Form';
import { Zeus } from '@coopenomics/sdk';
import { api as ProjectApi } from 'app/extensions/capital/entities/Project/api';
@@ -42,44 +42,49 @@ div
@click="() => openDialog(step)"
)
q-dialog(v-model="dialogOpen" persistent)
ModalBase(title="Предложение повестки" @close="closeDialog" style="min-width: 640px")
template(#default)
Loader(
v-if="generatingDocument"
text="Генерация документа..."
)
q-card-section(v-else-if="currentStep && currentGeneratedDoc && !generatingDocument")
div.row.items-center.q-gutter-xs.text-subtitle1.text-weight-medium
q-icon(name="help_outline" size="18px" class="text-primary")
span Вопрос на повестке
div.q-mt-sm.q-pa-sm.text-body1.rounded-borders {{ currentStep.question }}
BaseDialog(
v-model="dialogOpen",
title="Предложение повестки",
size="lg",
:close-on-backdrop="false",
:close-on-escape="false",
@update:model-value="(v) => !v && closeDialog()"
)
Loader(
v-if="generatingDocument"
text="Генерация документа..."
)
q-card-section(v-else-if="currentStep && currentGeneratedDoc && !generatingDocument")
div.row.items-center.q-gutter-xs.text-subtitle1.text-weight-medium
q-icon(name="help_outline" size="18px" class="text-primary")
span Вопрос на повестке
div.q-mt-sm.q-pa-sm.text-body1.rounded-borders {{ currentStep.question }}
q-separator.q-my-md
q-separator.q-my-md
div.row.items-center.q-gutter-xs.text-subtitle1.text-weight-medium
q-icon(name="gavel" size="18px" class="text-primary")
span Проект решения
div.q-mt-sm.q-pa-sm.rounded-borders
div(v-if="currentStep.decisionPrefix") {{ currentStep.decisionPrefix }}
DocumentHtmlReader(:html="currentGeneratedDoc.html" :sanitize="false")
div.q-mt-sm.text-caption.text-grey-6
strong {{ currentGeneratedDoc.full_title }}
div.row.items-center.q-gutter-xs.text-subtitle1.text-weight-medium
q-icon(name="gavel" size="18px" class="text-primary")
span Проект решения
div.q-mt-sm.q-pa-sm.rounded-borders
div(v-if="currentStep.decisionPrefix") {{ currentStep.decisionPrefix }}
DocumentHtmlReader(:html="currentGeneratedDoc.html" :sanitize="false")
div.q-mt-sm.text-caption.text-grey-6
strong {{ currentGeneratedDoc.full_title }}
div.q-pb-lg(v-if="currentGeneratedDoc && !generatingDocument")
q-btn(flat label="Отмена" @click="closeDialog" :disable="submitting")
q-btn(
color="primary"
label="Объявить"
:loading="submitting"
@click="submitStep"
)
div.q-pb-lg(v-if="currentGeneratedDoc && !generatingDocument")
q-btn(flat label="Отмена" @click="closeDialog" :disable="submitting")
q-btn(
color="primary"
label="Объявить"
:loading="submitting"
@click="submitStep"
)
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { DocumentHtmlReader } from 'src/shared/ui/DocumentHtmlReader';
import { ModalBase } from 'src/shared/ui/ModalBase';
import { BaseDialog } from 'src/shared/ui/base/BaseDialog';
import { WindowLoader, Loader } from 'src/shared/ui/Loader';
import type { ICouncilOnboardingStep } from 'src/shared/ui/CouncilOnboarding';
import { useCapitalOnboarding } from '../model';
@@ -1,37 +1,43 @@
<template lang="pug">
q-btn(
color='primary'
flat
class='q-mt-sm full-width'
:loading='isGenerating'
@click='showDialog = true'
label='Инвестировать'
)
q-dialog(v-model='showDialog', @hide='clear')
ModalBase(:title='"Инвестирование в программу"')
Form.q-pa-sm(
:handler-submit='handleInvest',
:is-submitting='isGenerating',
:button-cancel-txt='"Отменить"',
:button-submit-txt='"Инвестировать"',
@cancel='clear'
div
q-btn(
color='primary'
flat
class='q-mt-sm full-width'
:loading='isGenerating'
@click='showDialog = true'
label='Инвестировать'
)
BaseDialog(
v-model='showDialog',
title='Инвестирование в программу',
size='md',
@update:model-value='(v) => !v && clear()'
)
Form.q-pa-sm(
:handler-submit='handleInvest',
:is-submitting='isGenerating',
:button-cancel-txt='"Отменить"',
:button-submit-txt='"Инвестировать"',
@cancel='clear'
)
q-input(
v-model='quantity',
standout='bg-teal text-white',
placeholder='Введите сумму',
type='number',
:min='0',
:rules='[(val) => val > 0 || "Сумма должна быть положительной"]'
)
q-input(
v-model='quantity',
standout='bg-teal text-white',
placeholder='Введите сумму',
type='number',
:min='0',
:rules='[(val) => val > 0 || "Сумма должна быть положительной"]'
)
template(#append)
span.text-overline {{ currency }}
template(#append)
span.text-overline {{ currency }}
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import { Form } from 'src/shared/ui/Form';
import { ModalBase } from 'src/shared/ui/ModalBase';
import { BaseDialog } from 'src/shared/ui/base/BaseDialog';
import { useCreateProgramInvest } from '../model';
import { FailAlert, SuccessAlert } from 'src/shared/api/alerts';
import { useSystemStore } from 'src/entities/System/model';
@@ -1,41 +1,43 @@
<template lang="pug">
q-btn(
size='sm',
color='primary',
@click='showDialog = true',
:loading='isSubmitting',
label='Добавить соавтора'
)
q-dialog(v-model='showDialog', @hide='close')
ModalBase(title='Добавить соавторов')
Form.q-pa-sm(
:handler-submit='handleAddAuthors',
:is-submitting='isSubmitting',
:button-cancel-txt='"Отменить"',
:button-submit-txt='"Добавить"',
@cancel='close'
)
div(style='max-width: 400px')
.text-body2.q-mb-sm
| После добавления соавторов их удаление будет невозможно. Для добавления соавторов они должны предварительно получить допуск на участие в проекте.
ContributorSelector(
v-model='selectedAuthors'
:multi-select='true'
:dense='true'
:disable='isSubmitting'
:project-hash='props.project.project_hash'
placeholder='Выберите соавторов...'
label='Соавторы'
class='authors-selector'
)
.text-caption.text-grey-6.q-mt-sm(v-if='selectedAuthors.length > 0')
| Выбрано соавторов: {{ selectedAuthors.length }}
div
q-btn(
size='sm',
color='primary',
@click='showDialog = true',
:loading='isSubmitting',
label='Добавить соавтора'
)
BaseDialog(
v-model='showDialog',
title='Добавить соавторов',
size='md',
@update:model-value='(v) => !v && close()'
)
Form.q-pa-sm(
:handler-submit='handleAddAuthors',
:is-submitting='isSubmitting',
:button-cancel-txt='"Отменить"',
:button-submit-txt='"Добавить"',
@cancel='close'
)
div(style='max-width: 400px')
.text-body2.q-mb-sm
| После добавления соавторов их удаление будет невозможно. Для добавления соавторов они должны предварительно получить допуск на участие в проекте.
ContributorSelector(
v-model='selectedAuthors'
:multi-select='true'
:dense='true'
:disable='isSubmitting'
:project-hash='props.project.project_hash'
placeholder='Выберите соавторов...'
label='Соавторы'
class='authors-selector'
)
.text-caption.text-grey-6.q-mt-sm(v-if='selectedAuthors.length > 0')
| Выбрано соавторов: {{ selectedAuthors.length }}
</template>
<script setup lang="ts">
@@ -43,7 +45,7 @@ import { ref, watch } from 'vue';
import { useAddAuthor } from '../model';
import { FailAlert, SuccessAlert } from 'src/shared/api/alerts';
import { ContributorSelector } from '../../../../entities/Contributor';
import { ModalBase } from 'src/shared/ui/ModalBase';
import { BaseDialog } from 'src/shared/ui/base/BaseDialog';
import { Form } from 'src/shared/ui/Form';
import type { IProject } from '../../../../entities/Project/model';
import type { IContributor } from '../../../../entities/Contributor/model';
@@ -1,31 +1,37 @@
<template lang="pug">
q-btn(
v-if='canDelete'
flat
color='negative'
class='full-width q-mt-md'
label='Удалить'
@click='showDialog = true'
:loading='isSubmitting'
size="sm"
)
q-dialog(v-model='showDialog', @hide='close')
ModalBase(:title='dialogTitle')
Form.q-pa-sm(
:handler-submit='confirmDelete'
:is-submitting='isSubmitting'
:button-cancel-txt='"Отменить"'
:button-submit-txt='"Удалить"'
@cancel='close'
)
div(style='max-width: 360px')
p {{ confirmMessage }}
div
q-btn(
v-if='canDelete'
flat
color='negative'
class='full-width q-mt-md'
label='Удалить'
@click='showDialog = true'
:loading='isSubmitting'
size="sm"
)
BaseDialog(
v-model='showDialog',
:title='dialogTitle',
size='sm',
@update:model-value='(v) => !v && close()'
)
Form.q-pa-sm(
:handler-submit='confirmDelete'
:is-submitting='isSubmitting'
:button-cancel-txt='"Отменить"'
:button-submit-txt='"Удалить"'
@cancel='close'
)
div(style='max-width: 360px')
p {{ confirmMessage }}
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { FailAlert, SuccessAlert } from 'src/shared/api';
import { ModalBase } from 'src/shared/ui/ModalBase';
import { BaseDialog } from 'src/shared/ui/base/BaseDialog';
import { Form } from 'src/shared/ui/Form';
import { useDeleteProject } from '../model';
@@ -1,55 +1,61 @@
<template lang="pug">
q-btn(
v-if="props.project?.permissions?.can_set_plan"
size='sm',
color='primary',
@click='showDialog = true',
label='Установить план проекта'
)
q-dialog(v-model='showDialog', @hide='clear')
ModalBase(:title='"Установить план проекта"')
Form.q-pa-md(
:handler-submit='handleSetPlan',
:is-submitting='isSubmitting',
:button-submit-txt='"Установить план"',
:button-cancel-txt='"Отмена"',
@cancel='clear'
style="width: 600px; max-width: 100% !important;"
div
q-btn(
v-if="props.project?.permissions?.can_set_plan"
size='sm',
color='primary',
@click='showDialog = true',
label='Установить план проекта'
)
BaseDialog(
v-model='showDialog',
title='Установить план проекта',
size='md',
@update:model-value='(v) => !v && clear()'
)
Form.q-pa-md(
:handler-submit='handleSetPlan',
:is-submitting='isSubmitting',
:button-submit-txt='"Установить план"',
:button-cancel-txt='"Отмена"',
@cancel='clear'
style="width: 600px; max-width: 100% !important;"
)
q-input(
v-model.number='formData.plan_creators_hours'
label='Плановое количество часов исполнителей'
type='number'
:rules='[(val) => val > 0 || "Количество часов должно быть больше 0"]'
outlined
)
q-input(
v-model.number='formData.plan_creators_hours'
label='Плановое количество часов исполнителей'
type='number'
:rules='[(val) => val > 0 || "Количество часов должно быть больше 0"]'
outlined
)
template(#append)
span.text-grey-7 ч
template(#append)
span.text-grey-7 ч
q-input(
v-model='formData.plan_expenses'
label='Плановые расходы'
:rules='[(val) => !!val || "Плановые расходы обязательны"]'
outlined
)
template(#append)
span.text-grey-7 {{ governSymbol }}
q-input(
v-model='formData.plan_expenses'
label='Плановые расходы'
:rules='[(val) => !!val || "Плановые расходы обязательны"]'
outlined
)
template(#append)
span.text-grey-7 {{ governSymbol }}
q-input(
v-model='formData.plan_hour_cost'
label='Стоимость часа работы'
:rules='[(val) => !!val || "Стоимость часа работы обязательна"]'
outlined
)
template(#append)
span.text-grey-7 {{ governSymbol }}
q-input(
v-model='formData.plan_hour_cost'
label='Стоимость часа работы'
:rules='[(val) => !!val || "Стоимость часа работы обязательна"]'
outlined
)
template(#append)
span.text-grey-7 {{ governSymbol }}
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import { useSetPlan } from '../model';
import { FailAlert, SuccessAlert } from 'src/shared/api/alerts';
import { ModalBase } from 'src/shared/ui/ModalBase';
import { BaseDialog } from 'src/shared/ui/base/BaseDialog';
import { Form } from 'src/shared/ui/Form';
import type { IProject } from '../../../../entities/Project/model';
@@ -1,42 +1,44 @@
<template lang="pug">
q-btn(
color='primary',
@click='showDialog = true',
:loading='loading',
label='Внести результат'
)
div
q-btn(
color='primary',
@click='showDialog = true',
:loading='loading',
label='Внести результат'
)
q-dialog(v-model='showDialog', @hide='clear')
ModalBase(:title='"Подтверждение внесения результата"')
.q-pa-md
// Общая сумма стоимости сегмента
ColorCard(color='green')
.card-label Ваш паевой взнос
.card-value {{ contributionAmount }}
BaseDialog(
v-model='showDialog',
title='Подтверждение внесения результата',
size='md',
@update:model-value='(v) => !v && clear()'
)
.q-pa-md
ColorCard(color='green')
.card-label Ваш паевой взнос
.card-value {{ contributionAmount }}
// Сумма взятой ссуды (только если > 0)
ColorCard(color='orange', v-if='parseFloat(debtAmount) > 0')
.card-label Сумма погашаемой ссуды
.card-value {{ debtAmount }}
ColorCard(color='orange', v-if='parseFloat(debtAmount) > 0')
.card-label Сумма погашаемой ссуды
.card-value {{ debtAmount }}
Form.q-pa-md(
:handler-submit='handlePushResult',
:is-submitting='isSubmitting',
:button-submit-txt='"Подтвердить внесение"',
:button-cancel-txt='"Отмена"',
@cancel='clear'
)
Form.q-pa-md(
:handler-submit='handlePushResult',
:is-submitting='isSubmitting',
:button-submit-txt='"Подтвердить внесение"',
:button-cancel-txt='"Отмена"',
@cancel='clear'
)
template(#footer='')
.q-pa-md.text-center
p.text-caption Подтверждая внесение результата, вы соглашаетесь с генерацией и подписью заявления на паевой взнос
.q-pa-md.text-center
p.text-caption Подтверждая внесение результата, вы соглашаетесь с генерацией и подписью заявления на паевой взнос
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import { usePushResult } from '../model';
import { FailAlert, SuccessAlert } from 'src/shared/api/alerts';
import { ModalBase } from 'src/shared/ui/ModalBase';
import { BaseDialog } from 'src/shared/ui/base/BaseDialog';
import { Form } from 'src/shared/ui/Form';
import { ColorCard } from 'src/shared/ui/ColorCard';
import { formatAsset2Digits } from 'src/shared/lib/utils/formatAsset2Digits';
@@ -1,32 +1,38 @@
<template lang="pug">
q-btn(
color='grey',
flat,
dense,
clickable,
size='sm',
@click='showDialog = true',
:loading='isSubmitting'
)
q-icon(name='delete')
q-dialog(v-model='showDialog', @hide='close')
ModalBase(title='Удаление')
Form.q-pa-sm(
:handler-submit='deleteStory',
:is-submitting='isSubmitting',
:button-cancel-txt='"Отменить"',
:button-submit-txt='"Удалить"',
@cancel='close'
)
div(style='max-width: 300px')
p Вы уверены, что хотите удалить?
div
q-btn(
color='grey',
flat,
dense,
clickable,
size='sm',
@click='showDialog = true',
:loading='isSubmitting'
)
q-icon(name='delete')
BaseDialog(
v-model='showDialog',
title='Удаление',
size='sm',
@update:model-value='(v) => !v && close()'
)
Form.q-pa-sm(
:handler-submit='deleteStory',
:is-submitting='isSubmitting',
:button-cancel-txt='"Отменить"',
:button-submit-txt='"Удалить"',
@cancel='close'
)
div(style='max-width: 300px')
p Вы уверены, что хотите удалить?
</template>
<script lang="ts" setup>
import { FailAlert, SuccessAlert } from 'src/shared/api';
import { useDeleteStory } from '../model';
import { ref } from 'vue';
import { ModalBase } from 'src/shared/ui/ModalBase';
import { BaseDialog } from 'src/shared/ui/base/BaseDialog';
import { Form } from 'src/shared/ui/Form';
const { deleteStory: deleteStoryAction } = useDeleteStory();
@@ -74,7 +74,7 @@ import { useSystemStore } from 'src/entities/System/model';
import { EditAboutInput, EditHoursPerDayInput, EditRatePerHourInput } from 'app/extensions/capital/features/Contributor/EditContributor';
import { formatAsset2Digits } from 'src/shared/lib/utils/formatAsset2Digits';
import {ColorCard} from 'src/shared/ui';
import { AutoAvatar } from 'src/shared/ui/AutoAvatar';
import { AutoAvatar } from 'src/shared/ui/domain/AutoAvatar';
import { CapitalWalletsCardsWidget } from 'app/extensions/capital/widgets/CapitalWalletsCardsWidget';
const contributorStore = useContributorStore();
const system = useSystemStore();
@@ -294,6 +294,7 @@ const menuButtons = computed(() => {
props: {
routeName: 'component-description',
label: 'Описание',
icon: 'description',
routeParams: { project_hash: projectHash.value },
query,
},
@@ -305,6 +306,7 @@ const menuButtons = computed(() => {
props: {
routeName: 'component-tasks',
label: 'Задачи',
icon: 'task_alt',
routeParams: { project_hash: projectHash.value },
query,
},
@@ -316,6 +318,7 @@ const menuButtons = computed(() => {
props: {
routeName: 'component-requirements',
label: 'Артефакты',
icon: 'inventory_2',
routeParams: { project_hash: projectHash.value },
query,
},
@@ -327,6 +330,7 @@ const menuButtons = computed(() => {
props: {
routeName: 'component-planning',
label: 'План',
icon: 'timeline',
routeParams: { project_hash: projectHash.value },
query,
},
@@ -338,6 +342,7 @@ const menuButtons = computed(() => {
props: {
routeName: 'component-voting',
label: 'Голосование',
icon: 'how_to_vote',
routeParams: { project_hash: projectHash.value },
query,
},
@@ -349,6 +354,7 @@ const menuButtons = computed(() => {
props: {
routeName: 'component-results',
label: 'Результаты',
icon: 'assessment',
routeParams: { project_hash: projectHash.value },
query,
},
@@ -360,6 +366,7 @@ const menuButtons = computed(() => {
props: {
routeName: 'component-contributors',
label: 'Участники',
icon: 'groups',
routeParams: { project_hash: projectHash.value },
query,
},
@@ -371,6 +378,7 @@ const menuButtons = computed(() => {
props: {
routeName: 'component-history',
label: 'История',
icon: 'history',
routeParams: { project_hash: projectHash.value },
query,
},
@@ -52,7 +52,6 @@ div
import { onMounted, ref, computed } from 'vue';
import { useSystemStore } from 'src/entities/System/model';
import { useExpandableState } from 'src/shared/lib/composables';
import 'src/shared/ui/TitleStyles';
import { WindowLoader } from 'src/shared/ui/Loader';
import { ColorCard } from 'src/shared/ui/ColorCard/ui';
import { ResultSubmissionSegmentsWidget, ResultSubmissionActionsWidget } from 'app/extensions/capital/widgets';
@@ -58,7 +58,6 @@ import { onMounted, onBeforeUnmount, ref, computed } from 'vue';
import { useSystemStore } from 'src/entities/System/model';
import { useExpandableState, useDataPoller } from 'src/shared/lib/composables';
import { POLL_INTERVALS } from 'src/shared/lib/consts';
import 'src/shared/ui/TitleStyles';
import { WindowLoader } from 'src/shared/ui/Loader';
import { ColorCard } from 'src/shared/ui/ColorCard/ui';
import { ProjectVotingSegmentsWidget, SegmentVotesWidget } from 'app/extensions/capital/widgets';
@@ -38,7 +38,6 @@ import { WindowLoader } from 'src/shared/ui/Loader';
import { InvitationsListWidget } from 'app/extensions/capital/widgets';
import { ReferralLinkCard } from 'src/shared/ui/ReferralLinkCard';
import { ColorCard } from 'src/shared/ui/ColorCard/ui';
import 'src/shared/ui/TitleStyles';
const { referralLink } = useReferralLink();
@@ -267,6 +267,7 @@ const menuButtons = computed(() => {
props: {
routeName: 'project-description',
label: 'Описание',
icon: 'description',
routeParams: { project_hash: projectHash.value },
query,
},
@@ -278,6 +279,7 @@ const menuButtons = computed(() => {
props: {
routeName: 'project-requirements',
label: 'Артефакты',
icon: 'inventory_2',
routeParams: { project_hash: projectHash.value },
query,
},
@@ -289,6 +291,7 @@ const menuButtons = computed(() => {
props: {
routeName: 'project-components',
label: 'Компоненты',
icon: 'widgets',
routeParams: { project_hash: projectHash.value },
query,
},
@@ -300,6 +303,7 @@ const menuButtons = computed(() => {
props: {
routeName: 'project-planning',
label: 'План',
icon: 'timeline',
routeParams: { project_hash: projectHash.value },
query,
},
@@ -311,6 +315,7 @@ const menuButtons = computed(() => {
props: {
routeName: 'project-contributors',
label: 'Участники',
icon: 'groups',
routeParams: { project_hash: projectHash.value },
query,
},
@@ -322,6 +327,7 @@ const menuButtons = computed(() => {
props: {
routeName: 'project-history',
label: 'История',
icon: 'history',
routeParams: { project_hash: projectHash.value },
query,
},
@@ -45,7 +45,6 @@ div
import { onMounted, onBeforeMount, onBeforeUnmount, ref, computed, markRaw, watch } from 'vue';
import { useRouter } from 'vue-router';
import { useExpandableState } from 'src/shared/lib/composables';
import 'src/shared/ui/TitleStyles';
import { Fab } from 'src/shared/ui';
import { FilterDialogWithButton } from 'app/extensions/capital/shared/ui';
import { useHeaderActions } from 'src/shared/hooks';
@@ -34,7 +34,6 @@ div
import { onMounted, ref } from 'vue';
import { useSystemStore } from 'src/entities/System/model';
import { useExpandableState } from 'src/shared/lib/composables';
import 'src/shared/ui/TitleStyles';
import { WindowLoader } from 'src/shared/ui/Loader';
import { ListResultProjectsWidget, ResultSubmissionSegmentsWidget, ResultSubmissionActionsWidget } from 'app/extensions/capital/widgets';
import { useSegmentStore } from 'app/extensions/capital/entities/Segment/model';
@@ -42,7 +42,6 @@ import { onMounted, onBeforeUnmount, ref } from 'vue';
import { useSystemStore } from 'src/entities/System/model';
import { useExpandableState, useDataPoller } from 'src/shared/lib/composables';
import { POLL_INTERVALS } from 'src/shared/lib/consts';
import 'src/shared/ui/TitleStyles';
import { WindowLoader } from 'src/shared/ui/Loader';
import { ListVotingProjectWidget, ProjectVotingSegmentsWidget, SegmentVotesWidget } from 'app/extensions/capital/widgets';
import { useSessionStore } from 'src/entities/Session';
@@ -38,7 +38,6 @@ import { onMounted, ref } from 'vue';
import { useSystemStore } from 'src/entities/System/model';
import { useSessionStore } from 'src/entities/Session/model/store';
import { useExpandableState } from 'src/shared/lib/composables';
import 'src/shared/ui/TitleStyles';
import { WindowLoader } from 'src/shared/ui/Loader';
import { TimeStatsWidget, TimeIssuesWidget, TimeEntriesWidget } from 'app/extensions/capital/widgets';
@@ -1,27 +1,30 @@
<template lang="pug">
div
// Кнопка для открытия диалога
//- Canon header-кнопка: на мобильном иконка-only + tooltip.
q-btn(
size="md"
flat
icon="filter_list"
label="Фильтры"
:stretch="stretch"
:style="style"
@click="handleClick"
:size='isMobile ? "sm" : "md"',
flat,
:dense='isMobile',
icon='filter_list',
:label='isMobile ? undefined : "Фильтры"',
:stretch='stretch',
:style='style',
no-wrap,
@click='handleClick'
)
q-badge(
v-if="hasActiveFilters"
color="red"
v-if='hasActiveFilters'
color='red'
floating
rounded
)
q-tooltip(v-if='isMobile') Фильтры
// Диалог фильтров
//- Диалог фильтров
FilterDialog(
ref="filterDialogRef"
title="Фильтры проектов"
@filters-applied="handleFiltersApplied"
ref='filterDialogRef'
title='Фильтры проектов'
@filters-applied='handleFiltersApplied'
)
</template>
@@ -29,6 +32,9 @@ div
import { ref, computed } from 'vue';
import { FilterDialog } from 'app/extensions/capital/features/FilterDialog';
import { useProjectStore } from 'app/extensions/capital/entities/Project/model';
import { useWindowSize } from 'src/shared/hooks';
const { isMobile } = useWindowSize();
// Props для стилизации из header
defineProps<{
@@ -33,7 +33,6 @@ import { computed } from 'vue';
import { useWalletStore } from 'src/entities/Wallet/model';
import { ColorCard } from 'src/shared/ui';
import { formatAsset2Digits } from 'src/shared/lib/utils/formatAsset2Digits';
import { addAssets } from 'src/shared/lib/utils/addAssets';
import { Zeus } from '@coopenomics/sdk';
import { CreateProgramInvestButton } from 'app/extensions/capital/features/ProgramInvest/CreateProgramInvest/ui';
@@ -47,8 +46,7 @@ const generatorWallet = computed(() => {
return {
available: wallet?.available || '0.00',
blocked: wallet?.blocked || '0.00',
total: addAssets(wallet?.available || '0.00', wallet?.blocked || '0.00'),
total: wallet?.available || '0.00',
};
});
@@ -61,8 +59,7 @@ const blagorostWallet = computed(() => {
return {
available: wallet?.available || '0.00',
blocked: wallet?.blocked || '0.00',
total: addAssets(wallet?.available || '0.00', wallet?.blocked || '0.00'),
total: wallet?.available || '0.00',
};
});
@@ -67,7 +67,6 @@ import { EditAboutInput, EditHoursPerDayInput, EditRatePerHourInput } from 'app/
import { ContributorGamificationWidget } from 'app/extensions/capital/widgets/ContributorGamificationWidget';
import { ColorCard } from 'src/shared/ui';
import { formatAsset2Digits } from 'src/shared/lib/utils/formatAsset2Digits';
import 'src/shared/ui/CardStyles';
const contributorStore = useContributorStore();
const { info } = useSystemStore();
@@ -244,45 +244,30 @@ const calculateInvestorTotal = (contributor: IContributor) => {
// Функции для расчета сумм кошельков
const calculateMainWalletTotal = (contributor: IContributor) => {
const availableStr = contributor?.main_wallet?.available || '0.0000';
const blockedStr = contributor?.main_wallet?.blocked || '0.0000';
const total = Number(availableStr.split(' ')[0] || '0');
const available = Number(availableStr.split(' ')[0] || '0');
const blocked = Number(blockedStr.split(' ')[0] || '0');
const total = available + blocked;
// Определяем валюту из любого поля, которое ее содержит, или используем системный символ
const currency = availableStr.split(' ')[1] || blockedStr.split(' ')[1] || governSymbol.value;
// Определяем валюту из поля available или используем системный символ
const currency = availableStr.split(' ')[1] || governSymbol.value;
return currency ? `${total} ${currency}` : total.toString();
};
const calculateGenerationWalletTotal = (contributor: IContributor) => {
const availableStr = contributor?.generation_wallet?.available || '0.0000';
const blockedStr = contributor?.generation_wallet?.blocked || '0.0000';
const total = Number(availableStr.split(' ')[0] || '0');
const available = Number(availableStr.split(' ')[0] || '0');
const blocked = Number(blockedStr.split(' ')[0] || '0');
const total = available + blocked;
// Определяем валюту из любого поля, которое ее содержит, или используем системный символ
const currency = availableStr.split(' ')[1] || blockedStr.split(' ')[1] || governSymbol.value;
// Определяем валюту из поля available или используем системный символ
const currency = availableStr.split(' ')[1] || governSymbol.value;
return currency ? `${total} ${currency}` : total.toString();
};
const calculateBlagorostWalletTotal = (contributor: IContributor) => {
const availableStr = contributor?.blagorost_wallet?.available || '0.0000';
const blockedStr = contributor?.blagorost_wallet?.blocked || '0.0000';
const total = Number(availableStr.split(' ')[0] || '0');
const available = Number(availableStr.split(' ')[0] || '0');
const blocked = Number(blockedStr.split(' ')[0] || '0');
const total = available + blocked;
// Определяем валюту из любого поля, которое ее содержит, или используем системный символ
const currency = availableStr.split(' ')[1] || blockedStr.split(' ')[1] || governSymbol.value;
// Определяем валюту из поля available или используем системный символ
const currency = availableStr.split(' ')[1] || governSymbol.value;
return currency ? `${total} ${currency}` : total.toString();
};
@@ -1,128 +1,145 @@
<template lang="pug">
q-btn(color="primary" @click="showDialog = true" icon="file_upload") Импорт из CSV
q-dialog(v-model='showDialog', @hide='clear' maximized)
ModalBase(:title='"Импорт участников"')
.q-pa-md
q-card(v-if='!hasConfig' flat)
div
//- Canon header-кнопка: на мобильном иконка-only + tooltip.
q-btn(
@click='showDialog = true',
:color='isMobile ? "accent" : "primary"',
:flat='isMobile',
:dense='isMobile',
:size='isMobile ? "sm" : undefined',
icon='file_upload',
:label='isMobile ? undefined : "Импорт из CSV"',
no-wrap
)
q-tooltip(v-if='isMobile') Импорт участников из CSV
BaseDialog(
v-model='showDialog',
title='Импорт участников',
:maximized='true',
@update:model-value='(v) => !v && clear()'
)
.q-pa-md
q-card(v-if='!hasConfig' flat)
q-card-section
.row.items-center.q-gutter-sm
q-icon(name='upload_file', size='24px')
div
.text-h6 Импорт участников
.text-subtitle2 Загрузка участников из CSV файла
q-space
q-btn(flat color='primary' icon='download' @click='downloadSample') Скачать шаблон CSV
q-separator
q-card-section
template(v-if='parsedData.length === 0')
CsvUploader(@parsed='onCsvParsed')
template(
v-if='parsedData.length > 0 && !isImporting && !isImportCompleted'
)
q-separator
q-card-section
.row.items-center.q-gutter-sm
q-icon(name='upload_file', size='24px')
div
.text-h6 Импорт участников
.text-subtitle2 Загрузка участников из CSV файла
q-space
q-btn(flat color='primary' icon='download' @click='downloadSample') Скачать шаблон CSV
.row.items-center.q-gutter-sm.q-mb-md
q-icon(name='table_chart', size='20px')
.text-subtitle1 Предварительный просмотр данных ({{ parsedData.length }} записей)
q-table(
:rows='parsedData',
:columns='previewColumns',
:loading='false',
:pagination='{ rowsPerPage: 0 }',
row-key='id',
binary-state-sort
)
template(#body-cell-status='props')
q-td
q-chip(
:color='getStatusColor(props.value)',
text-color='white',
dense
) {{ getStatusText(props.value) }}
q-separator
q-card-section
template(v-if='parsedData.length === 0')
CsvUploader(@parsed='onCsvParsed')
.row.items-center.q-gutter-sm.q-mb-md
q-icon(name='playlist_add_check', size='20px')
.text-subtitle1 Импорт данных
template(
v-if='parsedData.length > 0 && !isImporting && !isImportCompleted'
)
q-separator
q-card-section
.row.items-center.q-gutter-sm.q-mb-md
q-icon(name='table_chart', size='20px')
.text-subtitle1 Предварительный просмотр данных ({{ parsedData.length }} записей)
q-table(
:rows='parsedData',
:columns='previewColumns',
:loading='false',
:pagination='{ rowsPerPage: 0 }',
row-key='id',
binary-state-sort
.row.justify-end.q-gutter-sm.q-mb-md
q-btn(
color='primary',
:loading='isImporting',
@click='startImport',
label='Начать импорт',
unelevated,
icon='play_arrow'
)
template(#body-cell-status='props')
q-td
q-chip(
:color='getStatusColor(props.value)',
text-color='white',
dense
) {{ getStatusText(props.value) }}
q-separator
template(v-if='importProgress > 0')
.q-mt-md
.text-body2.q-mb-sm Статистика
.row.q-gutter-md
.col-auto
.text-weight-medium
q-icon.q-mr-xs(name='check_circle', color='positive')
| Успешно: {{ successCount }}
.col-auto
.text-weight-medium
q-icon.q-mr-xs(name='error', color='negative')
| Ошибок: {{ errorCount }}
q-card-section
.row.items-center.q-gutter-sm.q-mb-md
q-icon(name='playlist_add_check', size='20px')
.text-subtitle1 Импорт данных
template(v-if='isImporting')
q-separator
.row.justify-end.q-gutter-sm.q-mb-md
q-btn(
color='primary',
:loading='isImporting',
@click='startImport',
label='Начать импорт',
unelevated,
icon='play_arrow'
)
template(v-if='importProgress > 0')
.q-mt-md
.text-body2.q-mb-sm Статистика
.row.q-gutter-md
.col-auto
.text-weight-medium
q-icon.q-mr-xs(name='check_circle', color='positive')
| Успешно: {{ successCount }}
.col-auto
.text-weight-medium
q-icon.q-mr-xs(name='error', color='negative')
| Ошибок: {{ errorCount }}
template(v-if='isImporting')
q-separator
q-card-section
.row.items-center.q-gutter-sm.q-mb-md
q-icon(name='sync', size='20px')
.text-subtitle1 Выполняется импорт данных
.q-mb-md
.text-body2.q-mb-sm Прогресс импорта: {{ importProgress }}/{{ parsedData.length }} ({{ progressPercent }}%)
q-linear-progress(
:value='progressPercent / 100',
color='primary',
size='8px'
)
.row.justify-end.q-gutter-sm
q-btn(
color='negative',
@click='stopImport',
label='Остановить',
flat,
icon='stop'
)
template(v-if='importResults.length > 0 || isImportCompleted')
q-separator
q-card-section
.row.items-center.q-gutter-sm.q-mb-md
q-icon(name='table_chart', size='20px')
.text-subtitle1 Результаты импорта
ImportResultsTable(
:items='importResults',
:is-importing='isImporting',
:current-index='currentImportIndex',
@retry='retryImport',
@retry-all-failed='retryAllFailed',
@clear='clearAll'
)
q-card(v-else)
q-card-section
.row.items-center.q-gutter-sm
q-icon(name='info', size='24px', color='info')
div
.text-h6 Конфигурация уже установлена
.text-subtitle2 Импорт участников недоступен после установки конфигурации контракта.
.row.items-center.q-gutter-sm.q-mb-md
q-icon(name='sync', size='20px')
.text-subtitle1 Выполняется импорт данных
.q-mb-md
.text-body2.q-mb-sm Прогресс импорта: {{ importProgress }}/{{ parsedData.length }} ({{ progressPercent }}%)
q-linear-progress(
:value='progressPercent / 100',
color='primary',
size='8px'
)
.row.justify-end.q-gutter-sm
q-btn(
color='negative',
@click='stopImport',
label='Остановить',
flat,
icon='stop'
)
template(v-if='importResults.length > 0 || isImportCompleted')
q-separator
q-card-section
.row.items-center.q-gutter-sm.q-mb-md
q-icon(name='table_chart', size='20px')
.text-subtitle1 Результаты импорта
ImportResultsTable(
:items='importResults',
:is-importing='isImporting',
:current-index='currentImportIndex',
@retry='retryImport',
@retry-all-failed='retryAllFailed',
@clear='clearAll'
)
q-card(v-else)
q-card-section
.row.items-center.q-gutter-sm
q-icon(name='info', size='24px', color='info')
div
.text-h6 Конфигурация уже установлена
.text-subtitle2 Импорт участников недоступен после установки конфигурации контракта.
</template>
<script setup lang="ts">
@@ -138,7 +155,10 @@ import { ref, computed, onMounted } from 'vue';
import { FailAlert, NotifyAlert, SuccessAlert } from 'src/shared/api/alerts';
import { useConfigStore } from 'app/extensions/capital/entities/Config/model';
import { useSystemStore } from 'src/entities/System/model';
import { ModalBase } from 'src/shared/ui/ModalBase';
import { BaseDialog } from 'src/shared/ui/base/BaseDialog';
import { useWindowSize } from 'src/shared/hooks';
const { isMobile } = useWindowSize();
// Определение колонок для предварительного просмотра
const previewColumns: QTableProps['columns'] = [
@@ -1,34 +1,38 @@
<template lang="pug">
q-btn(
color='positive',
icon='check',
size='sm',
flat,
round,
@click='showDialog = true',
:loading='isSubmitting'
)
q-tooltip Одобрить
q-dialog(v-model='showDialog', @hide='close')
ModalBase(title='Подтверждение одобрения')
Form.q-pa-sm(
:handler-submit='confirmApproval',
:is-submitting='isSubmitting',
:button-cancel-txt='"Отменить"',
:button-submit-txt='"Одобрить"',
@cancel='close'
)
div(style='max-width: 300px')
p Вы уверены, что хотите одобрить документ?
div
q-btn(
color='positive',
icon='check',
size='sm',
flat,
round,
@click='showDialog = true',
:loading='isSubmitting'
)
q-tooltip Одобрить
BaseDialog(
v-model='showDialog',
title='Подтверждение одобрения',
size='sm',
@update:model-value='(v) => !v && close()'
)
Form.q-pa-sm(
:handler-submit='confirmApproval',
:is-submitting='isSubmitting',
:button-cancel-txt='"Отменить"',
:button-submit-txt='"Одобрить"',
@cancel='close'
)
div(style='max-width: 300px')
p Вы уверены, что хотите одобрить документ?
</template>
<script lang="ts" setup>
import { FailAlert, SuccessAlert } from 'src/shared/api';
import { useConfirmApproval } from '../model';
import { ref } from 'vue';
import { ModalBase } from 'src/shared/ui/ModalBase';
import { BaseDialog } from 'src/shared/ui/base/BaseDialog';
import { Form } from 'src/shared/ui/Form';
import type { IDocumentAggregate } from 'src/entities/Document/model/types';
@@ -1,41 +1,46 @@
<template lang="pug">
q-btn(
color='negative',
icon='close',
size='sm',
flat,
round,
@click='showDialog = true',
:loading='isSubmitting'
)
q-tooltip Отклонить
div
q-btn(
color='negative',
icon='close',
size='sm',
flat,
round,
@click='showDialog = true',
:loading='isSubmitting'
)
q-tooltip Отклонить
q-dialog(v-model='showDialog', @hide='close')
ModalBase(title='Отклонение одобрения')
Form.q-pa-sm(
:handler-submit='declineApproval',
:is-submitting='isSubmitting',
:button-cancel-txt='"Отменить"',
:button-submit-txt='"Отклонить"',
@cancel='close'
)
div(style='max-width: 400px')
p Вы уверены, что хотите отклонить одобрение?
q-input.q-mt-md(
v-model='reason',
label='Причина отклонения',
outlined,
type='textarea',
rows='3',
:rules='[val => !!val || "Причина обязательна"]'
)
BaseDialog(
v-model='showDialog',
title='Отклонение одобрения',
size='md',
@update:model-value='(v) => !v && close()'
)
Form.q-pa-sm(
:handler-submit='declineApproval',
:is-submitting='isSubmitting',
:button-cancel-txt='"Отменить"',
:button-submit-txt='"Отклонить"',
@cancel='close'
)
div(style='max-width: 400px')
p Вы уверены, что хотите отклонить одобрение?
q-input.q-mt-md(
v-model='reason',
label='Причина отклонения',
outlined,
type='textarea',
rows='3',
:rules='[val => !!val || "Причина обязательна"]'
)
</template>
<script lang="ts" setup>
import { FailAlert, SuccessAlert } from 'src/shared/api';
import { useDeclineApproval } from '../model';
import { ref } from 'vue';
import { ModalBase } from 'src/shared/ui/ModalBase';
import { BaseDialog } from 'src/shared/ui/base/BaseDialog';
import { Form } from 'src/shared/ui/Form';
interface Props {
@@ -106,8 +106,8 @@ export const useOnboardingFlow = () => {
const importStep: AgendaStep = {
key: 'import_participants',
type: 'import',
title: 'Импорт пайщиков',
description: 'Загрузите пайщиков через CSV-файл.',
title: 'Импорт пайщиков (опционально)',
description: 'Необязательный шаг. Если у вас уже есть пайщики кооператива — загрузите их списком через CSV-файл. Если нет — просто двигайтесь дальше.',
done: false,
pending: false,
};
@@ -11,92 +11,122 @@ OnboardingCompletionCelebration(
)
// Показываем шаги если онбординг не завершен и данные загружены
q-card(v-else flat)
q-card-section.row.items-center.justify-between
div
div.text-h5 Адаптируйте кооператив к работе на платформе
q-chip(
.onboarding(v-else)
.onboarding__head
h2.onboarding__title Адаптируйте кооператив к работе на платформе
p.onboarding__lead
| Чтобы кооператив начал работать в цифровом контуре платформы, совет
| принимает несколько решений в электронной форме по одному на каждый
| шаг ниже. Сначала утверждаются положения о цифровых сервисах и формы
| документов кооператива, затем оформляется вступление в Потребительский
| кооператив «Восход». После этого вы импортируете пайщиков и объявляете
| общее собрание. Для каждого шага платформа сформирует проект повестки и
| зафиксирует принятое решение пройдите все шаги до окончания срока
| адаптации.
.onboarding__deadline
q-icon(name="fa-regular fa-clock" size="15px")
span {{ countdownLabel ? `Срок адаптации: ${countdownLabel}` : 'Отсчёт появится после получения данных' }}
color="primary"
text-color="white"
icon="schedule"
) {{ countdownLabel || 'Отсчёт появится после получения данных' }}
q-separator
q-card-section
q-list(separator)
q-item(v-for="(step, index) in steps" :key="step.key")
q-item-section
div.row.items-center.q-gutter-sm
q-icon(
:name="getIcon(step)"
:color="getIconColor(step)"
size="22px"
q-separator.onboarding__sep
.stepper.stepper--v
.step(
v-for="(step, index) in steps"
:key="step.key"
:class="stepClass(index)"
)
.step__rail
.step__num
q-icon(v-if="step.done" name="fa-solid fa-check")
template(v-else) {{ index + 1 }}
.step__line
.step__body
.step__label {{ step.title }}
.step__sublabel {{ step.description }}
.step__meta(v-if="!step.done && step.pending")
q-icon(name="fa-solid fa-hourglass-half" size="12px")
span {{ getPendingText(step) }}
.step__action(v-if="showAction(index)")
BaseButton(
v-if="step.type === 'agenda'"
variant="primary"
size="sm"
:loading="loadingAction"
@click="openAgendaDialog(step)"
)
div.text-subtitle1 {{ index + 1 }}. {{ step.title }}
div.text-caption.text-grey-7 {{ step.description }}
q-chip(
v-if="!step.done && step.pending"
dense
color="amber"
text-color="black"
icon="hourglass_top"
class="q-mt-xs"
) {{ getPendingText(step) }}
q-item-section(side)
q-btn(
v-if="step.type === 'agenda' && showAction(index)"
:disable="loadingAction"
color="primary"
label="Объявить собрание совета"
@click="openAgendaDialog(step)"
)
template(#icon-left)
q-icon(name="fa-solid fa-bullhorn" size="14px")
| Объявить собрание совета
q-btn(
v-else-if="step.type === 'import' && showAction(index)"
:disable="loadingAction || isActionDisabled(index)"
color="primary"
icon="upload_file"
push
label="Импорт пайщиков"
@click="importDialog = true"
)
q-tooltip(
v-if="isActionDisabled(index)"
anchor="top middle"
self="bottom middle"
) Для продолжения примите решения советом
q-btn(
v-else-if="step.type === 'meet' && showAction(index)"
:disable="loadingAction || isActionDisabled(index)"
color="primary"
label="Объявить общее собрание"
@click="openMeetDialog"
)
q-tooltip(
v-if="isActionDisabled(index)"
anchor="top middle"
self="bottom middle"
) Для продолжения примите решения советом
BaseButton(
v-else-if="step.type === 'import'"
variant="primary"
size="sm"
:disabled="loadingAction || isActionDisabled(index)"
@click="importDialog = true"
)
template(#icon-left)
q-icon(name="fa-solid fa-file-arrow-up" size="14px")
| Импорт пайщиков
q-tooltip(
v-if="isActionDisabled(index)"
anchor="top middle"
self="bottom middle"
) Для продолжения примите решения советом
q-dialog(v-model="agendaDialog.open" persistent)
ModalBase(title="Предложение повестки" @close="closeAgendaDialog" style="min-width: 640px")
template(#default)
q-card-section
div.row.items-center.q-gutter-xs.text-subtitle1.text-weight-medium
q-icon(name="help_outline" size="18px" class="text-primary")
span Вопрос на повестке
div.q-mt-sm.q-pa-sm.text-body1.rounded-borders {{ agendaDialog.question }}
BaseButton(
v-else-if="step.type === 'meet'"
variant="primary"
size="sm"
:disabled="loadingAction || isActionDisabled(index)"
@click="openMeetDialog"
)
template(#icon-left)
q-icon(name="fa-solid fa-users" size="14px")
| Объявить общее собрание
q-tooltip(
v-if="isActionDisabled(index)"
anchor="top middle"
self="bottom middle"
) Для продолжения примите решения советом
q-separator.q-my-md
BaseDialog(
:model-value="agendaDialog.open"
size="lg"
:title="agendaDialog.title || 'Предложение повестки'"
:close-on-backdrop="!loadingAction"
:close-on-escape="!loadingAction"
@update:model-value="onAgendaDialogToggle"
)
.agenda-dialog
.agenda-dialog__section
.agenda-dialog__label
q-icon(name="fa-regular fa-circle-question" size="16px")
span Вопрос на повестке
.agenda-dialog__question {{ agendaDialog.question }}
div.row.items-center.q-gutter-xs.text-subtitle1.text-weight-medium
q-icon(name="gavel" size="18px" class="text-primary")
span Проект решения
div.q-mt-sm.q-pa-sm.rounded-borders
DocumentHtmlReader(:html="agendaDialog.decision" :sanitize="false")
div.q-pb-lg
q-btn(flat label="Отмена" @click="closeAgendaDialog" :disable="loadingAction")
q-btn(color="primary" label="Объявить" :loading="loadingAction" @click="submitAgenda")
.agenda-dialog__section
.agenda-dialog__label
q-icon(name="fa-solid fa-gavel" size="16px")
span Проект решения
.agenda-dialog__decision
DocumentHtmlReader(:html="agendaDialog.decision" :sanitize="false")
template(#footer)
BaseButton(
variant="ghost"
:disabled="loadingAction"
@click="closeAgendaDialog"
) Отмена
BaseButton(
variant="primary"
:loading="loadingAction"
@click="submitAgenda"
)
template(#icon-left)
q-icon(name="fa-solid fa-bullhorn" size="15px")
| Объявить
CreateMeetForm(
v-model="meetDialog"
@@ -114,8 +144,9 @@ import { onMounted, ref } from 'vue';
import { CreateMeetForm } from 'src/features/Meet/CreateMeet/ui';
import { ParticipantsImportDialog } from 'src/features/User/ImportParticipants/ui';
import { DocumentHtmlReader } from 'src/shared/ui/DocumentHtmlReader';
import { ModalBase } from 'src/shared/ui/ModalBase';
import { WindowLoader } from 'src/shared/ui/Loader';
import { BaseButton } from 'src/shared/ui/base/BaseButton';
import { BaseDialog } from 'src/shared/ui/base/BaseDialog';
import OnboardingCompletionCelebration from './OnboardingCompletionCelebration.vue';
import { useOnboardingFlow } from '../model';
@@ -138,18 +169,11 @@ const {
const importDialog = ref(false);
const getIcon = (step: { done: boolean; pending: boolean; type: string }) => {
if (step.done) return 'task_alt';
if (step.pending) return 'hourglass_top';
if (step.type === 'import') return 'arrow_forward';
return 'radio_button_unchecked';
};
const getIconColor = (step: { done: boolean; pending: boolean; type: string }) => {
if (step.done) return 'green-6';
if (step.pending) return 'orange-6';
if (step.type === 'import') return '';
return 'grey-6';
// Закрытие диалога повестки (крестик / клик по фону / Escape) сбрасывает
// его состояние через closeAgendaDialog. Открытие идёт только через
// кнопку шага (openAgendaDialog), поэтому здесь обрабатываем лишь закрытие.
const onAgendaDialogToggle = (open: boolean) => {
if (!open) closeAgendaDialog();
};
const getPendingText = (step: { type: string }) => {
@@ -191,5 +215,104 @@ const isActionDisabled = (index: number) => {
return false;
};
// Канон-степпер: пройденный шаг is-done (галочка, заливка primary),
// текущий доступный к действию is-active (подсветка номера). Ожидающие
// решения и ещё недоступные шаги нейтральные.
const stepClass = (index: number) => {
const step = steps.value[index];
if (!step) return '';
if (step.done) return 'is-done';
if (showAction(index)) return 'is-active';
return '';
};
onMounted(init);
</script>
<style scoped lang="scss">
.onboarding {
display: flex;
flex-direction: column;
}
.onboarding__head {
display: flex;
flex-direction: column;
gap: var(--p-3, 12px);
}
.onboarding__title {
margin: 0;
font-size: var(--p-fs-h2);
font-weight: 600;
letter-spacing: -0.01em;
color: var(--p-ink);
}
.onboarding__lead {
margin: 0;
font-size: var(--p-fs-body);
line-height: 1.55;
color: var(--p-ink-2);
}
// Разделитель между шапкой и степпером канон-воздух сверху/снизу.
.onboarding__sep {
margin: var(--p-5, 20px) 0;
}
// Действие шага без обрамляющей рамки, просто отступ сверху.
.step__action {
margin-top: var(--p-3, 12px);
}
// Срок адаптации спокойная плашка-предупреждение, отсчёт виден сразу.
.onboarding__deadline {
align-self: flex-start;
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
border-radius: var(--p-r-pill, 999px);
background: var(--p-warn-soft);
color: var(--p-warn);
font-size: var(--p-fs-body-sm);
font-weight: 500;
}
// Диалог повестки.
.agenda-dialog {
display: flex;
flex-direction: column;
gap: var(--p-5, 20px);
}
.agenda-dialog__section {
display: flex;
flex-direction: column;
gap: var(--p-2, 8px);
}
// Подписи секций надстрочные метки (eyebrow): отдельный типографический
// ярус ниже заголовка окна и заметно отличный от него, чтобы не казаться
// конкурирующим заголовком на фоне крупного заголовка самого документа.
.agenda-dialog__label {
display: flex;
align-items: center;
gap: 7px;
font-size: var(--p-fs-body-sm);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--p-ink-3);
.q-icon {
color: var(--p-primary);
}
}
.agenda-dialog__question {
font-size: var(--p-fs-body);
line-height: 1.5;
color: var(--p-ink);
}
.agenda-dialog__decision {
padding: var(--p-4, 16px);
background: var(--p-surface-2);
border: 1px solid var(--p-line);
border-radius: var(--p-r-sm, 8px);
max-height: 50vh;
overflow-y: auto;
}
</style>
@@ -1,43 +1,41 @@
<template lang="pug">
form.full-width(@submit.prevent='submit')
.q-gutter-md
// Провайдер платежей по умолчанию
.row.q-gutter-md
.col-md-6.col-sm-12
q-select(
v-model='formData.provider_name'
:options='providerOptions'
label='Провайдер входящих платежей'
placeholder='Выберите провайдера'
dense
standout="bg-teal text-white"
emit-value
map-options
option-value='value'
option-label='label'
:loading='loading'
)
form.pp-form(@submit.prevent='submit')
q-select.pp-form__select(
v-model='formData.provider_name'
:options='providerOptions'
label='Провайдер входящих платежей'
placeholder='Выберите провайдера'
dense
outlined
color='primary'
emit-value
map-options
option-value='value'
option-label='label'
:loading='loading'
)
.col-md-12(v-if='selectedProviderDescription')
.q-mt-sm.text-caption.text-grey-7 {{ selectedProviderDescription }}
.pp-form__hint(v-if='selectedProviderDescription') {{ selectedProviderDescription }}
.q-mt-md
.row.justify-start.q-gutter-sm
q-btn(
flat
label='Отменить'
color='grey'
@click='resetForm'
:loading='loading'
:disable='!hasChanges'
)
q-btn(
type='submit'
label='Сохранить'
color='primary'
:loading='loading'
:disable='!hasChanges'
)
.pp-form__actions
q-btn(
flat
no-caps
label='Отменить'
color='grey-7'
@click='resetForm'
:loading='loading'
:disable='!hasChanges'
)
q-btn(
type='submit'
no-caps
unelevated
label='Сохранить'
color='primary'
:loading='loading'
:disable='!hasChanges'
)
</template>
<script lang="ts" setup>
@@ -133,8 +131,24 @@ const submit = async () => {
}
</script>
<style scoped>
.full-width {
<style scoped lang="scss">
.pp-form {
display: flex;
flex-direction: column;
gap: var(--p-4, 16px);
width: 100%;
}
.pp-form__select {
max-width: 480px;
}
.pp-form__hint {
max-width: 640px;
font-size: var(--p-fs-body-sm);
line-height: 1.5;
color: var(--p-ink-2);
}
.pp-form__actions {
display: flex;
gap: var(--p-2, 8px);
}
</style>
@@ -10,7 +10,8 @@ form.full-width(@submit.prevent='submit')
label='Рабочий стол для авторизованных пользователей'
placeholder='Выберите рабочий стол'
dense
standout="bg-teal text-white"
outlined
color="primary"
emit-value
map-options
option-value='value'
@@ -26,7 +27,8 @@ form.full-width(@submit.prevent='submit')
label='Страница для авторизованных пользователей'
placeholder='Выберите страницу'
dense
standout="bg-teal text-white"
outlined
color="primary"
emit-value
map-options
option-value='value'
@@ -45,7 +47,8 @@ form.full-width(@submit.prevent='submit')
label='Рабочий стол для неавторизованных пользователей'
placeholder='Выберите рабочий стол'
dense
standout="bg-teal text-white"
outlined
color="primary"
emit-value
map-options
option-value='value'
@@ -61,7 +64,8 @@ form.full-width(@submit.prevent='submit')
label='Страница для неавторизованных пользователей'
placeholder='Выберите страницу'
dense
standout="bg-teal text-white"
outlined
color="primary"
emit-value
map-options
option-value='value'
@@ -77,7 +77,7 @@ export default async function (): Promise<IWorkspaceConfig[]> {
name: 'extensions',
component: markRaw(ExtensionsManagement),
meta: {
title: 'Магазин приложений',
title: 'Каталог приложений',
icon: 'fa-solid fa-puzzle-piece',
roles: ['chairman'],
agreements: agreementsBase,

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