[C28-29][@ant] feat(contracts): новый контракт expense — 8 actions, шасси расходов
components/contracts/cpp/expense/:
- expense.hpp: 1 таблица proposals (scope=coopname; secondary by_hash/by_username/by_status),
типы item, callback_handler, enum Mechanics/RecipientType/ProposalStatus/ItemStatus.
- expense.cpp: 8 actions (createexp/authexp/declexp/payexp/reportexp/closeexp/returnexp/overspendexp).
- CMakeLists.txt: стандартный add_contract(expense expense expense.cpp).
components/contracts/cpp/lib/consts.hpp:
- _expense = expense_n + добавлен в contracts_whitelist.
components/contracts/CMakeLists.txt:
- add_contract_build(expense).
Принцип v3:
- Один процесс p.exp.expns, 5 ledger2 operation_codes (o.exp.{blgadv,blgdir,advrpt,advret,over}).
- createexp принимает operation_code+source_wallet+callback в payload — контракт агностичен к источнику.
- payexp на DIRECT-механике сразу помечает item REPORTED (BURN blago = один полный шаг).
- payexp на ADVANCE — отдельный reportexp пайщиком после чека.
- closeexp шлёт inline action на сохранённый callback (contract,action,data) если он непустой.
- overspendexp — две последовательные Ledger2::apply (OVERSPEND + ADVANCE_REPORT) в одной транзакции.
This commit is contained in:
@@ -58,6 +58,7 @@ add_contract_build(registrator)
|
||||
add_contract_build(gateway)
|
||||
add_contract_build(marketplace)
|
||||
add_contract_build(apps)
|
||||
add_contract_build(expense)
|
||||
add_contract_build(system)
|
||||
|
||||
# Опции для тестов
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
cmake_minimum_required(VERSION 3.5)
|
||||
|
||||
project(expense)
|
||||
|
||||
find_package(cdt)
|
||||
|
||||
add_contract(expense expense expense.cpp)
|
||||
if(IS_TESTNET)
|
||||
target_compile_definitions(expense PUBLIC IS_TESTNET=1)
|
||||
endif()
|
||||
@@ -0,0 +1,322 @@
|
||||
// expense.cpp — шасси расходов (MVP: Благорост).
|
||||
//
|
||||
// Принцип: контракт-владелец расходных операций, агностичен к программе-источнику.
|
||||
// operation_code передаётся в createexp; callback-handler сохраняется как переменная;
|
||||
// все ledger2-проводки идут через Ledger2::apply на коды из OPERATION_REGISTRY.
|
||||
|
||||
#include "expense.hpp"
|
||||
|
||||
namespace D = ExpenseDomain;
|
||||
|
||||
// ── helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
namespace {
|
||||
|
||||
asset zero_like(const asset& a) { return asset{0, a.symbol}; }
|
||||
|
||||
asset sum_planned(const std::vector<D::item>& items) {
|
||||
if (items.empty()) return asset{0, eosio::symbol{"RUB", 4}};
|
||||
asset total = zero_like(items.front().planned_amount);
|
||||
for (const auto& it : items) total += it.planned_amount;
|
||||
return total;
|
||||
}
|
||||
|
||||
void check_operation_code(eosio::name code) {
|
||||
// На MVP принимаем только зарегистрированные коды expense из ledger2 OPERATION_REGISTRY.
|
||||
eosio::check(
|
||||
code == operations::expense::BLAGO_ADVANCE ||
|
||||
code == operations::expense::BLAGO_DIRECT,
|
||||
"operation_code должен быть o.exp.blgadv или o.exp.blgdir на этапе payexp/createexp"
|
||||
);
|
||||
}
|
||||
|
||||
D::proposals_index get_proposals(eosio::name self, eosio::name coopname) {
|
||||
return D::proposals_index(self, coopname.value);
|
||||
}
|
||||
|
||||
auto find_proposal(D::proposals_index& tbl, const eosio::checksum256& proposal_hash) {
|
||||
auto idx = tbl.get_index<"byhash"_n>();
|
||||
return idx.find(proposal_hash);
|
||||
}
|
||||
|
||||
void send_callback_if_any(const D::callback_handler& cb,
|
||||
const eosio::checksum256& proposal_hash,
|
||||
const eosio::asset& amount,
|
||||
eosio::name self) {
|
||||
if (cb.contract.value == 0) return;
|
||||
eosio::action(
|
||||
eosio::permission_level{self, "active"_n},
|
||||
cb.contract,
|
||||
cb.action,
|
||||
std::make_tuple(proposal_hash, amount, cb.data)
|
||||
).send();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// ── actions ──────────────────────────────────────────────────────────────────
|
||||
|
||||
void expense::createexp(name coopname, name username,
|
||||
checksum256 proposal_hash,
|
||||
name operation_code,
|
||||
name source_wallet,
|
||||
std::vector<D::item> items,
|
||||
D::callback_handler callback,
|
||||
document2 statement) {
|
||||
require_auth(coopname);
|
||||
|
||||
verify_document_or_fail(statement, {username});
|
||||
check_operation_code(operation_code);
|
||||
eosio::check(!items.empty(), "СЗ должен содержать хотя бы один item");
|
||||
eosio::check(ledger2_is_known_wallet(source_wallet),
|
||||
"source_wallet не найден в LEDGER2_WALLET_REGISTRY");
|
||||
|
||||
auto tbl = get_proposals(get_self(), coopname);
|
||||
eosio::check(find_proposal(tbl, proposal_hash) == tbl.get_index<"byhash"_n>().end(),
|
||||
"СЗ с таким proposal_hash уже существует");
|
||||
|
||||
const auto total = sum_planned(items);
|
||||
|
||||
for (auto& it : items) {
|
||||
it.status = static_cast<uint8_t>(D::ItemStatus::APPROVED);
|
||||
it.actual_amount = zero_like(it.planned_amount);
|
||||
}
|
||||
|
||||
tbl.emplace(get_self(), [&](auto& row) {
|
||||
row.id = tbl.available_primary_key();
|
||||
row.proposal_hash = proposal_hash;
|
||||
row.username = username;
|
||||
row.operation_code = operation_code;
|
||||
row.source_wallet = source_wallet;
|
||||
row.status = static_cast<uint8_t>(D::ProposalStatus::CREATED);
|
||||
row.items = items;
|
||||
row.total_planned = total;
|
||||
row.total_actual = zero_like(total);
|
||||
row.callback = callback;
|
||||
row.statement_doc = statement;
|
||||
row.decision_doc = document2{};
|
||||
row.created_at = eosio::current_time_point();
|
||||
row.updated_at = row.created_at;
|
||||
});
|
||||
}
|
||||
|
||||
void expense::authexp(name coopname, checksum256 proposal_hash, document2 decision) {
|
||||
require_auth(coopname);
|
||||
|
||||
auto tbl = get_proposals(get_self(), coopname);
|
||||
auto idx = tbl.get_index<"byhash"_n>();
|
||||
auto it = idx.find(proposal_hash);
|
||||
eosio::check(it != idx.end(), "СЗ не найден");
|
||||
eosio::check(it->status == static_cast<uint8_t>(D::ProposalStatus::CREATED),
|
||||
"Авторизовать можно только СЗ в статусе CREATED");
|
||||
|
||||
verify_document_or_fail(decision);
|
||||
|
||||
idx.modify(it, get_self(), [&](auto& row) {
|
||||
row.decision_doc = decision;
|
||||
row.status = static_cast<uint8_t>(D::ProposalStatus::AUTHORIZED);
|
||||
row.updated_at = eosio::current_time_point();
|
||||
});
|
||||
}
|
||||
|
||||
void expense::declexp(name coopname, checksum256 proposal_hash, std::string reason) {
|
||||
require_auth(coopname);
|
||||
|
||||
auto tbl = get_proposals(get_self(), coopname);
|
||||
auto idx = tbl.get_index<"byhash"_n>();
|
||||
auto it = idx.find(proposal_hash);
|
||||
eosio::check(it != idx.end(), "СЗ не найден");
|
||||
eosio::check(it->status != static_cast<uint8_t>(D::ProposalStatus::CLOSED),
|
||||
"Нельзя отклонить закрытый СЗ");
|
||||
|
||||
idx.modify(it, get_self(), [&](auto& row) {
|
||||
row.status = static_cast<uint8_t>(D::ProposalStatus::DECLINED);
|
||||
row.updated_at = eosio::current_time_point();
|
||||
});
|
||||
}
|
||||
|
||||
void expense::payexp(name coopname, checksum256 proposal_hash, checksum256 item_hash,
|
||||
asset actual_amount) {
|
||||
require_auth(coopname);
|
||||
|
||||
auto tbl = get_proposals(get_self(), coopname);
|
||||
auto idx = tbl.get_index<"byhash"_n>();
|
||||
auto it = idx.find(proposal_hash);
|
||||
eosio::check(it != idx.end(), "СЗ не найден");
|
||||
eosio::check(it->status == static_cast<uint8_t>(D::ProposalStatus::AUTHORIZED) ||
|
||||
it->status == static_cast<uint8_t>(D::ProposalStatus::PARTIALLY_PAID),
|
||||
"Оплата возможна только из статусов AUTHORIZED / PARTIALLY_PAID");
|
||||
|
||||
bool item_found = false;
|
||||
D::Mechanics mech = D::Mechanics::ADVANCE;
|
||||
asset total_after = it->total_actual;
|
||||
|
||||
idx.modify(it, get_self(), [&](auto& row) {
|
||||
for (auto& i : row.items) {
|
||||
if (i.item_hash == item_hash) {
|
||||
eosio::check(i.status == static_cast<uint8_t>(D::ItemStatus::APPROVED),
|
||||
"Item уже оплачен или закрыт");
|
||||
i.actual_amount = actual_amount;
|
||||
i.status = static_cast<uint8_t>(D::ItemStatus::PAID);
|
||||
mech = static_cast<D::Mechanics>(i.mechanics);
|
||||
item_found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
eosio::check(item_found, "item с заданным hash не найден в СЗ");
|
||||
row.total_actual += actual_amount;
|
||||
row.status = static_cast<uint8_t>(D::ProposalStatus::PARTIALLY_PAID);
|
||||
row.updated_at = eosio::current_time_point();
|
||||
total_after = row.total_actual;
|
||||
});
|
||||
|
||||
// ledger2 проводка по operation_code из proposal — выдача аванса или прямая оплата.
|
||||
Ledger2::apply(get_self(), coopname, it->operation_code, actual_amount,
|
||||
it->username, proposal_hash, "expense:payexp");
|
||||
|
||||
// DIRECT — фоновый reportexp (закрываем подотчёт нулевым BURN, без чека от пайщика).
|
||||
if (mech == D::Mechanics::DIRECT) {
|
||||
// Для DIRECT нет ADVANCE_HOLD-кошелька, поэтому advrpt-BURN не применим.
|
||||
// DIRECT считается «закрытым» сразу после payexp: помечаем item REPORTED.
|
||||
idx.modify(it, get_self(), [&](auto& row) {
|
||||
for (auto& i : row.items) {
|
||||
if (i.item_hash == item_hash) {
|
||||
i.status = static_cast<uint8_t>(D::ItemStatus::REPORTED);
|
||||
break;
|
||||
}
|
||||
}
|
||||
row.updated_at = eosio::current_time_point();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void expense::reportexp(name coopname, checksum256 proposal_hash, checksum256 item_hash) {
|
||||
require_auth(coopname);
|
||||
|
||||
auto tbl = get_proposals(get_self(), coopname);
|
||||
auto idx = tbl.get_index<"byhash"_n>();
|
||||
auto it = idx.find(proposal_hash);
|
||||
eosio::check(it != idx.end(), "СЗ не найден");
|
||||
|
||||
asset item_amount = asset{0, it->total_actual.symbol};
|
||||
bool item_found = false;
|
||||
bool all_reported = true;
|
||||
|
||||
idx.modify(it, get_self(), [&](auto& row) {
|
||||
for (auto& i : row.items) {
|
||||
if (i.item_hash == item_hash) {
|
||||
eosio::check(i.status == static_cast<uint8_t>(D::ItemStatus::PAID),
|
||||
"Отчёт возможен только для оплаченных items");
|
||||
eosio::check(static_cast<D::Mechanics>(i.mechanics) == D::Mechanics::ADVANCE,
|
||||
"reportexp применим только к ADVANCE-механике");
|
||||
i.status = static_cast<uint8_t>(D::ItemStatus::REPORTED);
|
||||
item_amount = i.actual_amount;
|
||||
item_found = true;
|
||||
}
|
||||
if (i.status != static_cast<uint8_t>(D::ItemStatus::REPORTED) &&
|
||||
i.status != static_cast<uint8_t>(D::ItemStatus::RETURNED)) {
|
||||
all_reported = false;
|
||||
}
|
||||
}
|
||||
eosio::check(item_found, "item с заданным hash не найден в СЗ");
|
||||
if (all_reported) {
|
||||
row.status = static_cast<uint8_t>(D::ProposalStatus::REPORT_SUBMITTED);
|
||||
}
|
||||
row.updated_at = eosio::current_time_point();
|
||||
});
|
||||
|
||||
// ledger2: BURN ADVANCE_HOLD, без бухпроводки — canal 08/51 уже сделан на blgadv.
|
||||
Ledger2::apply(get_self(), coopname, operations::expense::ADVANCE_REPORT, item_amount,
|
||||
it->username, proposal_hash, "expense:reportexp");
|
||||
}
|
||||
|
||||
void expense::closeexp(name coopname, checksum256 proposal_hash) {
|
||||
require_auth(coopname);
|
||||
|
||||
auto tbl = get_proposals(get_self(), coopname);
|
||||
auto idx = tbl.get_index<"byhash"_n>();
|
||||
auto it = idx.find(proposal_hash);
|
||||
eosio::check(it != idx.end(), "СЗ не найден");
|
||||
eosio::check(it->status == static_cast<uint8_t>(D::ProposalStatus::REPORT_SUBMITTED),
|
||||
"Закрыть можно только СЗ в статусе REPORT_SUBMITTED");
|
||||
|
||||
idx.modify(it, get_self(), [&](auto& row) {
|
||||
row.status = static_cast<uint8_t>(D::ProposalStatus::CLOSED);
|
||||
row.updated_at = eosio::current_time_point();
|
||||
});
|
||||
|
||||
// Callback на финализацию (capital::onexpreport и т.п.) — если был установлен при createexp.
|
||||
send_callback_if_any(it->callback, proposal_hash, it->total_actual, get_self());
|
||||
}
|
||||
|
||||
void expense::returnexp(name coopname, checksum256 proposal_hash, checksum256 item_hash,
|
||||
asset return_amount) {
|
||||
require_auth(coopname);
|
||||
|
||||
auto tbl = get_proposals(get_self(), coopname);
|
||||
auto idx = tbl.get_index<"byhash"_n>();
|
||||
auto it = idx.find(proposal_hash);
|
||||
eosio::check(it != idx.end(), "СЗ не найден");
|
||||
|
||||
bool item_found = false;
|
||||
|
||||
idx.modify(it, get_self(), [&](auto& row) {
|
||||
for (auto& i : row.items) {
|
||||
if (i.item_hash == item_hash) {
|
||||
eosio::check(i.status == static_cast<uint8_t>(D::ItemStatus::PAID),
|
||||
"Возврат возможен только из статуса PAID");
|
||||
eosio::check(return_amount.amount > 0 && return_amount.amount <= i.actual_amount.amount,
|
||||
"return_amount должен быть положительным и не превышать actual_amount");
|
||||
i.actual_amount -= return_amount;
|
||||
i.status = static_cast<uint8_t>(D::ItemStatus::RETURNED);
|
||||
item_found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
eosio::check(item_found, "item с заданным hash не найден в СЗ");
|
||||
row.total_actual -= return_amount;
|
||||
row.updated_at = eosio::current_time_point();
|
||||
});
|
||||
|
||||
// ledger2: TRANSFER ADVANCE_HOLD → BLAGOROST_FUND, Dr 51 / Cr 08.
|
||||
Ledger2::apply(get_self(), coopname, operations::expense::ADVANCE_RETURN, return_amount,
|
||||
it->username, proposal_hash, "expense:returnexp");
|
||||
}
|
||||
|
||||
void expense::overspendexp(name coopname, checksum256 proposal_hash, checksum256 item_hash,
|
||||
asset overspend_amount) {
|
||||
require_auth(coopname);
|
||||
eosio::check(overspend_amount.amount > 0, "overspend_amount должен быть положительным");
|
||||
|
||||
auto tbl = get_proposals(get_self(), coopname);
|
||||
auto idx = tbl.get_index<"byhash"_n>();
|
||||
auto it = idx.find(proposal_hash);
|
||||
eosio::check(it != idx.end(), "СЗ не найден");
|
||||
|
||||
bool item_found = false;
|
||||
|
||||
idx.modify(it, get_self(), [&](auto& row) {
|
||||
for (auto& i : row.items) {
|
||||
if (i.item_hash == item_hash) {
|
||||
eosio::check(i.status == static_cast<uint8_t>(D::ItemStatus::PAID),
|
||||
"Перерасход регистрируется только из статуса PAID");
|
||||
eosio::check(static_cast<D::Mechanics>(i.mechanics) == D::Mechanics::ADVANCE,
|
||||
"overspendexp применим только к ADVANCE-механике");
|
||||
i.actual_amount += overspend_amount;
|
||||
i.status = static_cast<uint8_t>(D::ItemStatus::OVERSPENT);
|
||||
item_found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
eosio::check(item_found, "item с заданным hash не найден в СЗ");
|
||||
row.total_actual += overspend_amount;
|
||||
row.updated_at = eosio::current_time_point();
|
||||
});
|
||||
|
||||
// ledger2: 1) OVERSPEND (выдача доплаты) → 2) ADVANCE_REPORT (закрытие подотчёта).
|
||||
Ledger2::apply(get_self(), coopname, operations::expense::OVERSPEND, overspend_amount,
|
||||
it->username, proposal_hash, "expense:overspend");
|
||||
|
||||
Ledger2::apply(get_self(), coopname, operations::expense::ADVANCE_REPORT, overspend_amount,
|
||||
it->username, proposal_hash, "expense:overspend-report");
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
#pragma once
|
||||
|
||||
#include <eosio/eosio.hpp>
|
||||
#include <eosio/asset.hpp>
|
||||
#include <eosio/crypto.hpp>
|
||||
#include "../lib/index.hpp"
|
||||
|
||||
using namespace eosio;
|
||||
using std::string;
|
||||
|
||||
/**
|
||||
* @defgroup public_expense Контракт EXPENSE
|
||||
* @brief Шасси расходов цифрового кооператива (MVP — Благорост).
|
||||
*
|
||||
* Универсальный контракт: principal storage = СЗ-расход с массивом items;
|
||||
* operation_code из ledger2 OPERATION_REGISTRY передаётся в payload payexp;
|
||||
* callback на финализацию (контракт+action+data) сохраняется при createexp
|
||||
* как переменная — `expense` агностичен к программе-получателю.
|
||||
*
|
||||
* See: components/desktop/extensions/expenses/NAMING-C28-28.md (v3).
|
||||
*/
|
||||
|
||||
namespace ExpenseDomain {
|
||||
|
||||
/**
|
||||
* @brief Способ оплаты item-а в СЗ.
|
||||
* ADVANCE — пайщик-получатель оплачивает, затем приносит чек.
|
||||
* DIRECT — кассир/председатель платит организации напрямую.
|
||||
*/
|
||||
enum class Mechanics : uint8_t {
|
||||
ADVANCE = 0,
|
||||
DIRECT = 1,
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Получатель платежа в item-е.
|
||||
* SELF — сам создатель СЗ.
|
||||
* MEMBER — другой пайщик.
|
||||
* ORG — внешняя организация.
|
||||
*/
|
||||
enum class RecipientType : uint8_t {
|
||||
SELF = 0,
|
||||
MEMBER = 1,
|
||||
ORG = 2,
|
||||
};
|
||||
|
||||
enum class ProposalStatus : uint8_t {
|
||||
CREATED = 0, ///< СЗ создан, ждёт подписи signact1 создателя.
|
||||
AUTHORIZED = 1, ///< СЗ подписан signact2 (совет), готов к оплате.
|
||||
PARTIALLY_PAID = 2, ///< Часть item-ов оплачена.
|
||||
REPORT_SUBMITTED = 3, ///< Отчёт о расходе подан, ждёт авторизации совета.
|
||||
CLOSED = 4, ///< СЗ-отчёт авторизован — расход закрыт.
|
||||
DECLINED = 5, ///< СЗ отклонён до или после авторизации.
|
||||
};
|
||||
|
||||
enum class ItemStatus : uint8_t {
|
||||
APPROVED = 0, ///< Запланирован, ждёт оплаты.
|
||||
PAID = 1, ///< Оплачен (ADVANCE — выдан пайщику; DIRECT — оплачен организации).
|
||||
REPORTED = 2, ///< Закрыт отчётом (чек приложен).
|
||||
RETURNED = 3, ///< Неиспользованный остаток вернули в пул.
|
||||
OVERSPENT = 4, ///< Был перерасход — задокументирован отдельной OVERSPEND op.
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Callback на финализацию — устанавливается при createexp как переменная.
|
||||
*
|
||||
* Контракт expense не знает про capital/blagorost — он просто вызывает inline
|
||||
* action на (contract, action) и прокидывает data наружу. Пустой contract = нет callback.
|
||||
*
|
||||
* Пример: для расхода в РИД-проект Благороста UI/backend заполняет
|
||||
* { "capital"_n, "onexpreport"_n, packed(wip_project_hash) }.
|
||||
*/
|
||||
struct callback_handler {
|
||||
eosio::name contract;
|
||||
eosio::name action;
|
||||
std::vector<char> data;
|
||||
EOSLIB_SERIALIZE(callback_handler, (contract)(action)(data))
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Item — одна строка СЗ-расхода.
|
||||
*/
|
||||
struct item {
|
||||
eosio::checksum256 item_hash;
|
||||
uint8_t mechanics; ///< Mechanics
|
||||
uint8_t recipient_type; ///< RecipientType
|
||||
eosio::name recipient; ///< username (для SELF/MEMBER), либо name-идентификатор (для ORG)
|
||||
std::string description;
|
||||
eosio::asset planned_amount;
|
||||
eosio::asset actual_amount; ///< 0 пока не оплачено / не отчиталось
|
||||
uint8_t status; ///< ItemStatus
|
||||
|
||||
EOSLIB_SERIALIZE(item, (item_hash)(mechanics)(recipient_type)(recipient)(description)(planned_amount)(actual_amount)(status))
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Таблица СЗ-расходов. Scope = coopname.
|
||||
*/
|
||||
struct [[eosio::table, eosio::contract("expense")]] proposal {
|
||||
uint64_t id;
|
||||
eosio::checksum256 proposal_hash;
|
||||
eosio::name username; ///< создатель
|
||||
eosio::name operation_code; ///< из ledger2 OPERATION_REGISTRY (например, o.exp.blgadv)
|
||||
eosio::name source_wallet; ///< семантически — из какого ЦПП/фонда (w.cap.blago, w.sov.expns, …)
|
||||
uint8_t status; ///< ProposalStatus
|
||||
std::vector<item> items;
|
||||
eosio::asset total_planned;
|
||||
eosio::asset total_actual;
|
||||
callback_handler callback; ///< опционально; contract.value == 0 = нет callback
|
||||
document2 statement_doc; ///< type=2010, signact1 создателя
|
||||
document2 decision_doc; ///< type=2011, signact2 совета
|
||||
eosio::time_point_sec created_at;
|
||||
eosio::time_point_sec updated_at;
|
||||
|
||||
uint64_t primary_key() const { return id; }
|
||||
eosio::checksum256 by_hash() const { return proposal_hash; }
|
||||
uint64_t by_username() const { return username.value; }
|
||||
uint64_t by_status() const { return static_cast<uint64_t>(status); }
|
||||
|
||||
EOSLIB_SERIALIZE(proposal,
|
||||
(id)(proposal_hash)(username)(operation_code)(source_wallet)(status)
|
||||
(items)(total_planned)(total_actual)(callback)(statement_doc)(decision_doc)
|
||||
(created_at)(updated_at))
|
||||
};
|
||||
|
||||
using proposals_index = eosio::multi_index<"proposals"_n, proposal,
|
||||
eosio::indexed_by<"byhash"_n, eosio::const_mem_fun<proposal, eosio::checksum256, &proposal::by_hash>>,
|
||||
eosio::indexed_by<"byusername"_n, eosio::const_mem_fun<proposal, uint64_t, &proposal::by_username>>,
|
||||
eosio::indexed_by<"bystatus"_n, eosio::const_mem_fun<proposal, uint64_t, &proposal::by_status>>
|
||||
>;
|
||||
|
||||
} // namespace ExpenseDomain
|
||||
|
||||
/**
|
||||
* @ingroup public_contracts
|
||||
* @brief Шасси расходов — 8 actions, MVP только Благорост.
|
||||
*/
|
||||
class [[eosio::contract("expense")]] expense : public contract {
|
||||
public:
|
||||
using contract::contract;
|
||||
|
||||
/**
|
||||
* @brief Создать и подать СЗ-расход.
|
||||
*
|
||||
* Создание и подача — одна транзакция (slug createexp). На входе: items + источник
|
||||
* (source_wallet + operation_code из OPERATION_REGISTRY) + опц. callback на финализацию.
|
||||
* Подписывает создатель (signact1 statement_doc, type=2010).
|
||||
*/
|
||||
[[eosio::action]]
|
||||
void createexp(name coopname, name username,
|
||||
checksum256 proposal_hash,
|
||||
name operation_code,
|
||||
name source_wallet,
|
||||
std::vector<ExpenseDomain::item> items,
|
||||
ExpenseDomain::callback_handler callback,
|
||||
document2 statement);
|
||||
|
||||
/**
|
||||
* @brief Авторизовать СЗ советом (signact2 decision_doc, type=2011).
|
||||
* После этого расход доступен для оплаты.
|
||||
*/
|
||||
[[eosio::action]]
|
||||
void authexp(name coopname, checksum256 proposal_hash, document2 decision);
|
||||
|
||||
/**
|
||||
* @brief Отклонить СЗ (до или после авторизации).
|
||||
*/
|
||||
[[eosio::action]]
|
||||
void declexp(name coopname, checksum256 proposal_hash, std::string reason);
|
||||
|
||||
/**
|
||||
* @brief Оплатить item — выдача аванса (ADVANCE) или прямая оплата организации (DIRECT).
|
||||
* Контракт зовёт Ledger2::apply(operation_code, actual_amount, ...).
|
||||
* Для DIRECT-механики сразу за payexp вызывает reportexp (фоном).
|
||||
*/
|
||||
[[eosio::action]]
|
||||
void payexp(name coopname, checksum256 proposal_hash, checksum256 item_hash,
|
||||
asset actual_amount);
|
||||
|
||||
/**
|
||||
* @brief Отчёт о расходе (ADVANCE) — пайщик закрывает item чеком.
|
||||
* Зовёт Ledger2::apply(o.exp.advrpt). Когда все items reported — статус proposal
|
||||
* становится REPORT_SUBMITTED.
|
||||
*/
|
||||
[[eosio::action]]
|
||||
void reportexp(name coopname, checksum256 proposal_hash, checksum256 item_hash);
|
||||
|
||||
/**
|
||||
* @brief Закрытие расхода советом — финальный signact2 СЗ-отчёта.
|
||||
* Если у proposal заполнен callback — отправляет inline action на (callback.contract, callback.action, callback.data).
|
||||
*/
|
||||
[[eosio::action]]
|
||||
void closeexp(name coopname, checksum256 proposal_hash);
|
||||
|
||||
/**
|
||||
* @brief Возврат неиспользованного аванса (ADVANCE-остаток).
|
||||
* Зовёт Ledger2::apply(o.exp.advret).
|
||||
*/
|
||||
[[eosio::action]]
|
||||
void returnexp(name coopname, checksum256 proposal_hash, checksum256 item_hash,
|
||||
asset return_amount);
|
||||
|
||||
/**
|
||||
* @brief Доплата при перерасходе (ADVANCE).
|
||||
* Сначала Ledger2::apply(o.exp.over) — выдача доплаты;
|
||||
* сразу за ней Ledger2::apply(o.exp.advrpt) — закрытие подотчёта.
|
||||
* Две последовательные записи в одной транзакции.
|
||||
*/
|
||||
[[eosio::action]]
|
||||
void overspendexp(name coopname, checksum256 proposal_hash, checksum256 item_hash,
|
||||
asset overspend_amount);
|
||||
};
|
||||
@@ -165,6 +165,7 @@ static constexpr uint64_t _capital_program_id = 4;
|
||||
static constexpr eosio::name _ledger = "ledger"_n;
|
||||
static constexpr eosio::name _ledger2 = "ledger2"_n;
|
||||
static constexpr eosio::name _apps = "apps"_n;
|
||||
static constexpr eosio::name _expense = "expense"_n;
|
||||
static constexpr eosio::name _power_account = "eosio.power"_n;
|
||||
static constexpr eosio::name _saving_account = "eosio.saving"_n;
|
||||
|
||||
@@ -193,7 +194,8 @@ static constexpr uint64_t _capital_program_id = 4;
|
||||
"ledger"_n,
|
||||
"ledger2"_n,
|
||||
"capital"_n,
|
||||
"apps"_n
|
||||
"apps"_n,
|
||||
"expense"_n
|
||||
// Добавьте остальные стандартные или пользовательские контракты по необходимости
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user