fix(capital+loan): review PR#402 round 2 — изоляция loan, счётчик займов в сегменте, бездокументарные роли
Round 2 ревью PR #402 — 8 замечаний: LOAN-контракт независим от прикладных (capital/marketplace/...): - loan.debts: убран binary_extension<checksum256> project_hash и индекс byuserproj - loan.debts: добавлен binary_extension<name> source_contract (контракт-источник) - loan::createdebt(): убран параметр project_hash, payer пишется в source_contract - Loan::create_debt(): убран project_hash из сигнатуры и хелпера - Loan::count_user_project_debts(): удалена — capital считает у себя - Локальный контекст (project_hash и т.п.) остаётся в Capital::Debts CAPITAL — счётчик активных займов на сегменте через binary_extension: - Segment::active_debts_count (binary_extension<uint32_t>, хвостом) - increase/decrease/get_active_debts_count хелперы - debtpaycnfrm: после confirm_paid +1 счётчика сегмента - settledebt: после mark_settled -1 счётчика - signact2: лимит «10 займов на проект в одном паевом взносе» проверяется по счётчику (не через обход byprojhash); сам перебор ограничен счётчиком, ранний выход после нужного числа активных записей role_requests — бездокументарная схема и строгая типизация: - namespace Role { CREATOR, AUTHOR, MASTER, NONE } в role_requests.hpp - удалены поля statement и master_decision из role_request struct - create()/approve() — без параметров document2 - validate_role_or_fail и apply_role_to_project — через Role:: namespace - requestrole/inviterole/approverole/acceptinvite — без statement/master_decision - requestrateu — role = Role::NONE (не маркер "rate"_n), request_type = RATE_UPDATE - decline сохраняет только reason; решения подписываются coopname-транзакцией prerequisite fix: - names.hpp: убран дубль Names::Capital::CREATE_PROGRAM_EXPENSE (был объявлен и в callback-блоке line 55, и в акцепт-блоке line 130 — оставлен один) Замечания закрыты: - PR comment id=3264976591 (фу-фу byuserproj) — индекс удалён - id=3264980890 (count_user_project_debts) — функция удалена - id=3265021918 (loan независим от capital) — изоляция выполнена - id=3265072380 (rate как name через namespace) — RequestType::RATE_UPDATE + Role::NONE - id=3265080990 (счётчик в binary_extension вместо перебора) — active_debts_count - id=3265088096 (namespace типа роли) — Role::{CREATOR,AUTHOR,MASTER} - id=3265101459 (без statement/master_decision) — поля удалены - id=3265104196 (через Namespace проверка) — validate_role_or_fail через Role:: Сборка: cdt-cpp v4.1.0 capital.wasm + loan.wasm без ошибок. Тесты: capital-blagorost.test.ts — формулировки A2/D обновлены под счётчик и бездокументарную схему. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -60,30 +60,38 @@ describe('CAPITAL — расширения «Благорост» (Эпик 1)',
|
||||
)
|
||||
})
|
||||
|
||||
describe('A2 — Централизованный учёт займов (loan-контракт)', () => {
|
||||
describe('A2 — Централизованный учёт займов (loan-контракт + счётчик сегмента)', () => {
|
||||
it.todo(
|
||||
'Story 1.12: debtpaycnfrm создаёт запись в loan.debts через inline createdebt (с project_hash)'
|
||||
'Story 1.12: loan-контракт изолирован — loan.debts хранит только (coopname, username, debt_hash, amount, repaid_at) + source_contract; project_hash отсутствует'
|
||||
)
|
||||
|
||||
it.todo(
|
||||
'Story 1.12: settledebt в capital удаляет запись из loan.debts через inline settledebt'
|
||||
'Story 1.12: debtpaycnfrm создаёт запись в loan.debts через inline createdebt и поднимает loan.summaries.total пайщика'
|
||||
)
|
||||
|
||||
it.todo(
|
||||
'Story 1.12: signact2 с N≤10 займами на проекте — все погашаются inline-actions в loan'
|
||||
'Story 1.12: settledebt в capital удаляет запись из loan.debts через inline settledebt и опускает loan.summaries.total'
|
||||
)
|
||||
|
||||
it.todo(
|
||||
'Story 1.12: signact2 с >10 займами на проекте — fail с "Превышен лимит займов на проект для одного паевого взноса"'
|
||||
'Story 1.12: debt в loan.debts помечается source_contract = вызвавший контракт (capital для capital→loan)'
|
||||
)
|
||||
|
||||
it.todo(
|
||||
'Story 1.12: debtpaycnfrm увеличивает active_debts_count на сегменте (binary_extension), settledebt — уменьшает'
|
||||
)
|
||||
|
||||
it.todo(
|
||||
'Story 1.12: signact2 с N≤10 займами на проекте — все погашаются inline-actions в loan; active_debts_count = 0'
|
||||
)
|
||||
|
||||
it.todo(
|
||||
'Story 1.12: signact2 с >10 займами на проекте (active_debts_count > 10) — fail сразу по счётчику, без обхода byprojhash'
|
||||
)
|
||||
|
||||
it.todo(
|
||||
'Story 1.12: loan.createdebt от аккаунта вне contracts_whitelist — fail (только capital/marketplace/...)'
|
||||
)
|
||||
|
||||
it.todo(
|
||||
'Story 1.12: count_user_project_debts возвращает корректное число активных займов пайщика на проекте'
|
||||
)
|
||||
})
|
||||
|
||||
describe('B — Расходы программы', () => {
|
||||
@@ -108,9 +116,17 @@ describe('CAPITAL — расширения «Благорост» (Эпик 1)',
|
||||
)
|
||||
})
|
||||
|
||||
describe('D — Двухуровневые допуски и approved-ставка', () => {
|
||||
describe('D — Двухуровневые допуски и approved-ставка (бездокументарная схема)', () => {
|
||||
it.todo(
|
||||
'Story 1.7: requestrole создаёт rolerequests со статусом PENDING; approverole с 1200 фиксирует approved_rate'
|
||||
'Story 1.7: requestrole без statement создаёт rolerequests со статусом PENDING; approverole без master_decision фиксирует approved_rate'
|
||||
)
|
||||
|
||||
it.todo(
|
||||
'Story 1.7: requestrole с role вне Role::{CREATOR, AUTHOR, MASTER} — fail "Допустимые роли заявки..."'
|
||||
)
|
||||
|
||||
it.todo(
|
||||
'Story 1.7: requestrateu создаёт запись с role = Role::NONE и request_type = RequestType::RATE_UPDATE'
|
||||
)
|
||||
|
||||
it.todo(
|
||||
@@ -122,7 +138,7 @@ describe('CAPITAL — расширения «Благорост» (Эпик 1)',
|
||||
)
|
||||
|
||||
it.todo(
|
||||
'Story 1.7: inviterole → acceptinvite применяет инвайт-ставку к сегменту; declinvite оставляет сегмент без approved-rate'
|
||||
'Story 1.7: inviterole без statement → acceptinvite применяет инвайт-ставку к сегменту; declinvite оставляет сегмент без approved-rate'
|
||||
)
|
||||
|
||||
it.todo(
|
||||
@@ -130,7 +146,7 @@ describe('CAPITAL — расширения «Благорост» (Эпик 1)',
|
||||
)
|
||||
|
||||
it.todo(
|
||||
'Story 1.7: requestrateu + approverole с новой ставкой обновляет approved_rate в сегменте'
|
||||
'Story 1.7: requestrateu + approverole с новой ставкой обновляет approved_rate в сегменте; role.apply_role_to_project не вызывается'
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -32,11 +32,16 @@ void capital::debtpaycnfrm(name coopname, checksum256 debt_hash) {
|
||||
// Увеличиваем долг contributor (теперь долг активен и должен быть погашен через внесение результата)
|
||||
Capital::Contributors::increase_debt_amount(coopname, contributor->id, exist_debt.amount);
|
||||
|
||||
// Централизованный учёт займа: дублируем запись в контракт loan через inline action.
|
||||
// loan.debts становится единым реестром выданных займов для всех контрактов
|
||||
// (capital, marketplace и др.) — общий механизм погашения и проверки лимитов.
|
||||
// Локальный счётчик займов пайщика на проекте этого сегмента (+1).
|
||||
// Используется в signact2 как лимит «10 займов на проект» без обхода byprojhash.
|
||||
Capital::Segments::increase_active_debts_count(coopname, exist_debt.project_hash, exist_debt.username);
|
||||
|
||||
// Централизованный учёт займа в контракте loan — loan-контракт независим от capital,
|
||||
// знает только (coopname, username, debt_hash, amount, repaid_at) + source_contract
|
||||
// (заполняется самим loan::createdebt из payer-авторизации). Локальная привязка к
|
||||
// проекту/программе остаётся в Capital::Debts (поле project_hash).
|
||||
// due_at уже проставлен в confirm_paid, читаем обновлённое значение.
|
||||
auto fresh_debt = Capital::Debts::get_debt_or_fail(coopname, debt_hash);
|
||||
Loan::create_debt(_capital, coopname, exist_debt.username, exist_debt.project_hash,
|
||||
Loan::create_debt(_capital, coopname, exist_debt.username,
|
||||
debt_hash, fresh_debt.due_at, exist_debt.amount);
|
||||
};
|
||||
@@ -45,7 +45,10 @@ void capital::settledebt(name coopname, checksum256 debt_hash, eosio::asset amou
|
||||
Capital::Contributors::decrease_debt_amount(coopname, contributor->id, amount);
|
||||
Capital::Debts::mark_settled(coopname, exist_debt.id, memo, _capital);
|
||||
|
||||
// Централизованный учёт: удаляем запись из loan.debts через inline action.
|
||||
// Локальный счётчик займов пайщика на сегменте этого проекта (-1).
|
||||
Capital::Segments::decrease_active_debts_count(coopname, exist_debt.project_hash, exist_debt.username);
|
||||
|
||||
// Централизованный учёт: уменьшаем сводную и удаляем запись из loan.debts через inline action.
|
||||
Loan::settle_debt(_capital, coopname, exist_debt.username, debt_hash, amount);
|
||||
|
||||
// event ridge: и должник, и председатель видят факт погашения.
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ void capital::acceptinvite(name coopname, checksum256 request_hash) {
|
||||
eosio::check(req.direction == Capital::RoleRequests::Direction::INVITE,
|
||||
"Принять можно только инвайт");
|
||||
|
||||
Capital::RoleRequests::approve(coopname, req.id, req.rate_per_hour, req.hours_per_day, document2{});
|
||||
Capital::RoleRequests::approve(coopname, req.id, req.rate_per_hour, req.hours_per_day);
|
||||
Capital::Segments::set_approved_rate(
|
||||
coopname, req.project_hash, req.username, req.rate_per_hour, req.hours_per_day
|
||||
);
|
||||
|
||||
@@ -6,27 +6,27 @@
|
||||
* Сегмент должен уже существовать (создаётся при подписании приложения договора УХД
|
||||
* на этот проект через apprvappndx — L1-допуск).
|
||||
*
|
||||
* Бездокументарная схема: решение фиксируется только подписью транзакции
|
||||
* (require_auth coopname), документ-решение мастера не передаётся.
|
||||
*
|
||||
* @param coopname Кооператив
|
||||
* @param request_hash Хеш заявки (requestrole либо requestrateu)
|
||||
* @param approved_rate Утверждённая мастером ставка часа (≠ запрошенной — допустимо)
|
||||
* @param approved_hours Утверждённая норма часов
|
||||
* @param master_decision Документ-решение мастера
|
||||
* @ingroup public_actions
|
||||
* @ingroup public_capital_actions
|
||||
*/
|
||||
void capital::approverole(name coopname, checksum256 request_hash,
|
||||
eosio::asset approved_rate, uint64_t approved_hours,
|
||||
document2 master_decision) {
|
||||
eosio::asset approved_rate, uint64_t approved_hours) {
|
||||
require_auth(coopname);
|
||||
|
||||
verify_document_or_fail(master_decision);
|
||||
Wallet::validate_asset(approved_rate);
|
||||
eosio::check(approved_rate.amount > 0, "Утверждённая ставка должна быть положительной");
|
||||
eosio::check(approved_hours > 0 && approved_hours <= 8, "Норма часов — от 1 до 8");
|
||||
|
||||
auto req = Capital::RoleRequests::get_role_request_or_fail(coopname, request_hash);
|
||||
|
||||
Capital::RoleRequests::approve(coopname, req.id, approved_rate, approved_hours, master_decision);
|
||||
Capital::RoleRequests::approve(coopname, req.id, approved_rate, approved_hours);
|
||||
|
||||
Capital::Segments::set_approved_rate(
|
||||
coopname, req.project_hash, req.username, approved_rate, approved_hours
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
* приглашает кандидата на роль. Обратный поток к requestrole.
|
||||
* Кандидат далее acceptinvite (с применением приглашённой ставки) либо declinvite.
|
||||
*
|
||||
* Бездокументарная схема: приглашение и принятие/отказ оформляются только
|
||||
* подписью транзакции (require_auth coopname). Юридический документ-инвайт не нужен.
|
||||
*
|
||||
* Ставка часа в приглашении — это ставка, на которую кандидат соглашается через
|
||||
* acceptinvite. Не путать с contributors.rate_per_hour (личная глобальная ставка).
|
||||
*
|
||||
@@ -11,22 +14,20 @@
|
||||
* @param project_hash Проект / компонент
|
||||
* @param candidate Приглашаемый пайщик (потенциальный участник)
|
||||
* @param master Мастер-приглашающий (или координатор/председатель для роли master)
|
||||
* @param role Предлагаемая роль: creator | author | master
|
||||
* @param role Предлагаемая роль: Role::CREATOR | Role::AUTHOR | Role::MASTER
|
||||
* @param rate_per_hour Предлагаемая ставка часа
|
||||
* @param hours_per_day Предлагаемая норма часов в день
|
||||
* @param description Текст приглашения (может быть пустым)
|
||||
* @param statement Инвайт-документ (RoleInviteStatement — Эпик 3)
|
||||
* @ingroup public_actions
|
||||
* @ingroup public_capital_actions
|
||||
*/
|
||||
void capital::inviterole(name coopname, checksum256 request_hash, checksum256 project_hash,
|
||||
name candidate, name master, name role,
|
||||
eosio::asset rate_per_hour, uint64_t hours_per_day,
|
||||
std::string description, document2 statement) {
|
||||
std::string description) {
|
||||
require_auth(coopname);
|
||||
|
||||
Capital::RoleRequests::validate_role_or_fail(role);
|
||||
verify_document_or_fail(statement);
|
||||
Wallet::validate_asset(rate_per_hour);
|
||||
eosio::check(rate_per_hour.amount > 0, "Ставка часа должна быть положительной");
|
||||
eosio::check(hours_per_day > 0 && hours_per_day <= 8, "Норма часов — от 1 до 8");
|
||||
@@ -36,7 +37,7 @@ void capital::inviterole(name coopname, checksum256 request_hash, checksum256 pr
|
||||
rate_per_hour, hours_per_day,
|
||||
Capital::RoleRequests::Direction::INVITE,
|
||||
Capital::RoleRequests::RequestType::ROLE,
|
||||
description, statement
|
||||
description
|
||||
);
|
||||
|
||||
// event ridge: кандидат и мастер видят инвайт.
|
||||
|
||||
+9
-7
@@ -1,8 +1,12 @@
|
||||
/**
|
||||
* @brief Заявка пайщика на обновление approved-ставки на сегменте.
|
||||
* Тот же flow что и requestrole, но RequestType::RATE_UPDATE.
|
||||
* Тот же flow что и requestrole, но через RequestType::RATE_UPDATE.
|
||||
* approverole с новой ставкой → обновление сегмента; declinerole — без изменений.
|
||||
*
|
||||
* Бездокументарная схема: заявление-документ не передаётся, утверждение
|
||||
* фиксируется только транзакцией. role в записи остаётся Role::NONE
|
||||
* (валидация роли для RATE_UPDATE не применяется).
|
||||
*
|
||||
* @param coopname Кооператив
|
||||
* @param request_hash Хеш заявки
|
||||
* @param project_hash Проект / компонент
|
||||
@@ -11,28 +15,26 @@
|
||||
* @param new_rate Желаемая новая ставка
|
||||
* @param new_hours Желаемая новая норма часов
|
||||
* @param description Текст заявки на обновление ставки (может быть пустым)
|
||||
* @param statement Заявление (RateUpdateStatement — Эпик 3)
|
||||
* @ingroup public_actions
|
||||
* @ingroup public_capital_actions
|
||||
*/
|
||||
void capital::requestrateu(name coopname, checksum256 request_hash, checksum256 project_hash,
|
||||
name username, name master,
|
||||
eosio::asset new_rate, uint64_t new_hours,
|
||||
std::string description, document2 statement) {
|
||||
std::string description) {
|
||||
require_auth(coopname);
|
||||
|
||||
verify_document_or_fail(statement);
|
||||
Wallet::validate_asset(new_rate);
|
||||
eosio::check(new_rate.amount > 0, "Новая ставка должна быть положительной");
|
||||
eosio::check(new_hours > 0 && new_hours <= 8, "Норма часов — от 1 до 8");
|
||||
|
||||
// role не значим для RATE_UPDATE — кладём "rate"_n как маркер (валидацию ролей не запускаем).
|
||||
Capital::RoleRequests::create(
|
||||
coopname, request_hash, project_hash, username, master, "rate"_n,
|
||||
coopname, request_hash, project_hash, username, master,
|
||||
Capital::RoleRequests::Role::NONE,
|
||||
new_rate, new_hours,
|
||||
Capital::RoleRequests::Direction::REQUEST,
|
||||
Capital::RoleRequests::RequestType::RATE_UPDATE,
|
||||
description, statement
|
||||
description
|
||||
);
|
||||
|
||||
// event ridge: заявитель и мастер компонента видят запрос на обновление ставки.
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
* (или председатель — для роли master, когда мастера в проекте ещё нет)
|
||||
* решает: approverole (с фиксацией approved-ставки в сегменте) или declinerole.
|
||||
*
|
||||
* Бездокументарная схема: подача и решение оформляются только подписью транзакции
|
||||
* (require_auth coopname). Юридическое заявление при приёме на роль не требуется —
|
||||
* допуск даётся подписью действия на стороне кооператива.
|
||||
*
|
||||
* Ставка часа подаётся явно — это ставка пайщика именно под этот проект.
|
||||
* По дефолту фронт подставляет contributors.rate_per_hour, но пайщик может изменить.
|
||||
*
|
||||
@@ -13,22 +17,20 @@
|
||||
* @param project_hash Проект (или компонент — на уровне хэша одно)
|
||||
* @param username Заявитель
|
||||
* @param master Мастер компонента (ожидаемый одобряющий)
|
||||
* @param role Запрашиваемая роль: creator | author | master
|
||||
* @param role Запрашиваемая роль: Role::CREATOR | Role::AUTHOR | Role::MASTER
|
||||
* @param rate_per_hour Заявленная ставка часа под этот проект
|
||||
* @param hours_per_day Заявленная норма часов в день
|
||||
* @param description Текст заявки (может быть пустым)
|
||||
* @param statement Заявление (документ RoleRequestStatement — Эпик 3)
|
||||
* @ingroup public_actions
|
||||
* @ingroup public_capital_actions
|
||||
*/
|
||||
void capital::requestrole(name coopname, checksum256 request_hash, checksum256 project_hash,
|
||||
name username, name master, name role,
|
||||
eosio::asset rate_per_hour, uint64_t hours_per_day,
|
||||
std::string description, document2 statement) {
|
||||
std::string description) {
|
||||
require_auth(coopname);
|
||||
|
||||
Capital::RoleRequests::validate_role_or_fail(role);
|
||||
verify_document_or_fail(statement);
|
||||
Wallet::validate_asset(rate_per_hour);
|
||||
eosio::check(rate_per_hour.amount > 0, "Ставка часа должна быть положительной");
|
||||
eosio::check(hours_per_day > 0 && hours_per_day <= 8, "Норма часов в день — от 1 до 8");
|
||||
@@ -38,7 +40,7 @@ void capital::requestrole(name coopname, checksum256 request_hash, checksum256 p
|
||||
rate_per_hour, hours_per_day,
|
||||
Capital::RoleRequests::Direction::REQUEST,
|
||||
Capital::RoleRequests::RequestType::ROLE,
|
||||
description, statement
|
||||
description
|
||||
);
|
||||
|
||||
// event ridge: заявитель и мастер компонента видят новую заявку.
|
||||
|
||||
@@ -66,28 +66,35 @@ void capital::signact2(eosio::name coopname, eosio::name chairman, checksum256 r
|
||||
if (result -> debt_amount.amount > 0){
|
||||
Ledger2::apply(_capital, coopname, operations::capital::REPAY, result -> debt_amount, result -> username, result_hash, memo);
|
||||
|
||||
// Централизованное погашение конкретных займов в loan-контракте.
|
||||
// Собираем все активные (PAID/OVERDUE) займы пайщика на этом проекте,
|
||||
// лимит 10 — иначе одна транзакция EOSIO с 11+ inline actions не уложится в лимит.
|
||||
// Лимит «10 займов на проект в одном паевом взносе» проверяем по счётчику
|
||||
// active_debts_count на сегменте (binary_extension) — без обхода byprojhash.
|
||||
// Сам счётчик инкрементируется в debtpaycnfrm и декрементируется в mark_settled.
|
||||
uint32_t active_cnt = Capital::Segments::get_active_debts_count(coopname, result->project_hash, result->username);
|
||||
eosio::check(active_cnt <= 10,
|
||||
"Превышен лимит займов на проект для одного паевого взноса (10)");
|
||||
|
||||
// Перебираем долги пайщика по проекту, но не больше известного счётчика —
|
||||
// ранний выход сразу после нужного числа активных записей.
|
||||
Capital::Debts::debts_index debts(_capital, coopname.value);
|
||||
auto byproj = debts.get_index<"byprojhash"_n>();
|
||||
std::vector<std::tuple<uint64_t, checksum256, eosio::asset>> to_settle;
|
||||
to_settle.reserve(active_cnt);
|
||||
for (auto it = byproj.find(result->project_hash);
|
||||
it != byproj.end() && it->project_hash == result->project_hash; ++it) {
|
||||
it != byproj.end() && it->project_hash == result->project_hash
|
||||
&& to_settle.size() < active_cnt; ++it) {
|
||||
if (it->username == result->username
|
||||
&& (it->status == Capital::Debts::Status::PAID
|
||||
|| it->status == Capital::Debts::Status::OVERDUE)) {
|
||||
to_settle.emplace_back(it->id, it->debt_hash, it->amount);
|
||||
}
|
||||
}
|
||||
eosio::check(to_settle.size() <= 10,
|
||||
"Превышен лимит займов на проект для одного паевого взноса (10)");
|
||||
for (auto& tup : to_settle) {
|
||||
auto id = std::get<0>(tup);
|
||||
auto dh = std::get<1>(tup);
|
||||
auto amt = std::get<2>(tup);
|
||||
Loan::settle_debt(_capital, coopname, result->username, dh, amt);
|
||||
Capital::Debts::mark_settled(coopname, id, memo, _capital);
|
||||
Capital::Segments::decrease_active_debts_count(coopname, result->project_hash, result->username);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -348,17 +348,17 @@ public:
|
||||
[[eosio::action]]
|
||||
void declpgexp(name coopname, checksum256 expense_hash, std::string reason);
|
||||
|
||||
// L2-роли и approved-ставки сегмента (Story 1.7)
|
||||
// L2-роли и approved-ставки сегмента (Story 1.7) — бездокументарная схема,
|
||||
// решения фиксируются только подписью транзакции (require_auth coopname).
|
||||
[[eosio::action]]
|
||||
void requestrole(name coopname, checksum256 request_hash, checksum256 project_hash,
|
||||
name username, name master, name role,
|
||||
eosio::asset rate_per_hour, uint64_t hours_per_day,
|
||||
std::string description, document2 statement);
|
||||
std::string description);
|
||||
|
||||
[[eosio::action]]
|
||||
void approverole(name coopname, checksum256 request_hash,
|
||||
eosio::asset approved_rate, uint64_t approved_hours,
|
||||
document2 master_decision);
|
||||
eosio::asset approved_rate, uint64_t approved_hours);
|
||||
|
||||
[[eosio::action]]
|
||||
void declinerole(name coopname, checksum256 request_hash, std::string reason);
|
||||
@@ -367,7 +367,7 @@ public:
|
||||
void inviterole(name coopname, checksum256 request_hash, checksum256 project_hash,
|
||||
name candidate, name master, name role,
|
||||
eosio::asset rate_per_hour, uint64_t hours_per_day,
|
||||
std::string description, document2 statement);
|
||||
std::string description);
|
||||
|
||||
[[eosio::action]]
|
||||
void acceptinvite(name coopname, checksum256 request_hash);
|
||||
@@ -379,6 +379,6 @@ public:
|
||||
void requestrateu(name coopname, checksum256 request_hash, checksum256 project_hash,
|
||||
name username, name master,
|
||||
eosio::asset new_rate, uint64_t new_hours,
|
||||
std::string description, document2 statement);
|
||||
std::string description);
|
||||
|
||||
};
|
||||
|
||||
@@ -33,8 +33,24 @@ namespace RequestType {
|
||||
constexpr name RATE_UPDATE = "rateupdate"_n;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Допустимые роли заявки на L2-допуск.
|
||||
* Контрибьюторы и инвесторы — L1, через role_requests не идут.
|
||||
*/
|
||||
namespace Role {
|
||||
constexpr name CREATOR = "creator"_n;
|
||||
constexpr name AUTHOR = "author"_n;
|
||||
constexpr name MASTER = "master"_n;
|
||||
constexpr name NONE = name{}; ///< Для RATE_UPDATE: поле role не значимо
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Очередь заявок на L2-допуск/инвайт и на обновление approved-ставки.
|
||||
*
|
||||
* Бездокументарная схема: подача и решение оформляются только транзакцией
|
||||
* блокчейна (require_auth coopname). Юридическое заявление при приёме на роль
|
||||
* не требуется — допуск даётся через подпись действия на стороне кооператива.
|
||||
*
|
||||
* @par Область памяти (scope): coopname
|
||||
* @par Имя таблицы (table): rolerequests
|
||||
*/
|
||||
@@ -45,19 +61,17 @@ struct [[eosio::table, eosio::contract(CAPITAL)]] role_request {
|
||||
checksum256 project_hash; ///< Проект / компонент
|
||||
eosio::name username; ///< Заявитель (REQUEST) или кандидат (INVITE)
|
||||
eosio::name master; ///< Мастер компонента (одобряющий) — при invite на роль master это координатор/председатель
|
||||
eosio::name role; ///< Заявляемая роль: creator | author | master (контрибьютор — L1, сюда не попадает)
|
||||
eosio::asset rate_per_hour = asset(0, _root_govern_symbol); ///< Заявленная пайщиком/мастером ставка часа (всегда явная, не из contributors)
|
||||
uint64_t hours_per_day = 0; ///< Заявленная норма часов
|
||||
eosio::asset approved_rate = asset(0, _root_govern_symbol); ///< Утверждённая ставка (≠ requested при approve)
|
||||
uint64_t approved_hours = 0; ///< Утверждённая норма часов
|
||||
eosio::name direction = Direction::REQUEST; ///< request | invite
|
||||
eosio::name request_type = RequestType::ROLE; ///< role | rateupdate
|
||||
eosio::name status = Status::PENDING; ///< pending | approved | declined
|
||||
eosio::name role = Role::NONE; ///< Role::CREATOR | Role::AUTHOR | Role::MASTER (для RequestType::RATE_UPDATE — Role::NONE)
|
||||
eosio::asset rate_per_hour = asset(0, _root_govern_symbol); ///< Заявленная пайщиком/мастером ставка часа (всегда явная, не из contributors)
|
||||
uint64_t hours_per_day = 0; ///< Заявленная норма часов
|
||||
eosio::asset approved_rate = asset(0, _root_govern_symbol); ///< Утверждённая ставка (≠ requested при approve)
|
||||
uint64_t approved_hours = 0; ///< Утверждённая норма часов
|
||||
eosio::name direction = Direction::REQUEST; ///< request | invite
|
||||
eosio::name request_type = RequestType::ROLE; ///< role | rateupdate
|
||||
eosio::name status = Status::PENDING; ///< pending | approved | declined
|
||||
std::string description; ///< Текст заявки / приглашения (может быть пустым)
|
||||
document2 statement; ///< Заявление пайщика / инвайт-документ мастера
|
||||
document2 master_decision; ///< Решение мастера (approve/decline)
|
||||
std::string decline_reason; ///< Причина отказа (если DECLINED)
|
||||
time_point_sec created_at = current_time_point();
|
||||
time_point_sec created_at = current_time_point();
|
||||
|
||||
uint64_t primary_key() const { return id; }
|
||||
checksum256 by_request_hash() const { return request_hash; }
|
||||
@@ -97,8 +111,7 @@ inline void create(
|
||||
uint64_t hours_per_day,
|
||||
eosio::name direction,
|
||||
eosio::name request_type,
|
||||
const std::string &description,
|
||||
const document2 &statement
|
||||
const std::string &description
|
||||
) {
|
||||
role_requests_index t(_capital, coopname.value);
|
||||
eosio::check(!get_role_request(coopname, request_hash).has_value(),
|
||||
@@ -119,25 +132,25 @@ inline void create(
|
||||
r.request_type = request_type;
|
||||
r.status = Status::PENDING;
|
||||
r.description = description;
|
||||
r.statement = statement;
|
||||
r.created_at = current_time_point();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Проверяет, что role входит в допустимый набор {creator, author, master}.
|
||||
* @brief Проверяет, что role входит в допустимый набор Role::{CREATOR, AUTHOR, MASTER}.
|
||||
* Контрибьюторы и инвесторы — это L1-роли и через role_requests не идут.
|
||||
* Не применяется к заявкам RequestType::RATE_UPDATE (там role = Role::NONE).
|
||||
*/
|
||||
inline void validate_role_or_fail(eosio::name role) {
|
||||
eosio::check(
|
||||
role == "creator"_n || role == "author"_n || role == "master"_n,
|
||||
role == Role::CREATOR || role == Role::AUTHOR || role == Role::MASTER,
|
||||
"Допустимые роли заявки: creator, author, master"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Применяет заявленную роль к проекту через inline action:
|
||||
* author → addauthor, master → setmaster, creator → no-op
|
||||
* Role::AUTHOR → addauthor, Role::MASTER → setmaster, Role::CREATOR → no-op
|
||||
* (creator фиксируется фактически при первом createcmmt).
|
||||
*
|
||||
* Вызывается из approverole/acceptinvite после фиксации решения, чтобы
|
||||
@@ -150,14 +163,14 @@ inline void apply_role_to_project(
|
||||
eosio::name username,
|
||||
eosio::name role
|
||||
) {
|
||||
if (role == "author"_n) {
|
||||
if (role == Role::AUTHOR) {
|
||||
eosio::action(
|
||||
eosio::permission_level{coopname, "active"_n},
|
||||
_capital,
|
||||
"addauthor"_n,
|
||||
std::make_tuple(coopname, project_hash, username)
|
||||
).send();
|
||||
} else if (role == "master"_n) {
|
||||
} else if (role == Role::MASTER) {
|
||||
eosio::action(
|
||||
eosio::permission_level{coopname, "active"_n},
|
||||
_capital,
|
||||
@@ -171,8 +184,7 @@ inline void approve(
|
||||
eosio::name coopname,
|
||||
uint64_t request_id,
|
||||
const eosio::asset &approved_rate,
|
||||
uint64_t approved_hours,
|
||||
const document2 &master_decision
|
||||
uint64_t approved_hours
|
||||
) {
|
||||
role_requests_index t(_capital, coopname.value);
|
||||
auto itr = t.find(request_id);
|
||||
@@ -182,7 +194,6 @@ inline void approve(
|
||||
r.status = Status::APPROVED;
|
||||
r.approved_rate = approved_rate;
|
||||
r.approved_hours = approved_hours;
|
||||
r.master_decision = master_decision;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -120,6 +120,12 @@ namespace Capital::Segments {
|
||||
eosio::binary_extension<eosio::asset> approved_rate_per_hour; ///< Утверждённая мастером ставка часа на этом сегменте
|
||||
eosio::binary_extension<uint64_t> approved_hours_per_day; ///< Утверждённое мастером количество часов в день на этом сегменте
|
||||
|
||||
// Счётчик активных (PAID/OVERDUE) займов пайщика на проекте этого сегмента.
|
||||
// Инкрементируется в Capital::Debts::confirm_paid, декрементируется в mark_settled.
|
||||
// Используется как лимит «10 займов на проект в одном паевом взносе» (signact2),
|
||||
// чтобы не перебирать byprojhash и не упираться в 30-сек лимит транзакции.
|
||||
eosio::binary_extension<uint32_t> active_debts_count;
|
||||
|
||||
uint64_t primary_key() const { return id; } ///< Первичный ключ (1)
|
||||
|
||||
checksum256 by_project_hash() const { return project_hash; } ///< Индекс по хэшу проекта (2)
|
||||
@@ -449,14 +455,57 @@ inline void increase_debt_amount(eosio::name coopname, uint64_t segment_id, eosi
|
||||
inline void decrease_debt_amount(eosio::name coopname, uint64_t segment_id, eosio::asset amount) {
|
||||
Capital::Segments::segments_index segments(_capital, coopname.value);
|
||||
auto segment = segments.find(segment_id);
|
||||
|
||||
|
||||
eosio::check(segment->debt_amount >= amount, "Пайщик не может погасить долг больше, чем должен");
|
||||
|
||||
|
||||
segments.modify(segment, coopname, [&](auto &s) {
|
||||
s.debt_amount -= amount;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Увеличивает счётчик активных займов пайщика на сегменте на +1.
|
||||
* Вызывается в Capital::Debts::confirm_paid после перехода долга в PAID.
|
||||
*/
|
||||
inline void increase_active_debts_count(eosio::name coopname, const checksum256 &project_hash, eosio::name username) {
|
||||
segments_index segments(_capital, coopname.value);
|
||||
auto idx = segments.get_index<"byprojuser"_n>();
|
||||
auto rkey = combine_checksum_ids(project_hash, username);
|
||||
auto it = idx.find(rkey);
|
||||
eosio::check(it != idx.end(), "Сегмент участника на этом проекте не найден");
|
||||
idx.modify(it, coopname, [&](auto &s) {
|
||||
uint32_t cur = s.active_debts_count.has_value() ? *s.active_debts_count : 0;
|
||||
s.active_debts_count.emplace(cur + 1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Уменьшает счётчик активных займов пайщика на сегменте на -1.
|
||||
* Вызывается в Capital::Debts::mark_settled (settledebt и signact2-погашение).
|
||||
*/
|
||||
inline void decrease_active_debts_count(eosio::name coopname, const checksum256 &project_hash, eosio::name username) {
|
||||
segments_index segments(_capital, coopname.value);
|
||||
auto idx = segments.get_index<"byprojuser"_n>();
|
||||
auto rkey = combine_checksum_ids(project_hash, username);
|
||||
auto it = idx.find(rkey);
|
||||
eosio::check(it != idx.end(), "Сегмент участника на этом проекте не найден");
|
||||
idx.modify(it, coopname, [&](auto &s) {
|
||||
uint32_t cur = s.active_debts_count.has_value() ? *s.active_debts_count : 0;
|
||||
eosio::check(cur > 0, "Счётчик активных займов уже на нуле");
|
||||
s.active_debts_count.emplace(cur - 1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Текущее значение счётчика активных займов пайщика на сегменте.
|
||||
* Используется в signact2 как лимит обхода долгов проекта (≤10).
|
||||
*/
|
||||
inline uint32_t get_active_debts_count(eosio::name coopname, const checksum256 &project_hash, eosio::name username) {
|
||||
auto seg = get_segment(coopname, project_hash, username);
|
||||
if (!seg.has_value()) return 0;
|
||||
return seg->active_debts_count.has_value() ? *seg->active_debts_count : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Удаляет сегмент участника
|
||||
* @param coopname Имя кооператива
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
using namespace eosio;
|
||||
using std::string;
|
||||
|
||||
#define CREATEDEBT_SIGNATURE name coopname, name username, checksum256 project_hash, checksum256 debt_hash, time_point_sec repaid_at, asset quantity
|
||||
#define CREATEDEBT_SIGNATURE name coopname, name username, checksum256 debt_hash, time_point_sec repaid_at, asset quantity
|
||||
#define SETTLEDEBT_SIGNATURE name coopname, name username, checksum256 debt_hash, asset quantity
|
||||
|
||||
using createdebt_interface = void(CREATEDEBT_SIGNATURE);
|
||||
@@ -51,33 +51,9 @@ inline void assert_no_expired_debts(name coopname, name username) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Кол-во активных займов пайщика на конкретном проекте.
|
||||
* Используется в capital::signact2 для лимита 10 займов на проект
|
||||
* при автоматическом погашении из паевого взноса.
|
||||
*/
|
||||
inline uint64_t count_user_project_debts(
|
||||
name coopname, const checksum256 &project_hash, name username
|
||||
) {
|
||||
debts_index debts(_loan, coopname.value);
|
||||
auto idx = debts.get_index<"byuserproj"_n>();
|
||||
auto p_bytes = project_hash.extract_as_byte_array();
|
||||
uint64_t hi = (uint64_t)p_bytes[0] << 56 | (uint64_t)p_bytes[1] << 48
|
||||
| (uint64_t)p_bytes[2] << 40 | (uint64_t)p_bytes[3] << 32
|
||||
| (uint64_t)p_bytes[4] << 24 | (uint64_t)p_bytes[5] << 16
|
||||
| (uint64_t)p_bytes[6] << 8 | (uint64_t)p_bytes[7];
|
||||
uint128_t key = ((uint128_t)hi << 64) | username.value;
|
||||
|
||||
uint64_t cnt = 0;
|
||||
for (auto it = idx.lower_bound(key); it != idx.end() && it->by_user_project() == key; ++it) {
|
||||
++cnt;
|
||||
}
|
||||
return cnt;
|
||||
}
|
||||
|
||||
inline void create_debt(name calling_contract, CREATEDEBT_SIGNATURE) {
|
||||
Action::send<createdebt_interface>(_loan, Names::Loan::CREATE_DEBT, calling_contract, coopname, username,
|
||||
project_hash, debt_hash, repaid_at, quantity);
|
||||
debt_hash, repaid_at, quantity);
|
||||
}
|
||||
|
||||
inline void settle_debt(name calling_contract, SETTLEDEBT_SIGNATURE) {
|
||||
|
||||
@@ -52,7 +52,7 @@ namespace Names {
|
||||
constexpr eosio::name DECLINE_EXPENSE = "capdeclexpns"_n;
|
||||
|
||||
// Коллбэки для программных расходов
|
||||
constexpr eosio::name CREATE_PROGRAM_EXPENSE = "createpgexp"_n;
|
||||
// (CREATE_PROGRAM_EXPENSE объявлен ниже в блоке "акцептов" — общий для обоих контекстов)
|
||||
constexpr eosio::name AUTHORIZE_PROGRAM_EXPENSE = "authpgexp"_n;
|
||||
constexpr eosio::name DECLINE_PROGRAM_EXPENSE = "declpgexp"_n;
|
||||
constexpr eosio::name CONFIRM_PROGRAM_EXPENSE_PAYMENT = "pgexppay"_n;
|
||||
|
||||
@@ -20,25 +20,17 @@ struct [[eosio::table, eosio::contract(LOAN)]] debt {
|
||||
time_point_sec created_at;
|
||||
time_point_sec repaid_at;
|
||||
|
||||
// Привязка займа к проекту/программе вызывающего контракта (для паевого взноса
|
||||
// нужно знать, какие займы пайщика гасятся именно по этому проекту).
|
||||
// binary_extension: таблица debts уже в продакшене (но пустая), новые поля только хвостом.
|
||||
binary_extension<checksum256> project_hash;
|
||||
// Контракт-источник, инициировавший выдачу займа (capital / marketplace / ...).
|
||||
// loan не знает доменного контекста (проекта, программы) — это ответственность
|
||||
// вызывающего контракта; ему же принадлежит и локальная привязка debt_hash.
|
||||
// binary_extension: таблица в продакшене (пустая), новые поля только хвостом.
|
||||
binary_extension<name> source_contract;
|
||||
|
||||
uint64_t primary_key() const { return id; }
|
||||
uint64_t by_username() const { return username.value; }
|
||||
checksum256 by_debt_hash() const { return debt_hash; }
|
||||
uint64_t by_created() const { return created_at.sec_since_epoch(); }
|
||||
uint64_t by_repaid() const { return repaid_at.sec_since_epoch(); }
|
||||
uint128_t by_user_project() const {
|
||||
checksum256 ph = project_hash.has_value() ? *project_hash : checksum256{};
|
||||
auto p_bytes = ph.extract_as_byte_array();
|
||||
uint64_t hi = (uint64_t)p_bytes[0] << 56 | (uint64_t)p_bytes[1] << 48
|
||||
| (uint64_t)p_bytes[2] << 40 | (uint64_t)p_bytes[3] << 32
|
||||
| (uint64_t)p_bytes[4] << 24 | (uint64_t)p_bytes[5] << 16
|
||||
| (uint64_t)p_bytes[6] << 8 | (uint64_t)p_bytes[7];
|
||||
return ((uint128_t)hi << 64) | username.value;
|
||||
}
|
||||
};
|
||||
|
||||
typedef multi_index<
|
||||
@@ -46,8 +38,7 @@ typedef multi_index<
|
||||
indexed_by<"byusername"_n, const_mem_fun<debt, uint64_t, &debt::by_username>>,
|
||||
indexed_by<"bydebthash"_n, const_mem_fun<debt, checksum256, &debt::by_debt_hash>>,
|
||||
indexed_by<"bycreated"_n, const_mem_fun<debt, uint64_t, &debt::by_created>>,
|
||||
indexed_by<"byrepaid"_n, const_mem_fun<debt, uint64_t, &debt::by_repaid>>,
|
||||
indexed_by<"byuserproj"_n, const_mem_fun<debt, uint128_t, &debt::by_user_project>>>
|
||||
indexed_by<"byrepaid"_n, const_mem_fun<debt, uint64_t, &debt::by_repaid>>>
|
||||
debts_index;
|
||||
|
||||
} // namespace Loan
|
||||
|
||||
@@ -2,12 +2,13 @@ using namespace Loan;
|
||||
|
||||
/**
|
||||
* @brief Создание долгового обязательства.
|
||||
* Создает новое долговое обязательство для пользователя с указанным сроком погашения
|
||||
* и привязкой к проекту вызывающего контракта.
|
||||
* Создает новое долговое обязательство пайщика. loan-контракт независим от
|
||||
* прикладных контрактов — здесь нет привязки к проекту/программе/компоненту.
|
||||
* Локальный контекст (project_hash и т.п.) хранится на стороне вызывающего
|
||||
* контракта в его собственной таблице долгов.
|
||||
* @param coopname Наименование кооператива
|
||||
* @param username Имя пользователя-должника
|
||||
* @param project_hash Хэш проекта/программы (для лимита займов на проект при паевом взносе)
|
||||
* @param debt_hash Хэш долгового обязательства
|
||||
* @param debt_hash Хэш долгового обязательства (идентификатор)
|
||||
* @param repaid_at Срок погашения долга
|
||||
* @param quantity Сумма долга
|
||||
* @ingroup public_actions
|
||||
@@ -15,7 +16,7 @@ using namespace Loan;
|
||||
|
||||
* @note Авторизация требуется от аккаунта: @p contracts_whitelist
|
||||
*/
|
||||
void loan::createdebt(name coopname, name username, checksum256 project_hash, checksum256 debt_hash, time_point_sec repaid_at, asset quantity) {
|
||||
void loan::createdebt(name coopname, name username, checksum256 debt_hash, time_point_sec repaid_at, asset quantity) {
|
||||
name payer = check_auth_and_get_payer_or_fail(contracts_whitelist);
|
||||
|
||||
check(quantity.amount > 0, "Сумма должна быть положительной");
|
||||
@@ -35,7 +36,7 @@ void loan::createdebt(name coopname, name username, checksum256 project_hash, ch
|
||||
d.amount = quantity;
|
||||
d.created_at = time_point_sec(current_time_point());
|
||||
d.repaid_at = repaid_at;
|
||||
d.project_hash.emplace(project_hash);
|
||||
d.source_contract.emplace(payer);
|
||||
});
|
||||
|
||||
Loan::summaries_index summaries(_loan, coopname.value);
|
||||
|
||||
Reference in New Issue
Block a user