Merge remote-tracking branch 'origin/ledger3' into marketplace2
# Conflicts: # components/contracts/cpp/capital/p.cap.rid.standard.yaml
This commit is contained in:
@@ -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,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);
|
||||
|
||||
+12
-2
@@ -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,
|
||||
|
||||
+16
@@ -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);
|
||||
}
|
||||
+2
-3
@@ -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,
|
||||
|
||||
-109
@@ -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), уменьшает доступные суммы для кошелька
|
||||
- Конвертация в проект требует разрешения проекта
|
||||
- При наличии родительского проекта средства направляются в него
|
||||
|
||||
*/
|
||||
+57
-41
@@ -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)
|
||||
// - Долг погашается только за счет базовых сумм создателя/автора/координатора
|
||||
// - Долг погашается только за счет базовых сумм создателя/автора/координатора
|
||||
+44
-9
@@ -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);
|
||||
};
|
||||
@@ -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"
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
};
|
||||
+170
-924
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' }],
|
||||
|
||||
|
||||
+4
-4
@@ -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: 'Сумма для конвертации в главный кошелек не должна быть пустой' })
|
||||
|
||||
+54
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+87
-60
@@ -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,
|
||||
|
||||
+2
-2
@@ -13,8 +13,8 @@ export interface ConvertSegmentDomainInput {
|
||||
/** Хэш проекта */
|
||||
project_hash: string;
|
||||
|
||||
/** Хэш конвертации */
|
||||
convert_hash: string;
|
||||
/** Хэш результата (анкер процесса p.cap.rid) */
|
||||
result_hash: string;
|
||||
|
||||
/** Сумма для конвертации в главный кошелек */
|
||||
wallet_amount: string;
|
||||
|
||||
+51
@@ -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`
|
||||
);
|
||||
}
|
||||
}
|
||||
+9
-3
@@ -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;
|
||||
|
||||
+37
-9
@@ -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,
|
||||
],
|
||||
|
||||
+468
-1378
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",
|
||||
},
|
||||
|
||||
+10
-4
@@ -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:
|
||||
WalletIdCell(:wallet-name='props.row.id')
|
||||
.col-auto
|
||||
ExpandToggleButton(
|
||||
:expanded='expanded.has(props.row.id)'
|
||||
|
||||
+2
-2
@@ -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,5 +1,5 @@
|
||||
{
|
||||
"local": "046_create_drafts.ts",
|
||||
"local": "049_migrate_phase2_progwallets.ts",
|
||||
"test": "046_create_drafts.ts",
|
||||
"prod": "046_create_drafts.ts"
|
||||
}
|
||||
+468
-1378
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user