Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8e263b7424 | |||
| 876d06046b | |||
| c5e23f15f9 | |||
| 996012399e | |||
| c4ffdae7ef | |||
| e60d339d2b | |||
| e7241d8a91 | |||
| 64c11820b0 | |||
| bfe5592ae9 | |||
| 9e94510981 | |||
| b432ad155d | |||
| eea5652d1f | |||
| 31da96dceb | |||
| 57cb8f4302 | |||
| f051196754 | |||
| 1c573bf131 | |||
| f35a13a382 | |||
| aced4966c8 | |||
| 978d94f6b7 | |||
| e5613405f4 | |||
| b41a42ad0a | |||
| 16ee154424 | |||
| a15432233e | |||
| ba96a650b8 | |||
| 4011628e2a | |||
| 1f9753cf8d | |||
| edd9f3ff91 | |||
| 8539b1e6fd | |||
| 477779c984 | |||
| 9491b6356d | |||
| 0a95748580 | |||
| cab6be45b3 | |||
| 59b25c2995 | |||
| adc8790c3e | |||
| 6796ed4109 | |||
| 4f9ff0c1d3 |
@@ -16,19 +16,22 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# Версия pnpm берётся из `packageManager` корневого package.json,
|
||||
# синхронно с publish-packages.yaml и 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 pnpm
|
||||
run: npm install -g pnpm
|
||||
|
||||
- name: Install Python requirements
|
||||
run: |
|
||||
python -m venv venv
|
||||
|
||||
+8
-2
@@ -35,7 +35,11 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
RUN python3 -m venv /venv \
|
||||
&& /venv/bin/pip install --no-cache-dir WeasyPrint==67
|
||||
|
||||
RUN npm install -g pnpm lerna --no-fund --no-audit
|
||||
# pnpm пинится через `packageManager` поле в корневом package.json
|
||||
# (corepack-стандарт). Без пина CI каждый раз тянет latest, а pnpm 10
|
||||
# ужесточил политику build-скриптов и валит `--frozen-lockfile` с
|
||||
# ERR_PNPM_IGNORED_BUILDS на native-пакетах (electron / esbuild / …).
|
||||
RUN corepack enable && npm install -g lerna --no-fund --no-audit
|
||||
|
||||
# Сначала манифесты (для кэш-friendly install). `.dockerignore` уже
|
||||
# выкидывает node_modules/dist/.git, поэтому `COPY .` лёгкий.
|
||||
@@ -95,7 +99,9 @@ COPY --from=builder /app /app
|
||||
|
||||
# Глобальные pnpm/lerna — нужны чтобы потребители mono-base могли
|
||||
# делать `CMD ["pnpm","-F","<pkg>","run","start"]` в production.
|
||||
RUN npm install -g pnpm lerna --no-fund --no-audit
|
||||
# pnpm активируется через corepack (версия из `packageManager` корневого
|
||||
# package.json — синхронно со builder-стадией).
|
||||
RUN corepack enable && npm install -g lerna --no-fund --no-audit
|
||||
|
||||
# Sanity-check: WeasyPrint работает и виден через PATH.
|
||||
RUN weasyprint --version
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@coopenomics/blago-cli",
|
||||
"version": "2026.4.30-alpha-1",
|
||||
"version": "2026.5.10-4",
|
||||
"description": "CLI синхронизации артефактов Благорост с бэкендом через @coopenomics/sdk",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@coopenomics/boot",
|
||||
"type": "module",
|
||||
"version": "2026.4.30-alpha-1",
|
||||
"version": "2026.5.10-4",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@9.0.6",
|
||||
"description": "CLI-утилита инициализации блокчейна и кооператива",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@coopenomics/cleos",
|
||||
"version": "2026.4.30-alpha-1",
|
||||
"version": "2026.5.10-4",
|
||||
"private": true,
|
||||
"description": "Обёртка над кошельком cleos для EOSIO блокчейна",
|
||||
"scripts": {
|
||||
|
||||
@@ -11,6 +11,12 @@
|
||||
#include "src/expsub.cpp"
|
||||
#include "src/regcoop.cpp"
|
||||
#include "src/setcoop.cpp"
|
||||
#include "src/setpricing.cpp"
|
||||
#include "src/setglobals.cpp"
|
||||
#include "src/regclient.cpp"
|
||||
#include "src/delclient.cpp"
|
||||
#include "src/extendsub.cpp"
|
||||
#include "src/setattempt.cpp"
|
||||
|
||||
/**
|
||||
* \brief Миграция контракта.
|
||||
|
||||
@@ -247,6 +247,129 @@ public:
|
||||
std::optional<eosio::public_key> signing_key,
|
||||
std::optional<bool> active);
|
||||
|
||||
// ─── pricing & globals (v2.1) ───────────────────────────────────────
|
||||
|
||||
/**
|
||||
* \brief Установить ставку плана `plan` для пакета `package_id` (FR1, D1).
|
||||
* \details Per-plan upsert в таблице `pricings` (scope = `package_id`):
|
||||
* row нет → `emplace`; row есть → `modify` с обновлённым
|
||||
* `hourly_rate` и `updated_at` = текущее время блока.
|
||||
* RAM payer — `get_self()` (`apps`-контракт; ВОСХОД как owner
|
||||
* аккаунта оплачивает RAM координационной плоскости).
|
||||
*
|
||||
* Story v2.1.1 — каркас без бизнес-валидации. Полная валидация
|
||||
* (`hourly_rate.amount > 0`, символ против `_root_govern_symbol`,
|
||||
* plan-whitelist, snapshot предыдущей ставки в audit_log_admin)
|
||||
* добавляется в Story v2.1.2.
|
||||
*
|
||||
* \param package_id scope таблицы `pricings` (Antelope `name`).
|
||||
* \param plan имя плана, PK (в MVP всегда `default`).
|
||||
* \param hourly_rate ставка за час (общая валюта экосистемы — RUB,4).
|
||||
* \note Авторизация: `require_auth(get_self())` — действие выполняется
|
||||
* контрактом от своего имени по запросу из `CA-admin` через
|
||||
* `KE write-adapter` (Story v2.1.3).
|
||||
*/
|
||||
[[eosio::action]] void setpricing(eosio::name package_id,
|
||||
eosio::name plan,
|
||||
eosio::asset hourly_rate);
|
||||
|
||||
/**
|
||||
* \brief Установить общесистемные параметры каталога (FR2, D2).
|
||||
* \details Полная замена singleton `globals` (scope = `get_self()`):
|
||||
* row нет → создаём, есть → перезаписываем все четыре поля.
|
||||
* RAM payer — `get_self()`. Story v2.1.1 — каркас без
|
||||
* валидации диапазонов; валидация и snapshot предыдущего
|
||||
* state'а добавляются в Story v2.1.2.
|
||||
*
|
||||
* \note Авторизация: `require_auth(get_self())`.
|
||||
*/
|
||||
[[eosio::action]] void setglobals(uint32_t min_payment_period_seconds,
|
||||
uint32_t free_trial_period_seconds,
|
||||
uint32_t lead_time_seconds,
|
||||
uint8_t retry_max);
|
||||
|
||||
// ─── clients (multi-tenant onboarding, v2.4) ────────────────────────
|
||||
|
||||
/**
|
||||
* \brief Зарегистрировать кооператив-клиента каталога (FR7, D3).
|
||||
* \details `clients`-row {scope=catalog_operator, PK=client_coopname}
|
||||
* с `registered_at = now`. Идемпотентность строгая: повторный
|
||||
* вызов на ту же пару → `eosio_assert("client already registered")`.
|
||||
* RAM payer — `catalog_operator` (ВОСХОД оплачивает onboarding).
|
||||
* \param catalog_operator оператор каталога, подписывает транзакцию
|
||||
* и платит RAM (в MVP всегда `voskhod`).
|
||||
* \param client_coopname кооператив, который подключается к каталогу.
|
||||
* \note Авторизация: `require_auth(catalog_operator)`.
|
||||
*/
|
||||
[[eosio::action]] void regclient(eosio::name catalog_operator,
|
||||
eosio::name client_coopname);
|
||||
|
||||
/**
|
||||
* \brief Отозвать кооператив-клиента каталога (FR8, D3).
|
||||
* \details Удаляет row по PK; row нет → `eosio_assert("client not found")`
|
||||
* (не идемпотентно сознательно — отличаем «уже отзывали» от
|
||||
* «никогда не регистрировали»). Эффекты на CA-стороне:
|
||||
* инвалидация membership-cache (D9 hybrid TTL) + добавление
|
||||
* активных JWT в `JwtRevokeList` (FR15) живут off-chain.
|
||||
* \note Авторизация: `require_auth(catalog_operator)`.
|
||||
*/
|
||||
[[eosio::action]] void delclient(eosio::name catalog_operator,
|
||||
eosio::name client_coopname);
|
||||
|
||||
// ─── subscription extend / retry-counter (D4, v2.5/v2.6) ────────────
|
||||
|
||||
/**
|
||||
* \brief Идемпотентно продлить подписку через `charge_intent_id` (FR24, D4, D5).
|
||||
* \details Находит `regsub` по `(subscriber, package_id)`; проверяет,
|
||||
* что `last_charge_intent_id != charge_intent_id`. Иначе —
|
||||
* `eosio_assert("already extended")` (это **ok-сигнал** для
|
||||
* recovery-worker'а: дубль ack'а из кабинета или повтор после
|
||||
* CA-crash). На успешном пути:
|
||||
* - `end_at += period_seconds` (extend от текущей границы,
|
||||
* не от now — important для непрерывности подписки),
|
||||
* - `last_charge_intent_id = charge_intent_id`,
|
||||
* - `attempt = 0` (счётчик retry'ев сбрасывается на удачном
|
||||
* charge'е),
|
||||
* - `updated_at = now`.
|
||||
*
|
||||
* Story v2.5.2 (TS write-port `extendByChargeIntent`) ожидает
|
||||
* именно эту семантику; `AlreadyExtendedError` маппится с
|
||||
* on-chain assert'а.
|
||||
*
|
||||
* \param catalog_operator оператор каталога (платит RAM, подписывает).
|
||||
* \param subscriber кооператив-подписчик.
|
||||
* \param package_id пакет.
|
||||
* \param period_seconds на сколько продлить (в секундах). Должен быть > 0.
|
||||
* \param charge_intent_id UUIDv5 от `(coopname, package_id, period_start_at)`.
|
||||
* \note Авторизация: `require_auth(catalog_operator)`.
|
||||
*/
|
||||
[[eosio::action]] void extendsub(eosio::name catalog_operator,
|
||||
eosio::name subscriber,
|
||||
eosio::name package_id,
|
||||
uint32_t period_seconds,
|
||||
eosio::checksum256 charge_intent_id);
|
||||
|
||||
/**
|
||||
* \brief Установить `attempt`-счётчик для подписки (D4 retry-management).
|
||||
* \details Простая запись uint8 в существующую `regsub`-row. Используется
|
||||
* pricing-watcher'ом CA, когда `ack=declined` или charge провалился
|
||||
* (типичный путь: `setattempt(coopname, package_id, current+1)`).
|
||||
* Каскад с `extendsub` не нужен — последний сам сбросит attempt
|
||||
* в 0 при успехе.
|
||||
*
|
||||
* Альтернатива (on-chain incrattempt без передачи нового значения)
|
||||
* могла бы избежать сравнения counter'ов на гонках, но усложняет
|
||||
* контракт и редко полезна: pricing-watcher tick — единственный
|
||||
* путь, и он сериализован per-(coopname, package_id) на стороне
|
||||
* watcher-state в Postgres.
|
||||
*
|
||||
* \note Авторизация: `require_auth(catalog_operator)`.
|
||||
*/
|
||||
[[eosio::action]] void setattempt(eosio::name catalog_operator,
|
||||
eosio::name subscriber,
|
||||
eosio::name package_id,
|
||||
uint8_t attempt);
|
||||
|
||||
// ─── service tables ─────────────────────────────────────────────────
|
||||
|
||||
struct [[eosio::table, eosio::contract(APPS)]] counts : counts_base {};
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* \brief Отозвать кооператив-клиента каталога (Story v2.4.1, FR8).
|
||||
* \ingroup public_apps_actions
|
||||
*
|
||||
* Что делает действие:
|
||||
* 1. Требует `catalog_operator @ active` — `require_auth(catalog_operator)`.
|
||||
* 2. Валидирует, что `client_coopname` непустое.
|
||||
* 3. Удаляет row по primary key. Если row нет → `eosio_assert("client not found")`.
|
||||
* Идемпотентность сознательно НЕ соблюдается: повторный `delclient`
|
||||
* должен вернуть явную ошибку, чтобы вышестоящие watcher'ы
|
||||
* (cache-invalidation, JWT-revocation в CA) могли отличить «уже
|
||||
* отзывали» от «никогда не было».
|
||||
*
|
||||
* **Эффекты на TS-стороне** (живут в `apps-catalog`, не на цепи):
|
||||
* - membership-cache для `client_coopname` инвалидируется через
|
||||
* hybrid-TTL (D9): критический путь забирает уже актуальный ответ
|
||||
* из chain'а, read-API получает stale-ответ ≤ 60s.
|
||||
* - JWT с `coopname=client_coopname` помещаются в `JwtRevokeList`
|
||||
* (FR15): пока не истечёт TTL токена — он отозван по jti.
|
||||
*
|
||||
* RAM payer возвращается `catalog_operator`. `subs` записи кооператива
|
||||
* НЕ удаляются автоматически — это решение оператора, и история
|
||||
* подписок остаётся в `subs` для аудита (флаг `active=false`
|
||||
* выставляется отдельным `expsub`-вызовом).
|
||||
*/
|
||||
void apps::delclient(eosio::name catalog_operator,
|
||||
eosio::name client_coopname) {
|
||||
require_auth(catalog_operator);
|
||||
|
||||
eosio::check(catalog_operator.value != 0, "catalog_operator must not be empty");
|
||||
eosio::check(client_coopname.value != 0, "client_coopname must not be empty");
|
||||
|
||||
clients_index clients(get_self(), catalog_operator.value);
|
||||
auto it = clients.find(client_coopname.value);
|
||||
eosio::check(it != clients.end(), "client not found");
|
||||
|
||||
clients.erase(it);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* \brief Идемпотентное продление подписки через `charge_intent_id`
|
||||
* (D4, D5; story v2.5/v2.6, FR24).
|
||||
* \ingroup public_apps_actions
|
||||
*
|
||||
* Что делает действие:
|
||||
* 1. Требует подпись `catalog_operator @ active`.
|
||||
* 2. Валидирует входы: `subscriber`, `package_id` непустые,
|
||||
* `period_seconds > 0`.
|
||||
* 3. Находит `regsub` по `(subscriber, package_id)` через secondary
|
||||
* index `bycooppkg`. Если нет — `eosio_assert("subscription not found")`
|
||||
* (вызов до `regsub` — программная ошибка CA-стороны, а не штатный
|
||||
* control-flow).
|
||||
* 4. Сверяет `last_charge_intent_id != charge_intent_id`. Если уже
|
||||
* этот intent был применён — `eosio_assert("already extended")`.
|
||||
* Это и есть double-emit prevention уровня цепи (D5):
|
||||
* UUIDv5-детерминированный id + on-chain unique-by-last проверка
|
||||
* закрывают двойное продление при race'ах watcher-recovery
|
||||
* или повторных webhook'ах из кабинета.
|
||||
* 5. На успешном пути модифицирует `regsub`:
|
||||
* - `end_at += period_seconds` (extend ОТ текущей границы, не от now —
|
||||
* это важно для непрерывности оплаченных интервалов; см. AR12).
|
||||
* - `last_charge_intent_id = charge_intent_id`.
|
||||
* - `attempt = 0` (счётчик retry'ев сбрасывается на удачном charge'е).
|
||||
* - `updated_at = now`.
|
||||
*
|
||||
* RAM payer — `catalog_operator` (оператор оплачивает координацию;
|
||||
* клиент-кооператив за `regsub`-row не платит).
|
||||
*/
|
||||
void apps::extendsub(eosio::name catalog_operator,
|
||||
eosio::name subscriber,
|
||||
eosio::name package_id,
|
||||
uint32_t period_seconds,
|
||||
eosio::checksum256 charge_intent_id) {
|
||||
require_auth(catalog_operator);
|
||||
|
||||
eosio::check(subscriber.value != 0, "subscriber must not be empty");
|
||||
eosio::check(package_id.value != 0, "package_id must not be empty");
|
||||
eosio::check(period_seconds > 0, "period_seconds must be positive");
|
||||
|
||||
subs_index subs(_apps, _apps.value);
|
||||
auto by_cooppkg = subs.get_index<"bycooppkg"_n>();
|
||||
uint128_t key = ((uint128_t)subscriber.value << 64) | package_id.value;
|
||||
auto it = by_cooppkg.find(key);
|
||||
eosio::check(it != by_cooppkg.end(), "subscription not found");
|
||||
|
||||
eosio::check(it->last_charge_intent_id != charge_intent_id,
|
||||
"already extended");
|
||||
|
||||
auto now = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
|
||||
by_cooppkg.modify(it, catalog_operator, [&](auto &s) {
|
||||
s.end_at = eosio::time_point_sec(s.end_at.sec_since_epoch() + period_seconds);
|
||||
s.last_charge_intent_id = charge_intent_id;
|
||||
s.attempt = 0;
|
||||
s.updated_at = now;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* \brief Зарегистрировать кооператив-клиента каталога (Story v2.4.1).
|
||||
* \ingroup public_apps_actions
|
||||
*
|
||||
* Что делает действие:
|
||||
* 1. Требует подпись `catalog_operator @ active` —
|
||||
* `require_auth(catalog_operator)`. В MVP это всегда `voskhod`,
|
||||
* но контракт не хардкодит имя — на проверке authority единственное
|
||||
* допустимое подписавшее лицо это и есть scope записи.
|
||||
* 2. Валидирует, что `client_coopname` непустое (eosio::name(0) —
|
||||
* особое значение «отсутствие», не валидное имя).
|
||||
* 3. Идемпотентно с проверкой: row нет → `emplace`. Row есть →
|
||||
* `eosio_assert("client already registered")`. Это намеренный
|
||||
* «строгий» upsert — повторный `regclient` без явного `delclient`
|
||||
* должен ловить ошибки оператора (двойной вызов из-за UI-сбоя).
|
||||
*
|
||||
* RAM payer — `catalog_operator`. ВОСХОД оплачивает RAM записи как
|
||||
* часть стоимости подключения кооператива; клиент за хранение `clients`-row
|
||||
* не платит.
|
||||
*
|
||||
* Что **не делает** (выносится в TS-сторону, Story v2.4.5):
|
||||
* - Не валидирует наличие `coops`-записи у `client_coopname`. Кооператив
|
||||
* может быть подключён к каталогу до того, как оформлен в `coops`
|
||||
* (signing_key, chain_id — это уровень subnet-операций). Каталог
|
||||
* проверит это сам перед issue'ем JWT.
|
||||
* - Не выпускает события / webhook'и. Audit-trail off-chain в
|
||||
* `audit_log_admin` через `regsub`-watcher CA.
|
||||
*/
|
||||
void apps::regclient(eosio::name catalog_operator,
|
||||
eosio::name client_coopname) {
|
||||
require_auth(catalog_operator);
|
||||
|
||||
eosio::check(catalog_operator.value != 0, "catalog_operator must not be empty");
|
||||
eosio::check(client_coopname.value != 0, "client_coopname must not be empty");
|
||||
|
||||
clients_index clients(get_self(), catalog_operator.value);
|
||||
auto it = clients.find(client_coopname.value);
|
||||
eosio::check(it == clients.end(), "client already registered");
|
||||
|
||||
auto now = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
|
||||
clients.emplace(catalog_operator, [&](auto &c) {
|
||||
c.client_coopname = client_coopname;
|
||||
c.registered_at = now;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* \brief Установить `attempt`-счётчик в `regsub` (D4 retry-management).
|
||||
* \ingroup public_apps_actions
|
||||
*
|
||||
* Pricing-watcher CA вызывает это действие, когда `ack=declined` или
|
||||
* charge не дошёл, для отметки факта повторной попытки. Контракт
|
||||
* не вычисляет `current+1` сам — watcher держит state в Postgres
|
||||
* (charge_intents.attempt) и передаёт нужное значение явно. Так проще:
|
||||
* любая ошибочная гонка фиксируется на уровне Postgres-инкремента,
|
||||
* а контракт остаётся stateless относительно retry-логики.
|
||||
*
|
||||
* - `extendsub` сам сбрасывает `attempt=0` при удачном charge'е, поэтому
|
||||
* pricing-watcher не должен дёргать `setattempt(0)` после успеха.
|
||||
* - При `attempt > globals.retry_max` watcher эмитит
|
||||
* `SubscriptionExpiredError` (story v2.6.9) и прекращает попытки;
|
||||
* контракт это знание не дублирует — `retry_max` хранится в
|
||||
* `globals` singleton и читается только off-chain.
|
||||
*/
|
||||
void apps::setattempt(eosio::name catalog_operator,
|
||||
eosio::name subscriber,
|
||||
eosio::name package_id,
|
||||
uint8_t attempt) {
|
||||
require_auth(catalog_operator);
|
||||
|
||||
eosio::check(subscriber.value != 0, "subscriber must not be empty");
|
||||
eosio::check(package_id.value != 0, "package_id must not be empty");
|
||||
|
||||
subs_index subs(_apps, _apps.value);
|
||||
auto by_cooppkg = subs.get_index<"bycooppkg"_n>();
|
||||
uint128_t key = ((uint128_t)subscriber.value << 64) | package_id.value;
|
||||
auto it = by_cooppkg.find(key);
|
||||
eosio::check(it != by_cooppkg.end(), "subscription not found");
|
||||
|
||||
auto now = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
|
||||
by_cooppkg.modify(it, catalog_operator, [&](auto &s) {
|
||||
s.attempt = attempt;
|
||||
s.updated_at = now;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* \brief Установка общесистемных параметров каталога (singleton-перезапись).
|
||||
* \ingroup public_apps_actions
|
||||
*
|
||||
* Story v2.1.1: каркас без валидации диапазонов. Что делает действие:
|
||||
* 1. Требует `apps@active` — `require_auth(get_self())`.
|
||||
* 2. Полная перезапись `globals` singleton (scope = `get_self()`,
|
||||
* PK у singleton нет): четыре поля заполняются переданными значениями.
|
||||
*
|
||||
* Что **не делает** (выносится в Story v2.1.2):
|
||||
* - Проверка диапазонов: `min_payment_period_seconds >= 60`,
|
||||
* `free_trial_period_seconds <= 31 * 86400`, `retry_max <= 8` и т.п.
|
||||
* - Snapshot предыдущего state'а в audit_log_admin — off-chain
|
||||
* (journal-less invariant).
|
||||
*
|
||||
* RAM payer — `get_self()`. ВОСХОД как owner аккаунта `apps` оплачивает
|
||||
* хранение singleton'а (одна запись, минимальный RAM).
|
||||
*/
|
||||
void apps::setglobals(uint32_t min_payment_period_seconds,
|
||||
uint32_t free_trial_period_seconds,
|
||||
uint32_t lead_time_seconds,
|
||||
uint8_t retry_max) {
|
||||
require_auth(get_self());
|
||||
|
||||
globals_singleton globals(get_self(), get_self().value);
|
||||
globals.set(globals_row{
|
||||
.min_payment_period_seconds = min_payment_period_seconds,
|
||||
.free_trial_period_seconds = free_trial_period_seconds,
|
||||
.lead_time_seconds = lead_time_seconds,
|
||||
.retry_max = retry_max,
|
||||
},
|
||||
get_self());
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* \brief Установка ставки плана для пакета (per-plan upsert) с валидацией (Story v2.1.2).
|
||||
* \ingroup public_apps_actions
|
||||
*
|
||||
* Что делает действие:
|
||||
* 1. Требует `apps@active` — `require_auth(get_self())`.
|
||||
* 2. Валидирует входы: `package_id` и `plan` непустые (FR4 — сама длина
|
||||
* ≤ 13 уже гарантирована типом `eosio::name`, но `name(0)` — особое
|
||||
* значение «отсутствие», его явно отсекаем для удобной ошибки).
|
||||
* 3. Валидирует `hourly_rate.amount > 0` (FR1) — отрицательная или
|
||||
* нулевая ставка не имеет экономического смысла и блокирует
|
||||
* корректный расчёт `charge.required` в watcher'е.
|
||||
* 4. В таблице `pricings` (scope = `package_id`) находит запись по `plan`:
|
||||
* - row нет → `emplace` с переданными `hourly_rate` и `updated_at = now`.
|
||||
* - row есть → `modify`: обновляет `hourly_rate` и `updated_at`.
|
||||
*
|
||||
* **Snapshot policy (FR2, journal-less invariant).** Действие НИКОГДА не
|
||||
* трогает таблицу `subs`: поле `regsub.end_at` существующих подписок
|
||||
* остаётся как есть — перерасчёта прошлого нет. Текущий оплаченный
|
||||
* период подписок зафиксирован в момент `regsub`/`extendsub` по тогдашней
|
||||
* ставке, и ретро-изменения тарифа на него не влияют. Snapshot для
|
||||
* audit-журнала живёт off-chain в `audit_log_admin` (журнал-less
|
||||
* инвариант — на цепи нет log-таблиц, история восстанавливается из
|
||||
* trace'ов Antelope).
|
||||
*
|
||||
* Что **намеренно не валидируется здесь:**
|
||||
* - Символ `hourly_rate.symbol` против `_root_govern_symbol` (`RUB,4`).
|
||||
* Cross-currency расчёт не предусмотрен MVP, но жёсткая привязка
|
||||
* к одному символу даст ложноотрицательные ошибки при будущих
|
||||
* multi-currency сценариях. Адаптер CA-стороны нормализует символ
|
||||
* до отправки.
|
||||
* - Whitelist имён `plan`. В MVP только `default`, но on-chain
|
||||
* хранилище должно оставаться открытым для будущих tier-планов
|
||||
* без миграции контракта.
|
||||
*
|
||||
* RAM payer — `get_self()` (`apps`-контракт). Фактически платит ВОСХОД
|
||||
* как owner аккаунта `apps`; pricing-таблицы — часть координационной
|
||||
* плоскости, оплачиваемой оператором каталога.
|
||||
*/
|
||||
void apps::setpricing(eosio::name package_id,
|
||||
eosio::name plan,
|
||||
eosio::asset hourly_rate) {
|
||||
require_auth(get_self());
|
||||
|
||||
eosio::check(package_id.value != 0, "package_id must not be empty");
|
||||
eosio::check(plan.value != 0, "plan must not be empty");
|
||||
eosio::check(hourly_rate.amount > 0, "hourly_rate must be positive");
|
||||
|
||||
pricings_index pricings(get_self(), package_id.value);
|
||||
auto it = pricings.find(plan.value);
|
||||
auto now = eosio::time_point_sec(eosio::current_time_point().sec_since_epoch());
|
||||
|
||||
if (it == pricings.end()) {
|
||||
pricings.emplace(get_self(), [&](auto &p) {
|
||||
p.plan = plan;
|
||||
p.hourly_rate = hourly_rate;
|
||||
p.updated_at = now;
|
||||
});
|
||||
} else {
|
||||
pricings.modify(it, get_self(), [&](auto &p) {
|
||||
p.hourly_rate = hourly_rate;
|
||||
p.updated_at = now;
|
||||
});
|
||||
}
|
||||
}
|
||||
+22
-2
@@ -62,7 +62,27 @@ void capital::importcontrib(eosio::name coopname, eosio::name username, checksum
|
||||
// Пополнение кошелька программы благороста
|
||||
std::string internal_memo = Capital::Memo::get_import_contributor_memo(contributor_hash, contribution_amount);
|
||||
|
||||
// Увеличиваем паевой фонд через ledger2: ISSUE BLAGOROST_FUND, Dr 51 / Cr 80.
|
||||
// Если у пайщика существует пред-импорт-учёт РИД (`w.cap.preimp`) — сжигаем
|
||||
// его полностью через `o.cap.drppre` (Dr 80 / Cr 04, BURN PREIMP_FUND) ДО
|
||||
// основного импорта. После drppre основной `o.cap.import` переоткрывает
|
||||
// учёт на полный `contribution_amount` (включая возможную доплату/списание).
|
||||
// Один process_hash на обе операции — бэкенд видит цельную цепочку IMPORT.
|
||||
{
|
||||
userwallets_index user_wallets(_ledger2, coopname.value);
|
||||
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 не поддерживается при импорте — обратиться в поддержку");
|
||||
Ledger2::apply(
|
||||
_capital, coopname, operations::capital::DROP_PREIMP,
|
||||
preimp_it->available, username, contributor_hash,
|
||||
std::string{"Закрытие пред-импорт-учёта РИД-взноса при переходе на электронный учёт"}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Увеличиваем паевой фонд через ledger2: ISSUE BLAGOROST_FUND, Dr 04 / Cr 80.
|
||||
Ledger2::apply(_capital, coopname, operations::capital::IMPORT, contribution_amount, username, contributor_hash, internal_memo);
|
||||
|
||||
|
||||
}
|
||||
@@ -232,67 +232,220 @@ inline void emplace_wallet_only(eosio::name self_name,
|
||||
|
||||
#ifndef IS_TESTNET
|
||||
/**
|
||||
* Спец-ветка миграции для voskhod — РУЧНОЙ ХАРДКОД сумм по кошелькам.
|
||||
* Прямой emplace в wallets2 с произвольным available/blocked. Используется
|
||||
* только из voskhod-спец-ветки: там L2-агрегаты нужно завести строго по
|
||||
* фактическому распределению chain (Благорост — blocked, не available).
|
||||
*/
|
||||
inline void emplace_wallet_balance(eosio::name self_name,
|
||||
eosio::name coopname,
|
||||
eosio::name wallet_id,
|
||||
const eosio::asset& available,
|
||||
const eosio::asset& blocked) {
|
||||
if (available.amount == 0 && blocked.amount == 0) return;
|
||||
|
||||
wallets2_index wallets(self_name, coopname.value);
|
||||
const auto human_view = ledger2_get_wallet_human_name(wallet_id);
|
||||
eosio::check(!human_view.empty(),
|
||||
std::string{"migrate: unknown wallet "} + wallet_id.to_string());
|
||||
|
||||
auto it = wallets.find(wallet_id.value);
|
||||
eosio::check(it == wallets.end(),
|
||||
std::string{"migrate: wallet "} + wallet_id.to_string() + " уже создан");
|
||||
wallets.emplace(self_name, [&](auto& w) {
|
||||
w.id = wallet_id;
|
||||
w.name = std::string(human_view);
|
||||
w.available = available;
|
||||
w.blocked = blocked;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Прямой emplace в accounts2 БЕЗ бух-проводок. Используется только из
|
||||
* voskhod-спец-ветки: после хардкода в кошельках замыкаем баланс прямой
|
||||
* записью в accounts2 (отдельные «миграционные» операции под Dr 04 / Dr 08
|
||||
* усложнили бы общий реестр).
|
||||
*
|
||||
* Тип счёта определяется планом (LEDGER2_ACCOUNT_MAP):
|
||||
* - ACTIVE — записываем `balance` в `debit_balance`, `credit_balance = 0`;
|
||||
* - PASSIVE — записываем `balance` в `credit_balance`, `debit_balance = 0`.
|
||||
*/
|
||||
inline void emplace_account_balance(eosio::name self_name,
|
||||
eosio::name coopname,
|
||||
uint64_t account_id,
|
||||
const eosio::asset& balance) {
|
||||
if (balance.amount == 0) return;
|
||||
const auto* meta = ledger2_find_account_meta(account_id);
|
||||
eosio::check(meta != nullptr,
|
||||
std::string{"migrate: unknown account_id "} + std::to_string(account_id));
|
||||
|
||||
accounts2_index accounts(self_name, coopname.value);
|
||||
auto it = accounts.find(account_id);
|
||||
eosio::check(it == accounts.end(),
|
||||
std::string{"migrate: account "} + std::to_string(account_id) + " уже создан");
|
||||
|
||||
const eosio::asset zero(0, balance.symbol);
|
||||
const uint8_t type_u8 = static_cast<uint8_t>(meta->type);
|
||||
|
||||
accounts.emplace(self_name, [&](auto& a) {
|
||||
a.id = account_id;
|
||||
a.name = std::string(meta->name);
|
||||
a.account_type = type_u8;
|
||||
if (meta->type == AccountType::PASSIVE) {
|
||||
a.debit_balance = zero;
|
||||
a.credit_balance = balance;
|
||||
} else { // ACTIVE / ACTIVE_PASSIVE
|
||||
a.debit_balance = balance;
|
||||
a.credit_balance = zero;
|
||||
}
|
||||
a.balance = account2::compute_balance(type_u8, a.debit_balance, a.credit_balance);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Прямой emplace L3-записи в userwallets БЕЗ бух-проводок. Только для
|
||||
* `kind == USER_SHARED`. Записи с (0,0) — пропускаются.
|
||||
*/
|
||||
inline void emplace_userwallet_only(eosio::name self_name,
|
||||
eosio::name coopname,
|
||||
eosio::name wallet_id,
|
||||
eosio::name username,
|
||||
const eosio::asset& available,
|
||||
const eosio::asset& blocked) {
|
||||
if (available.amount == 0 && blocked.amount == 0) return;
|
||||
|
||||
const WalletKind kind = ledger2_get_wallet_kind(wallet_id);
|
||||
eosio::check(kind == WalletKind::USER_SHARED,
|
||||
std::string{"migrate: userwallets допустим только для USER_SHARED, wallet="} +
|
||||
wallet_id.to_string());
|
||||
|
||||
userwallets_index user_wallets(self_name, coopname.value);
|
||||
auto idx = user_wallets.get_index<"byuserwallet"_n>();
|
||||
const auto key = combine_ids(wallet_id.value, username.value);
|
||||
auto it = idx.find(key);
|
||||
eosio::check(it == idx.end(),
|
||||
std::string{"migrate: userwallet (wallet="} + wallet_id.to_string() +
|
||||
", user=" + username.to_string() + ") уже существует");
|
||||
user_wallets.emplace(self_name, [&](auto& uw) {
|
||||
uw.id = user_wallets.available_primary_key();
|
||||
uw.wallet_name = wallet_id;
|
||||
uw.username = username;
|
||||
uw.available = available;
|
||||
uw.blocked = blocked;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Заводит L3-запись `w.cap.preimp` для пайщика, если он существует в
|
||||
* `soviet::participants` коопа. Если нет (тестнет/чужой контекст) — silently skip.
|
||||
*
|
||||
* Использовать ИСКЛЮЧИТЕЛЬНО для воскходных пред-импорт-РИД-учётов (5 пайщиков
|
||||
* по договору УХД, заведённых до перехода на электронный учёт: ЭГОЛ/Анацко/
|
||||
* Кочетков/Мильшин/Манакин). Их деньги списаны с `progwallets[pid=1]` cleos-
|
||||
* командами; здесь фиксируем их как РИД-взносы на отдельном кошельке для
|
||||
* последующего drppre при importcontr.
|
||||
*/
|
||||
inline void emplace_preimp_if_present(eosio::name self_name,
|
||||
eosio::name coopname,
|
||||
eosio::name username,
|
||||
int64_t amount_raw) {
|
||||
participants_index parts(_soviet, coopname.value);
|
||||
if (parts.find(username.value) == parts.end()) {
|
||||
return; // тестнет / иной контекст — пропускаем без падения
|
||||
}
|
||||
const eosio::asset amt(amount_raw, _root_govern_symbol);
|
||||
const eosio::asset zero(0, _root_govern_symbol);
|
||||
emplace_userwallet_only(self_name, coopname, ledger2_wallets::PREIMP_FUND, username, amt, zero);
|
||||
}
|
||||
#endif // !IS_TESTNET
|
||||
|
||||
#ifndef IS_TESTNET
|
||||
/**
|
||||
* Спец-ветка миграции для voskhod — РУЧНОЙ ХАРДКОД балансов (accounts2 +
|
||||
* wallets2 + L3 для пред-импорт-учётов).
|
||||
*
|
||||
* Включена только в prod-сборке (без IS_TESTNET). На тестнете voskhod идёт
|
||||
* по стандартному арифметическому пути наравне с остальными кооперативами,
|
||||
* чтобы любые invariant-несостыковки давали внятную ошибку, а не маскировались
|
||||
* заранее заведёнными суммами.
|
||||
* заранее заведёнными суммами. Дополнительная защита внутри ветки —
|
||||
* `participants.find()`-guard в emplace_preimp_if_present, чтобы случайный
|
||||
* запуск voskhod-веточки в чужом окружении не падал.
|
||||
*
|
||||
* Цифры заведены вручную после сверки с фактическими данными mainnet
|
||||
* (snapshot 2026-04-29). При необходимости править — править прямо здесь,
|
||||
* пересобирать ledger2.wasm и катить.
|
||||
* (snapshot 2026-05-10) и согласованы с председателем (см. project-memory
|
||||
* `voskhod migrate2 — финальный план миграции`).
|
||||
*
|
||||
* Причина хардкода: legacy::accounts и soviet::progwallets рассинхронизированы
|
||||
* (деньги Благороста, прошедшие через 51, разошлись 80 vs progwallets,
|
||||
* есть бумажный шлейф на 51, и т.п.). Вместо арифметической сшивки —
|
||||
* заводим суммы вручную по тому, как кооператив должен выглядеть в ledger2.
|
||||
* (имущ. Благорост ~57M на legacy 80 без отражения в 04, бумажный шлейф на 51,
|
||||
* непокрытые хоз.расходы на 86). Арифметической сшивки нет — заводим суммы
|
||||
* напрямую по согласованной с бухгалтером картине.
|
||||
*
|
||||
* Имущественная часть на legacy 80 (РИД ~56.8M) сюда НЕ заводится:
|
||||
* она остаётся осадком на legacy 80 до Phase 2 (ADR-009), когда Благорост
|
||||
* схлопнется в w.cap.blago и заберёт имущество.
|
||||
* ┌────────────────────────────────────── accounts2 ──────────────────────────────────────┐
|
||||
* │ 51 (BANK_ACCOUNT, А) = 176 800 — банк (хардкод 145 000) + 31 800 минП │
|
||||
* │ 04 (INTANGIBLE_ASSETS, А) = 62 353 311 — имущ. Благорост 56 903 311 + preimp 5М4 │
|
||||
* │ 08 (NON_CURRENT_INVESTMENTS) = 543 400 — балансировка (свод НМА → 80) │
|
||||
* │ 80 (SHARE_FUND, П) = 62 946 011 — 419 900 ЦК + 57 044 311 Благорост │
|
||||
* │ + 31 800 минП + 5 450 000 preimp Cr-side │
|
||||
* │ 86 (TARGET_RECEIPTS, П) = 127 500 — хоз.расходы 115К + legacy 861 12 500 │
|
||||
* │ │
|
||||
* │ Σ Dr = 176 800 + 62 353 311 + 543 400 = 63 073 511 │
|
||||
* │ Σ Cr = 62 946 011 + 127 500 = 63 073 511 ✓ │
|
||||
* └─────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
*
|
||||
* Бух-баланс по этой таблице:
|
||||
* Dr 51 = w.reg.minshr + w.wal.share + w.reg.entry = 31800 + 569900 + 12500 = 614200
|
||||
* Cr 80 = w.reg.minshr + w.wal.share = 31800 + 569900 = 601700
|
||||
* Cr 86 = w.reg.entry = 12500
|
||||
* Σ Dr = Σ Cr = 614200 ✓
|
||||
* ┌────────────────────────────── wallets2 (L2-агрегаты) ─────────────────────────────┐
|
||||
* │ w.reg.minshr available = 31 800 (34 пайщика; minimum_amount; L3 → migrator-048)│
|
||||
* │ w.wal.share available = 419 900 (5 пайщиков ЦК-остаток; L3 → migrator-049) │
|
||||
* │ w.cap.blago blocked = 57 044 311 (12 пайщиков pid=4; L3 → migrator-049) │
|
||||
* │ w.cap.preimp available = 5 450 000 (5 пайщиков; L3 — ниже, прямой emplace) │
|
||||
* │ w.sov.expns available = 127 500 (хоз.расходы из числа целевого; COOPERATIVE) │
|
||||
* └────────────────────────────────────────────────────────────────────────────────────┘
|
||||
*
|
||||
* Σ Благороста на w.cap.blago (57 044 311) и Генератора на w.cap.gen (0) идут
|
||||
* прямым emplace без бух-проводок (параллельный progwallets-учёт).
|
||||
* L3 (userwallets) для USER_SHARED-кошельков заводит migrator (Phase 1 = 048
|
||||
* для w.reg.minshr; Phase 2 = 049 для w.wal.share / w.wal.member / w.cap.blago).
|
||||
* Здесь же — только 5 пред-импорт-преимп-записей, выпавших из progwallets
|
||||
* после ручных subbal'ов (см. ~/cleos.md, 5 пайщиков с РИД-взносами по
|
||||
* договорам УХД, не успевшим попасть в электронный учёт до миграции).
|
||||
*/
|
||||
inline void migrate_voskhod_facts(eosio::name self_name, const cooperative2& coop) {
|
||||
const eosio::name coopname = coop.username;
|
||||
const eosio::symbol sym = _root_govern_symbol;
|
||||
const eosio::name coopname = coop.username;
|
||||
const eosio::symbol sym = _root_govern_symbol;
|
||||
const eosio::asset zero(0, sym);
|
||||
|
||||
// ┌────────────────────────────────────────────────────────────────────┐
|
||||
// │ ТАБЛИЦА МИГРАЦИИ ВОСХОДА — править здесь │
|
||||
// │ amount задаётся в "копейках" (× 10000), т.к. RUB = 4 знака │
|
||||
// │ 31 800.0000 RUB → 318'000'000 │
|
||||
// └────────────────────────────────────────────────────────────────────┘
|
||||
// ───────── accounts2 ─────────
|
||||
emplace_account_balance(self_name, coopname, ledger2_accounts::BANK_ACCOUNT,
|
||||
eosio::asset( 1'768'000'000LL, sym)); // 176 800.0000 RUB
|
||||
emplace_account_balance(self_name, coopname, ledger2_accounts::INTANGIBLE_ASSETS,
|
||||
eosio::asset( 623'533'110'000LL, sym)); // 62 353 311.0000 RUB
|
||||
emplace_account_balance(self_name, coopname, ledger2_accounts::NON_CURRENT_INVESTMENTS,
|
||||
eosio::asset( 5'434'000'000LL, sym)); // 543 400.0000 RUB
|
||||
emplace_account_balance(self_name, coopname, ledger2_accounts::SHARE_FUND,
|
||||
eosio::asset( 629'460'110'000LL, sym)); // 62 946 011.0000 RUB
|
||||
emplace_account_balance(self_name, coopname, ledger2_accounts::TARGET_RECEIPTS,
|
||||
eosio::asset( 1'275'000'000LL, sym)); // 127 500.0000 RUB
|
||||
|
||||
// 1. w.reg.minshr — Минимальные паевые взносы (Dr 51 / Cr 80)
|
||||
const eosio::asset W_REG_MINSHR = eosio::asset( 318'000'000LL, sym); // 31 800.0000 RUB
|
||||
// ───────── wallets2 (L2) ─────────
|
||||
emplace_wallet_balance(self_name, coopname, ledger2_wallets::MIN_SHARE_FUND,
|
||||
eosio::asset( 318'000'000LL, sym), // 31 800.0000 RUB available
|
||||
zero);
|
||||
emplace_wallet_balance(self_name, coopname, ledger2_wallets::SHARE_FUND_PAY,
|
||||
eosio::asset( 4'199'000'000LL, sym), // 419 900.0000 RUB available
|
||||
zero);
|
||||
emplace_wallet_balance(self_name, coopname, ledger2_wallets::BLAGOROST_FUND,
|
||||
zero,
|
||||
eosio::asset(570'443'110'000LL, sym)); // 57 044 311.0000 RUB blocked
|
||||
emplace_wallet_balance(self_name, coopname, ledger2_wallets::PREIMP_FUND,
|
||||
eosio::asset(54'500'000'000LL, sym), // 5 450 000.0000 RUB available
|
||||
zero);
|
||||
emplace_wallet_balance(self_name, coopname, ledger2_wallets::SOV_EXPENSES,
|
||||
eosio::asset( 1'275'000'000LL, sym), // 127 500.0000 RUB available
|
||||
zero);
|
||||
|
||||
// 2. w.wal.share — Цифровой Кошелёк, паевая часть (Dr 51 / Cr 80)
|
||||
const eosio::asset W_WAL_SHARE = eosio::asset( 5'699'000'000LL, sym); // 569 900.0000 RUB
|
||||
|
||||
// 3. w.reg.entry — Вступительные взносы (Dr 51 / Cr 86)
|
||||
const eosio::asset W_REG_ENTRY = eosio::asset( 125'000'000LL, sym); // 12 500.0000 RUB
|
||||
|
||||
// 4. w.cap.blago — ЦПП «Благорост» — единый кошелёк программы (emplace, БЕЗ проводок); ADR-009
|
||||
const eosio::asset W_CAP_BLAGO = eosio::asset(570'443'110'000LL, sym); // 57 044 311.0000 RUB
|
||||
|
||||
// 5. w.cap.gen — ЦПП «Генератор» — единый кошелёк программы (emplace, БЕЗ проводок); 0 → не создаётся
|
||||
const eosio::asset W_CAP_GEN = eosio::asset( 0LL, sym); // 0.0000 RUB
|
||||
|
||||
// ── применяем таблицу ───────────────────────────────────────────────
|
||||
send_transit(self_name, coopname, operations::migration::MIN_SHARE, W_REG_MINSHR);
|
||||
send_transit(self_name, coopname, operations::migration::SHARE, W_WAL_SHARE);
|
||||
send_transit(self_name, coopname, operations::migration::ENTRY, W_REG_ENTRY);
|
||||
|
||||
emplace_wallet_only(self_name, coopname, ledger2_wallets::BLAGOROST_FUND, W_CAP_BLAGO);
|
||||
emplace_wallet_only(self_name, coopname, ledger2_wallets::GENERATOR_FUND, W_CAP_GEN);
|
||||
// ───────── L3: преимп-учёты (5 пайщиков с РИД-взносами по договорам УХД) ─────────
|
||||
// Сумма по таблице = 5 450 000 RUB = wallets2[w.cap.preimp].available ✓
|
||||
emplace_preimp_if_present(self_name, coopname, "honruwpdxtty"_n, 500'000'000LL); // 50 000.0000 ЭГОЛ
|
||||
emplace_preimp_if_present(self_name, coopname, "zlvsujtoctal"_n, 50'000'000'000LL); // 5 000 000.0000 Анацко
|
||||
emplace_preimp_if_present(self_name, coopname, "yxkjufikzxri"_n, 1'000'000'000LL); // 100 000.0000 Кочетков
|
||||
emplace_preimp_if_present(self_name, coopname, "vvqamckynxod"_n, 1'000'000'000LL); // 100 000.0000 Мильшин
|
||||
emplace_preimp_if_present(self_name, coopname, "hntppjjknmsu"_n, 2'000'000'000LL); // 200 000.0000 Манакин
|
||||
}
|
||||
#endif // !IS_TESTNET
|
||||
|
||||
|
||||
@@ -68,11 +68,13 @@ namespace operations {
|
||||
|
||||
// capital
|
||||
namespace capital {
|
||||
inline constexpr eosio::name IMPORT = "o.cap.import"_n; ///< Оффлайн-импорт пайщика Благорост (Dr 51 / Cr 80, ISSUE BLAGOROST_FUND).
|
||||
inline constexpr eosio::name IMPORT = "o.cap.import"_n; ///< Оффлайн-импорт пайщика Благорост (Dr 04 / Cr 80, ISSUE BLAGOROST_FUND). Только РИД-имущество — деньги через INVEST.
|
||||
inline constexpr eosio::name INVEST = "o.cap.invest"_n; ///< Инвестиция в ЦПП Благорост (TRANSFER SHARE_FUND_PAY → BLAGOROST_FUND, без Dr/Cr).
|
||||
inline constexpr eosio::name COMMIT_RID = "o.cap.commit"_n; ///< Коммит РИД (Dr 08 / Cr 80, ISSUE GENERATOR_FUND).
|
||||
inline constexpr eosio::name ACCEPT_RID = "o.cap.accept"_n; ///< Приём РИД в НМА (Dr 04 / Cr 08, NONE — только бухпроводка, кошелёк остаётся на GENERATOR_FUND до конвертации сегмента).
|
||||
inline constexpr eosio::name ACCEPT_PROPERTY = "o.cap.actprp"_n; ///< Акт-2 имущественный паевой взнос (Dr 51 / Cr 80, ISSUE BLAGOROST_FUND).
|
||||
inline constexpr eosio::name ACCEPT_PROPERTY = "o.cap.actprp"_n; ///< Акт-2 имущественный паевой взнос (Dr 04 / Cr 80, ISSUE BLAGOROST_FUND).
|
||||
inline constexpr eosio::name PREIMP = "o.cap.preimp"_n; ///< Первичный учёт РИД-взноса до перехода на электронный учёт (Dr 04 / Cr 80, ISSUE PREIMP_FUND).
|
||||
inline constexpr eosio::name DROP_PREIMP = "o.cap.drppre"_n; ///< Закрытие пред-импорт-учёта при переходе на электронный учёт (Dr 80 / Cr 04, BURN PREIMP_FUND). Вызывается из capital::importcontr перед o.cap.import.
|
||||
inline constexpr eosio::name LEND = "o.cap.lend"_n; ///< Выдача беспроцентного займа пайщику (Dr 58 / Cr 51, ISSUE LOAN_ISSUED).
|
||||
inline constexpr eosio::name REPAY = "o.cap.repay"_n; ///< Возврат займа пайщика по акту-2 (Dr 80 / Cr 58, TRANSFER LOAN_ISSUED → SHARE_FUND_PAY).
|
||||
inline constexpr eosio::name WITHDRAW_FROM_CAPITAL = "o.cap.wthcap"_n; ///< Возврат паевого из ЦПП «Благорост» в кошелёк пайщика (TRANSFER BLAGOROST_FUND → SHARE_FUND_PAY, без Dr/Cr).
|
||||
@@ -187,9 +189,11 @@ static constexpr OperationRegistryEntry OPERATION_REGISTRY[] = {
|
||||
ledger2_accounts::SHARE_FUND, ledger2_accounts::BANK_ACCOUNT,
|
||||
"Возврат паевого взноса пайщику" },
|
||||
|
||||
// 5. Импорт пайщика Благорост (offline): Dr 51 / Cr 80, ISSUE BLAGOROST_FUND (ADR-009: единый кошелёк программы)
|
||||
// 5. Импорт пайщика Благорост (offline): Dr 04 / Cr 80, ISSUE BLAGOROST_FUND (ADR-009: единый кошелёк программы).
|
||||
// Импорт фиксирует РИД-имущество пайщика как НМА — поэтому Dr 04, не Dr 51.
|
||||
// Денежные взносы в Благорост идут через `o.cap.invest` (TRANSFER SHARE_FUND_PAY → BLAGOROST_FUND).
|
||||
{ operations::capital::IMPORT, processes::capital::IMPORT, WalletOp::ISSUE, eosio::name{}, ledger2_wallets::BLAGOROST_FUND,
|
||||
ledger2_accounts::BANK_ACCOUNT, ledger2_accounts::SHARE_FUND,
|
||||
ledger2_accounts::INTANGIBLE_ASSETS, ledger2_accounts::SHARE_FUND,
|
||||
"Паевой взнос по целевой потребительской программе «Благорост» (офлайн-импорт)" },
|
||||
|
||||
// 6. Инвестиция из Цифрового Кошелька в Благорост: TRANSFER SHARE_FUND_PAY → BLAGOROST_FUND (без Dr/Cr — оба счёта 80)
|
||||
@@ -212,11 +216,31 @@ static constexpr OperationRegistryEntry OPERATION_REGISTRY[] = {
|
||||
ledger2_accounts::INTANGIBLE_ASSETS, ledger2_accounts::NON_CURRENT_INVESTMENTS,
|
||||
"Приём результата интеллектуальной деятельности в паевой фонд" },
|
||||
|
||||
// 9. Акт-2 имущественный паевой взнос: Dr 51 / Cr 80, ISSUE BLAGOROST_FUND (ADR-009)
|
||||
// 9. Акт-2 имущественный паевой взнос: Dr 04 / Cr 80, ISSUE BLAGOROST_FUND (ADR-009).
|
||||
// Имущественный (РИД) — Dr 04 (НМА), не Dr 51 (банк). Денежный паевой —
|
||||
// через o.wal.depcpl или o.cap.invest.
|
||||
{ operations::capital::ACCEPT_PROPERTY, processes::capital::PROPERTY, WalletOp::ISSUE, eosio::name{}, ledger2_wallets::BLAGOROST_FUND,
|
||||
ledger2_accounts::BANK_ACCOUNT, ledger2_accounts::SHARE_FUND,
|
||||
ledger2_accounts::INTANGIBLE_ASSETS, ledger2_accounts::SHARE_FUND,
|
||||
"Паевой взнос (не денежный) по программе «Благорост»" },
|
||||
|
||||
// 9a. Первичный учёт РИД-взноса: Dr 04 / Cr 80, ISSUE PREIMP_FUND.
|
||||
// Пайщик внёс РИД-имущество ДО перехода кооператива на электронный учёт.
|
||||
// Балансы фиксируются на отдельном кошельке `w.cap.preimp`, чтобы при
|
||||
// `capital::importcontr` их можно было обнулить через `o.cap.drppre` и
|
||||
// переоткрыть на полный объём через `o.cap.import` под единый Благорост-фонд.
|
||||
{ operations::capital::PREIMP, processes::capital::PREIMP, WalletOp::ISSUE, eosio::name{}, ledger2_wallets::PREIMP_FUND,
|
||||
ledger2_accounts::INTANGIBLE_ASSETS, ledger2_accounts::SHARE_FUND,
|
||||
"Первичный учёт РИД-взноса до перехода на электронный учёт" },
|
||||
|
||||
// 9b. Закрытие пред-импорт-учёта: Dr 80 / Cr 04, BURN PREIMP_FUND.
|
||||
// Вызывается из `capital::importcontr` ДО `o.cap.import`, если у пайщика
|
||||
// есть запись в `userwallets[w.cap.preimp]`. После закрытия `o.cap.import`
|
||||
// переоткрывает учёт на полный объём (включая возможную доплату).
|
||||
{ operations::capital::DROP_PREIMP, processes::capital::IMPORT, WalletOp::BURN,
|
||||
ledger2_wallets::PREIMP_FUND, eosio::name{},
|
||||
ledger2_accounts::SHARE_FUND, ledger2_accounts::INTANGIBLE_ASSETS,
|
||||
"Закрытие пред-импорт-учёта РИД-взноса при переходе на электронный учёт" },
|
||||
|
||||
// 10. Выдача беспроцентного займа пайщику: Dr 58 / Cr 51, ISSUE LOAN_ISSUED
|
||||
{ operations::capital::LEND, processes::capital::DEBT, WalletOp::ISSUE, eosio::name{}, ledger2_wallets::LOAN_ISSUED,
|
||||
ledger2_accounts::FINANCIAL_INVESTMENTS, ledger2_accounts::BANK_ACCOUNT,
|
||||
|
||||
@@ -46,11 +46,12 @@ namespace processes {
|
||||
|
||||
// capital
|
||||
namespace capital {
|
||||
inline constexpr eosio::name IMPORT = "p.cap.import"_n; ///< Оффлайн-импорт пайщика Благорост (одноактовый).
|
||||
inline constexpr eosio::name IMPORT = "p.cap.import"_n; ///< Оффлайн-импорт пайщика Благорост (o.cap.drppre (опц.) + o.cap.import).
|
||||
inline constexpr eosio::name INVEST = "p.cap.invest"_n; ///< Инвестиция в ЦПП Благорост (wallet-only 2001→9001).
|
||||
inline constexpr eosio::name DEBT = "p.cap.debt"_n; ///< Займ пайщику (o.cap.lend + o.cap.repay).
|
||||
inline constexpr eosio::name RID = "p.cap.rid"_n; ///< Приём РИД в паевой фонд: o.cap.commit (коммиты) + o.cap.accept (акт-2) + опц. o.cap.repay + o.cap.cnvshr/o.cap.cnvbl (финальная конвертация сегмента). Анкер процесса — result_hash.
|
||||
inline constexpr eosio::name PROPERTY = "p.cap.prop"_n; ///< Приём имущественного паевого взноса (одноактовый).
|
||||
inline constexpr eosio::name PREIMP = "p.cap.preimp"_n; ///< Первичный учёт РИД-взноса до перехода на электронный учёт (одноактовый, anchor = preimp register hash).
|
||||
inline constexpr eosio::name WTHCAP = "p.cap.wthcap"_n; ///< Возврат паевого из ЦПП «Благорост» в кошелёк пайщика (одноактовый).
|
||||
}
|
||||
|
||||
|
||||
@@ -52,14 +52,16 @@ struct ledger2_wallets {
|
||||
static constexpr eosio::name MIN_SHARE_FUND = "w.reg.minshr"_n; ///< Минимальный паевой взнос пайщика (USER_SHARED, без сверки соглашений)
|
||||
static constexpr eosio::name ENTRANCE_FEES = "w.reg.entry"_n; ///< Вступительные взносы (Cr 86, COOPERATIVE)
|
||||
|
||||
// soviet — членские (инфраструктура) + делегатские
|
||||
// soviet — членские (инфраструктура) + делегатские + хоз.расходы
|
||||
static constexpr eosio::name INFRA_FEES = "w.sov.infra"_n; ///< Членские взносы за инфраструктуру кооп. платформы (COOPERATIVE)
|
||||
static constexpr eosio::name DELEGATE_FEES = "w.sov.delgte"_n; ///< Делегатские членские взносы (цель CONVERT_TO_AXN, COOPERATIVE)
|
||||
static constexpr eosio::name SOV_EXPENSES = "w.sov.expns"_n; ///< Хозяйственные расходы из числа целевого финансирования (COOPERATIVE)
|
||||
|
||||
// capital — единые программные кошельки + займы
|
||||
// capital — единые программные кошельки + займы + пред-импорт
|
||||
static constexpr eosio::name LOAN_ISSUED = "w.cap.loan"_n; ///< Выданные пайщикам беспроцентные займы (COOPERATIVE; Dr 58 / Cr 51)
|
||||
static constexpr eosio::name BLAGOROST_FUND = "w.cap.blago"_n; ///< Благорост — единый агрегированный кошелёк программы (USER_SHARED; ADR-009)
|
||||
static constexpr eosio::name GENERATOR_FUND = "w.cap.gen"_n; ///< Генератор — единый агрегированный кошелёк программы (USER_SHARED; ADR-009)
|
||||
static constexpr eosio::name PREIMP_FUND = "w.cap.preimp"_n; ///< Первичный учёт РИД-взносов до перехода на электронный учёт (USER_SHARED; o.cap.preimp / o.cap.drppre)
|
||||
|
||||
// marketplace — выплаты
|
||||
static constexpr eosio::name SUPPLIER_PAYMENTS = "w.mkt.payout"_n; ///< Выплаты поставщикам (sink RECEIVE_CONFIRM, COOPERATIVE)
|
||||
@@ -91,19 +93,21 @@ struct Ledger2WalletMeta {
|
||||
WalletKind kind;
|
||||
};
|
||||
|
||||
inline constexpr std::array<Ledger2WalletMeta, 11> LEDGER2_WALLET_REGISTRY = {{
|
||||
// USER_SHARED (5) — L3-разрез по пайщику
|
||||
inline constexpr std::array<Ledger2WalletMeta, 13> LEDGER2_WALLET_REGISTRY = {{
|
||||
// USER_SHARED (6) — L3-разрез по пайщику
|
||||
{ ledger2_wallets::MIN_SHARE_FUND, "Минимальный паевой взнос", WalletKind::USER_SHARED },
|
||||
{ ledger2_wallets::SHARE_FUND_PAY, "Паевой взнос пайщика", WalletKind::USER_SHARED },
|
||||
{ ledger2_wallets::CK_MEMBER, "ЦК — членская часть пайщика", WalletKind::USER_SHARED },
|
||||
{ ledger2_wallets::BLAGOROST_FUND, "ЦПП «Благорост» — единый кошелёк программы у пайщика", WalletKind::USER_SHARED },
|
||||
{ ledger2_wallets::GENERATOR_FUND, "ЦПП «Генератор» — единый кошелёк программы у пайщика", WalletKind::USER_SHARED },
|
||||
{ ledger2_wallets::PREIMP_FUND, "Первичный учёт РИД-взносов до перехода на электронный учёт", WalletKind::USER_SHARED },
|
||||
|
||||
// COOPERATIVE (6) — единый кооперативный баланс, без L3
|
||||
// COOPERATIVE (7) — единый кооперативный баланс, без L3
|
||||
{ ledger2_wallets::ENTRANCE_FEES, "Вступительные взносы", WalletKind::COOPERATIVE },
|
||||
{ ledger2_wallets::WITHDRAWALS_SINK, "Возвраты паевых взносов пайщикам", WalletKind::COOPERATIVE },
|
||||
{ ledger2_wallets::INFRA_FEES, "Членские взносы за инфраструктуру кооп. платформы", WalletKind::COOPERATIVE },
|
||||
{ ledger2_wallets::DELEGATE_FEES, "Делегатские членские взносы", WalletKind::COOPERATIVE },
|
||||
{ ledger2_wallets::SOV_EXPENSES, "Хозяйственные расходы из числа целевого финансирования", WalletKind::COOPERATIVE },
|
||||
{ ledger2_wallets::LOAN_ISSUED, "Выданные пайщикам беспроцентные займы", WalletKind::COOPERATIVE },
|
||||
{ ledger2_wallets::SUPPLIER_PAYMENTS, "Выплаты поставщикам", WalletKind::COOPERATIVE },
|
||||
}};
|
||||
@@ -218,12 +222,13 @@ struct Ledger2WalletProgramMapping {
|
||||
uint64_t required_program_id; // 0 = исключение (без проверки)
|
||||
};
|
||||
|
||||
inline constexpr std::array<Ledger2WalletProgramMapping, 5> LEDGER2_USER_SHARED_PROGRAM_MAPPING = {{
|
||||
inline constexpr std::array<Ledger2WalletProgramMapping, 6> LEDGER2_USER_SHARED_PROGRAM_MAPPING = {{
|
||||
{ ledger2_wallets::MIN_SHARE_FUND, 0 /* w.reg.minshr — без проверки */ },
|
||||
{ ledger2_wallets::SHARE_FUND_PAY, 1 /* ЦК */ },
|
||||
{ ledger2_wallets::CK_MEMBER, 1 /* ЦК */ },
|
||||
{ ledger2_wallets::BLAGOROST_FUND, 4 /* Благорост */ },
|
||||
{ ledger2_wallets::GENERATOR_FUND, 3 /* Генератор */ },
|
||||
{ ledger2_wallets::PREIMP_FUND, 0 /* w.cap.preimp — РИД-учёт до перехода на электронный учёт, без проверки */ },
|
||||
}};
|
||||
|
||||
/**
|
||||
|
||||
@@ -74,3 +74,6 @@
|
||||
#include "table_apps_releases.hpp"
|
||||
#include "table_apps_subs.hpp"
|
||||
#include "table_apps_coops.hpp"
|
||||
#include "table_apps_pricings.hpp"
|
||||
#include "table_apps_globals.hpp"
|
||||
#include "table_apps_clients.hpp"
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
#pragma once
|
||||
|
||||
#include <eosio/eosio.hpp>
|
||||
#include <eosio/time.hpp>
|
||||
|
||||
#include "../consts.hpp"
|
||||
|
||||
namespace Apps {
|
||||
|
||||
using namespace eosio;
|
||||
|
||||
/**
|
||||
* \brief Реестр кооперативов-клиентов каталога приложений (D3, FR7, FR8, AR3).
|
||||
*
|
||||
* Хранит факт «оператор каталога подключил кооператив `client_coopname`
|
||||
* к каталогу». Сама подписка пакета хранится в `subs`; `clients` —
|
||||
* это уровень onboarding'а, отвечающий на вопрос «может ли вообще
|
||||
* этот кооператив пользоваться каталогом и его API».
|
||||
*
|
||||
* Идентификация:
|
||||
* - scope = `catalog_operator` (`eosio::name`). В MVP всегда `voskhod`
|
||||
* (single-operator forever по решению brief'а), но scope-based
|
||||
* layout не блокирует replica-режим: другой operator → другой scope,
|
||||
* миграция данных не нужна.
|
||||
* - PK = `client_coopname.value` (один кооператив = одна запись
|
||||
* в scope; повторный `regclient` → `eosio_assert("client already registered")`).
|
||||
*
|
||||
* RAM payer — `catalog_operator` (тот, кто подписал транзакцию).
|
||||
* Это ВОСХОД в MVP — он оплачивает «координацию», а не клиенты.
|
||||
* `delclient` освобождает RAM назад оператору.
|
||||
*
|
||||
* Зачем отдельная таблица, а не флаг в общей `coops`. `coops` —
|
||||
* не «общая таблица», а сущность из системы регистратора и совет-плоскости
|
||||
* (subnet-signing-keys, chain_id подсетей). Класть туда catalog-specific
|
||||
* флаги — ломать инкапсуляцию контракта; будущая ротация владельца коопа
|
||||
* не должна зависеть от состояния каталога приложений.
|
||||
*
|
||||
* \see architecture-v2 D3 — выбор scope-per-operator vs single-table.
|
||||
* \see architecture-v2 D4 — interaction с `regsub` (membership check).
|
||||
*/
|
||||
struct [[eosio::table, eosio::contract(APPS)]] client {
|
||||
name client_coopname; ///< primary key — кооператив-клиент
|
||||
time_point_sec registered_at; ///< таймштамп `regclient`-вызова
|
||||
|
||||
uint64_t primary_key() const { return client_coopname.value; }
|
||||
};
|
||||
|
||||
typedef eosio::multi_index<"clients"_n, client> clients_index;
|
||||
|
||||
} // namespace Apps
|
||||
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include <eosio/eosio.hpp>
|
||||
#include <eosio/singleton.hpp>
|
||||
|
||||
#include "../consts.hpp"
|
||||
|
||||
namespace Apps {
|
||||
|
||||
using namespace eosio;
|
||||
|
||||
/**
|
||||
* \brief Общесистемные параметры каталога приложений (D2, FR2).
|
||||
*
|
||||
* Singleton — одна запись на контракт. Хранит четыре параметра pull-биллинга
|
||||
* и retry-каденса:
|
||||
*
|
||||
* - `min_payment_period_seconds` — минимальный шаг pull-биллинга
|
||||
* (обычно 1 час; используется при квантизации `charge.required`-периодов).
|
||||
* - `free_trial_period_seconds` — период бесплатного использования
|
||||
* после `regclient` до первого регулярного charge'а.
|
||||
* - `lead_time_seconds` — за какое время до окончания текущего
|
||||
* оплаченного интервала CA-watcher эмитит `charge.required` (D6).
|
||||
* - `retry_max` — максимум transport-retry'ев при недоступности кабинета;
|
||||
* после исчерпания CA переключается на суточный billing-retry с
|
||||
* `attempt`-инкрементом (two-level retry pattern).
|
||||
*
|
||||
* Scope = `apps`-контракт (`get_self()`); singleton без PK.
|
||||
* RAM payer — `get_self()`. Изменения только через action `setglobals`.
|
||||
*
|
||||
* \see architecture-v2 D2 — почему singleton, а не поле в общей `coops`.
|
||||
* \see Story v2.1.1 — каркас (полная замена); v2.1.2 — валидация диапазонов.
|
||||
*/
|
||||
struct [[eosio::table, eosio::contract(APPS)]] globals_row {
|
||||
uint32_t min_payment_period_seconds = 0;
|
||||
uint32_t free_trial_period_seconds = 0;
|
||||
uint32_t lead_time_seconds = 0;
|
||||
uint8_t retry_max = 0;
|
||||
};
|
||||
|
||||
typedef eosio::singleton<"globals"_n, globals_row> globals_singleton;
|
||||
|
||||
} // namespace Apps
|
||||
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
#include <eosio/asset.hpp>
|
||||
#include <eosio/eosio.hpp>
|
||||
#include <eosio/time.hpp>
|
||||
|
||||
#include "../consts.hpp"
|
||||
|
||||
namespace Apps {
|
||||
|
||||
using namespace eosio;
|
||||
|
||||
/**
|
||||
* \brief Прайс-лист пакета: `{plan -> hourly_rate}` (D1, FR1, AR5).
|
||||
*
|
||||
* Pricing-as-source on-chain: ставка плана хранится в блокчейне и читается
|
||||
* каталогом через parser2 read-adapter без локального shadow-state.
|
||||
* Изменения операторских цен через action `setpricing` атомарно
|
||||
* фиксируют `hourly_rate` и `updated_at`-снапшот для аудита.
|
||||
*
|
||||
* Идентификация:
|
||||
* - scope = `package_id` (один shard на пакет).
|
||||
* - PK = `plan.value` (в MVP всегда `default`; tier-планы добавляются
|
||||
* записями в тот же scope без миграции).
|
||||
*
|
||||
* RAM payer — `apps`-контракт (`get_self()`). Фактически оплачивается
|
||||
* ВОСХОДом как owner аккаунта `apps` (catalog-operator). Кооперативы-клиенты
|
||||
* за хранение прайс-листа не платят — это часть инварианта
|
||||
* «координацию оплачивает оператор каталога».
|
||||
*
|
||||
* \see /home/admin/apps-catalog/docs/architecture-v2-pricing-multitenancy.md
|
||||
* D1 (Layout `pricings`-таблицы — scope-based per package).
|
||||
* \see Story v2.1.1 — каркас; v2.1.2 — валидация и snapshot policy.
|
||||
*/
|
||||
struct [[eosio::table, eosio::contract(APPS)]] pricing {
|
||||
name plan; ///< primary key (Antelope name, ≤12 chars)
|
||||
asset hourly_rate; ///< ставка за час (общая валюта экосистемы)
|
||||
time_point_sec updated_at; ///< таймштамп последнего `setpricing`
|
||||
|
||||
uint64_t primary_key() const { return plan.value; }
|
||||
};
|
||||
|
||||
typedef eosio::multi_index<"pricings"_n, pricing> pricings_index;
|
||||
|
||||
} // namespace Apps
|
||||
@@ -35,6 +35,24 @@ using namespace eosio;
|
||||
* для аудита истечения подписки. Cleanup просроченных записей — задача
|
||||
* отдельного периодического действия (вне MVP).
|
||||
*
|
||||
* **D4 v2-extension** — два новых поля для координации pull-биллинга
|
||||
* (Story v2.6 / D4 / D5):
|
||||
* - `attempt` — billing-retry counter уровня `regsub`. Инкрементируется
|
||||
* pricing-watcher'ом CA через `setattempt`, когда `ack=declined`
|
||||
* или charge провалился. Сбрасывается в `0` при успешном `extendsub`.
|
||||
* - `last_charge_intent_id` — UUIDv5 последнего применённого
|
||||
* charge.intent. Используется `extendsub` для double-emit prevention:
|
||||
* повторный вызов с тем же id отсекается `eosio_assert("already extended")`.
|
||||
* Дефолт — нулевой checksum256 (т.е. ещё ни одного extend не было).
|
||||
*
|
||||
* Поля добавлены в конец struct'а как nullable-by-default — старые
|
||||
* MVP-row'ы (если бы они были) интерпретировались бы с `attempt=0` и
|
||||
* нулевым `last_charge_intent_id`. Pre-deploy guard
|
||||
* `scripts/v2-migration-guard.ts` (Story v2.7.5) блокирует прод-деплой
|
||||
* при count > 0 production-scope записей, чтобы исключить
|
||||
* deserialization-сюрпризы; в MVP/dev таблица пустая и миграция
|
||||
* безопасна.
|
||||
*
|
||||
* Indexing:
|
||||
* - PK `id` — auto-inc.
|
||||
* - `bycooppkg` — composite (coopname, package_id) для idempotent upsert
|
||||
@@ -43,6 +61,8 @@ using namespace eosio;
|
||||
* - `byexpires` — sorted by `end_at` для будущего auto-cleanup.
|
||||
*
|
||||
* \see lib/domain/table_apps_coops.hpp — chain_id ↔ кооператив.
|
||||
* \see architecture-v2 D4 — runtime-nullable migration policy.
|
||||
* \see architecture-v2 D5 — UUIDv5-based double-emit prevention.
|
||||
*/
|
||||
struct [[eosio::table, eosio::contract(APPS)]] sub {
|
||||
uint64_t id;
|
||||
@@ -55,6 +75,8 @@ struct [[eosio::table, eosio::contract(APPS)]] sub {
|
||||
time_point_sec end_at;
|
||||
time_point_sec created_at;
|
||||
time_point_sec updated_at;
|
||||
uint8_t attempt = 0; ///< D4: billing-retry counter
|
||||
checksum256 last_charge_intent_id = {}; ///< D4: UUIDv5 последнего extendsub
|
||||
|
||||
uint64_t primary_key() const { return id; }
|
||||
uint128_t by_cooppkg() const { return ((uint128_t)coopname.value << 64) | package_id.value; }
|
||||
|
||||
@@ -1,462 +0,0 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Стандарт «Запрос маркетплейса» — обмен имуществом между пайщиками через
|
||||
# кооператив с двойной конвертацией (деньги → имущество и обратно).
|
||||
#
|
||||
# Многоступенчатый процесс (10+ узлов состояний). Две ledger2-операции:
|
||||
# • o.mkt.supply — подтверждение поставки на КУ (ISSUE → w.wal.share, Дт 51 / Кт 80)
|
||||
# • o.mkt.recv — выплата поставщику после получения (TRANSFER w.wal.share → w.mkt.payout, Дт 80 / Кт 51)
|
||||
#
|
||||
# Источники правды в коде:
|
||||
# • cpp/marketplace/marketplace.hpp — actions
|
||||
# • cpp/marketplace/src/deliver_on_offer/ — реализация
|
||||
# • cpp/lib/domain/table_marketplace_requests.hpp — entity requests
|
||||
# • cpp/lib/core/ledger2/operations.hpp — o.mkt.supply, o.mkt.recv
|
||||
# • cpp/lib/core/ledger2/processes.hpp — processes::marketplace::REQUEST
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
# ── Секция 1. Паспорт ───────────────────────────────────────────────────────
|
||||
process_type: p.mkt.reqst
|
||||
id: public_marketplace_request_process
|
||||
title: Исполнение заказа на поставку имущества
|
||||
slug: request
|
||||
status: proposed
|
||||
contract: marketplace
|
||||
summary: >
|
||||
Пайщик-заказчик размещает заявку на товар, поставщик принимает её, совет
|
||||
авторизует обе стороны (взнос и возврат), товар поставляется и принимается
|
||||
с гарантийным периодом. После завершения поставщик получает выплату.
|
||||
purpose: >
|
||||
«Исполнение заказа на поставку имущества» — кооператив принимает
|
||||
имущество у одного пайщика и передаёт его другому. Контрагентом
|
||||
для обеих сторон выступает сам кооператив: совет одновременно
|
||||
авторизует приём от поставщика и выдачу заказчику, кооперативный
|
||||
участок физически принимает имущество и передаёт его заказчику,
|
||||
поставщику начисляется выплата после подтверждения получения
|
||||
и истечения гарантийного срока.
|
||||
roles:
|
||||
- contributor # заказчик (money-contributor) и поставщик (product-contributor)
|
||||
- soviet
|
||||
- chairman # представитель кооперативного участка (КУ)
|
||||
|
||||
# ── Секция 2. Действия контракта ────────────────────────────────────────────
|
||||
actions:
|
||||
- name: marketplace::orderoffer
|
||||
human: Создать заявку
|
||||
actor: contributor
|
||||
role: opener
|
||||
purpose: >
|
||||
Заказчик размещает заявку на товар: блокирует денежные средства,
|
||||
подписывает документы конвертации входящей и возврата товара.
|
||||
- name: marketplace::accept
|
||||
human: Принять заявку
|
||||
actor: contributor
|
||||
role: progress
|
||||
purpose: >
|
||||
Поставщик принимает заявку: подписывает встречную конвертацию и
|
||||
заявление о внесении имущественного паевого взноса. В совет
|
||||
отправляются две повестки.
|
||||
- name: marketplace::authcontrib
|
||||
human: Авторизовать взнос имуществом
|
||||
actor: soviet
|
||||
role: progress
|
||||
purpose: Совет авторизует имущественный взнос поставщика.
|
||||
- name: marketplace::authreturn
|
||||
human: Авторизовать возврат
|
||||
actor: soviet
|
||||
role: progress
|
||||
purpose: >
|
||||
Совет авторизует возврат имущества (если придётся). Когда обе
|
||||
авторизации получены, статус заявки → `authorized`.
|
||||
- name: marketplace::supply
|
||||
human: Поставить имущество
|
||||
actor: contributor
|
||||
role: progress
|
||||
purpose: >
|
||||
Поставщик поставляет имущество на кооперативный участок и подписывает
|
||||
акт поставки.
|
||||
- name: marketplace::supplcnf
|
||||
human: Подтвердить поставку
|
||||
actor: chairman
|
||||
role: progress
|
||||
purpose: >
|
||||
Представитель кооперативного участка подтверждает приём имущества.
|
||||
Применяется ledger2-операция o.mkt.supply — имущество поступает в
|
||||
SHARE_FUND с проводкой Дт 51 / Кт 80.
|
||||
- name: marketplace::delivered
|
||||
human: Подготовить к выдаче
|
||||
actor: chairman
|
||||
role: progress
|
||||
purpose: >
|
||||
Кооперативный участок переводит имущество в статус «готово к выдаче».
|
||||
- name: marketplace::receive
|
||||
human: Получить имущество
|
||||
actor: contributor
|
||||
role: progress
|
||||
purpose: >
|
||||
Заказчик получает имущество на КУ и подписывает первый акт получения.
|
||||
- name: marketplace::receivecnf
|
||||
human: Подтвердить получение
|
||||
actor: contributor
|
||||
role: progress
|
||||
purpose: >
|
||||
Заказчик подписывает второй акт — приём имущества подтверждён.
|
||||
Применяется ledger2-операция o.mkt.recv — выплата поставщику
|
||||
(TRANSFER w.wal.share → w.mkt.payout, Дт 80 / Кт 51), устанавливается гарантийный период.
|
||||
- name: marketplace::complete
|
||||
human: Завершить
|
||||
actor: chairman
|
||||
role: closer
|
||||
purpose: >
|
||||
После истечения гарантийного периода заявка завершается, запись удаляется.
|
||||
- name: marketplace::declineacc
|
||||
human: Отклонить советом
|
||||
actor: soviet
|
||||
role: reject
|
||||
purpose: >
|
||||
Совет отклонил приём заявки. Заблокированные средства возвращаются
|
||||
заказчику, запись удаляется.
|
||||
- name: marketplace::decline
|
||||
human: Отказ на этапе принятия
|
||||
actor: contributor
|
||||
role: reject
|
||||
purpose: >
|
||||
Заявка отменена в статусе `active` или `accepted` (до авторизации).
|
||||
Заблокированные средства возвращаются заказчику.
|
||||
- name: marketplace::cancel
|
||||
human: Отменить с комиссией
|
||||
actor: contributor
|
||||
role: reject
|
||||
purpose: >
|
||||
Отмена инициатором (заказчик/поставщик) до подтверждения поставки.
|
||||
Удерживается комиссия за отмену, остаток возвращается.
|
||||
|
||||
# ── Секция 3. Граф состояний ────────────────────────────────────────────────
|
||||
entity: marketplace::request
|
||||
entity_human: Заявка маркетплейса
|
||||
entity_source: cpp/marketplace/src/deliver_on_offer/
|
||||
|
||||
states:
|
||||
- name: active
|
||||
human: Заявка размещена
|
||||
description: >
|
||||
Заказчик создал заявку, средства заблокированы на его кошельке.
|
||||
kind: normal
|
||||
- name: accepted
|
||||
human: Поставщик принял
|
||||
description: >
|
||||
Поставщик принял заявку, в совете созданы две повестки авторизации.
|
||||
kind: normal
|
||||
- name: authorized
|
||||
human: Авторизовано советом
|
||||
description: Получены обе авторизации совета — заявка готова к поставке.
|
||||
kind: normal
|
||||
- name: supplied1
|
||||
human: Имущество поставлено
|
||||
description: Поставщик передал имущество на кооперативный участок.
|
||||
kind: normal
|
||||
- name: supplied2
|
||||
human: Поставка подтверждена
|
||||
description: >
|
||||
КУ подтвердил приём имущества. Применена o.mkt.supply — имущество
|
||||
в SHARE_FUND.
|
||||
kind: normal
|
||||
- name: delivered
|
||||
human: Готово к выдаче
|
||||
description: КУ подготовил имущество к выдаче заказчику.
|
||||
kind: normal
|
||||
- name: received1
|
||||
human: Заказчик получил
|
||||
description: Заказчик получил имущество и подписал акт-1.
|
||||
kind: normal
|
||||
- name: received2
|
||||
human: Получение подтверждено
|
||||
description: >
|
||||
Заказчик подписал акт-2. Применена o.mkt.recv — поставщик получил
|
||||
выплату. Установлен гарантийный период.
|
||||
kind: normal
|
||||
- name: completed
|
||||
human: Завершено
|
||||
description: >
|
||||
Гарантийный период истёк, заявка закрыта, запись удалена.
|
||||
kind: final
|
||||
- name: removed
|
||||
human: Отклонено / отменено
|
||||
description: >
|
||||
Заявка прервана: средства возвращены, запись удалена. Операции
|
||||
ledger2 не создавались (или возвращены).
|
||||
kind: virtual
|
||||
virtual: true
|
||||
|
||||
transitions:
|
||||
- from: "∅"
|
||||
to: active
|
||||
action: marketplace::orderoffer
|
||||
actor: contributor
|
||||
guards:
|
||||
- Заказчик активен. Достаточный остаток на SHARE_FUND_PAY.
|
||||
- Подписаны RETURN_STMT и CONVERT_FROM.
|
||||
|
||||
- from: active
|
||||
to: accepted
|
||||
action: marketplace::accept
|
||||
actor: contributor
|
||||
guards:
|
||||
- Поставщик активен.
|
||||
- Подписаны CONVERT_TO и CONTRIB_STMT.
|
||||
|
||||
- from: accepted
|
||||
to: authorized
|
||||
action: marketplace::authcontrib
|
||||
actor: soviet
|
||||
guards:
|
||||
- Подписана CONTRIB_AUTH.
|
||||
- Также получена RETURN_AUTH.
|
||||
|
||||
- from: accepted
|
||||
to: authorized
|
||||
action: marketplace::authreturn
|
||||
actor: soviet
|
||||
guards:
|
||||
- Подписана RETURN_AUTH.
|
||||
- Также получена CONTRIB_AUTH.
|
||||
|
||||
- from: authorized
|
||||
to: supplied1
|
||||
action: marketplace::supply
|
||||
actor: contributor
|
||||
guards:
|
||||
- Поставщик подписал акт поставки.
|
||||
|
||||
- from: supplied1
|
||||
to: supplied2
|
||||
action: marketplace::supplcnf
|
||||
actor: chairman
|
||||
ledger_code: o.mkt.supply
|
||||
operations:
|
||||
- o.mkt.supply
|
||||
guards:
|
||||
- Представитель КУ подтвердил приём имущества.
|
||||
|
||||
- from: supplied2
|
||||
to: delivered
|
||||
action: marketplace::delivered
|
||||
actor: chairman
|
||||
guards:
|
||||
- КУ подготовил имущество к выдаче.
|
||||
|
||||
- from: delivered
|
||||
to: received1
|
||||
action: marketplace::receive
|
||||
actor: contributor
|
||||
guards:
|
||||
- Заказчик получил имущество.
|
||||
|
||||
- from: received1
|
||||
to: received2
|
||||
action: marketplace::receivecnf
|
||||
actor: contributor
|
||||
ledger_code: o.mkt.recv
|
||||
operations:
|
||||
- o.mkt.recv
|
||||
guards:
|
||||
- Заказчик подписал акт-2 получения.
|
||||
|
||||
- from: received2
|
||||
to: completed
|
||||
action: marketplace::complete
|
||||
actor: chairman
|
||||
guards:
|
||||
- Гарантийный период истёк.
|
||||
|
||||
- from: active
|
||||
to: removed
|
||||
action: marketplace::decline
|
||||
actor: contributor
|
||||
guards:
|
||||
- Заявка отменена до принятия.
|
||||
- from: accepted
|
||||
to: removed
|
||||
action: marketplace::declineacc
|
||||
actor: soviet
|
||||
guards:
|
||||
- Совет отклонил заявку до авторизации.
|
||||
- from: accepted
|
||||
to: removed
|
||||
action: marketplace::cancel
|
||||
actor: contributor
|
||||
guards:
|
||||
- Отмена с удержанием комиссии до supplied2.
|
||||
|
||||
# ── Секция 4. Сценарий ──────────────────────────────────────────────────────
|
||||
scenario:
|
||||
steps:
|
||||
- step: 1
|
||||
title: Создание заявки
|
||||
actor: contributor
|
||||
action: marketplace::orderoffer
|
||||
description: >
|
||||
Заказчик размещает заявку на товар: блокирует средства, подписывает
|
||||
RETURN_STMT (возврат) и CONVERT_FROM (входящая конвертация).
|
||||
- step: 2
|
||||
title: Принятие поставщиком
|
||||
actor: contributor
|
||||
action: marketplace::accept
|
||||
description: >
|
||||
Поставщик подписывает CONVERT_TO и CONTRIB_STMT, в совете создаются
|
||||
две повестки.
|
||||
- step: 3
|
||||
title: Авторизация советом
|
||||
actor: soviet
|
||||
action: marketplace::authcontrib
|
||||
description: >
|
||||
Совет подписывает CONTRIB_AUTH (взнос имуществом) и RETURN_AUTH
|
||||
(возврат имущества). По обеим авторизациям статус → `authorized`.
|
||||
- step: 4
|
||||
title: Поставка имущества
|
||||
actor: contributor
|
||||
action: marketplace::supply
|
||||
description: Поставщик доставляет имущество на КУ и подписывает SUPPLY_ACT.
|
||||
- step: 5
|
||||
title: Подтверждение поставки
|
||||
actor: chairman
|
||||
action: marketplace::supplcnf
|
||||
description: >
|
||||
Представитель КУ подтверждает приём (SUPPLY_ACT_CONF). Применяется
|
||||
o.mkt.supply — имущество поступает в SHARE_FUND (Дт 51 / Кт 80).
|
||||
- step: 6
|
||||
title: Получение заказчиком
|
||||
actor: contributor
|
||||
action: marketplace::receive
|
||||
description: Заказчик получает имущество, подписывает RECEIVE_ACT.
|
||||
- step: 7
|
||||
title: Подтверждение получения
|
||||
actor: contributor
|
||||
action: marketplace::receivecnf
|
||||
description: >
|
||||
Заказчик подписывает RECEIVE_ACT_CONF. Применяется o.mkt.recv —
|
||||
выплата поставщику (TRANSFER w.wal.share → w.mkt.payout, Дт 80 / Кт 51), установлен
|
||||
гарантийный период.
|
||||
- step: 8
|
||||
title: Завершение по гарантии
|
||||
actor: chairman
|
||||
action: marketplace::complete
|
||||
description: По истечении гарантийного срока заявка закрывается.
|
||||
|
||||
alternatives:
|
||||
- branch: Отказ совета
|
||||
at_step: 3
|
||||
action: marketplace::declineacc
|
||||
actor: soviet
|
||||
description: Совет отклоняет — средства возвращаются, запись удаляется.
|
||||
- branch: Отказ заказчика до принятия
|
||||
at_step: 1
|
||||
action: marketplace::decline
|
||||
actor: contributor
|
||||
description: Отмена до accept — средства возвращаются.
|
||||
- branch: Отмена с комиссией
|
||||
at_step: 4
|
||||
action: marketplace::cancel
|
||||
actor: contributor
|
||||
description: Отмена до supplied2 — удерживается комиссия.
|
||||
|
||||
# ── Секция 5. Документы и подписи ───────────────────────────────────────────
|
||||
documents:
|
||||
- step: 1
|
||||
title: Заявление о возврате имущества (RETURN_STMT)
|
||||
template: mkt-return-stmt
|
||||
registry_id: 0
|
||||
signed_by: [contributor]
|
||||
stored_in: requests.documents
|
||||
- step: 1
|
||||
title: Конвертация входящих средств (CONVERT_FROM)
|
||||
template: mkt-convert-from
|
||||
registry_id: 0
|
||||
signed_by: [contributor]
|
||||
stored_in: requests.documents
|
||||
- step: 2
|
||||
title: Конвертация исходящих (CONVERT_TO)
|
||||
template: mkt-convert-to
|
||||
registry_id: 0
|
||||
signed_by: [contributor]
|
||||
stored_in: requests.documents
|
||||
- step: 2
|
||||
title: Заявление об имущественном паевом взносе поставщика (CONTRIB_STMT)
|
||||
template: mkt-contrib-stmt
|
||||
registry_id: 0
|
||||
signed_by: [contributor]
|
||||
stored_in: requests.documents
|
||||
- step: 3
|
||||
title: Авторизация совета на имущественный взнос (CONTRIB_AUTH)
|
||||
template: mkt-contrib-auth
|
||||
registry_id: 0
|
||||
signed_by: [soviet]
|
||||
stored_in: requests.documents
|
||||
- step: 3
|
||||
title: Авторизация совета на возврат имущества (RETURN_AUTH)
|
||||
template: mkt-return-auth
|
||||
registry_id: 0
|
||||
signed_by: [soviet]
|
||||
stored_in: requests.documents
|
||||
- step: 4
|
||||
title: Акт поставки (SUPPLY_ACT)
|
||||
template: mkt-supply-act
|
||||
registry_id: 0
|
||||
signed_by: [contributor]
|
||||
stored_in: requests.documents
|
||||
- step: 5
|
||||
title: Акт подтверждения поставки (SUPPLY_ACT_CONF)
|
||||
template: mkt-supply-act-conf
|
||||
registry_id: 0
|
||||
signed_by: [chairman]
|
||||
stored_in: requests.documents
|
||||
- step: 6
|
||||
title: Акт получения (RECEIVE_ACT)
|
||||
template: mkt-receive-act
|
||||
registry_id: 0
|
||||
signed_by: [contributor]
|
||||
stored_in: requests.documents
|
||||
- step: 7
|
||||
title: Акт подтверждения получения (RECEIVE_ACT_CONF)
|
||||
template: mkt-receive-act-conf
|
||||
registry_id: 0
|
||||
signed_by: [contributor]
|
||||
stored_in: requests.documents
|
||||
|
||||
# ── Секция 6. Операции ──────────────────────────────────────────────────────
|
||||
operations:
|
||||
- ledger_code: o.mkt.supply
|
||||
human_name: Подтверждение поставки
|
||||
wallet_op: ISSUE
|
||||
wallet_from: ''
|
||||
wallet_to: w.wal.share # ЦПП «Цифровой Кошелёк» — паевые взносы деньгами
|
||||
debit: 51 # Расчётный счёт
|
||||
credit: 80 # Паевой фонд (складочный капитал)
|
||||
amount_ref: request.total_cost
|
||||
triggered_by: marketplace::supplcnf
|
||||
description: >
|
||||
Имущество поступило на кооперативный участок. В ledger2 фиксируется
|
||||
приход в SHARE_FUND поставщика — основание для последующей выплаты
|
||||
из паевого фонда.
|
||||
|
||||
- ledger_code: o.mkt.recv
|
||||
human_name: Подтверждение получения — выплата поставщику
|
||||
wallet_op: TRANSFER
|
||||
wallet_from: w.wal.share # ЦПП «Цифровой Кошелёк» — паевые взносы деньгами
|
||||
wallet_to: w.mkt.payout # Выплаты поставщикам
|
||||
debit: 80 # Паевой фонд
|
||||
credit: 51 # Расчётный счёт
|
||||
amount_ref: request.base_cost
|
||||
triggered_by: marketplace::receivecnf
|
||||
description: >
|
||||
После подтверждения получения заказчиком средства уходят с SHARE_FUND
|
||||
поставщика на кошелёк выплат поставщикам — фактическая денежная
|
||||
выплата за поставленное имущество.
|
||||
|
||||
# ── Секция 7. Связи ─────────────────────────────────────────────────────────
|
||||
related:
|
||||
- process_type: p.wal.depo
|
||||
id: public_wallet_deposit_process
|
||||
relation: provides
|
||||
note: >
|
||||
Заказчик и поставщик должны иметь остаток на SHARE_FUND_PAY — его
|
||||
создаёт «Внесение паевого взноса» (p.wal.depo).
|
||||
@@ -49,6 +49,7 @@
|
||||
#include "src/fund/fundwithdraw.cpp"
|
||||
|
||||
#include "src/participant/addparticipant.cpp"
|
||||
#include "src/participant/setminamt.cpp"
|
||||
#include "src/participant/block.cpp"
|
||||
#include "src/participant/cancelreg.cpp"
|
||||
#include "src/participant/unblock.cpp"
|
||||
|
||||
@@ -139,6 +139,9 @@ public:
|
||||
//regaccount.cpp
|
||||
[[eosio::action]] void addpartcpnt(eosio::name coopname, eosio::name username, eosio::name braname, eosio::name type, eosio::time_point_sec created_at, eosio::asset initial, eosio::asset minimum, bool spread_initial);
|
||||
|
||||
//setminamt.cpp
|
||||
[[eosio::action]] void setminamt(eosio::name coopname, eosio::name username, eosio::asset minimum);
|
||||
|
||||
//automator.cpp
|
||||
[[eosio::action]] void automate(eosio::name coopname, uint64_t board_id, eosio::name member, eosio::name action_type, eosio::name provider, std::string encrypted_private_key);
|
||||
[[eosio::action]] void disautomate(eosio::name coopname, uint64_t board_id, eosio::name member, uint64_t automation_id );
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* @brief Корректировка minimum_amount у уже принятого пайщика.
|
||||
* Технический фикс для случаев, когда поле рассинхронизировано с кооп-параметром
|
||||
* (например, у voskhod::ant исторически осталось 1 RUB вместо 300 RUB).
|
||||
* @param coopname Наименование кооператива
|
||||
* @param username Наименование пайщика
|
||||
* @param minimum Новая сумма minimum_amount
|
||||
* @ingroup public_actions
|
||||
* @ingroup public_soviet_actions
|
||||
|
||||
* @note Авторизация требуется от аккаунта @p coopname
|
||||
*/
|
||||
void soviet::setminamt(eosio::name coopname, eosio::name username, eosio::asset minimum) {
|
||||
require_auth(coopname);
|
||||
|
||||
auto cooperative = get_cooperative_or_fail(coopname);
|
||||
cooperative.check_symbol_or_fail(minimum);
|
||||
eosio::check(minimum.amount > 0, "minimum_amount должен быть положительным");
|
||||
|
||||
participants_index participants(_soviet, coopname.value);
|
||||
auto p = participants.find(username.value);
|
||||
eosio::check(p != participants.end(), "Пайщик не найден");
|
||||
|
||||
participants.modify(p, coopname, [&](auto &m){
|
||||
m.minimum_amount = minimum;
|
||||
});
|
||||
}
|
||||
@@ -17,10 +17,6 @@ void soviet::converttoaxn(eosio::name coopname, eosio::asset amount, document2 s
|
||||
|
||||
eosio::asset axon_quantity(axon_amount, _root_symbol);
|
||||
|
||||
// Списываем RUB с кошелька provider (_wallet_program)
|
||||
Wallet::sub_available_funds(_soviet, _provider, coopname, amount, _wallet_program, "Конвертация RUB в AXON");
|
||||
|
||||
|
||||
// Пополняем кошелёк членских взносов в ledger
|
||||
std::string memo = "Членский взнос из числа средств паевого взноса по соглашению о подключении к платформе Кооперативной Экономики от пайщика с username=" + coopname.to_string();
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ void soviet::addbal(eosio::name coopname, eosio::name username, uint64_t program
|
||||
b.coopname = coopname;
|
||||
b.username = username;
|
||||
b.agreement_id = 0;
|
||||
b.available = asset(0, quantity.symbol) + quantity;
|
||||
b.available = quantity;
|
||||
b.blocked = asset(0, quantity.symbol);
|
||||
b.membership_contribution = asset(0, quantity.symbol);
|
||||
});
|
||||
@@ -54,6 +54,4 @@ void soviet::addbal(eosio::name coopname, eosio::name username, uint64_t program
|
||||
p.available = p.available.value_or(asset(0, quantity.symbol)) + quantity;
|
||||
p.share_contributions = p.share_contributions.value_or(asset(0, quantity.symbol)) + quantity;
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@coopenomics/contracts",
|
||||
"version": "2026.4.30-alpha-1",
|
||||
"version": "2026.5.10-4",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "create-nodejs-express-app",
|
||||
"version": "2026.4.30-alpha-1",
|
||||
"version": "2026.5.10-4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "create-nodejs-express-app",
|
||||
"version": "2026.4.30-alpha-1",
|
||||
"version": "2026.5.10-4",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@a2seven/yoo-checkout": "^1.1.4",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@coopenomics/controller",
|
||||
"version": "2026.4.30-alpha-1",
|
||||
"version": "2026.5.10-4",
|
||||
"description": "Бэкенд GraphQL API кооператива на NestJS",
|
||||
"private": true,
|
||||
"bin": "bin/createNodejsApp.js",
|
||||
|
||||
@@ -97,6 +97,13 @@ export const PROCESS_HASH_LOCATOR: Readonly<Record<string, HashLocation[]>> = Ob
|
||||
// capital::pgproperties.property_hash — приём имущества в паевой фонд.
|
||||
'p.cap.prop': [{ code: 'capital', table: 'pgproperties', field: 'property_hash' }],
|
||||
|
||||
// p.cap.preimp — первичный учёт РИД-взноса до перехода кооператива на
|
||||
// электронный учёт (`o.cap.preimp`, ISSUE w.cap.preimp). Сущностной таблицы
|
||||
// под этот учёт пока нет — пред-импорт-записи живут только в
|
||||
// `userwallets[w.cap.preimp]`. Process_hash берётся из blockchain_actions
|
||||
// (как у одноактовых процессов).
|
||||
'p.cap.preimp': [],
|
||||
|
||||
'p.wal.depo': [{ code: 'wallet', table: 'deposits', field: 'deposit_hash' }],
|
||||
'p.wal.wthdrw': [{ code: 'wallet', table: 'withdraws', field: 'withdraw_hash' }],
|
||||
|
||||
|
||||
+81
-97
@@ -31,14 +31,13 @@ export class IssuePermissionsService {
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Определяет роль пользователя для конкретной задачи
|
||||
* @param username - имя пользователя
|
||||
* @param coopname - имя кооператива
|
||||
* @param projectHash - хеш проекта
|
||||
* @param issueSubmaster - ответственный задачи
|
||||
* @param issueCreators - создатели задачи
|
||||
* @param userRole - роль пользователя в системе (chairman, member, etc.)
|
||||
* @returns роль пользователя
|
||||
* Определяет НАБОР ролей пользователя для конкретной задачи.
|
||||
*
|
||||
* Пользователь может одновременно нести несколько ролей (например, член совета
|
||||
* + соавтор + исполнитель), и итоговые права на задачу — UNION разрешений по
|
||||
* всем его ролям. См. {@link IssueAccessPolicyService.hasPermission}.
|
||||
*
|
||||
* @returns множество ролей; минимум {@link UserRole.GUEST} / {@link UserRole.CONTRIBUTOR}.
|
||||
*/
|
||||
async getUserRoleForIssue(
|
||||
username: string | undefined,
|
||||
@@ -47,74 +46,64 @@ export class IssuePermissionsService {
|
||||
issueSubmaster?: string,
|
||||
issueCreators?: string[],
|
||||
userRole?: string
|
||||
): Promise<UserRole> {
|
||||
// Гости не имеют доступа
|
||||
): Promise<Set<UserRole>> {
|
||||
const roles = new Set<UserRole>();
|
||||
|
||||
if (!username) {
|
||||
return UserRole.GUEST;
|
||||
roles.add(UserRole.GUEST);
|
||||
return roles;
|
||||
}
|
||||
|
||||
// Сначала проверяем специфические роли проекта (они имеют приоритет над системными ролями)
|
||||
|
||||
// Проверяем, является ли пользователь мастером проекта
|
||||
// Project-specific роли — собираем все, что верны.
|
||||
const isMaster = await this.isProjectMaster(username, coopname, projectHash);
|
||||
if (isMaster) {
|
||||
return UserRole.MASTER;
|
||||
roles.add(UserRole.MASTER);
|
||||
}
|
||||
|
||||
// Проверяем, является ли пользователь ответственным
|
||||
if (issueSubmaster === username) {
|
||||
return UserRole.SUBMASTER;
|
||||
roles.add(UserRole.SUBMASTER);
|
||||
}
|
||||
|
||||
// Проверяем, является ли пользователь исполнителем (соисполнителем)
|
||||
if (issueCreators && issueCreators.includes(username)) {
|
||||
return UserRole.CREATOR;
|
||||
roles.add(UserRole.CREATOR);
|
||||
}
|
||||
|
||||
// Проверяем, является ли пользователь автором проекта
|
||||
const segment = await this.segmentRepository.findOne({
|
||||
username,
|
||||
project_hash: projectHash,
|
||||
coopname,
|
||||
});
|
||||
if (segment?.is_author) {
|
||||
return UserRole.AUTHOR;
|
||||
roles.add(UserRole.AUTHOR);
|
||||
}
|
||||
|
||||
// Проверяем, является ли пользователь участником проекта
|
||||
const contributor = await this.contributorRepository.findByUsernameAndCoopname(username, coopname);
|
||||
if (contributor && contributor.appendixes.includes(projectHash)) {
|
||||
return UserRole.CONTRIBUTOR;
|
||||
roles.add(UserRole.CONTRIBUTOR);
|
||||
}
|
||||
|
||||
// Теперь проверяем системные роли (только если нет специфических ролей проекта)
|
||||
// Системные роли совета — добавляем независимо от project-specific ролей.
|
||||
if (userRole === 'chairman' || userRole === 'member') {
|
||||
return UserRole.BOARD_MEMBER;
|
||||
roles.add(UserRole.BOARD_MEMBER);
|
||||
}
|
||||
|
||||
// По умолчанию - участник проекта (если есть доступ)
|
||||
return UserRole.CONTRIBUTOR;
|
||||
// Минимальный фолбэк, если ничего из вышеперечисленного не сработало,
|
||||
// но username известен — оставляем семантику «участник проекта».
|
||||
if (roles.size === 0) {
|
||||
roles.add(UserRole.CONTRIBUTOR);
|
||||
}
|
||||
|
||||
return roles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, есть ли у пользователя разрешение на действие
|
||||
* @param userRole - роль пользователя
|
||||
* @param action - действие
|
||||
* @returns true если действие разрешено
|
||||
*/
|
||||
hasPermission(userRole: UserRole, action: IssueAction): boolean {
|
||||
return this.issueAccessPolicyService.hasPermission(userRole, action);
|
||||
/** UNION-проверка прав на действие над задачей по набору ролей. */
|
||||
hasPermission(roles: Iterable<UserRole>, action: IssueAction): boolean {
|
||||
return this.issueAccessPolicyService.hasPermission(roles, action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет разрешение на переход между статусами
|
||||
* @param userRole - роль пользователя
|
||||
* @param currentStatus - текущий статус
|
||||
* @param newStatus - новый статус
|
||||
* @returns true если переход разрешен
|
||||
*/
|
||||
canTransitionStatus(userRole: UserRole, currentStatus: IssueStatus, newStatus: IssueStatus): boolean {
|
||||
return this.issueAccessPolicyService.canTransitionStatus(userRole, currentStatus, newStatus);
|
||||
/** UNION-проверка перехода статусов по набору ролей. */
|
||||
canTransitionStatus(roles: Iterable<UserRole>, currentStatus: IssueStatus, newStatus: IssueStatus): boolean {
|
||||
return this.issueAccessPolicyService.canTransitionStatus(roles, currentStatus, newStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -173,53 +162,58 @@ export class IssuePermissionsService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Определяет роль пользователя для проекта
|
||||
* @param username - имя пользователя
|
||||
* @param project - проект
|
||||
* @param userRole - системная роль пользователя (chairman, member, etc.)
|
||||
* @returns роль пользователя для проекта
|
||||
* Определяет НАБОР project-ролей пользователя.
|
||||
*
|
||||
* UNION-семантика по ролям. Симметрично с
|
||||
* {@link ProjectPermissionsService.getProjectUserRole}.
|
||||
*/
|
||||
async getProjectUserRole(
|
||||
username: string | undefined,
|
||||
project: any, // ProjectDomainEntity
|
||||
userRole?: string
|
||||
): Promise<ProjectUserRole> {
|
||||
// Гости не имеют доступа
|
||||
): Promise<Set<ProjectUserRole>> {
|
||||
const roles = new Set<ProjectUserRole>();
|
||||
|
||||
if (!username) {
|
||||
return ProjectUserRole.GUEST;
|
||||
roles.add(ProjectUserRole.GUEST);
|
||||
return roles;
|
||||
}
|
||||
|
||||
// Члены совета имеют полные права
|
||||
if (userRole === 'chairman' || userRole === 'member') {
|
||||
return userRole === 'chairman' ? ProjectUserRole.CHAIRMAN : ProjectUserRole.BOARD_MEMBER;
|
||||
if (userRole === 'chairman') {
|
||||
roles.add(ProjectUserRole.CHAIRMAN);
|
||||
} else if (userRole === 'member') {
|
||||
roles.add(ProjectUserRole.BOARD_MEMBER);
|
||||
}
|
||||
|
||||
// Проверяем, является ли пользователь мастером проекта
|
||||
const isMaster = await this.isProjectMaster(username, project.coopname, project.project_hash);
|
||||
if (isMaster) {
|
||||
return ProjectUserRole.MASTER;
|
||||
roles.add(ProjectUserRole.MASTER);
|
||||
}
|
||||
|
||||
const segment = await this.segmentRepository.findOne({
|
||||
username,
|
||||
project_hash: project.project_hash,
|
||||
coopname: project.coopname,
|
||||
});
|
||||
if (segment?.is_author) {
|
||||
roles.add(ProjectUserRole.AUTHOR);
|
||||
}
|
||||
|
||||
// Проверяем, является ли пользователь участником проекта
|
||||
const contributor = await this.contributorRepository.findByUsernameAndCoopname(username, project.coopname);
|
||||
const isContributor = contributor && contributor.appendixes.includes(project.project_hash);
|
||||
|
||||
if (isContributor) {
|
||||
return ProjectUserRole.CONTRIBUTOR;
|
||||
if (contributor && contributor.appendixes.includes(project.project_hash)) {
|
||||
roles.add(ProjectUserRole.CONTRIBUTOR);
|
||||
}
|
||||
|
||||
// По умолчанию - участник (если есть доступ к проекту)
|
||||
return ProjectUserRole.CONTRIBUTOR;
|
||||
if (roles.size === 0) {
|
||||
roles.add(ProjectUserRole.CONTRIBUTOR);
|
||||
}
|
||||
|
||||
return roles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, есть ли у пользователя разрешение на действие над проектом
|
||||
* @param userRole - роль пользователя для проекта
|
||||
* @param action - действие над проектом
|
||||
* @returns true если действие разрешено
|
||||
*/
|
||||
hasProjectPermission(userRole: ProjectUserRole, action: ProjectAction): boolean {
|
||||
return this.issueAccessPolicyService.hasProjectPermission(userRole, action);
|
||||
/** UNION-проверка прав на действие над проектом по набору ролей. */
|
||||
hasProjectPermission(roles: Iterable<ProjectUserRole>, action: ProjectAction): boolean {
|
||||
return this.issueAccessPolicyService.hasProjectPermission(roles, action);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -243,25 +237,22 @@ export class IssuePermissionsService {
|
||||
currentStatus: IssueStatus,
|
||||
userRole?: string
|
||||
): Promise<void> {
|
||||
// Определяем роль пользователя
|
||||
const role = await this.getUserRoleForIssue(username, coopname, projectHash, issueSubmaster, issueCreators, userRole);
|
||||
// Определяем набор ролей пользователя
|
||||
const roles = await this.getUserRoleForIssue(username, coopname, projectHash, issueSubmaster, issueCreators, userRole);
|
||||
|
||||
// Проверяем разрешение на изменение статуса
|
||||
if (!this.hasPermission(role, IssueAction.CHANGE_STATUS)) {
|
||||
if (!this.hasPermission(roles, IssueAction.CHANGE_STATUS)) {
|
||||
throw new Error(`У вас нет прав на изменение статуса задачи`);
|
||||
}
|
||||
|
||||
// Проверяем разрешение на переход между статусами
|
||||
if (!this.canTransitionStatus(role, currentStatus, newStatus)) {
|
||||
if (!this.canTransitionStatus(roles, currentStatus, newStatus)) {
|
||||
throw new Error(`Переход из статуса "${currentStatus}" в "${newStatus}" запрещен для вашей роли`);
|
||||
}
|
||||
|
||||
// Дополнительные проверки для специальных статусов
|
||||
if (newStatus === IssueStatus.DONE && !this.hasPermission(role, IssueAction.SET_DONE)) {
|
||||
if (newStatus === IssueStatus.DONE && !this.hasPermission(roles, IssueAction.SET_DONE)) {
|
||||
throw new Error('Только мастер проекта может устанавливать статус "Выполнена"');
|
||||
}
|
||||
|
||||
if (newStatus === IssueStatus.ON_REVIEW && !this.hasPermission(role, IssueAction.SET_ON_REVIEW)) {
|
||||
if (newStatus === IssueStatus.ON_REVIEW && !this.hasPermission(roles, IssueAction.SET_ON_REVIEW)) {
|
||||
throw new Error('Только ответственный исполнитель может устанавливать статус "На проверке"');
|
||||
}
|
||||
}
|
||||
@@ -284,11 +275,10 @@ export class IssuePermissionsService {
|
||||
issueCreators: string[] | undefined,
|
||||
userRole?: string
|
||||
): Promise<void> {
|
||||
// Определяем роль пользователя
|
||||
const role = await this.getUserRoleForIssue(username, coopname, projectHash, issueSubmaster, issueCreators, userRole);
|
||||
// Определяем набор ролей пользователя
|
||||
const roles = await this.getUserRoleForIssue(username, coopname, projectHash, issueSubmaster, issueCreators, userRole);
|
||||
|
||||
// Проверяем разрешение на установку оценки
|
||||
if (!this.hasPermission(role, IssueAction.SET_ESTIMATE)) {
|
||||
if (!this.hasPermission(roles, IssueAction.SET_ESTIMATE)) {
|
||||
throw new Error('Только мастер проекта может устанавливать оценку на задачи');
|
||||
}
|
||||
}
|
||||
@@ -311,22 +301,16 @@ export class IssuePermissionsService {
|
||||
issueCreators: string[] | undefined,
|
||||
userRole?: string
|
||||
): Promise<void> {
|
||||
// Определяем роль пользователя
|
||||
const role = await this.getUserRoleForIssue(username, coopname, projectHash, issueSubmaster, issueCreators, userRole);
|
||||
// Определяем набор ролей пользователя
|
||||
const roles = await this.getUserRoleForIssue(username, coopname, projectHash, issueSubmaster, issueCreators, userRole);
|
||||
|
||||
// Проверяем разрешение на установку приоритета
|
||||
if (!this.hasPermission(role, IssueAction.SET_PRIORITY)) {
|
||||
if (!this.hasPermission(roles, IssueAction.SET_PRIORITY)) {
|
||||
throw new Error('Только мастер проекта может устанавливать приоритет на задачи');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает список допустимых статусов для перехода из текущего статуса для данной роли
|
||||
* @param userRole - роль пользователя
|
||||
* @param currentStatus - текущий статус задачи
|
||||
* @returns массив допустимых статусов для перехода
|
||||
*/
|
||||
getAllowedStatusTransitions(userRole: UserRole, currentStatus: IssueStatus): IssueStatus[] {
|
||||
return this.issueAccessPolicyService.getAllowedStatusTransitions(userRole, currentStatus);
|
||||
/** UNION-список допустимых переходов статуса по набору ролей. */
|
||||
getAllowedStatusTransitions(roles: Iterable<UserRole>, currentStatus: IssueStatus): IssueStatus[] {
|
||||
return this.issueAccessPolicyService.getAllowedStatusTransitions(roles, currentStatus);
|
||||
}
|
||||
}
|
||||
|
||||
+29
-29
@@ -138,8 +138,8 @@ export class PermissionsService {
|
||||
|
||||
const username = currentUser.username;
|
||||
|
||||
// Определяем роль пользователя для этой задачи
|
||||
const userRole = await this.issuePermissionsService.getUserRoleForIssue(
|
||||
// Определяем НАБОР ролей пользователя для этой задачи (UNION-семантика).
|
||||
const roles = await this.issuePermissionsService.getUserRoleForIssue(
|
||||
username,
|
||||
issue.coopname,
|
||||
issue.project_hash,
|
||||
@@ -152,18 +152,18 @@ export class PermissionsService {
|
||||
const has_clearance = await this.isProjectContributor(username, issue.coopname, issue.project_hash);
|
||||
|
||||
// Рассчитываем права на основе матрицы доступа
|
||||
const can_edit_issue = this.issuePermissionsService.hasPermission(userRole, IssueAction.EDIT_ISSUE);
|
||||
const can_change_status = this.issuePermissionsService.hasPermission(userRole, IssueAction.CHANGE_STATUS);
|
||||
const can_assign_creator = this.issuePermissionsService.hasPermission(userRole, IssueAction.ASSIGN_CREATOR);
|
||||
const can_set_done = this.issuePermissionsService.hasPermission(userRole, IssueAction.SET_DONE);
|
||||
const can_set_on_review = this.issuePermissionsService.hasPermission(userRole, IssueAction.SET_ON_REVIEW);
|
||||
const can_set_estimate = this.issuePermissionsService.hasPermission(userRole, IssueAction.SET_ESTIMATE);
|
||||
const can_set_priority = this.issuePermissionsService.hasPermission(userRole, IssueAction.SET_PRIORITY);
|
||||
const can_delete_issue = this.issuePermissionsService.hasPermission(userRole, IssueAction.DELETE_ISSUE);
|
||||
const can_create_requirement = this.issuePermissionsService.hasPermission(userRole, IssueAction.CREATE_REQUIREMENT);
|
||||
const can_edit_requirement = this.issuePermissionsService.hasPermission(userRole, IssueAction.EDIT_REQUIREMENT);
|
||||
const can_delete_requirement = this.issuePermissionsService.hasPermission(userRole, IssueAction.DELETE_REQUIREMENT);
|
||||
const can_complete_requirement = this.issuePermissionsService.hasPermission(userRole, IssueAction.COMPLETE_REQUIREMENT);
|
||||
const can_edit_issue = this.issuePermissionsService.hasPermission(roles, IssueAction.EDIT_ISSUE);
|
||||
const can_change_status = this.issuePermissionsService.hasPermission(roles, IssueAction.CHANGE_STATUS);
|
||||
const can_assign_creator = this.issuePermissionsService.hasPermission(roles, IssueAction.ASSIGN_CREATOR);
|
||||
const can_set_done = this.issuePermissionsService.hasPermission(roles, IssueAction.SET_DONE);
|
||||
const can_set_on_review = this.issuePermissionsService.hasPermission(roles, IssueAction.SET_ON_REVIEW);
|
||||
const can_set_estimate = this.issuePermissionsService.hasPermission(roles, IssueAction.SET_ESTIMATE);
|
||||
const can_set_priority = this.issuePermissionsService.hasPermission(roles, IssueAction.SET_PRIORITY);
|
||||
const can_delete_issue = this.issuePermissionsService.hasPermission(roles, IssueAction.DELETE_ISSUE);
|
||||
const can_create_requirement = this.issuePermissionsService.hasPermission(roles, IssueAction.CREATE_REQUIREMENT);
|
||||
const can_edit_requirement = this.issuePermissionsService.hasPermission(roles, IssueAction.EDIT_REQUIREMENT);
|
||||
const can_delete_requirement = this.issuePermissionsService.hasPermission(roles, IssueAction.DELETE_REQUIREMENT);
|
||||
const can_complete_requirement = this.issuePermissionsService.hasPermission(roles, IssueAction.COMPLETE_REQUIREMENT);
|
||||
|
||||
let can_move_issue = false;
|
||||
const issueProject = await this.projectRepository.findByHash(issue.project_hash);
|
||||
@@ -174,8 +174,8 @@ export class PermissionsService {
|
||||
const projectOpenForMove = st === ProjectStatus.PENDING || st === ProjectStatus.ACTIVE;
|
||||
can_move_issue = projectPerms.can_manage_issues && projectOpenForMove;
|
||||
}
|
||||
// Получаем допустимые переходы статусов для текущего статуса и роли
|
||||
const allowed_status_transitions = this.issuePermissionsService.getAllowedStatusTransitions(userRole, issue.status);
|
||||
// Получаем допустимые переходы статусов для текущего статуса (UNION по ролям).
|
||||
const allowed_status_transitions = this.issuePermissionsService.getAllowedStatusTransitions(roles, issue.status);
|
||||
|
||||
return {
|
||||
can_edit_issue,
|
||||
@@ -229,8 +229,8 @@ export class PermissionsService {
|
||||
|
||||
const username = currentUser.username;
|
||||
|
||||
// Определяем роль пользователя для этого проекта
|
||||
const userRole = await this.projectPermissionsService.getProjectUserRole(username, project, currentUser.role);
|
||||
// Определяем НАБОР project-ролей пользователя (UNION-семантика).
|
||||
const roles = await this.projectPermissionsService.getProjectUserRole(username, project, currentUser.role);
|
||||
|
||||
// Проверяем наличие clearance (доступа к проекту)
|
||||
const has_clearance = project.coopname
|
||||
@@ -243,30 +243,30 @@ export class PermissionsService {
|
||||
: false;
|
||||
|
||||
// Рассчитываем права на основе матрицы доступа
|
||||
const can_edit_project = this.projectPermissionsService.hasProjectPermission(userRole, ProjectAction.EDIT_PROJECT);
|
||||
const can_manage_issues = this.projectPermissionsService.hasProjectPermission(userRole, ProjectAction.MANAGE_ISSUES);
|
||||
const can_edit_project = this.projectPermissionsService.hasProjectPermission(roles, ProjectAction.EDIT_PROJECT);
|
||||
const can_manage_issues = this.projectPermissionsService.hasProjectPermission(roles, ProjectAction.MANAGE_ISSUES);
|
||||
const can_change_project_status = this.projectPermissionsService.hasProjectPermission(
|
||||
userRole,
|
||||
roles,
|
||||
ProjectAction.CHANGE_PROJECT_STATUS
|
||||
);
|
||||
const can_delete_project = this.projectPermissionsService.hasProjectPermission(userRole, ProjectAction.DELETE_PROJECT);
|
||||
const can_set_master = this.projectPermissionsService.hasProjectPermission(userRole, ProjectAction.SET_MASTER);
|
||||
const can_manage_authors = this.projectPermissionsService.hasProjectPermission(userRole, ProjectAction.MANAGE_AUTHORS);
|
||||
const can_set_plan = this.projectPermissionsService.hasProjectPermission(userRole, ProjectAction.SET_PLAN);
|
||||
const can_delete_project = this.projectPermissionsService.hasProjectPermission(roles, ProjectAction.DELETE_PROJECT);
|
||||
const can_set_master = this.projectPermissionsService.hasProjectPermission(roles, ProjectAction.SET_MASTER);
|
||||
const can_manage_authors = this.projectPermissionsService.hasProjectPermission(roles, ProjectAction.MANAGE_AUTHORS);
|
||||
const can_set_plan = this.projectPermissionsService.hasProjectPermission(roles, ProjectAction.SET_PLAN);
|
||||
const can_create_requirement = this.projectPermissionsService.hasProjectPermission(
|
||||
userRole,
|
||||
roles,
|
||||
ProjectAction.CREATE_REQUIREMENT
|
||||
);
|
||||
const can_edit_requirement = this.projectPermissionsService.hasProjectPermission(
|
||||
userRole,
|
||||
roles,
|
||||
ProjectAction.EDIT_REQUIREMENT
|
||||
);
|
||||
const can_delete_requirement = this.projectPermissionsService.hasProjectPermission(
|
||||
userRole,
|
||||
roles,
|
||||
ProjectAction.DELETE_REQUIREMENT
|
||||
);
|
||||
const can_complete_requirement = this.projectPermissionsService.hasProjectPermission(
|
||||
userRole,
|
||||
roles,
|
||||
ProjectAction.COMPLETE_REQUIREMENT
|
||||
);
|
||||
|
||||
|
||||
+40
-46
@@ -30,52 +30,62 @@ export class ProjectPermissionsService {
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Определяет роль пользователя для конкретного проекта
|
||||
* Определяет НАБОР ролей пользователя для конкретного проекта.
|
||||
*
|
||||
* Один пользователь может одновременно быть, например, членом совета и
|
||||
* соавтором — итоговые права на проект считаются как UNION разрешений по
|
||||
* всем его ролям (см. {@link IssueAccessPolicyService.hasProjectPermission}).
|
||||
*
|
||||
* @param username - имя пользователя
|
||||
* @param project - проект
|
||||
* @param userRole - роль пользователя в системе (chairman, member, etc.)
|
||||
* @returns роль пользователя для проекта
|
||||
* @param userRole - системная роль пользователя (chairman / member / ...)
|
||||
* @returns множество project-ролей пользователя; пустого множества не бывает —
|
||||
* минимум {@link ProjectUserRole.GUEST}.
|
||||
*/
|
||||
async getProjectUserRole(username: string | undefined, project: any, userRole?: string): Promise<ProjectUserRole> {
|
||||
// Гости не имеют доступа
|
||||
async getProjectUserRole(
|
||||
username: string | undefined,
|
||||
project: any,
|
||||
userRole?: string
|
||||
): Promise<Set<ProjectUserRole>> {
|
||||
const roles = new Set<ProjectUserRole>();
|
||||
|
||||
if (!username) {
|
||||
return ProjectUserRole.GUEST;
|
||||
roles.add(ProjectUserRole.GUEST);
|
||||
return roles;
|
||||
}
|
||||
|
||||
// Председатель совета имеет полные права
|
||||
// Системные роли совета — добавляем независимо от project-specific ролей.
|
||||
if (userRole === 'chairman') {
|
||||
return ProjectUserRole.CHAIRMAN;
|
||||
roles.add(ProjectUserRole.CHAIRMAN);
|
||||
} else if (userRole === 'member') {
|
||||
roles.add(ProjectUserRole.BOARD_MEMBER);
|
||||
}
|
||||
|
||||
// Члены совета имеют расширенные права
|
||||
if (userRole === 'member') {
|
||||
return ProjectUserRole.BOARD_MEMBER;
|
||||
}
|
||||
|
||||
// Проверяем, является ли пользователь мастером проекта
|
||||
// Project-specific роли — собираем все, что верны.
|
||||
const isMaster = await this.isProjectMaster(username, project.coopname, project.project_hash);
|
||||
if (isMaster) {
|
||||
return ProjectUserRole.MASTER;
|
||||
roles.add(ProjectUserRole.MASTER);
|
||||
}
|
||||
|
||||
// Проверяем, является ли пользователь автором проекта
|
||||
const segment = await this.segmentRepository.findOne({
|
||||
username,
|
||||
project_hash: project.project_hash,
|
||||
coopname: project.coopname,
|
||||
});
|
||||
if (segment?.is_author) {
|
||||
return ProjectUserRole.AUTHOR;
|
||||
roles.add(ProjectUserRole.AUTHOR);
|
||||
}
|
||||
|
||||
// Проверяем, является ли пользователь участником проекта
|
||||
const contributor = await this.contributorRepository.findByUsernameAndCoopname(username, project.coopname);
|
||||
if (contributor && contributor.appendixes?.includes(project.project_hash)) {
|
||||
return ProjectUserRole.CONTRIBUTOR;
|
||||
roles.add(ProjectUserRole.CONTRIBUTOR);
|
||||
}
|
||||
|
||||
// По умолчанию - гость
|
||||
return ProjectUserRole.GUEST;
|
||||
if (roles.size === 0) {
|
||||
roles.add(ProjectUserRole.GUEST);
|
||||
}
|
||||
|
||||
return roles;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -90,34 +100,18 @@ export class ProjectPermissionsService {
|
||||
return project?.master === username;
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет разрешение на действие
|
||||
* @param userRole - роль пользователя
|
||||
* @param action - действие
|
||||
* @returns true если действие разрешено
|
||||
*/
|
||||
hasPermission(userRole: UserRole, action: IssueAction): boolean {
|
||||
return this.issueAccessPolicyService.hasPermission(userRole, action);
|
||||
/** UNION-проверка прав на действие над задачей по набору ролей. */
|
||||
hasPermission(roles: Iterable<UserRole>, action: IssueAction): boolean {
|
||||
return this.issueAccessPolicyService.hasPermission(roles, action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет разрешение на действие над проектом
|
||||
* @param userRole - роль пользователя для проекта
|
||||
* @param action - действие над проектом
|
||||
* @returns true если действие разрешено
|
||||
*/
|
||||
hasProjectPermission(userRole: ProjectUserRole, action: ProjectAction): boolean {
|
||||
return this.issueAccessPolicyService.hasProjectPermission(userRole, action);
|
||||
/** UNION-проверка прав на действие над проектом по набору ролей. */
|
||||
hasProjectPermission(roles: Iterable<ProjectUserRole>, action: ProjectAction): boolean {
|
||||
return this.issueAccessPolicyService.hasProjectPermission(roles, action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет разрешение на переход между статусами
|
||||
* @param userRole - роль пользователя
|
||||
* @param currentStatus - текущий статус
|
||||
* @param newStatus - новый статус
|
||||
* @returns true если переход разрешен
|
||||
*/
|
||||
canTransitionStatus(userRole: UserRole, currentStatus: any, newStatus: any): boolean {
|
||||
return this.issueAccessPolicyService.canTransitionStatus(userRole, currentStatus, newStatus);
|
||||
/** UNION-проверка перехода статусов по набору ролей. */
|
||||
canTransitionStatus(roles: Iterable<UserRole>, currentStatus: any, newStatus: any): boolean {
|
||||
return this.issueAccessPolicyService.canTransitionStatus(roles, currentStatus, newStatus);
|
||||
}
|
||||
}
|
||||
|
||||
+31
-27
@@ -600,27 +600,29 @@ export const PROJECT_PERMISSION_MATRIX: Record<ProjectUserRole, Record<ProjectAc
|
||||
|
||||
/**
|
||||
* Сервис политик доступа к задачам
|
||||
* Содержит бизнес-правила доступа к задачам в доменном слое
|
||||
*
|
||||
* Все проверки прав работают по UNION-семантике: пользователь может одновременно
|
||||
* нести несколько ролей (например, член совета + соавтор), и итоговое разрешение —
|
||||
* это OR по всем его ролям. Так лечится регрессия, при которой board_member
|
||||
* перебивал project-specific роль и обнулял её права.
|
||||
*/
|
||||
export class IssueAccessPolicyService {
|
||||
/**
|
||||
* Проверяет, есть ли у пользователя разрешение на действие
|
||||
* @param userRole - роль пользователя
|
||||
* @param action - действие
|
||||
* @returns true если действие разрешено
|
||||
* Проверяет, есть ли у пользователя разрешение на действие.
|
||||
* UNION по ролям: достаточно одной роли с allow=true.
|
||||
*/
|
||||
hasPermission(userRole: UserRole, action: IssueAction): boolean {
|
||||
return PERMISSION_MATRIX[userRole][action];
|
||||
hasPermission(roles: Iterable<UserRole>, action: IssueAction): boolean {
|
||||
for (const role of roles) {
|
||||
if (PERMISSION_MATRIX[role]?.[action]) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет разрешение на переход между статусами
|
||||
* @param userRole - роль пользователя
|
||||
* @param currentStatus - текущий статус
|
||||
* @param newStatus - новый статус
|
||||
* @returns true если переход разрешен
|
||||
* Проверяет разрешение на переход между статусами.
|
||||
* UNION по ролям: достаточно одной роли, для которой переход разрешён.
|
||||
*/
|
||||
canTransitionStatus(userRole: UserRole, currentStatus: IssueStatus, newStatus: IssueStatus): boolean {
|
||||
canTransitionStatus(roles: Iterable<UserRole>, currentStatus: IssueStatus, newStatus: IssueStatus): boolean {
|
||||
if (currentStatus === newStatus) {
|
||||
return true; // Можно "перейти" в тот же статус
|
||||
}
|
||||
@@ -635,40 +637,42 @@ export class IssueAccessPolicyService {
|
||||
return false;
|
||||
}
|
||||
|
||||
return newStatusTransitions[userRole] || false;
|
||||
for (const role of roles) {
|
||||
if (newStatusTransitions[role]) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, есть ли у пользователя разрешение на действие над проектом
|
||||
* @param userRole - роль пользователя для проекта
|
||||
* @param action - действие над проектом
|
||||
* @returns true если действие разрешено
|
||||
* Проверяет, есть ли у пользователя разрешение на действие над проектом.
|
||||
* UNION по ролям.
|
||||
*/
|
||||
hasProjectPermission(userRole: ProjectUserRole, action: ProjectAction): boolean {
|
||||
return PROJECT_PERMISSION_MATRIX[userRole][action];
|
||||
hasProjectPermission(roles: Iterable<ProjectUserRole>, action: ProjectAction): boolean {
|
||||
for (const role of roles) {
|
||||
if (PROJECT_PERMISSION_MATRIX[role]?.[action]) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает список допустимых статусов для перехода из текущего статуса для данной роли
|
||||
* @param userRole - роль пользователя
|
||||
* @param currentStatus - текущий статус задачи
|
||||
* @returns массив допустимых статусов для перехода (исключая текущий статус)
|
||||
* Получает список допустимых статусов для перехода из текущего статуса.
|
||||
* UNION по ролям: статус попадает в список, если хотя бы одна роль его разрешает.
|
||||
*/
|
||||
getAllowedStatusTransitions(userRole: UserRole, currentStatus: IssueStatus): IssueStatus[] {
|
||||
getAllowedStatusTransitions(roles: Iterable<UserRole>, currentStatus: IssueStatus): IssueStatus[] {
|
||||
const transitions = STATUS_TRANSITION_MATRIX[currentStatus];
|
||||
if (!transitions) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const rolesArr = [...roles];
|
||||
const allowedStatuses: IssueStatus[] = [];
|
||||
|
||||
for (const [newStatus, rolePermissions] of Object.entries(transitions)) {
|
||||
// Исключаем текущий статус из списка доступных переходов
|
||||
if (newStatus === currentStatus) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const isAllowed = rolePermissions[userRole] || false;
|
||||
const isAllowed = rolesArr.some((r) => rolePermissions[r]);
|
||||
if (isAllowed) {
|
||||
allowedStatuses.push(newStatus as IssueStatus);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import {
|
||||
IssueAccessPolicyService,
|
||||
IssueAction,
|
||||
ProjectAction,
|
||||
ProjectUserRole,
|
||||
UserRole,
|
||||
} from '../../../src/extensions/capital/domain/services/access-policy.service';
|
||||
import { IssueStatus } from '../../../src/extensions/capital/domain/enums/issue-status.enum';
|
||||
|
||||
describe('IssueAccessPolicyService — UNION-семантика по нескольким ролям', () => {
|
||||
const policy = new IssueAccessPolicyService();
|
||||
|
||||
describe('hasProjectPermission', () => {
|
||||
it('GUEST: запрещены все project-actions', () => {
|
||||
const roles = new Set([ProjectUserRole.GUEST]);
|
||||
expect(policy.hasProjectPermission(roles, ProjectAction.EDIT_REQUIREMENT)).toBe(false);
|
||||
expect(policy.hasProjectPermission(roles, ProjectAction.EDIT_PROJECT)).toBe(false);
|
||||
});
|
||||
|
||||
it('AUTHOR (соавтор): EDIT_REQUIREMENT=true, CHANGE_PROJECT_STATUS=false', () => {
|
||||
const roles = new Set([ProjectUserRole.AUTHOR]);
|
||||
expect(policy.hasProjectPermission(roles, ProjectAction.EDIT_REQUIREMENT)).toBe(true);
|
||||
expect(policy.hasProjectPermission(roles, ProjectAction.CHANGE_PROJECT_STATUS)).toBe(false);
|
||||
});
|
||||
|
||||
it('BOARD_MEMBER (чистый): CHANGE_PROJECT_STATUS=true, EDIT_REQUIREMENT=false (как раньше)', () => {
|
||||
const roles = new Set([ProjectUserRole.BOARD_MEMBER]);
|
||||
expect(policy.hasProjectPermission(roles, ProjectAction.CHANGE_PROJECT_STATUS)).toBe(true);
|
||||
expect(policy.hasProjectPermission(roles, ProjectAction.EDIT_REQUIREMENT)).toBe(false);
|
||||
});
|
||||
|
||||
// Главный кейс — починка регрессии «соавтор-член-совета не может править артефакты»
|
||||
it('BOARD_MEMBER + AUTHOR: получает права обеих ролей', () => {
|
||||
const roles = new Set([ProjectUserRole.BOARD_MEMBER, ProjectUserRole.AUTHOR]);
|
||||
expect(policy.hasProjectPermission(roles, ProjectAction.EDIT_REQUIREMENT)).toBe(true);
|
||||
expect(policy.hasProjectPermission(roles, ProjectAction.CREATE_REQUIREMENT)).toBe(true);
|
||||
expect(policy.hasProjectPermission(roles, ProjectAction.DELETE_REQUIREMENT)).toBe(true);
|
||||
expect(policy.hasProjectPermission(roles, ProjectAction.CHANGE_PROJECT_STATUS)).toBe(true);
|
||||
expect(policy.hasProjectPermission(roles, ProjectAction.SET_MASTER)).toBe(true);
|
||||
expect(policy.hasProjectPermission(roles, ProjectAction.MANAGE_ISSUES)).toBe(true);
|
||||
});
|
||||
|
||||
it('CHAIRMAN + MASTER: union (председатель остаётся всемогущим, мастер ничего не отнимает)', () => {
|
||||
const roles = new Set([ProjectUserRole.CHAIRMAN, ProjectUserRole.MASTER]);
|
||||
expect(policy.hasProjectPermission(roles, ProjectAction.DELETE_PROJECT)).toBe(true);
|
||||
expect(policy.hasProjectPermission(roles, ProjectAction.SET_MASTER)).toBe(true);
|
||||
expect(policy.hasProjectPermission(roles, ProjectAction.EDIT_REQUIREMENT)).toBe(true);
|
||||
});
|
||||
|
||||
it('пустое множество: всё запрещено', () => {
|
||||
expect(policy.hasProjectPermission(new Set(), ProjectAction.EDIT_REQUIREMENT)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasPermission (issue-actions)', () => {
|
||||
it('CONTRIBUTOR (чистый): EDIT_REQUIREMENT=false', () => {
|
||||
expect(policy.hasPermission(new Set([UserRole.CONTRIBUTOR]), IssueAction.EDIT_REQUIREMENT)).toBe(false);
|
||||
});
|
||||
|
||||
it('BOARD_MEMBER + AUTHOR на задаче: EDIT_REQUIREMENT=true (через AUTHOR)', () => {
|
||||
const roles = new Set([UserRole.BOARD_MEMBER, UserRole.AUTHOR]);
|
||||
expect(policy.hasPermission(roles, IssueAction.EDIT_REQUIREMENT)).toBe(true);
|
||||
expect(policy.hasPermission(roles, IssueAction.CHANGE_STATUS)).toBe(true);
|
||||
});
|
||||
|
||||
it('SUBMASTER + AUTHOR: SET_ON_REVIEW (через SUBMASTER) + EDIT_REQUIREMENT (через AUTHOR)', () => {
|
||||
const roles = new Set([UserRole.SUBMASTER, UserRole.AUTHOR]);
|
||||
expect(policy.hasPermission(roles, IssueAction.SET_ON_REVIEW)).toBe(true);
|
||||
expect(policy.hasPermission(roles, IssueAction.EDIT_REQUIREMENT)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('canTransitionStatus / getAllowedStatusTransitions', () => {
|
||||
it('BACKLOG → DONE доступен MASTER, недоступен AUTHOR; union → доступен', () => {
|
||||
expect(
|
||||
policy.canTransitionStatus(new Set([UserRole.AUTHOR]), IssueStatus.BACKLOG, IssueStatus.DONE)
|
||||
).toBe(false);
|
||||
expect(
|
||||
policy.canTransitionStatus(new Set([UserRole.MASTER]), IssueStatus.BACKLOG, IssueStatus.DONE)
|
||||
).toBe(true);
|
||||
expect(
|
||||
policy.canTransitionStatus(
|
||||
new Set([UserRole.AUTHOR, UserRole.MASTER]),
|
||||
IssueStatus.BACKLOG,
|
||||
IssueStatus.DONE
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('getAllowedStatusTransitions: union собирает разрешения по всем ролям', () => {
|
||||
const authorOnly = policy.getAllowedStatusTransitions(new Set([UserRole.AUTHOR]), IssueStatus.BACKLOG);
|
||||
const masterOnly = policy.getAllowedStatusTransitions(new Set([UserRole.MASTER]), IssueStatus.BACKLOG);
|
||||
const union = policy.getAllowedStatusTransitions(
|
||||
new Set([UserRole.AUTHOR, UserRole.MASTER]),
|
||||
IssueStatus.BACKLOG
|
||||
);
|
||||
const expectedUnion = Array.from(new Set([...authorOnly, ...masterOnly])).sort();
|
||||
expect([...union].sort()).toEqual(expectedUnion);
|
||||
// Sanity: master может больше автора → union строго ⊇ author-only
|
||||
for (const s of authorOnly) expect(union).toContain(s);
|
||||
});
|
||||
|
||||
it('переход в тот же статус — true, даже для GUEST', () => {
|
||||
expect(
|
||||
policy.canTransitionStatus(new Set([UserRole.GUEST]), IssueStatus.BACKLOG, IssueStatus.BACKLOG)
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "cooptypes",
|
||||
"type": "module",
|
||||
"version": "2026.4.30-alpha-1",
|
||||
"version": "2026.5.10-4",
|
||||
"description": "Shared TypeScript типы и интерфейсы экосистемы кооперативов",
|
||||
"author": "Alex Ant <dacom.dark.sun@gmail.com>",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -89,9 +89,11 @@ export const LEDGER2_OPERATION_REGISTRY: readonly OperationMeta[] = [
|
||||
human_name: 'Возврат паевого взноса пайщику' },
|
||||
|
||||
// capital (ADR-009: единые программные кошельки `w.cap.blago`/`w.cap.gen`)
|
||||
// IMPORT и ACCEPT_PROPERTY — Dr 04 (НМА), не Dr 51: импорт/акт-2 фиксируют
|
||||
// имущественный вклад как РИД, не деньги. Денежные взносы в Благорост — INVEST.
|
||||
{ code: 'o.cap.import', process_type: 'p.cap.import', contract: 'capital',
|
||||
name: 'IMPORT', wallet_op: 'ISSUE', wallet_from: null, wallet_to: 'w.cap.blago',
|
||||
debit: 51, credit: 80,
|
||||
debit: 4, credit: 80,
|
||||
human_name: 'Паевой взнос по ЦПП «Благорост» (офлайн-импорт)' },
|
||||
|
||||
{ code: 'o.cap.invest', process_type: 'p.cap.invest', contract: 'capital',
|
||||
@@ -111,9 +113,19 @@ export const LEDGER2_OPERATION_REGISTRY: readonly OperationMeta[] = [
|
||||
|
||||
{ code: 'o.cap.actprp', process_type: 'p.cap.prop', contract: 'capital',
|
||||
name: 'ACCEPT_PROPERTY', wallet_op: 'ISSUE', wallet_from: null, wallet_to: 'w.cap.blago',
|
||||
debit: 51, credit: 80,
|
||||
debit: 4, credit: 80,
|
||||
human_name: 'Паевой взнос (имущественный) по программе «Благорост»' },
|
||||
|
||||
{ code: 'o.cap.preimp', process_type: 'p.cap.preimp', contract: 'capital',
|
||||
name: 'PREIMP', wallet_op: 'ISSUE', wallet_from: null, wallet_to: 'w.cap.preimp',
|
||||
debit: 4, credit: 80,
|
||||
human_name: 'Первичный учёт РИД-взноса до перехода на электронный учёт' },
|
||||
|
||||
{ code: 'o.cap.drppre', process_type: 'p.cap.import', contract: 'capital',
|
||||
name: 'DROP_PREIMP', wallet_op: 'BURN', wallet_from: 'w.cap.preimp', wallet_to: null,
|
||||
debit: 80, credit: 4,
|
||||
human_name: 'Закрытие пред-импорт-учёта РИД-взноса при переходе на электронный учёт' },
|
||||
|
||||
{ code: 'o.cap.lend', process_type: 'p.cap.debt', contract: 'capital',
|
||||
name: 'LEND', wallet_op: 'ISSUE', wallet_from: null, wallet_to: 'w.cap.loan',
|
||||
debit: 58, credit: 51,
|
||||
|
||||
@@ -33,6 +33,7 @@ export const LEDGER2_PROCESS_REGISTRY: readonly ProcessMeta[] = [
|
||||
{ type: 'p.cap.debt', contract: 'capital', name: 'DEBT', human_name: 'Займ пайщику' },
|
||||
{ type: 'p.cap.rid', contract: 'capital', name: 'RID', human_name: 'Приём РИД в паевой фонд' },
|
||||
{ type: 'p.cap.prop', contract: 'capital', name: 'PROPERTY', human_name: 'Приём имущественного паевого взноса' },
|
||||
{ type: 'p.cap.preimp', contract: 'capital', name: 'PREIMP', human_name: 'Первичный учёт РИД-взноса до перехода на электронный учёт' },
|
||||
{ type: 'p.cap.wthcap', contract: 'capital', name: 'WTHCAP', human_name: 'Возврат паевого из ЦПП «Благорост» в кошелёк' },
|
||||
|
||||
// marketplace
|
||||
|
||||
@@ -24,10 +24,12 @@ export const LEDGER2_WALLET_REGISTRY: readonly WalletMeta[] = [
|
||||
{ name: "w.wal.member", human_name: "ЦК — членская часть пайщика", kind: "USER_SHARED" },
|
||||
{ name: "w.cap.blago", human_name: "ЦПП «Благорост» — единый кошелёк программы у пайщика", kind: "USER_SHARED" },
|
||||
{ name: "w.cap.gen", human_name: "ЦПП «Генератор» — единый кошелёк программы у пайщика", kind: "USER_SHARED" },
|
||||
{ name: "w.cap.preimp", human_name: "Первичный учёт РИД-взносов до подписания договора УХД", kind: "USER_SHARED" },
|
||||
{ name: "w.reg.entry", human_name: "Вступительные взносы", kind: "COOPERATIVE" },
|
||||
{ name: "w.wal.wthdrw", 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" },
|
||||
{ name: "w.cap.loan", human_name: "Выданные пайщикам беспроцентные займы", kind: "COOPERATIVE" },
|
||||
{ name: "w.mkt.payout", human_name: "Выплаты поставщикам", kind: "COOPERATIVE" },
|
||||
] as const
|
||||
@@ -51,4 +53,5 @@ export const LEDGER2_USER_SHARED_PROGRAM_MAPPING: readonly ProgramWalletMapping[
|
||||
{ wallet_name: "w.wal.member", required_program_id: 1, program_label: "ЦК" },
|
||||
{ wallet_name: "w.cap.blago", required_program_id: 4, program_label: "Благорост" },
|
||||
{ wallet_name: "w.cap.gen", required_program_id: 3, program_label: "Генератор" },
|
||||
{ wallet_name: "w.cap.preimp", required_program_id: 0, program_label: null },
|
||||
] as const
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@coopenomics/desktop",
|
||||
"version": "2026.4.30-alpha-1",
|
||||
"version": "2026.5.10-4",
|
||||
"description": "Рабочий стол кооператива — Vue 3 + Quasar Framework",
|
||||
"productName": "Desktop App",
|
||||
"author": "Alex Ant <dacom.dark.sun@gmail.com>",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@coopenomics/docs",
|
||||
"type": "module",
|
||||
"version": "2026.4.30-alpha-1",
|
||||
"version": "2026.5.10-4",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@9.9.0",
|
||||
"description": "Документация для монорепозитория",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@coopenomics/factory",
|
||||
"type": "module",
|
||||
"version": "2026.4.30-alpha-1",
|
||||
"version": "2026.5.10-4",
|
||||
"description": "Фабрика юридических документов кооператива",
|
||||
"author": "Alex Ant <chairman.voskhod@gmail.com>",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@coopenomics/inter",
|
||||
"type": "module",
|
||||
"version": "2026.4.30-alpha-1",
|
||||
"version": "2026.5.10-4",
|
||||
"description": "Контракты и DI-токены для связи расширений контроллера без прямых зависимостей",
|
||||
"author": "Alex Ant <dacom.dark.sun@gmail.com>",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "migrator",
|
||||
"version": "2026.4.30-alpha-1",
|
||||
"version": "2026.5.10-4",
|
||||
"description": "Утилита миграции данных между версиями",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@coopenomics/notifications",
|
||||
"version": "2026.4.30-alpha-1",
|
||||
"version": "2026.5.10-4",
|
||||
"description": "Библиотека типобезопасных workflow-уведомлений для Novu",
|
||||
"type": "module",
|
||||
"private": false,
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "pkg-placeholder",
|
||||
"version": "2026.4.30-alpha-1",
|
||||
"version": "2026.5.10-4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pkg-placeholder",
|
||||
"version": "2026.4.30-alpha-1",
|
||||
"version": "2026.5.10-4",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^2.16.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@coopenomics/parser",
|
||||
"type": "module",
|
||||
"version": "2026.4.30-alpha-1",
|
||||
"version": "2026.5.10-4",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@9.0.6",
|
||||
"description": "Индексатор блокчейна через State History Plugin",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@coopenomics/sdk",
|
||||
"type": "module",
|
||||
"version": "2026.4.30-alpha-1",
|
||||
"version": "2026.5.10-4",
|
||||
"private": false,
|
||||
"packageManager": "pnpm@9.9.0",
|
||||
"description": "TypeScript SDK для работы с API кооператива",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@coopenomics/setup",
|
||||
"version": "2026.4.30-alpha-1",
|
||||
"version": "2026.5.10-4",
|
||||
"private": true,
|
||||
"description": "Интерактивный мастер настройки окружения",
|
||||
"main": "index.js",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
||||
"version": "2026.4.30-alpha-1",
|
||||
"version": "2026.5.10-4",
|
||||
"packages": [
|
||||
"components/*",
|
||||
"components/controller/src/plugins/*"
|
||||
|
||||
@@ -1,5 +1,28 @@
|
||||
{
|
||||
"name": "monocoop",
|
||||
"packageManager": "pnpm@10.33.0",
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"@apollo/protobufjs",
|
||||
"@eosrio/node-abieos",
|
||||
"@nestjs/core",
|
||||
"@parcel/watcher",
|
||||
"@scarf/scarf",
|
||||
"core-js",
|
||||
"cpu-features",
|
||||
"electron",
|
||||
"esbuild",
|
||||
"libxmljs2",
|
||||
"msgpackr-extract",
|
||||
"nx",
|
||||
"protobufjs",
|
||||
"puppeteer",
|
||||
"simple-git-hooks",
|
||||
"ssh2",
|
||||
"unrs-resolver",
|
||||
"vue-demi"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@coopenomics/boot": "workspace:*",
|
||||
"@coopenomics/cleos": "workspace:*",
|
||||
|
||||
Reference in New Issue
Block a user