[989-1][@ant] feat: внедрить ledger2 с apply и переключить контракты-инициаторы с legacy ledger — нужна двойная запись и единый API именованных операций для миграции остатков и регуляторной отчётности
Made-with: Cursor
This commit is contained in:
@@ -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'),
|
||||
|
||||
@@ -95,6 +95,10 @@ export default {
|
||||
name: 'ledger',
|
||||
code_permissions_to: ['ledger'],
|
||||
},
|
||||
{
|
||||
name: 'ledger2',
|
||||
code_permissions_to: ['ledger2'],
|
||||
},
|
||||
{
|
||||
name: 'test',
|
||||
code_permissions_to: ['test'],
|
||||
|
||||
@@ -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);
|
||||
|
||||
+2
-2
@@ -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);
|
||||
|
||||
}
|
||||
+2
-2
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user