Merge remote-tracking branch 'origin/ledger3' into marketplace2

# Conflicts:
#	components/contracts/cpp/capital/p.cap.rid.standard.yaml
This commit is contained in:
coopops
2026-05-09 14:42:10 +00:00
45 changed files with 1770 additions and 4104 deletions
+2 -2
View File
@@ -12,7 +12,7 @@ fi
# Останавливаем и удаляем контейнеры вместе с volumes
echo "Останавливаем и удаляем контейнеры с volumes..."
docker compose down -v mongo postgres opensearch monoredis cooparser coopback || true
docker compose down -v mongo postgres monoredis cooparser coopback || true
# Останавливаем blockchain контейнер перед удалением данных
echo "Останавливаем blockchain контейнер..."
@@ -25,7 +25,7 @@ sudo rm -rf ../blockchain-data/
# Пересоздаем и запускаем базы данных
echo "Пересоздаем и запускаем базы данных..."
docker compose up -d mongo postgres opensearch monoredis
docker compose up -d mongo postgres monoredis
# Ждем готовности MongoDB
echo "Ждем готовности MongoDB..."
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/bash
cleos -u http://172.17.0.1:8888 "$@"
cleos -u http://172.17.0.1:8908 "$@"
@@ -14,7 +14,12 @@
* @note Авторизация требуется от аккаунта кооператива: @p coopname (@c active)
*/
void capital::regshare(eosio::name coopname, checksum256 project_hash, eosio::name username, eosio::asset user_shares) {
require_auth(coopname);
// Прямой вызов от кооператива (планировщик контроллера) ИЛИ inline-вызов
// от системного контракта (например, capital::apprvappndx — авторегистрация
// доли при допуске участника к проекту).
if (!has_auth(coopname)) {
check_auth_and_get_payer_or_fail(contracts_whitelist);
}
// Проверяем существование проекта
auto project = Capital::Projects::get_project_or_fail(coopname, project_hash);
@@ -26,14 +26,24 @@ void capital::createpinv(name coopname, name username, checksum256 invest_hash,
auto contributor = Capital::Contributors::get_active_contributor_or_fail(coopname, username);
// Проверяем наличие кошелька в программе благороста (_capital_wallet)
auto capital_wallet = Capital::Wallets::get_program_capital_wallet(coopname, username);
eosio::check(capital_wallet.has_value(), "У пайщика нет кошелька в программе благороста");
eosio::check(Capital::Wallets::has_program_capital_wallet(coopname, username),
"У пайщика нет кошелька в программе благороста");
std::string memo = Capital::Memo::get_program_invest_memo(contributor -> id);
// Как в createinvest: программные инвесторы не проходят через signact2 — фиксируем вклад здесь
Capital::Contributors::increase_investor_contribution(coopname, contributor -> id, amount);
// Геймификация: программный инвестор не имеет сегмента, поэтому energy_gain
// считаем от суммы напрямую (как в createinvest, только без агрегации по
// ролям сегмента). Без этого вызова уровень/энергия не растут при прямой
// инвестиции в Благорост.
{
auto config = Capital::State::get_global_state(coopname).config;
double energy_gain = Capital::Gamification::calculate_energy_gain(amount, contributor -> level, config);
Capital::Gamification::add_energy_and_check_levelup(coopname, contributor -> id, energy_gain);
}
// ledger2: TRANSFER w.wal.share → w.cap.blago (без бухпроводки — оба счёта 80).
// Источник правды UI для балансов кошельков пайщика — L3 ledger2::userwallets.
Ledger2::apply(_capital, coopname, operations::capital::INVEST, amount,
@@ -41,6 +41,22 @@ void capital::apprvappndx(eosio::name coopname, eosio::name username, checksum25
// Фиксируем документ в реестре как принятый
Soviet::make_complete_document(_capital, coopname, appendix->username, Names::Capital::APPROVE_APPENDIX, appendix_hash, approved_document);
// Авторегистрация доли по программе «Благорост» при допуске к проекту:
// если у пайщика уже есть баланс w.cap.blago — сразу заводим сегмент с
// user_shares = available + blocked. Без баланса regshare не вызываем —
// сегмент создастся при ближайшем тике scheduler'а, как только появится
// первая инвестиция в программу.
const auto user_shares =
Capital::Core::get_capital_program_user_share_balance(coopname, appendix->username);
if (user_shares.amount > 0) {
eosio::action(
eosio::permission_level{_capital, "active"_n},
_capital,
"regshare"_n,
std::make_tuple(coopname, appendix->project_hash, appendix->username, user_shares)
).send();
}
// Удаляем приложение
Capital::Appendix::delete_appendix(coopname, appendix->id);
}
@@ -100,8 +100,7 @@ void capital::regcontrib(eosio::name coopname, eosio::name username, checksum256
}
// Открываем кошельки для пайщика если необходимо
auto program_wallet = get_program_wallet(coopname, username, _source_program);
if (!program_wallet.has_value()) {
if (!has_program_wallet(coopname, username, _source_program)) {
Action::send<openprogwall_interface>(
_soviet,
Names::External::OPEN_PROGRAM_WALLET,
@@ -113,7 +112,7 @@ void capital::regcontrib(eosio::name coopname, eosio::name username, checksum256
);
}
if (!get_program_wallet(coopname, username, _capital_program).has_value()) {
if (!has_program_wallet(coopname, username, _capital_program)) {
Action::send<openprogwall_interface>(
_soviet,
Names::External::OPEN_PROGRAM_WALLET,
@@ -1,109 +0,0 @@
/**
@defgroup public_capital_conversion_process Конвертация взноса результатом
@ingroup public_capital_processes
@pre Результат интеллектуальной деятельности внесен в кооператив
@pre Выданная ссуда погашена
@pre Статус сегмента CONTRIBUTED
@pre Долг уменьшает доступные для конвертации в кошелек суммы создателя/автора/координатора
После внесения результата в кооператив - баланс пользователя должен быть сконвертирован в одно из трех направлений:
в главный кошелёк для немедленного использования в других программах или для возврата на банковский счёт, в кошелёк программы "Благорост"
для долгосрочного участия или в кошелёк проекта для получения возврата паевого взноса из числа членских взносов этого проекта.
@note Ограничения по направлениям конвертации:
- В главный кошелёк: только себестоимость создателей, авторов и координаторов минус погашенный долг, не больше обеспеченной суммы
- В кошелёк проекта: только себестоимость всех ролей (создатели, авторы, координаторы, инвесторы, пропертор)
- В кошелёк программы: все взносы минус погашенный долг минус суммы, направленные в проект и кошелек
\htmlonly
<div class="mermaid">
sequenceDiagram
participant В as Участник
participant C as Capital Contract
participant W as Wallet Contract
В->>C: Конвертация сегмента
Note over C: Проверка статуса CONTRIBUTED
Note over C: Проверка погашения долга
Note over C: Валидация ограничений по суммам
Note over C: Проверка разрешения can_convert_to_project
alt В главный кошелёкёк
C->>W: Разблокировка из программного кошелька
C->>W: Зачисление в доступные средства
Note over W: Средства доступны для вывода
end
alt В программу Капитализация
C->>W: Разблокировка из программного кошелька
C->>W: Блокировка в программе благороста
Note over W: Получение доли в CRPS программы
end
alt В кошелёк проекта
C->>C: Создание/обновление кошелька проекта
C->>C: Увеличение долей в проекте
Note over C: Право на членские взносы проекта
end
Note over C: Статус сегмента изменяется на COMPLETED
</div>
\endhtmlonly
## Пошаговое описание процесса
### Шаг 1: Конвертация сегмента
<b>Действие:</b> @ref capital::convertsegm "конвертация сегмента участника"
Участник распределяет свои доступные средства между тремя направлениями согласно
выбранной стратегии. Система проверяет соблюдение всех ограничений:
- Долг должен быть погашен (debt_settled >= debt_amount)
- Общая сумма конвертации равна всем взносам участника минус погашенный долг
- В главный кошелёкёк: только себестоимость создателей, авторов и координаторов минус погашенный долг, не больше обеспеченной суммы
- В кошелёк проекта: только себестоимость всех ролей (создатели, авторы, координаторы, инвесторы, пропертор)
- В благорост: все взносы минус погашенный долг минус конвертированные в проект и кошелек
- Проект разрешает конвертацию в свой кошелёк
@post разблокируются средства из программного кошелька
@post часть средств зачисляется в доступные для вывода
@post часть средств блокируется в программе благороста
@post часть средств направляется в кошелёк проекта
@post статус сегмента изменяется на COMPLETED
@post увеличиваются доли участника в выбранных направлениях
## Направления конвертации
<b>главный кошелёкёк</b> - средства становятся доступными для немедленного возврата
и использования. Конвертировать можно только себестоимость создателей, авторов
и координаторов минус погашенный долг, но не больше обеспеченной суммы.
<b>Программа Капитализация</b> - средства блокируются для участия в долгосрочной
программе накопления с выгодной от членских взносов.
Конвертируются все взносы минус погашенный долг минус те, что направлены в проект и кошелек.
Общая сумма конвертации должна равняться всем взносам участника минус погашенный долг.
<b>Кошелёк проекта</b> - средства остаются в проекте для получения членских
взносов конкретно от данного проекта. Конвертировать можно только себестоимость
всех ролей (создатели, авторы, координаторы, инвесторы, пропертор).
Требует разрешения проекта на конвертацию.
При наличии родительского проекта конвертация будет происходить в его кошелёк.
## Эффекты процесса
@include{doc} convertsegm-effect.dox
## Документы процесса
@include{doc} conversion-process-docs.dox
## Ограничения конвертации
- <b>В главный кошелёкёк:</b> только себестоимость создателей, авторов и координаторов минус погашенный долг, не больше обеспеченной суммы
- <b>В кошелёк проекта:</b> только себестоимость всех ролей (создатели, авторы, координаторы, инвесторы, пропертор)
- <b>В благорост:</b> все взносы минус погашенный долг минус конвертированные в проект и кошелек
- <b>Общее требование:</b> сумма всех конвертаций должна равняться всем взносам участника минус погашенный долг
- <b>Долг:</b> должен быть погашен перед конвертацией (debt_settled >= debt_amount), уменьшает доступные суммы для кошелька
- Конвертация в проект требует разрешения проекта
- При наличии родительского проекта средства направляются в него
*/
@@ -1,24 +1,26 @@
/**
*
* @brief Конвертирует сегмент участника в кошелек и программу благороста
* Конвертирует сегмент участника в кошелек и капитал:
* - Проверяет статус сегмента: CONTRIBUTED (результат внесён через pushrslt)
* - Валидирует актуальность сегмента
* - Проверяет что долг уже погашен (если был)
* - Проверяет наличие средств для конвертации (с учетом погашенного долга)
* - Валидирует корректность сумм конвертации
* - Выполняет операции с балансами (кошелек, капитал)
* - Удаляет сегмент после конвертации
*
*
* Для участников со смешанными ролями (инвестор + другие): investor_base уже в _capital_program,
* а интеллектуальные вклады в _source_program. Конвертация перемещает только неинвесторскую часть.
*
* @brief Конвертация сегмента финальная фаза процесса внесения РИД (`p.cap.rid`).
*
* Внесение РИД состоит из трёх ончейн-фаз, объединённых одним `result_hash`:
* 1. `pushrslt` заявление + закрытие долга (статус `STATEMENT`/`ACT1`).
* 2. `signact2` председатель подписывает акт-2: ACCEPT_RID (Dr 04 / Cr 08),
* REPAY (если был долг). Сегмент `CONTRIBUTED`, результат `ACT2`.
* 3. `convertsegm` (этот action) пайщик распределяет накопленный
* `available_for_program` между ЦК (SHARE_FUND_PAY) и
* ЦПП «Благорост» (BLAGOROST_FUND), кошельковое движение
* уже без бухпроводок (Dr/Cr закрыто в ACCEPT_RID).
* После конвертации сегмент удаляется, результат удаляется.
*
* Семантика проверок:
* - сегмент в статусе `CONTRIBUTED`, актуален (rfrshsegment);
* - долг к моменту конвертации погашен;
* - суммы конвертации соответствуют долям сегмента (см. eosio::check ниже);
* - объект результата всё ещё существует и в статусе `ACT2` (анкер процесса).
*
* @param coopname Наименование кооператива
* @param username Наименование пользователя-участника
* @param project_hash Хеш проекта
* @param convert_hash Хеш конвертации
* @param result_hash Хеш результата (анкер процесса `p.cap.rid`)
* @param wallet_amount Сумма для конвертации в кошелек
* @param capital_amount Сумма для конвертации в капитал
* @param convert_statement Заявление о конвертации
@@ -27,46 +29,54 @@
* @note Авторизация требуется от аккаунта: @p coopname
*/
void capital::convertsegm(eosio::name coopname, eosio::name username,
checksum256 project_hash, checksum256 convert_hash,
checksum256 project_hash, checksum256 result_hash,
eosio::asset wallet_amount, eosio::asset capital_amount,
document2 convert_statement) {
require_auth(coopname);
// Получаем сегмент пайщика
auto segment = Capital::Segments::get_segment_or_fail(coopname, project_hash, username,
auto segment = Capital::Segments::get_segment_or_fail(coopname, project_hash, username,
"Сегмент пайщика не найден");
// Определяем целевой проект для конвертации
auto current_project = Capital::Projects::get_project_or_fail(coopname, project_hash);
// Объект результата живёт от pushrslt до convertsegm (анкер процесса).
auto result = Capital::Results::get_result(coopname, result_hash);
eosio::check(result.has_value(), "Объект результата не найден по result_hash");
eosio::check(result->project_hash == project_hash, "result_hash не принадлежит указанному проекту");
eosio::check(result->username == username, "result_hash не принадлежит указанному пользователю");
eosio::check(result->status == Capital::Results::Status::ACT2,
"Неверный статус результата. Ожидается 'act2' (после signact2)");
// Участники с интеллектуальными ролями должны сначала внести результат
eosio::check(segment.status == Capital::Segments::Status::CONTRIBUTED,
"Результат не внесён. Сначала внесите результат через pushrslt");
eosio::check(segment.status == Capital::Segments::Status::CONTRIBUTED,
"Результат не внесён. Сначала внесите результат через pushrslt → signact2");
// Проверяем актуальность сегмента (включая синхронизацию с инвестициями)
Capital::Core::check_segment_is_updated(coopname, current_project, segment, "Сегмент не обновлен. Выполните rfrshsegment перед конвертацией");
// === ФАЗА 1: БАЗОВЫЕ ПРОВЕРКИ ===
Wallet::validate_asset(wallet_amount);
Wallet::validate_asset(capital_amount);
// Доступная сумма для конвертации в кошелек (только из интеллектуальных вкладов)
eosio::asset available_for_wallet = segment.provisional_amount - segment.debt_amount;
eosio::check(wallet_amount <= available_for_wallet, "Сумма конвертации в кошелек превышает доступную и обеспеченную суммы. Для конвертации доступна обеспеченная сумма за вычетом суммы выданных ссуд (provisional_amount - debt_amount)");
// Доступная сумма для конвертации в программу
eosio::asset available_for_program = segment.available_for_program;
// === ФАЗА 2: ВАЛИДАЦИЯ С СЕГМЕНТОМ ===
// Проверяем что общая сумма конвертации (кошелек + капитал) не превышает интеллектуальную стоимость сегмента
eosio::asset total_convert = wallet_amount + capital_amount;
eosio::check(total_convert <= segment.intellectual_cost,
eosio::check(total_convert <= segment.intellectual_cost,
"Общая сумма конвертации (" + total_convert.to_string() + ") не может превышать интеллектуальную стоимость сегмента (" + segment.intellectual_cost.to_string() + ")");
// Проверяем что общая сумма конвертации (кошелек + капитал) вместе с долгом равна интеллектуальной стоимости
// Это критическая проверка: мы должны списать ровно столько, сколько заблокировано в _source_program
eosio::check(total_convert + segment.debt_amount == segment.intellectual_cost,
eosio::check(total_convert + segment.debt_amount == segment.intellectual_cost,
"Сумма конвертации (" + total_convert.to_string() + ") и долга (" + segment.debt_amount.to_string() + ") должна быть равна интеллектуальной стоимости сегмента (" + segment.intellectual_cost.to_string() + ")");
eosio::check(total_convert == available_for_program, "Общая сумма конвертации должна равняться доступной сумме (intellectual_cost - debt_amount). Передано: " + total_convert.to_string() + ", Ожидается: " + available_for_program.to_string());
@@ -74,14 +84,16 @@ void capital::convertsegm(eosio::name coopname, eosio::name username,
eosio::check(capital_amount <= available_for_program, "Сумма конвертации в программу превышает сумму себестоимости и премий за вычетом суммы выданных ссуд");
eosio::check(capital_amount == (available_for_program - wallet_amount), "В программу должно быть сконвертировано всё доступное, что не конвертируется в кошелёк");
// Конвертация сегмента после ACT2: GENERATOR_FUND расщепляется на две цели —
// часть пайщику деньгами в ЦК, часть в Благорост-капитал.
// часть пайщику деньгами в ЦК, часть в Благорост-капитал. Бухпроводка Dr 04 / Cr 08
// уже сделана в signact2 (ACCEPT_RID, WalletOp::NONE) на полный available_for_program;
// здесь только кошельковые TRANSFER без проводок.
// Σ wallet_amount + capital_amount == available_for_program (проверено выше).
if (wallet_amount.amount > 0) {
Ledger2::apply(_capital, coopname, operations::capital::CONVERT_TO_SHARE,
wallet_amount, username, convert_hash,
Capital::Memo::get_convert_segment_to_wallet_memo(convert_hash));
wallet_amount, username, result_hash,
Capital::Memo::get_convert_segment_to_wallet_memo(result_hash));
// Учитываем использование инвестиций для компенсации
Capital::Projects::add_used_for_compensation(coopname, current_project.id, wallet_amount);
@@ -89,20 +101,24 @@ void capital::convertsegm(eosio::name coopname, eosio::name username,
if (capital_amount.amount > 0) {
Ledger2::apply(_capital, coopname, operations::capital::CONVERT_TO_BLAGO,
capital_amount, username, convert_hash,
Capital::Memo::get_convert_segment_to_capital_memo(convert_hash));
capital_amount, username, result_hash,
Capital::Memo::get_convert_segment_to_capital_memo(result_hash));
}
// Инкрементируем счётчик сконвертированных сегментов
Capital::Projects::increment_converted_segments(coopname, current_project.id);
// Удаляем сегмент после конвертации
Capital::Segments::remove_segment(coopname, segment.id);
// Удаляем объект результата — конвертация завершает процесс p.cap.rid.
// До этого момента result жил со статусом ACT2 как анкер процесса.
Capital::Results::delete_result(coopname, result->id);
}
// Конвертация реализована с учетом того что investor_base уже в _capital_program:
// - В кошелек: provisional_amount - debt_amount, не больше available_for_wallet
// - В благорост: все взносы минус долг минус кошелек (investor_base уже там)
// - total_segment_available = total_segment_cost - debt_amount (долг уже погашен в pushrslt)
// - Долг погашается только за счет базовых сумм создателя/автора/координатора
// - Долг погашается только за счет базовых сумм создателя/автора/координатора
@@ -4,12 +4,15 @@
* @ingroup public_capital_processes
*
* Процесс приёма паевого взноса результатом интеллектуальной деятельности
* после завершения проекта. Фиксирует доли участников для последующей конвертации.
* после завершения проекта. Включает фиксацию долей участников и **финальную
* конвертацию сегмента** (распределение между ЦК и ЦПП «Благорост»).
* Анкер процесса — `result_hash`.
*
* @note КРИТИЧЕСКИ ВАЖНО: Этот процесс НЕ применяется к чистым инвесторам.
* Чистые инвесторы (только роль is_investor) уже внесли свой вклад при инвестировании.
* Они НЕ проходят через pushrslt и переходят напрямую к конвертации через convertsegm.
* Участники со смешанными ролями (инвестор + создатель/автор/координатор) вносят
* Они НЕ проходят через pushrslt/convertsegm — для них работает purgesegment
* (очистка сегмента без конвертации, см. отдельный процесс).
* Участники со смешанными ролями (инвестор + создатель/автор/координатор) вносят
* результат только по интеллектуальной части, инвестиционная часть исключается.
*
* ## Диаграмма процесса
@@ -49,9 +52,13 @@
*
* alt Принятие председателем
* S->>C: 8. signact2(акт №2 от председателя)
* Note over C: Статус сегмента = CONTRIBUTED
* Note over C: Удаление временного объекта результата
* Note over C: Статус сегмента = CONTRIBUTED, результат = ACT2
* Note over C: ACCEPT_RID (Dr 04 / Cr 08)
* Note over C: Готовность к конвертации
*
* P->>C: 9. convertsegm(распределение между ЦК и Благорост)
* Note over C: TRANSFER GENERATOR_FUND → SHARE_FUND_PAY / BLAGOROST_FUND
* Note over C: Удаление сегмента и объекта результата
* else Отклонение до совета
* S->>C: 8. declineapprv(отклонение)
* Note over C: Возврат к статусу READY
@@ -151,9 +158,35 @@
*
* <b>Финальные изменения:</b>
* - Статус сегмента → `CONTRIBUTED`
* - Удаление временного объекта `results`
* - Готовность сегмента к конвертации
*
* - Статус результата → `ACT2` (анкер процесса до convertsegm)
* - ACCEPT_RID: Dr 04 / Cr 08 на полный `available_for_program` сегмента
* - REPAY (если был долг): Dr 80 / Cr 58
* - Готовность сегмента к финальной конвертации
*
* ### Шаг 6: Конвертация сегмента (финал процесса)
* Конвертация сегмента — финальная фаза процесса `p.cap.rid`.
*
* Пайщик распределяет накопленный `available_for_program` между двумя направлениями:
* - <b>В ЦК (SHARE_FUND_PAY):</b> только себестоимость создателей, авторов и
* координаторов минус погашенный долг, не больше обеспеченной суммы.
* - <b>В ЦПП «Благорост» (BLAGOROST_FUND):</b> остаток
* (`available_for_program - wallet_amount`).
*
* Кошельковое движение TRANSFER GENERATOR_FUND → цель **без бухпроводок**:
* Dr 04 / Cr 08 уже сделана в signact2 (ACCEPT_RID, WalletOp::NONE) на полный
* `available_for_program`.
*
* <b>Входящий документ:</b> Заявление на конвертацию (подпись участника).
*
* <b>Финальные изменения:</b>
* - Сегмент удаляется (`Capital::Segments::remove_segment`)
* - Объект результата удаляется (`Capital::Results::delete_result`)
* - Счётчик сконвертированных сегментов проекта инкрементируется
* - Кошельки `w.cap.gen` пайщика закрываются в ноль по этому сегменту
*
* @note `convert_hash` в API больше не используется — анкер процесса единый
* для всех фаз: `result_hash`.
*
* ### Альтернативные пути: Отклонение заявления
*
* #### Отклонение председателем
@@ -211,8 +244,10 @@
## Эффекты процесса
@include{doc} signact2-effect.dox
@include{doc} convertsegm-effect.dox
## Документы процесса
@include{doc} result-submission-docs.dox
@include{doc} conversion-process-docs.dox
*/
@@ -1,14 +1,14 @@
/**
* @brief Подписывает акт 2 по результату участника
* Подписывает второй акт председателем и завершает процесс принятия результата:
* Подписывает второй акт председателем и фиксирует приём РИД в паевой фонд
* (фаза процесса `p.cap.rid`, после которой пайщик может вызвать convertsegm):
* - Проверяет что подписывает председатель
* - Валидирует статус результата (должен быть act1) и статус сегмента (должен быть act1)
* - Проверяет подлинность документа акта от председателя и участника
* - Устанавливает второй акт
* - Начисляет заблокированные средства и обновляет учёт
* - Обновляет статус сегмента на contributed
* - Обновляет статус результата на act2
* - Удаляет объект результата после успешного принятия
* - Обновляет статус результата на act2 (анкер процесса до convertsegm)
* @param coopname Наименование кооператива
* @param username Наименование пользователя-председателя
* @param result_hash Хеш результата
@@ -85,9 +85,8 @@ void capital::signact2(eosio::name coopname, eosio::name chairman, checksum256 r
// Обновляем статус сегмента
Capital::Segments::update_segment_status(coopname, result->project_hash, result->username, Capital::Segments::Status::CONTRIBUTED);
// Изменяем статус результата на act2 перед удалением
// Статус результата → ACT2. Объект НЕ удаляется здесь: result_hash остаётся
// якорем процесса p.cap.rid до convertsegm, который завершает процесс и
// удаляет result.
Capital::Results::update_result_status(coopname, result -> id, Capital::Results::Status::ACT2);
// Удаляем объект результата после успешного принятия
Capital::Results::delete_result(coopname, result -> id);
};
+1 -1
View File
@@ -59,7 +59,6 @@
#include "app/voting/submit_vote/submitvote.cpp"
// Подача результатов
#include "app/result_submission/convert_segment/convertsegm.cpp"
#include "app/result_submission/purge_segment/purgesegment.cpp"
#include "app/result_submission/push_result/approverslt.cpp"
#include "app/result_submission/push_result/authrslt.cpp"
@@ -67,6 +66,7 @@
#include "app/result_submission/push_result/pushrslt.cpp"
#include "app/result_submission/push_result/signact1.cpp"
#include "app/result_submission/push_result/signact2.cpp"
#include "app/result_submission/push_result/convertsegm.cpp"
// Инвестиции в программу
#include "app/invests_managment/invest_in_program/allocate/allocate.cpp"
+1 -1
View File
@@ -144,7 +144,7 @@ public:
// Конвертация сегмента
[[eosio::action]]
void convertsegm(eosio::name coopname, eosio::name username, checksum256 project_hash, checksum256 convert_hash, asset wallet_amount, asset capital_amount, document2 convert_statement);
void convertsegm(eosio::name coopname, eosio::name username, checksum256 project_hash, checksum256 result_hash, asset wallet_amount, asset capital_amount, document2 convert_statement);
// Очистка сегмента чистого инвестора
[[eosio::action]]
@@ -67,12 +67,12 @@ namespace Capital::Memo {
return "Возврат заблокированных средств по отклоненной программной инвестиции для договора УХД с ID: " + std::to_string(contributor_id);
}
inline std::string get_convert_segment_to_wallet_memo(checksum256 convert_hash) {
return "Конвертация в кошелек: " + checksum256_to_hex(convert_hash);
inline std::string get_convert_segment_to_wallet_memo(checksum256 result_hash) {
return "Конвертация в кошелек: " + checksum256_to_hex(result_hash);
}
inline std::string get_convert_segment_to_capital_memo(checksum256 convert_hash) {
return "Конвертация в капитал: " + checksum256_to_hex(convert_hash);
inline std::string get_convert_segment_to_capital_memo(checksum256 result_hash) {
return "Конвертация в капитал: " + checksum256_to_hex(result_hash);
}
inline std::string get_return_unused_investments_memo() {
@@ -25,21 +25,17 @@ namespace Capital {
> capital_wallets_index;
namespace Wallets {
inline std::optional<progwallet> get_program_capital_wallet(eosio::name coopname, eosio::name username) {
/**
* Открыт ли у пайщика программный кошелёк ЦПП «Благорост»
* (есть подписанное программное соглашение в `wallet::users.programs[]`).
* Балансы читаются отдельно через ledger2::userwallets.
*/
inline bool has_program_capital_wallet(eosio::name coopname, eosio::name username) {
auto program_id = get_program_id(_capital_program);
auto program = get_program_or_fail(coopname, program_id);
auto capital_wallet = get_program_wallet(coopname, username, _capital_program);
if (!capital_wallet.has_value()) {
return std::nullopt;
}
return capital_wallet.value();
// Программа должна существовать в реестре кооператива.
get_program_or_fail(coopname, program_id);
return has_program_wallet(coopname, username, _capital_program);
}
/**
@@ -1,17 +1,22 @@
# ─────────────────────────────────────────────────────────────────────────────
# Стандарт «Приём результата интеллектуальной деятельности» — оформление РИД
# участника проекта программы «Благорост» как имущественного паевого взноса.
# участника проекта программы «Благорост» как имущественного паевого взноса
# с финальной конвертацией сегмента между ЦК и программой.
#
# Многоэтапный процесс. Три ledger2-операции:
# • o.cap.commit — коммит РИД при одобрении (Дт 8 / Кт 80, кошелёк w.cap.gen)
# • o.cap.accept — приём РИД в паевой фонд по акту-2 (TRANSFER w.cap.gen → w.cap.blago, Дт 4 / Кт 8)
# Многоэтапный процесс. Пять ledger2-операций:
# • o.cap.commit — коммит РИД при одобрении (Дт 8 / Кт 80, ISSUE w.cap.gen)
# • o.cap.accept — приём РИД в НМА по акту-2 (Дт 4 / Кт 8, NONE — без движения кошелька)
# • o.cap.repay — опционально, если у участника есть заём (см. p.cap.debt)
# • o.cap.cnvshr — конвертация сегмента в ЦК (TRANSFER w.cap.gen → w.wal.share, без проводки)
# • o.cap.cnvbl — конвертация сегмента в Благорост (TRANSFER w.cap.gen → w.cap.blago, без проводки)
#
# Анкер процесса — result_hash (живёт от pushrslt до convertsegm).
#
# Источники правды в коде:
# • cpp/capital/capital.hpp — actions
# • cpp/capital/app/generation/create_commit/{createcmmt,approvecmmt,declinecmmt}.cpp — коммит
# • cpp/capital/app/result_submission/push_result/{pushrslt,approverslt,authrslt,declrslt,signact1,signact2}.cpp — приём результата
# • cpp/lib/core/ledger2/operations.hpp — o.cap.commit/accept/repay
# • cpp/capital/app/result_submission/push_result/{pushrslt,approverslt,authrslt,declrslt,signact1,signact2,convertsegm}.cpp — приём + конвертация
# • cpp/lib/core/ledger2/operations.hpp — o.cap.commit/accept/repay/cnvshr/cnvbl
# ─────────────────────────────────────────────────────────────────────────────
# ── Секция 1. Паспорт ───────────────────────────────────────────────────────
@@ -29,6 +34,9 @@ purpose: >
председателя, авторизация совета и акт приёма-передачи в двух
подписях. Если у участника был беспроцентный заём проекта, он
закрывается в момент приёма РИД — без отдельной заявки.
Завершающее действие — распределение полученного паевого взноса:
часть участник может забрать в Цифровой Кошелёк, часть оставить
в программе «Благорост».
roles:
- contributor # Участник проекта (исполнитель РИД)
@@ -112,13 +120,26 @@ actions:
- name: capital::signact2
human: Принять РИД в паевой фонд
actor: Председатель
role: progress
purpose: >
Председатель ставит вторую подпись на акте приёма-передачи. Применяются
две ledger2-операции: o.cap.accept (Дт 4 / Кт 8 — РИД зачислен в НМА,
кошелёк остаётся на ЦПП «Генератор» до конвертации) и опционально
o.cap.repay (если у участника был заём проекта — закрытие займа).
Объект результата сохраняется со статусом ACT2 как анкер процесса
до финальной конвертации.
- name: capital::convertsegm
human: Распределить паевой взнос
actor: Участник
role: closer
purpose: >
Председатель ставит вторую подпись на акте приёма-передачи —
закрывающее действие. Применяются две ledger2-операции:
o.cap.accept (перевод РИД из «Генератор» в «Благорост — принятые
РИД» с проводкой Дт 4 / Кт 8) и опционально o.cap.repay
(если у участника был заём проекта — закрытие займа).
Участник распределяет накопленный паевой взнос между Цифровым
Кошельком и программой «Благорост». Применяются две ledger2-операции
без бухпроводок (бухзапись была сделана в signact2): o.cap.cnvshr
(часть в Цифровой Кошелёк) и o.cap.cnvbl (часть в программу «Благорост»).
После конвертации сегмент и объект результата удаляются — процесс
завершён.
# ── Секция 3. Граф состояний ────────────────────────────────────────────────
entity: "capital::commit → capital::result"
@@ -174,10 +195,20 @@ states:
human: РИД принят в паевой фонд
description: >
Председатель поставил вторую подпись на акте приёма-передачи: РИД
переведён с единого кошелька ЦПП «Генератор» (w.cap.gen) на единый
кошелёк ЦПП «Благорост» у пайщика (w.cap.blago), Дт 4 / Кт 8. Если у
участника был заём проекта — он закрылся (o.cap.repay). РИД считается
имущественным паевым взносом.
зачислен в нематериальные активы кооператива (Дт 4 / Кт 8 без
движения по кошелькам — кошелёк остаётся на ЦПП «Генератор»
у пайщика до конвертации). Если у участника был заём проекта — он
закрылся (o.cap.repay). Объект результата хранится в статусе ACT2
как анкер процесса до конвертации сегмента.
kind: normal
- name: converted
human: Паевой взнос распределён
description: >
Участник распределил полученный паевой взнос между Цифровым Кошельком
(w.wal.share) и программой «Благорост» (w.cap.blago). Кошелёк ЦПП
«Генератор» (w.cap.gen) у пайщика по этому сегменту закрыт. Сегмент
и объект результата удалены — процесс РИД завершён.
kind: final
- name: removed
@@ -261,6 +292,19 @@ transitions:
guards:
- Акт-2 подписан председателем (registry_id=1042).
- from: accepted
to: converted
action: capital::convertsegm
actor: Участник
ledger_code: o.cap.cnvshr
operations:
- o.cap.cnvshr
- o.cap.cnvbl
guards:
- Заявление о конвертации подписано участником.
- Сегмент актуален (rfrshsegment до вызова).
- Сумма wallet_amount + capital_amount равна доступной части паевого взноса.
- from: pushed
to: removed
action: capital::declrslt
@@ -365,18 +409,41 @@ scenario:
actor: Председатель
action: capital::signact2
description: >
Председатель ставит вторую подпись на акте приёма-передачи
закрывающее действие. Применяются две ledger2-операции:
(1) o.cap.accept — РИД переходит с единого кошелька ЦПП «Генератор»
у пайщика (w.cap.gen) на единый кошелёк ЦПП «Благорост» у пайщика
(w.cap.blago), Дт 4 / Кт 8;
Председатель ставит вторую подпись на акте приёма-передачи.
Применяются две ledger2-операции:
(1) o.cap.accept — РИД зачислен в нематериальные активы кооператива
(Дт 4 / Кт 8, без движения кошелька — РИД остаётся на едином
кошельке ЦПП «Генератор» у пайщика до конвертации);
(2) опционально o.cap.repay — если у участника был заём проекта,
заём закрывается (w.cap.loan → w.wal.share, Дт 80 / Кт 58).
pre:
- Результат в статусе ACT1.
post:
- РИД в едином кошельке ЦПП «Благорост» у пайщика (w.cap.blago).
- Результат в статусе ACT2 (анкер процесса до конвертации).
- В ledger2 применены o.cap.accept (+ опц. o.cap.repay).
- Сегмент в статусе CONTRIBUTED.
- step: 8
title: Распределение паевого взноса (конвертация сегмента)
actor: Участник
action: capital::convertsegm
description: >
Участник распределяет полученный паевой взнос между Цифровым
Кошельком и программой «Благорост». Применяются две ledger2-операции
без бухпроводок (бухзапись была сделана в signact2):
(1) o.cap.cnvshr — часть переводится с единого кошелька ЦПП
«Генератор» у пайщика (w.cap.gen) в Цифровой Кошелёк
(w.wal.share);
(2) o.cap.cnvbl — часть переводится с w.cap.gen на единый кошелёк
ЦПП «Благорост» у пайщика (w.cap.blago).
После применения сегмент и объект результата удаляются — процесс
завершён.
pre:
- Результат в статусе ACT2, сегмент в CONTRIBUTED.
post:
- Кошелёк w.cap.gen у пайщика по этому сегменту закрыт.
- Сегмент удалён, объект результата удалён.
- В ledger2 применены o.cap.cnvshr и o.cap.cnvbl (любая из сумм может быть нулевой).
alternatives:
- branch: Отказ мастера на коммите
@@ -428,6 +495,12 @@ documents:
signed_by: [ Участник, Председатель ]
stored_in: results.act2
- action: capital::convertsegm
title: Заявление о распределении паевого взноса по результату
registry_id: TBD-Standardization
signed_by: [ Участник ]
stored_in: action_payload.convert_statement
# ── Секция 6. Операции ──────────────────────────────────────────────────────
operations:
- ledger_code: o.cap.commit
@@ -446,24 +519,26 @@ operations:
- ledger_code: o.cap.accept
human_name: Приём РИД в паевой фонд
wallet_op: TRANSFER
wallet_from: w.cap.gen # ЦПП «Генератор» — единый кошелёк программы у пайщика
wallet_to: w.cap.blago # ЦПП «Благорост» — единый кошелёк программы у пайщика
wallet_op: NONE
wallet_from: '' # без перемещения по кошелькам — кошелёк остаётся на w.cap.gen
wallet_to: ''
debit: 4 # Нематериальные активы
credit: 8 # Вложения во внеоборотные активы
amount_ref: result.amount
amount_ref: segment.available_for_program
triggered_by: capital::signact2
description: >
Перевод одобренного коммита РИД из единого кошелька ЦПП «Генератор»
у пайщика (w.cap.gen) в единый кошелёк ЦПП «Благорост» у пайщика
(w.cap.blago). Бухгалтерия закрывает Кт 8 (вложения) и записывает
Дт 4 (нематериальные активы) — РИД зачислен в паевой фонд кооператива.
Бухгалтерская запись приёма РИД в нематериальные активы кооператива:
закрывает Кт 8 (вложения во внеоборотные активы) и записывает Дт 4
(нематериальные активы) на полную сумму available_for_program
сегмента. Перемещение по кошелькам не выполняется — РИД остаётся
на едином кошельке ЦПП «Генератор» у пайщика (w.cap.gen) до
финальной конвертации сегмента.
- ledger_code: o.cap.repay
human_name: Возврат беспроцентного займа пайщика по акту-2
wallet_op: TRANSFER
wallet_from: w.cap.loan # Выданные пайщикам беспроцентные займы
wallet_to: w.wal.share # ЦК — паевая часть пайщика
wallet_to: w.wal.share # ЦК — паевой взнос пайщика
debit: 80 # Паевой фонд (складочный капитал)
credit: 58 # Финансовые вложения
amount_ref: debt.amount
@@ -472,6 +547,34 @@ operations:
Опциональная операция при наличии у участника беспроцентного займа
проекта (см. p.cap.debt). На акте-2 закрытый РИД зачитывает заём:
финансовое вложение списывается (Кт 58), паевой фонд закрывает
обязательство (Дт 80), сумма становится доступной на SHARE_FUND_PAY
пайщика.
обязательство (Дт 80), сумма становится доступной на паевом
взносе пайщика.
- ledger_code: o.cap.cnvshr
human_name: 'Конвертация сегмента: РИД → паевой взнос деньгами'
wallet_op: TRANSFER
wallet_from: w.cap.gen # ЦПП «Генератор» — единый кошелёк программы у пайщика
wallet_to: w.wal.share # ЦК — паевой взнос пайщика
debit: '' # без бухпроводки — Дт 4 / Кт 8 уже сделана в o.cap.accept
credit: ''
amount_ref: convertsegm.wallet_amount
triggered_by: capital::convertsegm
description: >
Часть паевого взноса по результату направляется в Цифровой Кошелёк
пайщика. Бухгалтерская запись была сделана ранее в o.cap.accept
на полную сумму сегмента — здесь только перемещение по кошелькам.
- ledger_code: o.cap.cnvbl
human_name: 'Конвертация сегмента: РИД → ЦПП «Благорост»'
wallet_op: TRANSFER
wallet_from: w.cap.gen # ЦПП «Генератор» — единый кошелёк программы у пайщика
wallet_to: w.cap.blago # ЦПП «Благорост» — единый кошелёк программы у пайщика
debit: '' # без бухпроводки — Дт 4 / Кт 8 уже сделана в o.cap.accept
credit: ''
amount_ref: convertsegm.capital_amount
triggered_by: capital::convertsegm
description: >
Часть паевого взноса по результату остаётся в программе «Благорост»
пайщика. Бухгалтерская запись была сделана ранее в o.cap.accept
на полную сумму сегмента — здесь только перемещение по кошелькам.
@@ -196,12 +196,12 @@ static constexpr OperationRegistryEntry OPERATION_REGISTRY[] = {
{ operations::capital::INVEST, processes::capital::INVEST, WalletOp::TRANSFER,
ledger2_wallets::SHARE_FUND_PAY, ledger2_wallets::BLAGOROST_FUND,
0, 0,
"Инвестиция в ЦПП «Благорост» (перенос между кошельками паевого фонда)" },
"Инвестиция в ЦПП «Благорост»" },
// 7. Коммит РИД: Dr 08 / Cr 80, ISSUE GENERATOR_FUND (ADR-009: единый кошелёк программы Генератор)
{ operations::capital::COMMIT_RID, processes::capital::RID, WalletOp::ISSUE, eosio::name{}, ledger2_wallets::GENERATOR_FUND,
ledger2_accounts::NON_CURRENT_INVESTMENTS, ledger2_accounts::SHARE_FUND,
"Коммит результата интеллектуальной деятельности по программе «Благорост»" },
"Коммит результата интеллектуальной деятельности по программе «Генератор»" },
// 8. Приём РИД в НМА: Dr 04 / Cr 08, NONE — кошелёк остаётся на GENERATOR_FUND.
// Семантика: подписан акт-2, РИД принят как НМА (закрылся 08-й). Перемещение
@@ -264,17 +264,19 @@ static constexpr OperationRegistryEntry OPERATION_REGISTRY[] = {
"Возврат паевого из ЦПП «Благорост» в Цифровой Кошелёк" },
// 18. Конвертация сегмента (часть в ЦК): TRANSFER GENERATOR_FUND → SHARE_FUND_PAY, без Dr/Cr.
// Бухпроводка Dr 04 / Cr 08 уже сделана в ACCEPT_RID на полный
// available_for_program сегмента; здесь только перемещаем кошелёк.
{ operations::capital::CONVERT_TO_SHARE, processes::capital::CNVSEG, WalletOp::TRANSFER,
// Финальная фаза процесса p.cap.rid (после signact2). Бухпроводка
// Dr 04 / Cr 08 уже сделана в ACCEPT_RID на полный available_for_program
// сегмента; здесь только перемещаем кошелёк. process_hash = result_hash.
{ operations::capital::CONVERT_TO_SHARE, processes::capital::RID, WalletOp::TRANSFER,
ledger2_wallets::GENERATOR_FUND, ledger2_wallets::SHARE_FUND_PAY,
0, 0,
"Конвертация сегмента: РИД → паевой взнос деньгами" },
// 19. Конвертация сегмента (часть в Благорост): TRANSFER GENERATOR_FUND → BLAGOROST_FUND, без Dr/Cr.
// Бухпроводка Dr 04 / Cr 08 уже сделана в ACCEPT_RID; здесь только перенос
// кошелька в программный фонд.
{ operations::capital::CONVERT_TO_BLAGO, processes::capital::CNVSEG, WalletOp::TRANSFER,
// Финальная фаза процесса p.cap.rid (после signact2). Бухпроводка
// Dr 04 / Cr 08 уже сделана в ACCEPT_RID; здесь только перенос кошелька
// в программный фонд. process_hash = result_hash.
{ operations::capital::CONVERT_TO_BLAGO, processes::capital::RID, WalletOp::TRANSFER,
ledger2_wallets::GENERATOR_FUND, ledger2_wallets::BLAGOROST_FUND,
0, 0,
"Конвертация сегмента: РИД → ЦПП «Благорост»" },
@@ -14,7 +14,7 @@
* Один `process_type` может соответствовать нескольким operation_code — это
* явно разрешённая модель мульти-операционных процессов:
* - processes::registrator::ACCEPT ← o.reg.payent + o.reg.putmin
* - processes::capital::RID ← o.cap.commit + o.cap.accept (+ o.cap.repay)
* - processes::capital::RID ← o.cap.commit + o.cap.accept (+ o.cap.repay) + o.cap.cnvshr/o.cap.cnvbl
* - processes::marketplace::REQUEST ← o.mkt.supply + o.mkt.recv
*
* Одноактовые процессы: `capital::IMPORT`, `capital::PROPERTY`,
@@ -49,10 +49,9 @@ namespace processes {
inline constexpr eosio::name IMPORT = "p.cap.import"_n; ///< Оффлайн-импорт пайщика Благорост (одноактовый).
inline constexpr eosio::name INVEST = "p.cap.invest"_n; ///< Инвестиция в ЦПП Благорост (wallet-only 2001→9001).
inline constexpr eosio::name DEBT = "p.cap.debt"_n; ///< Займ пайщику (o.cap.lend + o.cap.repay).
inline constexpr eosio::name RID = "p.cap.rid"_n; ///< Приём РИД в паевой фонд (o.cap.commit + o.cap.accept + опц. o.cap.repay).
inline constexpr eosio::name RID = "p.cap.rid"_n; ///< Приём РИД в паевой фонд: o.cap.commit (коммиты) + o.cap.accept (акт-2) + опц. o.cap.repay + o.cap.cnvshr/o.cap.cnvbl (финальная конвертация сегмента). Анкер процесса — result_hash.
inline constexpr eosio::name PROPERTY = "p.cap.prop"_n; ///< Приём имущественного паевого взноса (одноактовый).
inline constexpr eosio::name WTHCAP = "p.cap.wthcap"_n; ///< Возврат паевого из ЦПП «Благорост» в кошелёк пайщика (одноактовый).
inline constexpr eosio::name CNVSEG = "p.cap.cnvseg"_n; ///< Конвертация сегмента после ACT2 (o.cap.cnvshr + o.cap.cnvbl).
}
// marketplace
@@ -44,7 +44,7 @@
*/
struct ledger2_wallets {
// wallet — паевой фонд + возвраты + ЦК
static constexpr eosio::name SHARE_FUND_PAY = "w.wal.share"_n; ///< ЦК — паевая часть пайщика (USER_SHARED)
static constexpr eosio::name SHARE_FUND_PAY = "w.wal.share"_n; ///< Паевой взнос пайщика (USER_SHARED)
static constexpr eosio::name CK_MEMBER = "w.wal.member"_n; ///< ЦК — членская часть пайщика (USER_SHARED)
static constexpr eosio::name WITHDRAWALS_SINK = "w.wal.wthdrw"_n; ///< Возвраты паевых взносов пайщикам (sink TRANSFER, COOPERATIVE)
@@ -94,7 +94,7 @@ struct Ledger2WalletMeta {
inline constexpr std::array<Ledger2WalletMeta, 11> LEDGER2_WALLET_REGISTRY = {{
// USER_SHARED (5) — L3-разрез по пайщику
{ ledger2_wallets::MIN_SHARE_FUND, "Минимальный паевой взнос", WalletKind::USER_SHARED },
{ ledger2_wallets::SHARE_FUND_PAY, "ЦК — паевая часть пайщика", WalletKind::USER_SHARED },
{ ledger2_wallets::SHARE_FUND_PAY, "Паевой взнос пайщика", WalletKind::USER_SHARED },
{ ledger2_wallets::CK_MEMBER, "ЦК — членская часть пайщика", WalletKind::USER_SHARED },
{ ledger2_wallets::BLAGOROST_FUND, "ЦПП «Благорост» — единый кошелёк программы у пайщика", WalletKind::USER_SHARED },
{ ledger2_wallets::GENERATOR_FUND, "ЦПП «Генератор» — единый кошелёк программы у пайщика", WalletKind::USER_SHARED },
+33 -53
View File
@@ -9,8 +9,16 @@
using namespace eosio;
/**
* Логика программ пайщика и кошелька (совет).
* Таблицы programs / progwallets / agreements — из domain/index.hpp (подключать раньше).
* Логика программ пайщика и кошелька.
*
* Источник правды по «участник ЦПП X» = `wallet::users[username].programs[]`
* (Эпик 2). signagree добавляет запись в этот вектор, revokeagree — удаляет;
* status="declined" в агриментах при ревоке не нужен — записи просто нет.
*
* Таблицы из domain/index.hpp:
* - soviet::programs — реестр программ кооператива.
* - soviet::coagreements — мапа program_type → program_id (per-coop).
* - wallet::users — подписанные программные соглашения пайщика.
*/
program get_program_or_fail(eosio::name coopname, uint64_t program_id) {
@@ -22,57 +30,33 @@ program get_program_or_fail(eosio::name coopname, uint64_t program_id) {
return program(*program_itr);
}
inline bool has_signed_program_agreement(eosio::name coopname, eosio::name username, uint64_t program_id) {
WalletTables::users_index users(_wallet, coopname.value);
auto user_it = users.find(username.value);
if (user_it == users.end()) return false;
for (const auto &p : user_it->programs) {
if (p.program_id == program_id) return true;
}
return false;
}
bool is_participant_of_cpp_by_program_id(eosio::name coopname, eosio::name username, uint64_t program_id) {
progwallets_index progwallets(_soviet, coopname.value);
auto wallets_by_username_and_program = progwallets.template get_index<"byuserprog"_n>();
auto username_and_program_index = combine_ids(username.value, program_id);
auto wallet = wallets_by_username_and_program.find(username_and_program_index);
if (wallet == wallets_by_username_and_program.end())
return false;
auto program_row = get_program_or_fail(coopname, program_id);
agreements2_index agreements(_soviet, coopname.value);
auto agreement_row = agreements.find(wallet->agreement_id);
if (agreement_row->status == "declined"_n)
return false;
return true;
return has_signed_program_agreement(coopname, username, program_id);
}
bool is_valid_participant_of_program_by_type(eosio::name coopname, eosio::name username,
eosio::name program_type) {
programs_index programs(_soviet, coopname.value);
progwallets_index progwallets(_soviet, coopname.value);
coagreements_index coagreements(_soviet, coopname.value);
auto coagreement_row = coagreements.find(program_type.value);
if (coagreement_row == coagreements.end())
return false;
programs_index programs(_soviet, coopname.value);
auto exist = programs.find(coagreement_row->program_id);
if (exist == programs.end())
return false;
auto wallets_by_username_and_program = progwallets.template get_index<"byuserprog"_n>();
auto username_and_program_index = combine_ids(username.value, exist->id);
auto wallet = wallets_by_username_and_program.find(username_and_program_index);
if (wallet == wallets_by_username_and_program.end())
return false;
agreements2_index agreements(_soviet, coopname.value);
auto agreement_row = agreements.find(wallet->agreement_id);
if (agreement_row->status == "declined"_n)
return false;
return true;
return has_signed_program_agreement(coopname, username, exist->id);
}
struct ProgramInfo {
@@ -102,18 +86,14 @@ inline uint64_t get_draft_id(const eosio::name &type) {
return it->second.draft_id;
}
std::optional<progwallet> get_program_wallet(eosio::name coopname, eosio::name username, eosio::name type) {
auto program_id = get_program_id(type);
progwallets_index progwallets(_soviet, coopname.value);
auto balances_by_username_and_program = progwallets.template get_index<"byuserprog"_n>();
auto username_and_program_index = combine_ids(username.value, program_id);
auto wallet = balances_by_username_and_program.find(username_and_program_index);
if (wallet == balances_by_username_and_program.end()) {
return std::nullopt;
}
return *wallet;
/**
* Открыт ли у пайщика программный кошелёк по типу программы.
*
* Источник — `wallet::users[username].programs[]`: пайщик считается «открывшим»
* программу, если у него есть подписанное программное соглашение нужного
* `program_id`. Балансы — отдельно через ledger2::userwallets, эта функция
* только про факт открытия.
*/
bool has_program_wallet(eosio::name coopname, eosio::name username, eosio::name type) {
return has_signed_program_agreement(coopname, username, get_program_id(type));
}
@@ -23,11 +23,11 @@ void soviet::addmemberfee(eosio::name coopname, eosio::name username, uint64_t p
auto participant = get_participant_or_fail(coopname, username);
auto exist_wallet = get_user_program_wallet_or_fail(coopname, username, program_id);
progwallets_index progwallets(_soviet, coopname.value);
auto wallet = progwallets.find(exist_wallet.id);
progwallets.modify(wallet, payer, [&](auto &p) {
progwallets.modify(wallet, payer, [&](auto &p) {
p.membership_contribution = p.membership_contribution.value_or(asset(0, quantity.symbol)) + quantity;
});
@@ -13,32 +13,34 @@
*/
void soviet::openprogwall(name coopname, name username, name program_type, uint64_t agreement_id) {
name payer = check_auth_and_get_payer_or_fail(contracts_whitelist);
auto program_wallet = get_program_wallet(coopname, username, program_type);
if (!program_wallet.has_value()) {
auto program_id = get_program_id(program_type);
if (agreement_id > 0) {
agreements2_index agreements(_soviet, coopname.value);
auto agreement = agreements.find(agreement_id);
check(agreement != agreements.end(), "Соглашение не найдено");
check(agreement -> username == username, "Неверный пользователь соглашения и кошелька");
};
progwallets_index progwallets(_soviet, coopname.value);
//TODO: make payer coopname
progwallets.emplace(_soviet, [&](auto &b) {
b.id = progwallets.available_primary_key();
b.program_id = program_id;
b.coopname = coopname;
b.username = username;
b.agreement_id = agreement_id;
b.available = asset(0, _root_govern_symbol);
b.blocked = asset(0, _root_govern_symbol);
b.membership_contribution = asset(0, _root_govern_symbol);
});
};
auto program_id = get_program_id(program_type);
// Идемпотентность по самой таблице progwallets (легаси-учёт): не плодим
// дубликаты, если запись уже существует. Проверка по «byuserprog» оставлена
// здесь локально — фазаут progwallets ещё не завершён, общий помощник в
// lib/core/programs.hpp переехал на wallet::users (другая семантика).
progwallets_index progwallets(_soviet, coopname.value);
auto by_user_program = progwallets.template get_index<"byuserprog"_n>();
auto wallet_idx_it = by_user_program.find(combine_ids(username.value, program_id));
if (wallet_idx_it != by_user_program.end()) return;
if (agreement_id > 0) {
agreements2_index agreements(_soviet, coopname.value);
auto agreement = agreements.find(agreement_id);
check(agreement != agreements.end(), "Соглашение не найдено");
check(agreement->username == username, "Неверный пользователь соглашения и кошелька");
}
//TODO: make payer coopname
progwallets.emplace(_soviet, [&](auto &b) {
b.id = progwallets.available_primary_key();
b.program_id = program_id;
b.coopname = coopname;
b.username = username;
b.agreement_id = agreement_id;
b.available = asset(0, _root_govern_symbol);
b.blocked = asset(0, _root_govern_symbol);
b.membership_contribution = asset(0, _root_govern_symbol);
});
};
File diff suppressed because it is too large Load Diff
@@ -79,11 +79,14 @@ export const PROCESS_HASH_LOCATOR: Readonly<Record<string, HashLocation[]>> = Ob
// долгоживущий якорь; все коммиты проекта группируются в один процесс).
'p.cap.commit': [{ code: 'capital', table: 'projects', field: 'project_hash' }],
// p.cap.rid — подписание акта-2 (внесение РИД в паевой фонд).
// До ДВУХ операций на один process_hash:
// 1. o.cap.accept (Dr 04 / Cr 08) — приём РИД
// 2. o.cap.repay (Dr 80 / Cr 58) — возврат займа, если был (опционально)
// o.cap.commit разнесён backend'ом в отдельный процесс p.cap.commit.
// p.cap.rid — внесение РИД в паевой фонд (полный жизненный цикл).
// До ЧЕТЫРЁХ операций на один process_hash (= result_hash):
// 1. o.cap.accept (Dr 04 / Cr 08, NONE) — приём РИД в signact2
// 2. o.cap.repay (Dr 80 / Cr 58, TRANSFER) — возврат займа, опционально
// 3. o.cap.cnvshr (TRANSFER w.cap.gen → w.wal.share) — финальная конвертация в ЦК
// 4. o.cap.cnvbl (TRANSFER w.cap.gen → w.cap.blago) — финальная конвертация в Благорост
// o.cap.commit разнесён backend'ом в отдельный процесс p.cap.commit (см. выше).
// Объект `capital::results` живёт от pushrslt до convertsegm (анкер процесса).
'p.cap.rid': [{ code: 'capital', table: 'results', field: 'result_hash' }],
'p.cap.import': [{ code: 'capital', table: 'contributors', field: 'contributor_hash' }],
@@ -97,6 +100,11 @@ export const PROCESS_HASH_LOCATOR: Readonly<Record<string, HashLocation[]>> = Ob
'p.wal.depo': [{ code: 'wallet', table: 'deposits', field: 'deposit_hash' }],
'p.wal.wthdrw': [{ code: 'wallet', table: 'withdraws', field: 'withdraw_hash' }],
// p.cap.wthcap — возврат паевого из ЦПП Благорост в ЦК.
// Жизнь запроса: createwthd3 → capauthwthd3 / capdeclwthd3 → approvewthd3.
// Сущностная таблица — `capital::prgwithdraws.withdraw_hash`.
'p.cap.wthcap': [{ code: 'capital', table: 'prgwithdraws', field: 'withdraw_hash' }],
// marketplace::requests.hash — поле так и называется `hash`.
'p.mkt.reqst': [{ code: 'marketplace', table: 'requests', field: 'hash' }],
@@ -24,10 +24,10 @@ export class ConvertSegmentInputDTO implements ConvertSegmentDomainInput {
@IsString({ message: 'Хэш проекта должен быть строкой' })
project_hash!: string;
@Field(() => String, { description: 'Хэш конвертации' })
@IsNotEmpty({ message: 'Хэш конвертации не должен быть пустым' })
@IsString({ message: 'Хэш конвертации должен быть строкой' })
convert_hash!: string;
@Field(() => String, { description: 'Хэш результата (анкер процесса p.cap.rid)' })
@IsNotEmpty({ message: 'Хэш результата не должен быть пустым' })
@IsString({ message: 'Хэш результата должен быть строкой' })
result_hash!: string;
@Field(() => String, { description: 'Сумма для конвертации в главный кошелек' })
@IsNotEmpty({ message: 'Сумма для конвертации в главный кошелек не должна быть пустой' })
@@ -0,0 +1,54 @@
import { Injectable, Logger } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { Ledger2Contract } from 'cooptypes';
import config from '~/config/config';
import type { IDelta } from '~/types/common';
import { ProgramShareRegistrationService } from '../services/program-share-registration.service';
const BLAGOROST_WALLET_NAME = 'w.cap.blago';
/**
* Слушатель дельт `ledger2::userwallets`. При изменении баланса пайщика на
* программном кошельке «Благорост» (`w.cap.blago`) запускает точечную
* синхронизацию долей по всем pending|active проектам пайщика. Это закрывает
* окно между поллингом scheduler'а (по умолчанию 1440 мин) и быстрыми
* пользовательскими сценариями: invest → commit → result.
*
* `present=false` (запись стёрта при обнулении баланса, Эпик 3 §6) пока не
* обрабатываем: scheduler в итоге сверит и обнулит долю; обработка отдельной
* action regshare с user_shares=0 — отдельная задача.
*/
@Injectable()
export class ProgramShareRegistrationOnUserWalletDeltaListener {
private readonly logger = new Logger(ProgramShareRegistrationOnUserWalletDeltaListener.name);
constructor(
private readonly programShareRegistrationService: ProgramShareRegistrationService
) {}
@OnEvent(`delta::${Ledger2Contract.contractName.production}::${Ledger2Contract.Tables.UserWallets.tableName}`)
async handleUserWalletDelta(delta: IDelta): Promise<void> {
if (!delta.present) return;
if (delta.scope !== config.coopname) return;
const value = delta.value as Ledger2Contract.Tables.UserWallets.IUserWallet | undefined;
if (!value) return;
if (String(value.wallet_name) !== BLAGOROST_WALLET_NAME) return;
if (!value.username) return;
const username = String(value.username);
try {
await this.programShareRegistrationService.syncProgramSharesForUser(
delta.scope,
username
);
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
const stack = error instanceof Error ? error.stack : undefined;
this.logger.warn(
`regshare event-driven sync не выполнен: coop=${delta.scope} user=${username}: ${message}`,
stack
);
}
}
}
@@ -1079,7 +1079,7 @@ export class MutationLogMapperService {
capital_amount: capitalValue,
project_amount: projectValue,
},
reference_id: data.convert_hash,
reference_id: data.result_hash,
};
}
@@ -1105,7 +1105,7 @@ export class MutationLogMapperService {
amount: amountValue,
symbol: symbol || 'RUB',
},
reference_id: data.convert_hash || data.result_hash,
reference_id: data.result_hash,
};
}
@@ -13,7 +13,9 @@ import {
} from '../../domain/repositories/project.repository';
import { ContributorStatus } from '../../domain/enums/contributor-status.enum';
import { ProjectStatus } from '../../domain/enums/project-status.enum';
import { WALLET_BLOCKCHAIN_PORT, type WalletBlockchainPort } from '~/domain/wallet/ports/wallet-blockchain.port';
import type { ContributorDomainEntity } from '../../domain/entities/contributor.entity';
import type { ProjectDomainEntity } from '../../domain/entities/project.entity';
import { WALLET_DOMAIN_PORT, type WalletDomainPort } from '~/domain/wallet/ports/wallet-domain.port';
import { getProgramId, ProgramType } from '~/domain/wallet/enums/program-type.enum';
import { AssetUtils } from '~/shared/utils/asset.utils';
import { HttpApiError } from '~/utils/httpApiError';
@@ -40,15 +42,19 @@ export class ProgramShareRegistrationService {
private readonly projectRepository: ProjectRepository,
@Inject(CAPITAL_BLOCKCHAIN_PORT)
private readonly capitalBlockchainPort: CapitalBlockchainPort,
@Inject(WALLET_BLOCKCHAIN_PORT)
private readonly walletBlockchainPort: WalletBlockchainPort
@Inject(WALLET_DOMAIN_PORT)
private readonly walletDomainPort: WalletDomainPort
) {}
/**
* Обход участников в статусах active/import, проектов pending/active; при изменении user_shares относительно capital_contributor_shares — regshare.
*/
async syncProgramSharesForCoop(coopname: string): Promise<void> {
const programId = getProgramId(ProgramType.BLAGOROST);
const projects = await this.findActiveProjects(coopname);
if (projects.length === 0) {
this.logger.debug(`Синхронизация regshare: нет проектов в статусах pending/active для ${coopname}`);
return;
}
const contributors = (await this.contributorRepository.findAll()).filter(
(c) =>
@@ -56,77 +62,98 @@ export class ProgramShareRegistrationService {
(c.status === ContributorStatus.ACTIVE || c.status === ContributorStatus.IMPORT)
);
const projects = (await this.projectRepository.findAll()).filter(
for (const contributor of contributors) {
await this.syncContributor(coopname, contributor, projects);
}
}
/**
* Точечная синхронизация regshare для одного пайщика — вызывается из listener'а
* на дельты `ledger2::userwallets[w.cap.blago]`. Не пишет лог, если у пайщика
* нет ни одного pending/active проекта.
*/
async syncProgramSharesForUser(coopname: string, username: string): Promise<void> {
const projects = await this.findActiveProjects(coopname);
if (projects.length === 0) return;
const contributor = (await this.contributorRepository.findAll()).find(
(c) =>
c.coopname === coopname &&
c.username === username &&
(c.status === ContributorStatus.ACTIVE || c.status === ContributorStatus.IMPORT)
);
if (!contributor) return;
await this.syncContributor(coopname, contributor, projects);
}
private async findActiveProjects(coopname: string): Promise<ProjectDomainEntity[]> {
return (await this.projectRepository.findAll()).filter(
(p) =>
p.coopname === coopname &&
(p.status === ProjectStatus.PENDING || p.status === ProjectStatus.ACTIVE)
);
}
if (projects.length === 0) {
this.logger.debug(`Синхронизация regshare: нет проектов в статусах pending/active для ${coopname}`);
private async syncContributor(
coopname: string,
contributor: ContributorDomainEntity,
projects: ProjectDomainEntity[]
): Promise<void> {
const programId = getProgramId(ProgramType.BLAGOROST);
const wallet = await this.walletDomainPort.getProgramWallet({
coopname,
username: contributor.username,
program_id: programId,
});
if (!wallet || !wallet.available || !wallet.blocked) return;
let targetShares: string;
try {
targetShares = AssetUtils.sumAssets([wallet.available, wallet.blocked]);
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
this.logger.warn(
`Синхронизация regshare: не удалось сложить балансы кошелька ${contributor.username}: ${message}`
);
return;
}
for (const contributor of contributors) {
const wallet = await this.walletBlockchainPort.getProgramWallet(
const targetParsed = AssetUtils.parseAsset(targetShares);
if (!targetParsed.symbol) return;
for (const project of projects) {
const segment = await this.capitalBlockchainPort.getSegmentByProjectUser(
coopname,
contributor.username,
programId
project.project_hash,
contributor.username
);
if (!wallet) {
continue;
}
const registeredStr =
segment?.capital_contributor_shares ?? AssetUtils.formatAsset(0, targetParsed.symbol);
if (this.sameAssetAmount(registeredStr, targetShares)) continue;
let targetShares: string;
try {
targetShares = AssetUtils.sumAssets([wallet.available, wallet.blocked]);
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
this.logger.warn(
`Синхронизация regshare: не удалось сложить балансы кошелька ${contributor.username}: ${message}`
);
continue;
}
const targetParsed = AssetUtils.parseAsset(targetShares);
if (!targetParsed.symbol) {
continue;
}
for (const project of projects) {
const segment = await this.capitalBlockchainPort.getSegmentByProjectUser(
await this.capitalBlockchainPort.registerShare({
coopname,
project.project_hash,
contributor.username
project_hash: project.project_hash,
username: contributor.username,
user_shares: targetShares,
});
this.logger.log(
`regshare: ${contributor.username} → проект ${project.project_hash}, user_shares=${targetShares} (было ${registeredStr})`
);
await delay(REGSHARE_TX_GAP_MS);
} catch (error: unknown) {
const message = error instanceof HttpApiError ? error.message : error instanceof Error ? error.message : String(error);
const stack = error instanceof Error ? error.stack : undefined;
this.logger.warn(
`regshare не выполнен: coop=${coopname} project=${project.project_hash} user=${contributor.username}: ${message}`,
stack
);
const registeredStr =
segment?.capital_contributor_shares ?? AssetUtils.formatAsset(0, targetParsed.symbol);
if (this.sameAssetAmount(registeredStr, targetShares)) {
continue;
}
try {
await this.capitalBlockchainPort.registerShare({
coopname,
project_hash: project.project_hash,
username: contributor.username,
user_shares: targetShares,
});
this.logger.log(
`regshare: ${contributor.username} → проект ${project.project_hash}, user_shares=${targetShares} (было ${registeredStr})`
);
await delay(REGSHARE_TX_GAP_MS);
} catch (error: unknown) {
const message = error instanceof HttpApiError ? error.message : error instanceof Error ? error.message : String(error);
const stack = error instanceof Error ? error.stack : undefined;
this.logger.warn(
`regshare не выполнен: coop=${coopname} project=${project.project_hash} user=${contributor.username}: ${message}`,
stack
);
}
}
}
}
@@ -300,6 +300,7 @@ import { GitHubService } from './infrastructure/services/github.service';
import { GitHubSyncSchedulerService } from './infrastructure/services/github-sync-scheduler.service';
import { ProgramShareRegistrationSchedulerService } from './infrastructure/services/program-share-registration-scheduler.service';
import { ProgramShareRegistrationService } from './application/services/program-share-registration.service';
import { ProgramShareRegistrationOnUserWalletDeltaListener } from './application/listeners/program-share-registration-on-user-wallet-delta.listener';
import { CapitalDevelopmentRepositoryGitSyncService } from './application/services/capital-development-repository-git-sync.service';
import { CapitalGithubExtensionLifecycleListener } from './application/listeners/capital-github-extension-lifecycle.listener';
import { GitCommitMarkersSyncService } from './application/services/git-commit-markers-sync.service';
@@ -824,6 +825,7 @@ IssueIdGenerationService,
GitHubSyncSchedulerService,
ProgramShareRegistrationService,
ProgramShareRegistrationSchedulerService,
ProgramShareRegistrationOnUserWalletDeltaListener,
GitCommitMarkersSyncService,
{
provide: ISSUE_LINKED_GIT_COMMIT_REPOSITORY,
@@ -13,8 +13,8 @@ export interface ConvertSegmentDomainInput {
/** Хэш проекта */
project_hash: string;
/** Хэш конвертации */
convert_hash: string;
/** Хэш результата (анкер процесса p.cap.rid) */
result_hash: string;
/** Сумма для конвертации в главный кошелек */
wallet_amount: string;
@@ -0,0 +1,51 @@
import { Injectable, OnModuleInit } from '@nestjs/common';
import { InjectDataSource } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';
import { WinstonLoggerService } from '~/application/logger/logger-app.service';
/**
* Инициализатор partial unique-индекса `idx_user_wallets_natural_key`
* по `(coopname, wallet_name, username) WHERE present = true`.
*
* Контракт ledger2 удаляет L3-запись (`cleanup_l3_if_empty`) при обнулении
* баланса; следующая операция на той же паре `(wallet_name, username)`
* получит **новый** `id` (контракт берёт `available_primary_key()`). Если
* postgres-mirror держит старую запись с `present=false` под full-set
* unique-индексом, дельта новой записи проваливается с `duplicate key`.
* `WHERE present = true` исключает удалённые из ограничения.
*
* TypeORM `@Index({ unique: true, where: '"present" = true' })` при
* `synchronize: true` индекс не пересоздаёт (где-то теряет сравнение по
* `WHERE`), поэтому DDL применяется здесь явно при старте.
*/
@Injectable()
export class UserWalletIndexInitializer implements OnModuleInit {
constructor(
@InjectDataSource() private readonly dataSource: DataSource,
private readonly logger: WinstonLoggerService
) {
this.logger.setContext(UserWalletIndexInitializer.name);
}
async onModuleInit(): Promise<void> {
const indexname = 'idx_user_wallets_natural_key';
const expectedDef =
'CREATE UNIQUE INDEX idx_user_wallets_natural_key ON public.user_wallets USING btree (coopname, wallet_name, username) WHERE (present = true)';
const rows: Array<{ indexdef: string }> = await this.dataSource.query(
`SELECT indexdef FROM pg_indexes WHERE indexname = $1`,
[indexname]
);
const current = rows[0]?.indexdef ?? null;
if (current === expectedDef) return;
this.logger.log(
`Пересоздание ${indexname} как partial unique (текущий: ${current ?? '<нет>'})`
);
await this.dataSource.query(`DROP INDEX IF EXISTS ${indexname}`);
await this.dataSource.query(
`CREATE UNIQUE INDEX ${indexname} ON user_wallets (coopname, wallet_name, username) WHERE present = true`
);
}
}
@@ -7,15 +7,21 @@ export const EntityName = 'user_wallets';
* TypeORM-сущность L3 кошельков (`ledger2::userwallets`, Эпик 3).
*
* `id` — глобальный `uint64_t` из блокчейна (auto_increment ledger2-таблицы).
* Уникальная пара `(coopname, wallet_name, username)` — индекс для быстрого
* lookup'а по человеко-читаемому ключу (его шлёт UI).
* Уникальная пара `(coopname, wallet_name, username)` — partial unique
* `WHERE present = true`: при обнулении баланса контракт удаляет L3-запись
* (`cleanup_l3_if_empty`), позже та же пара (coopname, wallet_name, username)
* может появиться снова уже с новым `id` (контракт берёт следующий
* `available_primary_key()`). Если индекс держит весь набор — старая
* present=false запись блокирует upsert новой через unique constraint
* `idx_user_wallets_natural_key`. С partial индексом удалённые записи
* выпадают из ограничения, новая может встать рядом.
*/
@Entity(EntityName)
@Index(`idx_${EntityName}_blockchain_id`, ['id'])
@Index(`idx_${EntityName}_coopname`, ['coopname'])
@Index(`idx_${EntityName}_username`, ['username'])
@Index(`idx_${EntityName}_wallet_name`, ['wallet_name'])
@Index(`idx_${EntityName}_natural_key`, ['coopname', 'wallet_name', 'username'], { unique: true })
@Index(`idx_${EntityName}_natural_key`, ['coopname', 'wallet_name', 'username'], { unique: true, where: '"present" = true' })
export class UserWalletTypeormEntity extends BaseTypeormEntity {
static getTableName(): string {
return EntityName;
@@ -20,6 +20,20 @@ import type {
const LEDGER2_CODE = Ledger2Contract.contractName.production;
/**
* Сохранить symbol/precision исходного актива, но занулить amount.
* Используется при present=false — запись удалена в чейне (`cleanup_l2_if_empty`
* / `cleanup_l3_if_empty`), но в реестре оставляем строку с нулями, чтобы
* пользователь мог раскрыть и посмотреть историю движений.
*/
function zeroAssetLike(s: unknown): string {
const str = typeof s === 'string' ? s : '';
const [num = '0.0000', symbol = 'RUB'] = str.trim().split(/\s+/);
const dotIdx = num.indexOf('.');
const decimals = dotIdx === -1 ? 4 : num.length - dotIdx - 1;
return `${(0).toFixed(decimals)} ${symbol}`;
}
/**
* Чтение состояния ledger2 из Postgres-таблиц блокчейн-синка.
*
@@ -41,41 +55,55 @@ export class TypeOrmLedger2StateRepository implements Ledger2StatePort {
) {}
async getAccounts(coopname: string): Promise<Ledger2AccountDomainInterface[]> {
// Берём самую свежую row на ключ (DISTINCT ON по block_num DESC) вместе
// с present-флагом. Удалённые в чейне записи (present=false) **не**
// отбрасываем: оставляем строку в реестре с обнулёнными суммами и
// именем/id из last_snapshot — у такой записи может быть история
// (debit/credit), которую пользователь хочет раскрыть.
const rows = await this.deltaRepo.manager.query(
`SELECT DISTINCT ON (primary_key) value
`SELECT DISTINCT ON (primary_key) value, present
FROM blockchain_deltas
WHERE code = $1 AND "table" = '${Ledger2Contract.Tables.Accounts.tableName}' AND scope = $2
ORDER BY primary_key, block_num DESC, created_at DESC`,
[LEDGER2_CODE, coopname],
);
return (rows as Array<{ value: Record<string, unknown> }>).map((row) => {
return (rows as Array<{ value: Record<string, unknown>; present: boolean }>).map((row) => {
const v = row.value ?? {};
const live = row.present === true;
return {
id: parseInt(String(v.id ?? 0), 10),
name: String(v.name ?? ''),
balance: String(v.balance ?? '0.0000 RUB'),
debitBalance: String(v.debit_balance ?? '0.0000 RUB'),
creditBalance: String(v.credit_balance ?? '0.0000 RUB'),
balance: live ? String(v.balance ?? '0.0000 RUB') : zeroAssetLike(v.balance),
debitBalance: live ? String(v.debit_balance ?? '0.0000 RUB') : zeroAssetLike(v.debit_balance),
creditBalance: live ? String(v.credit_balance ?? '0.0000 RUB') : zeroAssetLike(v.credit_balance),
accountType: typeof v.account_type === 'number' ? v.account_type : Number(v.account_type ?? 0),
};
});
}
async getWallets(coopname: string): Promise<Ledger2WalletDomainInterface[]> {
// Контракт удаляет L2-запись (`cleanup_l2_if_empty`) при обнулении баланса.
// Parser2 кладёт delete как row с present=false и value=last_snapshot.
// В реестре кошельков такую запись **оставляем** (с available=0/blocked=0
// и id/name из snapshot), чтобы пользователь мог раскрыть и посмотреть
// историю движений по кошельку. WHERE present=true тут НЕЛЬЗЯ: фильтр
// выкинет delete-row, и DISTINCT ON выберет предыдущую present=true со
// старым снимком — кошелёк продолжал бы светиться с прежним балансом.
const rows = await this.deltaRepo.manager.query(
`SELECT DISTINCT ON (primary_key) value
`SELECT DISTINCT ON (primary_key) value, present
FROM blockchain_deltas
WHERE code = $1 AND "table" = '${Ledger2Contract.Tables.Wallets.tableName}' AND scope = $2
ORDER BY primary_key, block_num DESC, created_at DESC`,
[LEDGER2_CODE, coopname],
);
return (rows as Array<{ value: Record<string, unknown> }>).map((row) => {
return (rows as Array<{ value: Record<string, unknown>; present: boolean }>).map((row) => {
const v = row.value ?? {};
const live = row.present === true;
return {
id: String(v.id ?? ''),
name: String(v.name ?? ''),
available: String(v.available ?? '0.0000 RUB'),
blocked: String(v.blocked ?? '0.0000 RUB'),
available: live ? String(v.available ?? '0.0000 RUB') : zeroAssetLike(v.available),
blocked: live ? String(v.blocked ?? '0.0000 RUB') : zeroAssetLike(v.blocked),
};
});
}
@@ -86,6 +86,7 @@ import { USER_WALLET_REPOSITORY } from '~/domain/wallet/repositories/user-wallet
import { UserWalletTypeormRepository } from './repositories/user-wallet.typeorm-repository';
import { UserWalletDeltaMapper } from './blockchain/mappers/user-wallet-delta.mapper';
import { UserWalletSyncService } from './blockchain/services/user-wallet-sync.service';
import { UserWalletIndexInitializer } from './blockchain/services/user-wallet-index-initializer.service';
@Global()
@Module({
@@ -247,6 +248,7 @@ import { UserWalletSyncService } from './blockchain/services/user-wallet-sync.se
UserWalletTypeormRepository,
UserWalletDeltaMapper,
UserWalletSyncService,
UserWalletIndexInitializer,
EntityVersionRepository,
EntityVersioningService,
],
File diff suppressed because it is too large Load Diff
@@ -230,7 +230,7 @@ export interface IConvertsegm {
coopname: IName
username: IName
project_hash: IChecksum256
convert_hash: IChecksum256
result_hash: IChecksum256
wallet_amount: IAsset
capital_amount: IAsset
convert_statement: IDocument2
@@ -97,12 +97,12 @@ export const LEDGER2_OPERATION_REGISTRY: readonly OperationMeta[] = [
{ code: 'o.cap.invest', process_type: 'p.cap.invest', contract: 'capital',
name: 'INVEST', wallet_op: 'TRANSFER', wallet_from: 'w.wal.share', wallet_to: 'w.cap.blago',
debit: null, credit: null,
human_name: 'Инвестиция в ЦПП «Благорост» (перенос между кошельками)' },
human_name: 'Инвестиция в ЦПП «Благорост»' },
{ code: 'o.cap.commit', process_type: 'p.cap.rid', contract: 'capital',
name: 'COMMIT_RID', wallet_op: 'ISSUE', wallet_from: null, wallet_to: 'w.cap.gen',
debit: 8, credit: 80,
human_name: 'Коммит РИД по программе «Благорост»' },
human_name: 'Коммит РИД по программе «Генератор»' },
{ code: 'o.cap.accept', process_type: 'p.cap.rid', contract: 'capital',
name: 'ACCEPT_RID', wallet_op: 'NONE', wallet_from: null, wallet_to: null,
@@ -129,12 +129,12 @@ export const LEDGER2_OPERATION_REGISTRY: readonly OperationMeta[] = [
debit: null, credit: null,
human_name: 'Возврат паевого из ЦПП «Благорост» в Цифровой Кошелёк' },
{ code: 'o.cap.cnvshr', process_type: 'p.cap.cnvseg', contract: 'capital',
{ code: 'o.cap.cnvshr', process_type: 'p.cap.rid', contract: 'capital',
name: 'CONVERT_TO_SHARE', wallet_op: 'TRANSFER', wallet_from: 'w.cap.gen', wallet_to: 'w.wal.share',
debit: null, credit: null,
human_name: 'Конвертация сегмента: РИД → паевой взнос деньгами' },
{ code: 'o.cap.cnvbl', process_type: 'p.cap.cnvseg', contract: 'capital',
{ code: 'o.cap.cnvbl', process_type: 'p.cap.rid', contract: 'capital',
name: 'CONVERT_TO_BLAGO', wallet_op: 'TRANSFER', wallet_from: 'w.cap.gen', wallet_to: 'w.cap.blago',
debit: null, credit: null,
human_name: 'Конвертация сегмента: РИД → ЦПП «Благорост»' },
@@ -20,7 +20,7 @@ export interface WalletMeta {
*/
export const LEDGER2_WALLET_REGISTRY: readonly WalletMeta[] = [
{ name: "w.reg.minshr", human_name: "Минимальный паевой взнос", kind: "USER_SHARED" },
{ name: "w.wal.share", human_name: "ЦК — паевая часть пайщика", kind: "USER_SHARED" },
{ name: "w.wal.share", human_name: "Паевой взнос пайщика", kind: "USER_SHARED" },
{ name: "w.wal.member", human_name: "ЦК — членская часть пайщика", kind: "USER_SHARED" },
{ name: "w.cap.blago", human_name: "ЦПП «Благорост» — единый кошелёк программы у пайщика", kind: "USER_SHARED" },
{ name: "w.cap.gen", human_name: "ЦПП «Генератор» — единый кошелёк программы у пайщика", kind: "USER_SHARED" },
@@ -38,7 +38,7 @@ exports[`ledger2 wallets registry (generated from C++) > LEDGER2_WALLET_REGISTRY
"name": "w.reg.minshr",
},
{
"human_name": "ЦК — паевая часть пайщика",
"human_name": "Паевой взнос пайщика",
"kind": "USER_SHARED",
"name": "w.wal.share",
},
@@ -2,9 +2,9 @@ import type { Mutations } from '@coopenomics/sdk';
import { api } from '../api';
import { useSignDocument } from 'src/shared/lib/document/model/entity';
import { useSessionStore } from 'src/entities/Session/model';
import { generateUniqueHash } from 'src/shared/lib/utils/generateUniqueHash';
import { useSystemStore } from 'src/entities/System/model';
import { formatToAsset } from 'src/shared/lib/utils/formatToAsset';
import { useResultStore } from 'app/extensions/capital/entities/Result/model';
export type IConvertSegmentInput =
Mutations.Capital.ConvertSegment.IInput['data'];
@@ -13,6 +13,7 @@ export function useConvertSegment() {
const { signDocument } = useSignDocument();
const { username } = useSessionStore();
const { governSymbol, governPrecision } = useSystemStore();
const resultStore = useResultStore();
// Функция для форматирования суммы в EOSIO asset строку
const formatToEosioAsset = (amount: number): string => {
@@ -42,13 +43,18 @@ export function useConvertSegment() {
1, // signatureId = 1 для одинарной подписи
);
// 3. Получаем сегмент с подписанным документом
const convert_hash = await generateUniqueHash();
// 3. Получаем result_hash из объекта результата (анкер процесса p.cap.rid).
// Контракт convertsegm проверит, что result существует и в статусе ACT2.
const result = await resultStore.loadResultByFilters(segmentData.username, segmentData.project_hash);
if (!result) {
throw new Error('Объект результата не найден — внесение РИД не завершено (нужен signact2 от председателя).');
}
const convertData: IConvertSegmentInput = {
coopname: segmentData.coopname,
username: segmentData.username,
project_hash: segmentData.project_hash,
convert_hash,
result_hash: result.result_hash,
wallet_amount: formatToEosioAsset(segmentData.wallet_amount),
capital_amount: formatToEosioAsset(segmentData.capital_amount),
convert_statement: signedDocument,
@@ -22,7 +22,8 @@ div.page-shell
:expanded='expanded.has(props.row.id)'
@click='toggleExpand(props.row.id)'
)
q-td.font-monospace {{ props.row.id }}
q-td
WalletIdCell(:wallet-name='props.row.id')
q-td {{ props.row.name }}
q-td.text-right {{ formatAsset2Digits(props.row.available) }}
q-td.text-right {{ formatAsset2Digits(props.row.blocked) }}
@@ -101,7 +102,9 @@ div.page-shell
.row.items-center.q-gutter-x-md
.col
.text-body1 {{ props.row.name }}
.text-caption.caption-muted.font-monospace ID: {{ props.row.id }}
.text-caption.caption-muted
| ID:&nbsp;
WalletIdCell(:wallet-name='props.row.id')
.col-auto
ExpandToggleButton(
:expanded='expanded.has(props.row.id)'
+2 -2
View File
@@ -1,11 +1,11 @@
{
"name": "browser-harness",
"name": "@coopenomics/docs-harness",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "browser-harness",
"name": "@coopenomics/docs-harness",
"version": "1.0.0",
"license": "ISC",
"devDependencies": {
+1 -1
View File
@@ -1,5 +1,5 @@
{
"local": "046_create_drafts.ts",
"local": "049_migrate_phase2_progwallets.ts",
"test": "046_create_drafts.ts",
"prod": "046_create_drafts.ts"
}
File diff suppressed because it is too large Load Diff