feat(loan): централизация учёта займов — capital вызывает loan через inline action
Ревью PR#402 (debtpaycnfrm:23): учёт займов должен быть централизован в одном контракте, как Gateway для платежей. Существующий контракт loan/ уже задеплоен и в whitelist, но capital его не вызывал — теперь вызывает. loan-контракт: — table_loan_debts: добавлено поле project_hash (binary_extension<checksum256> — таблица уже в продакшене, расширение схемы только хвостом) + индекс byuserproj — Loan::create_debt: новая сигнатура с project_hash; createdebt сохраняет привязку к проекту — Loan::count_user_project_debts: хелпер для лимита займов на проект в паевом взносе capital: — debtpaycnfrm: после confirm_paid дублирует запись в loan.debts через inline Loan::create_debt (с due_at из обновлённой capital-записи и project_hash) — settledebt: дополнительно зовёт inline Loan::settle_debt; принимает также OVERDUE (не только PAID) — погашение просроченного через председателя — signact2 (паевой взнос): собирает все PAID/OVERDUE займы пайщика на проекте из capital.debts, проверяет лимит 10 (transaction limit EOSIO), на каждый — inline Loan::settle_debt + capital.Debts::mark_settled. Никакого «боевого взноса» — только паевой по интеллектуальному результату Boot/deploy не требует правок: loan/ уже в configs/contracts.ts и configs/index.ts, аккаунт loan создаётся, контракт деплоится. capital уже в contracts_whitelist в lib/consts. Тесты: добавлен describe «A2 — Централизованный учёт займов» с 6 it.todo по новым сценариям (createdebt в loan, settle через capital, лимит 10, whitelist). Полный перенос таблицы debts из capital в loan (с удалением capital.debts) — отдельная Story 1.12, оставлена на следующую итерацию: текущий PR сохраняет параллельный учёт в обеих таблицах для безопасной миграции прода.
This commit is contained in:
@@ -60,6 +60,32 @@ describe('CAPITAL — расширения «Благорост» (Эпик 1)',
|
||||
)
|
||||
})
|
||||
|
||||
describe('A2 — Централизованный учёт займов (loan-контракт)', () => {
|
||||
it.todo(
|
||||
'Story 1.12: debtpaycnfrm создаёт запись в loan.debts через inline createdebt (с project_hash)'
|
||||
)
|
||||
|
||||
it.todo(
|
||||
'Story 1.12: settledebt в capital удаляет запись из loan.debts через inline settledebt'
|
||||
)
|
||||
|
||||
it.todo(
|
||||
'Story 1.12: signact2 с N≤10 займами на проекте — все погашаются inline-actions в loan'
|
||||
)
|
||||
|
||||
it.todo(
|
||||
'Story 1.12: signact2 с >10 займами на проекте — fail с "Превышен лимит займов на проект для одного паевого взноса"'
|
||||
)
|
||||
|
||||
it.todo(
|
||||
'Story 1.12: loan.createdebt от аккаунта вне contracts_whitelist — fail (только capital/marketplace/...)'
|
||||
)
|
||||
|
||||
it.todo(
|
||||
'Story 1.12: count_user_project_debts возвращает корректное число активных займов пайщика на проекте'
|
||||
)
|
||||
})
|
||||
|
||||
describe('B — Расходы программы', () => {
|
||||
it.todo(
|
||||
'Story 1.4: exppaycnfrm после complete_expense делает Ledger2 PAY_EXPENSE (BLAGOROST_FUND → SOV_EXPENSES)'
|
||||
|
||||
@@ -31,5 +31,12 @@ 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 и др.) — общий механизм погашения и проверки лимитов.
|
||||
// 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,
|
||||
debt_hash, fresh_debt.due_at, exist_debt.amount);
|
||||
};
|
||||
@@ -24,8 +24,9 @@ void capital::settledebt(name coopname, checksum256 debt_hash, eosio::asset amou
|
||||
|
||||
auto exist_debt = Capital::Debts::get_debt_or_fail(coopname, debt_hash);
|
||||
|
||||
eosio::check(exist_debt.status == Capital::Debts::Status::PAID,
|
||||
"Погашение деньгами доступно только для активного займа (статус paid)");
|
||||
eosio::check(exist_debt.status == Capital::Debts::Status::PAID
|
||||
|| exist_debt.status == Capital::Debts::Status::OVERDUE,
|
||||
"Погашение допустимо для активного (paid) или просроченного (overdue) займа");
|
||||
|
||||
eosio::check(amount.is_valid(), "Сумма погашения некорректна");
|
||||
eosio::check(amount.symbol == exist_debt.amount.symbol,
|
||||
@@ -44,6 +45,9 @@ 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.
|
||||
Loan::settle_debt(_capital, coopname, exist_debt.username, debt_hash, amount);
|
||||
|
||||
// event ridge: и должник, и председатель видят факт погашения.
|
||||
require_recipient(exist_debt.username);
|
||||
require_recipient(coopname);
|
||||
|
||||
@@ -61,10 +61,34 @@ void capital::signact2(eosio::name coopname, eosio::name chairman, checksum256 r
|
||||
}
|
||||
|
||||
// Возврат беспроцентного займа пайщика: Dr 80 / Cr 58, TRANSFER LOAN_ISSUED → SHARE_FUND_PAY.
|
||||
// Семантика: пайщик погасил ссуду результатом (интеллектуальным взносом),
|
||||
// Семантика: пайщик погасил ссуду результатом (паевой взнос интеллектуальным результатом),
|
||||
// у кооперативa уменьшилось фин. вложение 58, деньги вернулись на расчётный.
|
||||
if (result -> debt_amount.amount > 0){
|
||||
Ledger2::apply(_capital, coopname, operations::capital::REPAY, result -> debt_amount, result -> username, result_hash, memo);
|
||||
|
||||
// Централизованное погашение конкретных займов в loan-контракте.
|
||||
// Собираем все активные (PAID/OVERDUE) займы пайщика на этом проекте,
|
||||
// лимит 10 — иначе одна транзакция EOSIO с 11+ inline actions не уложится в лимит.
|
||||
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;
|
||||
for (auto it = byproj.find(result->project_hash);
|
||||
it != byproj.end() && it->project_hash == result->project_hash; ++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);
|
||||
}
|
||||
}
|
||||
|
||||
// Обновляем накопительные показатели контрибьютора на основе его ролей в сегменте
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
using namespace eosio;
|
||||
using std::string;
|
||||
|
||||
#define CREATEDEBT_SIGNATURE name coopname, name username, checksum256 debt_hash, time_point_sec repaid_at, asset quantity
|
||||
#define CREATEDEBT_SIGNATURE name coopname, name username, checksum256 project_hash, 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,9 +51,33 @@ 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,
|
||||
debt_hash, repaid_at, quantity);
|
||||
project_hash, debt_hash, repaid_at, quantity);
|
||||
}
|
||||
|
||||
inline void settle_debt(name calling_contract, SETTLEDEBT_SIGNATURE) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <eosio/asset.hpp>
|
||||
#include <eosio/binary_extension.hpp>
|
||||
#include <eosio/crypto.hpp>
|
||||
#include <eosio/eosio.hpp>
|
||||
|
||||
@@ -19,11 +20,25 @@ 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;
|
||||
|
||||
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<
|
||||
@@ -31,7 +46,8 @@ 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<"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>>>
|
||||
debts_index;
|
||||
|
||||
} // namespace Loan
|
||||
|
||||
@@ -3,8 +3,10 @@ using namespace Loan;
|
||||
/**
|
||||
* @brief Создание долгового обязательства.
|
||||
* Создает новое долговое обязательство для пользователя с указанным сроком погашения
|
||||
* и привязкой к проекту вызывающего контракта.
|
||||
* @param coopname Наименование кооператива
|
||||
* @param username Имя пользователя-должника
|
||||
* @param project_hash Хэш проекта/программы (для лимита займов на проект при паевом взносе)
|
||||
* @param debt_hash Хэш долгового обязательства
|
||||
* @param repaid_at Срок погашения долга
|
||||
* @param quantity Сумма долга
|
||||
@@ -13,7 +15,7 @@ using namespace Loan;
|
||||
|
||||
* @note Авторизация требуется от аккаунта: @p contracts_whitelist
|
||||
*/
|
||||
void loan::createdebt(name coopname, name username, checksum256 debt_hash, time_point_sec repaid_at, asset quantity) {
|
||||
void loan::createdebt(name coopname, name username, checksum256 project_hash, 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, "Сумма должна быть положительной");
|
||||
@@ -33,6 +35,7 @@ void loan::createdebt(name coopname, name username, checksum256 debt_hash, time_
|
||||
d.amount = quantity;
|
||||
d.created_at = time_point_sec(current_time_point());
|
||||
d.repaid_at = repaid_at;
|
||||
d.project_hash.emplace(project_hash);
|
||||
});
|
||||
|
||||
Loan::summaries_index summaries(_loan, coopname.value);
|
||||
|
||||
Reference in New Issue
Block a user