fix(ledger2,wallet): возврат паевого взноса — BURN_BLOCKED из заблокированной суммы
Завершение возврата паевого (o.wal.wthcpl) переключено с TRANSFER (SHARE_FUND_PAY → WITHDRAWALS_SINK) на новый WalletOp::BURN_BLOCKED: заблокированная при createwthd сумма сжигается на кошельке пайщика, получателя на цепи нет — деньги уходят из системы банковским переводом. Чинит assertion 'walletop TRANSFER: недостаточно L3-средств' на completewthd: после REQUEST_WITHDRAW (BLOCK) сумма лежит в L3.blocked, а TRANSFER проверял L3.available. Изменения: - ledger2: WalletOp::BURN_BLOCKED=6 + case в walletop.cpp (списание из L2/L3 blocked). OPERATION_REGISTRY: o.wal.wthcpl с BURN_BLOCKED, без wallet_to. burn_pattern_correct обобщён на оба BURN-варианта. - cooptypes: WalletOp += 'BURN_BLOCKED'; o.wal.wthcpl wallet_to=null. - wallets.hpp: WITHDRAWALS_SINK (w.wal.wthdrw) помечен DEPRECATED — остаётся в реестре для исторических L2-балансов. - standard p.wal.wthdrw: приведён к коду — одностадийный flow (pending → authorized → completed), без approvewthd. - wallet::approvewthd удалён как dead code: action не в whitelist callback'ов, из createwthd создаётся повестка сразу с callback=authwthd. Удалены: .cpp, declaration в wallet.hpp, include в wallet.cpp, struct и action в abi_json/wallet.json. - sov.authpkg.standard.yaml: упоминание approvewthd → authwthd. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @brief Атомарная операция по кошельку (issue/transfer/block/unblock/burn).
|
||||
* @brief Атомарная операция по кошельку (issue/transfer/block/unblock/burn/burn_blocked).
|
||||
*
|
||||
* Внутренний action ledger2 — вызывается только через inline из apply().
|
||||
* Auth: только сам ledger2 (require_auth(get_self())).
|
||||
@@ -56,7 +56,10 @@ void ledger2::walletop(eosio::name coopname,
|
||||
// op_code = 5 (NONE) намеренно не допускается: NONE-операции — это только
|
||||
// бухпроводка без кошелькового движения, apply.cpp не диспатчит для них walletop.
|
||||
// Прямой вызов с op_code=5 был бы no-op и сбил бы инвариант parity.
|
||||
eosio::check(op_code <= 4, "walletop: неизвестный op_code");
|
||||
// Допустимы 0..4 (ISSUE/TRANSFER/BLOCK/UNBLOCK/BURN) и 6 (BURN_BLOCKED).
|
||||
eosio::check(op_code <= static_cast<uint8_t>(WalletOp::BURN_BLOCKED) &&
|
||||
op_code != static_cast<uint8_t>(WalletOp::NONE),
|
||||
"walletop: неизвестный op_code");
|
||||
|
||||
wallets2_index wallets(get_self(), coopname.value);
|
||||
userwallets_index user_wallets(get_self(), coopname.value);
|
||||
@@ -169,7 +172,7 @@ void ledger2::walletop(eosio::name coopname,
|
||||
// миграций 048/049.
|
||||
//
|
||||
// walletop по построению применяет одно и то же `amount` к L2 и L3 (см.
|
||||
// ниже case'ы ISSUE/TRANSFER/BLOCK/UNBLOCK/BURN), а sender-guard на
|
||||
// ниже case'ы ISSUE/TRANSFER/BLOCK/UNBLOCK/BURN/BURN_BLOCKED), а sender-guard на
|
||||
// строке 46-47 запрещает обход. Поэтому инвариант сохраняется по
|
||||
// конструкции; полную сверку выполняет бэкенд («стол бухгалтера»),
|
||||
// вне транзакционного hot path.
|
||||
@@ -298,6 +301,36 @@ void ledger2::walletop(eosio::name coopname,
|
||||
user_wallets.modify(uw_pri, payer, [&](auto& r) { r.available -= amount; });
|
||||
}
|
||||
|
||||
cleanup_l2_if_empty(wallet_from);
|
||||
cleanup_l3_if_empty(wallet_from);
|
||||
break;
|
||||
}
|
||||
case WalletOp::NONE: {
|
||||
// Недостижимо: проверка op_code != NONE стоит на входе action; этот case
|
||||
// нужен только чтобы покрыть enum в switch (-Wswitch).
|
||||
eosio::check(false, "walletop NONE: запрещённый op_code");
|
||||
break;
|
||||
}
|
||||
case WalletOp::BURN_BLOCKED: {
|
||||
eosio::check(wallet_from.value != 0, "walletop BURN_BLOCKED: требуется wallet_from");
|
||||
eosio::check(wallet_to.value == 0, "walletop BURN_BLOCKED: wallet_to должен быть пустым");
|
||||
|
||||
auto it = wallets.find(wallet_from.value);
|
||||
eosio::check(it != wallets.end() && it->blocked >= amount,
|
||||
std::string{"walletop BURN_BLOCKED: недостаточно blocked на кошельке "} +
|
||||
wallet_from.to_string());
|
||||
wallets.modify(it, payer, [&](auto& w) { w.blocked -= amount; });
|
||||
|
||||
if (is_user_shared_l3(wallet_from)) {
|
||||
auto uw = find_l3(wallet_from);
|
||||
eosio::check(uw != user_wallets.get_index<"byuserwallet"_n>().end() &&
|
||||
uw->blocked >= amount,
|
||||
std::string{"walletop BURN_BLOCKED: недостаточно L3-blocked у пайщика "} +
|
||||
username.to_string() + " на " + wallet_from.to_string());
|
||||
auto uw_pri = user_wallets.find(uw->id);
|
||||
user_wallets.modify(uw_pri, payer, [&](auto& r) { r.blocked -= amount; });
|
||||
}
|
||||
|
||||
cleanup_l2_if_empty(wallet_from);
|
||||
cleanup_l3_if_empty(wallet_from);
|
||||
break;
|
||||
|
||||
@@ -61,7 +61,7 @@ namespace operations {
|
||||
// wallet
|
||||
namespace wallet {
|
||||
inline constexpr eosio::name COMPLETE_DEPOSIT = "o.wal.depcpl"_n; ///< Завершение внесения паевого взноса (Dr 51 / Cr 80, ISSUE SHARE_FUND_PAY).
|
||||
inline constexpr eosio::name COMPLETE_WITHDRAW = "o.wal.wthcpl"_n; ///< Завершение возврата паевого взноса (Dr 80 / Cr 51, TRANSFER SHARE_FUND_PAY → WITHDRAWALS_SINK).
|
||||
inline constexpr eosio::name COMPLETE_WITHDRAW = "o.wal.wthcpl"_n; ///< Завершение возврата паевого взноса (Dr 80 / Cr 51, BURN_BLOCKED SHARE_FUND_PAY — деньги уходят из системы, без wallet_to).
|
||||
inline constexpr eosio::name REQUEST_WITHDRAW = "o.wal.wthreq"_n; ///< Запрос на возврат паевого: BLOCK на SHARE_FUND_PAY (без Dr/Cr).
|
||||
inline constexpr eosio::name DECLINE_WITHDRAW = "o.wal.wthdec"_n; ///< Отклонение запроса на возврат: UNBLOCK на SHARE_FUND_PAY (без Dr/Cr).
|
||||
}
|
||||
@@ -130,12 +130,13 @@ namespace operations {
|
||||
* Compile-time правило `(debit==0) ⇔ (credit==0)` ловит смешанные пары.
|
||||
*/
|
||||
enum class WalletOp : uint8_t {
|
||||
ISSUE = 0, ///< первичный вход средств на кошелёк wallet_to (wallet_from = empty)
|
||||
TRANSFER = 1, ///< перемещение wallet_from → wallet_to (с Dr/Cr ИЛИ без — по парам account_id)
|
||||
BLOCK = 2, ///< available-=amount, blocked+=amount на wallet_from
|
||||
UNBLOCK = 3, ///< blocked-=amount, available+=amount на wallet_from
|
||||
BURN = 4, ///< изъятие amount с wallet_from, без wallet_to. Покрывает оба кейса: (a) штатное сжигание как бизнес-операция в OPERATION_REGISTRY; (b) зеркало ISSUE при `ledger2::revert` (различие — через operation_code: `o.adj.rev` для adjustment-mirror).
|
||||
NONE = 5, ///< только бухпроводка без перемещения средств (wallet_from = empty, wallet_to = empty, debit ≠ 0, credit ≠ 0). Покрывает кейсы внутрибалансовых проводок типа Dr 04 / Cr 08 (приём РИД в НМА), когда кошелёк уже на нужном программном фонде.
|
||||
ISSUE = 0, ///< первичный вход средств на кошелёк wallet_to (wallet_from = empty)
|
||||
TRANSFER = 1, ///< перемещение wallet_from → wallet_to (с Dr/Cr ИЛИ без — по парам account_id)
|
||||
BLOCK = 2, ///< available-=amount, blocked+=amount на wallet_from
|
||||
UNBLOCK = 3, ///< blocked-=amount, available+=amount на wallet_from
|
||||
BURN = 4, ///< изъятие amount с wallet_from->available, без wallet_to. Покрывает оба кейса: (a) штатное сжигание как бизнес-операция в OPERATION_REGISTRY; (b) зеркало ISSUE при `ledger2::revert` (различие — через operation_code: `o.adj.rev` для adjustment-mirror).
|
||||
NONE = 5, ///< только бухпроводка без перемещения средств (wallet_from = empty, wallet_to = empty, debit ≠ 0, credit ≠ 0). Покрывает кейсы внутрибалансовых проводок типа Dr 04 / Cr 08 (приём РИД в НМА), когда кошелёк уже на нужном программном фонде.
|
||||
BURN_BLOCKED = 6, ///< изъятие amount из wallet_from->blocked, без wallet_to. Используется при завершении возврата паевого (`o.wal.wthcpl`): средства предварительно заблокированы через BLOCK (REQUEST_WITHDRAW), на завершении сжигаются — выходят из системы (получателя на цепи нет). На L3 синхронно: from_uw->blocked -= amount.
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -144,7 +145,7 @@ enum class WalletOp : uint8_t {
|
||||
* Семантика полей по `wallet_op`:
|
||||
* - ISSUE: wallet_from = eosio::name{}, wallet_to = required.
|
||||
* - TRANSFER: wallet_from = required, wallet_to = required (≠ from).
|
||||
* - BLOCK / UNBLOCK / BURN: wallet_from = required, wallet_to = eosio::name{}.
|
||||
* - BLOCK / UNBLOCK / BURN / BURN_BLOCKED: wallet_from = required, wallet_to = eosio::name{}.
|
||||
*
|
||||
* Семантика бух.проводки:
|
||||
* - Без проводок: debit_account_id == 0 И credit_account_id == 0.
|
||||
@@ -156,7 +157,7 @@ struct OperationRegistryEntry {
|
||||
eosio::name process_type; ///< тип процесса с префиксом `p.<contract>.<noun>`
|
||||
WalletOp wallet_op;
|
||||
eosio::name wallet_from; ///< пустое имя для ISSUE
|
||||
eosio::name wallet_to; ///< пустое имя для BLOCK/UNBLOCK/BURN
|
||||
eosio::name wallet_to; ///< пустое имя для BLOCK/UNBLOCK/BURN/BURN_BLOCKED
|
||||
uint64_t debit_account_id; ///< 0 если без бухпроводки (тогда credit_account_id тоже == 0)
|
||||
uint64_t credit_account_id; ///< 0 если без бухпроводки (тогда debit_account_id тоже == 0)
|
||||
const char* human_name;
|
||||
@@ -183,9 +184,12 @@ static constexpr OperationRegistryEntry OPERATION_REGISTRY[] = {
|
||||
ledger2_accounts::BANK_ACCOUNT, ledger2_accounts::SHARE_FUND,
|
||||
"Внесение пайщиком паевого взноса" },
|
||||
|
||||
// 4. Возврат паевого взноса: Dr 80 / Cr 51, TRANSFER SHARE_FUND_PAY → WITHDRAWALS_SINK
|
||||
{ operations::wallet::COMPLETE_WITHDRAW, processes::wallet::WITHDRAW, WalletOp::TRANSFER,
|
||||
ledger2_wallets::SHARE_FUND_PAY, ledger2_wallets::WITHDRAWALS_SINK,
|
||||
// 4. Возврат паевого взноса: Dr 80 / Cr 51, BURN_BLOCKED SHARE_FUND_PAY.
|
||||
// Сжигание из заблокированной суммы пайщика (BLOCK был на REQUEST_WITHDRAW):
|
||||
// деньги уходят из системы (банковский перевод пайщику), получателя на цепи нет.
|
||||
// Бухгалтерия: паевой фонд уменьшается (Дт 80), расчётный счёт уменьшается (Кт 51).
|
||||
{ operations::wallet::COMPLETE_WITHDRAW, processes::wallet::WITHDRAW, WalletOp::BURN_BLOCKED,
|
||||
ledger2_wallets::SHARE_FUND_PAY, eosio::name{},
|
||||
ledger2_accounts::SHARE_FUND, ledger2_accounts::BANK_ACCOUNT,
|
||||
"Возврат паевого взноса пайщику" },
|
||||
|
||||
@@ -336,7 +340,7 @@ static constexpr size_t OPERATION_REGISTRY_SIZE = sizeof(OPERATION_REGISTRY) / s
|
||||
// оба существуют в `LEDGER2_ACCOUNT_MAP`.
|
||||
// 4. Для TRANSFER: `wallet_from` ≠ `wallet_to`, оба ≠ 0.
|
||||
// 5. Для ISSUE: `wallet_from` == 0 и `wallet_to` ≠ 0.
|
||||
// 6. Для BLOCK / UNBLOCK / BURN: `wallet_from` ≠ 0, `wallet_to` == 0.
|
||||
// 6. Для BLOCK / UNBLOCK / BURN / BURN_BLOCKED: `wallet_from` ≠ 0, `wallet_to` == 0.
|
||||
// 7. Все id кошельков из записей существуют в `LEDGER2_WALLET_REGISTRY`.
|
||||
namespace ledger2_registry_detail {
|
||||
constexpr bool operation_codes_unique() {
|
||||
@@ -380,11 +384,11 @@ namespace ledger2_registry_detail {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Правило 6: BURN — wallet_from required, wallet_to == 0 (ADR-003).
|
||||
// Правило 6: BURN / BURN_BLOCKED — wallet_from required, wallet_to == 0 (ADR-003).
|
||||
constexpr bool burn_pattern_correct() {
|
||||
for (size_t i = 0; i < OPERATION_REGISTRY_SIZE; ++i) {
|
||||
const auto& e = OPERATION_REGISTRY[i];
|
||||
if (e.wallet_op != WalletOp::BURN) continue;
|
||||
if (e.wallet_op != WalletOp::BURN && e.wallet_op != WalletOp::BURN_BLOCKED) continue;
|
||||
if (e.wallet_from.value == 0) return false;
|
||||
if (e.wallet_to.value != 0) return false;
|
||||
}
|
||||
@@ -436,7 +440,7 @@ static_assert(ledger2_registry_detail::dr_ne_cr_when_posting(),
|
||||
static_assert(ledger2_registry_detail::transfer_wallet_from_ne_to(),
|
||||
"OPERATION_REGISTRY: TRANSFER с wallet_from == wallet_to или одним из них == 0");
|
||||
static_assert(ledger2_registry_detail::burn_pattern_correct(),
|
||||
"OPERATION_REGISTRY: BURN требует wallet_from ≠ 0 и wallet_to == 0");
|
||||
"OPERATION_REGISTRY: BURN / BURN_BLOCKED требует wallet_from ≠ 0 и wallet_to == 0");
|
||||
static_assert(ledger2_registry_detail::none_pattern_correct(),
|
||||
"OPERATION_REGISTRY: NONE требует wallet_from == 0, wallet_to == 0 и обе проводки заполненными");
|
||||
static_assert(ledger2_registry_detail::accounts_exist_in_map(),
|
||||
|
||||
@@ -46,7 +46,7 @@ struct ledger2_wallets {
|
||||
// wallet — паевой фонд + возвраты + ЦК
|
||||
static constexpr eosio::name SHARE_FUND_PAY = "w.wal.share"_n; ///< Паевой взнос пайщика (USER_SHARED)
|
||||
static constexpr eosio::name CK_MEMBER = "w.wal.member"_n; ///< ЦК — членская часть пайщика (USER_SHARED)
|
||||
static constexpr eosio::name WITHDRAWALS_SINK = "w.wal.wthdrw"_n; ///< Возвраты паевых взносов пайщикам (sink TRANSFER, COOPERATIVE)
|
||||
static constexpr eosio::name WITHDRAWALS_SINK = "w.wal.wthdrw"_n; ///< DEPRECATED 2026-05-21: после переключения o.wal.wthcpl с TRANSFER на BURN_BLOCKED кошелёк больше не получает новых средств. Оставлен в реестре для исторических L2-балансов (накопленные возвраты до перехода). Не использовать в новых операциях.
|
||||
|
||||
// registrator — минимальный паевой + вступительные
|
||||
static constexpr eosio::name MIN_SHARE_FUND = "w.reg.minshr"_n; ///< Минимальный паевой взнос пайщика (USER_SHARED, без сверки соглашений)
|
||||
@@ -109,7 +109,7 @@ inline constexpr std::array<Ledger2WalletMeta, 14> LEDGER2_WALLET_REGISTRY = {{
|
||||
// ломала convertsegm у пайщиков, чья доля выросла через CRPS.
|
||||
{ ledger2_wallets::GENERATOR_FUND, "ЦПП «Генератор» — единый кошелёк программы", WalletKind::COOPERATIVE },
|
||||
{ ledger2_wallets::ENTRANCE_FEES, "Вступительные взносы", WalletKind::COOPERATIVE },
|
||||
{ ledger2_wallets::WITHDRAWALS_SINK, "Возвраты паевых взносов пайщикам", WalletKind::COOPERATIVE },
|
||||
{ ledger2_wallets::WITHDRAWALS_SINK, "Возвраты паевых взносов пайщикам (deprecated, не используется в новых операциях)", WalletKind::COOPERATIVE },
|
||||
{ ledger2_wallets::INFRA_FEES, "Членские взносы за инфраструктуру кооп. платформы", WalletKind::COOPERATIVE },
|
||||
{ ledger2_wallets::DELEGATE_FEES, "Делегатские членские взносы", WalletKind::COOPERATIVE },
|
||||
{ ledger2_wallets::SOV_EXPENSES, "Хозяйственные расходы из числа целевого финансирования", WalletKind::COOPERATIVE },
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
# ↓
|
||||
# authorized=true → exec → callback в исходный контракт (confirmreg /
|
||||
# authmeet / approvereg / authpgprp / authrslt
|
||||
# / authcontrib / approvewthd / ... — конкретный
|
||||
# / authcontrib / authwthd / ... — конкретный
|
||||
# handler заранее зашит в decision.type).
|
||||
#
|
||||
# Документ повестки приходит из контракта-инициатора (без registry_id —
|
||||
|
||||
@@ -2,13 +2,15 @@
|
||||
# Стандарт «Возврат паевого взноса» — деньгами, через целевую потребительскую
|
||||
# программу «Цифровой Кошелёк» (SHARE_FUND_PAY).
|
||||
#
|
||||
# Пятиактовый процесс с авторизацией советом. Запись заявки живёт в таблице
|
||||
# `withdraws` от создания до выплаты или отказа. На закрывающем действии
|
||||
# completewthd срабатывает одна операция o.wal.wthcpl (TRANSFER w.wal.share → w.wal.wthdrw).
|
||||
# Четырёхактовый процесс с одной авторизацией советом. Запись заявки живёт в
|
||||
# таблице `withdraws` от создания до выплаты или отказа. На закрывающем
|
||||
# действии completewthd срабатывает одна операция o.wal.wthcpl — сжигание
|
||||
# заблокированной суммы (BURN_BLOCKED w.wal.share): деньги уходят из системы
|
||||
# пайщику банковским переводом, получателя на цепи нет.
|
||||
#
|
||||
# Источники правды в коде:
|
||||
# • cpp/wallet/wallet.hpp — actions
|
||||
# • cpp/wallet/src/withdraw/{createwthd,authwthd,approvewthd,completewthd,declinewthd}.cpp
|
||||
# • cpp/wallet/src/withdraw/{createwthd,authwthd,completewthd,declinewthd}.cpp
|
||||
# • cpp/lib/domain/table_wallet_withdraws.hpp — entity withdraws
|
||||
# • cpp/lib/core/ledger2/operations.hpp — o.wal.wthcpl
|
||||
# • cpp/lib/core/ledger2/processes.hpp — processes::wallet::WITHDRAW
|
||||
@@ -22,8 +24,8 @@ slug: withdraw
|
||||
status: proposed
|
||||
contract: wallet
|
||||
summary: >
|
||||
Пайщик получает обратно ранее внесённый паевой взнос деньгами. Процесс
|
||||
авторизуется советом и закрывается выплатой через Gateway.
|
||||
Пайщик получает обратно ранее внесённый паевой взнос деньгами. Заявку
|
||||
авторизует совет, кассир подтверждает выплату через Gateway.
|
||||
purpose: >
|
||||
«Возврат паевого взноса» — пайщик получает обратно ранее внесённые
|
||||
деньги. Заявку рассматривает совет, после авторизации выплата
|
||||
@@ -43,21 +45,14 @@ actions:
|
||||
purpose: >
|
||||
Пайщик создаёт заявку на возврат паевого взноса. Контракт блокирует
|
||||
сумму на кошельке SHARE_FUND_PAY (статус available → blocked) и
|
||||
создаёт повестку в совете о возврате.
|
||||
- name: wallet::approvewthd
|
||||
human: Одобрить решением совета
|
||||
actor: soviet
|
||||
role: progress
|
||||
purpose: >
|
||||
Совет добавляет к заявке документ-решение об одобрении возврата.
|
||||
Заявка получает подписанное решение, но платёж ещё не отправлен.
|
||||
создаёт повестку в совете о возврате с callback'ом авторизации.
|
||||
- name: wallet::authwthd
|
||||
human: Авторизовать выплату
|
||||
actor: soviet
|
||||
role: progress
|
||||
purpose: >
|
||||
Совет авторизует выплату: контракт меняет статус заявки на
|
||||
`authorized` и отправляет в Gateway исходящий платёж с
|
||||
Совет авторизует выплату единым решением: контракт переводит заявку
|
||||
в статус `authorized` и отправляет в Gateway исходящий платёж с
|
||||
callback'ами completewthd / declinewthd.
|
||||
- name: wallet::completewthd
|
||||
human: Подтвердить выплату
|
||||
@@ -65,16 +60,16 @@ actions:
|
||||
role: closer
|
||||
purpose: >
|
||||
Gateway подтвердил списание средств в пользу пайщика. Это закрывающее
|
||||
действие: применяется операция o.wal.wthcpl (TRANSFER w.wal.share → w.wal.wthdrw),
|
||||
разблокированная сумма уходит со счёта 80 на счёт 51, запись заявки
|
||||
удаляется.
|
||||
действие: применяется операция o.wal.wthcpl (BURN_BLOCKED w.wal.share)
|
||||
— заблокированная сумма сжигается, в бухгалтерии проходит обратная
|
||||
проводка Дт 80 / Кт 51. Запись заявки удаляется.
|
||||
- name: wallet::declinewthd
|
||||
human: Отклонить
|
||||
actor: soviet
|
||||
role: reject
|
||||
purpose: >
|
||||
Отказ на любом из этапов до выплаты. Сумма разблокируется на кошельке
|
||||
пайщика, запись заявки удаляется, операция в ledger2 не создаётся.
|
||||
пайщика (UNBLOCK через o.wal.wthdec), запись заявки удаляется.
|
||||
|
||||
# ── Секция 3. Граф состояний ────────────────────────────────────────────────
|
||||
entity: wallet::withdraw
|
||||
@@ -88,12 +83,6 @@ states:
|
||||
Заявка создана, сумма заблокирована на кошельке пайщика (available → blocked),
|
||||
повестка отправлена в совет.
|
||||
kind: normal
|
||||
- name: approved
|
||||
human: Решение совета подписано
|
||||
description: >
|
||||
Совет добавил к заявке документ с положительным решением, но
|
||||
исходящий платёж в Gateway ещё не отправлен.
|
||||
kind: normal
|
||||
- name: authorized
|
||||
human: Выплата отправлена
|
||||
description: >
|
||||
@@ -103,15 +92,15 @@ states:
|
||||
- name: completed
|
||||
human: Возврат выплачен
|
||||
description: >
|
||||
Gateway подтвердил списание, средства переведены с кошелька пайщика
|
||||
на системный кошелёк выплат (w.wal.share → w.wal.wthdrw), в бухгалтерии прошла
|
||||
обратная проводка Дт 80 / Кт 51. Запись заявки удалена.
|
||||
Gateway подтвердил списание, заблокированная сумма сжигается на кошельке
|
||||
пайщика (BURN_BLOCKED w.wal.share). В бухгалтерии прошла обратная
|
||||
проводка Дт 80 / Кт 51. Запись заявки удалена.
|
||||
kind: final
|
||||
- name: removed
|
||||
human: Отклонено
|
||||
description: >
|
||||
Возврат не состоялся: сумма разблокирована, запись удалена,
|
||||
операция в ledger2 не создавалась.
|
||||
Возврат не состоялся: сумма разблокирована (UNBLOCK через o.wal.wthdec),
|
||||
запись удалена.
|
||||
kind: virtual
|
||||
virtual: true
|
||||
|
||||
@@ -120,6 +109,9 @@ transitions:
|
||||
to: pending
|
||||
action: wallet::createwthd
|
||||
actor: contributor
|
||||
ledger_code: o.wal.wthreq
|
||||
operations:
|
||||
- o.wal.wthreq
|
||||
guards:
|
||||
- Пайщик имеет статус active.
|
||||
- Сумма возврата ≤ доступного остатка на SHARE_FUND_PAY (w.wal.share).
|
||||
@@ -127,18 +119,11 @@ transitions:
|
||||
- Заявление подписано ЭЦП пайщика.
|
||||
|
||||
- from: pending
|
||||
to: approved
|
||||
action: wallet::approvewthd
|
||||
actor: soviet
|
||||
guards:
|
||||
- Решение совета по повестке возврата подписано.
|
||||
|
||||
- from: approved
|
||||
to: authorized
|
||||
action: wallet::authwthd
|
||||
actor: soviet
|
||||
guards:
|
||||
- Документ авторизации совета подписан.
|
||||
- Решение совета по повестке возврата подписано и авторизовано.
|
||||
|
||||
- from: authorized
|
||||
to: completed
|
||||
@@ -154,20 +139,19 @@ transitions:
|
||||
to: removed
|
||||
action: wallet::declinewthd
|
||||
actor: soviet
|
||||
ledger_code: o.wal.wthdec
|
||||
operations:
|
||||
- o.wal.wthdec
|
||||
guards:
|
||||
- Совет отклонил возврат либо платёж не прошёл.
|
||||
|
||||
- from: approved
|
||||
to: removed
|
||||
action: wallet::declinewthd
|
||||
actor: soviet
|
||||
guards:
|
||||
- Совет отклонил возврат либо платёж не прошёл.
|
||||
- Совет отклонил возврат.
|
||||
|
||||
- from: authorized
|
||||
to: removed
|
||||
action: wallet::declinewthd
|
||||
actor: gateway_operator
|
||||
ledger_code: o.wal.wthdec
|
||||
operations:
|
||||
- o.wal.wthdec
|
||||
guards:
|
||||
- Платёж не прошёл / отклонён Gateway.
|
||||
|
||||
@@ -182,7 +166,7 @@ scenario:
|
||||
Пайщик оформляет и подписывает заявление о возврате паевого взноса.
|
||||
Контракт создаёт запись в таблице withdraws со статусом `pending`,
|
||||
блокирует сумму на кошельке SHARE_FUND_PAY и направляет в совет
|
||||
повестку о возврате.
|
||||
повестку о возврате с callback'ом авторизации.
|
||||
pre:
|
||||
- Пайщик — активный участник кооператива.
|
||||
- Сумма возврата ≤ доступного остатка.
|
||||
@@ -192,46 +176,34 @@ scenario:
|
||||
- Совет получил повестку о возврате.
|
||||
|
||||
- step: 2
|
||||
title: Одобрение советом
|
||||
actor: soviet
|
||||
action: wallet::approvewthd
|
||||
description: >
|
||||
Совет принимает положительное решение по повестке возврата и
|
||||
добавляет к заявке документ-решение. Статус заявки → `approved`.
|
||||
pre:
|
||||
- Заявка в статусе `pending`.
|
||||
- Решение совета подписано.
|
||||
post:
|
||||
- Статус заявки `approved`, документ-решение сохранён.
|
||||
|
||||
- step: 3
|
||||
title: Авторизация выплаты
|
||||
title: Авторизация выплаты советом
|
||||
actor: soviet
|
||||
action: wallet::authwthd
|
||||
description: >
|
||||
Совет подписывает авторизацию выплаты — контракт инициирует
|
||||
исходящий платёж в Gateway с callback'ами completewthd / declinewthd.
|
||||
Совет принимает положительное решение по повестке возврата и
|
||||
авторизует выплату единым шагом. Контракт инициирует исходящий
|
||||
платёж в Gateway с callback'ами completewthd / declinewthd.
|
||||
Статус заявки → `authorized`.
|
||||
pre:
|
||||
- Заявка в статусе `approved`.
|
||||
- Документ авторизации совета подписан.
|
||||
- Заявка в статусе `pending`.
|
||||
- Решение совета подписано и авторизовано председателем.
|
||||
post:
|
||||
- Статус заявки `authorized`.
|
||||
- В Gateway создан исходящий платёж с callback'ами.
|
||||
|
||||
- step: 4
|
||||
- step: 3
|
||||
title: Подтверждение выплаты
|
||||
actor: gateway_operator
|
||||
action: wallet::completewthd
|
||||
description: >
|
||||
Gateway подтверждает списание средств в пользу пайщика. Контракт
|
||||
применяет ledger2-операцию o.wal.wthcpl (TRANSFER w.wal.share → w.wal.wthdrw с
|
||||
применяет ledger2-операцию o.wal.wthcpl (BURN_BLOCKED w.wal.share с
|
||||
обратной проводкой Дт 80 / Кт 51) и удаляет запись заявки.
|
||||
pre:
|
||||
- Заявка в статусе `authorized`.
|
||||
- Gateway подтвердил списание.
|
||||
post:
|
||||
- Сумма переведена с SHARE_FUND_PAY (w.wal.share) на WITHDRAWALS (w.wal.wthdrw).
|
||||
- Заблокированная сумма сожжена с SHARE_FUND_PAY (w.wal.share).
|
||||
- В ledger2 применена операция o.wal.wthcpl.
|
||||
- Запись заявки удалена.
|
||||
|
||||
@@ -242,8 +214,8 @@ scenario:
|
||||
actor: soviet
|
||||
description: >
|
||||
Совет принял отрицательное решение либо Gateway отклонил платёж.
|
||||
Сумма разблокирована на кошельке пайщика, запись удалена, операция
|
||||
в ledger2 не создаётся.
|
||||
Сумма разблокирована на кошельке пайщика (UNBLOCK через o.wal.wthdec),
|
||||
запись удалена.
|
||||
|
||||
# ── Секция 5. Документы и подписи ───────────────────────────────────────────
|
||||
documents:
|
||||
@@ -254,12 +226,6 @@ documents:
|
||||
stored_in: withdraws.statement
|
||||
|
||||
- step: 2
|
||||
title: Решение совета о возврате паевого взноса (одобрение)
|
||||
registry_id: 901
|
||||
signed_by: [soviet]
|
||||
stored_in: withdraws.approved_statement
|
||||
|
||||
- step: 3
|
||||
title: Решение совета о возврате паевого взноса (авторизация на выплату)
|
||||
registry_id: 901
|
||||
signed_by: [soviet]
|
||||
@@ -267,18 +233,46 @@ documents:
|
||||
|
||||
# ── Секция 6. Операции ──────────────────────────────────────────────────────
|
||||
operations:
|
||||
- ledger_code: o.wal.wthreq
|
||||
human_name: Блокировка паевого под запрос на возврат
|
||||
wallet_op: BLOCK
|
||||
wallet_from: w.wal.share
|
||||
wallet_to: ~
|
||||
debit: ~
|
||||
credit: ~
|
||||
amount_ref: withdraw.quantity
|
||||
triggered_by: wallet::createwthd
|
||||
description: >
|
||||
На создании заявки контракт блокирует сумму на кошельке пайщика
|
||||
(SHARE_FUND_PAY): available → blocked. Бухгалтерская проводка не
|
||||
создаётся — движение внутри одного бухсчёта 80.
|
||||
|
||||
- ledger_code: o.wal.wthdec
|
||||
human_name: Разблокировка паевого после отклонения запроса на возврат
|
||||
wallet_op: UNBLOCK
|
||||
wallet_from: w.wal.share
|
||||
wallet_to: ~
|
||||
debit: ~
|
||||
credit: ~
|
||||
amount_ref: withdraw.quantity
|
||||
triggered_by: wallet::declinewthd
|
||||
description: >
|
||||
Зеркало REQUEST_WITHDRAW: blocked → available на кошельке пайщика.
|
||||
Бухгалтерская проводка не создаётся.
|
||||
|
||||
- ledger_code: o.wal.wthcpl
|
||||
human_name: Возврат паевого взноса пайщику
|
||||
wallet_op: TRANSFER
|
||||
wallet_op: BURN_BLOCKED
|
||||
wallet_from: w.wal.share # ЦПП «Цифровой Кошелёк» — паевые взносы деньгами
|
||||
wallet_to: w.wal.wthdrw # Возвраты паевых взносов пайщикам
|
||||
wallet_to: ~ # деньги уходят из системы, получателя на цепи нет
|
||||
debit: 80 # Паевой фонд (складочный капитал)
|
||||
credit: 51 # Расчётный счёт
|
||||
amount_ref: withdraw.quantity
|
||||
triggered_by: wallet::completewthd
|
||||
description: >
|
||||
Обратная операция к o.wal.depcpl: средства уходят с кошелька пайщика
|
||||
«ЦПП Цифровой Кошелёк» (w.wal.share) на системный кошелёк выплат (w.wal.wthdrw).
|
||||
Обратная операция к o.wal.depcpl: заблокированная сумма сжигается с
|
||||
кошелька пайщика «ЦПП Цифровой Кошелёк» (w.wal.share) — деньги уходят
|
||||
из системы банковским переводом пайщику. На цепи получателя нет.
|
||||
В бухгалтерии прошла проводка Дт 80 / Кт 51 — паевой фонд уменьшился,
|
||||
деньги ушли с расчётного счёта пайщику.
|
||||
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
void wallet::approvewthd(eosio::name coopname, checksum256 withdraw_hash, document2 approved_statement) {
|
||||
require_auth(_soviet);
|
||||
|
||||
auto exist_withdraw = Wallet::get_withdraw(coopname, withdraw_hash);
|
||||
eosio::check(exist_withdraw.has_value(), "Объект возврата паевого взноса не найден");
|
||||
|
||||
Wallet::withdraws_index withdraws(_wallet, coopname.value);
|
||||
auto withdraw = withdraws.find(exist_withdraw -> id);
|
||||
|
||||
withdraws.modify(withdraw, _wallet, [&](auto &w){
|
||||
w.status = "approved"_n;
|
||||
w.approved_statement = approved_statement;
|
||||
});
|
||||
|
||||
//отправляем в совет
|
||||
action(permission_level{ _wallet, "active"_n}, _soviet, "createagenda"_n,
|
||||
std::make_tuple(
|
||||
coopname,
|
||||
withdraw -> username,
|
||||
get_valid_soviet_action("withdraw"_n),
|
||||
withdraw_hash,
|
||||
_wallet,
|
||||
"authwthd"_n,
|
||||
"declinewthd"_n,
|
||||
withdraw -> statement,
|
||||
std::string("")
|
||||
)
|
||||
).send();
|
||||
|
||||
}
|
||||
@@ -6,7 +6,6 @@
|
||||
#include "src/deposit/createdpst.cpp"
|
||||
#include "src/deposit/declinedpst.cpp"
|
||||
|
||||
#include "src/withdraw/approvewthd.cpp"
|
||||
#include "src/withdraw/authwthd.cpp"
|
||||
#include "src/withdraw/completewthd.cpp"
|
||||
#include "src/withdraw/createwthd.cpp"
|
||||
|
||||
@@ -28,7 +28,6 @@ public:
|
||||
[[eosio::action]] void completewthd(COMPLETEWTHD_SIGNATURE);
|
||||
[[eosio::action]] void declinewthd(DECLINEWTHD_SIGNATURE);
|
||||
[[eosio::action]] void authwthd(AUTHWTHD_SIGNATURE);
|
||||
[[eosio::action]] void approvewthd(eosio::name coopname, checksum256 withdraw_hash, document2 approved_statement);
|
||||
|
||||
// программные соглашения (Эпик 2 компонента 48; ADR-008)
|
||||
[[eosio::action]] void signagree(eosio::name coopname, eosio::name username, uint64_t program_id, document2 document, uint64_t draft_id);
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"account_name":"wallet","abi":{"version":"eosio::abi/1.2","types":[],"structs":[{"name":"approvewthd","base":"","fields":[{"name":"coopname","type":"name"},{"name":"withdraw_hash","type":"checksum256"},{"name":"approved_statement","type":"document"}]},{"name":"authwthd","base":"","fields":[{"name":"coopname","type":"name"},{"name":"withdraw_hash","type":"checksum256"}]},{"name":"completedpst","base":"","fields":[{"name":"coopname","type":"name"},{"name":"deposit_hash","type":"checksum256"}]},{"name":"completewthd","base":"","fields":[{"name":"coopname","type":"name"},{"name":"withdraw_hash","type":"checksum256"},{"name":"memo","type":"string"}]},{"name":"createdpst","base":"","fields":[{"name":"coopname","type":"name"},{"name":"username","type":"name"},{"name":"deposit_hash","type":"checksum256"},{"name":"quantity","type":"asset"}]},{"name":"createwthd","base":"","fields":[{"name":"coopname","type":"name"},{"name":"username","type":"name"},{"name":"withdraw_hash","type":"checksum256"},{"name":"quantity","type":"asset"},{"name":"statement","type":"document"}]},{"name":"declinedpst","base":"","fields":[{"name":"coopname","type":"name"},{"name":"deposit_hash","type":"checksum256"},{"name":"reason","type":"string"}]},{"name":"declinewthd","base":"","fields":[{"name":"coopname","type":"name"},{"name":"withdraw_hash","type":"checksum256"},{"name":"reason","type":"string"}]},{"name":"deposit","base":"","fields":[{"name":"id","type":"uint64"},{"name":"coopname","type":"name"},{"name":"username","type":"name"},{"name":"deposit_hash","type":"checksum256"},{"name":"quantity","type":"asset"},{"name":"status","type":"name"},{"name":"created_at","type":"time_point_sec"}]},{"name":"document","base":"","fields":[{"name":"hash","type":"checksum256"},{"name":"public_key","type":"public_key"},{"name":"signature","type":"signature"},{"name":"meta","type":"string"}]},{"name":"migrate","base":"","fields":[]},{"name":"withdraw","base":"","fields":[{"name":"id","type":"uint64"},{"name":"username","type":"name"},{"name":"coopname","type":"name"},{"name":"withdraw_hash","type":"checksum256"},{"name":"status","type":"name"},{"name":"quantity","type":"asset"},{"name":"statement","type":"document"},{"name":"approved_statement","type":"document"},{"name":"authorization","type":"document"},{"name":"created_at","type":"time_point_sec"}]}],"actions":[{"name":"approvewthd","type":"approvewthd","ricardian_contract":""},{"name":"authwthd","type":"authwthd","ricardian_contract":""},{"name":"completedpst","type":"completedpst","ricardian_contract":""},{"name":"completewthd","type":"completewthd","ricardian_contract":""},{"name":"createdpst","type":"createdpst","ricardian_contract":""},{"name":"createwthd","type":"createwthd","ricardian_contract":""},{"name":"declinedpst","type":"declinedpst","ricardian_contract":""},{"name":"declinewthd","type":"declinewthd","ricardian_contract":""},{"name":"migrate","type":"migrate","ricardian_contract":""}],"tables":[{"name":"deposits","index_type":"i64","key_names":[],"key_types":[],"type":"deposit"},{"name":"withdraws","index_type":"i64","key_names":[],"key_types":[],"type":"withdraw"}],"ricardian_clauses":[],"error_messages":[],"abi_extensions":[],"variants":[],"action_results":[]}}
|
||||
{"account_name":"wallet","abi":{"version":"eosio::abi/1.2","types":[],"structs":[{"name":"authwthd","base":"","fields":[{"name":"coopname","type":"name"},{"name":"withdraw_hash","type":"checksum256"}]},{"name":"completedpst","base":"","fields":[{"name":"coopname","type":"name"},{"name":"deposit_hash","type":"checksum256"}]},{"name":"completewthd","base":"","fields":[{"name":"coopname","type":"name"},{"name":"withdraw_hash","type":"checksum256"},{"name":"memo","type":"string"}]},{"name":"createdpst","base":"","fields":[{"name":"coopname","type":"name"},{"name":"username","type":"name"},{"name":"deposit_hash","type":"checksum256"},{"name":"quantity","type":"asset"}]},{"name":"createwthd","base":"","fields":[{"name":"coopname","type":"name"},{"name":"username","type":"name"},{"name":"withdraw_hash","type":"checksum256"},{"name":"quantity","type":"asset"},{"name":"statement","type":"document"}]},{"name":"declinedpst","base":"","fields":[{"name":"coopname","type":"name"},{"name":"deposit_hash","type":"checksum256"},{"name":"reason","type":"string"}]},{"name":"declinewthd","base":"","fields":[{"name":"coopname","type":"name"},{"name":"withdraw_hash","type":"checksum256"},{"name":"reason","type":"string"}]},{"name":"deposit","base":"","fields":[{"name":"id","type":"uint64"},{"name":"coopname","type":"name"},{"name":"username","type":"name"},{"name":"deposit_hash","type":"checksum256"},{"name":"quantity","type":"asset"},{"name":"status","type":"name"},{"name":"created_at","type":"time_point_sec"}]},{"name":"document","base":"","fields":[{"name":"hash","type":"checksum256"},{"name":"public_key","type":"public_key"},{"name":"signature","type":"signature"},{"name":"meta","type":"string"}]},{"name":"migrate","base":"","fields":[]},{"name":"withdraw","base":"","fields":[{"name":"id","type":"uint64"},{"name":"username","type":"name"},{"name":"coopname","type":"name"},{"name":"withdraw_hash","type":"checksum256"},{"name":"status","type":"name"},{"name":"quantity","type":"asset"},{"name":"statement","type":"document"},{"name":"approved_statement","type":"document"},{"name":"authorization","type":"document"},{"name":"created_at","type":"time_point_sec"}]}],"actions":[{"name":"authwthd","type":"authwthd","ricardian_contract":""},{"name":"completedpst","type":"completedpst","ricardian_contract":""},{"name":"completewthd","type":"completewthd","ricardian_contract":""},{"name":"createdpst","type":"createdpst","ricardian_contract":""},{"name":"createwthd","type":"createwthd","ricardian_contract":""},{"name":"declinedpst","type":"declinedpst","ricardian_contract":""},{"name":"declinewthd","type":"declinewthd","ricardian_contract":""},{"name":"migrate","type":"migrate","ricardian_contract":""}],"tables":[{"name":"deposits","index_type":"i64","key_names":[],"key_types":[],"type":"deposit"},{"name":"withdraws","index_type":"i64","key_names":[],"key_types":[],"type":"withdraw"}],"ricardian_clauses":[],"error_messages":[],"abi_extensions":[],"variants":[],"action_results":[]}}
|
||||
@@ -11,11 +11,11 @@
|
||||
*
|
||||
* Идентификаторы кошельков (`wallet_from`/`wallet_to`) — eosio::name с
|
||||
* префиксом `w.<contract>.<waltype>` (см. `./wallets.ts`). Пустая строка
|
||||
* (`""`) — sentinel «кошелёк вне системы» для ISSUE и BURN.
|
||||
* (`""`) — sentinel «кошелёк вне системы» для ISSUE, BURN и BURN_BLOCKED.
|
||||
*/
|
||||
import type { IName } from '../interfaces/ledger2'
|
||||
|
||||
export type WalletOp = 'ISSUE' | 'TRANSFER' | 'BLOCK' | 'UNBLOCK' | 'BURN' | 'NONE'
|
||||
export type WalletOp = 'ISSUE' | 'TRANSFER' | 'BLOCK' | 'UNBLOCK' | 'BURN' | 'BURN_BLOCKED' | 'NONE'
|
||||
|
||||
export interface OperationMeta {
|
||||
/** Машинный идентификатор — eosio::name в контракте. */
|
||||
@@ -34,7 +34,7 @@ export interface OperationMeta {
|
||||
wallet_op: WalletOp | null
|
||||
/** Кошелёк-источник (null для ISSUE и для adjustment-операций). */
|
||||
wallet_from: IName | null
|
||||
/** Кошелёк-приёмник (null для BLOCK/UNBLOCK/BURN и для adjustment-операций). */
|
||||
/** Кошелёк-приёмник (null для BLOCK/UNBLOCK/BURN/BURN_BLOCKED и для adjustment-операций). */
|
||||
wallet_to: IName | null
|
||||
/** Код счёта Дт (null без бухпроводки, ADR-003: ⇔ credit == null). */
|
||||
debit: number | null
|
||||
@@ -84,7 +84,7 @@ export const LEDGER2_OPERATION_REGISTRY: readonly OperationMeta[] = [
|
||||
human_name: 'Разблокировка паевого после отклонения запроса на возврат' },
|
||||
|
||||
{ code: 'o.wal.wthcpl', process_type: 'p.wal.wthdrw', contract: 'wallet',
|
||||
name: 'COMPLETE_WITHDRAW', wallet_op: 'TRANSFER', wallet_from: 'w.wal.share', wallet_to: 'w.wal.wthdrw',
|
||||
name: 'COMPLETE_WITHDRAW', wallet_op: 'BURN_BLOCKED', wallet_from: 'w.wal.share', wallet_to: null,
|
||||
debit: 80, credit: 51,
|
||||
human_name: 'Возврат паевого взноса пайщику' },
|
||||
|
||||
|
||||
Reference in New Issue
Block a user