[989-1][@ant] feat: внедрить ledger2 с apply и переключить контракты-инициаторы с legacy ledger — нужна двойная запись и единый API именованных операций для миграции остатков и регуляторной отчётности

Made-with: Cursor
This commit is contained in:
Alex Ant
2026-04-17 12:44:41 +05:00
parent a585e78fd9
commit b67d41b00f
50 changed files with 1282 additions and 639 deletions
+5
View File
@@ -70,6 +70,11 @@ export default [
path: path.resolve(process.cwd(), '../contracts/build/contracts/ledger'),
target: 'ledger',
},
{
name: 'ledger2',
path: path.resolve(process.cwd(), '../contracts/build/contracts/ledger2'),
target: 'ledger2',
},
{
name: 'test',
path: path.resolve(process.cwd(), '../contracts/build/contracts/test'),
+4
View File
@@ -95,6 +95,10 @@ export default {
name: 'ledger',
code_permissions_to: ['ledger'],
},
{
name: 'ledger2',
code_permissions_to: ['ledger2'],
},
{
name: 'test',
code_permissions_to: ['test'],
+1 -1
View File
@@ -44,7 +44,7 @@ endmacro()
# Добавляем все контракты
add_contract_build(test)
add_contract_build(ledger)
# add_contract_build(ledger2)
add_contract_build(ledger2)
add_contract_build(meet)
add_contract_build(loan)
add_contract_build(wallet)
@@ -25,9 +25,9 @@ void capital::debtpaycnfrm(name coopname, checksum256 debt_hash) {
// Обновляем статус долга на PAID
Capital::Debts::update_debt_status(coopname, exist_debt.id, Capital::Debts::Status::PAID, _gateway);
// Увеличиваем значение счёта выданных ссуд
// Увеличиваем значение счёта выданных ссуд через ledger2
auto memo = Capital::Memo::get_debt_memo(exist_debt.username);
Ledger::add(_capital, coopname, Ledger::accounts::LONG_TERM_LOANS, exist_debt.amount, memo, debt_hash, exist_debt.username);
Ledger2::apply(_capital, coopname, ledger2_ops::LOAN_REPAYMENT, exist_debt.amount, exist_debt.username, debt_hash, memo);
// Увеличиваем долг contributor (теперь долг активен и должен быть погашен через внесение результата)
Capital::Contributors::increase_debt_amount(coopname, contributor->id, exist_debt.amount);
@@ -64,7 +64,7 @@ void capital::importcontrib(eosio::name coopname, eosio::name username, checksum
Wallet::add_blocked_funds(_capital, coopname, username, contribution_amount, _capital_program, internal_memo);
// Увеличиваем паевой фонд через бухгалтерский учет
Ledger::add(_capital, coopname, Ledger::accounts::SHARE_FUND, contribution_amount, internal_memo, contributor_hash, username);
// Увеличиваем паевой фонд через ledger2 (двойные проводки + движение кошелька)
Ledger2::apply(_capital, coopname, ledger2_ops::CAPITAL_IMPORT, contribution_amount, username, contributor_hash, internal_memo);
}
@@ -47,8 +47,8 @@ void capital::act2pgprp(eosio::name coopname, eosio::name username, checksum256
// Начисляем заблокированные средства в кошелек программы благороста
Wallet::add_blocked_funds(_capital, coopname, property.username, property.property_amount, _capital_program, memo);
// Увеличиваем паевой фонд через бухгалтерский учет
Ledger::add(_capital, coopname, Ledger::accounts::SHARE_FUND, property.property_amount, memo, property_hash, property.username);
// Увеличиваем паевой фонд через ledger2 (ISSUE в SHARE_FUND)
Ledger2::apply(_capital, coopname, ledger2_ops::ACT2_PROGRAM_PROP, property.property_amount, property.username, property_hash, memo);
// Удаляем предложение после успешной обработки
Capital::ProgramProperties::delete_program_property(coopname, property.id);
@@ -48,14 +48,14 @@ void capital::signact2(eosio::name coopname, eosio::name chairman, checksum256 r
// Начисляем заблокированные средства в кошелек программы генерации
if (segment.available_for_program.amount > 0) {
Wallet::add_blocked_funds(_capital, coopname, result -> username, segment.available_for_program, _source_program, memo);
// Увеличиваем паевой фонд за вычетом ссуммы долга
Ledger::add(_capital, coopname, Ledger::accounts::SHARE_FUND, segment.available_for_program, memo, result_hash, result -> username);
// Увеличиваем паевой фонд за счёт результата участника через ledger2 (ISSUE в SHARE_FUND)
Ledger2::apply(_capital, coopname, ledger2_ops::ACT2_SHARE, segment.available_for_program, result -> username, result_hash, memo);
}
// Уменьшаем сумму выданных ссуд кооператива
// Уменьшаем сумму выданных ссуд кооператива (TRANSFER LONG_TERM_LOANS → DEBT_CLOSED_SINK)
if (result -> debt_amount.amount > 0){
Ledger::sub(_capital, coopname, Ledger::accounts::LONG_TERM_LOANS, result -> debt_amount, memo, result_hash, result -> username);
Ledger2::apply(_capital, coopname, ledger2_ops::ACT2_LOAN, result -> debt_amount, result -> username, result_hash, memo);
}
// Обновляем накопительные показатели контрибьютора на основе его ролей в сегменте
@@ -1,14 +1,12 @@
cmake_minimum_required(VERSION 3.5)
# Проект
project(ledger)
project(ledger2)
find_package(cdt)
# Добавление контракта
add_contract(ledger ledger ledger.cpp)
add_contract(ledger2 ledger2 ledger2.cpp)
if(IS_TESTNET)
target_compile_definitions(ledger PUBLIC IS_TESTNET=1)
target_compile_definitions(ledger2 PUBLIC IS_TESTNET=1)
else()
# Не определяем IS_TESTNET для продакшн режима
endif()
@@ -1,16 +0,0 @@
#include "ledger.hpp"
#include <ctime>
#include <eosio/transaction.hpp>
#include "src/common/add.cpp"
#include "src/common/sub.cpp"
#include "src/common/block.cpp"
#include "src/common/unblock.cpp"
#include "src/common/writeoff.cpp"
#include "src/common/writeoffcnsl.cpp"
#include "src/writeoff/create.cpp"
#include "src/writeoff/auth.cpp"
#include "src/writeoff/complete.cpp"
#include "src/writeoff/decline.cpp"
#include "src/migrate/migrate.cpp"
@@ -1,76 +0,0 @@
#include <eosio/asset.hpp>
#include <eosio/contract.hpp>
#include <eosio/crypto.hpp>
#include <eosio/eosio.hpp>
#include <eosio/multi_index.hpp>
#include <eosio/system.hpp>
#include <eosio/time.hpp>
#include "../lib/index.hpp"
using namespace eosio;
/**
\defgroup public_ledger Контракт LEDGER
* Смарт-контракт бухгалтерской книги кооператива предназначен для управления счетами согласно стандартам кооперативной отчетности.
*/
/**
\defgroup public_ledger_processes Процессы
\ingroup public_ledger
*/
/**
\defgroup public_ledger_actions Действия
\ingroup public_ledger
*/
/**
\defgroup public_ledger_tables Таблицы
\ingroup public_ledger
*/
/**
\defgroup public_ledger_consts Константы
\ingroup public_ledger
*/
/**
* @ingroup public_contracts
* @brief Контракт ledger для управления бухгалтерской книгой кооператива
* @details Обеспечивает ведение счетов согласно стандартам кооперативной отчетности
* Счета создаются автоматически при первом пополнении и удаляются при обнулении всех балансов
*/
class [[eosio::contract(LEDGER)]] ledger : public eosio::contract {
public:
ledger(eosio::name receiver, eosio::name code,
eosio::datastream<const char *> ds)
: eosio::contract(receiver, code, ds) {}
void apply(uint64_t receiver, uint64_t code, uint64_t action);
[[eosio::action]] void migrate();
// Операции со счетами (common процесс)
[[eosio::action]] void add(eosio::name coopname, uint64_t account_id, eosio::asset quantity, std::string comment, checksum256 hash, eosio::name username);
[[eosio::action]] void sub(eosio::name coopname, uint64_t account_id, eosio::asset quantity, std::string comment, checksum256 hash, eosio::name username);
// Операции блокировки/разблокировки средств
[[eosio::action]] void block(eosio::name coopname, uint64_t account_id, eosio::asset quantity, std::string comment, checksum256 hash, eosio::name username);
[[eosio::action]] void unblock(eosio::name coopname, uint64_t account_id, eosio::asset quantity, std::string comment, checksum256 hash, eosio::name username);
// Атомарная операция списания
[[eosio::action]] void writeoff(eosio::name coopname, uint64_t account_id, eosio::asset quantity, std::string comment, checksum256 hash, eosio::name username);
// Атомарная операция отмены списания
[[eosio::action]] void writeoffcnsl(eosio::name coopname, uint64_t account_id, eosio::asset quantity, std::string comment, checksum256 hash, eosio::name username);
// Операции процесса списания через совет (writeoff процесс)
[[eosio::action]] void create(eosio::name coopname, eosio::name username, uint64_t account_id, eosio::asset quantity, std::string reason, document2 document, checksum256 writeoff_hash);
[[eosio::action]] void auth(eosio::name coopname, checksum256 writeoff_hash);
// Коллбэки от gateway
[[eosio::action]] void complete(eosio::name coopname, checksum256 writeoff_hash);
[[eosio::action]] void decline(eosio::name coopname, checksum256 writeoff_hash, std::string reason);
};
@@ -0,0 +1,6 @@
#include "ledger2.hpp"
#include <eosio/transaction.hpp>
#include "src/common/apply.cpp"
#include "src/migrate/migrate.cpp"
@@ -0,0 +1,74 @@
#pragma once
#include <eosio/asset.hpp>
#include <eosio/contract.hpp>
#include <eosio/crypto.hpp>
#include <eosio/eosio.hpp>
#include <eosio/multi_index.hpp>
#include <eosio/system.hpp>
#include <eosio/time.hpp>
#include "../lib/index.hpp"
#include "../lib/core/ledger2/accounts.hpp"
#include "../lib/core/ledger2/actions.hpp"
#include "../lib/core/ledger2/wallets.hpp"
using namespace eosio;
/**
\defgroup public_ledger2 Контракт LEDGER2
\ingroup public_contracts
\brief Двухконтурный учёт: управленческие кошельки + двойная бухгалтерия.
Контракт ledger2 реализует единый интерфейс `apply(action_code, ...)` для
всех финансовых движений кооператива. Учёт ведётся на двух уровнях:
• Уровень кошельков (wallets) + журнал wjournal — атомарные операции
ISSUE / TRANSFER / BLOCK / UNBLOCK.
• Уровень счетов (accounts) + журнал journal — парные проводки Dr/Cr,
сальдо учитывает тип счёта (активный/пассивный/активно-пассивный).
На каждый именованный action_code атомарно совершается ровно одно движение
на уровне кошельков (одна запись в wjournal) и ровно одна парная проводка
на уровне счетов (одна запись в journal). Записи двух журналов перекрёстно
линкованы через journal.wjournal_entry_id ↔ wjournal.journal_entry_id.
Внешних add/sub/writeoff нет — всё движение средств описывается через
ACTION_REGISTRY.
*/
/** \defgroup public_ledger2_actions Действия ledger2 \ingroup public_ledger2 */
/** \defgroup public_ledger2_tables Таблицы ledger2 \ingroup public_ledger2 */
/** \defgroup public_ledger2_consts Константы ledger2 \ingroup public_ledger2 */
/**
* @ingroup public_contracts
* @brief Контракт ledger2.
* @details Единственное внешнее финансовое action — apply(). Миграция с
* ledger — одноразовое migrate(). Внешних add/sub/writeoff нет:
* всё движение средств описывается через ACTION_REGISTRY.
*/
class [[eosio::contract(LEDGER2)]] ledger2 : public eosio::contract {
public:
ledger2(eosio::name receiver, eosio::name code,
eosio::datastream<const char*> ds)
: eosio::contract(receiver, code, ds) {}
/**
* @brief Единая точка входа финансовых движений ledger2.
* @ingroup public_ledger2_actions
*/
[[eosio::action]] void apply(eosio::name coopname,
eosio::name initiator,
eosio::name action_code,
eosio::asset amount,
eosio::name username,
eosio::checksum256 document_hash,
std::string memo);
/**
* @brief Одноразовая миграция остатков с ledger.
* @ingroup public_ledger2_actions
*/
[[eosio::action]] void migrate();
};
@@ -1,49 +0,0 @@
/**
* @brief Пополнение счета (дебетовая операция).
* Увеличивает доступные средства на указанном счете. Счет создается автоматически при первом пополнении.
* @param coopname Наименование кооператива
* @param account_id ID счета для пополнения
* @param quantity Сумма для пополнения
* @param comment Комментарий к операции
* @ingroup public_actions
* @ingroup public_ledger_actions
* @note Авторизация требуется от аккаунта: @p coopname
*/
[[eosio::action]]
void ledger::add(eosio::name coopname, uint64_t account_id, eosio::asset quantity, std::string comment, checksum256 hash, eosio::name username) {
require_recipient(coopname);
eosio::name payer = coopname;
if (!has_auth(coopname)) {
payer = check_auth_and_get_payer_or_fail(contracts_whitelist);
}
eosio::check(quantity.is_valid(), "Некорректная сумма");
eosio::check(quantity.amount > 0, "Сумма должна быть положительной");
eosio::check(quantity.symbol == _root_govern_symbol, "Некорректный символ валюты");
laccounts_index accounts(_ledger, coopname.value);
auto account_iter = accounts.find(account_id);
if (account_iter == accounts.end()) {
// Счет не существует - создаем автоматически, получая имя из ACCOUNT_MAP
std::string account_name = Ledger::get_account_name_by_id(account_id);
eosio::check(account_name != "Неизвестный счет", "Счет с таким ID не предусмотрен в плане счетов");
// Создаем новый счет
accounts.emplace(payer, [&](auto& acc) {
acc.id = account_id;
acc.name = account_name;
acc.available = quantity;
acc.blocked = eosio::asset(0, _root_govern_symbol);
acc.writeoff = eosio::asset(0, _root_govern_symbol);
});
} else {
// Счет существует - пополняем available
accounts.modify(account_iter, payer, [&](auto& acc) {
acc.available += quantity;
});
}
}
@@ -0,0 +1,194 @@
/**
* @brief Единая точка входа ledger2 для всех финансовых движений.
*
* Два уровня учёта внутри одного вызова apply:
* 1) Операционный уровень — кошельки (wallets) + журнал wjournal.
* Выполняется атомарная операция ISSUE / TRANSFER / BLOCK / UNBLOCK
* и её факт фиксируется ровно одной записью в wjournal.
* 2) Бухгалтерский уровень — счета (accounts) + журнал journal.
* Выполняется ровно одна парная проводка Dr/Cr, дебет/кредит
* начисляется с учётом активности/пассивности счетов
* (AccountType определяется планом счетов LEDGER2_ACCOUNT_MAP).
*
* Оба уровня применяются в одной транзакции. Записи в journal и wjournal
* перекрёстно линкованы: journal.wjournal_entry_id ↔ wjournal.journal_entry_id.
*
* Flow:
* 1. auth: coopname или контракт из whitelist
* 2. lookup ACTION_REGISTRY по action_code
* 3. wallet op: изменение кошельков + emplace в wjournal
* 4. accounts: upsert Dr/Cr (фиксируем AccountType при первой проводке)
* 5. journal: emplace пары проводок, линк на wjournal_entry_id
* 6. backfill wjournal.journal_entry_id
*
* @ingroup public_ledger2_actions
*/
[[eosio::action]]
void ledger2::apply(eosio::name coopname,
eosio::name initiator,
eosio::name action_code,
eosio::asset amount,
eosio::name username,
eosio::checksum256 document_hash,
std::string memo) {
require_recipient(coopname);
// -------- auth --------
eosio::name payer = coopname;
if (!has_auth(coopname)) {
payer = check_auth_and_get_payer_or_fail(contracts_whitelist);
}
// -------- validate amount --------
eosio::check(amount.is_valid(), "Некорректная сумма");
eosio::check(amount.amount > 0, "Сумма должна быть положительной");
eosio::check(amount.symbol == _root_govern_symbol,
"Некорректный символ валюты для операций ledger2");
// -------- lookup registry --------
const ActionRegistryEntry* entry = ledger2_find_action(action_code);
eosio::check(entry != nullptr,
std::string{"Unknown action code: "} + action_code.to_string());
// =====================================================================
// Уровень 1 — кошельки + журнал wjournal (wallet-level)
// =====================================================================
wallets2_index wallets(get_self(), coopname.value);
auto upsert_wallet = [&](uint64_t wallet_id) -> wallets2_index::const_iterator {
auto it = wallets.find(wallet_id);
if (it == wallets.end()) {
std::string name = ledger2_get_wallet_name_by_id(wallet_id);
eosio::check(!name.empty(),
std::string{"Unknown wallet id: "} + std::to_string(wallet_id));
it = wallets.emplace(payer, [&](auto& w) {
w.id = wallet_id;
w.name = name;
w.available = eosio::asset(0, amount.symbol);
w.blocked = eosio::asset(0, amount.symbol);
});
}
return it;
};
auto cleanup_if_empty = [&](uint64_t wallet_id) {
auto it = wallets.find(wallet_id);
if (it != wallets.end() && it->is_empty()) {
wallets.erase(it);
}
};
switch (entry->wallet_op) {
case WalletOp::ISSUE: {
eosio::check(entry->wallet_to != 0, "ISSUE requires wallet_to");
auto it = upsert_wallet(entry->wallet_to);
wallets.modify(it, payer, [&](auto& w) { w.available += amount; });
break;
}
case WalletOp::TRANSFER: {
eosio::check(entry->wallet_from != 0 && entry->wallet_to != 0,
"TRANSFER requires wallet_from and wallet_to");
auto from_it = wallets.find(entry->wallet_from);
eosio::check(from_it != wallets.end() && from_it->available >= amount,
std::string{"Insufficient wallet balance: "} +
std::to_string(entry->wallet_from));
wallets.modify(from_it, payer, [&](auto& w) { w.available -= amount; });
auto to_it = upsert_wallet(entry->wallet_to);
wallets.modify(to_it, payer, [&](auto& w) { w.available += amount; });
cleanup_if_empty(entry->wallet_from);
break;
}
case WalletOp::BLOCK: {
eosio::check(entry->wallet_from != 0, "BLOCK requires wallet_from");
auto it = wallets.find(entry->wallet_from);
eosio::check(it != wallets.end() && it->available >= amount,
std::string{"Insufficient wallet balance for BLOCK: "} +
std::to_string(entry->wallet_from));
wallets.modify(it, payer, [&](auto& w) {
w.available -= amount;
w.blocked += amount;
});
break;
}
case WalletOp::UNBLOCK: {
eosio::check(entry->wallet_from != 0, "UNBLOCK requires wallet_from");
auto it = wallets.find(entry->wallet_from);
eosio::check(it != wallets.end() && it->blocked >= amount,
std::string{"Insufficient blocked balance for UNBLOCK: "} +
std::to_string(entry->wallet_from));
wallets.modify(it, payer, [&](auto& w) {
w.blocked -= amount;
w.available += amount;
});
break;
}
}
// Фиксация атомарной операции кошелька в журнале wjournal.
wjournal_index wjournal(get_self(), coopname.value);
uint64_t wjournal_id = wjournal.available_primary_key();
const auto now = eosio::current_time_point();
wjournal.emplace(payer, [&](auto& w) {
w.id = wjournal_id;
w.wallet_op = static_cast<uint8_t>(entry->wallet_op);
w.wallet_from = entry->wallet_from;
w.wallet_to = entry->wallet_to;
w.amount = amount;
w.action_code = action_code;
w.journal_entry_id = 0; // backfill после записи в journal
w.username = username;
w.memo = memo;
w.document_hash = document_hash;
w.created_at = now;
});
// =====================================================================
// Уровень 2 — счета + журнал journal (account-level, двойная запись)
// =====================================================================
accounts2_index accounts(get_self(), coopname.value);
auto upsert_account = [&](uint64_t account_id) -> accounts2_index::const_iterator {
auto it = accounts.find(account_id);
if (it == accounts.end()) {
const auto* meta = ledger2_find_account_meta(account_id);
eosio::check(meta != nullptr,
std::string{"Unknown account id: "} + std::to_string(account_id));
it = accounts.emplace(payer, [&](auto& a) {
a.id = account_id;
a.name = meta->name;
a.account_type = static_cast<uint8_t>(meta->type);
a.debit_balance = eosio::asset(0, amount.symbol);
a.credit_balance = eosio::asset(0, amount.symbol);
});
}
return it;
};
// Дебетуемый счёт: debit_balance += amount (обороты).
// Рост/падение сальдо определится в get_balance() по account_type.
auto dr_it = upsert_account(entry->debit_account_id);
accounts.modify(dr_it, payer, [&](auto& a) { a.debit_balance += amount; });
// Кредитуемый счёт: credit_balance += amount (обороты).
auto cr_it = upsert_account(entry->credit_account_id);
accounts.modify(cr_it, payer, [&](auto& a) { a.credit_balance += amount; });
// Парная проводка в journal.
journal_index journal(get_self(), coopname.value);
uint64_t journal_id = journal.available_primary_key();
journal.emplace(payer, [&](auto& e) {
e.id = journal_id;
e.debit_account_id = entry->debit_account_id;
e.credit_account_id = entry->credit_account_id;
e.amount = amount;
e.action_code = action_code;
e.wjournal_entry_id = wjournal_id;
e.operation_hash = document_hash;
e.memo = memo;
e.created_at = now;
});
// Backfill: проставляем id проводки в запись wjournal.
auto w_it = wjournal.find(wjournal_id);
wjournal.modify(w_it, payer, [&](auto& w) { w.journal_entry_id = journal_id; });
}
@@ -1,38 +0,0 @@
/**
* @brief Блокировка средств на счете.
* Переводит средства из доступных в заблокированные
* @param coopname Наименование кооператива
* @param account_id ID счета для блокировки средств
* @param quantity Сумма для блокировки
* @param comment Комментарий к операции
* @ingroup public_actions
* @ingroup public_ledger_actions
* @note Авторизация требуется от аккаунта: @p coopname
*/
[[eosio::action]]
void ledger::block(eosio::name coopname, uint64_t account_id, eosio::asset quantity, std::string comment, checksum256 hash, eosio::name username) {
require_recipient(coopname);
eosio::name payer = coopname;
if (!has_auth(coopname)) {
payer = check_auth_and_get_payer_or_fail(contracts_whitelist);
}
eosio::check(quantity.is_valid(), "Некорректная сумма");
eosio::check(quantity.amount > 0, "Сумма должна быть положительной");
eosio::check(quantity.symbol == _root_govern_symbol, "Некорректный символ валюты");
laccounts_index accounts(_ledger, coopname.value);
auto account_iter = accounts.find(account_id);
eosio::check(account_iter != accounts.end(), "Счет не найден");
// Проверяем достаточность доступных средств
eosio::check(account_iter->available >= quantity, "Недостаточно доступных средств для блокировки");
// Переносим средства из available в blocked
accounts.modify(account_iter, payer, [&](auto& acc) {
acc.available -= quantity;
acc.blocked += quantity;
});
}
@@ -1,44 +0,0 @@
/**
* @brief Списание со счета (кредитовая операция).
* Уменьшает доступные средства на указанном счете. Счет удаляется при обнулении всех балансов.
* @param coopname Наименование кооператива
* @param account_id ID счета для списания
* @param quantity Сумма для списания
* @param comment Комментарий к операции
* @ingroup public_actions
* @ingroup public_ledger_actions
* @note Авторизация требуется от аккаунта: @p coopname
*/
[[eosio::action]]
void ledger::sub(eosio::name coopname, uint64_t account_id, eosio::asset quantity, std::string comment, checksum256 hash, eosio::name username) {
require_recipient(coopname);
eosio::name payer = coopname;
if (!has_auth(coopname)) {
payer = check_auth_and_get_payer_or_fail(contracts_whitelist);
}
eosio::check(quantity.is_valid(), "Некорректная сумма");
eosio::check(quantity.amount > 0, "Сумма должна быть положительной");
eosio::check(quantity.symbol == _root_govern_symbol, "Некорректный символ валюты");
laccounts_index accounts(_ledger, coopname.value);
auto account_iter = accounts.find(account_id);
eosio::check(account_iter != accounts.end(), "Счет не найден");
// Проверяем достаточность доступных средств
eosio::check(account_iter->available >= quantity, "Недостаточно доступных средств для списания");
// Обновляем баланс счета
accounts.modify(account_iter, payer, [&](auto& acc) {
acc.available -= quantity;
});
// Проверяем и удаляем счет если он полностью пуст
if (account_iter->is_empty()) {
accounts.erase(account_iter);
}
}
@@ -1,38 +0,0 @@
/**
* @brief Разблокировка средств на счете.
* Переводит средства из заблокированных в доступные
* @param coopname Наименование кооператива
* @param account_id ID счета для разблокировки средств
* @param quantity Сумма для разблокировки
* @param comment Комментарий к операции
* @ingroup public_actions
* @ingroup public_ledger_actions
* @note Авторизация требуется от аккаунта: @p coopname
*/
[[eosio::action]]
void ledger::unblock(eosio::name coopname, uint64_t account_id, eosio::asset quantity, std::string comment, checksum256 hash, eosio::name username) {
require_recipient(coopname);
eosio::name payer = coopname;
if (!has_auth(coopname)) {
payer = check_auth_and_get_payer_or_fail(contracts_whitelist);
}
eosio::check(quantity.is_valid(), "Некорректная сумма");
eosio::check(quantity.amount > 0, "Сумма должна быть положительной");
eosio::check(quantity.symbol == _root_govern_symbol, "Некорректный символ валюты");
laccounts_index accounts(_ledger, coopname.value);
auto account_iter = accounts.find(account_id);
eosio::check(account_iter != accounts.end(), "Счет не найден");
// Проверяем достаточность заблокированных средств
eosio::check(account_iter->blocked >= quantity, "Недостаточно заблокированных средств для разблокировки");
// Переносим средства из blocked в available
accounts.modify(account_iter, payer, [&](auto& acc) {
acc.blocked -= quantity;
acc.available += quantity;
});
}
@@ -1,38 +0,0 @@
/**
* @brief Атомарное списание средств со счета.
* Списывает средства в категорию списанных без возможности возврата
* @param coopname Наименование кооператива
* @param account_id ID счета для списания
* @param quantity Сумма для списания
* @param comment Комментарий к операции
* @ingroup public_actions
* @ingroup public_ledger_actions
* @note Авторизация требуется от аккаунта: @p coopname
*/
[[eosio::action]]
void ledger::writeoff(eosio::name coopname, uint64_t account_id, eosio::asset quantity, std::string comment, checksum256 hash, eosio::name username) {
require_recipient(coopname);
eosio::name payer = coopname;
if (!has_auth(coopname)) {
payer = check_auth_and_get_payer_or_fail(contracts_whitelist);
}
eosio::check(quantity.is_valid(), "Некорректная сумма");
eosio::check(quantity.amount > 0, "Сумма должна быть положительной");
eosio::check(quantity.symbol == _root_govern_symbol, "Некорректный символ валюты");
laccounts_index accounts(_ledger, coopname.value);
auto account_iter = accounts.find(account_id);
eosio::check(account_iter != accounts.end(), "Счет не найден");
// Проверяем достаточность заблокированных средств
eosio::check(account_iter->blocked >= quantity, "Недостаточно заблокированных средств для списания");
// Атомарно переводим средства из blocked в writeoff
accounts.modify(account_iter, payer, [&](auto& acc) {
acc.blocked -= quantity;
acc.writeoff += quantity;
});
}
@@ -1,38 +0,0 @@
/**
* @brief Атомарная отмена списания средств
* Возвращает средства из категории списанных в доступные
* @param coopname Наименование кооператива
* @param account_id ID счета для отмены списания
* @param quantity Сумма для отмены списания
* @param comment Комментарий к операции
* @ingroup public_actions
* @ingroup public_ledger_actions
* @note Авторизация требуется от аккаунта: @p coopname
*/
[[eosio::action]]
void ledger::writeoffcnsl(eosio::name coopname, uint64_t account_id, eosio::asset quantity, std::string comment, checksum256 hash, eosio::name username) {
eosio::name payer = coopname;
if (!has_auth(coopname)) {
payer = check_auth_and_get_payer_or_fail(contracts_whitelist);
}
eosio::check(quantity.is_valid(), "Некорректная сумма");
eosio::check(quantity.amount > 0, "Сумма должна быть положительной");
eosio::check(quantity.symbol == _root_govern_symbol, "Некорректный символ валюты");
laccounts_index accounts(_ledger, coopname.value);
auto account_iter = accounts.find(account_id);
eosio::check(account_iter != accounts.end(), "Счет не найден");
// Проверяем достаточность списанных средств
eosio::check(account_iter->writeoff >= quantity, "Недостаточно списанных средств для отмены списания");
// Атомарно переводим средства из writeoff в blocked
accounts.modify(account_iter, payer, [&](auto& acc) {
acc.writeoff -= quantity;
acc.blocked += quantity;
});
}
@@ -1,32 +0,0 @@
/**
* @brief Инициализация всех счетов кооператива согласно плану счетов
* @param coopname - имя кооператива
*/
[[eosio::action]]
void ledger::init(eosio::name coopname) {
require_auth(coopname);
// Проверяем что кооператив существует
cooperatives2_index coops(_registrator, _registrator.value);
auto coop_iter = coops.find(coopname.value);
eosio::check(coop_iter != coops.end(), "Кооператив не найден");
laccounts_index accounts(_ledger, coopname.value);
// Проверяем что счета еще не инициализированы
auto existing = accounts.begin();
eosio::check(existing == accounts.end(), "Счета уже инициализированы для данного кооператива");
// Инициализируем все счета из карты с нулевыми значениями
for (const auto& account_data : ACCOUNT_MAP) {
uint64_t id = std::get<0>(account_data);
std::string name = std::get<1>(account_data);
accounts.emplace(coopname, [&](auto& acc) {
acc.id = id;
acc.name = name;
acc.allocation = eosio::asset(0, _root_govern_symbol);
acc.writeoff = eosio::asset(0, _root_govern_symbol);
});
}
}
@@ -1,59 +1,73 @@
/**
* @brief Миграция данных контракта
* Инициализирует расчетный счет по сумме паевых и вступительных взносов
* @ingroup public_actions
* @ingroup public_ledger_actions
* @note Авторизация требуется от аккаунта: @p ledger
* @brief Одноразовая миграция остатков с ledger на ledger2.
*
* Перебирает все кооперативы из registrator::coops и для каждого:
* - читает таблицу ledger::accounts (laccount: id, name, available, blocked, writeoff);
* - для каждой записи с available+blocked > 0 создаёт запись в ledger2::wallets
* с id = старый account id, name = старое имя. Поле writeoff игнорируется.
*
* Накопительные таблицы journal / operations / accounts (двойные проводки)
* при миграции НЕ заполняются — они нарастают только от новых apply-вызовов.
*
* Защита от повторного запуска: таблица meta (scope = _ledger2) с флагом migrated.
*
* @ingroup public_ledger2_actions
*/
void ledger::migrate(){
require_auth(_ledger);
void ledger2::migrate() {
require_auth(get_self());
ledger2_meta_index meta_tbl(get_self(), get_self().value);
auto meta_it = meta_tbl.find(0);
eosio::check(meta_it == meta_tbl.end() || !meta_it->migrated,
"ledger2: already migrated");
// Получаем все кооперативы
cooperatives2_index coops(_registrator, _registrator.value);
uint64_t processed_coops = 0;
for (auto coop_iter = coops.begin(); coop_iter != coops.end(); ++coop_iter) {
eosio::name coopname = coop_iter->username;
for (auto coop_it = coops.begin(); coop_it != coops.end(); ++coop_it) {
eosio::name coopname = coop_it->username;
// Работаем со счетами кооператива
laccounts_index accounts(_ledger, coopname.value);
// Старые счета ledger хранятся в multi_index<"accounts", laccount> со scope = coopname
laccounts_index old_accounts(_ledger, coopname.value);
wallets2_index new_wallets(get_self(), coopname.value);
// Получаем текущий баланс расчетного счета
auto bank_account = accounts.find(Ledger::accounts::BANK_ACCOUNT);
eosio::asset current_bank_balance = eosio::asset(0, _root_govern_symbol);
for (auto acc_it = old_accounts.begin(); acc_it != old_accounts.end(); ++acc_it) {
eosio::asset total_balance = acc_it->available + acc_it->blocked;
if (total_balance.amount == 0) continue; // только ненулевые; writeoff игнорируем
if (bank_account != accounts.end()) {
current_bank_balance = bank_account->available;
}
// Если расчетный счет пустой, пересчитываем его
if (current_bank_balance.amount == 0) {
// Получаем баланс паевого фонда
auto share_fund = accounts.find(Ledger::accounts::SHARE_FUND);
eosio::asset share_fund_balance = eosio::asset(0, _root_govern_symbol);
if (share_fund != accounts.end()) {
share_fund_balance = share_fund->available;
}
// Получаем баланс вступительных взносов
auto entrance_fees = accounts.find(Ledger::accounts::ENTRANCE_FEES);
eosio::asset entrance_fees_balance = eosio::asset(0, _root_govern_symbol);
if (entrance_fees != accounts.end()) {
entrance_fees_balance = entrance_fees->available;
}
// Новая сумма для расчетного счета
eosio::asset new_bank_balance = share_fund_balance + entrance_fees_balance;
if (new_bank_balance.amount > 0) {
checksum256 empty_hash;
memset(&empty_hash, 0, sizeof(empty_hash));
Ledger::add(_ledger, coopname, Ledger::accounts::BANK_ACCOUNT,
new_bank_balance,
"Миграция: инициализация расчетного счета по сумме паевых и вступительных взносов",
empty_hash, coopname);
// Создаём/обновляем кошелёк с тем же id (простая совместимость)
auto wallet_it = new_wallets.find(acc_it->id);
if (wallet_it == new_wallets.end()) {
new_wallets.emplace(get_self(), [&](auto& w) {
w.id = acc_it->id;
w.name = acc_it->name;
w.available = acc_it->available;
w.blocked = acc_it->blocked;
});
} else {
new_wallets.modify(wallet_it, get_self(), [&](auto& w) {
w.available = acc_it->available;
w.blocked = acc_it->blocked;
});
}
}
++processed_coops;
}
}
// Ставим флаг миграции
if (meta_it == meta_tbl.end()) {
meta_tbl.emplace(get_self(), [&](auto& m) {
m.id = 0;
m.migrated = true;
m.migrated_coops = processed_coops;
m.migrated_at = eosio::current_time_point();
});
} else {
meta_tbl.modify(meta_it, get_self(), [&](auto& m) {
m.migrated = true;
m.migrated_coops = processed_coops;
m.migrated_at = eosio::current_time_point();
});
}
}
@@ -1,42 +0,0 @@
/**
* @brief Авторизация списания средств советом
* Подтверждает операцию списания после одобрения советом
* @param coopname Наименование кооператива
* @param writeoff_hash Хэш операции списания для авторизации
* @ingroup public_actions
* @ingroup public_ledger_actions
* @note Авторизация требуется от аккаунта: @p soviet
*/
[[eosio::action]]
void ledger::auth(eosio::name coopname, checksum256 writeoff_hash) {
require_auth(_soviet);
auto writeoff_opt = Ledger::get_writeoff_by_hash(writeoff_hash);
eosio::check(writeoff_opt.has_value(), "Операция не найдена");
auto writeoff = writeoff_opt.value();
eosio::check(writeoff.coopname == coopname, "Неверный кооператив");
eosio::check(writeoff.status == "pending"_n, "Операция уже обработана");
// Обновляем статус операции
writeoffs_index writeoffs(_ledger, _ledger.value);
auto writeoff_iter = writeoffs.find(writeoff.id);
writeoffs.modify(writeoff_iter, _soviet, [&](auto& w) {
w.status = "approved"_n;
});
// Отправляем запрос на создание исходящего платежа в gateway через интерфейс
Action::send<createoutpay_interface>(
_gateway,
"createoutpay"_n,
_ledger,
coopname,
writeoff.username,
writeoff_hash,
writeoff.quantity,
_ledger, // callback_contract
"complete"_n, // confirm_callback
"decline"_n // decline_callback
);
}
@@ -1,38 +0,0 @@
/**
* @brief Завершение операции списания от gateway
* Коллбэк от gateway при успешном завершении операции
* @param coopname Наименование кооператива
* @param writeoff_hash Хэш операции списания для завершения
* @ingroup public_actions
* @ingroup public_ledger_actions
* @note Авторизация требуется от аккаунта: @p gateway
*/
[[eosio::action]]
void ledger::complete(eosio::name coopname, checksum256 writeoff_hash) {
require_auth(_gateway);
auto writeoff_opt = Ledger::get_writeoff_by_hash(writeoff_hash);
eosio::check(writeoff_opt.has_value(), "Операция не найдена");
auto writeoff = writeoff_opt.value();
eosio::check(writeoff.coopname == coopname, "Неверный кооператив");
eosio::check(writeoff.status == "approved"_n, "Операция не одобрена");
// Обновляем статус операции
writeoffs_index writeoffs(_ledger, _ledger.value);
auto writeoff_iter = writeoffs.find(writeoff.id);
writeoffs.modify(writeoff_iter, _gateway, [&](auto& w) {
w.status = "paid"_n;
});
// Списываем средства со счета
laccounts_index accounts(_ledger, coopname.value);
auto account_iter = accounts.find(writeoff.account_id);
eosio::check(account_iter != accounts.end(), "Счет не найден");
accounts.modify(account_iter, _gateway, [&](auto& acc) {
acc.blocked -= writeoff.quantity;
acc.writeoff += writeoff.quantity;
});
}
@@ -1,77 +0,0 @@
/**
*
* @brief Создание заявления на списание средств через совет.
* Создает операцию списания, требующую одобрения советом
* @param coopname Наименование кооператива
* @param username Имя пользователя-инициатора списания
* @param account_id ID счета для списания
* @param quantity Сумма для списания
* @param reason Обоснование списания
* @param document Документ обоснования
* @param writeoff_hash Хэш операции списания
* @ingroup public_actions
* @ingroup public_ledger_actions
* @note Авторизация требуется от аккаунта: @p username
*/
[[eosio::action]]
void ledger::create(eosio::name coopname, eosio::name username, uint64_t account_id, eosio::asset quantity, std::string reason, document2 document, checksum256 writeoff_hash) {
require_auth(coopname);
eosio::check(quantity.is_valid(), "Некорректная сумма");
eosio::check(quantity.amount > 0, "Сумма должна быть положительной");
eosio::check(quantity.symbol == _root_govern_symbol, "Некорректный символ валюты");
eosio::check(reason.length() > 0, "Обоснование должно быть от 1 символа");
// Проверяем что кооператив существует
cooperatives2_index coops(_registrator, _registrator.value);
auto coop_iter = coops.find(coopname.value);
eosio::check(coop_iter != coops.end(), "Кооператив не найден");
// Проверяем что счет существует
laccounts_index accounts(_ledger, coopname.value);
auto account_iter = accounts.find(account_id);
eosio::check(account_iter != accounts.end(), "Счет не найден");
// Проверяем достаточность средств
eosio::check(account_iter -> available >= quantity, "Недостаточно средств на счету");
// Проверяем уникальность writeoff_hash
auto existing_writeoff = Ledger::get_writeoff_by_hash(writeoff_hash);
eosio::check(!existing_writeoff.has_value(), "Операция с таким хэшем уже существует");
// Создаем операцию ожидающую решения совета
auto operation_id = get_global_id_in_scope(_ledger, _ledger, "writeoffs"_n);
writeoffs_index writeoffs(_ledger, _ledger.value);
writeoffs.emplace(username, [&](auto& op) {
op.id = operation_id;
op.coopname = coopname;
op.username = username;
op.account_id = account_id;
op.quantity = quantity;
op.reason = reason;
op.document = document;
op.writeoff_hash = writeoff_hash;
op.status = "pending"_n;
});
// Создаем агенду в совете через Action::send
::Soviet::create_agenda(
_ledger,
coopname,
username,
get_valid_soviet_action("ledgerwthd"_n), // тип решения
writeoff_hash,
_ledger, // callback_contract
Names::Ledger::AUTHORIZE_WRITEOFF, // confirm_callback
Names::Ledger::DECLINE_WRITEOFF, // decline_callback
document,
reason
);
// Блокируем средства на счету
std::string comment = "Служебная записка отправлена в совет для списания средств со счета " + account_iter->name + " на сумму " + quantity.to_string();
Ledger::block(coopname, coopname, account_id, quantity, comment, writeoff_hash, username);
}
@@ -1,27 +0,0 @@
/**
* @brief Отклонение операции списания от gateway
* Коллбэк от gateway при отклонении операции
* @param coopname Наименование кооператива
* @param writeoff_hash Хэш операции списания для отклонения
* @param reason Причина отклонения операции
* @ingroup public_actions
* @ingroup public_ledger_actions
* @note Авторизация требуется от аккаунта: @p gateway или @p soviet
*/
[[eosio::action]]
void ledger::decline(eosio::name coopname, checksum256 writeoff_hash, std::string reason) {
// Может быть вызван от _soviet (отклонение решения) или _gateway (отклонение платежа)
eosio::check(has_auth(_soviet) || has_auth(_gateway), "Недостаточно прав для отклонения операции");
auto writeoff_opt = Ledger::get_writeoff_by_hash(writeoff_hash);
eosio::check(writeoff_opt.has_value(), "Операция не найдена");
auto writeoff = writeoff_opt.value();
eosio::check(writeoff.coopname == coopname, "Неверный кооператив");
// Обновляем статус операции
writeoffs_index writeoffs(_ledger, _ledger.value);
auto writeoff_iter = writeoffs.find(writeoff.id);
writeoffs.erase(writeoff_iter);
}
+3
View File
@@ -138,6 +138,7 @@ static constexpr uint64_t _capital_program_id = 4;
#define LOAN "loan"
#define MEET "meet"
#define LEDGER "ledger"
#define LEDGER2 "ledger2"
/**
@@ -161,6 +162,7 @@ static constexpr uint64_t _capital_program_id = 4;
static constexpr eosio::name _loan = "loan"_n;
static constexpr eosio::name _meet = "meet"_n;
static constexpr eosio::name _ledger = "ledger"_n;
static constexpr eosio::name _ledger2 = "ledger2"_n;
static constexpr eosio::name _power_account = "eosio.power"_n;
static constexpr eosio::name _saving_account = "eosio.saving"_n;
@@ -187,6 +189,7 @@ static constexpr uint64_t _capital_program_id = 4;
"eosio.power"_n,
"eosio.saving"_n,
"ledger"_n,
"ledger2"_n,
"capital"_n
// Добавьте остальные стандартные или пользовательские контракты по необходимости
};
@@ -12,6 +12,7 @@
#include "wallet/wallet.hpp"
#include "gateway/gateway.hpp"
#include "ledger/ledger.hpp"
#include "ledger2/ledger2.hpp"
#include "registrator/registrator.hpp"
#include "loan/loan.hpp"
#include "branch/branch.hpp"
@@ -0,0 +1,188 @@
#pragma once
#include <cstdint>
#include <string>
#include <tuple>
#include <vector>
/**
* @brief Тип бухгалтерского счёта.
*
* Определяет, как рассчитывается сальдо и как растёт счёт при проводке:
* - ACTIVE: остаток — дебетовый. Dr ↑ при поступлении, Cr ↑ при расходе.
* balance = debit_turnover credit_turnover.
* - PASSIVE: остаток — кредитовый. Cr ↑ при начислении, Dr ↑ при погашении.
* balance = credit_turnover debit_turnover.
* - ACTIVE_PASSIVE: остаток может быть любого знака; возвращаем
* debit credit (положительный → активный остаток, отрицательный → пассивный).
*
* @ingroup public_ledger2_consts
*/
enum class AccountType : uint8_t {
ACTIVE = 0,
PASSIVE = 1,
ACTIVE_PASSIVE = 2,
};
/**
* @brief План счетов ledger2 со смещением *1000.
*
* Правило кодирования id: integer(code) * 1000.
* Примеры: 51 → 51000, 80 → 80000, 86 → 86000,
* 86.01 (старый id 861) → 861000,
* 86.02 (старый id 862) → 862000.
*
* @ingroup public_ledger2_consts
*/
struct ledger2_accounts {
// Денежные средства (А)
static constexpr uint64_t CASH = 50 * 1000; ///< 50 — Касса (А)
static constexpr uint64_t BANK_ACCOUNT = 51 * 1000; ///< 51 — Расчётный счёт (А)
// Расчёты с пайщиками (А-П)
static constexpr uint64_t MEMBER_SETTLEMENTS = 75 * 1000;
static constexpr uint64_t MEMBER_DEBT = 751 * 1000;
static constexpr uint64_t INCOME_ACCRUALS = 752 * 1000;
// Паевой фонд (П)
static constexpr uint64_t SHARE_FUND = 80 * 1000; ///< 80 — Паевой фонд (П)
// Расчёты с пайщиками (А-П)
static constexpr uint64_t MEMBER_FEES = 761 * 1000;
static constexpr uint64_t PROPERTY_TRANSFER = 762 * 1000;
static constexpr uint64_t OTHER_SETTLEMENTS = 763 * 1000;
// Активы
static constexpr uint64_t FIXED_ASSETS = 1 * 1000; ///< 01 (А)
static constexpr uint64_t INTANGIBLE_ASSETS = 4 * 1000; ///< 04 (А)
static constexpr uint64_t MATERIALS_GOODS = 10 * 1000; ///< 10 (А)
static constexpr uint64_t MAIN_PRODUCTION = 20 * 1000; ///< 20 (А)
static constexpr uint64_t NON_PROFIT_ACTIVITY = 201 * 1000;
static constexpr uint64_t GENERAL_EXPENSES = 26 * 1000; ///< 26 (А)
static constexpr uint64_t LOANS_ISSUED = 583 * 1000; ///< 58.3 (А)
static constexpr uint64_t LOAN_INTEREST = 911 * 1000;
static constexpr uint64_t FINANCIAL_INVESTMENTS = 58 * 1000; ///< 58 (А)
static constexpr uint64_t SHARES_AND_STAKES = 581 * 1000;
static constexpr uint64_t SECURITIES = 582 * 1000;
static constexpr uint64_t ACCOUNTABLE_PERSONS = 71 * 1000; ///< 71 (А-П)
static constexpr uint64_t DEBTORS_CREDITORS = 76 * 1000; ///< 76 (А-П)
// Пассивы
static constexpr uint64_t RESERVES = 63 * 1000; ///< 63 (П)
static constexpr uint64_t LONG_TERM_LOANS = 67 * 1000; ///< 67 — Расчёты по долгосрочным займам (П)
static constexpr uint64_t TAXES_FEES = 68 * 1000; ///< 68 (П)
static constexpr uint64_t SOCIAL_INSURANCE = 69 * 1000; ///< 69 (П)
static constexpr uint64_t SALARY = 70 * 1000; ///< 70 (П)
// Капитал, прибыль, фонды (П)
static constexpr uint64_t ADDITIONAL_CAPITAL = 83 * 1000;
static constexpr uint64_t FUNDS_PO_1 = 831 * 1000;
static constexpr uint64_t CURRENT_YEAR_PROFIT = 841 * 1000;
static constexpr uint64_t PREVIOUS_YEARS_PROFIT = 842 * 1000;
static constexpr uint64_t FUNDS_PO_2 = 843 * 1000;
static constexpr uint64_t UNDISTRIBUTED_PROFIT = 84 * 1000;
// Целевые поступления (П) — субсчета со смещением *1000
static constexpr uint64_t TARGET_RECEIPTS = 86 * 1000; ///< 86 — Целевые поступления (П, родительский)
static constexpr uint64_t ENTRANCE_FEES = 861 * 1000; ///< 86.01 — Вступительные взносы (П)
static constexpr uint64_t MEMBERSHIP_FEES = 862 * 1000; ///< 86.02 — Членские взносы (П)
static constexpr uint64_t RESERVE_FUND = 863 * 1000; ///< 86.3 — Резервный фонд (П)
static constexpr uint64_t INDIVISIBLE_FUND = 864 * 1000;
static constexpr uint64_t ECONOMIC_ACTIVITY_FUND = 865 * 1000;
static constexpr uint64_t MUTUAL_SECURITY_FUND = 866 * 1000;
static constexpr uint64_t DEVELOPMENT_FUND = 867 * 1000;
static constexpr uint64_t DELEGATE_FEES_FUND = 868 * 1000; ///< 86.8 — Членские взносы делегатов (П)
// Прочие счета
static constexpr uint64_t OTHER_INCOME_EXPENSES = 91 * 1000; ///< 91 (А-П)
static constexpr uint64_t FUTURE_EXPENSES_RESERVE = 96 * 1000; ///< 96 (П)
static constexpr uint64_t FUTURE_INCOME = 98 * 1000; ///< 98 (П)
static constexpr uint64_t FREE_RECEIPT = 981 * 1000; ///< 98.1 (П)
};
/**
* @brief Справочник счёта: id → (имя, тип).
*
* Покрывает все id, используемые в ACTION_REGISTRY. Заполняется при
* первой проводке в соответствующий счёт — справочные значения переносятся
* в запись `accounts` один раз и дальше хранятся там.
*/
struct Ledger2AccountMeta {
uint64_t id;
std::string name;
AccountType type;
};
static const std::vector<Ledger2AccountMeta> LEDGER2_ACCOUNT_MAP = {
{ ledger2_accounts::CASH, "Касса", AccountType::ACTIVE },
{ ledger2_accounts::BANK_ACCOUNT, "Расчётный счёт", AccountType::ACTIVE },
{ ledger2_accounts::FIXED_ASSETS, "Основные средства", AccountType::ACTIVE },
{ ledger2_accounts::INTANGIBLE_ASSETS, "Нематериальные активы", AccountType::ACTIVE },
{ ledger2_accounts::MATERIALS_GOODS, "Материалы и товары", AccountType::ACTIVE },
{ ledger2_accounts::MAIN_PRODUCTION, "Основное производство", AccountType::ACTIVE },
{ ledger2_accounts::NON_PROFIT_ACTIVITY, "Некоммерческая деятельность", AccountType::ACTIVE },
{ ledger2_accounts::GENERAL_EXPENSES, "Общехозяйственные расходы", AccountType::ACTIVE },
{ ledger2_accounts::FINANCIAL_INVESTMENTS, "Финансовые вложения из средств ПО", AccountType::ACTIVE },
{ ledger2_accounts::SHARES_AND_STAKES, "Паи и акции", AccountType::ACTIVE },
{ ledger2_accounts::SECURITIES, "Ценные бумаги", AccountType::ACTIVE },
{ ledger2_accounts::LOANS_ISSUED, "Расчёты по выданным займам", AccountType::ACTIVE },
{ ledger2_accounts::LOAN_INTEREST, "Проценты за пользование займами", AccountType::ACTIVE },
{ ledger2_accounts::ACCOUNTABLE_PERSONS, "Расчёты с подотчётными лицами", AccountType::ACTIVE_PASSIVE },
{ ledger2_accounts::RESERVES, "Резервы под снижение стоимости", AccountType::PASSIVE },
{ ledger2_accounts::LONG_TERM_LOANS, "Расчёты по долгосрочным кредитам и займам", AccountType::PASSIVE },
{ ledger2_accounts::TAXES_FEES, "Расчёты по налогам и сборам", AccountType::PASSIVE },
{ ledger2_accounts::SOCIAL_INSURANCE, "Расчёты по социальному страхованию", AccountType::PASSIVE },
{ ledger2_accounts::SALARY, "Расчёты с персоналом по оплате труда", AccountType::PASSIVE },
{ ledger2_accounts::SHARE_FUND, "Паевой фонд (складочный капитал)", AccountType::PASSIVE },
{ ledger2_accounts::ADDITIONAL_CAPITAL, "Добавочный капитал", AccountType::PASSIVE },
{ ledger2_accounts::FUNDS_PO_1, "Фонды потребительского общества (83.1)", AccountType::PASSIVE },
{ ledger2_accounts::FUNDS_PO_2, "Фонды потребительского общества (84.3)", AccountType::PASSIVE },
{ ledger2_accounts::UNDISTRIBUTED_PROFIT, "Нераспределённая прибыль", AccountType::PASSIVE },
{ ledger2_accounts::CURRENT_YEAR_PROFIT, "Нераспределённая прибыль отчётного года", AccountType::PASSIVE },
{ ledger2_accounts::PREVIOUS_YEARS_PROFIT, "Нераспределённая прибыль прошлых лет", AccountType::PASSIVE },
{ ledger2_accounts::TARGET_RECEIPTS, "Целевые поступления", AccountType::PASSIVE },
{ ledger2_accounts::ENTRANCE_FEES, "Вступительные взносы", AccountType::PASSIVE },
{ ledger2_accounts::MEMBERSHIP_FEES, "Членские взносы", AccountType::PASSIVE },
{ ledger2_accounts::RESERVE_FUND, "Резервный фонд", AccountType::PASSIVE },
{ ledger2_accounts::INDIVISIBLE_FUND, "Неделимый фонд", AccountType::PASSIVE },
{ ledger2_accounts::ECONOMIC_ACTIVITY_FUND,"Фонд обеспечения хозяйственной деятельности", AccountType::PASSIVE },
{ ledger2_accounts::MUTUAL_SECURITY_FUND, "Фонд взаимного обеспечения", AccountType::PASSIVE },
{ ledger2_accounts::DEVELOPMENT_FUND, "Фонд развития потребительской кооперации", AccountType::PASSIVE },
{ ledger2_accounts::DELEGATE_FEES_FUND, "Членские взносы делегатов", AccountType::PASSIVE },
{ ledger2_accounts::MEMBER_SETTLEMENTS, "Расчёты с пайщиками по внесению/возврату паевых взносов", AccountType::ACTIVE_PASSIVE },
{ ledger2_accounts::MEMBER_DEBT, "Задолженность пайщиков по внесению взносов", AccountType::ACTIVE },
{ ledger2_accounts::INCOME_ACCRUALS, "Начисление доходов участникам от предпринимательской деятельности ПО", AccountType::PASSIVE },
{ ledger2_accounts::MEMBER_FEES, "Расчёты по членским взносам", AccountType::ACTIVE_PASSIVE },
{ ledger2_accounts::PROPERTY_TRANSFER, "Передача имущества для некоммерческой деятельности", AccountType::ACTIVE_PASSIVE },
{ ledger2_accounts::OTHER_SETTLEMENTS, "Другие расчёты", AccountType::ACTIVE_PASSIVE },
{ ledger2_accounts::DEBTORS_CREDITORS, "Расчёты с дебиторами и кредиторами", AccountType::ACTIVE_PASSIVE },
{ ledger2_accounts::OTHER_INCOME_EXPENSES, "Прочие доходы и расходы", AccountType::ACTIVE_PASSIVE },
{ ledger2_accounts::FUTURE_EXPENSES_RESERVE, "Резервы предстоящих расходов", AccountType::PASSIVE },
{ ledger2_accounts::FUTURE_INCOME, "Доходы будущих периодов", AccountType::PASSIVE },
{ ledger2_accounts::FREE_RECEIPT, "Безвозмездные поступления", AccountType::PASSIVE },
};
inline const Ledger2AccountMeta* ledger2_find_account_meta(uint64_t account_id) {
for (const auto& row : LEDGER2_ACCOUNT_MAP) {
if (row.id == account_id) return &row;
}
return nullptr;
}
inline std::string ledger2_get_account_name_by_id(uint64_t account_id) {
const auto* meta = ledger2_find_account_meta(account_id);
return meta ? meta->name : std::string{};
}
inline AccountType ledger2_get_account_type_by_id(uint64_t account_id) {
const auto* meta = ledger2_find_account_meta(account_id);
return meta ? meta->type : AccountType::ACTIVE_PASSIVE;
}
@@ -0,0 +1,142 @@
#pragma once
#include <cstdint>
#include <string_view>
#include <eosio/eosio.hpp>
#include "accounts.hpp"
#include "wallets.hpp"
/**
* @brief Именованные операции ledger2 (action codes).
*
* Реестр — строго хардкод. Любая новая операция требует релиза
* контракта, менять на лету нельзя. На один action_code приходится
* ровно одна запись в ACTION_REGISTRY и атомарно одно движение
* кошелька + одна пара проводок.
*
* @ingroup public_ledger2_consts
*/
namespace ledger2_ops {
inline constexpr eosio::name ENTRANCE_FEE = "entrancefee"_n;
inline constexpr eosio::name INITIAL_SHARE = "initshare"_n;
inline constexpr eosio::name DEPOSIT_COMPLETE = "depositcpl"_n;
inline constexpr eosio::name WITHDRAW_COMPLETE= "withdrawcpl"_n;
inline constexpr eosio::name CAPITAL_IMPORT = "capimport"_n;
inline constexpr eosio::name LOAN_REPAYMENT = "loanrepay"_n;
inline constexpr eosio::name ACT2_SHARE = "act2share"_n;
inline constexpr eosio::name ACT2_LOAN = "act2loan"_n;
inline constexpr eosio::name SUPPLY_CONFIRM = "supplycnf"_n;
inline constexpr eosio::name RECEIVE_CONFIRM = "receivecnf"_n;
inline constexpr eosio::name CONVERT_TO_AXN = "converttoaxn"_n;
inline constexpr eosio::name ACT2_PROGRAM_PROP= "act2prop"_n;
}
/**
* @brief Элементарные операции по кошелькам.
*/
enum class WalletOp : uint8_t {
ISSUE = 0, ///< первичный вход средств на кошелёк wallet_to
TRANSFER = 1, ///< перемещение wallet_from → wallet_to
BLOCK = 2, ///< available-=amount, blocked+=amount на wallet_from
UNBLOCK = 3, ///< blocked-=amount, available+=amount на wallet_from
};
/**
* @brief Описание одной именованной операции.
*/
struct ActionRegistryEntry {
eosio::name code; ///< action_code
WalletOp wallet_op;
uint64_t wallet_from; ///< 0 для ISSUE
uint64_t wallet_to; ///< 0 для BLOCK/UNBLOCK
uint64_t debit_account_id;
uint64_t credit_account_id;
const char* human_name;
};
/**
* @brief Хардкод-реестр именованных операций MVP.
*
* Порядок записей не важен (линейный поиск в ledger2::apply).
* Добавлять новые записи только через релиз контракта.
*/
static constexpr ActionRegistryEntry ACTION_REGISTRY[] = {
// 1. Вступительный взнос: money comes in → ENTRANCE_FEES wallet; Dr 51 / Cr 86.01
{ ledger2_ops::ENTRANCE_FEE, WalletOp::ISSUE, 0, ledger2_wallets::ENTRANCE_FEES,
ledger2_accounts::BANK_ACCOUNT, ledger2_accounts::ENTRANCE_FEES,
"Вступительный взнос пайщика" },
// 2. Минимальный пай: money comes in → MIN_SHARE_FUND wallet (отдельный
// фонд для обязательных паёв при регистрации); Dr 51 / Cr 80
{ ledger2_ops::INITIAL_SHARE, WalletOp::ISSUE, 0, ledger2_wallets::MIN_SHARE_FUND,
ledger2_accounts::BANK_ACCOUNT, ledger2_accounts::SHARE_FUND,
"Минимальный паевой взнос" },
// 3. Депозит (завершение): money comes in → SHARE_FUND; Dr 51 / Cr 80
{ ledger2_ops::DEPOSIT_COMPLETE, WalletOp::ISSUE, 0, ledger2_wallets::SHARE_FUND,
ledger2_accounts::BANK_ACCOUNT, ledger2_accounts::SHARE_FUND,
"Внесение пайщиком паевого взноса" },
// 4. Возврат пая (завершение): money leaves → TRANSFER SHARE_FUND → WITHDRAWALS_SINK;
// Dr 80 / Cr 51 (пассив уменьшается, актив уменьшается)
{ ledger2_ops::WITHDRAW_COMPLETE, WalletOp::TRANSFER, ledger2_wallets::SHARE_FUND, ledger2_wallets::WITHDRAWALS_SINK,
ledger2_accounts::SHARE_FUND, ledger2_accounts::BANK_ACCOUNT,
"Возврат паевого взноса пайщику" },
// 5. Капитальный взнос: money comes in → SHARE_FUND; Dr 51 / Cr 80
{ ledger2_ops::CAPITAL_IMPORT, WalletOp::ISSUE, 0, ledger2_wallets::SHARE_FUND,
ledger2_accounts::BANK_ACCOUNT, ledger2_accounts::SHARE_FUND,
"Капитальный взнос (Благорост)" },
// 6. Заём получен: задолженность кооператива растёт; LONG_TERM_LOANS wallet++; Dr 51 / Cr 67
{ ledger2_ops::LOAN_REPAYMENT, WalletOp::ISSUE, 0, ledger2_wallets::LONG_TERM_LOANS,
ledger2_accounts::BANK_ACCOUNT, ledger2_accounts::LONG_TERM_LOANS,
"Получение долгосрочного займа" },
// 7. Сдача задания — прирост паевого фонда: money in → SHARE_FUND; Dr 51 / Cr 80
{ ledger2_ops::ACT2_SHARE, WalletOp::ISSUE, 0, ledger2_wallets::SHARE_FUND,
ledger2_accounts::BANK_ACCOUNT, ledger2_accounts::SHARE_FUND,
"Прирост паевого фонда по акту выполненных работ" },
// 8. Сдача задания — гашение долга: LONG_TERM_LOANS → DEBT_CLOSED_SINK; Dr 67 / Cr 51
{ ledger2_ops::ACT2_LOAN, WalletOp::TRANSFER, ledger2_wallets::LONG_TERM_LOANS, ledger2_wallets::DEBT_CLOSED_SINK,
ledger2_accounts::LONG_TERM_LOANS, ledger2_accounts::BANK_ACCOUNT,
"Гашение долга по акту выполненных работ" },
// 9. Подтверждение поставки: money in → SHARE_FUND; Dr 51 / Cr 80
{ ledger2_ops::SUPPLY_CONFIRM, WalletOp::ISSUE, 0, ledger2_wallets::SHARE_FUND,
ledger2_accounts::BANK_ACCOUNT, ledger2_accounts::SHARE_FUND,
"Подтверждение поставки товара/услуги" },
// 10. Подтверждение получения: SHARE_FUND → SUPPLIER_PAYMENTS; Dr 80 / Cr 51
{ ledger2_ops::RECEIVE_CONFIRM, WalletOp::TRANSFER, ledger2_wallets::SHARE_FUND, ledger2_wallets::SUPPLIER_PAYMENTS,
ledger2_accounts::SHARE_FUND, ledger2_accounts::BANK_ACCOUNT,
"Подтверждение получения товара/услуги — выплата поставщику" },
// 11. Конверт в AXN: SHARE_FUND → DELEGATE_FEES; Dr 80 / Cr 868 (делегатские взносы)
{ ledger2_ops::CONVERT_TO_AXN, WalletOp::TRANSFER, ledger2_wallets::SHARE_FUND, ledger2_wallets::DELEGATE_FEES,
ledger2_accounts::SHARE_FUND, ledger2_accounts::DELEGATE_FEES_FUND,
"Конвертация паевого взноса в делегатские членские взносы" },
// 12. Акт 2 по программному имущественному взносу: money in → SHARE_FUND; Dr 51 / Cr 80
{ ledger2_ops::ACT2_PROGRAM_PROP, WalletOp::ISSUE, 0, ledger2_wallets::SHARE_FUND,
ledger2_accounts::BANK_ACCOUNT, ledger2_accounts::SHARE_FUND,
"Прирост паевого фонда по акту 2 программного имущественного взноса" },
};
static constexpr size_t ACTION_REGISTRY_SIZE = sizeof(ACTION_REGISTRY) / sizeof(ACTION_REGISTRY[0]);
/**
* @brief Линейный поиск записи реестра по action_code.
* @return указатель на запись или nullptr при отсутствии.
*/
inline const ActionRegistryEntry* ledger2_find_action(eosio::name action_code) {
for (size_t i = 0; i < ACTION_REGISTRY_SIZE; ++i) {
if (ACTION_REGISTRY[i].code == action_code) {
return &ACTION_REGISTRY[i];
}
}
return nullptr;
}
@@ -0,0 +1,51 @@
#pragma once
#include <string>
#include <eosio/action.hpp>
#include <eosio/asset.hpp>
#include <eosio/crypto.hpp>
#include <eosio/eosio.hpp>
#include "../../consts.hpp"
#include "accounts.hpp"
#include "actions.hpp"
#include "wallets.hpp"
/**
* @brief Хелпер для вызова ledger2::apply из контрактов-инициаторов.
*
* Используется вместо прежних Ledger::add / Ledger::sub / Ledger::transfer.
* Именованные коды операций — в `ledger2_ops::*`. Массив ACTION_REGISTRY
* определяет маппинг action_code → (wallet_op, Dr, Cr), поэтому на стороне
* инициатора достаточно передать code, amount, имя пайщика-инициатора,
* хэш документа и memo.
*/
class Ledger2 {
public:
/**
* @brief Отправить inline action ledger2::apply.
*
* @param actor контракт-инициатор (его permission используется)
* @param coopname кооператив (scope в ledger2)
* @param action_code именованная операция из ledger2_ops::*
* @param amount сумма операции (положительная, символ RUB)
* @param username пайщик-инициатор (для истории в operations)
* @param document_hash хэш документа-основания
* @param memo произвольный текстовый комментарий
*/
static inline void apply(eosio::name actor,
eosio::name coopname,
eosio::name action_code,
eosio::asset amount,
eosio::name username,
eosio::checksum256 document_hash,
std::string memo) {
eosio::action(
eosio::permission_level{actor, "active"_n},
_ledger2,
"apply"_n,
std::make_tuple(coopname, actor, action_code, amount, username, document_hash, memo)
).send();
}
};
@@ -0,0 +1,57 @@
#pragma once
#include <cstdint>
#include <string>
#include <tuple>
#include <vector>
/**
* @brief Стандарт кошельков ledger2 (id — простой порядковый).
*
* Кошельки ledger2 — это целевые фонды, КУДА выпускаются средства,
* не бухгалтерские счета денежных средств. Учёт остатков кассы/банка
* (счёт 51) живёт только на уровне accounts (двойная запись). На
* уровне кошельков мы отмечаем, в какой фонд зачислены поступления.
*
* При первом ISSUE/TRANSFER кошелёк создаётся автоматически по имени
* из WALLET_REGISTRY. При обнулении available+blocked запись удаляется.
*
* Важно: минимальный паевой взнос и обычные паевые — это РАЗНЫЕ
* кошельки (MIN_SHARE_FUND vs SHARE_FUND). Бухгалтерская проводка у
* обоих одинаковая (Dr 51 / Cr 80), а фонд назначения разный.
*
* @ingroup public_ledger2_consts
*/
struct ledger2_wallets {
// Целевые фонды поступлений
static constexpr uint64_t MIN_SHARE_FUND = 1; ///< Минимальные паевые взносы (обязательный пай при регистрации)
static constexpr uint64_t SHARE_FUND = 2; ///< Паевой фонд (добровольные паевые взносы)
static constexpr uint64_t ENTRANCE_FEES = 3; ///< Вступительные взносы
static constexpr uint64_t MEMBERSHIP_FEES = 4; ///< Членские взносы
static constexpr uint64_t DELEGATE_FEES = 5; ///< Членские взносы делегатов
static constexpr uint64_t LONG_TERM_LOANS = 6; ///< Долгосрочные займы (задолженность)
// Кошельки-накопители («sinks») для моделирования выплат без writeoff
static constexpr uint64_t WITHDRAWALS_SINK = 10; ///< Возвраты паёв пайщикам
static constexpr uint64_t SUPPLIER_PAYMENTS = 11; ///< Выплаты поставщикам маркетплейса
static constexpr uint64_t DEBT_CLOSED_SINK = 12; ///< Гашение обязательств кооператива
};
static const std::vector<std::tuple<uint64_t, std::string>> LEDGER2_WALLET_REGISTRY = {
{ ledger2_wallets::MIN_SHARE_FUND, "Минимальные паевые взносы" },
{ ledger2_wallets::SHARE_FUND, "Паевой фонд" },
{ ledger2_wallets::ENTRANCE_FEES, "Вступительные взносы" },
{ ledger2_wallets::MEMBERSHIP_FEES, "Членские взносы" },
{ ledger2_wallets::DELEGATE_FEES, "Членские взносы делегатов" },
{ ledger2_wallets::LONG_TERM_LOANS, "Долгосрочные займы" },
{ ledger2_wallets::WITHDRAWALS_SINK, "Возвраты паевых взносов пайщикам" },
{ ledger2_wallets::SUPPLIER_PAYMENTS, "Выплаты поставщикам" },
{ ledger2_wallets::DEBT_CLOSED_SINK, "Гашение обязательств кооператива" },
};
inline std::string ledger2_get_wallet_name_by_id(uint64_t wallet_id) {
for (const auto& row : LEDGER2_WALLET_REGISTRY) {
if (std::get<0>(row) == wallet_id) return std::get<1>(row);
}
return std::string{};
}
@@ -55,6 +55,11 @@
#include "table_gateway_outcomes.hpp"
#include "table_ledger_laccount.hpp"
#include "table_ledger_writeoffs.hpp"
#include "table_ledger2_wallet.hpp"
#include "table_ledger2_account.hpp"
#include "table_ledger2_journal.hpp"
#include "table_ledger2_wjournal.hpp"
#include "table_ledger2_meta.hpp"
#include "table_loan_debts.hpp"
#include "table_loan_summaries.hpp"
#include "table_marketplace_requests.hpp"
@@ -0,0 +1,49 @@
#pragma once
#include <eosio/asset.hpp>
#include <eosio/eosio.hpp>
#include <string>
#include "../consts.hpp"
/**
* @ingroup public_tables
* @ingroup public_ledger2_tables
* @par table: accounts (ledger2)
*
* @brief Бухгалтерский счёт ledger2 (двойная запись).
*
* id — со смещением *1000: 86 → 86000, 86.01 → 861000, 86.0100 → 8610000.
*
* account_type определяется планом счетов и сохраняется в записи при первой
* проводке:
* - 0 = ACTIVE → balance = debit_balance credit_balance
* - 1 = PASSIVE → balance = credit_balance debit_balance
* - 2 = ACTIVE_PASSIVE → balance = debit_balance credit_balance (знаковая)
*
* debit_balance / credit_balance накапливают обороты независимо. Это нужно
* для сверки инварианта Σ debit == Σ credit по всему журналу (инвариант
* двойной записи). Сальдо считается в get_balance() с учётом типа.
*/
struct [[eosio::table, eosio::contract(LEDGER2)]] account2 {
uint64_t id;
std::string name;
uint8_t account_type; ///< AccountType: 0=ACTIVE, 1=PASSIVE, 2=ACTIVE_PASSIVE
eosio::asset debit_balance;
eosio::asset credit_balance;
uint64_t primary_key() const { return id; }
bool is_empty() const {
return debit_balance.amount == 0 && credit_balance.amount == 0;
}
/// @brief Сальдо с учётом активности/пассивности счёта.
eosio::asset get_balance() const {
// 1 == PASSIVE — кредитовый остаток; для всех остальных типов возвращаем Dr − Cr
if (account_type == 1) return credit_balance - debit_balance;
return debit_balance - credit_balance;
}
};
typedef eosio::multi_index<"accounts"_n, account2> accounts2_index;
@@ -0,0 +1,54 @@
#pragma once
#include <eosio/asset.hpp>
#include <eosio/crypto.hpp>
#include <eosio/eosio.hpp>
#include <eosio/time.hpp>
#include <string>
#include "../consts.hpp"
/**
* @ingroup public_tables
* @ingroup public_ledger2_tables
* @par table: journal (ledger2)
*
* @brief Журнал проводок бухгалтерских счетов ledger2 (account-level,
* двойная запись).
*
* На каждый успешный ledger2::apply создаётся ровно одна запись —
* пара (debit_account_id, credit_account_id) на сумму amount.
* Запись линкуется с парной операцией кошелька в таблице wjournal
* через wjournal_entry_id.
*
* Инвариант: для любого диапазона дат Σ amount (по всем записям) равна
* Σ amount (тот же набор) — записи Dr/Cr идут парами, сумма одна,
* поэтому totalDebit == totalCredit всегда.
*/
struct [[eosio::table, eosio::contract(LEDGER2)]] journal_entry {
uint64_t id;
uint64_t debit_account_id;
uint64_t credit_account_id;
eosio::asset amount;
eosio::name action_code;
uint64_t wjournal_entry_id; ///< id парной записи в wjournal
eosio::checksum256 operation_hash;
std::string memo;
eosio::time_point created_at;
uint64_t primary_key() const { return id; }
uint64_t by_debit() const { return debit_account_id; }
uint64_t by_credit() const { return credit_account_id; }
uint64_t by_action() const { return action_code.value; }
uint64_t by_wjournal() const { return wjournal_entry_id; }
uint64_t by_created() const { return static_cast<uint64_t>(created_at.elapsed.count()); }
};
typedef eosio::multi_index<
"journal"_n, journal_entry,
eosio::indexed_by<"bydebit"_n, eosio::const_mem_fun<journal_entry, uint64_t, &journal_entry::by_debit>>,
eosio::indexed_by<"bycredit"_n, eosio::const_mem_fun<journal_entry, uint64_t, &journal_entry::by_credit>>,
eosio::indexed_by<"byaction"_n, eosio::const_mem_fun<journal_entry, uint64_t, &journal_entry::by_action>>,
eosio::indexed_by<"bywjournal"_n, eosio::const_mem_fun<journal_entry, uint64_t, &journal_entry::by_wjournal>>,
eosio::indexed_by<"bycreated"_n, eosio::const_mem_fun<journal_entry, uint64_t, &journal_entry::by_created>>
> journal_index;
@@ -0,0 +1,25 @@
#pragma once
#include <eosio/eosio.hpp>
#include <eosio/time.hpp>
#include "../consts.hpp"
/**
* @ingroup public_tables
* @ingroup public_ledger2_tables
* @par table: meta (ledger2, scope = _ledger2)
*
* @brief Singleton-таблица ledger2 для системных флагов
* (в частности — завершённости миграции с ledger).
*/
struct [[eosio::table, eosio::contract(LEDGER2)]] ledger2_meta {
uint64_t id; // всегда 0
bool migrated; // миграция ledger→ledger2 завершена
uint64_t migrated_coops;
eosio::time_point migrated_at;
uint64_t primary_key() const { return id; }
};
typedef eosio::multi_index<"meta"_n, ledger2_meta> ledger2_meta_index;
@@ -0,0 +1,36 @@
#pragma once
#include <eosio/asset.hpp>
#include <eosio/eosio.hpp>
#include <string>
#include "../consts.hpp"
/**
* @ingroup public_tables
* @ingroup public_ledger2_tables
* @par table: wallets (ledger2)
*
* @brief Кошелёк управленческого учёта ledger2.
*
* Простой порядковый id (без смещения). Запись создаётся upsert-ом
* при первом ISSUE/TRANSFER на кошелёк и удаляется, когда
* available == 0 && blocked == 0. Писем «writeoff» не существует —
* все выплаты моделируются как TRANSFER в кошельки-накопители.
*/
struct [[eosio::table, eosio::contract(LEDGER2)]] wallet2 {
uint64_t id;
std::string name;
eosio::asset available;
eosio::asset blocked;
uint64_t primary_key() const { return id; }
bool is_empty() const {
return available.amount == 0 && blocked.amount == 0;
}
eosio::asset get_total() const { return available + blocked; }
};
typedef eosio::multi_index<"wallets"_n, wallet2> wallets2_index;
@@ -0,0 +1,60 @@
#pragma once
#include <eosio/asset.hpp>
#include <eosio/crypto.hpp>
#include <eosio/eosio.hpp>
#include <eosio/time.hpp>
#include <string>
#include "../consts.hpp"
/**
* @ingroup public_tables
* @ingroup public_ledger2_tables
* @par table: wjournal (ledger2)
*
* @brief Журнал операций по кошелькам ledger2 (wallet-level).
*
* На каждый успешный ledger2::apply создаётся ровно одна запись,
* описывающая атомарное движение кошельков:
* - ISSUE (wallet_to получает amount)
* - TRANSFER (wallet_from → wallet_to на amount)
* - BLOCK (wallet_from: available → blocked на amount)
* - UNBLOCK (wallet_from: blocked → available на amount)
*
* Линки на action_code и journal_entry_id позволяют сверять движение
* кошелька с парной бухгалтерской проводкой в таблице journal.
*/
struct [[eosio::table, eosio::contract(LEDGER2)]] wjournal_entry {
uint64_t id;
uint8_t wallet_op; ///< WalletOp: 0=ISSUE, 1=TRANSFER, 2=BLOCK, 3=UNBLOCK
uint64_t wallet_from; ///< 0 для ISSUE
uint64_t wallet_to; ///< 0 для BLOCK/UNBLOCK
eosio::asset amount;
eosio::name action_code;
uint64_t journal_entry_id; ///< id парной бухгалтерской проводки
eosio::name username;
std::string memo;
eosio::checksum256 document_hash;
eosio::time_point created_at;
uint64_t primary_key() const { return id; }
uint64_t by_op() const { return wallet_op; }
uint64_t by_from() const { return wallet_from; }
uint64_t by_to() const { return wallet_to; }
uint64_t by_action() const { return action_code.value; }
uint64_t by_user() const { return username.value; }
uint64_t by_journal() const { return journal_entry_id; }
uint64_t by_created() const { return static_cast<uint64_t>(created_at.elapsed.count()); }
};
typedef eosio::multi_index<
"wjournal"_n, wjournal_entry,
eosio::indexed_by<"byop"_n, eosio::const_mem_fun<wjournal_entry, uint64_t, &wjournal_entry::by_op>>,
eosio::indexed_by<"byfrom"_n, eosio::const_mem_fun<wjournal_entry, uint64_t, &wjournal_entry::by_from>>,
eosio::indexed_by<"byto"_n, eosio::const_mem_fun<wjournal_entry, uint64_t, &wjournal_entry::by_to>>,
eosio::indexed_by<"byaction"_n, eosio::const_mem_fun<wjournal_entry, uint64_t, &wjournal_entry::by_action>>,
eosio::indexed_by<"byuser"_n, eosio::const_mem_fun<wjournal_entry, uint64_t, &wjournal_entry::by_user>>,
eosio::indexed_by<"byjournal"_n, eosio::const_mem_fun<wjournal_entry, uint64_t, &wjournal_entry::by_journal>>,
eosio::indexed_by<"bycreated"_n, eosio::const_mem_fun<wjournal_entry, uint64_t, &wjournal_entry::by_created>>
> wjournal_index;
@@ -43,8 +43,8 @@
// Проводки по кошельку
std::string memo = "Подтверждение получения для заказа №" + std::to_string(change.id);
// Паевой фонд - уменьшаем циркуляцию
Ledger::sub(_marketplace, coopname, Ledger::accounts::SHARE_FUND, change.base_cost, memo, request_hash, username);
// Паевой фонд - уменьшаем циркуляцию через ledger2 (TRANSFER SHARE_FUND → SUPPLIER_PAYMENTS)
Ledger2::apply(_marketplace, coopname, ledger2_ops::RECEIVE_CONFIRM, change.base_cost, username, request_hash, memo);
// Заказчик - списываем заблокированный баланс
Wallet::sub_blocked_funds(_marketplace, coopname, change.money_contributor, change.total_cost, _marketplace_program, memo);
@@ -41,8 +41,8 @@
// Проводки по кошельку
std::string memo = "Подтверждение поставки для заказа №" + std::to_string(change.id);
// Паевой фонд - увеличиваем циркуляцию
Ledger::add(_marketplace, coopname, Ledger::accounts::SHARE_FUND, change.total_cost, memo, request_hash, username);
// Паевой фонд - увеличиваем циркуляцию через ledger2 (ISSUE в SHARE_FUND)
Ledger2::apply(_marketplace, coopname, ledger2_ops::SUPPLY_CONFIRM, change.total_cost, username, request_hash, memo);
// Поставщик - начисляем и блокируем средства
Wallet::add_available_funds(_marketplace, coopname, change.product_contributor, change.base_cost, _marketplace_program, memo);
@@ -75,13 +75,13 @@
std::string username_str = username.to_string();
checksum256 username_hash = eosio::sha256(username_str.data(), username_str.size());
Ledger::add(_registrator, coopname, Ledger::accounts::SHARE_FUND, minimum, memo, username_hash, username);
Ledger::add(_registrator, coopname, Ledger::accounts::BANK_ACCOUNT, minimum, memo, username_hash, username);
// Минимальный паевой взнос через ledger2 (ISSUE в SHARE_FUND, Dr: BANK_ACCOUNT, Cr: SHARE_FUND)
Ledger2::apply(_registrator, coopname, ledger2_ops::INITIAL_SHARE, minimum, username, username_hash, memo);
if (spread_initial) {
memo = "Вступительный взнос при вступлении пайщика с username=" + username.to_string();
Ledger::add(_registrator, coopname, Ledger::accounts::ENTRANCE_FEES, initial, memo, username_hash, username);
Ledger::add(_registrator, coopname, Ledger::accounts::BANK_ACCOUNT, initial, memo, username_hash, username);
// Вступительный взнос через ledger2 (ISSUE в ENTRANCE_FEES, Dr: BANK_ACCOUNT, Cr: ENTRANCE_FEES)
Ledger2::apply(_registrator, coopname, ledger2_ops::ENTRANCE_FEE, initial, username, username_hash, memo);
}
// Увеличиваем счётчик активных пайщиков
@@ -46,13 +46,10 @@ void registrator::confirmreg(eosio::name coopname, checksum256 registration_hash
).send();
std::string memo = "Минимальный паевой взнос при вступлении пайщика с username=" + candidate -> username.to_string();
//добавляем
Ledger::add(_registrator, coopname, Ledger::accounts::SHARE_FUND, candidate -> minimum, memo, registration_hash, candidate -> username);
Ledger::add(_registrator, coopname, Ledger::accounts::BANK_ACCOUNT, candidate -> minimum, memo, registration_hash, candidate -> username);
Ledger2::apply(_registrator, coopname, ledger2_ops::INITIAL_SHARE, candidate -> minimum, candidate -> username, registration_hash, memo);
memo = "Вступительный взнос при вступлении пайщика с username=" + candidate -> username.to_string();
Ledger::add(_registrator, coopname, Ledger::accounts::ENTRANCE_FEES, candidate -> initial, memo, registration_hash, candidate -> username);
Ledger::add(_registrator, coopname, Ledger::accounts::BANK_ACCOUNT, candidate -> initial, memo, registration_hash, candidate -> username);
Ledger2::apply(_registrator, coopname, ledger2_ops::ENTRANCE_FEE, candidate -> initial, candidate -> username, registration_hash, memo);
// Увеличиваем счетчик активных пайщиков
cooperatives2_index cooperatives(_registrator, _registrator.value);
auto coop_itr = cooperatives.find(coopname.value);
@@ -27,9 +27,9 @@ void soviet::converttoaxn(eosio::name coopname, eosio::asset amount, document2 s
// Уменьшаем паевой кошелёк провайдера
checksum256 hash = statement.hash;
Ledger::sub(_soviet, _provider, Ledger::accounts::SHARE_FUND, amount, memo, hash, coopname);
Ledger::add(_soviet, _provider, Ledger::accounts::DELEGATE_FEES_FUND, amount, memo, hash, coopname);
// Перенос паевых средств провайдера в фонд делегатских взносов через ledger2
// (TRANSFER SHARE_FUND → DELEGATE_FEES внутри кошелька _provider)
Ledger2::apply(_soviet, _provider, ledger2_ops::CONVERT_TO_AXN, amount, coopname, hash, memo);
// Вызываем инъекцию AXON токенов на кооператив
action(
@@ -14,8 +14,7 @@ void wallet::completedpst(eosio::name coopname, checksum256 deposit_hash) {
std::string memo = "Паевой взнос по целевой потребительской программе 'Цифровой Кошелёк' пайщика с username=" + deposit -> username.to_string();
Wallet::add_available_funds(_wallet, coopname, deposit -> username, deposit ->quantity, _wallet_program, memo);
Ledger::add(_wallet, coopname, Ledger::accounts::SHARE_FUND, deposit -> quantity, memo, deposit_hash, deposit -> username);
Ledger::add(_wallet, coopname, Ledger::accounts::BANK_ACCOUNT, deposit -> quantity, memo, deposit_hash, deposit -> username);
Ledger2::apply(_wallet, coopname, ledger2_ops::DEPOSIT_COMPLETE, deposit -> quantity, deposit -> username, deposit_hash, memo);
//оповещаем пользователя
require_recipient(deposit -> username);
@@ -11,8 +11,7 @@ void wallet::completewthd(COMPLETEWTHD_SIGNATURE) {
std::string memo_in = "Возврат части паевого взноса по ЦПП 'Цифровой Кошелёк' пайщику с username=" + withdraw -> username.to_string();
Ledger::sub(_wallet, coopname, Ledger::accounts::SHARE_FUND, withdraw -> quantity, memo_in, withdraw_hash, withdraw -> username);
Ledger::sub(_wallet, coopname, Ledger::accounts::BANK_ACCOUNT, withdraw -> quantity, memo_in, withdraw_hash, withdraw -> username);
Ledger2::apply(_wallet, coopname, ledger2_ops::WITHDRAW_COMPLETE, withdraw -> quantity, withdraw -> username, withdraw_hash, memo_in);
Wallet::sub_blocked_funds(_wallet, coopname, withdraw -> username, withdraw -> quantity, _wallet_program, memo_in);
+2
View File
@@ -65,6 +65,7 @@ import { GatewayModule } from './application/gateway/gateway.module';
import { WalletModule } from './application/wallet/wallet.module';
import { NotificationModule } from './application/notification/notification.module';
import { LedgerModule } from './application/ledger/ledger.module';
import { Ledger2Module } from './application/ledger2/ledger2.module';
import { BlockchainExplorerModule } from './application/blockchain-explorer/blockchain-explorer.module';
import { ProviderModule } from './application/provider/provider.module';
import { UserModule } from './application/user/user.module';
@@ -144,6 +145,7 @@ import { MutationLoggingInterceptor } from './application/common/interceptors/mu
WalletModule,
NotificationModule,
LedgerModule,
Ledger2Module,
BlockchainExplorerModule,
ProviderModule,
UserModule,
@@ -0,0 +1,35 @@
import { InputType, Field, GraphQLISODateTime } from '@nestjs/graphql';
import { IsString, IsNotEmpty, IsOptional, IsDate } from 'class-validator';
import { Type } from 'class-transformer';
/**
* Входные параметры запроса проверки инварианта Dr=Cr по журналу ledger2.
* fromDate / toDate включительно; если не указаны — весь журнал.
*/
@InputType('ValidateJournalInvariantInput')
export class ValidateJournalInvariantInputDTO {
@Field(() => String, {
description: 'Имя кооператива (scope таблицы journal в контракте ledger2)',
})
@IsString()
@IsNotEmpty()
coopname!: string;
@Field(() => GraphQLISODateTime, {
nullable: true,
description: 'Нижняя граница диапазона по created_at включительно (ISO-8601). Если не указана — с начала журнала.',
})
@IsOptional()
@Type(() => Date)
@IsDate()
fromDate?: Date;
@Field(() => GraphQLISODateTime, {
nullable: true,
description: 'Верхняя граница диапазона по created_at включительно (ISO-8601). Если не указана — до конца журнала.',
})
@IsOptional()
@Type(() => Date)
@IsDate()
toDate?: Date;
}
@@ -0,0 +1,36 @@
import { ObjectType, Field, Int, GraphQLISODateTime } from '@nestjs/graphql';
/**
* Результат проверки инварианта Dr=Cr по журналу ledger2.
* ok == true, если суммарные дебетовые и кредитовые обороты в выбранном
* диапазоне совпадают.
*/
@ObjectType('ValidateJournalInvariantResult')
export class ValidateJournalInvariantResultDTO {
@Field(() => String)
coopname!: string;
@Field(() => GraphQLISODateTime, { nullable: true })
fromDate?: Date;
@Field(() => GraphQLISODateTime, { nullable: true })
toDate?: Date;
@Field(() => Int, { description: 'Количество записей журнала в диапазоне' })
entriesCount!: number;
@Field(() => String, { description: 'Сумма дебетовых оборотов (в минимальных единицах актива)' })
totalDebit!: string;
@Field(() => String, { description: 'Сумма кредитовых оборотов (в минимальных единицах актива)' })
totalCredit!: string;
@Field(() => String, { description: 'Разница totalDebit - totalCredit (ожидается "0")' })
difference!: string;
@Field(() => String, { nullable: true, description: 'Символ актива (обычно RUB), null если диапазон пуст' })
symbol?: string;
@Field(() => Boolean, { description: 'true, если инвариант Dr=Cr соблюдён' })
ok!: boolean;
}
@@ -0,0 +1,15 @@
import { Module } from '@nestjs/common';
import { Ledger2Resolver } from './resolvers/ledger2.resolver';
import { Ledger2Service } from './services/ledger2.service';
/**
* Модуль приложения для ledger2.
* BlockchainService уже глобален (BlockchainModule @Global), отдельных доменных
* портов пока не требуется — валидатор читает таблицу journal напрямую.
*/
@Module({
imports: [],
providers: [Ledger2Resolver, Ledger2Service],
exports: [Ledger2Service],
})
export class Ledger2Module {}
@@ -0,0 +1,32 @@
import { Resolver, Query, Args } from '@nestjs/graphql';
import { UseGuards } from '@nestjs/common';
import { AuthRoles } from '~/application/auth/decorators/auth.decorator';
import { GqlJwtAuthGuard } from '~/application/auth/guards/graphql-jwt-auth.guard';
import { RolesGuard } from '~/application/auth/guards/roles.guard';
import { Ledger2Service } from '../services/ledger2.service';
import { ValidateJournalInvariantInputDTO } from '../dto/validate-journal-input.dto';
import { ValidateJournalInvariantResultDTO } from '../dto/validate-journal-output.dto';
/**
* GraphQL резолвер ledger2.
* Сейчас предоставляет единственный запрос validateJournalInvariant —
* сверку Dr=Cr по журналу двойных проводок в заданном диапазоне дат.
*/
@Resolver()
export class Ledger2Resolver {
constructor(private readonly ledger2Service: Ledger2Service) {}
@Query(() => ValidateJournalInvariantResultDTO, {
name: 'validateJournalInvariant',
description:
'Проверить инвариант Dr=Cr по журналу двойных проводок контракта ledger2 в заданном диапазоне дат. Возвращает суммарные обороты и флаг ok.',
})
@UseGuards(GqlJwtAuthGuard, RolesGuard)
@AuthRoles(['chairman', 'member'])
async validateJournalInvariant(
@Args('data', { type: () => ValidateJournalInvariantInputDTO })
data: ValidateJournalInvariantInputDTO
): Promise<ValidateJournalInvariantResultDTO> {
return this.ledger2Service.validateJournalInvariant(data);
}
}
@@ -0,0 +1,115 @@
import { Injectable } from '@nestjs/common';
import { BlockchainService } from '~/infrastructure/blockchain/blockchain.service';
import { ValidateJournalInvariantInputDTO } from '../dto/validate-journal-input.dto';
import { ValidateJournalInvariantResultDTO } from '../dto/validate-journal-output.dto';
const LEDGER2_CONTRACT = 'ledger2';
const JOURNAL_TABLE = 'journal';
/**
* Запись журнала двойных проводок ledger2 в том виде, в каком её возвращает wharfkit.
* amount — это eosio::asset, сериализуемый Antelope в строку вида "100.0000 RUB".
* created_at — ISO timestamp.
*/
interface Ledger2JournalRow {
id: number | string;
debit_account_id: number | string;
credit_account_id: number | string;
amount: string;
action_code: string;
wjournal_entry_id: number | string;
operation_hash: string;
memo: string;
created_at: string;
}
/**
* Разбирает asset "100.0000 RUB" → { amount: 1000000n (minor units), symbol: "RUB" }.
* Используются bigint-минимальные-единицы, чтобы избежать float-погрешностей.
*/
function parseAsset(raw: string): { amount: bigint; symbol: string } {
const trimmed = raw.trim();
const spaceIdx = trimmed.indexOf(' ');
if (spaceIdx < 0) {
throw new Error(`Некорректный asset: "${raw}"`);
}
const numericPart = trimmed.slice(0, spaceIdx);
const symbol = trimmed.slice(spaceIdx + 1);
const negative = numericPart.startsWith('-');
const unsigned = negative ? numericPart.slice(1) : numericPart;
const dotIdx = unsigned.indexOf('.');
let whole: string;
let fractional: string;
if (dotIdx < 0) {
whole = unsigned;
fractional = '';
} else {
whole = unsigned.slice(0, dotIdx);
fractional = unsigned.slice(dotIdx + 1);
}
const digits = (whole || '0') + fractional;
if (!/^\d+$/.test(digits)) {
throw new Error(`Некорректные цифры в asset: "${raw}"`);
}
const value = BigInt(digits);
return { amount: negative ? -value : value, symbol };
}
@Injectable()
export class Ledger2Service {
constructor(private readonly blockchainService: BlockchainService) {}
/**
* Перечисляет записи таблицы journal (scope = coopname), фильтрует по
* created_at в указанном диапазоне и возвращает суммарные обороты.
*/
async validateJournalInvariant(
input: ValidateJournalInvariantInputDTO
): Promise<ValidateJournalInvariantResultDTO> {
const rows = await this.blockchainService.getAllRows<Ledger2JournalRow>(
LEDGER2_CONTRACT,
input.coopname,
JOURNAL_TABLE
);
const fromMs = input.fromDate ? input.fromDate.getTime() : undefined;
const toMs = input.toDate ? input.toDate.getTime() : undefined;
let totalDebit = 0n;
let totalCredit = 0n;
let entriesCount = 0;
let symbol: string | undefined;
for (const row of rows) {
const createdMs = Date.parse(row.created_at);
if (Number.isNaN(createdMs)) continue;
if (fromMs !== undefined && createdMs < fromMs) continue;
if (toMs !== undefined && createdMs > toMs) continue;
const { amount, symbol: rowSymbol } = parseAsset(row.amount);
if (symbol === undefined) symbol = rowSymbol;
totalDebit += amount;
totalCredit += amount;
entriesCount += 1;
}
const difference = totalDebit - totalCredit;
return {
coopname: input.coopname,
fromDate: input.fromDate,
toDate: input.toDate,
entriesCount,
totalDebit: totalDebit.toString(),
totalCredit: totalCredit.toString(),
difference: difference.toString(),
symbol,
ok: difference === 0n,
};
}
}