e073a6d9bc
- expense::createexp принимает контракты-инициаторы по contracts_whitelist (capital@eosio.code не имеет coopname@active — inline падал по authority); capital::createpgexp шлёт inline от _capital@active. - capital::onpgexpdone: require_auth(_expense) вместо _capital — callback шасси шёл с authority expense@active и всегда падал. - payexp: cap actual<=plan + DIRECT-item сразу REPORTED с пересчётом статуса proposal — DIRECT-only СЗ навсегда зависал в PARTIALLY_PAID. - returnexp/overspendexp: settlement-семантика по PRD — статус item остаётся PAID, отчёт закрывает item штатным reportexp (раньше RETURNED/OVERSPENT были терминальными тупиками: перерасход блокировал closeexp навсегда). - declexp: только CREATED/AUTHORIZED — decline после оплат разъезжался с учётом пула в capital (возвращал весь резерв при ушедших деньгах). - onpgexpdone CLOSED: перерасход сверх резерва списывается из program_expense_pool (spend_program_expense_pool). - names.hpp: redefinition CREATE_PROGRAM_EXPENSE + несуществующий Names::Capital::Callbacks — контракты не компилировались вовсе. - verify_document_or_fail(statement, {creator}) в createpgexp; снос мёртвых set_program_approved/set_program_authorized. Оба контракта собраны: expense.wasm + capital.wasm. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
273 lines
12 KiB
C++
273 lines
12 KiB
C++
#pragma once
|
|
|
|
#include <eosio/eosio.hpp>
|
|
#include <eosio/asset.hpp>
|
|
|
|
using namespace eosio;
|
|
using std::string;
|
|
|
|
namespace Capital::Expenses {
|
|
/**
|
|
* @brief Константы статусов расходов
|
|
* @ingroup public_consts
|
|
* @ingroup public_capital_consts
|
|
|
|
*/
|
|
namespace Status {
|
|
const eosio::name CREATED = "created"_n; ///< Расход создан
|
|
const eosio::name APPROVED = "approved"_n; ///< Расход одобрен председателем
|
|
const eosio::name AUTHORIZED = "authorized"_n; ///< Расход авторизован советом
|
|
const eosio::name PAID = "paid"_n; ///< Расход оплачен
|
|
const eosio::name DECLINED = "declined"_n; ///< Расход отклонен
|
|
}
|
|
}
|
|
|
|
namespace Capital {
|
|
|
|
/**
|
|
* @brief Таблица расходов хранит информацию о расходах проектов кооператива.
|
|
* @ingroup public_tables
|
|
* @ingroup public_capital_tables
|
|
|
|
* @par Область памяти (scope): coopname
|
|
* @par Имя таблицы (table): expenses
|
|
*/
|
|
struct [[eosio::table, eosio::contract(CAPITAL)]] expense {
|
|
uint64_t id; ///< ID расхода (внутренний ключ)
|
|
name coopname; ///< Имя кооператива
|
|
name username; ///< Имя пользователя, создавшего расход
|
|
|
|
name status = Expenses::Status::CREATED; ///< Статус расхода (created | approved | authorized | paid | declined)
|
|
checksum256 project_hash; ///< Хэш проекта, связанного с расходом
|
|
checksum256 expense_hash; ///< Хэш расхода
|
|
uint64_t fund_id; ///< Идентификатор фонда списания (expfunds в контакте fund)
|
|
eosio::asset amount; ///< Сумма расхода
|
|
std::string description; ///< Публичное описание расхода
|
|
|
|
document2 expense_statement; ///< Служебная записка
|
|
document2 approved_statement; ///< Принятая записка председателем или доверенным
|
|
document2 authorization; ///< Утвержденная записка советом
|
|
|
|
time_point_sec spended_at = current_time_point(); ///< Дата и время создания расхода
|
|
|
|
uint64_t primary_key() const { return id; } ///< Первичный ключ (1)
|
|
uint64_t by_username() const { return username.value; } ///< Индекс по имени пользователя (2)
|
|
checksum256 by_expense_hash() const { return expense_hash; } ///< Индекс по хэшу расхода (3)
|
|
checksum256 by_project_hash() const { return project_hash; } ///< Индекс по хэшу проекта (4)
|
|
};
|
|
|
|
typedef eosio::multi_index<
|
|
"expenses"_n, expense,
|
|
indexed_by<"byusername"_n, const_mem_fun<expense, uint64_t, &expense::by_username>>,
|
|
indexed_by<"byhash"_n, const_mem_fun<expense, checksum256, &expense::by_expense_hash>>,
|
|
indexed_by<"byprojhash"_n, const_mem_fun<expense, checksum256, &expense::by_project_hash>>
|
|
> expense_index;
|
|
|
|
|
|
/**
|
|
* @brief Получает расход по хэшу.
|
|
* @param coopname Имя кооператива (scope таблицы).
|
|
* @param hash Хэш расхода.
|
|
* @return `std::optional<expense>` - найденное действие или `std::nullopt`, если его нет.
|
|
*/
|
|
inline std::optional<expense> get_expense(eosio::name coopname, const checksum256 &hash) {
|
|
expense_index expenses(_capital, coopname.value);
|
|
auto expense_index = expenses.get_index<"byhash"_n>();
|
|
|
|
auto itr = expense_index.find(hash);
|
|
if (itr == expense_index.end()) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
return expense(*itr);
|
|
}
|
|
|
|
} // namespace Capital
|
|
|
|
namespace Capital::Expenses {
|
|
|
|
|
|
/**
|
|
* @brief Получает расход по хэшу или вызывает ошибку.
|
|
* @param coopname Имя кооператива.
|
|
* @param expense_hash Хэш расхода.
|
|
* @return expense - найденный расход.
|
|
*/
|
|
inline expense get_expense_or_fail(eosio::name coopname, const checksum256 &expense_hash) {
|
|
auto expense = Capital::get_expense(coopname, expense_hash);
|
|
eosio::check(expense.has_value(), "Расход с указанным хэшем не найден");
|
|
return expense.value();
|
|
}
|
|
|
|
/**
|
|
* @brief Создает запись расхода в таблице.
|
|
* @param coopname Имя кооператива.
|
|
* @param project_hash Хэш проекта.
|
|
* @param expense_hash Хэш расхода.
|
|
* @param username Исполнитель расхода.
|
|
* @param amount Сумма расхода.
|
|
* @param description Описание.
|
|
* @param statement Служебная записка.
|
|
*/
|
|
inline void create_expense(eosio::name coopname, const checksum256 &project_hash,
|
|
const checksum256 &expense_hash, eosio::name username,
|
|
const eosio::asset &amount,
|
|
const std::string &description, const document2 &statement) {
|
|
|
|
Capital::expense_index expenses(_capital, coopname.value);
|
|
uint64_t expense_id = get_global_id_in_scope(_capital, coopname, "expenses"_n);
|
|
|
|
expenses.emplace(coopname, [&](auto &e) {
|
|
e.id = expense_id;
|
|
e.coopname = coopname;
|
|
e.username = username;
|
|
e.project_hash = project_hash;
|
|
e.expense_hash = expense_hash;
|
|
e.fund_id = Ledger::accounts::ECONOMIC_ACTIVITY_FUND;
|
|
e.status = Expenses::Status::CREATED;
|
|
e.spended_at = current_time_point();
|
|
e.expense_statement = statement;
|
|
e.amount = amount;
|
|
e.description = description;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @brief Обновляет статус и одобренную записку расхода.
|
|
* @param coopname Имя кооператива.
|
|
* @param expense_id ID расхода.
|
|
* @param approved_statement Одобренная записка.
|
|
*/
|
|
inline void set_approved(eosio::name coopname, uint64_t expense_id,
|
|
const document2 &approved_statement) {
|
|
Capital::expense_index expenses(_capital, coopname.value);
|
|
auto expense = expenses.find(expense_id);
|
|
|
|
eosio::check(expense != expenses.end(), "Расход не найден");
|
|
|
|
expenses.modify(expense, coopname, [&](auto &e) {
|
|
e.status = Expenses::Status::APPROVED;
|
|
e.approved_statement = approved_statement;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @brief Обновляет статус и авторизацию расхода.
|
|
* @param coopname Имя кооператива.
|
|
* @param expense_id ID расхода.
|
|
* @param authorization Документ авторизации.
|
|
*/
|
|
inline void set_authorized(eosio::name coopname, uint64_t expense_id,
|
|
const document2 &authorization) {
|
|
Capital::expense_index expenses(_capital, coopname.value);
|
|
auto expense = expenses.find(expense_id);
|
|
eosio::check(expense != expenses.end(), "Расход не найден");
|
|
|
|
expenses.modify(expense, coopname, [&](auto &e) {
|
|
e.status = Expenses::Status::AUTHORIZED;
|
|
e.authorization = authorization;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @brief Удаляет запись расхода из таблицы.
|
|
* @param coopname Имя кооператива.
|
|
* @param expense_id ID расхода.
|
|
*/
|
|
inline void delete_expense(eosio::name coopname, uint64_t expense_id) {
|
|
Capital::expense_index expenses(_capital, coopname.value);
|
|
auto expense = expenses.find(expense_id);
|
|
eosio::check(expense != expenses.end(), "Расход не найден");
|
|
|
|
expenses.erase(expense);
|
|
}
|
|
|
|
} // namespace Capital::Expenses
|
|
|
|
namespace Capital {
|
|
|
|
/**
|
|
* @brief Таблица программных расходов — целевые списания «Благорост»,
|
|
* не привязанные к конкретному проекту.
|
|
* @par Область памяти (scope): coopname
|
|
* @par Имя таблицы (table): progexpenses
|
|
*/
|
|
struct [[eosio::table, eosio::contract(CAPITAL)]] program_expense {
|
|
uint64_t id; ///< Внутренний ключ
|
|
name coopname; ///< Имя кооператива
|
|
name username; ///< Инициатор расхода (председатель / казначей)
|
|
|
|
name status = Expenses::Status::CREATED; ///< Статус (CREATED/APPROVED/AUTHORIZED/PAID/DECLINED — общий enum)
|
|
checksum256 expense_hash; ///< Хеш расхода — анкер процесса p.cap.expns
|
|
uint64_t fund_id; ///< Идентификатор фонда списания
|
|
eosio::asset amount; ///< Сумма расхода
|
|
std::string description; ///< Описание (для отчётности)
|
|
|
|
document2 expense_statement; ///< Служебная записка (ProgramExpenseStatement)
|
|
document2 approved_statement; ///< Записка, принятая председателем
|
|
document2 authorization; ///< Решение совета
|
|
|
|
time_point_sec spended_at = current_time_point();
|
|
|
|
uint64_t primary_key() const { return id; }
|
|
uint64_t by_username() const { return username.value; }
|
|
checksum256 by_expense_hash() const { return expense_hash; }
|
|
};
|
|
|
|
typedef eosio::multi_index<
|
|
"progexpenses"_n, program_expense,
|
|
indexed_by<"byusername"_n, const_mem_fun<program_expense, uint64_t, &program_expense::by_username>>,
|
|
indexed_by<"byhash"_n, const_mem_fun<program_expense, checksum256, &program_expense::by_expense_hash>>
|
|
> program_expense_index;
|
|
|
|
inline std::optional<program_expense> get_program_expense(eosio::name coopname, const checksum256 &hash) {
|
|
program_expense_index t(_capital, coopname.value);
|
|
auto idx = t.get_index<"byhash"_n>();
|
|
auto itr = idx.find(hash);
|
|
if (itr == idx.end()) return std::nullopt;
|
|
return program_expense(*itr);
|
|
}
|
|
|
|
} // namespace Capital
|
|
|
|
namespace Capital::Expenses {
|
|
|
|
inline program_expense get_program_expense_or_fail(eosio::name coopname, const checksum256 &expense_hash) {
|
|
auto v = Capital::get_program_expense(coopname, expense_hash);
|
|
eosio::check(v.has_value(), "Программный расход с указанным хэшем не найден");
|
|
return v.value();
|
|
}
|
|
|
|
inline void create_program_expense(
|
|
eosio::name coopname,
|
|
const checksum256 &expense_hash,
|
|
eosio::name username,
|
|
const eosio::asset &amount,
|
|
const std::string &description,
|
|
const document2 &statement
|
|
) {
|
|
Capital::program_expense_index t(_capital, coopname.value);
|
|
uint64_t id = get_global_id_in_scope(_capital, coopname, "progexpenses"_n);
|
|
|
|
t.emplace(coopname, [&](auto &e) {
|
|
e.id = id;
|
|
e.coopname = coopname;
|
|
e.username = username;
|
|
e.expense_hash = expense_hash;
|
|
e.fund_id = Ledger::accounts::GENERAL_EXPENSES;
|
|
e.status = Status::CREATED;
|
|
e.spended_at = current_time_point();
|
|
e.expense_statement = statement;
|
|
e.amount = amount;
|
|
e.description = description;
|
|
});
|
|
}
|
|
|
|
inline void delete_program_expense(eosio::name coopname, uint64_t id) {
|
|
Capital::program_expense_index t(_capital, coopname.value);
|
|
auto itr = t.find(id);
|
|
eosio::check(itr != t.end(), "Программный расход не найден");
|
|
t.erase(itr);
|
|
}
|
|
|
|
} // namespace Capital::Expenses
|