Compare commits

...

21 Commits

Author SHA1 Message Date
ant de55c3aa3e Merge pull request 'Эпик 1 (Благорост): контракт capital — займы, расходы программы, L2-роли' (#3) from feat/blagorost-E1-contract into blagorost
Reviewed-on: #3
2026-06-02 06:17:24 +00:00
coopops 32fc211006 fix(capital): стандарт debt — «подписанный акт приёма-передачи»
Поправка терминологии в p.cap.debt.standard.yaml: «вторая подпись на
акте» → «подписанный акт приёма-передачи» — корректное юридическое
название документа, который завершает приём результата работ по проекту.

Затронуты: operations.o.cap.repay.description, related.p.cap.rid.note.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 06:27:59 +00:00
coopops 24663bdaac fix(capital,loan): gitea PR #3 review — 3 коммента
1. loan.debts.source_contract — убрана binary_extension-обёртка, простой
   name. Таблица в продакшене пустая, расширение не нужно.

2. p.cap.debt.standard.yaml переписан бизнес-языком для председателей /
   бухгалтеров / пайщиков. Из прозы убраны идентификаторы кошельков
   (w.cap.*), коды операций (o.cap.*), бухгалтерские проводки в шапке и
   action.purpose / state.description / scenario / alternatives. Технические
   детали остались только в structured-секциях 5 (documents) и 6 (operations).

3. «акт-2» убран как ошибочная терминология — у проекта один акт, на
   нём ставится вторая подпись. Поправлено везде в стандарте.

Compile: loan + capital собраны через cdt-cpp v4.1.0 без ошибок.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 06:25:00 +00:00
coopops c3a433b591 refactor(capital): финальный канон имён debt-операций невозврата
Утверждено автором 2026-05-19. Семантика операции через имя, группировка по
сущности (dbt для debt-операций), без аббревиатур типа seize/wroff/seizecollat.

Переименования (относительно предыдущего refactor-коммита):
  o.cap.seize       → o.cap.crtnma   (CREATE_NMA — создать НМА кооператива)
  o.cap.wroff       → o.cap.dbtwrf   (DEBT_WRITEOFF — списать долг)
  capital::seizecollat → capital::closedebt (закрыть долг)
  Status::SEIZED    → Status::WRITEOFF
  mark_seized       → mark_writeoff
  seizecollat.cpp   → closedebt.cpp

Кошелёк w.cap.nma и проводки (Дт 04 / Кт 08 + Дт 80 / Кт 58) — без изменений.

Канон утверждён ДО реализации (правило в auto-memory:
feedback-kanon-naming-soglasovyvat); предыдущий заход с собственными
именами отвергнут автором.

Compile: cdt-cpp v4.1.0 — exit 0, capital.abi содержит action closedebt.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 18:55:05 +00:00
coopops ca877ab294 refactor(capital): канон имён ledger2 + реализован action seizecollat
Канон имён в ledger2-онтологии — самодокументирующиеся английские термины
без аббревиатур. Префикс контракта (cap) не дублировать в имени.

Переименования (старые имена — отвергнутый рабочий драфт; нигде больше
не используются):
  o.cap.dflt    → o.cap.seize    (SEIZE_COLLATERAL — изъятие обеспечения)
  o.cap.lnwoff  → o.cap.wroff    (WRITE_OFF_LOAN — списание займа)
  w.cap.coopn   → w.cap.nma      (NMA — НМА кооператива)
  capital::dfltdebt → capital::seizecollat
  state defaulted (virtual) → seized (Status::SEIZED + mark_seized)

Story 1.13 — action capital::seizecollat:
  - guard: debt.status ∈ {paid, overdue} ∧ project.status == cancelled;
  - apply o.cap.seize  (TRANSFER w.cap.gen → w.cap.nma, Дт 04 / Кт 08);
  - apply o.cap.wroff  (BURN w.cap.loan,                Дт 80 / Кт 58);
  - decrease segment.debt_amount + active_debts_count;
  - subtract project.used_for_compensation;
  - decrease contributors.debt_amount;
  - mark debt SEIZED (mark_seized);
  - inline Loan::settle_debt (loan-контракт);
  - require_recipient(username, coopname);
  - require_auth(coopname) — без подписи пайщика, без сбора совета.

Решения 2026-05-19 (Игорь Смуров / Алексей Муравьёв):
  - срок займа фикс — 1 год;
  - проводки невозврата: Дт 04 / Кт 08 + Дт 80 / Кт 58;
  - автоматизм без подписи пайщика и без сбора совета.

Compile: cdt-cpp v4.1.0 OK (warnings only Ricardian + deprecation).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 18:30:47 +00:00
coopops 4d6a03a142 feat(capital): закрыть невозврат займа — обращение взыскания на коммиты-обеспечение
Утверждённые проводки и срок (обсуждение Matrix 2026-05-19, Игорь Смуров / Алексей Муравьев):

* Срок займа — фиксированный 1 год вместо 3 мес. (`DEFAULT_DEBT_TERM_SECONDS = 365 × 24 × 3600`).
* Новые операции ledger2:
  - `o.cap.dflt` — обращение взыскания на коммиты-обеспечение (Дт 04 / Кт 08, TRANSFER `w.cap.gen` → `w.cap.coopn`).
  - `o.cap.lnwoff` — списание займа в счёт обращения взыскания (Дт 80 / Кт 58, BURN `w.cap.loan`).
* Новый кошелёк `w.cap.coopn` (COOP_NMA, COOPERATIVE) — НМА кооператива, полученные из обеспечения.
* Кооперативный стандарт `p.cap.debt.standard.yaml` обновлён: состояние `defaulted`, шаг `dfltdebt`, две новые операции в секции 6, альтернативная ветка в сценарии.
* Тесты-заготовки в `capital-blagorost.test.ts` (Story 1.13).

Реализация самого action `capital::dfltdebt` остаётся за Story 1.13 — здесь только ontology + стандарт + проводки.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 18:07:58 +00:00
coopops c185b0167f fix(capital+loan): review PR#402 round 2 — изоляция loan, счётчик займов в сегменте, бездокументарные роли
Round 2 ревью PR #402 — 8 замечаний:

LOAN-контракт независим от прикладных (capital/marketplace/...):
- loan.debts: убран binary_extension<checksum256> project_hash и индекс byuserproj
- loan.debts: добавлен binary_extension<name> source_contract (контракт-источник)
- loan::createdebt(): убран параметр project_hash, payer пишется в source_contract
- Loan::create_debt(): убран project_hash из сигнатуры и хелпера
- Loan::count_user_project_debts(): удалена — capital считает у себя
- Локальный контекст (project_hash и т.п.) остаётся в Capital::Debts

CAPITAL — счётчик активных займов на сегменте через binary_extension:
- Segment::active_debts_count (binary_extension<uint32_t>, хвостом)
- increase/decrease/get_active_debts_count хелперы
- debtpaycnfrm: после confirm_paid +1 счётчика сегмента
- settledebt: после mark_settled -1 счётчика
- signact2: лимит «10 займов на проект в одном паевом взносе» проверяется
  по счётчику (не через обход byprojhash); сам перебор ограничен счётчиком,
  ранний выход после нужного числа активных записей

role_requests — бездокументарная схема и строгая типизация:
- namespace Role { CREATOR, AUTHOR, MASTER, NONE } в role_requests.hpp
- удалены поля statement и master_decision из role_request struct
- create()/approve() — без параметров document2
- validate_role_or_fail и apply_role_to_project — через Role:: namespace
- requestrole/inviterole/approverole/acceptinvite — без statement/master_decision
- requestrateu — role = Role::NONE (не маркер "rate"_n), request_type = RATE_UPDATE
- decline сохраняет только reason; решения подписываются coopname-транзакцией

prerequisite fix:
- names.hpp: убран дубль Names::Capital::CREATE_PROGRAM_EXPENSE (был объявлен
  и в callback-блоке line 55, и в акцепт-блоке line 130 — оставлен один)

Замечания закрыты:
- PR comment id=3264976591 (фу-фу byuserproj) — индекс удалён
- id=3264980890 (count_user_project_debts) — функция удалена
- id=3265021918 (loan независим от capital) — изоляция выполнена
- id=3265072380 (rate как name через namespace) — RequestType::RATE_UPDATE + Role::NONE
- id=3265080990 (счётчик в binary_extension вместо перебора) — active_debts_count
- id=3265088096 (namespace типа роли) — Role::{CREATOR,AUTHOR,MASTER}
- id=3265101459 (без statement/master_decision) — поля удалены
- id=3265104196 (через Namespace проверка) — validate_role_or_fail через Role::

Сборка: cdt-cpp v4.1.0 capital.wasm + loan.wasm без ошибок.
Тесты: capital-blagorost.test.ts — формулировки A2/D обновлены под счётчик
       и бездокументарную схему.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 09:40:09 +00:00
coopops 07f35ea084 feat(loan): централизация учёта займов — capital вызывает loan через inline action
Ревью PR#402 (debtpaycnfrm:23): учёт займов должен быть централизован в одном контракте,
как Gateway для платежей. Существующий контракт loan/ уже задеплоен и в whitelist,
но capital его не вызывал — теперь вызывает.

loan-контракт:
— table_loan_debts: добавлено поле project_hash (binary_extension<checksum256> —
  таблица уже в продакшене, расширение схемы только хвостом) + индекс byuserproj
— Loan::create_debt: новая сигнатура с project_hash; createdebt сохраняет привязку к проекту
— Loan::count_user_project_debts: хелпер для лимита займов на проект в паевом взносе

capital:
— debtpaycnfrm: после confirm_paid дублирует запись в loan.debts через inline
  Loan::create_debt (с due_at из обновлённой capital-записи и project_hash)
— settledebt: дополнительно зовёт inline Loan::settle_debt; принимает также OVERDUE
  (не только PAID) — погашение просроченного через председателя
— signact2 (паевой взнос): собирает все PAID/OVERDUE займы пайщика на проекте
  из capital.debts, проверяет лимит 10 (transaction limit EOSIO), на каждый —
  inline Loan::settle_debt + capital.Debts::mark_settled. Никакого «боевого взноса» —
  только паевой по интеллектуальному результату

Boot/deploy не требует правок: loan/ уже в configs/contracts.ts и configs/index.ts,
аккаунт loan создаётся, контракт деплоится. capital уже в contracts_whitelist в lib/consts.

Тесты: добавлен describe «A2 — Централизованный учёт займов» с 6 it.todo
по новым сценариям (createdebt в loan, settle через capital, лимит 10, whitelist).

Полный перенос таблицы debts из capital в loan (с удалением capital.debts) —
отдельная Story 1.12, оставлена на следующую итерацию: текущий PR сохраняет
параллельный учёт в обеих таблицах для безопасной миграции прода.
2026-05-19 08:16:15 +00:00
coopops 4a13cd26c0 fix(capital/roles): review PR#402 — 3 роли, description, явная ставка, inline add author/master
role_requests:
— role строго {creator, author, master}; contributor — L1, в role_requests не попадает
— validate_role_or_fail() — валидация на уровне requestrole/inviterole
— description (std::string, может быть пустой) — текст заявки/приглашения от пайщика к мастеру и наоборот
— apply_role_to_project(): inline addauthor для role=author, setmaster для role=master,
  creator → no-op (фиксируется при первом createcmmt)

Actions:
— requestrole/inviterole/requestrateu: добавлено description в сигнатуру + проброс в RoleRequests::create
— approverole/acceptinvite: после approve/segment.set_approved_rate вызывают
  apply_role_to_project — заявленная роль немедленно отражается в реестре участников
  (раньше creator становился creator только после первого commit'а)
— inviterole: уточнено в комментарии что master-параметр может быть председателем
  при приглашении кандидата на роль master (когда мастера в проекте ещё нет)

Ставка часа всегда подаётся явно (rate_per_hour, hours_per_day) — это ставка пайщика
именно под этот проект; фронт по дефолту подставляет contributors.rate_per_hour,
но пайщик может изменить под конкретный проект. createcmmt берёт approved_rate
сегмента, не глобальную из contributors.
2026-05-19 08:09:08 +00:00
coopops 59a8b9b6ee fix(capital): review PR#402 — binary_extension + auth + лимиты + 86 счёт
— segments.approved_rate_per_hour/hours_per_day → binary_extension<> (прод-таблица)
— global_state.program_expense_pool/reserved → binary_extension<asset> (прод-таблица)
— settledebt: require_auth(coopname), убран has_auth(username) — backend подписывает
— markdebtoverd: batch_limit 100 → 25 (transaction time limit EOSIO 0.5с)
— approverole/inviterole/requestrole/requestrateu: норма часов 12 → 8 (фронт-ограничение)
— debtpayretry: новый action для повторной отправки outpay из AUTHORIZED + last_pay_error
— PAY_EXPENSE: проводка Dr 86 / Cr 51 (TARGET_RECEIPTS/BANK_ACCOUNT) — целевое финансирование, не общехоз 26
— memo.get_settle_debt_memo: "Погашение займа участником" (без "деньгами") — погашение не только наличными
2026-05-19 08:04:50 +00:00
coopops eb720a88ef test(capital): scaffold capital-blagorost.test.ts под новые ветви Эпика 1
Story 1.11 эпика «Благорост» (blago issue 51F-23).

Существующий capital.test.ts ≈2650 строк — раздувать монолит нерационально.
Новые сценарии вынесены в отдельный файл со структурой describe/it.todo() —
каркас под пошаговое наполнение реальными helper'ами по мере появления
backend-обвязки (Эпик 4: processDebtDeclinePay, processSettleDebt,
processMarkOverdue, processProgramExpense, processRequestRole, и т.д.).

Покрытые сценарии (it.todo):
- A: debtpaydcln (×2), settledebt (×3), markdebtoverd (×3) + due_at (×1)
- B: exppaycnfrm проводка, topupprogexp, createpgexp резервирование,
  полный цикл создания/одобрения/авторизации/оплаты, declpgexp на 3 фазах
- D: requestrole/approverole + approved_rate в createcmmt (1200 ≠ 1500),
  editcontrib не трогает approved, inviterole/acceptinvite/declinvite,
  declinerole, requestrateu
- E: require_recipient на всех новых actions (Story 1.10)

Существующие тесты не сломаны (отдельный файл, отдельный describe).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 17:07:41 +00:00
coopops 58b8654989 feat(capital): require_recipient во все новые actions эпика «Благорост»
Story 1.10 эпика «Благорост» (blago issue 51F-22).

Notification-services (Эпик 6) подписываются на chain через action traces
и триггерят Novu workflows. На каждой ветке переходов требуется явный
require_recipient, чтобы участники, не подписавшие транзакцию, всё равно
получили её в свою цепочку recipient — это и есть «event ridge» из PRD-E.

Покрыто:
- debtpaydcln  → должник + coopname
- settledebt   → должник + coopname
- createpgexp  → creator + coopname
- apprvpgexp   → expense.username + approver
- authpgexp    → expense.username + coopname
- pgexppay     → expense.username + coopname
- declpgexp    → expense.username + coopname
- requestrole  → username + master
- approverole  → req.username + req.master
- declinerole  → req.username + req.master
- inviterole   → candidate + master
- acceptinvite → req.username + req.master
- declinvite   → req.username + req.master
- requestrateu → username + master

markdebtoverd сознательно пропущен — это batch-action (до 100 записей
за вызов), массовая рассылка require_recipient ухудшит RAM-стоимость
транзакции. Novu по этому переходу подписывается на изменение статуса
debt записи через парсер.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 17:05:58 +00:00
coopops 138d7ab350 chore(capital): чистка debug print() в startvoting + [[deprecated]] на program_invest
Story 1.9 эпика «Благорост» (blago issue 51F-21).

PRD: «удалить отладочные print() в startvoting.cpp; пометить progrinvests
deprecated до 07.2026». Логирующие print() в returntopool.cpp и lvlnotify.cpp
оставлены — они несут смысл, не отладочные маркеры.

Изменения:
- startvoting.cpp: убраны 5 маркеров print("z"/"a"/"b"/"c"/"d")
- program_invests.hpp: атрибут [[deprecated("remove after 2026-07")]] на struct
  program_invest + расширенный doxygen-комментарий с пояснением жизненного цикла

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 17:04:30 +00:00
coopops b2adb56535 feat(capital/roles): L2-роли — requestrole/approverole/declinerole/inviterole/acceptinvite/declinvite/requestrateu
Story 1.7 эпика «Благорост» (blago issue 51F-19).

PRD-D1/D3/D4: «двухуровневые допуски, мастер компонента валидирует исполнителей
и ставку часа на сегменте». До этого коммита actions L2-цикла в контракте
не существовало; ставки фиксировались только глобально через editcontrib.

Изменения:
- domain/entities/role_requests.hpp: таблица rolerequests, статусы pending/approved/declined,
  направление request/invite, тип role/rateupdate, helpers create/approve/decline
- 7 новых cpp actions в app/participation_management/roles/:
  - requestrole — пайщик просит L2-роль (creator/author/contributor) + ставку
  - approverole — мастер одобряет: фиксация approved-ставки в сегменте через
    Capital::Segments::set_approved_rate (Story 1.8)
  - declinerole — мастер отклоняет с причиной (запись для аудита остаётся)
  - inviterole — мастер инициирует обратный поток, приглашает кандидата
  - acceptinvite — кандидат принимает инвайт → applied к сегменту
  - declinvite — кандидат отказывается (имя укорочено с declineinvite — лимит 12 chars)
  - requestrateu — пайщик просит обновить approved-ставку (тот же flow, RequestType::RATE_UPDATE);
    approverole с новой ставкой → новая approved в сегменте
- name declineinvite сокращена до declinvite (13 → 10 chars, eosio лимит 12)

Авторизация на текущем этапе — require_auth(coopname); backend проверяет
master-роль приглашающего/одобряющего перед отправкой action.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 17:03:35 +00:00
coopops 1f486ce48a feat(capital/segments): approved_rate_per_hour в сегменте + createcmmt через approved-ставку
Story 1.8 эпика «Благорост» (blago issue 51F-20).

PRD-D2: «approved_rate_per_hour хранится в сегменте; createcmmt использует
approved-ставку». До этого коммита createcmmt брал ставку из глобального
contributors.rate_per_hour — мастер компонента не мог дифференцировать.

Изменения:
- segments.hpp: поля approved_rate_per_hour, approved_hours_per_day (по умолчанию 0
  — fallback на глобальную ставку из contributors)
- segments.hpp::set_approved_rate(coopname, project_hash, username, rate, hours)
  — helper будет вызван из approverole (Story 1.7)
- createcmmt.cpp: если segment.approved_rate_per_hour.amount > 0 — используется
  approved; иначе contributor->rate_per_hour. Меняется и creator_base, и
  delta_amounts (через calculate_fact_generation_amounts)
- editcontrib (без изменения) — теперь не влияет на approved-ставки на
  сегментах: контракт берёт сегментные значения вперёд глобальных

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 17:00:43 +00:00
coopops 7ca2695121 feat(capital/expense): расходы программы — createpgexp/apprvpgexp/authpgexp/pgexppay/declpgexp + таблица progexpenses
Story 1.5 эпика «Благорост» (blago issue 51F-17).

Полный цикл целевого расхода программы «Благорост» (не привязанного к проекту):
от заявки председателя до подтверждения оплаты gateway. Источник средств —
program_expense_pool (Story 1.6), резерв снимается окончательно в pgexppay.

PRD-B1: «createprogexp/approveprogex/authprogexp/progexppay/declprogexp». Имена
действий сокращены под 12-символьный лимит EOSIO — семантика та же.

Изменения:
- expenses.hpp: struct program_expense + таблица progexpenses (scope coopname)
  + helper'ы create/get/set_program_approved/set_program_authorized/delete
- 5 новых cpp actions в expense_managment/program_expenses/:
  - createpgexp — резервирует в State::reserve_program_expense, создаёт запись
  - apprvpgexp — председатель → soviet::create_agenda с CAPITAL_RESOLVE_PROGRAM_EXPENSE
  - authpgexp — совет → ::Gateway::create_outpay (через Action::send), статус AUTHORIZED
  - pgexppay — gateway callback → consume_program_expense + Ledger2::apply PAY_EXPENSE + delete
  - declpgexp — отказ от coopname/_soviet/_gateway → release_program_expense + delete
- names.hpp: CREATE_PROGRAM_EXPENSE, AUTHORIZE_PROGRAM_EXPENSE, DECLINE_PROGRAM_EXPENSE,
  CONFIRM_PROGRAM_EXPENSE_PAYMENT, SovietActions::CAPITAL_RESOLVE_PROGRAM_EXPENSE

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 16:58:46 +00:00
coopops bbfc592357 feat(capital/state): program_expense_pool + topupprogexp
Story 1.6 эпика «Благорост» (blago issue 51F-18).

Программные расходы (Story 1.5) списываются из дедикейтнутого пула, а не из
аллокаций проектов. До этого коммита поля для такого пула в global_state не было.

Изменения:
- global_state.program_expense_pool — доступная сумма программы под расходы
- global_state.program_expense_reserved — зарезервированная под approved/authorized
  программные расходы (Story 1.5: createprogexp резервирует, declprogexp возвращает,
  exppaycnfrm списывает окончательно)
- State::topup_program_expense_pool / reserve_program_expense /
  release_program_expense / consume_program_expense — utility-функции для Story 1.5
- topupprogexp(coopname, amount) action — председатель переводит amount из
  global_available_invest_pool в program_expense_pool; деньги остаются на
  ledger2-кошельке BLAGOROST_FUND, меняется только разбивка по назначению

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 16:55:44 +00:00
coopops 18646f8353 feat(capital/expense): exppaycnfrm — проводка по фонду программы
Story 1.4 эпика «Благорост» (blago issue 51F-16).

Закрывает TODO в exppaycnfrm.cpp:35 — после подтверждения оплаты проектного
расхода списываем сумму с программного фонда «Благорост» в пул хозрасходов
из целевого финансирования.

Изменения:
- processes::capital::EXPENSE (p.cap.expns) — новый process_type для цикла
  целевого расхода (создание → одобрение → авторизация → outpay → подтверждение)
- operations::capital::PAY_EXPENSE (o.cap.expns) — TRANSFER BLAGOROST_FUND
  → SOV_EXPENSES, без Dr/Cr; параллельная бухпроводка Dr 26 / Cr 51 на
  расчётном делается gateway и здесь не дублируется
- запись 20 в OPERATION_REGISTRY (lib/core/ledger2/operations.hpp)
- get_expense_pay_memo() в Capital::Memo
- exppaycnfrm.cpp теперь после complete_expense вызывает Ledger2::apply
  до delete_expense (нужны expense.amount/username для проводки)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 16:54:00 +00:00
coopops e932260847 feat(capital/debt): markdebtoverd + due_at в debtpaycnfrm — статус просрочки
Story 1.3 эпика «Благорост» (blago issue 51F-15).

PRD-A3, FR-A11: «срок погашения 3 месяца, дальше — overdue, без блокировки операций».
Поле due_at и статус OVERDUE добавлены, helper sweep_overdue вычитан в action.

Изменения:
- статус OVERDUE — выставляется markdebtoverd для долгов с now > due_at
- поле debt.due_at (time_point_sec) — на 3 месяца (DEFAULT_DEBT_TERM_SECONDS = 90 дней)
  от даты подтверждения выплаты gateway
- helper debts.hpp::confirm_paid — атомарный переход pay_pending → paid с
  проставлением due_at и сбросом last_pay_error
- helper debts.hpp::sweep_overdue(coopname, batch_limit) — итерация по debts
  в scope, перевод paid→overdue для просроченных, не более batch_limit записей
- markdebtoverd(coopname) action — backend-cron вызовет раз в сутки (Эпик 4),
  batch=100 чтобы уложиться в 30s транзакции; повтор до 0 изменений
- mark_settled расширен: SETTLED доступен также из OVERDUE
- debtpaycnfrm теперь использует confirm_paid (раньше update_debt_status +
  только PAID, без due_at)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 16:51:13 +00:00
coopops b42394bfb4 feat(capital/debt): settledebt — прямое погашение займа деньгами
Story 1.2 эпика «Благорост» (blago issue 51F-14).

settledebt был stub'ом. Реализация по PRD-A3: должник или председатель
вносит сумму, ровно равную остатку долга — заём закрывается.

Изменения:
- сигнатура: settledebt(coopname, debt_hash, amount, statement) — раньше
  принимался username, но идентифицировать долг надо по hash (у одного
  пайщика может быть несколько активных)
- статус SETTLED — финальное состояние погашенного долга (как через
  деньги settledebt, так и через результат signact2)
- helper debts.hpp::mark_settled — атомарный переход paid → settled с
  пометкой даты погашения и memo
- get_settle_debt_memo() — описание проводки в ledger2
- авторизация: has_auth(debt.username) || has_auth(coopname) — раньше
  было только coopname, что мешало пайщику самому погасить долг
- проводка REPAY (Dr 80 / Cr 58) на полную сумму долга
- contributors.decrease_debt_amount на ту же сумму

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 16:48:42 +00:00
coopops ff8f60e4b2 feat(capital/debt): debtpaydcln + статус PAY_PENDING — откат outpay без удаления долга
Story 1.1 эпика «Благорост» (blago issue 51F-13).

PRD-A3 говорит «отказ оплаты по реквизитам не приводит к удалению долга».
До правки debtpaydcln был stub'ом, статусы не различали «в полёте до gateway»
и «авторизован, ждёт повторной отправки».

Изменения в lifecycle долга:
- статус PAY_PENDING — долг отдан в gateway на outpay
- поле last_pay_error — текст ошибки от gateway (пусто, пока нет отказов)
- утилиты debts.hpp: start_pay() (approved/authorized → PAY_PENDING + сохранение
  решения совета + сброс last_pay_error) и mark_pay_declined() (PAY_PENDING →
  AUTHORIZED + last_pay_error)

Касается actions:
- debtauthcnfr теперь через start_pay() переводит долг в PAY_PENDING и
  параллельно фиксирует document решения совета как authorization
- debtpaycnfrm требует PAY_PENDING (а не AUTHORIZED) перед PAID
- debtpaydcln вызывает mark_pay_declined() — повторная outpay из AUTHORIZED
  доступна без новой авторизации совета

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 16:46:11 +00:00
42 changed files with 1975 additions and 218 deletions
@@ -0,0 +1,174 @@
import { describe, it } from 'vitest'
/**
* Расширение Capital Test под эпик «Благорост» (Story 1.11).
*
* Существующий capital.test.ts (~2650 строк) покрывает базовые happy-paths.
* Новые ветви Story 1.1–1.10 описаны здесь отдельно, чтобы не раздувать
* монолитный файл. it.todo() — каркас под последующее наполнение helpers'ами
* по аналогии с processDebt / processFundProgram / etc.
*
* Helpers, которые потребуются (часть из них появится в Эпиках 2–5):
* - processDebtDeclinePay (gateway → debtpaydcln)
* - processSettleDebt (settledebt деньгами)
* - processMarkOverdue (markdebtoverd batch)
* - processTopupProgramExpense (chairman → topupprogexp)
* - processProgramExpense (createpgexp → apprvpgexp → authpgexp → pgexppay)
* - processDeclineProgramExpense (declpgexp на разных фазах)
* - processRequestRole / processApproveRole / processDeclineRole
* - processInviteRole / processAcceptInvite / processDeclineInvite
* - processRequestRateUpdate
*/
describe('CAPITAL — расширения «Благорост» (Эпик 1)', () => {
describe('A — Займы', () => {
it.todo(
'Story 1.1: debtpaydcln возвращает PAY_PENDING → AUTHORIZED с last_pay_error и не удаляет запись'
)
it.todo(
'Story 1.1: debtpayretry повторно отправляет outpay из AUTHORIZED без новой авторизации совета'
)
it.todo(
'Story 1.1: debtpayretry отвергается если last_pay_error пустой (платёж ещё не пробовали)'
)
it.todo(
'Story 1.2: settledebt на полную сумму → SETTLED, проводка REPAY 30000, contributors.debt_amount уменьшается'
)
it.todo(
'Story 1.2: settledebt доступен и для OVERDUE, не только PAID'
)
it.todo(
'Story 1.2: settledebt отвергается при amount ≠ debt.amount'
)
it.todo(
'Story 1.3: debtpaycnfrm проставляет due_at = now + 1 год (фиксированный срок, решение 2026-05-19)'
)
it.todo(
'Story 1.13: closedebt — компонент cancelled → автоматическое закрытие долга: o.cap.crtnma (Дт 04 / Кт 08, TRANSFER GENERATOR_FUND → NMA, создание НМА из коммитов-обеспечения) + o.cap.dbtwrf (Дт 80 / Кт 58, BURN LOAN_ISSUED, write-off долга); сегмент уменьшен на сумму займа; долг → WRITEOFF; без подписи пайщика и без сбора совета (решение 2026-05-19)'
)
it.todo(
'Story 1.13: closedebt отвергается, если родительский компонент не в статусе cancelled'
)
it.todo(
'Story 1.13: closedebt отвергается, если долг не в статусе paid/overdue'
)
it.todo(
'Story 1.13: closedebt — глобальный contributors.debt_amount пайщика уменьшен; запись в loan.debts удалена; loan.summaries.total опущен'
)
it.todo(
'Story 1.3: markdebtoverd переводит PAID → OVERDUE для долгов с now > due_at; PAID с due_at в будущем не трогает'
)
it.todo(
'Story 1.3: markdebtoverd обрабатывает не более batch_limit (25) записей за вызов'
)
})
describe('A2 — Централизованный учёт займов (loan-контракт + счётчик сегмента)', () => {
it.todo(
'Story 1.12: loan-контракт изолирован — loan.debts хранит только (coopname, username, debt_hash, amount, repaid_at) + source_contract; project_hash отсутствует'
)
it.todo(
'Story 1.12: debtpaycnfrm создаёт запись в loan.debts через inline createdebt и поднимает loan.summaries.total пайщика'
)
it.todo(
'Story 1.12: settledebt в capital удаляет запись из loan.debts через inline settledebt и опускает loan.summaries.total'
)
it.todo(
'Story 1.12: debt в loan.debts помечается source_contract = вызвавший контракт (capital для capital→loan)'
)
it.todo(
'Story 1.12: debtpaycnfrm увеличивает active_debts_count на сегменте (binary_extension), settledebt — уменьшает'
)
it.todo(
'Story 1.12: signact2 с N≤10 займами на проекте — все погашаются inline-actions в loan; active_debts_count = 0'
)
it.todo(
'Story 1.12: signact2 с >10 займами на проекте (active_debts_count > 10) — fail сразу по счётчику, без обхода byprojhash'
)
it.todo(
'Story 1.12: loan.createdebt от аккаунта вне contracts_whitelist — fail (только capital/marketplace/...)'
)
})
describe('B — Расходы программы', () => {
it.todo(
'Story 1.4: exppaycnfrm после complete_expense делает Ledger2 PAY_EXPENSE (BLAGOROST_FUND → SOV_EXPENSES)'
)
it.todo(
'Story 1.5/1.6: topupprogexp пополняет program_expense_pool из global_available_invest_pool'
)
it.todo(
'Story 1.5: createpgexp резервирует amount; вторая createpgexp на превышение — fail'
)
it.todo(
'Story 1.5: cycle createpgexp → apprvpgexp → authpgexp → pgexppay = paid + delete + Ledger2 PAY_EXPENSE'
)
it.todo(
'Story 1.5: declpgexp на любом из {created, approved, authorized} возвращает reserved → pool, удаляет запись'
)
})
describe('D — Двухуровневые допуски и approved-ставка (бездокументарная схема)', () => {
it.todo(
'Story 1.7: requestrole без statement создаёт rolerequests со статусом PENDING; approverole без master_decision фиксирует approved_rate'
)
it.todo(
'Story 1.7: requestrole с role вне Role::{CREATOR, AUTHOR, MASTER} — fail "Допустимые роли заявки..."'
)
it.todo(
'Story 1.7: requestrateu создаёт запись с role = Role::NONE и request_type = RequestType::RATE_UPDATE'
)
it.todo(
'Story 1.8: createcmmt с approved_rate=1200 и creator_hours=4 → generation_amounts на 4800, не на 6000 (глобальная)'
)
it.todo(
'Story 1.7: editcontrib с новой глобальной ставкой не меняет approved_rate_per_hour на сегменте'
)
it.todo(
'Story 1.7: inviterole без statement → acceptinvite применяет инвайт-ставку к сегменту; declinvite оставляет сегмент без approved-rate'
)
it.todo(
'Story 1.7: declinerole оставляет запись со статусом DECLINED + reason, сегмент не трогает'
)
it.todo(
'Story 1.7: requestrateu + approverole с новой ставкой обновляет approved_rate в сегменте; role.apply_role_to_project не вызывается'
)
})
describe('E — Event ridges (Story 1.10)', () => {
it.todo(
'Все новые actions вызывают require_recipient для (автор события, обратная сторона, председатель)'
)
})
})
@@ -0,0 +1,90 @@
/**
* @brief Закрывает долг созданием НМА кооператива из коммитов-обеспечения.
*
* Срабатывает, когда родительский компонент проекта явно перешёл в `cancelled`:
* проект до акта-2 не дойдёт, возврат через `signact2` невозможен.
* Коммиты-обеспечение, накопленные пайщиком на сегменте проекта-компонента
* (`w.cap.gen`), формируют НМА кооператива на отдельном кошельке `w.cap.nma`,
* а обязательство пайщика по займу списывается (write-off). Никаких санкций
* к пайщику не применяется, совет не собирается, подпись пайщика не требуется.
*
* Двухоперационная свёртка ledger2:
* - o.cap.crtnma — TRANSFER w.cap.gen → w.cap.nma, Дт 04 / Кт 08.
* Зеркалит o.cap.accept, но без участия пайщика: НМА из
* коммитов-обеспечения долга идут в имущество кооператива,
* а не на share программы пайщика.
* - o.cap.dbtwrf — BURN w.cap.loan, Дт 80 / Кт 58.
* Списание долга (write-off). Зеркало o.cap.repay, но share
* пайщика не пополняется — долг закрыт имущественно.
*
* Решение 2026-05-19 (Игорь Смуров / Алексей Муравьёв): срок займа фиксированный
* (1 год от выплаты), но при явной отмене компонента закрытие происходит
* досрочно — без ожидания due_at.
*
* @param coopname Наименование кооператива (scope)
* @param debt_hash Хеш закрываемого займа
* @ingroup public_actions
* @ingroup public_capital_actions
*
* @note Авторизация только от @p coopname. Action идёт из backend кооператива,
* отслеживающего переход компонента в `cancelled`.
*/
void capital::closedebt(name coopname, checksum256 debt_hash) {
require_auth(coopname);
// 1. Получить долг
auto exist_debt = Capital::Debts::get_debt_or_fail(coopname, debt_hash);
// 2. Долг должен быть активным (выплачен, не погашен)
eosio::check(exist_debt.status == Capital::Debts::Status::PAID
|| exist_debt.status == Capital::Debts::Status::OVERDUE,
"Закрытие долга допустимо для активного (paid) или просроченного (overdue) займа");
// 3. Guard на статус родительского компонента: только `cancelled` (отменён).
// Если компонент завершён нормально (`finalized`/`result`) — возврат идёт
// через signact2 → o.cap.repay, не сюда.
auto project = Capital::Projects::get_project_or_fail(coopname, exist_debt.project_hash);
eosio::check(project.status == Capital::Projects::Status::CANCELLED,
"Закрытие долга через НМА допустимо только при cancelled статусе компонента проекта");
// 4. Сегмент пайщика на компоненте
auto exist_segment = Capital::Segments::get_segment_or_fail(coopname,
exist_debt.project_hash, exist_debt.username, "Сегмент пайщика не найден");
// 5. Контрибьютор для обновления глобального долга
auto contributor = Capital::Contributors::get_contributor(coopname, exist_debt.username);
eosio::check(contributor.has_value(), "Контрибьютор не найден");
auto memo = Capital::Memo::get_debt_memo(exist_debt.username);
// 6. o.cap.crtnma: TRANSFER GENERATOR_FUND → NMA, Дт 04 / Кт 08.
// Из коммитов-обеспечения долга создаётся НМА кооператива.
Ledger2::apply(_capital, coopname, operations::capital::CREATE_NMA,
exist_debt.amount, exist_debt.username, debt_hash, memo);
// 7. o.cap.dbtwrf: BURN LOAN_ISSUED, Дт 80 / Кт 58.
// Финансовое вложение кооператива в пайщика списано (write-off); share пайщика
// не пополняется — долг закрыт имущественно.
Ledger2::apply(_capital, coopname, operations::capital::DEBT_WRITEOFF,
exist_debt.amount, exist_debt.username, debt_hash, memo);
// 8. Обновить агрегаты сегмента: -debt_amount, -active_debts_count.
Capital::Segments::decrease_debt_amount(coopname, exist_segment.id, exist_debt.amount);
Capital::Segments::decrease_active_debts_count(coopname, exist_debt.project_hash, exist_debt.username);
// 9. Уменьшить used_for_compensation в проекте (зеркало createdebt).
Capital::Projects::subtract_used_for_compensation(coopname, project.id, exist_debt.amount);
// 10. Глобальный долг контрибьютора уменьшаем (как в settledebt).
Capital::Contributors::decrease_debt_amount(coopname, contributor->id, exist_debt.amount);
// 11. Запись долга → WRITEOFF (для аудита).
Capital::Debts::mark_writeoff(coopname, exist_debt.id);
// 12. Централизованный реестр в loan-контракте: уменьшаем сводную, удаляем запись.
Loan::settle_debt(_capital, coopname, exist_debt.username, debt_hash, exist_debt.amount);
// 13. Уведомить пайщика и кооператив.
require_recipient(exist_debt.username);
require_recipient(coopname);
}
@@ -19,11 +19,11 @@ void capital::debtauthcnfr(eosio::name coopname, checksum256 debt_hash, document
// Получаем долг
auto exist_debt = Capital::Debts::get_debt_or_fail(coopname, debt_hash);
// Обновляем статус долга
Capital::Debts::update_debt_status(coopname, exist_debt.id, Capital::Debts::Status::AUTHORIZED,
_capital, decision);
// Фиксируем решение совета и переводим долг в pay_pending —
// outpay уходит в gateway, дальше debtpaycnfrm (успех) или debtpaydcln (отказ).
Capital::Debts::start_pay(coopname, exist_debt.id, decision, _capital);
// создаём объект исходящего платежа в gateway с коллбэком после обработки
::Gateway::create_outcome(
_capital,
@@ -18,12 +18,11 @@ void capital::debtpaycnfrm(name coopname, checksum256 debt_hash) {
auto contributor = Capital::Contributors::get_contributor(coopname, exist_debt.username);
eosio::check(contributor.has_value(), "Контрибьютор не найден");
// Проверяем что долг в статусе 'authorized' (готов к выплате)
eosio::check(exist_debt.status == Capital::Debts::Status::AUTHORIZED,
"Долг должен быть в статусе 'authorized' для подтверждения оплаты");
// Обновляем статус долга на PAID
Capital::Debts::update_debt_status(coopname, exist_debt.id, Capital::Debts::Status::PAID, _gateway);
// Переводим долг в PAID + проставляем due_at = now + 1 год (срок погашения,
// решение 2026-05-19 — срок зафиксирован, не привязан к календарному
// сроку проекта).
// confirm_paid внутри проверяет статус pay_pending и атомарно сбрасывает last_pay_error.
Capital::Debts::confirm_paid(coopname, exist_debt.id, _gateway);
// Выдача пайщику беспроцентного займа: Dr 58 / Cr 51, ISSUE LOAN_ISSUED (4051).
// Семантика момента — деньги ушли пайщику, у кооператива появилось финансовое
@@ -34,5 +33,17 @@ void capital::debtpaycnfrm(name coopname, checksum256 debt_hash) {
// Увеличиваем долг contributor (теперь долг активен и должен быть погашен через внесение результата)
Capital::Contributors::increase_debt_amount(coopname, contributor->id, exist_debt.amount);
};
// Локальный счётчик займов пайщика на проекте этого сегмента (+1).
// Используется в signact2 как лимит «10 займов на проект» без обхода byprojhash.
Capital::Segments::increase_active_debts_count(coopname, exist_debt.project_hash, exist_debt.username);
// Централизованный учёт займа в контракте loan — loan-контракт независим от capital,
// знает только (coopname, username, debt_hash, amount, repaid_at) + source_contract
// (заполняется самим loan::createdebt из payer-авторизации). Локальная привязка к
// проекту/программе остаётся в Capital::Debts (поле project_hash).
// due_at уже проставлен в confirm_paid, читаем обновлённое значение.
auto fresh_debt = Capital::Debts::get_debt_or_fail(coopname, debt_hash);
Loan::create_debt(_capital, coopname, exist_debt.username,
debt_hash, fresh_debt.due_at, exist_debt.amount);
};
@@ -1,23 +1,27 @@
/**
* @brief Отклоняет оплату долга
* Отклоняет оплату долга и удаляет его из базы данных:
* - Получает долг
* - Удаляет долг из базы данных с указанием причины
* @param coopname Наименование кооператива
* @param debt_hash Хеш долга для отклонения оплаты
* @param reason Причина отклонения оплаты долга
* @brief Отклоняет оплату долга по реквизитам (gateway возвращает ошибку перечисления).
*
* Решение совета уже принято в @ref capital::debtauthcnfr, долг НЕ удаляется и НЕ откатывается
* в approved. Запись остаётся, статус возвращается из @c pay_pending в @c authorized
* с заполненным полем @c last_pay_error = @p reason. Из @c authorized доступна повторная
* отправка в gateway без новой авторизации совета.
*
* @param coopname Наименование кооператива
* @param debt_hash Хеш долга, для которого пришёл отказ от gateway
* @param reason Текстовая причина отказа (заполняется gateway-оператором)
* @ingroup public_actions
* @ingroup public_capital_actions
*
* @note Авторизация требуется от аккаунта: @p _gateway
*/
void capital::debtpaydcln(name coopname, checksum256 debt_hash, std::string reason) {
require_auth(_gateway);
//TODO: отказ в оплате по реквизитам не должен приводить к удалению и откату т.к. решение о займе уже принято
// // Получаем долг
// auto exist_debt = Capital::Debts::get_debt_or_fail(coopname, debt_hash);
// // Удаляем долг
// Capital::Debts::delete_debt(coopname, debt_hash);
};
auto exist_debt = Capital::Debts::get_debt_or_fail(coopname, debt_hash);
Capital::Debts::mark_pay_declined(coopname, exist_debt.id, reason, _capital);
// event ridge: должник и кооператив получают уведомление об отказе оплаты.
require_recipient(exist_debt.username);
require_recipient(coopname);
};
@@ -0,0 +1,40 @@
/**
* @brief Повторная отправка платежа по займу в gateway после предыдущего отказа.
*
* Допустимо только из AUTHORIZED с непустым last_pay_error (то есть после debtpaydcln).
* Использует уже сохранённое решение совета — новой авторизации не требуется.
* Переводит займ в PAY_PENDING, очищает last_pay_error и создаёт новый outpay.
*
* @param coopname Кооператив
* @param debt_hash Хеш займа для повторной отправки
* @ingroup public_actions
* @ingroup public_capital_actions
*
* @note Авторизация требуется от @p coopname (председатель/backend). Пайщик подпись не ставит.
*/
void capital::debtpayretry(eosio::name coopname, checksum256 debt_hash) {
require_auth(coopname);
auto exist_debt = Capital::Debts::get_debt_or_fail(coopname, debt_hash);
eosio::check(exist_debt.status == Capital::Debts::Status::AUTHORIZED,
"Повторная отправка платежа возможна только из статуса authorized");
eosio::check(!exist_debt.last_pay_error.empty(),
"Повторная отправка платежа возможна только после предыдущего отказа (debtpaydcln)");
Capital::Debts::start_pay(coopname, exist_debt.id, exist_debt.authorization, _capital);
::Gateway::create_outcome(
_capital,
coopname,
exist_debt.username,
exist_debt.debt_hash,
exist_debt.amount,
_capital,
Names::Capital::CONFIRM_DEBT_PAYMENT,
Names::Capital::DECLINE_DEBT
);
// event ridge: должник и председатель видят повторную отправку.
require_recipient(exist_debt.username);
require_recipient(coopname);
}
@@ -0,0 +1,25 @@
/**
* @brief Помечает просроченные займы статусом OVERDUE.
*
* Долг считается просроченным, когда current_time_point > debt.due_at
* (поле due_at проставляется в debtpaycnfrm на 1 год от даты выплаты;
* срок зафиксирован — решение 2026-05-19).
* Из OVERDUE долг можно так же закрыть settledebt или signact2 как из PAID.
*
* Action рассчитан на запуск из backend через @Cron (см. Эпик 4) — раз в сутки.
* Один вызов помечает не более 25 записей чтобы уложиться в 0.5-секундный
* лимит транзакции EOSIO; backend повторяет до тех пор, пока action не
* помечает 0 — это сигнал, что в текущем scope больше нечего переводить.
*
* @param coopname Кооператив (scope таблицы debts)
* @ingroup public_actions
* @ingroup public_capital_actions
*
* @note Авторизация требуется от аккаунта: @p coopname (председатель / системный).
*/
void capital::markdebtoverd(name coopname) {
require_auth(coopname);
constexpr uint32_t MAX_PER_BATCH = 25;
Capital::Debts::sweep_overdue(coopname, MAX_PER_BATCH);
}
@@ -1,22 +1,57 @@
/**
* @brief Погашает долг участника
* Погашает долг участника в системе кооператива:
* - Проверяет подлинность заявления о погашении
* - Обрабатывает погашение долга (TODO: реализация)
* @param coopname Наименование кооператива
* @param username Наименование пользователя-должника
* @param amount Сумма погашения долга
* @param statement Заявление о погашении долга
* @brief Погашает активный заём пайщика.
*
* Прямой канал погашения (без сдачи результата): сумма, ровно равная остатку долга,
* закрывает заём. В ledger2 фиксируется операция REPAY (Dr 80 / Cr 58),
* глобальный долг пайщика в contributors.debt_amount уменьшается, запись долга
* переходит в SETTLED.
*
* Альтернативный путь закрытия — через подписание акта-2 по результату (см. signact2.cpp),
* там та же операция REPAY вызывается на сумму result->debt_amount.
*
* @param coopname Наименование кооператива
* @param debt_hash Хеш погашаемого долга
* @param amount Сумма погашения (должна совпасть с debt.amount — полное закрытие)
* @param statement Документ-заявление на погашение (SettleDebtStatement)
* @ingroup public_actions
* @ingroup public_capital_actions
* @note Авторизация требуется от аккаунта: @p coopname
*
* @note Авторизация только от @p coopname. Пайщик транзакции не подписывает — backend кооператива
* подставляет себя.
*/
void capital::settledebt(name coopname, name username, eosio::asset amount, document2 statement){
void capital::settledebt(name coopname, checksum256 debt_hash, eosio::asset amount, document2 statement) {
require_auth(coopname);
//TODO: ?
// // Получаем долг
// auto exist_debt = Capital::Debts::get_debt_or_fail(coopname, debt_hash);
}
auto exist_debt = Capital::Debts::get_debt_or_fail(coopname, debt_hash);
eosio::check(exist_debt.status == Capital::Debts::Status::PAID
|| exist_debt.status == Capital::Debts::Status::OVERDUE,
"Погашение допустимо для активного (paid) или просроченного (overdue) займа");
eosio::check(amount.is_valid(), "Сумма погашения некорректна");
eosio::check(amount.symbol == exist_debt.amount.symbol,
"Символ суммы погашения не совпадает с символом долга");
eosio::check(amount == exist_debt.amount,
"Сумма погашения должна полностью покрывать остаток долга");
auto contributor = Capital::Contributors::get_contributor(coopname, exist_debt.username);
eosio::check(contributor.has_value(), "Контрибьютор не найден");
// Возврат займа пайщиком: Dr 80 / Cr 58 — закрываем финансовое вложение в участника.
auto memo = Capital::Memo::get_settle_debt_memo(exist_debt.username, debt_hash);
Ledger2::apply(_capital, coopname, operations::capital::REPAY, amount,
exist_debt.username, debt_hash, memo);
Capital::Contributors::decrease_debt_amount(coopname, contributor->id, amount);
Capital::Debts::mark_settled(coopname, exist_debt.id, memo, _capital);
// Локальный счётчик займов пайщика на сегменте этого проекта (-1).
Capital::Segments::decrease_active_debts_count(coopname, exist_debt.project_hash, exist_debt.username);
// Централизованный учёт: уменьшаем сводную и удаляем запись из loan.debts через inline action.
Loan::settle_debt(_capital, coopname, exist_debt.username, debt_hash, amount);
// event ridge: и должник, и председатель видят факт погашения.
require_recipient(exist_debt.username);
require_recipient(coopname);
}
@@ -28,9 +28,16 @@ void capital::exppaycnfrm(eosio::name coopname, checksum256 expense_hash) {
// Обновляем used_expense_pool в проекте
Capital::Projects::complete_expense(coopname, project.id, expense.amount);
// Списываем сумму расхода с программного фонда в пул хозрасходов:
// TRANSFER BLAGOROST_FUND → SOV_EXPENSES (без Dr/Cr — оба под счётом 80).
// Бухгалтерская проводка Dr 26 / Cr 51 на расчётном фиксируется gateway отдельно.
auto memo = Capital::Memo::get_expense_pay_memo(expense_hash);
Ledger2::apply(_capital, coopname,
operations::capital::PAY_EXPENSE,
expense.amount, expense.username,
expense_hash, memo);
// Удаляем запись расхода
Capital::Expenses::delete_expense(coopname, expense.id);
//TODO: здесь должна быть проводка по фонду
}
@@ -0,0 +1,41 @@
/**
* @brief Одобряет программный расход председателем и отправляет в совет.
*
* @param coopname Кооператив
* @param approver Председатель (или доверенный)
* @param expense_hash Хеш расхода
* @param approved_statement Принятая записка (с подписью председателя)
* @ingroup public_actions
* @ingroup public_capital_actions
*
* @note Авторизация требуется от @p coopname.
*/
void capital::apprvpgexp(name coopname, name approver, checksum256 expense_hash,
document2 approved_statement) {
require_auth(coopname);
verify_document_or_fail(approved_statement);
auto expense = Capital::Expenses::get_program_expense_or_fail(coopname, expense_hash);
eosio::check(expense.status == Capital::Expenses::Status::CREATED,
"Программный расход должен быть в статусе 'created'");
Capital::Expenses::set_program_approved(coopname, expense.id, approved_statement);
::Soviet::create_agenda(
_capital,
coopname,
expense.username,
Names::SovietActions::CAPITAL_RESOLVE_PROGRAM_EXPENSE,
expense_hash,
_capital,
Names::Capital::AUTHORIZE_PROGRAM_EXPENSE,
Names::Capital::DECLINE_PROGRAM_EXPENSE,
expense.expense_statement,
std::string("")
);
// event ridge: автор расхода и approver (председатель/доверенный) уведомляются.
require_recipient(expense.username);
require_recipient(approver);
}
@@ -0,0 +1,37 @@
/**
* @brief Авторизует программный расход советом и создаёт исходящий платёж в gateway.
*
* @param coopname Кооператив
* @param expense_hash Хеш расхода
* @param authorization Документ-решение совета
* @ingroup public_actions
* @ingroup public_capital_actions
*
* @note Авторизация требуется от @p _soviet.
*/
void capital::authpgexp(name coopname, checksum256 expense_hash, document2 authorization) {
require_auth(_soviet);
auto expense = Capital::Expenses::get_program_expense_or_fail(coopname, expense_hash);
eosio::check(expense.status == Capital::Expenses::Status::APPROVED,
"Программный расход должен быть в статусе 'approved'");
Capital::Expenses::set_program_authorized(coopname, expense.id, authorization);
Action::send<createoutpay_interface>(
_gateway,
Names::External::CREATE_OUTPAY,
_capital,
coopname,
expense.username,
expense.expense_hash,
expense.amount,
_capital,
Names::Capital::CONFIRM_PROGRAM_EXPENSE_PAYMENT,
Names::Capital::DECLINE_PROGRAM_EXPENSE
);
// event ridge: автор расхода и председатель уведомляются о решении совета.
require_recipient(expense.username);
require_recipient(coopname);
}
@@ -0,0 +1,39 @@
/**
* @brief Создаёт целевой программный расход и резервирует средства в state.
*
* Программный расход — целевое списание программы «Благорост», не привязанное к проекту.
* Источник средств — пул @c program_expense_pool, наполняемый через @ref capital::topupprogexp.
*
* @param coopname Кооператив
* @param expense_hash Хеш расхода (анкер процесса p.cap.expns)
* @param creator Инициатор (председатель / казначей; авторизуется через coopname)
* @param amount Сумма
* @param description Описание (для отчётности)
* @param statement Документ-заявление (ProgramExpenseStatement)
* @ingroup public_actions
* @ingroup public_capital_actions
*
* @note Авторизация требуется от @p coopname.
*/
void capital::createpgexp(name coopname, checksum256 expense_hash, name creator,
eosio::asset amount, std::string description, document2 statement) {
require_auth(coopname);
verify_document_or_fail(statement);
Wallet::validate_asset(amount);
eosio::check(amount.amount > 0, "Сумма программного расхода должна быть положительной");
auto exist = Capital::get_program_expense(coopname, expense_hash);
eosio::check(!exist.has_value(), "Программный расход с указанным хэшем уже существует");
// Резервируем средства в пуле программных расходов сразу при создании,
// чтобы две параллельные заявки не превысили доступный пул.
Capital::State::reserve_program_expense(coopname, amount);
Capital::Expenses::create_program_expense(coopname, expense_hash, creator,
amount, description, statement);
// event ridge: создатель расхода и председатель видят появление заявки.
require_recipient(creator);
require_recipient(coopname);
}
@@ -0,0 +1,37 @@
/**
* @brief Отклоняет программный расход на любом этапе до подтверждения оплаты.
*
* Допустимо из created (отказ председателя), approved (отказ совета через callback)
* и authorized (отказ gateway по реквизитам). Возвращает зарезервированную сумму
* обратно в program_expense_pool и удаляет запись.
*
* @param coopname Кооператив
* @param expense_hash Хеш расхода
* @param reason Причина отказа
* @ingroup public_actions
* @ingroup public_capital_actions
*
* @note Авторизация требуется от @p coopname (если отказ инициирован председателем),
* @p _soviet (если совет отказал) или @p _gateway (если выпуск платежа провалился).
*/
void capital::declpgexp(name coopname, checksum256 expense_hash, std::string reason) {
eosio::check(
eosio::has_auth(coopname) || eosio::has_auth(_soviet) || eosio::has_auth(_gateway),
"Отклонить программный расход может только председатель, совет или gateway"
);
auto expense = Capital::Expenses::get_program_expense_or_fail(coopname, expense_hash);
eosio::check(
expense.status == Capital::Expenses::Status::CREATED ||
expense.status == Capital::Expenses::Status::APPROVED ||
expense.status == Capital::Expenses::Status::AUTHORIZED,
"Программный расход уже завершён (paid/declined)"
);
Capital::State::release_program_expense(coopname, expense.amount);
Capital::Expenses::delete_program_expense(coopname, expense.id);
// event ridge: автор расхода и председатель уведомляются об отклонении.
require_recipient(expense.username);
require_recipient(coopname);
}
@@ -0,0 +1,38 @@
/**
* @brief Подтверждает оплату программного расхода (callback от gateway).
*
* Снимает резерв с @c program_expense_reserved, делает ledger2-проводку
* @ref operations::capital::PAY_EXPENSE (TRANSFER BLAGOROST_FUND → SOV_EXPENSES)
* и удаляет запись расхода — финал процесса @c p.cap.expns.
*
* @param coopname Кооператив
* @param expense_hash Хеш расхода
* @ingroup public_actions
* @ingroup public_capital_actions
*
* @note Авторизация требуется от @p _gateway.
*/
void capital::pgexppay(name coopname, checksum256 expense_hash) {
require_auth(_gateway);
auto expense = Capital::Expenses::get_program_expense_or_fail(coopname, expense_hash);
eosio::check(expense.status == Capital::Expenses::Status::AUTHORIZED,
"Программный расход должен быть в статусе 'authorized' для подтверждения оплаты");
// Окончательное списание из резерва (деньги ушли gateway).
Capital::State::consume_program_expense(coopname, expense.amount);
// Бухгалтерская/кошельковая проводка.
auto memo = Capital::Memo::get_expense_pay_memo(expense_hash);
Ledger2::apply(_capital, coopname,
operations::capital::PAY_EXPENSE,
expense.amount, expense.username,
expense_hash, memo);
// Запись больше не нужна как ожидающая — закрываем процесс.
Capital::Expenses::delete_program_expense(coopname, expense.id);
// event ridge: автор расхода и председатель уведомляются о факте оплаты.
require_recipient(expense.username);
require_recipient(coopname);
}
@@ -0,0 +1,21 @@
/**
* @brief Пополняет пул программных расходов из общего инвестиционного пула программы.
*
* Программные расходы — это целевые списания «Благорост», не аллоцированные ни в один проект
* (хозяйственные расходы программы, оплата инфраструктуры программы и т.п.). Прежде чем
* создавать запрос на программный расход, председатель пополняет дедикейтнутый пул
* @c program_expense_pool из общего @c global_available_invest_pool — деньги остаются
* на том же ledger2-кошельке @c BLAGOROST_FUND, меняется только разбивка по назначению.
*
* @param coopname Кооператив (scope state)
* @param amount Сумма пополнения (символ строго совпадает с программой)
* @ingroup public_actions
* @ingroup public_capital_actions
*
* @note Авторизация требуется от аккаунта: @p coopname (председатель).
*/
void capital::topupprogexp(name coopname, eosio::asset amount) {
require_auth(coopname);
Capital::State::topup_program_expense_pool(coopname, amount);
}
@@ -47,11 +47,22 @@ void capital::createcmmt(eosio::name coopname, eosio::name username, checksum256
eosio::check(creator_hours > 0, "Только положительная сумма часов создателя");
eosio::check(creator_hours <= 1000000, "Превышено максимальное количество часов (1 млн)");
// Если на сегменте уже утверждена мастером approved-ставка (Story 1.7/1.8) — берём её,
// иначе fallback на глобальную rate_per_hour из contributors. Это позволяет мастеру
// компонента дифференцировать ставку по проекту без изменения договора УХД.
auto segment_opt = Capital::Segments::get_segment(coopname, project_hash, username);
eosio::asset effective_rate = contributor->rate_per_hour;
if (segment_opt.has_value()
&& segment_opt->approved_rate_per_hour.has_value()
&& segment_opt->approved_rate_per_hour->amount > 0) {
effective_rate = *segment_opt->approved_rate_per_hour;
}
// считаем сумму фактических затрат создателя на основе ставки в час и потраченного времени
eosio::asset creator_base = contributor -> rate_per_hour * creator_hours;
eosio::asset creator_base = effective_rate * creator_hours;
// Вычисляем фактическое изменение сумм генерации
auto delta_amounts = Capital::Core::Generation::calculate_fact_generation_amounts(contributor -> rate_per_hour, creator_hours);
auto delta_amounts = Capital::Core::Generation::calculate_fact_generation_amounts(effective_rate, creator_hours);
// Создаем коммит без отправки на аппрув председателю
// Одобрение осуществляется мастером проекта через approvecmmt
@@ -0,0 +1,34 @@
/**
* @brief Кандидат принимает инвайт от мастера: ставка/часы из заявки становятся
* approved-параметрами сегмента.
*
* @param coopname Кооператив
* @param request_hash Хеш инвайт-заявки
* @ingroup public_actions
* @ingroup public_capital_actions
*
* @note Авторизация требуется от @p coopname (backend подставляет кандидата).
*/
void capital::acceptinvite(name coopname, checksum256 request_hash) {
require_auth(coopname);
auto req = Capital::RoleRequests::get_role_request_or_fail(coopname, request_hash);
eosio::check(req.direction == Capital::RoleRequests::Direction::INVITE,
"Принять можно только инвайт");
Capital::RoleRequests::approve(coopname, req.id, req.rate_per_hour, req.hours_per_day);
Capital::Segments::set_approved_rate(
coopname, req.project_hash, req.username, req.rate_per_hour, req.hours_per_day
);
// Подключаем кандидата к проекту по приглашённой роли.
if (req.request_type == Capital::RoleRequests::RequestType::ROLE) {
Capital::RoleRequests::apply_role_to_project(
coopname, req.project_hash, req.username, req.role
);
}
// event ridge: кандидат и мастер видят принятие инвайта.
require_recipient(req.username);
require_recipient(req.master);
}
@@ -0,0 +1,46 @@
/**
* @brief Мастер компонента одобряет заявку на L2-допуск либо обновление approved-ставки.
*
* Фиксирует approved-ставку и норматив часов в сегменте через
* @ref Capital::Segments::set_approved_rate — далее `createcmmt` берёт их.
* Сегмент должен уже существовать (создаётся при подписании приложения договора УХД
* на этот проект через apprvappndx — L1-допуск).
*
* Бездокументарная схема: решение фиксируется только подписью транзакции
* (require_auth coopname), документ-решение мастера не передаётся.
*
* @param coopname Кооператив
* @param request_hash Хеш заявки (requestrole либо requestrateu)
* @param approved_rate Утверждённая мастером ставка часа (≠ запрошенной — допустимо)
* @param approved_hours Утверждённая норма часов
* @ingroup public_actions
* @ingroup public_capital_actions
*/
void capital::approverole(name coopname, checksum256 request_hash,
eosio::asset approved_rate, uint64_t approved_hours) {
require_auth(coopname);
Wallet::validate_asset(approved_rate);
eosio::check(approved_rate.amount > 0, "Утверждённая ставка должна быть положительной");
eosio::check(approved_hours > 0 && approved_hours <= 8, "Норма часов — от 1 до 8");
auto req = Capital::RoleRequests::get_role_request_or_fail(coopname, request_hash);
Capital::RoleRequests::approve(coopname, req.id, approved_rate, approved_hours);
Capital::Segments::set_approved_rate(
coopname, req.project_hash, req.username, approved_rate, approved_hours
);
// Только для ROLE-заявок: подключаем участника к проекту по заявленной роли.
// RATE_UPDATE роль не меняет, только ставку — inline action не нужен.
if (req.request_type == Capital::RoleRequests::RequestType::ROLE) {
Capital::RoleRequests::apply_role_to_project(
coopname, req.project_hash, req.username, req.role
);
}
// event ridge: заявитель и мастер компонента видят решение.
require_recipient(req.username);
require_recipient(req.master);
}
@@ -0,0 +1,20 @@
/**
* @brief Мастер компонента отклоняет заявку на L2-допуск / на обновление approved-ставки.
* Запись остаётся в таблице со статусом DECLINED + причиной — для аудита.
*
* @param coopname Кооператив
* @param request_hash Хеш заявки
* @param reason Причина отказа
* @ingroup public_actions
* @ingroup public_capital_actions
*/
void capital::declinerole(name coopname, checksum256 request_hash, std::string reason) {
require_auth(coopname);
auto req = Capital::RoleRequests::get_role_request_or_fail(coopname, request_hash);
Capital::RoleRequests::decline(coopname, req.id, reason);
// event ridge: заявитель и мастер компонента видят отказ.
require_recipient(req.username);
require_recipient(req.master);
}
@@ -0,0 +1,22 @@
/**
* @brief Кандидат отказывается от инвайта мастера.
*
* @param coopname Кооператив
* @param request_hash Хеш инвайт-заявки
* @param reason Причина отказа
* @ingroup public_actions
* @ingroup public_capital_actions
*/
void capital::declinvite(name coopname, checksum256 request_hash, std::string reason) {
require_auth(coopname);
auto req = Capital::RoleRequests::get_role_request_or_fail(coopname, request_hash);
eosio::check(req.direction == Capital::RoleRequests::Direction::INVITE,
"Отказаться можно только от инвайта");
Capital::RoleRequests::decline(coopname, req.id, reason);
// event ridge: кандидат и мастер видят отказ от инвайта.
require_recipient(req.username);
require_recipient(req.master);
}
@@ -0,0 +1,46 @@
/**
* @brief Мастер компонента (или председатель — для роли master при пустом слоте мастера)
* приглашает кандидата на роль. Обратный поток к requestrole.
* Кандидат далее acceptinvite (с применением приглашённой ставки) либо declinvite.
*
* Бездокументарная схема: приглашение и принятие/отказ оформляются только
* подписью транзакции (require_auth coopname). Юридический документ-инвайт не нужен.
*
* Ставка часа в приглашении — это ставка, на которую кандидат соглашается через
* acceptinvite. Не путать с contributors.rate_per_hour (личная глобальная ставка).
*
* @param coopname Кооператив
* @param request_hash Хеш заявки (анкер; уникален в scope coopname)
* @param project_hash Проект / компонент
* @param candidate Приглашаемый пайщик (потенциальный участник)
* @param master Мастер-приглашающий (или координатор/председатель для роли master)
* @param role Предлагаемая роль: Role::CREATOR | Role::AUTHOR | Role::MASTER
* @param rate_per_hour Предлагаемая ставка часа
* @param hours_per_day Предлагаемая норма часов в день
* @param description Текст приглашения (может быть пустым)
* @ingroup public_actions
* @ingroup public_capital_actions
*/
void capital::inviterole(name coopname, checksum256 request_hash, checksum256 project_hash,
name candidate, name master, name role,
eosio::asset rate_per_hour, uint64_t hours_per_day,
std::string description) {
require_auth(coopname);
Capital::RoleRequests::validate_role_or_fail(role);
Wallet::validate_asset(rate_per_hour);
eosio::check(rate_per_hour.amount > 0, "Ставка часа должна быть положительной");
eosio::check(hours_per_day > 0 && hours_per_day <= 8, "Норма часов — от 1 до 8");
Capital::RoleRequests::create(
coopname, request_hash, project_hash, candidate, master, role,
rate_per_hour, hours_per_day,
Capital::RoleRequests::Direction::INVITE,
Capital::RoleRequests::RequestType::ROLE,
description
);
// event ridge: кандидат и мастер видят инвайт.
require_recipient(candidate);
require_recipient(master);
}
@@ -0,0 +1,43 @@
/**
* @brief Заявка пайщика на обновление approved-ставки на сегменте.
* Тот же flow что и requestrole, но через RequestType::RATE_UPDATE.
* approverole с новой ставкой → обновление сегмента; declinerole — без изменений.
*
* Бездокументарная схема: заявление-документ не передаётся, утверждение
* фиксируется только транзакцией. role в записи остаётся Role::NONE
* (валидация роли для RATE_UPDATE не применяется).
*
* @param coopname Кооператив
* @param request_hash Хеш заявки
* @param project_hash Проект / компонент
* @param username Заявитель
* @param master Мастер компонента (ожидаемый одобряющий)
* @param new_rate Желаемая новая ставка
* @param new_hours Желаемая новая норма часов
* @param description Текст заявки на обновление ставки (может быть пустым)
* @ingroup public_actions
* @ingroup public_capital_actions
*/
void capital::requestrateu(name coopname, checksum256 request_hash, checksum256 project_hash,
name username, name master,
eosio::asset new_rate, uint64_t new_hours,
std::string description) {
require_auth(coopname);
Wallet::validate_asset(new_rate);
eosio::check(new_rate.amount > 0, "Новая ставка должна быть положительной");
eosio::check(new_hours > 0 && new_hours <= 8, "Норма часов — от 1 до 8");
Capital::RoleRequests::create(
coopname, request_hash, project_hash, username, master,
Capital::RoleRequests::Role::NONE,
new_rate, new_hours,
Capital::RoleRequests::Direction::REQUEST,
Capital::RoleRequests::RequestType::RATE_UPDATE,
description
);
// event ridge: заявитель и мастер компонента видят запрос на обновление ставки.
require_recipient(username);
require_recipient(master);
}
@@ -0,0 +1,49 @@
/**
* @brief Заявка пайщика на L2-допуск (роль creator/author/master) на компоненте.
*
* Создаёт запись в `rolerequests` со статусом PENDING. Дальше мастер компонента
* (или председатель — для роли master, когда мастера в проекте ещё нет)
* решает: approverole (с фиксацией approved-ставки в сегменте) или declinerole.
*
* Бездокументарная схема: подача и решение оформляются только подписью транзакции
* (require_auth coopname). Юридическое заявление при приёме на роль не требуется —
* допуск даётся подписью действия на стороне кооператива.
*
* Ставка часа подаётся явно — это ставка пайщика именно под этот проект.
* По дефолту фронт подставляет contributors.rate_per_hour, но пайщик может изменить.
*
* @param coopname Кооператив
* @param request_hash Хеш заявки (анкер для approverole/declinerole)
* @param project_hash Проект (или компонент — на уровне хэша одно)
* @param username Заявитель
* @param master Мастер компонента (ожидаемый одобряющий)
* @param role Запрашиваемая роль: Role::CREATOR | Role::AUTHOR | Role::MASTER
* @param rate_per_hour Заявленная ставка часа под этот проект
* @param hours_per_day Заявленная норма часов в день
* @param description Текст заявки (может быть пустым)
* @ingroup public_actions
* @ingroup public_capital_actions
*/
void capital::requestrole(name coopname, checksum256 request_hash, checksum256 project_hash,
name username, name master, name role,
eosio::asset rate_per_hour, uint64_t hours_per_day,
std::string description) {
require_auth(coopname);
Capital::RoleRequests::validate_role_or_fail(role);
Wallet::validate_asset(rate_per_hour);
eosio::check(rate_per_hour.amount > 0, "Ставка часа должна быть положительной");
eosio::check(hours_per_day > 0 && hours_per_day <= 8, "Норма часов в день — от 1 до 8");
Capital::RoleRequests::create(
coopname, request_hash, project_hash, username, master, role,
rate_per_hour, hours_per_day,
Capital::RoleRequests::Direction::REQUEST,
Capital::RoleRequests::RequestType::ROLE,
description
);
// event ridge: заявитель и мастер компонента видят новую заявку.
require_recipient(username);
require_recipient(master);
}
@@ -14,21 +14,20 @@
*/
void capital::startvoting(name coopname, checksum256 project_hash) {
require_auth(coopname);
print("z");
// Проверяем существование проекта и получаем его
auto project = Capital::Projects::get_project_or_fail(coopname, project_hash);
print("a");
// Проверяем что проект авторизован советом
//eosio::check(project.is_authorized, "Проект не авторизован советом");
// Проверяем, что проект в статусе "active"
eosio::check(project.status == Capital::Projects::Status::ACTIVE, "Проект должен быть в статусе 'active'");
eosio::check(project.counts.total_commits > 0, "Проект без коммитов не может быть поставлен на голосование");
print("b");
// Обновляем статус проекта на "voting"
Capital::Projects::update_status(coopname, project.id, Capital::Projects::Status::VOTING);
print("c");
// Инициализируем голосование по методу Водянова
Capital::Core::Voting::initialize_project_voting(coopname, project.id);
print("d");
}
}
@@ -61,10 +61,41 @@ void capital::signact2(eosio::name coopname, eosio::name chairman, checksum256 r
}
// Возврат беспроцентного займа пайщика: Dr 80 / Cr 58, TRANSFER LOAN_ISSUED → SHARE_FUND_PAY.
// Семантика: пайщик погасил ссуду результатом (интеллектуальным взносом),
// Семантика: пайщик погасил ссуду результатом (паевой взнос интеллектуальным результатом),
// у кооперативa уменьшилось фин. вложение 58, деньги вернулись на расчётный.
if (result -> debt_amount.amount > 0){
Ledger2::apply(_capital, coopname, operations::capital::REPAY, result -> debt_amount, result -> username, result_hash, memo);
// Лимит «10 займов на проект в одном паевом взносе» проверяем по счётчику
// active_debts_count на сегменте (binary_extension) — без обхода byprojhash.
// Сам счётчик инкрементируется в debtpaycnfrm и декрементируется в mark_settled.
uint32_t active_cnt = Capital::Segments::get_active_debts_count(coopname, result->project_hash, result->username);
eosio::check(active_cnt <= 10,
"Превышен лимит займов на проект для одного паевого взноса (10)");
// Перебираем долги пайщика по проекту, но не больше известного счётчика —
// ранний выход сразу после нужного числа активных записей.
Capital::Debts::debts_index debts(_capital, coopname.value);
auto byproj = debts.get_index<"byprojhash"_n>();
std::vector<std::tuple<uint64_t, checksum256, eosio::asset>> to_settle;
to_settle.reserve(active_cnt);
for (auto it = byproj.find(result->project_hash);
it != byproj.end() && it->project_hash == result->project_hash
&& to_settle.size() < active_cnt; ++it) {
if (it->username == result->username
&& (it->status == Capital::Debts::Status::PAID
|| it->status == Capital::Debts::Status::OVERDUE)) {
to_settle.emplace_back(it->id, it->debt_hash, it->amount);
}
}
for (auto& tup : to_settle) {
auto id = std::get<0>(tup);
auto dh = std::get<1>(tup);
auto amt = std::get<2>(tup);
Loan::settle_debt(_capital, coopname, result->username, dh, amt);
Capital::Debts::mark_settled(coopname, id, memo, _capital);
Capital::Segments::decrease_active_debts_count(coopname, result->project_hash, result->username);
}
}
// Обновляем накопительные показатели контрибьютора на основе его ролей в сегменте
@@ -92,9 +92,29 @@
#include "app/debt_managment/create_debt/debtauthcnfr.cpp"
#include "app/debt_managment/create_debt/debtpaycnfrm.cpp"
#include "app/debt_managment/create_debt/debtpaydcln.cpp"
#include "app/debt_managment/create_debt/debtpayretry.cpp"
#include "app/debt_managment/create_debt/declinedebt.cpp"
#include "app/debt_managment/create_debt/markdebtoverd.cpp"
#include "app/debt_managment/create_debt/closedebt.cpp"
#include "app/debt_managment/settle_debt/settledebt.cpp"
// Управление программными расходами
#include "app/expense_managment/program_expenses/topupprogexp.cpp"
#include "app/expense_managment/program_expenses/createpgexp.cpp"
#include "app/expense_managment/program_expenses/apprvpgexp.cpp"
#include "app/expense_managment/program_expenses/authpgexp.cpp"
#include "app/expense_managment/program_expenses/pgexppay.cpp"
#include "app/expense_managment/program_expenses/declpgexp.cpp"
// L2-роли (Story 1.7)
#include "app/participation_management/roles/requestrole.cpp"
#include "app/participation_management/roles/approverole.cpp"
#include "app/participation_management/roles/declinerole.cpp"
#include "app/participation_management/roles/inviterole.cpp"
#include "app/participation_management/roles/acceptinvite.cpp"
#include "app/participation_management/roles/declinvite.cpp"
#include "app/participation_management/roles/requestrateu.cpp"
// Управление расходами
#include "app/expense_managment/create_expense/approveexpns.cpp"
#include "app/expense_managment/create_expense/capauthexpns.cpp"
+64 -2
View File
@@ -315,11 +315,73 @@ public:
[[eosio::action]]
void debtpaydcln(name coopname, checksum256 debt_hash, std::string reason);
[[eosio::action]]
void debtpayretry(eosio::name coopname, checksum256 debt_hash);
[[eosio::action]]
void declinedebt(name coopname, eosio::name username, checksum256 debt_hash, std::string reason);
[[eosio::action]]
void settledebt(name coopname, name username, eosio::asset amount, document2 statement);
void settledebt(name coopname, checksum256 debt_hash, eosio::asset amount, document2 statement);
[[eosio::action]]
void markdebtoverd(name coopname);
[[eosio::action]]
void closedebt(name coopname, checksum256 debt_hash);
[[eosio::action]]
void topupprogexp(name coopname, eosio::asset amount);
[[eosio::action]]
void createpgexp(name coopname, checksum256 expense_hash, name creator,
eosio::asset amount, std::string description, document2 statement);
[[eosio::action]]
void apprvpgexp(name coopname, name approver, checksum256 expense_hash,
document2 approved_statement);
[[eosio::action]]
void authpgexp(name coopname, checksum256 expense_hash, document2 authorization);
[[eosio::action]]
void pgexppay(name coopname, checksum256 expense_hash);
[[eosio::action]]
void declpgexp(name coopname, checksum256 expense_hash, std::string reason);
// L2-роли и approved-ставки сегмента (Story 1.7) — бездокументарная схема,
// решения фиксируются только подписью транзакции (require_auth coopname).
[[eosio::action]]
void requestrole(name coopname, checksum256 request_hash, checksum256 project_hash,
name username, name master, name role,
eosio::asset rate_per_hour, uint64_t hours_per_day,
std::string description);
[[eosio::action]]
void approverole(name coopname, checksum256 request_hash,
eosio::asset approved_rate, uint64_t approved_hours);
[[eosio::action]]
void declinerole(name coopname, checksum256 request_hash, std::string reason);
[[eosio::action]]
void inviterole(name coopname, checksum256 request_hash, checksum256 project_hash,
name candidate, name master, name role,
eosio::asset rate_per_hour, uint64_t hours_per_day,
std::string description);
[[eosio::action]]
void acceptinvite(name coopname, checksum256 request_hash);
[[eosio::action]]
void declinvite(name coopname, checksum256 request_hash, std::string reason);
[[eosio::action]]
void requestrateu(name coopname, checksum256 request_hash, checksum256 project_hash,
name username, name master,
eosio::asset new_rate, uint64_t new_hours,
std::string description);
};
@@ -15,10 +15,14 @@ namespace Capital::Debts {
*/
namespace Status {
constexpr name CREATED = "created"_n; ///< Долг создан
constexpr name APPROVED = "approved"_n; ///< Долг одобрен
constexpr name AUTHORIZED = "authorized"_n; ///< Долг авторизован советом
constexpr name PAID = "paid"_n; ///< Долг выплачен (готов к использованию)
constexpr name CREATED = "created"_n; ///< Долг создан
constexpr name APPROVED = "approved"_n; ///< Долг одобрен председателем
constexpr name AUTHORIZED = "authorized"_n; ///< Долг авторизован советом, ожидает повторной отправки на оплату
constexpr name PAY_PENDING = "paypending"_n; ///< Долг отправлен в gateway на оплату (исходящий платёж создан, ждём debtpaycnfrm/debtpaydcln)
constexpr name PAID = "paid"_n; ///< Долг выплачен пайщику, активный (ждёт погашения)
constexpr name OVERDUE = "overdue"_n; ///< Долг просрочен (now > due_at) — выставляется action markdebtoverd
constexpr name SETTLED = "settled"_n; ///< Долг полностью погашен (деньгами через settledebt либо результатом через signact2)
constexpr name WRITEOFF = "writeoff"_n; ///< Долг списан (write-off) за счёт созданного НМА из коммитов-обеспечения (через closedebt при отмене родительского компонента; решение 2026-05-19)
}
/**
@@ -42,7 +46,9 @@ struct [[eosio::table, eosio::contract(CAPITAL)]] debt {
document2 approved_statement; ///< Одобренное заявление
document2 authorization; ///< Авторизация совета
std::string memo; ///< Примечание
std::string last_pay_error; ///< Последняя ошибка outpay от gateway (заполняется debtpaydcln; пусто = успех или ещё не отправлен)
time_point_sec due_at; ///< Срок погашения (current_time_point + 1 год при переходе в paid; решение 2026-05-19); now > due_at → overdue
uint64_t primary_key() const { return id; } ///< Первичный ключ (1)
uint64_t by_username() const { return username.value; } ///< Индекс по имени пользователя (2)
@@ -145,6 +151,179 @@ inline void update_debt_status(
});
}
/**
* @brief Переводит долг в PAY_PENDING — фиксирует решение совета и факт отправки в gateway.
* Допустимо из APPROVED (первая отправка после авторизации совета) или из AUTHORIZED
* (повторная отправка после предыдущего отказа gateway). Очищает last_pay_error.
*/
inline void start_pay(
eosio::name coopname,
uint64_t debt_id,
const document2 &authorization,
eosio::name payer = name{}
) {
if (payer == name{}) payer = coopname;
debts_index debts(_capital, coopname.value);
auto debt = debts.find(debt_id);
eosio::check(debt != debts.end(), "Долг не найден");
eosio::check(debt->status == Status::APPROVED || debt->status == Status::AUTHORIZED,
"Старт оплаты допустим только из approved/authorized");
debts.modify(debt, payer, [&](auto &d) {
d.status = Status::PAY_PENDING;
d.authorization = authorization;
d.last_pay_error = "";
});
}
/**
* @brief Возвращает долг из PAY_PENDING в AUTHORIZED с записью причины ошибки оплаты.
* Долг не удаляется — решение совета уже принято, доступна повторная отправка в gateway.
*/
inline void mark_pay_declined(
eosio::name coopname,
uint64_t debt_id,
const std::string &reason,
eosio::name payer = name{}
) {
if (payer == name{}) payer = coopname;
debts_index debts(_capital, coopname.value);
auto debt = debts.find(debt_id);
eosio::check(debt != debts.end(), "Долг не найден");
eosio::check(debt->status == Status::PAY_PENDING,
"Отказ оплаты допустим только из статуса pay_pending");
debts.modify(debt, payer, [&](auto &d) {
d.status = Status::AUTHORIZED;
d.last_pay_error = reason;
});
}
/**
* @brief Кол-во секунд в стандартном сроке погашения займа (1 год = 365 дней).
*
* Решение 2026-05-19 (Игорь Смуров / Алексей Муравьёв): срок займа
* фиксированный — год, не привязан к календарному сроку проекта. Если
* родительский компонент явно перешёл в `cancelled` раньше — долг закрывается
* досрочно через создание НМА кооператива из коммитов-обеспечения и списание
* долга (см. action `capital::closedebt`, операции o.cap.crtnma + o.cap.dbtwrf).
*/
constexpr uint32_t DEFAULT_DEBT_TERM_SECONDS = 365 * 24 * 60 * 60;
/**
* @brief Переводит долг в PAID, фиксирует дату выплаты и проставляет due_at.
*
* Используется в debtpaycnfrm после успешной outpay из gateway. due_at рассчитывается
* от current_time_point на год вперёд; долги с now > due_at и status == PAID
* далее перейдут в OVERDUE через markdebtoverd.
*/
inline void confirm_paid(
eosio::name coopname,
uint64_t debt_id,
eosio::name payer = name{}
) {
if (payer == name{}) payer = coopname;
debts_index debts(_capital, coopname.value);
auto debt = debts.find(debt_id);
eosio::check(debt != debts.end(), "Долг не найден");
eosio::check(debt->status == Status::PAY_PENDING,
"Подтверждение оплаты допустимо только из pay_pending");
auto now = eosio::current_time_point();
debts.modify(debt, payer, [&](auto &d) {
d.status = Status::PAID;
d.due_at = eosio::time_point_sec(now.sec_since_epoch() + DEFAULT_DEBT_TERM_SECONDS);
d.last_pay_error = "";
});
}
/**
* @brief Помечает долги PAID с now > due_at как OVERDUE в пределах одного batch.
*
* @param coopname Кооператив (scope)
* @param batch_limit Максимальное число записей за вызов (защита от 30-сек лимита транзакции)
* @return Сколько долгов помечено за этот вызов; backend-cron повторяет до 0.
*/
inline uint32_t sweep_overdue(
eosio::name coopname,
uint32_t batch_limit
) {
debts_index debts(_capital, coopname.value);
auto now = eosio::current_time_point();
auto now_sec = eosio::time_point_sec(now.sec_since_epoch());
uint32_t marked = 0;
for (auto it = debts.begin(); it != debts.end() && marked < batch_limit; ++it) {
if (it->status == Status::PAID && it->due_at != time_point_sec() && now_sec > it->due_at) {
debts.modify(it, coopname, [&](auto &d) {
d.status = Status::OVERDUE;
});
++marked;
}
}
return marked;
}
/**
* @brief Закрывает долг как погашенный — переводит в SETTLED, сохраняет документ-основание
* в поле memo (если передан) и фиксирует дату погашения.
*
* Деньги и проводка REPAY к этому моменту уже должны быть применены в Ledger2,
* а contributors.debt_amount — уменьшен. Функция отвечает только за состояние записи.
*/
inline void mark_settled(
eosio::name coopname,
uint64_t debt_id,
const std::string &settle_memo = "",
eosio::name payer = name{}
) {
if (payer == name{}) payer = coopname;
debts_index debts(_capital, coopname.value);
auto debt = debts.find(debt_id);
eosio::check(debt != debts.end(), "Долг не найден");
eosio::check(debt->status == Status::PAID || debt->status == Status::OVERDUE,
"Погашение допустимо только из статуса paid или overdue");
debts.modify(debt, payer, [&](auto &d) {
d.status = Status::SETTLED;
d.repaid_at = eosio::current_time_point();
if (!settle_memo.empty()) {
d.memo = settle_memo;
}
});
}
/**
* @brief Закрывает долг как списанный — переводит в WRITEOFF.
*
* Применяется в closedebt после применения o.cap.crtnma + o.cap.dbtwrf и
* декремента сегмента. Семантически зеркалит mark_settled, но не пишет
* memo о погашении: долг закрыт имущественно через созданное НМА из
* коммитов-обеспечения, без зачисления на share пайщика. Решение 2026-05-19.
*/
inline void mark_writeoff(
eosio::name coopname,
uint64_t debt_id,
eosio::name payer = name{}
) {
if (payer == name{}) payer = coopname;
debts_index debts(_capital, coopname.value);
auto debt = debts.find(debt_id);
eosio::check(debt != debts.end(), "Долг не найден");
eosio::check(debt->status == Status::PAID || debt->status == Status::OVERDUE,
"Списание долга допустимо только из статуса paid или overdue");
debts.modify(debt, payer, [&](auto &d) {
d.status = Status::WRITEOFF;
d.repaid_at = eosio::current_time_point();
});
}
/**
* @brief Удаляет долг
*/
@@ -178,8 +178,116 @@ namespace Capital::Expenses {
Capital::expense_index expenses(_capital, coopname.value);
auto expense = expenses.find(expense_id);
eosio::check(expense != expenses.end(), "Расход не найден");
expenses.erase(expense);
}
} // namespace Capital::Expenses
namespace Capital {
/**
* @brief Таблица программных расходов — целевые списания «Благорост»,
* не привязанные к конкретному проекту.
* @par Область памяти (scope): coopname
* @par Имя таблицы (table): progexpenses
*/
struct [[eosio::table, eosio::contract(CAPITAL)]] program_expense {
uint64_t id; ///< Внутренний ключ
name coopname; ///< Имя кооператива
name username; ///< Инициатор расхода (председатель / казначей)
name status = Expenses::Status::CREATED; ///< Статус (CREATED/APPROVED/AUTHORIZED/PAID/DECLINED — общий enum)
checksum256 expense_hash; ///< Хеш расхода — анкер процесса p.cap.expns
uint64_t fund_id; ///< Идентификатор фонда списания
eosio::asset amount; ///< Сумма расхода
std::string description; ///< Описание (для отчётности)
document2 expense_statement; ///< Служебная записка (ProgramExpenseStatement)
document2 approved_statement; ///< Записка, принятая председателем
document2 authorization; ///< Решение совета
time_point_sec spended_at = current_time_point();
uint64_t primary_key() const { return id; }
uint64_t by_username() const { return username.value; }
checksum256 by_expense_hash() const { return expense_hash; }
};
typedef eosio::multi_index<
"progexpenses"_n, program_expense,
indexed_by<"byusername"_n, const_mem_fun<program_expense, uint64_t, &program_expense::by_username>>,
indexed_by<"byhash"_n, const_mem_fun<program_expense, checksum256, &program_expense::by_expense_hash>>
> program_expense_index;
inline std::optional<program_expense> get_program_expense(eosio::name coopname, const checksum256 &hash) {
program_expense_index t(_capital, coopname.value);
auto idx = t.get_index<"byhash"_n>();
auto itr = idx.find(hash);
if (itr == idx.end()) return std::nullopt;
return program_expense(*itr);
}
} // namespace Capital
namespace Capital::Expenses {
inline program_expense get_program_expense_or_fail(eosio::name coopname, const checksum256 &expense_hash) {
auto v = Capital::get_program_expense(coopname, expense_hash);
eosio::check(v.has_value(), "Программный расход с указанным хэшем не найден");
return v.value();
}
inline void create_program_expense(
eosio::name coopname,
const checksum256 &expense_hash,
eosio::name username,
const eosio::asset &amount,
const std::string &description,
const document2 &statement
) {
Capital::program_expense_index t(_capital, coopname.value);
uint64_t id = get_global_id_in_scope(_capital, coopname, "progexpenses"_n);
t.emplace(coopname, [&](auto &e) {
e.id = id;
e.coopname = coopname;
e.username = username;
e.expense_hash = expense_hash;
e.fund_id = Ledger::accounts::GENERAL_EXPENSES;
e.status = Status::CREATED;
e.spended_at = current_time_point();
e.expense_statement = statement;
e.amount = amount;
e.description = description;
});
}
inline void set_program_approved(eosio::name coopname, uint64_t id, const document2 &approved_statement) {
Capital::program_expense_index t(_capital, coopname.value);
auto itr = t.find(id);
eosio::check(itr != t.end(), "Программный расход не найден");
t.modify(itr, coopname, [&](auto &e) {
e.status = Status::APPROVED;
e.approved_statement = approved_statement;
});
}
inline void set_program_authorized(eosio::name coopname, uint64_t id, const document2 &authorization) {
Capital::program_expense_index t(_capital, coopname.value);
auto itr = t.find(id);
eosio::check(itr != t.end(), "Программный расход не найден");
t.modify(itr, coopname, [&](auto &e) {
e.status = Status::AUTHORIZED;
e.authorization = authorization;
});
}
inline void delete_program_expense(eosio::name coopname, uint64_t id) {
Capital::program_expense_index t(_capital, coopname.value);
auto itr = t.find(id);
eosio::check(itr != t.end(), "Программный расход не найден");
t.erase(itr);
}
} // namespace Capital::Expenses
@@ -1,5 +1,7 @@
#pragma once
#include <eosio/binary_extension.hpp>
using namespace eosio;
using std::string;
@@ -40,7 +42,11 @@ namespace Capital {
double program_membership_cumulative_reward_per_share; ///< Накопительное вознаграждение на долю в членских взносах
config config; ///< Управляемая конфигурация контракта
// binary_extension: таблица state уже в продакшене, новые поля только хвостом.
eosio::binary_extension<asset> program_expense_pool; ///< Доступная сумма программы под целевые расходы (без аллокации в проекты)
eosio::binary_extension<asset> program_expense_reserved; ///< Сумма, зарезервированная под approved/authorized программные расходы
uint64_t primary_key() const { return coopname.value; } ///< Первичный ключ (1)
};
@@ -78,7 +84,7 @@ inline global_state get_global_state(name coopname) {
/**
* @brief Проверяет что контракт инициализирован.
*
*
* @param coopname Имя кооператива.
*/
inline void is_initialized(name coopname) {
@@ -86,6 +92,83 @@ inline void is_initialized(name coopname) {
auto itr = global_state_inst.find(coopname.value);
eosio::check(itr != global_state_inst.end(), "Контракт не инициализирован");
}
/// Lazy-default для binary_extension<asset>: пустое значение = 0 в _root_govern_symbol.
inline asset _read_pool(const eosio::binary_extension<asset> &field) {
return field.has_value() ? *field : asset(0, _root_govern_symbol);
}
/**
* @brief Пополняет пул программных расходов на @p amount из общего пула инвестиций программы.
* Вызывается из action topupprogexp (председатель).
*/
inline void topup_program_expense_pool(eosio::name coopname, const asset &amount) {
global_state_table global_state_inst(_capital, _capital.value);
auto itr = global_state_inst.find(coopname.value);
eosio::check(itr != global_state_inst.end(), "Контракт не инициализирован");
eosio::check(amount.is_valid() && amount.amount > 0, "Сумма пополнения должна быть положительной");
eosio::check(itr->global_available_invest_pool >= amount,
"Недостаточно свободных инвестиций программы для пополнения пула расходов");
global_state_inst.modify(itr, _capital, [&](auto &s) {
s.global_available_invest_pool -= amount;
s.program_expense_pool.emplace(_read_pool(s.program_expense_pool) + amount);
});
}
/**
* @brief Резервирует средства под approved/authorized программный расход:
* program_expense_pool -= amount, program_expense_reserved += amount.
*/
inline void reserve_program_expense(eosio::name coopname, const asset &amount) {
global_state_table global_state_inst(_capital, _capital.value);
auto itr = global_state_inst.find(coopname.value);
eosio::check(itr != global_state_inst.end(), "Контракт не инициализирован");
eosio::check(amount.is_valid() && amount.amount > 0, "Сумма резерва должна быть положительной");
eosio::check(_read_pool(itr->program_expense_pool) >= amount,
"Недостаточно средств в пуле программных расходов");
global_state_inst.modify(itr, _capital, [&](auto &s) {
s.program_expense_pool.emplace(_read_pool(s.program_expense_pool) - amount);
s.program_expense_reserved.emplace(_read_pool(s.program_expense_reserved) + amount);
});
}
/**
* @brief Освобождает зарезервированное при отклонении программного расхода:
* program_expense_reserved -= amount, program_expense_pool += amount.
*/
inline void release_program_expense(eosio::name coopname, const asset &amount) {
global_state_table global_state_inst(_capital, _capital.value);
auto itr = global_state_inst.find(coopname.value);
eosio::check(itr != global_state_inst.end(), "Контракт не инициализирован");
eosio::check(amount.is_valid() && amount.amount > 0, "Сумма освобождения должна быть положительной");
eosio::check(_read_pool(itr->program_expense_reserved) >= amount,
"Зарезервированных программных расходов меньше указанной суммы");
global_state_inst.modify(itr, _capital, [&](auto &s) {
s.program_expense_reserved.emplace(_read_pool(s.program_expense_reserved) - amount);
s.program_expense_pool.emplace(_read_pool(s.program_expense_pool) + amount);
});
}
/**
* @brief Окончательное списание программного расхода после exppaycnfrm:
* program_expense_reserved -= amount. Деньги уже ушли gateway,
* ledger2-проводка PAY_EXPENSE сделана в action подтверждения.
*/
inline void consume_program_expense(eosio::name coopname, const asset &amount) {
global_state_table global_state_inst(_capital, _capital.value);
auto itr = global_state_inst.find(coopname.value);
eosio::check(itr != global_state_inst.end(), "Контракт не инициализирован");
eosio::check(amount.is_valid() && amount.amount > 0, "Сумма списания должна быть положительной");
eosio::check(_read_pool(itr->program_expense_reserved) >= amount,
"Зарезервированных программных расходов меньше указанной суммы");
global_state_inst.modify(itr, _capital, [&](auto &s) {
s.program_expense_reserved.emplace(_read_pool(s.program_expense_reserved) - amount);
});
}
}// namespace State
}// namespace Capital
@@ -17,6 +17,7 @@
#include "projects.hpp"
#include "results.hpp"
#include "debts.hpp"
#include "role_requests.hpp"
#include "wallets.hpp"
#include "plan_pool.hpp"
#include "fact_pool.hpp"
@@ -87,4 +87,12 @@ namespace Capital::Memo {
return "Внесение имущества в программу Благорост: " + checksum256_to_hex(property_hash);
}
inline std::string get_settle_debt_memo(eosio::name username, checksum256 debt_hash) {
return "Погашение займа участником " + username.to_string() + " (заём " + checksum256_to_hex(debt_hash) + ")";
}
inline std::string get_expense_pay_memo(checksum256 expense_hash) {
return "Оплата целевого расхода программы «Благорост»: " + checksum256_to_hex(expense_hash);
}
} // namespace Capital::Memo
@@ -4,17 +4,23 @@ using namespace eosio;
using std::string;
namespace Capital {
//TODO: УДАЛИТЬ после 07.2026 т.к. не используем
/**
* @brief Таблица программных инвестиций (progrinvests): ранее использовалась для заявок на зачёт с этапом одобрения советом; при текущей логике createpinv строки не создаёт, таблица сохранена для совместимости и чтения исторических записей.
* @deprecated description: не используется
* @brief Таблица программных инвестиций (progrinvests): ранее использовалась
* для заявок на зачёт с этапом одобрения советом. При текущей логике
* @ref capital::createpinv строки не создаёт, таблица сохранена для
* совместимости и чтения исторических записей.
*
* @deprecated Запланировано к удалению после 2026-07. Не использовать в новом
* коде. Чтение через scope coopname допустимо до этой даты для
* ретроспективных отчётов; писать через emplace/modify нельзя.
*
* @ingroup public_tables
* @ingroup public_capital_tables
*
* @par Область памяти (scope): coopname
* @par Имя таблицы (table): progrinvests
* @par Имя таблицы (table): progrinvests
*/
struct [[eosio::table, eosio::contract(CAPITAL)]] program_invest {
struct [[deprecated("remove after 2026-07"), eosio::table, eosio::contract(CAPITAL)]] program_invest {
uint64_t id; ///< ID программной инвестиции (внутренний ключ)
eosio::name coopname; ///< Имя кооператива
eosio::name username; ///< Имя инвестора
@@ -0,0 +1,211 @@
#pragma once
#include <eosio/eosio.hpp>
#include <eosio/asset.hpp>
using namespace eosio;
using std::string;
namespace Capital::RoleRequests {
/**
* @brief Статусы заявки на роль / обновление approved-ставки.
*/
namespace Status {
constexpr name PENDING = "pending"_n;
constexpr name APPROVED = "approved"_n;
constexpr name DECLINED = "declined"_n;
}
/**
* @brief Направление: пайщик просит сам (REQUEST) или мастер инвайтит кандидата (INVITE).
*/
namespace Direction {
constexpr name REQUEST = "request"_n;
constexpr name INVITE = "invite"_n;
}
/**
* @brief Тип заявки: «дай мне роль/допуск L2» или «обнови мне approved-ставку».
*/
namespace RequestType {
constexpr name ROLE = "role"_n;
constexpr name RATE_UPDATE = "rateupdate"_n;
}
/**
* @brief Допустимые роли заявки на L2-допуск.
* Контрибьюторы и инвесторы — L1, через role_requests не идут.
*/
namespace Role {
constexpr name CREATOR = "creator"_n;
constexpr name AUTHOR = "author"_n;
constexpr name MASTER = "master"_n;
constexpr name NONE = name{}; ///< Для RATE_UPDATE: поле role не значимо
}
/**
* @brief Очередь заявок на L2-допуск/инвайт и на обновление approved-ставки.
*
* Бездокументарная схема: подача и решение оформляются только транзакцией
* блокчейна (require_auth coopname). Юридическое заявление при приёме на роль
* не требуется — допуск даётся через подпись действия на стороне кооператива.
*
* @par Область памяти (scope): coopname
* @par Имя таблицы (table): rolerequests
*/
struct [[eosio::table, eosio::contract(CAPITAL)]] role_request {
uint64_t id; ///< Внутренний ключ
eosio::name coopname; ///< Кооператив
checksum256 request_hash; ///< Хеш заявки (анкер)
checksum256 project_hash; ///< Проект / компонент
eosio::name username; ///< Заявитель (REQUEST) или кандидат (INVITE)
eosio::name master; ///< Мастер компонента (одобряющий) — при invite на роль master это координатор/председатель
eosio::name role = Role::NONE; ///< Role::CREATOR | Role::AUTHOR | Role::MASTER (для RequestType::RATE_UPDATE — Role::NONE)
eosio::asset rate_per_hour = asset(0, _root_govern_symbol); ///< Заявленная пайщиком/мастером ставка часа (всегда явная, не из contributors)
uint64_t hours_per_day = 0; ///< Заявленная норма часов
eosio::asset approved_rate = asset(0, _root_govern_symbol); ///< Утверждённая ставка (≠ requested при approve)
uint64_t approved_hours = 0; ///< Утверждённая норма часов
eosio::name direction = Direction::REQUEST; ///< request | invite
eosio::name request_type = RequestType::ROLE; ///< role | rateupdate
eosio::name status = Status::PENDING; ///< pending | approved | declined
std::string description; ///< Текст заявки / приглашения (может быть пустым)
std::string decline_reason; ///< Причина отказа (если DECLINED)
time_point_sec created_at = current_time_point();
uint64_t primary_key() const { return id; }
checksum256 by_request_hash() const { return request_hash; }
uint128_t by_project_user() const {
return combine_checksum_ids(project_hash, username);
}
};
typedef eosio::multi_index<
"rolerequests"_n, role_request,
indexed_by<"byhash"_n, const_mem_fun<role_request, checksum256, &role_request::by_request_hash>>,
indexed_by<"byprojuser"_n, const_mem_fun<role_request, uint128_t, &role_request::by_project_user>>
> role_requests_index;
inline std::optional<role_request> get_role_request(eosio::name coopname, const checksum256 &request_hash) {
role_requests_index t(_capital, coopname.value);
auto idx = t.get_index<"byhash"_n>();
auto itr = idx.find(request_hash);
if (itr == idx.end()) return std::nullopt;
return role_request(*itr);
}
inline role_request get_role_request_or_fail(eosio::name coopname, const checksum256 &request_hash) {
auto v = get_role_request(coopname, request_hash);
eosio::check(v.has_value(), "Заявка на роль не найдена");
return v.value();
}
inline void create(
eosio::name coopname,
const checksum256 &request_hash,
const checksum256 &project_hash,
eosio::name username,
eosio::name master,
eosio::name role,
const eosio::asset &rate_per_hour,
uint64_t hours_per_day,
eosio::name direction,
eosio::name request_type,
const std::string &description
) {
role_requests_index t(_capital, coopname.value);
eosio::check(!get_role_request(coopname, request_hash).has_value(),
"Заявка с указанным хэшем уже существует");
uint64_t id = get_global_id_in_scope(_capital, coopname, "rolerequests"_n);
t.emplace(coopname, [&](auto &r) {
r.id = id;
r.coopname = coopname;
r.request_hash = request_hash;
r.project_hash = project_hash;
r.username = username;
r.master = master;
r.role = role;
r.rate_per_hour = rate_per_hour;
r.hours_per_day = hours_per_day;
r.direction = direction;
r.request_type = request_type;
r.status = Status::PENDING;
r.description = description;
r.created_at = current_time_point();
});
}
/**
* @brief Проверяет, что role входит в допустимый набор Role::{CREATOR, AUTHOR, MASTER}.
* Контрибьюторы и инвесторы — это L1-роли и через role_requests не идут.
* Не применяется к заявкам RequestType::RATE_UPDATE (там role = Role::NONE).
*/
inline void validate_role_or_fail(eosio::name role) {
eosio::check(
role == Role::CREATOR || role == Role::AUTHOR || role == Role::MASTER,
"Допустимые роли заявки: creator, author, master"
);
}
/**
* @brief Применяет заявленную роль к проекту через inline action:
* Role::AUTHOR → addauthor, Role::MASTER → setmaster, Role::CREATOR → no-op
* (creator фиксируется фактически при первом createcmmt).
*
* Вызывается из approverole/acceptinvite после фиксации решения, чтобы
* заявленная при подаче роль немедленно отражалась в реестре участников.
* Inline action идёт под permission coopname — председатель/backend подписывает.
*/
inline void apply_role_to_project(
eosio::name coopname,
const checksum256 &project_hash,
eosio::name username,
eosio::name role
) {
if (role == Role::AUTHOR) {
eosio::action(
eosio::permission_level{coopname, "active"_n},
_capital,
"addauthor"_n,
std::make_tuple(coopname, project_hash, username)
).send();
} else if (role == Role::MASTER) {
eosio::action(
eosio::permission_level{coopname, "active"_n},
_capital,
"setmaster"_n,
std::make_tuple(coopname, project_hash, username)
).send();
}
}
inline void approve(
eosio::name coopname,
uint64_t request_id,
const eosio::asset &approved_rate,
uint64_t approved_hours
) {
role_requests_index t(_capital, coopname.value);
auto itr = t.find(request_id);
eosio::check(itr != t.end(), "Заявка не найдена");
eosio::check(itr->status == Status::PENDING, "Заявка уже обработана");
t.modify(itr, coopname, [&](auto &r) {
r.status = Status::APPROVED;
r.approved_rate = approved_rate;
r.approved_hours = approved_hours;
});
}
inline void decline(eosio::name coopname, uint64_t request_id, const std::string &reason) {
role_requests_index t(_capital, coopname.value);
auto itr = t.find(request_id);
eosio::check(itr != t.end(), "Заявка не найдена");
eosio::check(itr->status == Status::PENDING, "Заявка уже обработана");
t.modify(itr, coopname, [&](auto &r) {
r.status = Status::DECLINED;
r.decline_reason = reason;
});
}
} // namespace Capital::RoleRequests
@@ -2,6 +2,7 @@
#include <eosio/eosio.hpp>
#include <eosio/asset.hpp>
#include <eosio/binary_extension.hpp>
using namespace eosio;
@@ -112,7 +113,19 @@ namespace Capital::Segments {
eosio::asset available_for_program = asset(0, _root_govern_symbol); ///< Доступная сумма для конвертации в программу (intellectual_cost - debt_amount)
eosio::asset available_for_wallet = asset(0, _root_govern_symbol); ///< Доступная сумма для конвертации в кошелек (provisional_amount - debt_amount)
double share_percent = 0.0; ///< Доля участника в результате (intellectual_cost / fact.total * 100)
// Допуски L2 — утверждаются мастером компонента/сегмента через approverole (Story 1.7).
// Используются в createcmmt вместо глобального contributors.rate_per_hour, если значение установлено.
// binary_extension: таблица segments уже в продакшене, расширение схемы только хвостом.
eosio::binary_extension<eosio::asset> approved_rate_per_hour; ///< Утверждённая мастером ставка часа на этом сегменте
eosio::binary_extension<uint64_t> approved_hours_per_day; ///< Утверждённое мастером количество часов в день на этом сегменте
// Счётчик активных (PAID/OVERDUE) займов пайщика на проекте этого сегмента.
// Инкрементируется в Capital::Debts::confirm_paid, декрементируется в mark_settled.
// Используется как лимит «10 займов на проект в одном паевом взносе» (signact2),
// чтобы не перебирать byprojhash и не упираться в 30-сек лимит транзакции.
eosio::binary_extension<uint32_t> active_debts_count;
uint64_t primary_key() const { return id; } ///< Первичный ключ (1)
checksum256 by_project_hash() const { return project_hash; } ///< Индекс по хэшу проекта (2)
@@ -156,6 +169,30 @@ namespace Capital::Segments {
return segment(*segment_itr);
}
/**
* @brief Фиксирует утверждённую мастером ставку часа и норматив часов в день на сегменте.
* Вызывается из action approverole (Story 1.7). createcmmt далее берёт эту
* ставку для расчёта generation_amounts вместо глобальной из contributors.
*/
inline void set_approved_rate(
eosio::name coopname,
const checksum256 &project_hash,
eosio::name username,
const eosio::asset &rate_per_hour,
uint64_t hours_per_day
) {
segments_index segments(_capital, coopname.value);
auto idx = segments.get_index<"byprojuser"_n>();
auto rkey = combine_checksum_ids(project_hash, username);
auto it = idx.find(rkey);
eosio::check(it != idx.end(), "Сегмент участника на этом проекте не найден");
idx.modify(it, coopname, [&](auto &s) {
s.approved_rate_per_hour.emplace(rate_per_hour);
s.approved_hours_per_day.emplace(hours_per_day);
});
}
/**
* @brief Подсчитывает количество авторов в проекте.
@@ -418,14 +455,57 @@ inline void increase_debt_amount(eosio::name coopname, uint64_t segment_id, eosi
inline void decrease_debt_amount(eosio::name coopname, uint64_t segment_id, eosio::asset amount) {
Capital::Segments::segments_index segments(_capital, coopname.value);
auto segment = segments.find(segment_id);
eosio::check(segment->debt_amount >= amount, "Пайщик не может погасить долг больше, чем должен");
segments.modify(segment, coopname, [&](auto &s) {
s.debt_amount -= amount;
});
}
/**
* @brief Увеличивает счётчик активных займов пайщика на сегменте на +1.
* Вызывается в Capital::Debts::confirm_paid после перехода долга в PAID.
*/
inline void increase_active_debts_count(eosio::name coopname, const checksum256 &project_hash, eosio::name username) {
segments_index segments(_capital, coopname.value);
auto idx = segments.get_index<"byprojuser"_n>();
auto rkey = combine_checksum_ids(project_hash, username);
auto it = idx.find(rkey);
eosio::check(it != idx.end(), "Сегмент участника на этом проекте не найден");
idx.modify(it, coopname, [&](auto &s) {
uint32_t cur = s.active_debts_count.has_value() ? *s.active_debts_count : 0;
s.active_debts_count.emplace(cur + 1);
});
}
/**
* @brief Уменьшает счётчик активных займов пайщика на сегменте на -1.
* Вызывается в Capital::Debts::mark_settled (settledebt и signact2-погашение).
*/
inline void decrease_active_debts_count(eosio::name coopname, const checksum256 &project_hash, eosio::name username) {
segments_index segments(_capital, coopname.value);
auto idx = segments.get_index<"byprojuser"_n>();
auto rkey = combine_checksum_ids(project_hash, username);
auto it = idx.find(rkey);
eosio::check(it != idx.end(), "Сегмент участника на этом проекте не найден");
idx.modify(it, coopname, [&](auto &s) {
uint32_t cur = s.active_debts_count.has_value() ? *s.active_debts_count : 0;
eosio::check(cur > 0, "Счётчик активных займов уже на нуле");
s.active_debts_count.emplace(cur - 1);
});
}
/**
* @brief Текущее значение счётчика активных займов пайщика на сегменте.
* Используется в signact2 как лимит обхода долгов проекта (≤10).
*/
inline uint32_t get_active_debts_count(eosio::name coopname, const checksum256 &project_hash, eosio::name username) {
auto seg = get_segment(coopname, project_hash, username);
if (!seg.has_value()) return 0;
return seg->active_debts_count.has_value() ? *seg->active_debts_count : 0;
}
/**
* @brief Удаляет сегмент участника
* @param coopname Имя кооператива
@@ -1,16 +1,5 @@
# ─────────────────────────────────────────────────────────────────────────────
# Стандарт «Займ пайщику» — выдача беспроцентного целевого займа из паевого
# фонда кооператива и его возврат через акт приёма-передачи проекта.
#
# Многоактовый процесс с авторизацией советом. Две операции:
# • o.cap.lend — выдача займа на момент авторизации (ISSUE → w.cap.loan, Дт 58 / Кт 51)
# • o.cap.repay — возврат займа из акта-2 (TRANSFER w.cap.loan → w.wal.share, Дт 80 / Кт 58)
#
# Источники правды в коде:
# • cpp/capital/capital.hpp — actions
# • cpp/capital/src/debt/{createdebt,approvedebt,debtauthcnfr,debtpaycnfrm,declinedebt,debtpaydcln}.cpp
# • cpp/lib/core/ledger2/operations.hpp — o.cap.lend, o.cap.repay
# • cpp/lib/core/ledger2/processes.hpp — processes::capital::DEBT
# Стандарт «Займ пайщику»
# ─────────────────────────────────────────────────────────────────────────────
# ── Секция 1. Паспорт ───────────────────────────────────────────────────────
@@ -20,103 +9,104 @@ title: Выдача займа пайщику
slug: debt
status: proposed
contract: capital
summary: >
Пайщик получает из паевого фонда кооператива беспроцентный целевой заём
на срок проекта; возврат происходит при сдаче акта-2 проекта.
purpose: >
«Заём пайщику» — кооператив выдаёт пайщику беспроцентный целевой заём
из паевого фонда на срок проекта. Заявку последовательно одобряют
председатель и совет, выплата уходит пайщику через кассира. Возврат
происходит автоматически при сдаче акта приёма-передачи проекта —
отдельная заявка на возврат не нужна.
Пайщик получает из паевого фонда кооператива беспроцентный целевой заём
сроком на один год. Решение о выдаче принимает совет кооператива. Заём
возвращается автоматически при сдаче результата работ по проекту. Если же
проект до сдачи не доведён и был отменён, долг закрывается за счёт
имущества, которое пайщик ранее передал кооперативу в обеспечение по этому
займу. Никаких санкций к пайщику в случае невозврата не применяется.
roles:
- contributor
- chairman
- soviet
- gateway_operator
- Пайщик
- Председатель
- Совет
- Кассир
# ── Секция 2. Действия контракта ────────────────────────────────────────────
actions:
- name: capital::createdebt
human: Подать заявление
actor: contributor
role: opener
actor: Пайщик
purpose: >
Пайщик создаёт заявку на беспроцентный целевой заём. Контракт фиксирует
запись долга в статусе `created`, обновляет сегмент CRPS пайщика на
сумму займа.
Пайщик подаёт заявление на беспроцентный целевой заём.
- name: capital::approvedebt
human: Одобрить решением председателя
actor: chairman
role: progress
human: Одобрить
actor: Председатель
purpose: >
Председатель добавляет к заявке документ-одобрение займа. Статус
заявки → `approved`.
Председатель одобряет заявление пайщика и выносит его на утверждение
советом.
- name: capital::debtauthcnfr
human: Авторизовать выплату
actor: soviet
role: progress
actor: Совет
purpose: >
Совет авторизует выплату займа. На этом этапе применяется ledger2-
операция o.cap.lend и Gateway получает поручение на исходящий платёж.
Статус → `authorized`.
Совет утверждает выдачу займа и поручает кассиру выполнить выплату.
- name: capital::debtpaycnfrm
human: Подтвердить выплату
actor: gateway_operator
role: closer
actor: Кассир
purpose: >
Gateway подтверждает зачисление займа на счёт пайщика. Статус → `paid`.
Это закрывающее действие: запись долга сохраняется до возврата
(операция o.cap.repay сработает уже на акте-2 в процессе РИД).
Кассир подтверждает зачисление денежных средств пайщику. С этого
момента отсчитывается срок возврата — один год.
- name: capital::declinedebt
human: Отклонить
actor: chairman
role: reject
actor: Председатель
purpose: >
Председатель или совет отклоняет заявку на заём. Сегмент CRPS откатывается,
запись долга удаляется, операции в ledger2 не создаются.
Председатель или совет отклоняет заявление о выдаче займа.
- name: capital::debtpaydcln
human: Отклонить выплату
actor: gateway_operator
role: reject
actor: Кассир
purpose: >
Gateway отклонил исходящий платёж по займу (тех. ошибка, возврат).
Запись долга удаляется.
Кассир сообщает, что выплата не прошла (технический сбой, возврат
средств банком). Заявление снимается.
- name: capital::closedebt
human: Закрыть займ
actor: Председатель совета
purpose: >
Если проект, под который был выдан заём, отменён до сдачи результата —
долг закрывается за счёт имущества, ранее переданного пайщиком
кооперативу в обеспечение по этому займу. Делается автоматически:
без сбора совета и без подписи пайщика.
# ── Секция 3. Граф состояний ────────────────────────────────────────────────
entity: capital::debt
entity_human: Долг пайщика
entity_source: cpp/capital/src/debt/
states:
- name: created
human: Заявление подано
description: >
Заявка на заём создана. Сегмент CRPS пайщика обновлён на запрошенную сумму.
Пайщик подал заявление на заём. Документ ждёт решения председателя.
kind: normal
- name: approved
human: Председатель одобрил
human: Одобрено председателем
description: >
К заявке добавлен документ-одобрение председателя. Заявка ждёт авторизации
советом.
Председатель одобрил заявление. Документ ушёл на утверждение совету.
kind: normal
- name: authorized
human: Совет авторизовал
human: Утверждено советом
description: >
Совет авторизовал заём, в ledger2 применена операция o.cap.lend
(Дт 58 / Кт 51), Gateway получил поручение на исходящий платёж.
Совет утвердил выдачу займа. Кассиру поручено выполнить выплату.
kind: normal
- name: paid
human: Заём выплачен
description: >
Gateway подтвердил зачисление займа пайщику. Запись долга остаётся
в реестре до момента возврата через акт приёма-передачи проекта.
kind: final
- name: removed
human: Отклонено
Кассир подтвердил зачисление средств пайщику. С этого дня
отсчитывается срок возврата — один год. Возврат произойдёт
автоматически вместе со сдачей результата работ по проекту.
kind: normal
- name: writeoff
human: Списано через имущество-обеспечение
description: >
Заявка отклонена на одном из этапов; запись удалена, операции
o.cap.lend / o.cap.repay не создавались.
Проект, под который был выдан заём, отменён до сдачи результата.
Долг закрыт автоматически за счёт имущества, переданного пайщиком
кооперативу в обеспечение по этому займу. Запись о займе остаётся
в реестре с этой пометкой для последующего аудита; никаких претензий
к пайщику не предъявляется.
kind: normal
- name: removed
human: Заявление отклонено
description: >
Заявление снято на одном из этапов согласования — председателем,
советом или кассиром.
kind: virtual
virtual: true
@@ -124,129 +114,141 @@ transitions:
- from: "∅"
to: created
action: capital::createdebt
actor: contributor
actor: Пайщик
guards:
- Пайщик имеет статус active и участвует в проекте.
- Сегмент CRPS пайщика актуален.
- provisional_amount ≥ запрошенная сумма займа.
- Заявление подписано ЭЦП.
- Пайщик — активный участник кооператива.
- Пайщик участвует в проекте, под который запрашивается заём.
- На сегменте пайщика хватает доступной суммы для запроса.
- Заявление подписано пайщиком.
- from: created
to: approved
action: capital::approvedebt
actor: chairman
actor: Председатель
guards:
- Документ одобрения председателя подписан.
- Председатель подписал решение об одобрении.
- from: approved
to: authorized
action: capital::debtauthcnfr
actor: soviet
ledger_code: o.cap.lend
operations:
- o.cap.lend
actor: Совет
guards:
- Документ авторизации совета подписан.
- Совет принял решение о выдаче займа.
- from: authorized
to: paid
action: capital::debtpaycnfrm
actor: gateway_operator
actor: Кассир
guards:
- Gateway подтвердил зачисление займа пайщику.
- Кассир подтвердил зачисление средств пайщику.
- from: created
to: removed
action: capital::declinedebt
actor: chairman
actor: Председатель
guards:
- Председатель или совет отклонили заявку.
- Председатель или совет отклонили заявление.
- from: approved
to: removed
action: capital::declinedebt
actor: soviet
actor: Совет
guards:
- Совет отклонил заявку.
- Совет отклонил заявление.
- from: authorized
to: removed
action: capital::debtpaydcln
actor: gateway_operator
actor: Кассир
guards:
- Платёж не прошёл / отклонён Gateway.
- Платёж не прошёл — банк отклонил или вернул перевод.
- from: paid
to: writeoff
action: capital::closedebt
actor: Председатель совета
guards:
- Проект, под который был выдан заём, отменён.
- У пайщика на сегменте есть имущество, переданное в обеспечение.
# ── Секция 4. Сценарий ──────────────────────────────────────────────────────
scenario:
steps:
- step: 1
title: Подача заявления на заём
actor: contributor
title: Подача заявления
actor: Пайщик
action: capital::createdebt
description: >
Пайщик подаёт заявление на беспроцентный целевой заём.
Контракт создаёт запись долга со статусом `created` и обновляет
сегмент CRPS на запрошенную сумму.
- step: 2
title: Одобрение председателем
actor: chairman
actor: Председатель
action: capital::approvedebt
description: >
Председатель добавляет к заявке документ-одобрение. Статус
заявки → `approved`.
Председатель рассматривает заявление и при согласии одобряет его,
вынося на утверждение совету.
- step: 3
title: Авторизация советом и выдача займа
actor: soviet
title: Утверждение советом и поручение на выплату
actor: Совет
action: capital::debtauthcnfr
description: >
Совет авторизует выплату. В ledger2 применяется операция o.cap.lend
(ISSUE → w.cap.loan, Дт 58 / Кт 51), Gateway получает поручение на
исходящий платёж. Статус → `authorized`.
Совет утверждает выдачу займа. Кассиру передаётся поручение на
перечисление средств пайщику.
- step: 4
title: Подтверждение выплаты
actor: gateway_operator
actor: Кассир
action: capital::debtpaycnfrm
description: >
Gateway подтверждает зачисление средств пайщику. Статус → `paid`.
Запись долга остаётся в реестре до момента возврата через акт-2
проекта (операция o.cap.repay сработает в процессе p.cap.rid).
Кассир выполняет перевод и подтверждает зачисление средств пайщику.
С этого дня отсчитывается срок возврата — один год.
alternatives:
- branch: Отказ председателя или совета
at_step: 2
action: capital::declinedebt
actor: chairman
actor: Председатель
description: >
Заявка отклонена. Сегмент CRPS откатывается, запись долга удаляется,
операции в ledger2 не создаются.
- branch: Отказ Gateway
Заявление отклонено на этапе согласования. Деньги пайщику не
перечисляются.
- branch: Не прошёл платёж
at_step: 4
action: capital::debtpaydcln
actor: gateway_operator
actor: Кассир
description: >
Платёж не прошёл / отклонён. Запись долга удаляется.
Банк отклонил или вернул перевод. Заявление снимается; пайщик может
подать его заново.
- branch: Невозврат — закрытие через имущество-обеспечение
at_step: 4
action: capital::closedebt
actor: Председатель совета
description: >
Если проект, под который был выдан заём, отменён до сдачи
результата — долг закрывается автоматически за счёт имущества,
переданного пайщиком кооперативу в обеспечение по этому займу.
Сбора совета не требуется, подпись пайщика не требуется,
претензий к пайщику не предъявляется.
# ── Секция 5. Документы и подписи ───────────────────────────────────────────
documents:
- step: 1
- action: capital::createdebt
title: Заявление на получение займа
registry_id: 1050
signed_by: [contributor]
signed_by: [Пайщик]
stored_in: debts.statement
- step: 2
- action: capital::approvedebt
title: Решение совета о предоставлении займа (одобрение председателем)
registry_id: 1051
signed_by: [chairman]
signed_by: [Председатель]
stored_in: debts.approved_statement
- step: 3
title: Решение совета о предоставлении займа (авторизация совета)
- action: capital::debtauthcnfr
title: Решение совета о предоставлении займа (утверждение советом)
registry_id: 1051
signed_by: [soviet]
signed_by: [Совет]
stored_in: debts.authorization
# ── Секция 6. Операции ──────────────────────────────────────────────────────
@@ -254,32 +256,65 @@ operations:
- ledger_code: o.cap.lend
human_name: Выдача пайщику беспроцентного займа
wallet_op: ISSUE
wallet_from: ''
wallet_to: w.cap.loan # Выданные пайщикам беспроцентные займы
debit: 58 # Финансовые вложения
credit: 51 # Расчётный счёт
debit: 58
credit: 51
wallet_from: null
wallet_to: w.cap.loan
amount_ref: debt.amount
triggered_by: capital::debtauthcnfr
description: >
Выдача беспроцентного займа пайщику. Деньги уходят с расчётного счёта
кооператива (Кт 51) и появляются как финансовое вложение Дт 58.
Кошелёк «Выданные пайщикам беспроцентные займы» (w.cap.loan) фиксирует
обязательство пайщика перед кооперативом.
Кооператив перечисляет пайщику беспроцентный целевой заём. На стороне
кооператива деньги уходят с расчётного счёта и фиксируются как
финансовое вложение в пайщика-должника.
- ledger_code: o.cap.repay
human_name: Возврат беспроцентного займа пайщика по акту-2
human_name: Возврат беспроцентного займа пайщика при сдаче результата
wallet_op: TRANSFER
wallet_from: w.cap.loan # Выданные пайщикам беспроцентные займы
wallet_to: w.wal.share # ЦПП «Цифровой Кошелёк» — паевые взносы деньгами
debit: 80 # Паевой фонд (складочный капитал)
credit: 58 # Финансовые вложения
debit: 80
credit: 58
wallet_from: w.cap.loan
wallet_to: w.wal.share
user_wallet: w.wal.share
user_ref: debt.username
available_delta: +debt.amount
blocked_delta: null
amount_ref: debt.amount
triggered_by: capital::debtpaycnfrm
description: >
Возврат займа в момент сдачи акта-2 проекта (происходит как часть
процесса «Приём РИД» — p.cap.rid). Финансовое вложение закрывается
(Кт 58), сумма зачитывается в паевой фонд (Дт 80) и появляется как
доступный остаток на SHARE_FUND_PAY пайщика.
Происходит в момент сдачи результата работ по проекту
(подписанный акт приёма-передачи). Долг пайщика гасится, а сумма
займа зачитывается ему как паевой взнос — появляется как свободный
остаток на личном паевом кошельке пайщика.
- ledger_code: o.cap.crtnma
human_name: Создание имущества кооператива из обеспечения по займу
wallet_op: TRANSFER
debit: 4
credit: 8
wallet_from: w.cap.gen
wallet_to: w.cap.nma
amount_ref: debt.amount
triggered_by: capital::closedebt
description: >
Срабатывает при невозврате: проект, под который был выдан заём,
отменён до сдачи результата. Имущество, ранее переданное пайщиком
кооперативу в обеспечение, переходит в собственность кооператива
как нематериальный актив.
- ledger_code: o.cap.dbtwrf
human_name: Списание долга за счёт имущества-обеспечения
wallet_op: BURN
debit: 80
credit: 58
wallet_from: w.cap.loan
wallet_to: null
amount_ref: debt.amount
triggered_by: capital::closedebt
description: >
Парная операция к созданию имущества из обеспечения: обязательство
пайщика по займу закрывается. На паевой кошелёк пайщика ничего не
зачисляется — долг закрыт имущественно, через переданное в
обеспечение имущество.
# ── Секция 7. Связи ─────────────────────────────────────────────────────────
related:
@@ -287,11 +322,20 @@ related:
id: public_capital_rid_process
relation: triggers
note: >
Возврат займа (o.cap.repay) технически происходит в процессе
«Приём РИД» при подписании акта-2 проекта.
Возврат займа физически происходит при сдаче результата работ по
проекту (подписанный акт приёма-передачи) — в процессе «Приём
результата интеллектуальной деятельности».
- process_type: p.reg.accept
id: public_registrator_accept_process
relation: provides
note: >
Заём может получить только активный пайщик кооператива.
- process_type: p.cap.rid
id: public_capital_rid_process
relation: affects
note: >
Закрытие долга через имущество-обеспечение уменьшает на сегменте
пайщика ту часть, которая раньше была передана в обеспечение и
теперь стала собственностью кооператива.
@@ -77,9 +77,12 @@ namespace operations {
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 CREATE_NMA = "o.cap.crtnma"_n; ///< Создать НМА кооператива из коммитов-обеспечения долга при невозврате займа (Dr 04 / Cr 08, TRANSFER GENERATOR_FUND → NMA). Решение 2026-05-19.
inline constexpr eosio::name DEBT_WRITEOFF = "o.cap.dbtwrf"_n; ///< Списание долга (write-off) за счёт созданного НМА (Dr 80 / Cr 58, BURN LOAN_ISSUED). Зеркало REPAY без зачисления на share пайщика — долг закрыт имущественно. Решение 2026-05-19.
inline constexpr eosio::name WITHDRAW_FROM_CAPITAL = "o.cap.wthcap"_n; ///< Возврат паевого из ЦПП «Благорост» в кошелёк пайщика (TRANSFER BLAGOROST_FUND → SHARE_FUND_PAY, без Dr/Cr).
inline constexpr eosio::name CONVERT_TO_SHARE = "o.cap.cnvshr"_n; ///< Конвертация сегмента: РИД → главный кошелёк (TRANSFER GENERATOR_FUND → SHARE_FUND_PAY, без Dr/Cr — бухпроводка уже была сделана в ACCEPT_RID).
inline constexpr eosio::name CONVERT_TO_BLAGO = "o.cap.cnvbl"_n; ///< Конвертация сегмента: РИД → ЦПП «Благорост» (TRANSFER GENERATOR_FUND → BLAGOROST_FUND, без Dr/Cr — бухпроводка уже была сделана в ACCEPT_RID).
inline constexpr eosio::name PAY_EXPENSE = "o.cap.expns"_n; ///< Оплата целевого расхода программы (Dr 86 / Cr 51, TRANSFER BLAGOROST_FUND → SOV_EXPENSES). Списание целевого финансирования на расчётный счёт.
}
// marketplace
@@ -252,6 +255,28 @@ static constexpr OperationRegistryEntry OPERATION_REGISTRY[] = {
ledger2_accounts::SHARE_FUND, ledger2_accounts::FINANCIAL_INVESTMENTS,
"Возврат беспроцентного займа пайщика по акту-2" },
// 11a. Создать НМА кооператива из коммитов-обеспечения долга при невозврате:
// Dr 04 / Cr 08, TRANSFER GENERATOR_FUND → NMA.
// Срабатывает автоматически из `capital::closedebt`, когда родительский
// компонент перешёл в `cancelled` (решение 2026-05-19 — без сбора совета,
// без подписи пайщика). РИД-коммиты, накопленные на GENERATOR_FUND пайщика
// как обеспечение долга, формируют НМА кооператива на отдельном кошельке
// `w.cap.nma`. Пара по 04 зеркалит `o.cap.accept`, но без участия пайщика —
// сегмент пайщика уменьшается на сумму операции.
{ operations::capital::CREATE_NMA, processes::capital::DEBT, WalletOp::TRANSFER,
ledger2_wallets::GENERATOR_FUND, ledger2_wallets::NMA,
ledger2_accounts::INTANGIBLE_ASSETS, ledger2_accounts::NON_CURRENT_INVESTMENTS,
"Создание НМА кооператива из коммитов-обеспечения долга" },
// 11b. Списание долга (write-off) за счёт созданного НМА: Dr 80 / Cr 58, BURN LOAN_ISSUED.
// Парная к `o.cap.crtnma` операция, закрывает обязательство пайщика по займу.
// Зеркало `o.cap.repay`, но без зачисления на share пайщика (долг закрыт
// имущественно — через НМА из коммитов-обеспечения). Решение 2026-05-19.
{ operations::capital::DEBT_WRITEOFF, processes::capital::DEBT, WalletOp::BURN,
ledger2_wallets::LOAN_ISSUED, eosio::name{},
ledger2_accounts::SHARE_FUND, ledger2_accounts::FINANCIAL_INVESTMENTS,
"Списание долга (write-off) за счёт созданного НМА" },
// 12. Подтверждение поставки: Dr 51 / Cr 80, ISSUE SHARE_FUND_PAY
{ operations::marketplace::CONFIRM_SUPPLY, processes::marketplace::REQUEST, WalletOp::ISSUE, eosio::name{}, ledger2_wallets::SHARE_FUND_PAY,
ledger2_accounts::BANK_ACCOUNT, ledger2_accounts::SHARE_FUND,
@@ -305,6 +330,16 @@ static constexpr OperationRegistryEntry OPERATION_REGISTRY[] = {
0, 0,
"Конвертация сегмента: РИД → ЦПП «Благорост»" },
// 20. Оплата целевого расхода программы: Dr 86 / Cr 51, TRANSFER BLAGOROST_FUND → SOV_EXPENSES.
// Закрывает TODO в exppaycnfrm.cpp: списывает сумму расхода из программного фонда
// «Благорост» в пул «Хозяйственные расходы из числа целевого финансирования»;
// одновременно фиксируется бухпроводка по уменьшению целевого финансирования (86)
// и списанию с расчётного счёта (51).
{ operations::capital::PAY_EXPENSE, processes::capital::EXPENSE, WalletOp::TRANSFER,
ledger2_wallets::BLAGOROST_FUND, ledger2_wallets::SOV_EXPENSES,
ledger2_accounts::TARGET_RECEIPTS, ledger2_accounts::BANK_ACCOUNT,
"Оплата целевого расхода по программе «Благорост»" },
// ----- Миграционные (o.mig.*) — вызываются только из migrate.cpp -----
// 15. Миграция: минимальный паевой: Dr 51 / Cr 80, ISSUE MIN_SHARE_FUND
@@ -48,11 +48,12 @@ namespace processes {
namespace capital {
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 DEBT = "p.cap.debt"_n; ///< Займ пайщику (o.cap.lend + o.cap.repay; альтернативная ветка невозврата — o.cap.crtnma + o.cap.dbtwrf, решение 2026-05-19).
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; ///< Возврат паевого из ЦПП «Благорост» в кошелёк пайщика (одноактовый).
inline constexpr eosio::name EXPENSE = "p.cap.expns"_n; ///< Целевой расход программы (создание → одобрение председателя → авторизация совета → outpay → подтверждение оплаты). Anchor — expense_hash.
}
// marketplace
@@ -63,6 +63,7 @@ struct ledger2_wallets {
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)
static constexpr eosio::name NMA = "w.cap.nma"_n; ///< НМА кооператива — собственное имущество, перешедшее из коммитов-обеспечения при невозврате займа (COOPERATIVE; решение 2026-05-19)
// marketplace — выплаты
static constexpr eosio::name SUPPLIER_PAYMENTS = "w.mkt.payout"_n; ///< Выплаты поставщикам (sink RECEIVE_CONFIRM, COOPERATIVE)
@@ -94,7 +95,7 @@ struct Ledger2WalletMeta {
WalletKind kind;
};
inline constexpr std::array<Ledger2WalletMeta, 14> LEDGER2_WALLET_REGISTRY = {{
inline constexpr std::array<Ledger2WalletMeta, 15> LEDGER2_WALLET_REGISTRY = {{
// USER_SHARED (6) — L3-разрез по пайщику
{ ledger2_wallets::MIN_SHARE_FUND, "Минимальный паевой взнос", WalletKind::USER_SHARED },
{ ledger2_wallets::SHARE_FUND_PAY, "Паевой взнос пайщика", WalletKind::USER_SHARED },
@@ -103,7 +104,7 @@ inline constexpr std::array<Ledger2WalletMeta, 14> LEDGER2_WALLET_REGISTRY = {{
{ ledger2_wallets::GENERATOR_FUND, "ЦПП «Генератор» — единый кошелёк программы у пайщика", WalletKind::USER_SHARED },
{ ledger2_wallets::PREIMP_FUND, "Первичный учёт РИД-взносов до перехода на электронный учёт", WalletKind::USER_SHARED },
// COOPERATIVE (8) — единый кооперативный баланс, без L3
// COOPERATIVE (9) — единый кооперативный баланс, без L3
{ ledger2_wallets::ENTRANCE_FEES, "Вступительные взносы", WalletKind::COOPERATIVE },
{ ledger2_wallets::WITHDRAWALS_SINK, "Возвраты паевых взносов пайщикам", WalletKind::COOPERATIVE },
{ ledger2_wallets::INFRA_FEES, "Членские взносы за инфраструктуру кооп. платформы", WalletKind::COOPERATIVE },
@@ -112,6 +113,7 @@ inline constexpr std::array<Ledger2WalletMeta, 14> LEDGER2_WALLET_REGISTRY = {{
{ ledger2_wallets::MIN_SHARE_USED, "Использованные минимальные паевые взносы", WalletKind::COOPERATIVE },
{ ledger2_wallets::LOAN_ISSUED, "Выданные пайщикам беспроцентные займы", WalletKind::COOPERATIVE },
{ ledger2_wallets::SUPPLIER_PAYMENTS, "Выплаты поставщикам", WalletKind::COOPERATIVE },
{ ledger2_wallets::NMA, "НМА кооператива (из коммитов-обеспечения по займам)", WalletKind::COOPERATIVE },
}};
static constexpr size_t LEDGER2_WALLET_REGISTRY_SIZE = LEDGER2_WALLET_REGISTRY.size();
@@ -50,6 +50,12 @@ namespace Names {
// Коллбэки для расходов
constexpr eosio::name AUTHORIZE_EXPENSE = "capauthexpns"_n;
constexpr eosio::name DECLINE_EXPENSE = "capdeclexpns"_n;
// Коллбэки для программных расходов
// (CREATE_PROGRAM_EXPENSE объявлен ниже в блоке "акцептов" — общий для обоих контекстов)
constexpr eosio::name AUTHORIZE_PROGRAM_EXPENSE = "authpgexp"_n;
constexpr eosio::name DECLINE_PROGRAM_EXPENSE = "declpgexp"_n;
constexpr eosio::name CONFIRM_PROGRAM_EXPENSE_PAYMENT = "pgexppay"_n;
// Коллбэки для долгов
constexpr eosio::name AUTHORIZE_DEBT = "debtauthcnfr"_n;
@@ -91,6 +97,7 @@ namespace Names {
constexpr eosio::name CAPITAL_WITHDRAW_FROM_PROGRAM = "capwthdrprog"_n;
constexpr eosio::name CAPITAL_WITHDRAW_FROM_PROJECT = "capwthdrproj"_n;
constexpr eosio::name CAPITAL_RESOLVE_EXPENSE = "capresexpns"_n;
constexpr eosio::name CAPITAL_RESOLVE_PROGRAM_EXPENSE = "caprespgexp"_n;
constexpr eosio::name CREATE_RESULT = "createresult"_n;
constexpr eosio::name CREATE_DEBT = "createdebt"_n;
constexpr eosio::name CREATE_PROJECT = "createprj"_n;
@@ -120,6 +127,7 @@ namespace Names {
constexpr eosio::name CREATE_INVESTMENT = "createinvest"_n; // акцепт инвестиции
constexpr eosio::name CREATE_PROGRAM_INVESTMENT = "createpinv"_n; // акцепт программной инвестиции
constexpr eosio::name CREATE_EXPENSE = "createexpnse"_n; // акцепт расхода
constexpr eosio::name CREATE_PROGRAM_EXPENSE = "createpgexp"_n; // акцепт программного расхода
constexpr eosio::name CREATE_WITHDRAW_1 = "createwthd1"_n; // акцепт возврата из задания
constexpr eosio::name CREATE_WITHDRAW_2 = "createwthd2"_n; // акцепт возврата из проекта
constexpr eosio::name CREATE_WITHDRAW_3 = "createwthd3"_n; // акцепт возврата из программы
@@ -19,6 +19,11 @@ struct [[eosio::table, eosio::contract(LOAN)]] debt {
time_point_sec created_at;
time_point_sec repaid_at;
// Контракт-источник, инициировавший выдачу займа (capital / marketplace / ...).
// loan не знает доменного контекста (проекта, программы) — это ответственность
// вызывающего контракта; ему же принадлежит и локальная привязка debt_hash.
name source_contract;
uint64_t primary_key() const { return id; }
uint64_t by_username() const { return username.value; }
checksum256 by_debt_hash() const { return debt_hash; }
@@ -2,10 +2,13 @@ using namespace Loan;
/**
* @brief Создание долгового обязательства.
* Создает новое долговое обязательство для пользователя с указанным сроком погашения
* Создает новое долговое обязательство пайщика. loan-контракт независим от
* прикладных контрактов — здесь нет привязки к проекту/программе/компоненту.
* Локальный контекст (project_hash и т.п.) хранится на стороне вызывающего
* контракта в его собственной таблице долгов.
* @param coopname Наименование кооператива
* @param username Имя пользователя-должника
* @param debt_hash Хэш долгового обязательства
* @param debt_hash Хэш долгового обязательства (идентификатор)
* @param repaid_at Срок погашения долга
* @param quantity Сумма долга
* @ingroup public_actions
@@ -33,6 +36,7 @@ void loan::createdebt(name coopname, name username, checksum256 debt_hash, time_
d.amount = quantity;
d.created_at = time_point_sec(current_time_point());
d.repaid_at = repaid_at;
d.source_contract = payer;
});
Loan::summaries_index summaries(_loan, coopname.value);