feat(capital/debt): markdebtoverd + due_at в debtpaycnfrm — статус просрочки
Story 1.3 эпика «Благорост» (blago issue 51F-15). PRD-A3, FR-A11: «срок погашения 3 месяца, дальше — overdue, без блокировки операций». Поле due_at и статус OVERDUE добавлены, helper sweep_overdue вычитан в action. Изменения: - статус OVERDUE — выставляется markdebtoverd для долгов с now > due_at - поле debt.due_at (time_point_sec) — на 3 месяца (DEFAULT_DEBT_TERM_SECONDS = 90 дней) от даты подтверждения выплаты gateway - helper debts.hpp::confirm_paid — атомарный переход pay_pending → paid с проставлением due_at и сбросом last_pay_error - helper debts.hpp::sweep_overdue(coopname, batch_limit) — итерация по debts в scope, перевод paid→overdue для просроченных, не более batch_limit записей - markdebtoverd(coopname) action — backend-cron вызовет раз в сутки (Эпик 4), batch=100 чтобы уложиться в 30s транзакции; повтор до 0 изменений - mark_settled расширен: SETTLED доступен также из OVERDUE - debtpaycnfrm теперь использует confirm_paid (раньше update_debt_status + только PAID, без due_at) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -18,12 +18,9 @@ void capital::debtpaycnfrm(name coopname, checksum256 debt_hash) {
|
||||
auto contributor = Capital::Contributors::get_contributor(coopname, exist_debt.username);
|
||||
eosio::check(contributor.has_value(), "Контрибьютор не найден");
|
||||
|
||||
// Проверяем что долг в статусе 'pay_pending' (outpay в полёте до gateway)
|
||||
eosio::check(exist_debt.status == Capital::Debts::Status::PAY_PENDING,
|
||||
"Долг должен быть в статусе 'pay_pending' для подтверждения оплаты");
|
||||
|
||||
// Обновляем статус долга на PAID
|
||||
Capital::Debts::update_debt_status(coopname, exist_debt.id, Capital::Debts::Status::PAID, _gateway);
|
||||
// Переводим долг в PAID + проставляем due_at = now + 3 месяца (срок погашения).
|
||||
// confirm_paid внутри проверяет статус pay_pending и атомарно сбрасывает last_pay_error.
|
||||
Capital::Debts::confirm_paid(coopname, exist_debt.id, _gateway);
|
||||
|
||||
// Выдача пайщику беспроцентного займа: Dr 58 / Cr 51, ISSUE LOAN_ISSUED (4051).
|
||||
// Семантика момента — деньги ушли пайщику, у кооператива появилось финансовое
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* @brief Помечает просроченные займы статусом OVERDUE.
|
||||
*
|
||||
* Долг считается просроченным, когда current_time_point > debt.due_at
|
||||
* (поле due_at проставляется в debtpaycnfrm на 3 месяца от даты выплаты).
|
||||
* Из OVERDUE долг можно так же закрыть settledebt или signact2 как из PAID.
|
||||
*
|
||||
* Action рассчитан на запуск из backend через @Cron (см. Эпик 4) — раз в сутки.
|
||||
* Один вызов помечает не более 100 записей чтобы уложиться в 30-секундный
|
||||
* лимит транзакции EOSIO; backend повторяет до тех пор, пока action не
|
||||
* помечает 0 — это сигнал, что в текущем scope больше нечего переводить.
|
||||
*
|
||||
* @param coopname Кооператив (scope таблицы debts)
|
||||
* @ingroup public_actions
|
||||
* @ingroup public_capital_actions
|
||||
*
|
||||
* @note Авторизация требуется от аккаунта: @p coopname (председатель / системный).
|
||||
*/
|
||||
void capital::markdebtoverd(name coopname) {
|
||||
require_auth(coopname);
|
||||
|
||||
constexpr uint32_t MAX_PER_BATCH = 100;
|
||||
Capital::Debts::sweep_overdue(coopname, MAX_PER_BATCH);
|
||||
}
|
||||
@@ -93,6 +93,7 @@
|
||||
#include "app/debt_managment/create_debt/debtpaycnfrm.cpp"
|
||||
#include "app/debt_managment/create_debt/debtpaydcln.cpp"
|
||||
#include "app/debt_managment/create_debt/declinedebt.cpp"
|
||||
#include "app/debt_managment/create_debt/markdebtoverd.cpp"
|
||||
#include "app/debt_managment/settle_debt/settledebt.cpp"
|
||||
|
||||
// Управление расходами
|
||||
|
||||
@@ -322,4 +322,7 @@ public:
|
||||
[[eosio::action]]
|
||||
void settledebt(name coopname, checksum256 debt_hash, eosio::asset amount, document2 statement);
|
||||
|
||||
[[eosio::action]]
|
||||
void markdebtoverd(name coopname);
|
||||
|
||||
};
|
||||
|
||||
@@ -20,6 +20,7 @@ namespace Status {
|
||||
constexpr name AUTHORIZED = "authorized"_n; ///< Долг авторизован советом, ожидает повторной отправки на оплату
|
||||
constexpr name PAY_PENDING = "paypending"_n; ///< Долг отправлен в gateway на оплату (исходящий платёж создан, ждём debtpaycnfrm/debtpaydcln)
|
||||
constexpr name PAID = "paid"_n; ///< Долг выплачен пайщику, активный (ждёт погашения)
|
||||
constexpr name OVERDUE = "overdue"_n; ///< Долг просрочен (now > due_at) — выставляется action markdebtoverd
|
||||
constexpr name SETTLED = "settled"_n; ///< Долг полностью погашен (деньгами через settledebt либо результатом через signact2)
|
||||
}
|
||||
|
||||
@@ -45,6 +46,7 @@ struct [[eosio::table, eosio::contract(CAPITAL)]] debt {
|
||||
document2 authorization; ///< Авторизация совета
|
||||
std::string memo; ///< Примечание
|
||||
std::string last_pay_error; ///< Последняя ошибка outpay от gateway (заполняется debtpaydcln; пусто = успех или ещё не отправлен)
|
||||
time_point_sec due_at; ///< Срок погашения (current_time_point + 3 мес. при переходе в paid); now > due_at → overdue
|
||||
|
||||
uint64_t primary_key() const { return id; } ///< Первичный ключ (1)
|
||||
|
||||
@@ -198,6 +200,66 @@ inline void mark_pay_declined(
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Кол-во секунд в стандартном сроке погашения займа (3 месяца ≈ 90 дней).
|
||||
*/
|
||||
constexpr uint32_t DEFAULT_DEBT_TERM_SECONDS = 90 * 24 * 60 * 60;
|
||||
|
||||
/**
|
||||
* @brief Переводит долг в PAID, фиксирует дату выплаты и проставляет due_at.
|
||||
*
|
||||
* Используется в debtpaycnfrm после успешной outpay из gateway. due_at рассчитывается
|
||||
* от current_time_point на 3 месяца вперёд; долги с now > due_at и status == PAID
|
||||
* далее перейдут в OVERDUE через markdebtoverd.
|
||||
*/
|
||||
inline void confirm_paid(
|
||||
eosio::name coopname,
|
||||
uint64_t debt_id,
|
||||
eosio::name payer = name{}
|
||||
) {
|
||||
if (payer == name{}) payer = coopname;
|
||||
|
||||
debts_index debts(_capital, coopname.value);
|
||||
auto debt = debts.find(debt_id);
|
||||
eosio::check(debt != debts.end(), "Долг не найден");
|
||||
eosio::check(debt->status == Status::PAY_PENDING,
|
||||
"Подтверждение оплаты допустимо только из pay_pending");
|
||||
|
||||
auto now = eosio::current_time_point();
|
||||
debts.modify(debt, payer, [&](auto &d) {
|
||||
d.status = Status::PAID;
|
||||
d.due_at = eosio::time_point_sec(now.sec_since_epoch() + DEFAULT_DEBT_TERM_SECONDS);
|
||||
d.last_pay_error = "";
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Помечает долги PAID с now > due_at как OVERDUE в пределах одного batch.
|
||||
*
|
||||
* @param coopname Кооператив (scope)
|
||||
* @param batch_limit Максимальное число записей за вызов (защита от 30-сек лимита транзакции)
|
||||
* @return Сколько долгов помечено за этот вызов; backend-cron повторяет до 0.
|
||||
*/
|
||||
inline uint32_t sweep_overdue(
|
||||
eosio::name coopname,
|
||||
uint32_t batch_limit
|
||||
) {
|
||||
debts_index debts(_capital, coopname.value);
|
||||
auto now = eosio::current_time_point();
|
||||
auto now_sec = eosio::time_point_sec(now.sec_since_epoch());
|
||||
|
||||
uint32_t marked = 0;
|
||||
for (auto it = debts.begin(); it != debts.end() && marked < batch_limit; ++it) {
|
||||
if (it->status == Status::PAID && it->due_at != time_point_sec() && now_sec > it->due_at) {
|
||||
debts.modify(it, coopname, [&](auto &d) {
|
||||
d.status = Status::OVERDUE;
|
||||
});
|
||||
++marked;
|
||||
}
|
||||
}
|
||||
return marked;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Закрывает долг как погашенный — переводит в SETTLED, сохраняет документ-основание
|
||||
* в поле memo (если передан) и фиксирует дату погашения.
|
||||
@@ -216,8 +278,8 @@ inline void mark_settled(
|
||||
debts_index debts(_capital, coopname.value);
|
||||
auto debt = debts.find(debt_id);
|
||||
eosio::check(debt != debts.end(), "Долг не найден");
|
||||
eosio::check(debt->status == Status::PAID,
|
||||
"Погашение допустимо только из статуса paid");
|
||||
eosio::check(debt->status == Status::PAID || debt->status == Status::OVERDUE,
|
||||
"Погашение допустимо только из статуса paid или overdue");
|
||||
|
||||
debts.modify(debt, payer, [&](auto &d) {
|
||||
d.status = Status::SETTLED;
|
||||
|
||||
Reference in New Issue
Block a user