diff --git a/components/boot/package.json b/components/boot/package.json index 4db9455c764..4d2fac59e5d 100644 --- a/components/boot/package.json +++ b/components/boot/package.json @@ -26,6 +26,7 @@ "boot:remote": "esno src/index.ts boot:remote", "reboot:clean": "cd components/boot/scripts && bash clean_reboot.sh", "reboot:extra": "cd components/boot/scripts && bash extra_reboot.sh", + "reboot:blago": "cd components/boot/scripts && bash blago_reboot.sh", "create-coop": "esno src/index.ts create-coop", "clear": "./scripts/clear.sh", "start": "./scripts/restart.sh", diff --git a/components/boot/scripts/blago_reboot.sh b/components/boot/scripts/blago_reboot.sh new file mode 100755 index 00000000000..f975c93e450 --- /dev/null +++ b/components/boot/scripts/blago_reboot.sh @@ -0,0 +1,40 @@ +#!/bin/bash + +# Полный перезапуск dev-среды для теста Благороста одной командой: +# 1) extra_reboot.sh — чистая цепь + кооператив + совет 5 человек + +# dev-shortcut онбординга capital (extensions.capital: все *_done=true); +# 2) ожидание готовности controller (GraphQL отвечает); +# 3) seed-capital до фазы $SEED_UP_TO (default 04-contributor): программы +# УХД/Благорост + setconfig, проекты, регистрация ant как участника. +# +# Глубину сида можно менять: SEED_UP_TO=08-investments pnpm run reboot:blago + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/../../.." && pwd)" +if [ -f "$ROOT_DIR/.env" ]; then + set -a + # shellcheck disable=SC1091 + source "$ROOT_DIR/.env" + set +a +fi + +bash "$SCRIPT_DIR/extra_reboot.sh" + +# Per-instance адрес controller'а — из корневого .env (API_URL); coopback после +# force-recreate поднимается десятки секунд, seed-фазы без него падают. +GRAPHQL_URL="${API_URL:-http://127.0.0.1:2998/v1/graphql}" +echo "Ждём готовности controller ($GRAPHQL_URL)..." +until curl -s --max-time 3 -o /dev/null -X POST -H 'Content-Type: application/json' \ + --data '{"query":"{__typename}"}' "$GRAPHQL_URL"; do + sleep 3 +done +echo "Controller готов." + +UP_TO="${SEED_UP_TO:-04-contributor}" +echo "Сидируем capital до фазы $UP_TO..." +cd "$SCRIPT_DIR/.." +CONTROLLER_GRAPHQL_URL="$GRAPHQL_URL" pnpm exec esno src/scripts/seed-capital/index.ts --up-to="$UP_TO" + +echo "Готово: среда Благорост поднята (seed до $UP_TO)." diff --git a/components/boot/src/configs/contracts.ts b/components/boot/src/configs/contracts.ts index b4f139d2f65..ec803a4fce9 100644 --- a/components/boot/src/configs/contracts.ts +++ b/components/boot/src/configs/contracts.ts @@ -128,4 +128,9 @@ export default [ path: path.join(userBase, 'apps'), target: 'apps', }, + { + name: 'expense', + path: path.join(userBase, 'expense'), + target: 'expense', + }, ] diff --git a/components/boot/src/configs/index.ts b/components/boot/src/configs/index.ts index 9b3030975ca..2d30081db68 100644 --- a/components/boot/src/configs/index.ts +++ b/components/boot/src/configs/index.ts @@ -154,6 +154,10 @@ export default { name: 'apps', code_permissions_to: ['apps'], }, + { + name: 'expense', + code_permissions_to: ['expense'], + }, // { // name: provider_chairman, // }, diff --git a/components/boot/src/configs/protocol_features/BUILTIN-SAVANNA.json b/components/boot/src/configs/protocol_features/BUILTIN-SAVANNA.json new file mode 100644 index 00000000000..1266a6520bc --- /dev/null +++ b/components/boot/src/configs/protocol_features/BUILTIN-SAVANNA.json @@ -0,0 +1,33 @@ +{ + "protocol_feature_type": "builtin", + "dependencies": [ + "09e86cb0accf8d81c9e85d34bea4b925ae936626d00c984e4691186891f5bc16", + "1a99a59d87e06e09ec5b028a9cbb7749b4a5ad8819004365d02dc4379a8b7241", + "2652f5f96006294109b3dd0bbde63693f55324af452b799ee137a81a905eed25", + "299dcb6af692324b899b39f16d5a530a33062804e41f09dc97e9f156b4476707", + "35c2186cc36f7bb4aeaf4487b36e57039ccf45a9136aa856a5d569ecca55ef2b", + "4a90c00d55454dc5b059055ca213579c6ea856967712a56017487886a4d4cc0f", + "4e7bf348da00a945489b2a681749eb56f5de00b900014e137ddae39f48f69d67", + "4fca8bd82bbd181e714e283f83e1b45d95ca5af40fb89ad3977b653c448f78c2", + "5443fcf88330c586bc0e5f3dee10e7f63c76c00249c87fe4fbf7f38c082006b4", + "63320dd4a58212e4d32d1f58926b73ca33a247326c2a5e9fd39268d2384e011a", + "68dcaa34c0517d19666e6b33add67351d8c5f69e999ca1e37931bc410a297428", + "6bcb40a24e49c26d0a60513b6aeb8551d264e4717f306b81a37a5afb3b47cedc", + "8ba52fe7a3956c5cd3a656a3174b931d3bb2abb45578befc59f283ecd816a405", + "ad9e3d8f650687709fd68f4b90b41f7d825a365b02c23a636cef88ac2ac00c43", + "bcd2a26394b36614fd4894241d3c451ab0f6fd110958c3423073621a70826e99", + "c3a6138c5061cf291310887c0b5c71fcaffeab90d5deb50d3b9e687cead45071", + "d528b9f6e9693f45ed277af93474fd473ce7d831dae2180cca35d907bd10cb40", + "e0fb64b1085cc5538970158d05a009c24e276fb94e1a0bf6a528b48fbc4ff526", + "ef43112c6543b88db2283a2e077278c315ae2c84719a8b25f25cc88565fbea99", + "f0af56d2c5a48d60a4a5b5c903edfb7db3a736a94ed589d0b797df33ff9d3e1d", + "fce57d2331667353a0eac6b4209b67b843a7262a848af0a49a6e2fa9f6584eb4" + ], + "description_digest": "01a8bebb4922c38293a8909668f7d00ba4a21a455c69719ad7dcc33ea1e631c0", + "subjective_restrictions": { + "earliest_allowed_activation_time": "1970-01-01T00:00:00.000", + "preactivation_required": true, + "enabled": true + }, + "builtin_feature_codename": "SAVANNA" +} \ No newline at end of file diff --git a/components/contracts/CMakeLists.txt b/components/contracts/CMakeLists.txt index c7e29c0b988..e0f4263240e 100644 --- a/components/contracts/CMakeLists.txt +++ b/components/contracts/CMakeLists.txt @@ -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) # Опции для тестов diff --git a/components/contracts/cpp/capital/app/expense_managment/program_expenses/createpgexp.cpp b/components/contracts/cpp/capital/app/expense_managment/program_expenses/createpgexp.cpp new file mode 100644 index 00000000000..8b931b49831 --- /dev/null +++ b/components/contracts/cpp/capital/app/expense_managment/program_expenses/createpgexp.cpp @@ -0,0 +1,65 @@ +/** + * @brief Инициировать программный расход через шасси expense. + * + * capital — только инициатор. Резервирует средства в @c program_expense_pool, + * создаёт скелет записи progexpense (status=CREATED, как маркер pending), затем + * шлёт inline action `expense::createexp(...)` с callback handler `{capital, onpgexpdone}`. + * Шасси expense обслуживает весь дальнейший flow (авторизация совета, выдача аванса, + * отчёт, закрытие либо отклонение) и на терминальном переходе шлёт inline `onpgexpdone`. + * + * @param coopname Кооператив + * @param expense_hash Хеш расхода (= proposal_hash в шасси) + * @param creator Инициатор расхода + * @param items Позиции СЗ (см. ExpenseDomain::item); механика оплаты per-item + * @param description Описание для отчётности + * @param statement Документ-заявление (ProgramExpenseStatement) + * + * @ingroup public_actions + * @ingroup public_capital_actions + * + * @note Авторизация требуется от @p coopname. + */ +void capital::createpgexp(name coopname, checksum256 expense_hash, name creator, + std::vector items, + std::string description, document2 statement) { + require_auth(coopname); + + verify_document_or_fail(statement, {creator}); + eosio::check(!items.empty(), "Программный расход должен содержать хотя бы один item"); + + // Сумма расхода — сумма planned_amount всех items. + asset amount = items.front().planned_amount; + amount.amount = 0; + for (const auto &it : items) { + amount += it.planned_amount; + } + Wallet::validate_asset(amount); + eosio::check(amount.amount > 0, "Сумма программного расхода должна быть положительной"); + + auto exist = Capital::get_program_expense(coopname, expense_hash); + eosio::check(!exist.has_value(), "Программный расход с указанным хэшем уже существует"); + + // Резерв в пуле до отправки в шасси — две параллельные заявки не превысят пул. + Capital::State::reserve_program_expense(coopname, amount); + + Capital::Expenses::create_program_expense(coopname, expense_hash, creator, + amount, description, statement); + + // Inline action в шасси expense с callback handler на capital::onpgexpdone. + // data пустой — capital резолвит запись по expense_hash, дополнительный payload не нужен. + ExpenseDomain::callback_handler callback{ + .contract = _capital, + .action = Names::Capital::ON_PROGRAM_EXPENSE_DONE, + .data = std::vector{} + }; + + // Authority — _capital: у capital@eosio.code нет coopname@active, а + // expense::createexp принимает контракты-инициаторы по contracts_whitelist. + eosio::action( + eosio::permission_level{_capital, "active"_n}, + _expense, + Names::External::CREATE_EXPENSE_PROPOSAL, + std::make_tuple(coopname, creator, expense_hash, + ledger2_wallets::PROGRAM_EXPENSE_POOL, items, callback, statement) + ).send(); +} diff --git a/components/contracts/cpp/capital/app/expense_managment/program_expenses/onpgexpdone.cpp b/components/contracts/cpp/capital/app/expense_managment/program_expenses/onpgexpdone.cpp new file mode 100644 index 00000000000..c802dbd55fb --- /dev/null +++ b/components/contracts/cpp/capital/app/expense_managment/program_expenses/onpgexpdone.cpp @@ -0,0 +1,65 @@ +/** + * @brief Callback от шасси expense на финализацию program-расхода. + * + * Шасси expense шлёт его inline (authority — _expense@active) когда закончило + * flow (closeexp или declexp). capital не делает ledger2-проводок — + * они уже сделаны шасси на payexp/reportexp/returnexp/overspendexp. capital + * только обновляет @c program_expense_pool и удаляет запись из @c progexpenses. + * + * status == ExpenseDomain::ProposalStatus::CLOSED (4): + * total_actual <= reserved → consume(total_actual) + release(остаток); + * total_actual > reserved (был overspendexp) → consume(reserved) + + * прямое списание превышения из program_expense_pool. + * status == ExpenseDomain::ProposalStatus::DECLINED (5): + * release(reserved); decline возможен только до оплат (total_actual == 0). + * + * @param coopname Кооператив + * @param expense_hash Хеш расхода (ключ в progexpenses) + * @param status ExpenseDomain::ProposalStatus + * @param total_actual Фактически потраченная сумма + * @param data Произвольный payload от инициатора (не используется на MVP) + * + * @ingroup public_actions + * @ingroup public_capital_actions + * + * @note Авторизация требуется от @p _expense — inline-callback шасси. + */ +void capital::onpgexpdone(name coopname, checksum256 expense_hash, uint8_t status, + eosio::asset total_actual, std::vector data) { + require_auth(_expense); + + auto pgexp = Capital::Expenses::get_program_expense_or_fail(coopname, expense_hash); + const auto reserved = pgexp.amount; + + if (status == static_cast(ExpenseDomain::ProposalStatus::CLOSED)) { + eosio::check(total_actual.symbol == reserved.symbol, + "Символ total_actual не совпадает с зарезервированной суммой"); + eosio::check(total_actual.amount >= 0, + "total_actual не может быть отрицательным"); + + if (total_actual.amount <= reserved.amount) { + if (total_actual.amount > 0) { + Capital::State::consume_program_expense(coopname, total_actual); + } + const asset leftover = reserved - total_actual; + if (leftover.amount > 0) { + Capital::State::release_program_expense(coopname, leftover); + } + } else { + // Перерасход (overspendexp в шасси): резерв списываем полностью, + // превышение — напрямую из свободного пула программных расходов. + Capital::State::consume_program_expense(coopname, reserved); + const asset excess = total_actual - reserved; + Capital::State::spend_program_expense_pool(coopname, excess); + } + } else if (status == static_cast(ExpenseDomain::ProposalStatus::DECLINED)) { + // Шасси отклоняет СЗ только до первой оплаты — резерв возвращается целиком. + eosio::check(total_actual.amount == 0, + "DECLINED с ненулевым total_actual — нарушение инварианта шасси"); + Capital::State::release_program_expense(coopname, reserved); + } else { + eosio::check(false, "onpgexpdone принимает только статусы CLOSED либо DECLINED"); + } + + Capital::Expenses::delete_program_expense(coopname, pgexp.id); +} diff --git a/components/contracts/cpp/capital/app/expense_managment/program_expenses/topupprogexp.cpp b/components/contracts/cpp/capital/app/expense_managment/program_expenses/topupprogexp.cpp new file mode 100644 index 00000000000..5a1e448e977 --- /dev/null +++ b/components/contracts/cpp/capital/app/expense_managment/program_expenses/topupprogexp.cpp @@ -0,0 +1,33 @@ +/** + * @brief Пополняет пул программных расходов из общего инвестиционного пула программы. + * + * Программные расходы — это целевые списания «Благорост», не аллоцированные ни в один проект + * (хозяйственные расходы программы, оплата инфраструктуры программы и т.п.). Прежде чем + * создавать запрос на программный расход, председатель пополняет дедикейтнутый пул + * @c program_expense_pool из общего @c global_available_invest_pool. Помимо счётчиков + * state наполняется КООПЕРАТИВНЫЙ ledger2-кошелёк @c PROGRAM_EXPENSE_POOL (w.cap.pgexp) — + * именно из него шасси expense оплачивает СЗ; личные L3-кошельки пайщиков (w.cap.blago) + * при расходах не изменяются. + * + * @param coopname Кооператив (scope state) + * @param amount Сумма пополнения (символ строго совпадает с программой) + * @ingroup public_actions + * @ingroup public_capital_actions + * + * @note Авторизация требуется от аккаунта: @p coopname (председатель). + */ +void capital::topupprogexp(name coopname, eosio::asset amount) { + require_auth(coopname); + + Capital::State::topup_program_expense_pool(coopname, amount); + + // Кошелёк-резерв пула: ISSUE w.cap.pgexp без бухпроводки (деньги физически + // на 51 с момента взносов — выделяется только назначение под расходы). + // Синтетический process_hash — у пополнения нет естественного анкера. + const uint64_t now_us = eosio::current_time_point().time_since_epoch().count(); + const std::string anchor = coopname.to_string() + ":pgtopup:" + std::to_string(now_us); + const checksum256 process_hash = eosio::sha256(anchor.c_str(), anchor.size()); + + Ledger2::apply(_capital, coopname, operations::capital::PROGRAM_EXPENSE_TOPUP, amount, + name{}, process_hash, "capital:topupprogexp"); +} diff --git a/components/contracts/cpp/capital/capital.cpp b/components/contracts/cpp/capital/capital.cpp index 3d35ba9d9d5..79b4d0b9b37 100644 --- a/components/contracts/cpp/capital/capital.cpp +++ b/components/contracts/cpp/capital/capital.cpp @@ -95,6 +95,12 @@ #include "app/debt_managment/create_debt/declinedebt.cpp" #include "app/debt_managment/settle_debt/settledebt.cpp" +// Управление программными расходами через шасси expense. +// capital — инициатор + получатель callback, шасси обслуживает весь flow. +#include "app/expense_managment/program_expenses/topupprogexp.cpp" +#include "app/expense_managment/program_expenses/createpgexp.cpp" +#include "app/expense_managment/program_expenses/onpgexpdone.cpp" + // Управление расходами #include "app/expense_managment/create_expense/approveexpns.cpp" #include "app/expense_managment/create_expense/capauthexpns.cpp" diff --git a/components/contracts/cpp/capital/capital.hpp b/components/contracts/cpp/capital/capital.hpp index 02c8827db4c..3bf111ae226 100644 --- a/components/contracts/cpp/capital/capital.hpp +++ b/components/contracts/cpp/capital/capital.hpp @@ -48,6 +48,7 @@ constexpr uint128_t CRPS_PRECISION_FACTOR = 100000000000000ULL; ///< Масшт #include #include "../lib/index.hpp" #include "domain/index.hpp" +#include "../expense/expense.hpp" // ExpenseDomain::item / callback_handler — для inline-action в шасси using namespace eosio; @@ -322,4 +323,38 @@ public: [[eosio::action]] void settledebt(name coopname, name username, eosio::asset amount, document2 statement); + // Программные расходы через шасси expense. + // capital — инициатор + получатель callback, шасси держит весь flow расхода. + + [[eosio::action]] + void topupprogexp(name coopname, eosio::asset amount); + + /** + * @brief Инициировать программный расход через шасси expense. + * Резервирует amount в program_expense_pool, создаёт запись progexpense, + * шлёт inline action expense::createexp с callback={capital, onpgexpdone}. + * Шасси обслуживает весь flow (auth/pay/report/close|decline) и шлёт callback. + * + * items[] — позиции СЗ (см. ExpenseDomain::item); механика оплаты per-item + * (item.mechanics: ADVANCE — аванс, DIRECT — прямая оплата). + */ + [[eosio::action]] + void createpgexp(name coopname, checksum256 expense_hash, name creator, + std::vector items, + std::string description, document2 statement); + + /** + * @brief Callback от шасси expense на финализацию program-расхода. + * Шасси шлёт его inline (authority — _expense@active, require_auth(_expense)) + * после терминального перехода flow (CLOSED либо DECLINED). + * + * status == CLOSED → consume(total_actual) + release(остаток резерва); + * при total_actual > reserved (перерасход) превышение + * списывается напрямую из program_expense_pool. + * status == DECLINED → release(reserved); decline возможен только до оплат. + */ + [[eosio::action]] + void onpgexpdone(name coopname, checksum256 expense_hash, uint8_t status, + eosio::asset total_actual, std::vector data); + }; diff --git a/components/contracts/cpp/capital/domain/entities/expenses.hpp b/components/contracts/cpp/capital/domain/entities/expenses.hpp index b7c1df2bf19..c88e4921607 100644 --- a/components/contracts/cpp/capital/domain/entities/expenses.hpp +++ b/components/contracts/cpp/capital/domain/entities/expenses.hpp @@ -178,8 +178,96 @@ namespace Capital::Expenses { 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>, + indexed_by<"byhash"_n, const_mem_fun> + > program_expense_index; + + inline std::optional 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 \ No newline at end of file diff --git a/components/contracts/cpp/capital/domain/entities/global_state.hpp b/components/contracts/cpp/capital/domain/entities/global_state.hpp index d58395140fe..9357b8a5a6f 100644 --- a/components/contracts/cpp/capital/domain/entities/global_state.hpp +++ b/components/contracts/cpp/capital/domain/entities/global_state.hpp @@ -39,8 +39,11 @@ namespace Capital { asset program_membership_distributed = asset(0, _root_govern_symbol); ///< Распределенная сумма членских взносов по программе double program_membership_cumulative_reward_per_share; ///< Накопительное вознаграждение на долю в членских взносах + asset program_expense_pool = asset(0, _root_govern_symbol); ///< Доступная сумма программы под целевые расходы (без аллокации в проекты) + asset program_expense_reserved = asset(0, _root_govern_symbol); ///< Сумма, зарезервированная под approved/authorized программные расходы + config config; ///< Управляемая конфигурация контракта - + uint64_t primary_key() const { return coopname.value; } ///< Первичный ключ (1) }; @@ -78,7 +81,7 @@ inline global_state get_global_state(name coopname) { /** * @brief Проверяет что контракт инициализирован. - * + * * @param coopname Имя кооператива. */ inline void is_initialized(name coopname) { @@ -86,6 +89,96 @@ inline void is_initialized(name coopname) { auto itr = global_state_inst.find(coopname.value); eosio::check(itr != global_state_inst.end(), "Контракт не инициализирован"); } + +/** + * @brief Пополняет пул программных расходов на @p amount из общего пула инвестиций программы. + * Вызывается из action topupprogexp (председатель). + */ +inline void topup_program_expense_pool(eosio::name coopname, const asset &amount) { + global_state_table global_state_inst(_capital, _capital.value); + auto itr = global_state_inst.find(coopname.value); + eosio::check(itr != global_state_inst.end(), "Контракт не инициализирован"); + eosio::check(amount.is_valid() && amount.amount > 0, "Сумма пополнения должна быть положительной"); + eosio::check(itr->global_available_invest_pool >= amount, + "Недостаточно свободных инвестиций программы для пополнения пула расходов"); + + global_state_inst.modify(itr, _capital, [&](auto &s) { + s.global_available_invest_pool -= amount; + s.program_expense_pool += amount; + }); +} + +/** + * @brief Резервирует средства под approved/authorized программный расход: + * program_expense_pool -= amount, program_expense_reserved += amount. + */ +inline void reserve_program_expense(eosio::name coopname, const asset &amount) { + global_state_table global_state_inst(_capital, _capital.value); + auto itr = global_state_inst.find(coopname.value); + eosio::check(itr != global_state_inst.end(), "Контракт не инициализирован"); + eosio::check(amount.is_valid() && amount.amount > 0, "Сумма резерва должна быть положительной"); + eosio::check(itr->program_expense_pool >= amount, + "Недостаточно средств в пуле программных расходов"); + + global_state_inst.modify(itr, _capital, [&](auto &s) { + s.program_expense_pool -= amount; + s.program_expense_reserved += amount; + }); +} + +/** + * @brief Освобождает зарезервированное при отклонении программного расхода: + * program_expense_reserved -= amount, program_expense_pool += amount. + */ +inline void release_program_expense(eosio::name coopname, const asset &amount) { + global_state_table global_state_inst(_capital, _capital.value); + auto itr = global_state_inst.find(coopname.value); + eosio::check(itr != global_state_inst.end(), "Контракт не инициализирован"); + eosio::check(amount.is_valid() && amount.amount > 0, "Сумма освобождения должна быть положительной"); + eosio::check(itr->program_expense_reserved >= amount, + "Зарезервированных программных расходов меньше указанной суммы"); + + global_state_inst.modify(itr, _capital, [&](auto &s) { + s.program_expense_reserved -= amount; + s.program_expense_pool += amount; + }); +} + +/** + * @brief Окончательное списание программного расхода (callback onpgexpdone, CLOSED): + * program_expense_reserved -= amount. Деньги уже ушли через шасси expense, + * ledger2-проводки сделаны на payexp/reportexp/returnexp. + */ +inline void consume_program_expense(eosio::name coopname, const asset &amount) { + global_state_table global_state_inst(_capital, _capital.value); + auto itr = global_state_inst.find(coopname.value); + eosio::check(itr != global_state_inst.end(), "Контракт не инициализирован"); + eosio::check(amount.is_valid() && amount.amount > 0, "Сумма списания должна быть положительной"); + eosio::check(itr->program_expense_reserved >= amount, + "Зарезервированных программных расходов меньше указанной суммы"); + + global_state_inst.modify(itr, _capital, [&](auto &s) { + s.program_expense_reserved -= amount; + }); +} + +/** + * @brief Прямое списание из свободного пула программных расходов — превышение + * факта над резервом при закрытии расхода с перерасходом (overspendexp + * в шасси выдал доплату из BLAGOROST_FUND сверх зарезервированного). + */ +inline void spend_program_expense_pool(eosio::name coopname, const asset &amount) { + global_state_table global_state_inst(_capital, _capital.value); + auto itr = global_state_inst.find(coopname.value); + eosio::check(itr != global_state_inst.end(), "Контракт не инициализирован"); + eosio::check(amount.is_valid() && amount.amount > 0, "Сумма списания должна быть положительной"); + eosio::check(itr->program_expense_pool >= amount, + "Недостаточно средств в пуле программных расходов для покрытия перерасхода — пополните пул (topupprogexp)"); + + global_state_inst.modify(itr, _capital, [&](auto &s) { + s.program_expense_pool -= amount; + }); +} }// namespace State }// namespace Capital \ No newline at end of file diff --git a/components/contracts/cpp/expense/CMakeLists.txt b/components/contracts/cpp/expense/CMakeLists.txt new file mode 100644 index 00000000000..c5b8d6ca0ac --- /dev/null +++ b/components/contracts/cpp/expense/CMakeLists.txt @@ -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() diff --git a/components/contracts/cpp/expense/expense.cpp b/components/contracts/cpp/expense/expense.cpp new file mode 100644 index 00000000000..b0ad286775a --- /dev/null +++ b/components/contracts/cpp/expense/expense.cpp @@ -0,0 +1,425 @@ +// expense.cpp — шасси расходов (первый потребитель: Благорост). +// +// Принцип: контракт-владелец расходных операций, агностичен к программе-источнику. +// Кошелёк-пул приходит в createexp параметром source_wallet; ledger2-коды всех +// операций жизненного цикла выводятся из него через EXPENSE_OPERATION_SETS +// (фабричная настройка — подключение нового пула не меняет этот контракт). +// Механика оплаты per-item (ADVANCE | DIRECT) выбирает код внутри набора; +// callback-handler сохраняется как переменная. + +#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& 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; +} + +// Набор ledger2-кодов операций шасси по кошельку-источнику СЗ. Один источник = +// один набор (EXPENSE_OPERATION_SETS в ledger2) — в коде expense нет +// захардкоженных operation_code, контракт настраивается на любой пул. +const ExpenseOperationSet& expense_ops_for(eosio::name source_wallet) { + const auto* set = find_expense_operation_set(source_wallet); + eosio::check(set != nullptr, + "Для кошелька-источника не настроен набор операций шасси расходов (EXPENSE_OPERATION_SETS)"); + return *set; +} + +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, + eosio::name coopname, + const eosio::checksum256& proposal_hash, + uint8_t status, + const eosio::asset& amount, + eosio::name self) { + if (cb.contract.value == 0) return; + // Payload: (coopname, proposal_hash, status, total_actual, data). + // status = ExpenseDomain::ProposalStatus — инициатор различает CLOSED vs DECLINED. + // coopname нужен инициатору для скоупа таблиц (capital::progexpenses scope=coopname). + eosio::action( + eosio::permission_level{self, "active"_n}, + cb.contract, + cb.action, + std::make_tuple(coopname, proposal_hash, status, amount, cb.data) + ).send(); +} + +} // namespace + +// ── actions ────────────────────────────────────────────────────────────────── + +void expense::createexp(name coopname, name username, + checksum256 proposal_hash, + name source_wallet, + std::vector items, + D::callback_handler callback, + document2 statement) { + // Прямой вызов backend'а — авторизация кооператива; inline от контракта-инициатора + // (capital::createpgexp и т.п.) — авторизация по whitelist, как в ledger2::apply. + if (!has_auth(coopname)) { + check_auth_and_get_payer_or_fail(contracts_whitelist); + } + + verify_document_or_fail(statement, {username}); + eosio::check(!items.empty(), "СЗ должен содержать хотя бы один item"); + eosio::check(ledger2_is_known_wallet(source_wallet), + "source_wallet не найден в LEDGER2_WALLET_REGISTRY"); + // Источник определяет ledger2-коды всех операций жизненного цикла — + // отказываем сразу при создании, а не в момент оплаты. + expense_ops_for(source_wallet); + + auto tbl = get_proposals(get_self(), coopname); + eosio::check(find_proposal(tbl, proposal_hash) == tbl.get_index<"byhash"_n>().end(), + "СЗ с таким proposal_hash уже существует"); + + // Механика оплаты задаётся на каждом item отдельно (ADVANCE | DIRECT) — + // в одном СЗ допустимо смешение: ledger2-проводка payexp идёт по-позиционно + // с кодом, выведенным из механики конкретного item'а. + for (auto& it : items) { + eosio::check(it.planned_amount.is_valid() && it.planned_amount.amount > 0, + "planned_amount каждого item должен быть положительным"); + eosio::check(it.mechanics <= static_cast(D::Mechanics::DIRECT), + "Неизвестная mechanics item (0 = ADVANCE, 1 = DIRECT)"); + eosio::check(it.recipient_type <= static_cast(D::RecipientType::ORG), + "Неизвестный recipient_type item"); + + // Инвариант: пайщик (SELF/MEMBER) получает только аванс под отчёт на личные + // реквизиты (расходом станет после его отчёта чеком); организация — только + // прямую оплату по выставленным реквизитам, аванс ей не выдаётся. + if (it.recipient_type == static_cast(D::RecipientType::ORG)) { + eosio::check(it.mechanics == static_cast(D::Mechanics::DIRECT), + "Организации/ИП доступна только оплата по счёту (DIRECT)"); + } else { + eosio::check(it.mechanics == static_cast(D::Mechanics::ADVANCE), + "Пайщику средства выдаются только авансом под отчёт (ADVANCE)"); + eosio::check(it.recipient != name(), + "recipient обязателен для получателя-пайщика"); + eosio::check(is_account(it.recipient), + "Аккаунт получателя-пайщика не существует"); + } + + it.status = static_cast(D::ItemStatus::APPROVED); + it.actual_amount = zero_like(it.planned_amount); + } + + const auto total = sum_planned(items); + + tbl.emplace(get_self(), [&](auto& row) { + row.id = tbl.available_primary_key(); + row.coopname = coopname; + row.proposal_hash = proposal_hash; + row.username = username; + row.source_wallet = source_wallet; + row.status = static_cast(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; + }); + + // СЗ утверждает совет (как возвраты паевых взносов): ставим вопрос в повестку. + // Утверждение председателем после голосования → callback authexp от _soviet; + // отклонение (отрицательный консенсус / просрочка) → declexp с причиной. + ::Soviet::create_agenda( + _expense, + coopname, + username, + get_valid_soviet_action("createexp"_n), + proposal_hash, + _expense, + "authexp"_n, + "declexp"_n, + statement, + std::string("") + ); +} + +void expense::authexp(name coopname, checksum256 proposal_hash, document2 decision) { + require_auth(_soviet); + + 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(D::ProposalStatus::CREATED), + "Авторизовать можно только СЗ в статусе CREATED"); + + verify_document_or_fail(decision); + + idx.modify(it, get_self(), [&](auto& row) { + row.decision_doc = decision; + row.status = static_cast(D::ProposalStatus::AUTHORIZED); + row.updated_at = eosio::current_time_point(); + }); +} + +void expense::declexp(name coopname, checksum256 proposal_hash, std::string reason) { + require_auth(_soviet); + + 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(), "СЗ не найден"); + // Отклонение возможно только пока ни один item не оплачен: после payexp деньги + // уже ушли через ledger2, и decline разъехался бы с учётом инициатора + // (callback DECLINED возвращает инициатору весь резерв). Частично исполненный + // СЗ завершается обычным путём: reportexp / returnexp → closeexp. + eosio::check(it->status == static_cast(D::ProposalStatus::CREATED) || + it->status == static_cast(D::ProposalStatus::AUTHORIZED), + "Отклонить можно только СЗ без оплат (статусы CREATED / AUTHORIZED)"); + + idx.modify(it, get_self(), [&](auto& row) { + row.status = static_cast(D::ProposalStatus::DECLINED); + row.updated_at = eosio::current_time_point(); + }); + + // Callback инициатору (capital::onpgexpdone и т.п.); total_actual здесь всегда 0 — + // оплат до decline не было (см. проверку статусов выше). + send_callback_if_any(it->callback, coopname, proposal_hash, + static_cast(D::ProposalStatus::DECLINED), + it->total_actual, get_self()); +} + +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(D::ProposalStatus::AUTHORIZED) || + it->status == static_cast(D::ProposalStatus::PARTIALLY_PAID), + "Оплата возможна только из статусов AUTHORIZED / PARTIALLY_PAID"); + eosio::check(actual_amount.is_valid() && actual_amount.amount > 0, + "actual_amount должен быть положительным"); + + bool item_found = false; + uint8_t paid_mechanics = 0; + name paid_recipient{}; + + idx.modify(it, get_self(), [&](auto& row) { + bool all_reported = true; + for (auto& i : row.items) { + if (i.item_hash == item_hash) { + eosio::check(i.status == static_cast(D::ItemStatus::APPROVED), + "Item уже оплачен или закрыт"); + eosio::check(actual_amount.symbol == i.planned_amount.symbol, + "Символ actual_amount не совпадает с планом item"); + eosio::check(actual_amount.amount <= i.planned_amount.amount, + "Сумма оплаты не может превышать план item; доплата сверх плана — через overspendexp"); + i.actual_amount = actual_amount; + // DIRECT не имеет фазы подотчёта (нет ADVANCE_HOLD): закрывающие документы + // прикладывает сам кассир, item считается отчитанным сразу после оплаты. + i.status = static_cast(i.mechanics) == D::Mechanics::DIRECT + ? static_cast(D::ItemStatus::REPORTED) + : static_cast(D::ItemStatus::PAID); + paid_mechanics = i.mechanics; + paid_recipient = i.recipient; + item_found = true; + } + if (i.status != static_cast(D::ItemStatus::REPORTED)) { + all_reported = false; + } + } + eosio::check(item_found, "item с заданным hash не найден в СЗ"); + row.total_actual += actual_amount; + // DIRECT-only СЗ становится готов к закрытию сразу после последней оплаты. + row.status = all_reported + ? static_cast(D::ProposalStatus::REPORT_SUBMITTED) + : static_cast(D::ProposalStatus::PARTIALLY_PAID); + row.updated_at = eosio::current_time_point(); + }); + + // ledger2-проводка по механике оплаченного item'а — выдача аванса или прямая + // оплата; коды выводятся из кошелька-источника СЗ. СЗ может смешивать механики. + // username нужен ledger2 только для USER_SHARED-стороны (кошелёк-подотчёт) — + // это ПОЛУЧАТЕЛЬ аванса, не автор СЗ. + const auto& ops = expense_ops_for(it->source_wallet); + const auto code = static_cast(paid_mechanics) == D::Mechanics::ADVANCE + ? ops.advance + : ops.direct; + Ledger2::apply(get_self(), coopname, code, actual_amount, + paid_recipient, proposal_hash, "expense:payexp"); +} + +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(), "СЗ не найден"); + eosio::check(it->status == static_cast(D::ProposalStatus::PARTIALLY_PAID), + "Отчёт возможен только по СЗ в статусе PARTIALLY_PAID"); + + asset item_amount = asset{0, it->total_actual.symbol}; + bool item_found = false; + name item_recipient{}; + + idx.modify(it, get_self(), [&](auto& row) { + bool all_reported = true; + for (auto& i : row.items) { + if (i.item_hash == item_hash) { + eosio::check(i.status == static_cast(D::ItemStatus::PAID), + "Отчёт возможен только для оплаченных items"); + eosio::check(static_cast(i.mechanics) == D::Mechanics::ADVANCE, + "reportexp применим только к ADVANCE-механике"); + i.status = static_cast(D::ItemStatus::REPORTED); + item_amount = i.actual_amount; + item_recipient = i.recipient; + item_found = true; + } + if (i.status != static_cast(D::ItemStatus::REPORTED)) { + all_reported = false; + } + } + eosio::check(item_found, "item с заданным hash не найден в СЗ"); + if (all_reported) { + row.status = static_cast(D::ProposalStatus::REPORT_SUBMITTED); + } + row.updated_at = eosio::current_time_point(); + }); + + // ledger2: BURN кошелька-подотчёта на остаточный actual item'а (выдано − + // возвращено + доплачено), без бухпроводки — проводка уже сделана при выдаче. + // Подотчёт числится на получателе аванса — его username и закрывает burn. + // Полный возврат аванса (actual == 0) — burn не нужен, подотчёт уже пуст. + if (item_amount.amount > 0) { + Ledger2::apply(get_self(), coopname, expense_ops_for(it->source_wallet).report, item_amount, + item_recipient, 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(D::ProposalStatus::REPORT_SUBMITTED), + "Закрыть можно только СЗ в статусе REPORT_SUBMITTED"); + + idx.modify(it, get_self(), [&](auto& row) { + row.status = static_cast(D::ProposalStatus::CLOSED); + row.updated_at = eosio::current_time_point(); + }); + + // Callback на финализацию (capital::onpgexpdone и т.п.) — если был установлен при createexp. + send_callback_if_any(it->callback, coopname, proposal_hash, + static_cast(D::ProposalStatus::CLOSED), + 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(), "СЗ не найден"); + eosio::check(it->status == static_cast(D::ProposalStatus::PARTIALLY_PAID), + "Возврат возможен только по СЗ в статусе PARTIALLY_PAID"); + + bool item_found = false; + name item_recipient{}; + + idx.modify(it, get_self(), [&](auto& row) { + for (auto& i : row.items) { + if (i.item_hash == item_hash) { + eosio::check(i.status == static_cast(D::ItemStatus::PAID), + "Возврат возможен только из статуса PAID"); + eosio::check(static_cast(i.mechanics) == D::Mechanics::ADVANCE, + "returnexp применим только к ADVANCE-механике"); + eosio::check(return_amount.is_valid() && + return_amount.symbol == i.actual_amount.symbol, + "Символ return_amount не совпадает с item"); + eosio::check(return_amount.amount > 0 && return_amount.amount <= i.actual_amount.amount, + "return_amount должен быть положительным и не превышать actual_amount"); + // Settlement-запись: статус item НЕ меняется (остаётся PAID) — после + // возврата остатка получатель штатно отчитывается reportexp по + // фактически потраченной части (при полном возврате — actual == 0). + i.actual_amount -= return_amount; + item_recipient = i.recipient; + item_found = true; + break; + } + } + eosio::check(item_found, "item с заданным hash не найден в СЗ"); + row.total_actual -= return_amount; + row.updated_at = eosio::current_time_point(); + }); + + // ledger2: TRANSFER подотчёт → пул-источник (зеркало выдачи аванса). + // Подотчёт снимается с получателя аванса. + Ledger2::apply(get_self(), coopname, expense_ops_for(it->source_wallet).refund, return_amount, + item_recipient, 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(), "СЗ не найден"); + eosio::check(it->status == static_cast(D::ProposalStatus::PARTIALLY_PAID), + "Перерасход возможен только по СЗ в статусе PARTIALLY_PAID"); + + bool item_found = false; + name item_recipient{}; + + idx.modify(it, get_self(), [&](auto& row) { + for (auto& i : row.items) { + if (i.item_hash == item_hash) { + eosio::check(i.status == static_cast(D::ItemStatus::PAID), + "Перерасход регистрируется только из статуса PAID"); + eosio::check(static_cast(i.mechanics) == D::Mechanics::ADVANCE, + "overspendexp применим только к ADVANCE-механике"); + eosio::check(overspend_amount.is_valid() && + overspend_amount.symbol == i.actual_amount.symbol, + "Символ overspend_amount не совпадает с item"); + // Settlement-запись: статус item НЕ меняется (остаётся PAID) — подотчёт + // (теперь на полную сумму выдано + доплата) закрывается штатным reportexp, + // который сделает burn ADVANCE_HOLD на итоговый actual. + i.actual_amount += overspend_amount; + item_recipient = i.recipient; + item_found = true; + break; + } + } + eosio::check(item_found, "item с заданным hash не найден в СЗ"); + row.total_actual += overspend_amount; + row.updated_at = eosio::current_time_point(); + }); + + // ledger2: доплата перерасхода — TRANSFER пул-источник → подотчёт получателя + // (зеркало выдачи аванса на сумму доплаты). Закрытие на полную сумму — в reportexp. + Ledger2::apply(get_self(), coopname, expense_ops_for(it->source_wallet).overspend, overspend_amount, + item_recipient, proposal_hash, "expense:overspend"); +} diff --git a/components/contracts/cpp/expense/expense.hpp b/components/contracts/cpp/expense/expense.hpp new file mode 100644 index 00000000000..975360e1f7e --- /dev/null +++ b/components/contracts/cpp/expense/expense.hpp @@ -0,0 +1,233 @@ +#pragma once + +#include +#include +#include +#include "../lib/index.hpp" + +using namespace eosio; +using std::string; + +/** + * @defgroup public_expense Контракт EXPENSE + * @brief Шасси расходов цифрового кооператива (MVP — Благорост). + * + * Универсальный контракт: principal storage = СЗ-расход с массивом items; + * кошелёк-источник (пул) приходит параметром source_wallet, ledger2-коды всех + * операций жизненного цикла выводятся из него через EXPENSE_OPERATION_SETS + * (ledger2/operations.hpp) — подключение нового пула не меняет контракт; + * механика оплаты per-item (ADVANCE | DIRECT) выбирает код внутри набора; + * 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, ///< Закрыт отчётом (чек приложен). DIRECT-item получает его сразу при payexp. + RETURNED = 3, ///< Зарезервировано, не используется: returnexp уменьшает actual_amount, статус остаётся PAID до отчёта. + OVERSPENT = 4, ///< Зарезервировано, не используется: overspendexp увеличивает actual_amount, статус остаётся PAID до отчёта. + }; + + /** + * @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 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::name coopname; ///< кооператив (= scope таблицы; дублируем полем по канону) + eosio::checksum256 proposal_hash; + eosio::name username; ///< создатель + eosio::name source_wallet; ///< семантически — из какого ЦПП/фонда (w.cap.blago, w.sov.expns, …) + uint8_t status; ///< ProposalStatus + std::vector 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(status); } + + EOSLIB_SERIALIZE(proposal, + (id)(coopname)(proposal_hash)(username)(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>, + eosio::indexed_by<"byusername"_n, eosio::const_mem_fun>, + eosio::indexed_by<"bystatus"_n, eosio::const_mem_fun> + >; + +} // 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) + опц. callback на финализацию. + * Подписывает создатель (signact1 statement_doc, type=2010). + * + * Авторизация: coopname (прямой вызов backend'а) либо контракт-инициатор из + * contracts_whitelist (inline, например capital::createpgexp). + * Механика оплаты — per-item (item.mechanics: ADVANCE | DIRECT); ledger2-код + * операции выводится из механики позиции в момент оплаты payexp. + * + * Ставит вопрос в повестку совета (soviet::createagenda, тип createexp): + * утверждение совета → callback authexp, отклонение → declexp. + */ + [[eosio::action]] + void createexp(name coopname, name username, + checksum256 proposal_hash, + name source_wallet, + std::vector items, + ExpenseDomain::callback_handler callback, + document2 statement); + + /** + * @brief Авторизовать СЗ советом (signact2 decision_doc, type=2011). + * Callback решения совета — вызывается только контрактом soviet после + * голосования и утверждения председателем. После этого расход доступен для оплаты. + */ + [[eosio::action]] + void authexp(name coopname, checksum256 proposal_hash, document2 decision); + + /** + * @brief Отклонить СЗ. Callback решения совета (отрицательный консенсус + * голосов «против» либо просрочка повестки) — вызывается только контрактом + * soviet. Возможно только до первой оплаты (CREATED / AUTHORIZED): + * после payexp средства уже ушли через ledger2, и СЗ завершается обычным + * путём reportexp / returnexp → closeexp. + */ + [[eosio::action]] + void declexp(name coopname, checksum256 proposal_hash, std::string reason); + + /** + * @brief Оплатить item — выдача аванса (ADVANCE) или прямая оплата организации (DIRECT). + * Контракт зовёт Ledger2::apply с кодом из набора операций кошелька-источника + * (EXPENSE_OPERATION_SETS: source_wallet + механика item'а → operation_code). + * actual_amount не может превышать план item (доплата — через overspendexp). + * DIRECT-item не имеет фазы подотчёта и помечается REPORTED сразу; когда все + * items REPORTED — proposal переходит в REPORT_SUBMITTED прямо из payexp. + */ + [[eosio::action]] + void payexp(name coopname, checksum256 proposal_hash, checksum256 item_hash, + asset actual_amount); + + /** + * @brief Отчёт о расходе (ADVANCE) — пайщик закрывает item чеком. + * Зовёт Ledger2::apply(o.exp.advrpt) на остаточный actual item'а (при полном + * возврате аванса burn пропускается). Когда все 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-остаток). + * Settlement-запись: уменьшает actual_amount item'а, статус остаётся PAID — + * фактически потраченная часть закрывается затем штатным reportexp. + * Зовёт Ledger2::apply(o.exp.advret). + */ + [[eosio::action]] + void returnexp(name coopname, checksum256 proposal_hash, checksum256 item_hash, + asset return_amount); + + /** + * @brief Доплата при перерасходе (ADVANCE). + * Settlement-запись: увеличивает actual_amount item'а, статус остаётся PAID. + * Зовёт Ledger2::apply(o.exp.over) — выдача доплаты; закрытие подотчёта на + * полную сумму (выдано + доплата) делает последующий reportexp. + */ + [[eosio::action]] + void overspendexp(name coopname, checksum256 proposal_hash, checksum256 item_hash, + asset overspend_amount); +}; diff --git a/components/contracts/cpp/lib/consts.hpp b/components/contracts/cpp/lib/consts.hpp index 6606c30e639..454e0bf4969 100644 --- a/components/contracts/cpp/lib/consts.hpp +++ b/components/contracts/cpp/lib/consts.hpp @@ -103,7 +103,10 @@ static const std::set soviet_actions = { //WALLET "createwthd"_n, //создать заявление на возврат паевого взноса - + + //EXPENSE + "createexp"_n, //служебная записка-смета о расходах (шасси расходов) + //LEDGER "ledgerwthd"_n, //заявление на списание со счета через ledger @@ -166,6 +169,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; @@ -194,7 +198,8 @@ static constexpr uint64_t _capital_program_id = 4; "ledger"_n, "ledger2"_n, "capital"_n, - "apps"_n + "apps"_n, + "expense"_n // Добавьте остальные стандартные или пользовательские контракты по необходимости }; diff --git a/components/contracts/cpp/lib/core/ledger2/operations.hpp b/components/contracts/cpp/lib/core/ledger2/operations.hpp index 3213a07693b..e1684a531f8 100644 --- a/components/contracts/cpp/lib/core/ledger2/operations.hpp +++ b/components/contracts/cpp/lib/core/ledger2/operations.hpp @@ -86,6 +86,7 @@ namespace operations { inline constexpr eosio::name WITHDRAW_FROM_CAPITAL = "o.cap.wthcap"_n; ///< Возврат паевого из ЦПП «Благорост» в кошелёк пайщика (TRANSFER BLAGOROST_FUND → SHARE_FUND_PAY, без Dr/Cr). inline constexpr eosio::name CONVERT_TO_SHARE = "o.cap.cnvshr"_n; ///< Конвертация сегмента: РИД → главный кошелёк (TRANSFER GENERATOR_FUND → SHARE_FUND_PAY, без Dr/Cr — бухпроводка уже была сделана в ACCEPT_RID). inline constexpr eosio::name CONVERT_TO_BLAGO = "o.cap.cnvbl"_n; ///< Конвертация сегмента: РИД → ЦПП «Благорост» (TRANSFER GENERATOR_FUND → BLAGOROST_FUND, без Dr/Cr — бухпроводка уже была сделана в ACCEPT_RID). + inline constexpr eosio::name PROGRAM_EXPENSE_TOPUP = "o.cap.pgtop"_n; ///< Пополнение пула программных расходов из инвестиций программы (ISSUE PROGRAM_EXPENSE_POOL, без Dr/Cr — деньги уже на 51, выделяется кооперативный резерв под расходы; паевые L3-кошельки пайщиков не трогаются). } // marketplace @@ -99,6 +100,27 @@ namespace operations { inline constexpr eosio::name CONVERT_AXN = "o.sov.axncnv"_n; ///< Трансляция паевого взноса в членский (Dr 80 / Cr 86, TRANSFER SHARE_FUND_PAY → DELEGATE_FEES). } + // expense — шасси расходов (MVP: только Благорост; хозрасходы из членских — отдельный эпик). + // + // Принципы (см. components/desktop/extensions/expenses/NAMING-C28-28.md): + // - Контракт `expense` универсальный: operation_code передаётся в payload. + // - Источник оплат — КООПЕРАТИВНЫЙ пул расходов (PROGRAM_EXPENSE_POOL), + // пополняемый o.cap.pgtop; личные L3-кошельки пайщиков (w.cap.blago) + // при оплатах СЗ не изменяются. + // - При расходе из Благороста паевой фонд (80) НЕ трогается: меняется только форма + // актива 51 → 08 (Дт 08 / Кт 51 для обеих механик). + // - ADVANCE-отчёт (`o.exp.advrpt`) НЕ создаёт новой бухпроводки: проводка уже + // сделана на `o.exp.blgadv` при выдаче. + // - Callback на финализацию — переменная (`callback{contract, action, data}`), + // заполняется при `expense::createexp`; expense ничего не знает про capital. + namespace expense { + inline constexpr eosio::name BLAGO_ADVANCE = "o.exp.blgadv"_n; ///< Выдача подотчётных из пула расходов (TRANSFER PROGRAM_EXPENSE_POOL → ADVANCE_HOLD, Dr 08 / Cr 51). + inline constexpr eosio::name BLAGO_DIRECT = "o.exp.blgdir"_n; ///< Прямая оплата из пула расходов (BURN PROGRAM_EXPENSE_POOL, Dr 08 / Cr 51). + inline constexpr eosio::name ADVANCE_REPORT = "o.exp.advrpt"_n; ///< Закрытие подотчёта пайщика (BURN ADVANCE_HOLD, без бухпроводки — canal 08/51 уже сделан на blgadv). + inline constexpr eosio::name ADVANCE_RETURN = "o.exp.advret"_n; ///< Возврат неиспользованного подотчёта (TRANSFER ADVANCE_HOLD → PROGRAM_EXPENSE_POOL, Dr 51 / Cr 08). + inline constexpr eosio::name OVERSPEND = "o.exp.over"_n; ///< Доплата сверх подотчёта (TRANSFER PROGRAM_EXPENSE_POOL → ADVANCE_HOLD, Dr 08 / Cr 51); сразу за ней expense вызывает ADVANCE_REPORT. + } + // migration (только из migrate.cpp) // // В OPERATION_REGISTRY включены **только** те транзиты, которые проводятся @@ -362,6 +384,72 @@ static constexpr OperationRegistryEntry OPERATION_REGISTRY[] = { 0, 0, "Конвертация сегмента: РИД → ЦПП «Благорост»" }, + // 19a. Пополнение пула программных расходов: ISSUE PROGRAM_EXPENSE_POOL, без Dr/Cr. + // Совет выделяет часть свободных инвестиций программы под целевые расходы: + // деньги физически на 51 с момента взносов, здесь появляется кооперативный + // резерв-кошелёк, из которого шасси expense оплачивает СЗ. Паевые L3-кошельки + // пайщиков (w.cap.blago, счёт 80) не изменяются — права требования сохраняются. + { operations::capital::PROGRAM_EXPENSE_TOPUP, processes::capital::PGEXP, WalletOp::ISSUE, + eosio::name{}, ledger2_wallets::PROGRAM_EXPENSE_POOL, + 0, 0, + "Пополнение пула программных расходов ЦПП «Благорост»" }, + + // ----- Шасси расходов (o.exp.*) — вызываются из контракта expense ----- + // + // Базовое состояние Благороста ДО расхода: + // - Деньги физически на 51 с момента o.wal.depcpl (Dr 51 / Cr 80). + // - 80 (паевой) наполнен; кошелёк w.cap.blago. + // - 08 пустой (`o.cap.invest` — TRANSFER без проводок). + // + // Принцип: расход не уменьшает паевой фонд (80). Меняется только форма актива + // 51 → 08 (банк уходит, появляется WIP-проект). + + // 20. Выдача подотчётных из пула программных расходов: Dr 08 / Cr 51, + // TRANSFER PROGRAM_EXPENSE_POOL → ADVANCE_HOLD. + // Источник — КООПЕРАТИВНЫЙ пул расходов (пополняется o.cap.pgtop); личные + // L3-кошельки пайщиков (w.cap.blago) при оплате СЗ не трогаются — их паевые + // взносы в программе не уменьшаются. Деньги физически уходят пайщику (Cr 51), + // стоимость капитализируется в WIP (Dr 08). ADVANCE_HOLD фиксирует + // ответственность получателя аванса (USER_SHARED) до отчёта. + { operations::expense::BLAGO_ADVANCE, processes::expense::PROPOSAL, WalletOp::TRANSFER, + ledger2_wallets::PROGRAM_EXPENSE_POOL, ledger2_wallets::ADVANCE_HOLD, + ledger2_accounts::NON_CURRENT_INVESTMENTS, ledger2_accounts::BANK_ACCOUNT, + "Выдача подотчётных из пула расходов ЦПП «Благорост»" }, + + // 21. Прямая оплата из пула программных расходов (DIRECT): Dr 08 / Cr 51, + // BURN PROGRAM_EXPENSE_POOL. Оплата организации по счёту; деньги уходят с 51, + // стоимость капитализируется в 08. Кошелёк-резерв не задействован. + { operations::expense::BLAGO_DIRECT, processes::expense::PROPOSAL, WalletOp::BURN, + ledger2_wallets::PROGRAM_EXPENSE_POOL, eosio::name{}, + ledger2_accounts::NON_CURRENT_INVESTMENTS, ledger2_accounts::BANK_ACCOUNT, + "Прямая оплата из пула расходов ЦПП «Благорост»" }, + + // 22. Закрытие подотчёта пайщика по отчёту: BURN ADVANCE_HOLD, БЕЗ бухпроводки. + // Проводка Dr 08 / Cr 51 уже сделана на BLAGO_ADVANCE при выдаче. При отчёте только + // снимается кошелёк-резерв пайщика — никакого canal 08/51 второй раз. + { operations::expense::ADVANCE_REPORT, processes::expense::PROPOSAL, WalletOp::BURN, + ledger2_wallets::ADVANCE_HOLD, eosio::name{}, + 0, 0, + "Закрытие подотчёта пайщика по отчёту" }, + + // 23. Возврат неиспользованного подотчёта: Dr 51 / Cr 08, + // TRANSFER ADVANCE_HOLD → PROGRAM_EXPENSE_POOL. + // Зеркало BLAGO_ADVANCE: деньги возвращаются на 51, WIP-стоимость уменьшается, + // остаток снова доступен пулу расходов. + { operations::expense::ADVANCE_RETURN, processes::expense::PROPOSAL, WalletOp::TRANSFER, + ledger2_wallets::ADVANCE_HOLD, ledger2_wallets::PROGRAM_EXPENSE_POOL, + ledger2_accounts::BANK_ACCOUNT, ledger2_accounts::NON_CURRENT_INVESTMENTS, + "Возврат неиспользованного подотчёта в пул расходов" }, + + // 24. Доплата сверх подотчёта (перерасход): Dr 08 / Cr 51, + // TRANSFER PROGRAM_EXPENSE_POOL → ADVANCE_HOLD. + // Зеркало BLAGO_ADVANCE на сумму перерасхода. Контракт expense сразу за OVERSPEND + // вызывает ADVANCE_REPORT — две последовательные записи в одной транзакции `expense::overspendexp`. + { operations::expense::OVERSPEND, processes::expense::PROPOSAL, WalletOp::TRANSFER, + ledger2_wallets::PROGRAM_EXPENSE_POOL, ledger2_wallets::ADVANCE_HOLD, + ledger2_accounts::NON_CURRENT_INVESTMENTS, ledger2_accounts::BANK_ACCOUNT, + "Доплата сверх подотчёта (перерасход)" }, + // ----- Миграционные (o.mig.*) — вызываются только из migrate.cpp ----- // 15. Миграция: минимальный паевой: Dr 51 / Cr 80, ISSUE MIN_SHARE_FUND @@ -544,3 +632,105 @@ inline constexpr const OperationAdjustmentEntry* find_adjustment(eosio::name ope } return nullptr; } + +// ===================================================================== +// Наборы операций шасси расходов — фабричная настройка контракта expense. +// ===================================================================== +// +// Контракт `expense` агностичен к программе-источнику: кошелёк-пул приходит в +// `createexp` параметром `source_wallet`, а ledger2-коды всех пяти операций +// жизненного цикла (аванс / прямая оплата / отчёт / возврат / перерасход) +// выводятся из этого кошелька через таблицу ниже — в коде expense нет ни +// одного захардкоженного operation_code. +// +// Подключение шасси к новому пулу (например, кошельку членских взносов +// кооперативного участка) = добавить 5 операций в OPERATION_REGISTRY и одну +// строку здесь. Контракт expense при этом не меняется. +struct ExpenseOperationSet { + eosio::name source_wallet; ///< пул-источник средств (COOPERATIVE-кошелёк) + eosio::name advance; ///< выдача аванса под отчёт (TRANSFER pool → подотчёт) + eosio::name direct; ///< прямая оплата организации по счёту (BURN pool) + eosio::name report; ///< закрытие подотчёта по отчёту (BURN подотчёта) + eosio::name refund; ///< возврат неиспользованного аванса (TRANSFER подотчёт → pool) + eosio::name overspend; ///< доплата при перерасходе (TRANSFER pool → подотчёт) +}; + +static constexpr ExpenseOperationSet EXPENSE_OPERATION_SETS[] = { + // Пул программных расходов ЦПП «Благорост» — source_wallet заполняет + // capital::createpgexp при создании СЗ через inline expense::createexp. + { ledger2_wallets::PROGRAM_EXPENSE_POOL, + operations::expense::BLAGO_ADVANCE, + operations::expense::BLAGO_DIRECT, + operations::expense::ADVANCE_REPORT, + operations::expense::ADVANCE_RETURN, + operations::expense::OVERSPEND }, +}; + +static constexpr size_t EXPENSE_OPERATION_SETS_SIZE = + sizeof(EXPENSE_OPERATION_SETS) / sizeof(EXPENSE_OPERATION_SETS[0]); + +// Compile-time валидация наборов: каждый код существует в OPERATION_REGISTRY, +// тип wallet-операции и привязка кошельков соответствуют роли кода в наборе. +namespace ledger2_expense_sets_detail { + constexpr const OperationRegistryEntry* find_op(eosio::name code) { + for (size_t i = 0; i < OPERATION_REGISTRY_SIZE; ++i) { + if (OPERATION_REGISTRY[i].code == code) return &OPERATION_REGISTRY[i]; + } + return nullptr; + } + + constexpr bool source_wallets_unique() { + for (size_t i = 0; i < EXPENSE_OPERATION_SETS_SIZE; ++i) { + for (size_t j = i + 1; j < EXPENSE_OPERATION_SETS_SIZE; ++j) { + if (EXPENSE_OPERATION_SETS[i].source_wallet == EXPENSE_OPERATION_SETS[j].source_wallet) { + return false; + } + } + } + return true; + } + + constexpr bool sets_consistent() { + for (size_t i = 0; i < EXPENSE_OPERATION_SETS_SIZE; ++i) { + const auto& s = EXPENSE_OPERATION_SETS[i]; + const auto* adv = find_op(s.advance); + const auto* dir = find_op(s.direct); + const auto* rep = find_op(s.report); + const auto* ref = find_op(s.refund); + const auto* ovr = find_op(s.overspend); + if (!adv || !dir || !rep || !ref || !ovr) return false; + // Аванс: пул → кошелёк-подотчёт. + if (adv->wallet_op != WalletOp::TRANSFER || adv->wallet_from != s.source_wallet) return false; + const eosio::name hold = adv->wallet_to; + // Прямая оплата: сжигание с пула (подотчёт не задействован). + if (dir->wallet_op != WalletOp::BURN || dir->wallet_from != s.source_wallet) return false; + // Отчёт: сжигание подотчёта. + if (rep->wallet_op != WalletOp::BURN || rep->wallet_from != hold) return false; + // Возврат: подотчёт → пул. + if (ref->wallet_op != WalletOp::TRANSFER || + ref->wallet_from != hold || ref->wallet_to != s.source_wallet) return false; + // Перерасход: пул → подотчёт (зеркало аванса на сумму доплаты). + if (ovr->wallet_op != WalletOp::TRANSFER || + ovr->wallet_from != s.source_wallet || ovr->wallet_to != hold) return false; + } + return true; + } +} // namespace ledger2_expense_sets_detail + +static_assert(ledger2_expense_sets_detail::source_wallets_unique(), + "EXPENSE_OPERATION_SETS: source_wallet должен быть уникален"); +static_assert(ledger2_expense_sets_detail::sets_consistent(), + "EXPENSE_OPERATION_SETS: набор операций не согласован с OPERATION_REGISTRY " + "(коды/типы wallet-операций/привязка кошельков)"); + +/** + * @brief Поиск набора операций шасси расходов по кошельку-источнику. + */ +inline const ExpenseOperationSet* find_expense_operation_set(eosio::name source_wallet) { + for (size_t i = 0; i < EXPENSE_OPERATION_SETS_SIZE; ++i) { + if (EXPENSE_OPERATION_SETS[i].source_wallet == source_wallet) { + return &EXPENSE_OPERATION_SETS[i]; + } + } + return nullptr; +} diff --git a/components/contracts/cpp/lib/core/ledger2/processes.hpp b/components/contracts/cpp/lib/core/ledger2/processes.hpp index 65f4adc62c2..8a2cc519a41 100644 --- a/components/contracts/cpp/lib/core/ledger2/processes.hpp +++ b/components/contracts/cpp/lib/core/ledger2/processes.hpp @@ -54,6 +54,7 @@ namespace processes { inline constexpr eosio::name PROPERTY = "p.cap.prop"_n; ///< Приём имущественного паевого взноса (одноактовый). inline constexpr eosio::name PREIMP = "p.cap.preimp"_n; ///< Первичный учёт РИД-взноса до перехода на электронный учёт (одноактовый, anchor = preimp register hash). inline constexpr eosio::name WTHCAP = "p.cap.wthcap"_n; ///< Возврат паевого из ЦПП «Благорост» в кошелёк пайщика (одноактовый). + inline constexpr eosio::name PGEXP = "p.cap.pgexp"_n; ///< Пул программных расходов: пополнение из инвестиций программы (o.cap.pgtop, одноактовый). } // marketplace @@ -61,6 +62,11 @@ namespace processes { inline constexpr eosio::name REQUEST = "p.mkt.reqst"_n; ///< Цикл запроса маркетплейса (o.mkt.supply + o.mkt.recv). } + // expense — шасси расходов (СЗ → авторизация → платёж → отчёт → закрытие) + namespace expense { + inline constexpr eosio::name PROPOSAL = "p.exp.expns"_n; ///< Цикл расхода по СЗ: o.exp.blgadv|blgdir (платёж) + опц. o.exp.over (перерасход) + o.exp.advrpt (отчёт ADVANCE) + опц. o.exp.advret (возврат). Анкер процесса — proposal_hash. + } + // soviet namespace soviet { inline constexpr eosio::name AXN_CONVERT = "p.sov.axncnv"_n; ///< Конвертация паевого RUB → делегатский ЧВ (одноактовый). diff --git a/components/contracts/cpp/lib/core/ledger2/wallets.hpp b/components/contracts/cpp/lib/core/ledger2/wallets.hpp index 68ccec21e16..094edf86dbd 100644 --- a/components/contracts/cpp/lib/core/ledger2/wallets.hpp +++ b/components/contracts/cpp/lib/core/ledger2/wallets.hpp @@ -65,9 +65,16 @@ struct ledger2_wallets { static constexpr eosio::name BLAGOROST_FUND = "w.cap.blago"_n; ///< Благорост — единый агрегированный кошелёк программы (USER_SHARED; ADR-009) static constexpr eosio::name GENERATOR_FUND = "w.cap.gen"_n; ///< Генератор — единый агрегированный кошелёк программы (COOPERATIVE — кооперативный пул, без L3-разреза по пайщику; L3-разрез из ADR-009 отменён из-за несовместимости с CRPS-перераспределением, см. wallets.hpp:107) static constexpr eosio::name PREIMP_FUND = "w.cap.preimp"_n; ///< Первичный учёт РИД-взносов до перехода на электронный учёт (USER_SHARED; o.cap.preimp / o.cap.drppre) + static constexpr eosio::name PROGRAM_EXPENSE_POOL = "w.cap.pgexp"_n; ///< Пул программных расходов ЦПП «Благорост» (COOPERATIVE) — кооперативный кошелёк, из которого шасси expense оплачивает СЗ; пополняется topupprogexp (o.cap.pgtop), паевые L3-кошельки пайщиков (w.cap.blago) при расходах не трогаются // marketplace — выплаты static constexpr eosio::name SUPPLIER_PAYMENTS = "w.mkt.payout"_n; ///< Выплаты поставщикам (sink RECEIVE_CONFIRM, COOPERATIVE) + + // expense — шасси расходов (подотчёт пайщика, USER_SHARED) + // Зеркало паттерна w.wal.wpend: кошелёк-резерв на пайщике-получателе ADVANCE-механики. + // На момент выдачи аванса фиксирует ответственность пайщика; на отчёте — BURN без новой бухпроводки + // (canal Дт 08 / Кт 51 уже сделан на o.exp.blgadv). + static constexpr eosio::name ADVANCE_HOLD = "w.exp.adv"_n; ///< Подотчётные средства пайщика (USER_SHARED; резерв при ADVANCE-механике шасси расходов) }; /** @@ -96,16 +103,17 @@ struct Ledger2WalletMeta { WalletKind kind; }; -inline constexpr std::array LEDGER2_WALLET_REGISTRY = {{ - // USER_SHARED (6) — L3-разрез по пайщику +inline constexpr std::array LEDGER2_WALLET_REGISTRY = {{ + // USER_SHARED (7) — L3-разрез по пайщику { ledger2_wallets::MIN_SHARE_FUND, "Минимальный паевой взнос", WalletKind::USER_SHARED }, { ledger2_wallets::SHARE_FUND_PAY, "Паевой взнос пайщика", WalletKind::USER_SHARED }, { ledger2_wallets::CK_MEMBER, "ЦК — членская часть пайщика", WalletKind::USER_SHARED }, { ledger2_wallets::BLAGOROST_FUND, "ЦПП «Благорост» — единый кошелёк программы у пайщика", WalletKind::USER_SHARED }, { ledger2_wallets::PREIMP_FUND, "Первичный учёт РИД-взносов до перехода на электронный учёт", WalletKind::USER_SHARED }, + { ledger2_wallets::ADVANCE_HOLD, "Подотчётные средства пайщика", WalletKind::USER_SHARED }, { ledger2_wallets::REGISTRATION_PENDING, "Регистрационный взнос в ожидании решения совета", WalletKind::USER_SHARED }, - // COOPERATIVE (10) — единый кооперативный баланс, без L3 + // COOPERATIVE (10) — единый кооперативный баланс, без L3 (Generator + 9 «единых пулов») // GENERATOR_FUND переведён сюда из USER_SHARED (см. wallets.hpp:64) — // CRPS-распределение между сегментами проекта не поддерживает per-user // компенсирующие TRANSFER на approvecmmt, поэтому L3-проверка walletop @@ -120,6 +128,7 @@ inline constexpr std::array LEDGER2_WALLET_REGISTRY = {{ { ledger2_wallets::MIN_SHARE_USED, "Использованные минимальные паевые взносы", WalletKind::COOPERATIVE }, { ledger2_wallets::LOAN_ISSUED, "Выданные пайщикам беспроцентные займы", WalletKind::COOPERATIVE }, { ledger2_wallets::SUPPLIER_PAYMENTS, "Выплаты поставщикам", WalletKind::COOPERATIVE }, + { ledger2_wallets::PROGRAM_EXPENSE_POOL, "Пул программных расходов ЦПП «Благорост»", WalletKind::COOPERATIVE }, }}; static constexpr size_t LEDGER2_WALLET_REGISTRY_SIZE = LEDGER2_WALLET_REGISTRY.size(); @@ -232,13 +241,14 @@ struct Ledger2WalletProgramMapping { uint64_t required_program_id; // 0 = исключение (без проверки) }; -inline constexpr std::array LEDGER2_USER_SHARED_PROGRAM_MAPPING = {{ +inline constexpr std::array LEDGER2_USER_SHARED_PROGRAM_MAPPING = {{ { ledger2_wallets::MIN_SHARE_FUND, 0 /* w.reg.minshr — без проверки */ }, { ledger2_wallets::SHARE_FUND_PAY, 1 /* ЦК */ }, { ledger2_wallets::CK_MEMBER, 1 /* ЦК */ }, { ledger2_wallets::BLAGOROST_FUND, 4 /* Благорост */ }, { ledger2_wallets::GENERATOR_FUND, 3 /* Генератор */ }, { ledger2_wallets::PREIMP_FUND, 0 /* w.cap.preimp — РИД-учёт до перехода на электронный учёт, без проверки */ }, + { ledger2_wallets::ADVANCE_HOLD, 0 /* w.exp.adv — подотчёт пайщика по СЗ; программа-источник проверена контрактом expense, повторная gate не нужна */ }, { ledger2_wallets::REGISTRATION_PENDING, 0 /* w.reg.pend — кандидат ещё не член, соглашения нет, без проверки */ }, }}; diff --git a/components/contracts/cpp/lib/core/names.hpp b/components/contracts/cpp/lib/core/names.hpp index aae1d35d8fd..a0d0b56ee3c 100644 --- a/components/contracts/cpp/lib/core/names.hpp +++ b/components/contracts/cpp/lib/core/names.hpp @@ -50,6 +50,10 @@ namespace Names { // Коллбэки для расходов constexpr eosio::name AUTHORIZE_EXPENSE = "capauthexpns"_n; constexpr eosio::name DECLINE_EXPENSE = "capdeclexpns"_n; + + // Программные расходы обслуживаются шасси expense; capital видит только + // инициирование (createpgexp) и этот callback завершения flow. + constexpr eosio::name ON_PROGRAM_EXPENSE_DONE = "onpgexpdone"_n; // Коллбэки для долгов constexpr eosio::name AUTHORIZE_DEBT = "debtauthcnfr"_n; @@ -80,10 +84,13 @@ namespace Names { // Внешние действия в других контрактах constexpr eosio::name CREATE_OUTPAY = "createoutpay"_n; constexpr eosio::name OPEN_PROGRAM_WALLET = "openprogwall"_n; - + // Действия для отправки в soviet constexpr eosio::name CREATE_APPROVAL = "createapprv"_n; constexpr eosio::name CREATE_AGENDA = "createagenda"_n; + + // Шасси expense + constexpr eosio::name CREATE_EXPENSE_PROPOSAL = "createexp"_n; } namespace SovietActions { @@ -120,6 +127,7 @@ namespace Names { constexpr eosio::name CREATE_INVESTMENT = "createinvest"_n; // акцепт инвестиции constexpr eosio::name CREATE_PROGRAM_INVESTMENT = "createpinv"_n; // акцепт программной инвестиции constexpr eosio::name CREATE_EXPENSE = "createexpnse"_n; // акцепт расхода + constexpr eosio::name CREATE_PROGRAM_EXPENSE = "createpgexp"_n; // акцепт программного расхода constexpr eosio::name CREATE_WITHDRAW_1 = "createwthd1"_n; // акцепт возврата из задания constexpr eosio::name CREATE_WITHDRAW_2 = "createwthd2"_n; // акцепт возврата из проекта constexpr eosio::name CREATE_WITHDRAW_3 = "createwthd3"_n; // акцепт возврата из программы diff --git a/components/controller/.env-example b/components/controller/.env-example index 26a2c23273d..47cf412171d 100644 --- a/components/controller/.env-example +++ b/components/controller/.env-example @@ -34,6 +34,13 @@ SMTP_USERNAME= SMTP_PASSWORD= EMAIL_FROM="'КО-ПОЧТАЛЬОН' " +# Напоминатель об отчёте по авансу под отчёт (extensions/expenses). +# Интервал тика сканера зеркала расходов (мс). Дефолт 6 ч. Письмо пайщику — +# не чаще раза в неделю (троттлит идемпотентность Центра уведомлений). +EXPENSE_ADVANCE_REMINDER_INTERVAL_MS=21600000 +# Сколько дней аванс может висеть без отчёта до первого напоминания. Дефолт 3. +EXPENSE_ADVANCE_REMINDER_GRACE_DAYS=3 + # BLOCKHAIN BLOCKCHAIN_RPC=http://127.0.0.1:8888 CHAIN_ID=cae86058a6d8698833afb474ab8a5ad8599c6cf54f9ebcf275dbac7055c16fe1 diff --git a/components/controller/src/application/common/dto/pagination.dto.ts b/components/controller/src/application/common/dto/pagination.dto.ts index e41734e6def..3d517b1f020 100644 --- a/components/controller/src/application/common/dto/pagination.dto.ts +++ b/components/controller/src/application/common/dto/pagination.dto.ts @@ -53,3 +53,47 @@ export function createPaginationResult(ItemType: new (...args: any[]) => T, n } return PaginationResult; } + +/** + * Сборка `PaginationResult` для consumer'ов, читающих через inter-порт + * (нет своего Repository.findAndCount, доступен только raw `{items, totalCount}`). + * + * Зачем: вычисление `totalPages`/`currentPage` из `PaginationInputDTO` — общая + * детерминированная логика; дублировать её в каждом consumer'е = canon-долг. + */ +export function buildPaginationResult( + raw: { items: T[]; totalCount: number }, + options: PaginationInputDTO | undefined, + mapItem: (it: T) => U, +): PaginationResult { + const limit = options?.limit; + const page = options?.page != null ? Math.max(1, options.page) : 1; + const totalPages = limit != null && limit > 0 ? Math.max(1, Math.ceil(raw.totalCount / limit)) : 1; + return { + items: raw.items.map(mapItem), + totalCount: raw.totalCount, + totalPages, + currentPage: page, + }; +} + +/** + * Канон-конверсия `PaginationInputDTO` → `{limit, offset, sortBy, sortOrder}` + * для адаптеров inter-портов / внешних read API, не принимающих page-form. + */ +export function paginationInputToOffset(options?: PaginationInputDTO): { + limit?: number; + offset?: number; + sortBy?: string; + sortOrder?: 'ASC' | 'DESC'; +} { + const limit = options?.limit; + const page = options?.page != null ? Math.max(1, options.page) : 1; + const offset = limit != null ? (page - 1) * limit : undefined; + return { + limit, + offset, + sortBy: options?.sortBy, + sortOrder: options?.sortOrder, + }; +} diff --git a/components/controller/src/application/document/documents-dto/expense-proposal-decision-document.dto.ts b/components/controller/src/application/document/documents-dto/expense-proposal-decision-document.dto.ts new file mode 100644 index 00000000000..05991993e4b --- /dev/null +++ b/components/controller/src/application/document/documents-dto/expense-proposal-decision-document.dto.ts @@ -0,0 +1,133 @@ +import { InputType, Field, IntersectionType, OmitType, Int } from '@nestjs/graphql'; +import { IsString, IsNotEmpty, IsArray, ValidateNested, ArrayMinSize, IsInt, IsOptional } from 'class-validator'; +import { Type } from 'class-transformer'; +import { Cooperative } from 'cooptypes'; +import { GenerateMetaDocumentInputDTO } from '~/application/document/dto/generate-meta-document-input.dto'; +import { MetaDocumentInputDTO } from '~/application/document/dto/meta-document-input.dto'; +import { SignedDigitalDocumentInputDTO } from '~/application/document/dto/signed-digital-document-input.dto'; + +type Action = Cooperative.Registry.ExpenseProposalDecision.Action; +type ItemAction = Cooperative.Registry.ExpenseProposalStatement.IExpenseItem; +type HeaderAction = Cooperative.Registry.ExpenseProposalStatement.IExpenseProposalHeader; +type DecisionBody = Cooperative.Registry.ExpenseProposalDecision.IExpenseProposalDecisionBody; + +@InputType('ExpenseProposalDecisionItemInput') +class ExpenseProposalDecisionItemInputDTO implements ItemAction { + @Field() + @IsString() + number!: string; + + @Field() + @IsString() + description!: string; + + @Field() + @IsString() + amount!: string; + + @Field() + @IsString() + recipient_type!: 'SELF' | 'MEMBER' | 'ORG'; + + @Field() + @IsString() + mechanics!: 'ADVANCE' | 'DIRECT'; +} + +@InputType('ExpenseProposalDecisionHeaderInput') +class ExpenseProposalDecisionHeaderInputDTO implements HeaderAction { + @Field() + @IsString() + description!: string; + + @Field() + @IsString() + total_amount!: string; + + @Field(() => Int) + @IsInt() + items_count!: number; + + @Field() + @IsString() + source_wallet!: string; + + @Field({ nullable: true }) + @IsOptional() + @IsString() + deadline?: string; + + @Field({ nullable: true }) + @IsOptional() + @IsString() + fund_name?: string; +} + +@InputType('ExpenseProposalDecisionBodyInput') +class ExpenseProposalDecisionBodyInputDTO implements DecisionBody { + @Field({ description: 'Род решения (approve / decline)' }) + @IsString() + kind!: 'approve' | 'decline'; + + @Field({ description: 'Причина отказа (для decline)', nullable: true }) + @IsOptional() + @IsString() + reason?: string; +} + +@InputType('BaseExpenseProposalDecisionMetaDocumentInput') +class BaseExpenseProposalDecisionMetaDocumentInputDTO implements Omit { + @Field({ description: 'Хеш сметы расхода' }) + @IsString() + @IsNotEmpty() + proposal_hash!: string; + + @Field(() => Int, { description: 'Идентификатор решения совета (повестка) — источник данных голосования' }) + @IsInt() + decision_id!: number; + + @Field(() => ExpenseProposalDecisionHeaderInputDTO, { description: 'Шапка СЗ' }) + @ValidateNested() + @Type(() => ExpenseProposalDecisionHeaderInputDTO) + proposal!: ExpenseProposalDecisionHeaderInputDTO; + + @Field(() => [ExpenseProposalDecisionItemInputDTO], { description: 'Позиции расхода' }) + @IsArray() + @ArrayMinSize(1) + @ValidateNested({ each: true }) + @Type(() => ExpenseProposalDecisionItemInputDTO) + items!: ExpenseProposalDecisionItemInputDTO[]; + + @Field(() => ExpenseProposalDecisionBodyInputDTO, { description: 'Резолюция совета (утвердить / отказать)' }) + @ValidateNested() + @Type(() => ExpenseProposalDecisionBodyInputDTO) + resolution!: ExpenseProposalDecisionBodyInputDTO; +} + +/** + * Input генерации документа решения по СЗ (registry 2011). + */ +@InputType('ExpenseProposalDecisionGenerateDocumentInput') +export class ExpenseProposalDecisionGenerateDocumentInputDTO extends IntersectionType( + BaseExpenseProposalDecisionMetaDocumentInputDTO, + OmitType(GenerateMetaDocumentInputDTO, ['registry_id'] as const) +) { + registry_id!: number; +} + +@InputType('ExpenseProposalDecisionSignedMetaDocumentInput') +export class ExpenseProposalDecisionSignedMetaDocumentInputDTO extends IntersectionType( + BaseExpenseProposalDecisionMetaDocumentInputDTO, + MetaDocumentInputDTO +) {} + +/** + * Подписанный документ-решение по СЗ (registry 2011). + */ +@InputType('ExpenseProposalDecisionSignedDocumentInput') +export class ExpenseProposalDecisionSignedDocumentInputDTO extends SignedDigitalDocumentInputDTO { + @Field(() => ExpenseProposalDecisionSignedMetaDocumentInputDTO, { + description: 'Метаинформация решения по СЗ', + }) + public readonly meta!: ExpenseProposalDecisionSignedMetaDocumentInputDTO; +} diff --git a/components/controller/src/application/document/documents-dto/expense-proposal-statement-document.dto.ts b/components/controller/src/application/document/documents-dto/expense-proposal-statement-document.dto.ts new file mode 100644 index 00000000000..d352a64b62a --- /dev/null +++ b/components/controller/src/application/document/documents-dto/expense-proposal-statement-document.dto.ts @@ -0,0 +1,212 @@ +import { InputType, Field, IntersectionType, OmitType, Int } from '@nestjs/graphql'; +import { IsString, IsNotEmpty, IsArray, ValidateNested, ArrayMinSize, IsInt, IsOptional } from 'class-validator'; +import { Type } from 'class-transformer'; +import { Cooperative } from 'cooptypes'; +import { GenerateMetaDocumentInputDTO } from '~/application/document/dto/generate-meta-document-input.dto'; +import { MetaDocumentInputDTO } from '~/application/document/dto/meta-document-input.dto'; +import { SignedDigitalDocumentInputDTO } from '~/application/document/dto/signed-digital-document-input.dto'; + +type Action = Cooperative.Registry.ExpenseProposalStatement.Action; +type ItemAction = Cooperative.Registry.ExpenseProposalStatement.IExpenseItem; +type HeaderAction = Cooperative.Registry.ExpenseProposalStatement.IExpenseProposalHeader; + +/** + * Позиция-вход генерации СЗ-документа. Приватные поля (имя/реквизиты/назначение) + * — это вход для фабрики: сервер сохраняет их off-chain в doc_data и публикует + * в meta только `doc_data_hash`. На on-chain эти поля НЕ попадают. + */ +@InputType('ExpenseProposalItemInput') +class ExpenseProposalItemInputDTO { + @Field({ description: 'Порядковый номер строки' }) + @IsString() + number!: string; + + @Field({ description: 'Описание расхода' }) + @IsString() + description!: string; + + @Field({ description: 'Сумма строки' }) + @IsString() + amount!: string; + + @Field({ description: 'Тип получателя (SELF / MEMBER / ORG)' }) + @IsString() + recipient_type!: 'SELF' | 'MEMBER' | 'ORG'; + + @Field({ description: 'Способ оплаты (ADVANCE / DIRECT)' }) + @IsString() + mechanics!: 'ADVANCE' | 'DIRECT'; + + @Field({ description: 'Имя получателя', nullable: true }) + @IsOptional() + @IsString() + recipient_name?: string; + + @Field({ description: 'Реквизиты получателя', nullable: true }) + @IsOptional() + @IsString() + requisites?: string; + + @Field({ description: 'Назначение платежа — отдельной строкой после реквизитов', nullable: true }) + @IsOptional() + @IsString() + payment_purpose?: string; + + @Field({ + description: + 'Идентификатор сохранённых реквизитов получателя-пайщика — сервер подставит полные реквизиты в документ.', + nullable: true, + }) + @IsOptional() + @IsString() + payment_method_id?: string; + + @Field({ + description: 'Имя аккаунта получателя-пайщика (владелец реквизитов).', + nullable: true, + }) + @IsOptional() + @IsString() + recipient_username?: string; +} + +/** + * Публичная позиция подписанной meta — ровно то, что публикуется on-chain. + * Без имени/реквизитов/назначения платежа (они off-chain в doc_data). + */ +@InputType('ExpenseProposalSignedItemInput') +class ExpenseProposalSignedItemInputDTO implements ItemAction { + @Field({ description: 'Порядковый номер строки' }) + @IsString() + number!: string; + + @Field({ description: 'Описание расхода' }) + @IsString() + description!: string; + + @Field({ description: 'Сумма строки' }) + @IsString() + amount!: string; + + @Field({ description: 'Тип получателя (SELF / MEMBER / ORG)' }) + @IsString() + recipient_type!: 'SELF' | 'MEMBER' | 'ORG'; + + @Field({ description: 'Способ оплаты (ADVANCE / DIRECT)' }) + @IsString() + mechanics!: 'ADVANCE' | 'DIRECT'; +} + +@InputType('ExpenseProposalHeaderInput') +class ExpenseProposalHeaderInputDTO implements HeaderAction { + @Field({ description: 'Описание цели расходов' }) + @IsString() + description!: string; + + @Field({ description: 'Итоговая сумма расходов' }) + @IsString() + total_amount!: string; + + @Field(() => Int, { description: 'Количество позиций' }) + @IsInt() + items_count!: number; + + @Field({ description: 'Кошелёк-источник' }) + @IsString() + source_wallet!: string; + + @Field({ description: 'Срок исполнения («в срок до»), формат DD.MM.YYYY', nullable: true }) + @IsOptional() + @IsString() + deadline?: string; + + @Field({ + description: 'Фонд списания — подставляется сервером из параметров шасси расходов, передавать не нужно', + nullable: true, + }) + @IsOptional() + @IsString() + fund_name?: string; +} + +/** + * База ВХОДА генерации — богатые позиции (приватные поля уйдут в doc_data на + * сервере). `doc_data_hash` здесь нет: его вычисляет сервер при генерации. + */ +@InputType('BaseExpenseProposalStatementGenerateMetaDocumentInput') +class BaseExpenseProposalStatementGenerateMetaDocumentInputDTO { + @Field({ description: 'Хеш сметы расхода (детерминированный)' }) + @IsString() + @IsNotEmpty() + proposal_hash!: string; + + @Field(() => ExpenseProposalHeaderInputDTO, { description: 'Шапка СЗ' }) + @ValidateNested() + @Type(() => ExpenseProposalHeaderInputDTO) + proposal!: ExpenseProposalHeaderInputDTO; + + @Field(() => [ExpenseProposalItemInputDTO], { description: 'Позиции расхода' }) + @IsArray() + @ArrayMinSize(1) + @ValidateNested({ each: true }) + @Type(() => ExpenseProposalItemInputDTO) + items!: ExpenseProposalItemInputDTO[]; +} + +/** + * База ПОДПИСАННОЙ meta — ровно то, что подписывается и едет on-chain: + * публичные позиции + `doc_data_hash` (реквизиты off-chain). + */ +@InputType('BaseExpenseProposalStatementSignedMetaDocumentInput') +class BaseExpenseProposalStatementSignedMetaDocumentInputDTO implements Omit { + @Field({ description: 'Хеш сметы расхода (детерминированный)' }) + @IsString() + @IsNotEmpty() + proposal_hash!: string; + + @Field(() => ExpenseProposalHeaderInputDTO, { description: 'Шапка СЗ' }) + @ValidateNested() + @Type(() => ExpenseProposalHeaderInputDTO) + proposal!: ExpenseProposalHeaderInputDTO; + + @Field(() => [ExpenseProposalSignedItemInputDTO], { description: 'Публичные позиции расхода (без реквизитов)' }) + @IsArray() + @ArrayMinSize(1) + @ValidateNested({ each: true }) + @Type(() => ExpenseProposalSignedItemInputDTO) + items!: ExpenseProposalSignedItemInputDTO[]; + + @Field({ description: 'Идентификатор приватных данных документа off-chain (реквизиты/имя/назначение)' }) + @IsString() + @IsNotEmpty() + doc_data_hash!: string; +} + +/** + * Input генерации документа СЗ-заявления (registry 2010). + * Backend через factory собирает PDF, возвращает `IGeneratedDocument` (без подписей). + */ +@InputType('ExpenseProposalStatementGenerateDocumentInput') +export class ExpenseProposalStatementGenerateDocumentInputDTO extends IntersectionType( + BaseExpenseProposalStatementGenerateMetaDocumentInputDTO, + OmitType(GenerateMetaDocumentInputDTO, ['registry_id'] as const) +) { + registry_id!: number; +} + +@InputType('ExpenseProposalStatementSignedMetaDocumentInput') +export class ExpenseProposalStatementSignedMetaDocumentInputDTO extends IntersectionType( + BaseExpenseProposalStatementSignedMetaDocumentInputDTO, + MetaDocumentInputDTO +) {} + +/** + * Подписанный документ СЗ-заявления (registry 2010). + */ +@InputType('ExpenseProposalStatementSignedDocumentInput') +export class ExpenseProposalStatementSignedDocumentInputDTO extends SignedDigitalDocumentInputDTO { + @Field(() => ExpenseProposalStatementSignedMetaDocumentInputDTO, { + description: 'Метаинформация СЗ-заявления', + }) + public readonly meta!: ExpenseProposalStatementSignedMetaDocumentInputDTO; +} diff --git a/components/controller/src/application/gateway/dto/payment-filters-input.dto.ts b/components/controller/src/application/gateway/dto/payment-filters-input.dto.ts index 1b291e09da5..b4b3288308a 100644 --- a/components/controller/src/application/gateway/dto/payment-filters-input.dto.ts +++ b/components/controller/src/application/gateway/dto/payment-filters-input.dto.ts @@ -28,4 +28,10 @@ export class PaymentFiltersInputDTO implements PaymentFiltersDomainInterface { @Field(() => String, { nullable: true, description: 'Хэш платежа' }) hash?: string; + + @Field(() => String, { + nullable: true, + description: 'Хэш расхода (служебной записки): вернёт все платежи, связанные с этим расходом', + }) + proposal_hash?: string; } diff --git a/components/controller/src/application/gateway/interactors/gateway.interactor.ts b/components/controller/src/application/gateway/interactors/gateway.interactor.ts index 82beef4307d..6b452fde234 100644 --- a/components/controller/src/application/gateway/interactors/gateway.interactor.ts +++ b/components/controller/src/application/gateway/interactors/gateway.interactor.ts @@ -26,6 +26,7 @@ import { AccountType } from '~/application/account/enum/account-type.enum'; import { PaymentMethodRepository, PAYMENT_METHOD_REPOSITORY } from '~/domain/common/repositories/payment-method.repository'; import type { PaymentDetailsDomainInterface } from '~/domain/gateway/interfaces/payment-domain.interface'; import { AccountDomainPort, ACCOUNT_DOMAIN_PORT } from '~/domain/account/ports/account-domain.port'; +import { INTER_EXPENSE_CHASSIS, type InterExpenseChassisPort } from '@coopenomics/inter'; /** * Интерактор домена gateway для управления платежами (просмотр, изменение статуса и создание) @@ -54,7 +55,9 @@ export class GatewayInteractor { @Inject(ACCOUNT_DOMAIN_SERVICE) private readonly accountDomainService: AccountDomainService, @Inject(PAYMENT_METHOD_REPOSITORY) - private readonly paymentMethodRepository: PaymentMethodRepository + private readonly paymentMethodRepository: PaymentMethodRepository, + @Inject(INTER_EXPENSE_CHASSIS) + private readonly expenseChassis: InterExpenseChassisPort ) {} /** @@ -185,6 +188,25 @@ export class GatewayInteractor { await this.paymentRepository.update(payment.id, { status: PaymentStatusEnum.COMPLETED }); } this.logger.log(`Пользователь ${payment.username} внес паевой взнос на сумму ${blockchainFormattedQuantity}`); + } else if (payment.type === PaymentTypeEnum.EXPENSE_RETURN) { + // Возврат неиспользованного аванса под отчёт (недорасход): пайщик отчитался + // о факте меньше выданного аванса и вернул разницу на расчётный счёт. + // Подтверждение приёма средств кассиром проводит on-chain + // expense::returnexp (TRANSFER подотчёт → пул на сумму возврата), затем + // expense::reportexp закрывает позицию на фактически потраченную часть. + // Это НЕ паевой взнос — completeIncome здесь не вызывается (другая ledger2- + // семантика). proposal_hash и item_hash — в blockchain_data. + const bc = payment.blockchain_data as { proposal_hash?: string; item_hash?: string } | undefined; + if (!bc?.proposal_hash || !bc?.item_hash) { + throw new Error(`У платежа возврата ${payment.hash} нет proposal_hash/item_hash в blockchain_data`); + } + const returnAmount = QuantityUtils.formatQuantityForBlockchain(payment.quantity, payment.symbol); + await this.expenseChassis.returnItem(payment.coopname, bc.proposal_hash, bc.item_hash, returnAmount); + await this.expenseChassis.reportItem(payment.coopname, bc.proposal_hash, bc.item_hash); + if (payment.id) { + await this.paymentRepository.update(payment.id, { status: PaymentStatusEnum.COMPLETED }); + } + this.logger.log(`Возврат недорасхода ${payment.hash} (СЗ ${bc.proposal_hash}) проведён on-chain (returnexp + reportexp)`); } } catch (e: any) { if (payment.id) { @@ -246,6 +268,62 @@ export class GatewayInteractor { return; } + // Оплата позиции СЗ-расхода: подтверждение кассой проводит on-chain + // expense::payexp (ledger2-проводка по механике позиции — аванс/оплата по + // счёту). Реквизиты и назначение платежа в чейн не уходят — они живут + // только в этом платеже и снимке шасси. proposal_hash — в blockchain_data + // (hash платежа = item_hash, его одного payexp недостаточно). + if (payment.type === PaymentTypeEnum.EXPENSE) { + const proposalHash = (payment.blockchain_data as { proposal_hash?: string } | undefined)?.proposal_hash; + try { + if (!proposalHash) { + throw new Error(`У платежа расхода ${payment.hash} отсутствует proposal_hash в blockchain_data`); + } + const actualAmount = QuantityUtils.formatQuantityForBlockchain(payment.quantity, payment.symbol); + await this.expenseChassis.payItem(payment.coopname, proposalHash, payment.hash, actualAmount); + if (payment.id) { + await this.paymentRepository.update(payment.id, { status: PaymentStatusEnum.COMPLETED }); + } + this.logger.log(`Оплата позиции расхода ${payment.hash} (СЗ ${proposalHash}) проведена on-chain (payexp)`); + } catch (e: any) { + const message = e?.message ?? String(e); + if (payment.id) { + await this.paymentRepository.update(payment.id, { status: PaymentStatusEnum.FAILED, message }); + } + this.logger.error(`Ошибка оплаты позиции расхода ${payment.hash}: ${message}`, e); + } + return; + } + + // Доплата при перерасходе аванса под отчёт: пайщик отчитался о факте больше + // выданного аванса, кооператив доплачивает разницу. Подтверждение выплаты + // кассиром проводит on-chain expense::overspendexp (TRANSFER пул → подотчёт на + // сумму доплаты), затем expense::reportexp закрывает позицию на полный факт. + // proposal_hash и item_hash — в blockchain_data (hash платежа уникальный, не + // равен item_hash, чтобы не пересекаться с платежом выдачи аванса). + if (payment.type === PaymentTypeEnum.EXPENSE_OVERSPEND) { + const bc = payment.blockchain_data as { proposal_hash?: string; item_hash?: string } | undefined; + try { + if (!bc?.proposal_hash || !bc?.item_hash) { + throw new Error(`У платежа доплаты ${payment.hash} нет proposal_hash/item_hash в blockchain_data`); + } + const overspendAmount = QuantityUtils.formatQuantityForBlockchain(payment.quantity, payment.symbol); + await this.expenseChassis.overspendItem(payment.coopname, bc.proposal_hash, bc.item_hash, overspendAmount); + await this.expenseChassis.reportItem(payment.coopname, bc.proposal_hash, bc.item_hash); + if (payment.id) { + await this.paymentRepository.update(payment.id, { status: PaymentStatusEnum.COMPLETED }); + } + this.logger.log(`Доплата перерасхода ${payment.hash} (СЗ ${bc.proposal_hash}) проведена on-chain (overspendexp + reportexp)`); + } catch (e: any) { + const message = e?.message ?? String(e); + if (payment.id) { + await this.paymentRepository.update(payment.id, { status: PaymentStatusEnum.FAILED, message }); + } + this.logger.error(`Ошибка доплаты перерасхода ${payment.hash}: ${message}`, e); + } + return; + } + try { const completeOutcomeData: CompleteOutcomeDomainInterface = { coopname: payment.coopname, diff --git a/components/controller/src/application/gateway/services/payment-notification.service.ts b/components/controller/src/application/gateway/services/payment-notification.service.ts index b9def0d46c0..2f7b83f7c8f 100644 --- a/components/controller/src/application/gateway/services/payment-notification.service.ts +++ b/components/controller/src/application/gateway/services/payment-notification.service.ts @@ -55,9 +55,10 @@ export class PaymentNotificationService implements OnModuleInit { // Получаем отображаемое имя пользователя const userName = await this.accountPort.getDisplayName(payment.username); - // Выбираем workflow по статусу И направлению платежа: исходящий PAID — это - // возврат взноса пайщику, у него своё уведомление «Возврат выполнен», а не - // «Платёж принят» (иначе пайщик при возврате получает письмо как на приём). + // Выбираем workflow по статусу И направлению — но БЕЗ привязки к типу платежа + // (мы не всегда знаем, это возврат взноса, аванс под отчёт или доплата). + // Исходящий PAID → пайщик получил деньги → «Платёж выполнен»; входящий PAID → + // кооператив принял платёж пайщика → «Платёж принят». Оба текста универсальны. const isOutgoing = payment.direction === PaymentDirectionEnum.OUTGOING; const workflowId = payment.status === PaymentStatusEnum.PAID diff --git a/components/controller/src/domain/document/ports/generator.port.ts b/components/controller/src/domain/document/ports/generator.port.ts index 1042fdabbfb..2194690d398 100644 --- a/components/controller/src/domain/document/ports/generator.port.ts +++ b/components/controller/src/domain/document/ports/generator.port.ts @@ -16,6 +16,10 @@ export interface GeneratorPort { list(collection: string, filter?: Record): Promise>; getHistory(collection: string, filter: Record): Promise; search(query: string): Promise; + + saveDocData

>(payload: P, registry_id: number): Promise<{ hash: string }>; + + getDocData

>(hash: string): Promise

; } export const GENERATOR_PORT = Symbol('GeneratorPort'); diff --git a/components/controller/src/domain/document/services/document-domain.service.ts b/components/controller/src/domain/document/services/document-domain.service.ts index aa47d843ba6..e78bf187f54 100644 --- a/components/controller/src/domain/document/services/document-domain.service.ts +++ b/components/controller/src/domain/document/services/document-domain.service.ts @@ -25,6 +25,14 @@ export class DocumentDomainService { return await this.generatorInfrastructureService.generateDocument(data); } + public async saveDocData

>(payload: P, registry_id: number): Promise<{ hash: string }> { + return await this.generatorInfrastructureService.saveDocData(payload, registry_id); + } + + public async getDocData

>(hash: string): Promise

{ + return await this.generatorInfrastructureService.getDocData

(hash); + } + public async getDocumentByHash(hash: string): Promise { const document = await this.documentRepository.findByHash(hash); return document; diff --git a/components/controller/src/domain/gateway/entities/payment-domain.entity.ts b/components/controller/src/domain/gateway/entities/payment-domain.entity.ts index 4bbaeb1605d..89b23790bca 100644 --- a/components/controller/src/domain/gateway/entities/payment-domain.entity.ts +++ b/components/controller/src/domain/gateway/entities/payment-domain.entity.ts @@ -114,6 +114,9 @@ export class PaymentDomainEntity implements PaymentDomainInterface { [PaymentTypeEnum.WITHDRAWAL]: 'Возврат взноса', [PaymentTypeEnum.REGISTRATION_REFUND]: 'Возврат вступит. и мин.паевого взноса', [PaymentTypeEnum.MEMBERSHIP_EXIT]: 'Возврат паевого взноса при выходе из кооператива', + [PaymentTypeEnum.EXPENSE]: 'Оплата расхода по служебной записке', + [PaymentTypeEnum.EXPENSE_RETURN]: 'Возврат неиспользованного аванса под отчёт', + [PaymentTypeEnum.EXPENSE_OVERSPEND]: 'Доплата по перерасходу аванса', }; return typeLabels[this.type] || this.type; } diff --git a/components/controller/src/domain/gateway/enums/payment-type.enum.ts b/components/controller/src/domain/gateway/enums/payment-type.enum.ts index 9fc25408df2..2055b0be300 100644 --- a/components/controller/src/domain/gateway/enums/payment-type.enum.ts +++ b/components/controller/src/domain/gateway/enums/payment-type.enum.ts @@ -13,6 +13,20 @@ export enum PaymentTypeEnum { // Отдельный тип, не WITHDRAWAL — это полный выход с блокировкой аккаунта, // а не частичный возврат паевого действующему пайщику. MEMBERSHIP_EXIT = 'membership_exit', + // Исходящая оплата позиции служебной записки-сметы (шасси expense). Создаётся + // автоматически после авторизации СЗ советом; подтверждение кассиром проводит + // on-chain оплату expense::payexp по позиции. + EXPENSE = 'expense', + // Входящий возврат неиспользованного аванса под отчёт (недорасход). Создаётся, + // когда пайщик отчитался о факте меньше выданного аванса: он возвращает разницу + // на расчётный счёт кооператива; подтверждение кассиром (приём средств) + // проводит on-chain expense::returnexp и закрывает позицию expense::reportexp. + EXPENSE_RETURN = 'expense_return', + // Исходящая доплата при перерасходе аванса под отчёт. Создаётся, когда пайщик + // отчитался о факте больше выданного аванса: кооператив доплачивает разницу; + // подтверждение кассиром (выплата) проводит on-chain expense::overspendexp и + // закрывает позицию expense::reportexp. + EXPENSE_OVERSPEND = 'expense_overspend', } /** @@ -32,6 +46,9 @@ export const PAYMENT_TYPE_LABELS: Record = { [PaymentTypeEnum.WITHDRAWAL]: 'Возврат паевого взноса', [PaymentTypeEnum.REGISTRATION_REFUND]: 'Возврат вступит. и мин.паевого взноса', [PaymentTypeEnum.MEMBERSHIP_EXIT]: 'Возврат паевого взноса при выходе из кооператива', + [PaymentTypeEnum.EXPENSE]: 'Оплата расхода по служебной записке', + [PaymentTypeEnum.EXPENSE_RETURN]: 'Возврат неиспользованного аванса под отчёт', + [PaymentTypeEnum.EXPENSE_OVERSPEND]: 'Доплата по перерасходу аванса', }; /** @@ -54,15 +71,17 @@ export const PAYMENT_DIRECTION_LABELS: Record = { * Определяет направление платежа по его типу */ export function getPaymentDirection(type: PaymentTypeEnum): PaymentDirectionEnum { - const incomingTypes = [PaymentTypeEnum.REGISTRATION, PaymentTypeEnum.DEPOSIT]; - - return incomingTypes.includes(type) ? PaymentDirectionEnum.INCOMING : PaymentDirectionEnum.OUTGOING; + return INCOMING_PAYMENT_TYPES.includes(type) ? PaymentDirectionEnum.INCOMING : PaymentDirectionEnum.OUTGOING; } /** * Входящие типы платежей */ -export const INCOMING_PAYMENT_TYPES = [PaymentTypeEnum.REGISTRATION, PaymentTypeEnum.DEPOSIT]; +export const INCOMING_PAYMENT_TYPES = [ + PaymentTypeEnum.REGISTRATION, + PaymentTypeEnum.DEPOSIT, + PaymentTypeEnum.EXPENSE_RETURN, +]; /** * Исходящие типы платежей @@ -71,4 +90,6 @@ export const OUTGOING_PAYMENT_TYPES = [ PaymentTypeEnum.WITHDRAWAL, PaymentTypeEnum.REGISTRATION_REFUND, PaymentTypeEnum.MEMBERSHIP_EXIT, + PaymentTypeEnum.EXPENSE, + PaymentTypeEnum.EXPENSE_OVERSPEND, ]; diff --git a/components/controller/src/domain/gateway/interfaces/payment-filters-domain.interface.ts b/components/controller/src/domain/gateway/interfaces/payment-filters-domain.interface.ts index 35218f7794d..e0f7b6efeea 100644 --- a/components/controller/src/domain/gateway/interfaces/payment-filters-domain.interface.ts +++ b/components/controller/src/domain/gateway/interfaces/payment-filters-domain.interface.ts @@ -13,6 +13,7 @@ export interface PaymentFiltersDomainInterface { direction?: PaymentDirectionEnum; provider?: string; hash?: string; + proposal_hash?: string; } /** diff --git a/components/controller/src/domain/process-registry/config/process-hash-locator.ts b/components/controller/src/domain/process-registry/config/process-hash-locator.ts index ca82f53ffcd..51afe2cb5d2 100644 --- a/components/controller/src/domain/process-registry/config/process-hash-locator.ts +++ b/components/controller/src/domain/process-registry/config/process-hash-locator.ts @@ -119,6 +119,16 @@ export const PROCESS_HASH_LOCATOR: Readonly> = Ob // Сущностная таблица — `capital::prgwithdraws.withdraw_hash`. 'p.cap.wthcap': [{ code: 'capital', table: 'prgwithdraws', field: 'withdraw_hash' }], + // p.cap.pgexp — пополнение пула программных расходов (o.cap.pgtop, ISSUE + // w.cap.pgexp). Одноактовый, process_hash синтетический (sha256 в контракте) — + // entity-таблицы нет, данные из blockchain_actions. + 'p.cap.pgexp': [], + + // p.exp.expns — расход по СЗ (шасси expense): o.exp.blgadv/blgdir (оплата) + + // опц. o.exp.over + o.exp.advrpt (отчёт) + опц. o.exp.advret (возврат). + // Анкер процесса — proposal_hash в `expense::proposals`. + 'p.exp.expns': [{ code: 'expense', table: 'proposals', field: 'proposal_hash' }], + // marketplace::requests.hash — поле так и называется `hash`. 'p.mkt.reqst': [{ code: 'marketplace', table: 'requests', field: 'hash' }], diff --git a/components/controller/src/extensions/capital/application/dto/contract_management/config-output.dto.ts b/components/controller/src/extensions/capital/application/dto/contract_management/config-output.dto.ts index 1d2b81f594e..7c24e4244ad 100644 --- a/components/controller/src/extensions/capital/application/dto/contract_management/config-output.dto.ts +++ b/components/controller/src/extensions/capital/application/dto/contract_management/config-output.dto.ts @@ -76,6 +76,18 @@ export class StateOutputDTO extends BaseOutputDTO { }) global_available_invest_pool!: string; + @Field(() => String, { + nullable: true, + description: 'Пул программных расходов — средства, переведённые под целевые расходы программы', + }) + program_expense_pool?: string; + + @Field(() => String, { + nullable: true, + description: 'Зарезервировано в пуле программных расходов под активные расходы (служебные записки в работе)', + }) + program_expense_reserved?: string; + @Field(() => String, { description: 'Общая сумма членских взносов по программе', }) diff --git a/components/controller/src/extensions/capital/application/dto/program_expenses/create-program-expense.input.ts b/components/controller/src/extensions/capital/application/dto/program_expenses/create-program-expense.input.ts new file mode 100644 index 00000000000..266c82873ec --- /dev/null +++ b/components/controller/src/extensions/capital/application/dto/program_expenses/create-program-expense.input.ts @@ -0,0 +1,47 @@ +import { Field, InputType } from '@nestjs/graphql'; +import { ArrayMinSize, IsArray, IsNotEmpty, IsString, ValidateNested } from 'class-validator'; +import { Type } from 'class-transformer'; +import { ExpenseItemInputDTO } from '~/extensions/expenses/application/dto/expense-item.input'; +import { ExpenseProposalStatementSignedDocumentInputDTO } from '~/application/document/documents-dto/expense-proposal-statement-document.dto'; + +/** + * Создание программного расхода капитала через шасси `expense`. + * Capital резервирует `program_expense_pool` на сумму items и шлёт inline + * action `expense::createexp` с callback `{capital, onpgexpdone}`. Дальше + * lifecycle обслуживает шасси; capital получает callback на финализацию. + */ +@InputType('CapitalCreateProgramExpenseInput') +export class CreateProgramExpenseInputDTO { + @Field(() => String, { description: 'Имя кооператива.' }) + @IsNotEmpty() + @IsString() + coopname!: string; + + @Field(() => String, { description: 'Хэш СЗ-расхода (детерминированный, из UI). Он же станет proposal_hash в шасси.' }) + @IsNotEmpty() + @IsString() + expense_hash!: string; + + @Field(() => String, { description: 'Имя пайщика-создателя СЗ (председатель).' }) + @IsNotEmpty() + @IsString() + creator!: string; + + @Field(() => [ExpenseItemInputDTO], { description: 'Строки расхода. Способ оплаты (аванс под отчёт / оплата по счёту) задаётся на каждой строке отдельно.' }) + @IsArray() + @ArrayMinSize(1) + @ValidateNested({ each: true }) + @Type(() => ExpenseItemInputDTO) + items!: ExpenseItemInputDTO[]; + + @Field(() => String, { description: 'Описание программного расхода.' }) + @IsString() + description!: string; + + @Field(() => ExpenseProposalStatementSignedDocumentInputDTO, { + description: 'Подписанная СЗ-смета (document2, registry 2010).', + }) + @ValidateNested() + @Type(() => ExpenseProposalStatementSignedDocumentInputDTO) + statement!: ExpenseProposalStatementSignedDocumentInputDTO; +} diff --git a/components/controller/src/extensions/capital/application/dto/program_expenses/program-expense.output.ts b/components/controller/src/extensions/capital/application/dto/program_expenses/program-expense.output.ts new file mode 100644 index 00000000000..bb5c7be8878 --- /dev/null +++ b/components/controller/src/extensions/capital/application/dto/program_expenses/program-expense.output.ts @@ -0,0 +1,51 @@ +import { Field, ObjectType } from '@nestjs/graphql'; +import { ExpenseProposalStatus } from '~/extensions/expenses/domain/enums/expense-proposal-status.enum'; +import { ExpenseMechanics } from '~/extensions/expenses/domain/enums/expense-mechanics.enum'; +import { ExpenseRecipientType } from '~/extensions/expenses/domain/enums/expense-recipient-type.enum'; +import { ExpenseItemStatus } from '~/extensions/expenses/domain/enums/expense-item-status.enum'; + +/** Owner-callback из шасси (зеркало InterExpenseCallbackHandler). */ +@ObjectType('CapitalProgramExpenseCallback') +export class ProgramExpenseCallbackOutputDTO { + @Field(() => String) contract!: string; + @Field(() => String) action!: string; + @Field(() => String) data!: string; +} + +/** Item в выводе (зеркало InterExpenseItem). */ +@ObjectType('CapitalProgramExpenseItem') +export class ProgramExpenseItemOutputDTO { + @Field(() => String) item_hash!: string; + @Field(() => ExpenseMechanics) mechanics!: ExpenseMechanics; + @Field(() => ExpenseRecipientType) recipient_type!: ExpenseRecipientType; + @Field(() => String) recipient!: string; + @Field(() => String, { description: 'Имя получателя (ФИО пайщика или название организации)' }) + recipient_name!: string; + @Field(() => String) description!: string; + @Field(() => String) planned_amount!: string; + @Field(() => String) actual_amount!: string; + @Field(() => ExpenseItemStatus) status!: ExpenseItemStatus; +} + +/** + * Программный расход в проекции Капитала — те же данные шасси, спроецированные + * под consumer-extension. Capital НЕ хранит свой mirror items/total — берёт через + * inter-порт INTER_EXPENSE_CHASSIS. + */ +@ObjectType('CapitalProgramExpense') +export class ProgramExpenseOutputDTO { + @Field(() => String) coopname!: string; + @Field(() => String) expense_hash!: string; + @Field(() => String) creator!: string; + @Field(() => String, { description: 'Имя инициатора (ФИО пайщика или название организации)' }) + creator_name!: string; + @Field(() => String) source_wallet!: string; + @Field(() => ExpenseProposalStatus) status!: ExpenseProposalStatus; + @Field(() => ProgramExpenseCallbackOutputDTO, { nullable: true }) + callback?: ProgramExpenseCallbackOutputDTO; + @Field(() => [ProgramExpenseItemOutputDTO]) items!: ProgramExpenseItemOutputDTO[]; + @Field(() => String) total_planned!: string; + @Field(() => String) total_actual!: string; + @Field(() => String) created_at!: string; + @Field(() => String) updated_at!: string; +} diff --git a/components/controller/src/extensions/capital/application/dto/program_expenses/topup-program-expense.input.ts b/components/controller/src/extensions/capital/application/dto/program_expenses/topup-program-expense.input.ts new file mode 100644 index 00000000000..a1783bbfe4c --- /dev/null +++ b/components/controller/src/extensions/capital/application/dto/program_expenses/topup-program-expense.input.ts @@ -0,0 +1,19 @@ +import { Field, InputType } from '@nestjs/graphql'; +import { IsNotEmpty, IsString } from 'class-validator'; + +/** + * Пополнение пула программных расходов кооператива (председатель). + * Перевод суммы из `global_available_invest_pool` в `program_expense_pool`. + */ +@InputType('CapitalTopupProgramExpenseInput') +export class TopupProgramExpenseInputDTO { + @Field(() => String, { description: 'Имя кооператива.' }) + @IsNotEmpty() + @IsString() + coopname!: string; + + @Field(() => String, { description: 'Сумма пополнения (asset, eg "10000.0000 RUB").' }) + @IsNotEmpty() + @IsString() + amount!: string; +} diff --git a/components/controller/src/extensions/capital/application/resolvers/program-expenses.resolver.ts b/components/controller/src/extensions/capital/application/resolvers/program-expenses.resolver.ts new file mode 100644 index 00000000000..d79aed2a799 --- /dev/null +++ b/components/controller/src/extensions/capital/application/resolvers/program-expenses.resolver.ts @@ -0,0 +1,80 @@ +import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; +import { UseGuards } from '@nestjs/common'; +import { GqlJwtAuthGuard } from '~/application/auth/guards/graphql-jwt-auth.guard'; +import { RolesGuard } from '~/application/auth/guards/roles.guard'; +import { AuthRoles } from '~/application/auth/decorators/auth.decorator'; +import { TransactionDTO } from '~/application/common/dto/transaction-result-response.dto'; +import { + createPaginationResult, + PaginationInputDTO, + PaginationResult, +} from '~/application/common/dto/pagination.dto'; +import { ProgramExpensesManagementService } from '../services/program-expenses-management.service'; +import { CreateProgramExpenseInputDTO } from '../dto/program_expenses/create-program-expense.input'; +import { TopupProgramExpenseInputDTO } from '../dto/program_expenses/topup-program-expense.input'; +import { ProgramExpenseOutputDTO } from '../dto/program_expenses/program-expense.output'; + +const paginatedProgramExpensesResult = createPaginationResult( + ProgramExpenseOutputDTO, + 'PaginatedCapitalProgramExpenses', +); + +/** + * GraphQL-резолвер программных расходов капитала. Председатель/совет. + * Write → on-chain capital; read → шасси через inter-порт. + */ +@Resolver() +export class ProgramExpensesResolver { + constructor(private readonly service: ProgramExpensesManagementService) {} + + @Mutation(() => TransactionDTO, { + name: 'capitalCreateProgramExpense', + description: 'Создание программного расхода капитала через шасси expense.', + }) + @UseGuards(GqlJwtAuthGuard, RolesGuard) + @AuthRoles(['chairman', 'member']) + async createProgramExpense( + @Args('data', { type: () => CreateProgramExpenseInputDTO }) data: CreateProgramExpenseInputDTO, + ): Promise { + return this.service.createProgramExpense(data); + } + + @Mutation(() => TransactionDTO, { + name: 'capitalTopupProgramExpensePool', + description: 'Пополнение пула программных расходов капитала из инвестиционного пула.', + }) + @UseGuards(GqlJwtAuthGuard, RolesGuard) + @AuthRoles(['chairman']) + async topupProgramExpense( + @Args('data', { type: () => TopupProgramExpenseInputDTO }) data: TopupProgramExpenseInputDTO, + ): Promise { + return this.service.topupProgramExpense(data); + } + + @Query(() => paginatedProgramExpensesResult, { + name: 'capitalProgramExpenses', + description: 'Список программных расходов капитала (через шасси expense).', + }) + @UseGuards(GqlJwtAuthGuard, RolesGuard) + @AuthRoles(['chairman', 'member']) + async listProgramExpenses( + @Args('coopname', { type: () => String }) coopname: string, + @Args('options', { type: () => PaginationInputDTO, nullable: true }) options?: PaginationInputDTO, + ): Promise> { + return this.service.listProgramExpenses(coopname, options); + } + + @Query(() => ProgramExpenseOutputDTO, { + name: 'capitalProgramExpense', + description: 'Программный расход по expense_hash.', + nullable: true, + }) + @UseGuards(GqlJwtAuthGuard, RolesGuard) + @AuthRoles(['chairman', 'member']) + async getProgramExpense( + @Args('coopname') coopname: string, + @Args('expense_hash') expenseHash: string, + ): Promise { + return this.service.getProgramExpense(coopname, expenseHash); + } +} diff --git a/components/controller/src/extensions/capital/application/services/program-expenses-management.service.ts b/components/controller/src/extensions/capital/application/services/program-expenses-management.service.ts new file mode 100644 index 00000000000..e6d8b9f914b --- /dev/null +++ b/components/controller/src/extensions/capital/application/services/program-expenses-management.service.ts @@ -0,0 +1,239 @@ +import { Injectable, Inject } from '@nestjs/common'; +import { CapitalContract } from 'cooptypes'; +import type { TransactResult } from '@wharfkit/session'; +import { config } from '~/config'; +import { + INTER_EXPENSE_CHASSIS, + type InterExpenseChassisPort, + type InterExpenseItem, + type InterExpenseProposalRead, + type InterExpenseProposalStatus, + type InterExpenseRequisiteItemInput, +} from '@coopenomics/inter'; +import { AccountDataPort, ACCOUNT_DATA_PORT } from '~/domain/account/ports/account-data.port'; +import { CapitalBlockchainPort, CAPITAL_BLOCKCHAIN_PORT } from '../../domain/interfaces/capital-blockchain.port'; +import { DomainToBlockchainUtils } from '~/shared/utils/domain-to-blockchain.utils'; +import { + buildPaginationResult, + PaginationInputDTO, + paginationInputToOffset, + type PaginationResult, +} from '~/application/common/dto/pagination.dto'; +import type { CreateProgramExpenseInputDTO } from '../dto/program_expenses/create-program-expense.input'; +import type { TopupProgramExpenseInputDTO } from '../dto/program_expenses/topup-program-expense.input'; +import type { + ProgramExpenseCallbackOutputDTO, + ProgramExpenseItemOutputDTO, + ProgramExpenseOutputDTO, +} from '../dto/program_expenses/program-expense.output'; +import { ExpenseProposalStatus } from '~/extensions/expenses/domain/enums/expense-proposal-status.enum'; +import { ExpenseMechanics } from '~/extensions/expenses/domain/enums/expense-mechanics.enum'; +import { ExpenseRecipientType } from '~/extensions/expenses/domain/enums/expense-recipient-type.enum'; +import { ExpenseItemStatus } from '~/extensions/expenses/domain/enums/expense-item-status.enum'; + +/** + * Управление программными расходами Капитала. + * + * Тонкое расширение: write — `capital::createpgexp` / `topupprogexp` через + * `CapitalBlockchainPort`; read — через inter-порт `INTER_EXPENSE_CHASSIS` + * (шасси-extension отвечает за хранение proposals). + */ +@Injectable() +export class ProgramExpensesManagementService { + constructor( + @Inject(CAPITAL_BLOCKCHAIN_PORT) + private readonly capitalBlockchainPort: CapitalBlockchainPort, + @Inject(INTER_EXPENSE_CHASSIS) + private readonly expenseChassis: InterExpenseChassisPort, + @Inject(ACCOUNT_DATA_PORT) + private readonly accountDataPort: AccountDataPort, + private readonly domainToBlockchainUtils: DomainToBlockchainUtils, + ) {} + + async createProgramExpense(data: CreateProgramExpenseInputDTO): Promise { + // Реквизиты получателей: валидация ДО on-chain заявки, снимок в шасси — + // ПОСЛЕ (фиксация «куда платить» на момент создания СЗ). + const requisiteItems: InterExpenseRequisiteItemInput[] = data.items.map((it) => ({ + proposalHash: data.expense_hash, + itemHash: it.item_hash, + recipient: it.recipient, + isOrganization: it.recipient_type === ExpenseRecipientType.ORG, + mechanics: it.mechanics === ExpenseMechanics.DIRECT ? 'DIRECT' : 'ADVANCE', + paymentMethodId: it.payment_method_id, + requisites: it.requisites, + paymentPurpose: it.payment_purpose, + })); + await this.expenseChassis.validateRequisites(data.coopname, requisiteItems); + + const zeroAmount = `0.0000 ${config.blockchain.root_govern_symbol}`; + const blockchainData: CapitalContract.Actions.CreateProgramExpense.ICreateProgramExpense = { + coopname: data.coopname, + expense_hash: data.expense_hash, + creator: data.creator, + items: data.items.map((it) => ({ + item_hash: it.item_hash, + mechanics: it.mechanics === ExpenseMechanics.DIRECT ? 1 : 0, + recipient_type: + it.recipient_type === ExpenseRecipientType.SELF + ? 0 + : it.recipient_type === ExpenseRecipientType.MEMBER + ? 1 + : 2, + recipient: it.recipient, + description: it.description, + planned_amount: it.planned_amount, + actual_amount: zeroAmount, + status: 0, + })), + description: data.description, + statement: this.domainToBlockchainUtils.convertSignedDocumentToBlockchainFormat(data.statement), + }; + const result = await this.capitalBlockchainPort.createProgramExpense(blockchainData); + + await this.expenseChassis.snapshotRequisites(data.coopname, requisiteItems); + + return result; + } + + async topupProgramExpense(data: TopupProgramExpenseInputDTO): Promise { + return this.capitalBlockchainPort.topupProgramExpense({ + coopname: data.coopname, + amount: data.amount, + }); + } + + async listProgramExpenses( + coopname: string, + options?: PaginationInputDTO, + ): Promise> { + const { limit, offset, sortBy, sortOrder } = paginationInputToOffset(options); + const allowedSort = sortBy === 'createdAt' || sortBy === 'updatedAt' ? sortBy : undefined; + + const result = await this.expenseChassis.listProposalsByOwner(coopname, 'capital', 'onpgexpdone', { + limit, + offset, + sortBy: allowedSort, + sortOrder, + }); + + const names = await this.resolveDisplayNames( + result.items.flatMap((p) => [p.creator, ...this.memberRecipients(p)]), + ); + return buildPaginationResult(result, options, (p) => this.toOutput(p, names)); + } + + async getProgramExpense(coopname: string, expenseHash: string): Promise { + const proposal = await this.expenseChassis.readProposalByHash(coopname, expenseHash); + if (!proposal) return null; + const names = await this.resolveDisplayNames([proposal.creator, ...this.memberRecipients(proposal)]); + return this.toOutput(proposal, names); + } + + /** Получатели-пайщики СЗ (у ORG-строк recipient — уже название организации). */ + private memberRecipients(p: InterExpenseProposalRead): string[] { + return p.items + .filter((it) => this.mapRecipientType(it.recipientType) !== ExpenseRecipientType.ORG) + .map((it) => it.recipient); + } + + /** ФИО/название организации по username; при ошибке резолва остаётся username. */ + private async resolveDisplayNames(usernames: string[]): Promise> { + const unique = [...new Set(usernames.filter(Boolean))]; + const entries = await Promise.all( + unique.map(async (username): Promise<[string, string]> => { + try { + const displayName = await this.accountDataPort.getDisplayName(username); + return [username, displayName || username]; + } catch { + return [username, username]; + } + }), + ); + return new Map(entries); + } + + private toOutput(p: InterExpenseProposalRead, names: Map): ProgramExpenseOutputDTO { + return { + coopname: p.coopname, + expense_hash: p.proposalHash, + creator: p.creator, + creator_name: names.get(p.creator) ?? p.creator, + source_wallet: p.sourceWalletCode, + status: this.mapStatus(p.status), + callback: p.callback ? this.toCallbackOutput(p.callback) : undefined, + items: p.items.map((it) => this.toItemOutput(it, names)), + total_planned: p.totalPlanned, + total_actual: p.totalActual, + created_at: p.createdAt, + updated_at: p.updatedAt, + }; + } + + private toItemOutput(it: InterExpenseItem, names: Map): ProgramExpenseItemOutputDTO { + const recipientType = this.mapRecipientType(it.recipientType); + return { + item_hash: it.itemHash, + mechanics: this.mapMechanics(it.mechanics), + recipient_type: recipientType, + recipient: it.recipient, + recipient_name: + recipientType === ExpenseRecipientType.ORG + ? it.recipient + : (names.get(it.recipient) ?? it.recipient), + description: it.description, + planned_amount: it.plannedAmount, + actual_amount: it.actualAmount, + status: this.mapItemStatus(it.status), + }; + } + + private toCallbackOutput(cb: { contract: string; action: string; data: string }): ProgramExpenseCallbackOutputDTO { + return { contract: cb.contract, action: cb.action, data: cb.data }; + } + + private mapStatus(status: InterExpenseProposalStatus): ExpenseProposalStatus { + switch (status) { + case 'CREATED': + return ExpenseProposalStatus.CREATED; + case 'AUTHORIZED': + return ExpenseProposalStatus.AUTHORIZED; + case 'PARTIALLY_PAID': + return ExpenseProposalStatus.PARTIALLY_PAID; + case 'REPORT_SUBMITTED': + return ExpenseProposalStatus.REPORT_SUBMITTED; + case 'CLOSED': + return ExpenseProposalStatus.CLOSED; + case 'DECLINED': + return ExpenseProposalStatus.DECLINED; + default: + return ExpenseProposalStatus.UNDEFINED; + } + } + + private mapMechanics(raw: number): ExpenseMechanics { + return raw === 1 ? ExpenseMechanics.DIRECT : ExpenseMechanics.ADVANCE; + } + + private mapRecipientType(raw: number): ExpenseRecipientType { + if (raw === 2) return ExpenseRecipientType.ORG; + if (raw === 1) return ExpenseRecipientType.MEMBER; + return ExpenseRecipientType.SELF; + } + + private mapItemStatus(raw: number): ExpenseItemStatus { + switch (raw) { + case 0: + return ExpenseItemStatus.APPROVED; + case 1: + return ExpenseItemStatus.PAID; + case 2: + return ExpenseItemStatus.REPORTED; + case 3: + return ExpenseItemStatus.RETURNED; + case 4: + return ExpenseItemStatus.OVERSPENT; + default: + return ExpenseItemStatus.UNDEFINED; + } + } +} diff --git a/components/controller/src/extensions/capital/application/use-cases/contract-management.interactor.ts b/components/controller/src/extensions/capital/application/use-cases/contract-management.interactor.ts index 5330e4ff258..6c2143fb9f6 100644 --- a/components/controller/src/extensions/capital/application/use-cases/contract-management.interactor.ts +++ b/components/controller/src/extensions/capital/application/use-cases/contract-management.interactor.ts @@ -52,6 +52,8 @@ export class ContractManagementInteractor { return { coopname: blockchainState.coopname, global_available_invest_pool: blockchainState.global_available_invest_pool, + program_expense_pool: blockchainState.program_expense_pool, + program_expense_reserved: blockchainState.program_expense_reserved, program_membership_funded: blockchainState.program_membership_funded, program_membership_available: blockchainState.program_membership_available, program_membership_distributed: blockchainState.program_membership_distributed, diff --git a/components/controller/src/extensions/capital/capital-extension.module.ts b/components/controller/src/extensions/capital/capital-extension.module.ts index 6123ce4255c..8882f124825 100644 --- a/components/controller/src/extensions/capital/capital-extension.module.ts +++ b/components/controller/src/extensions/capital/capital-extension.module.ts @@ -374,6 +374,7 @@ import { VotingService } from './application/services/voting.service'; import { ResultSubmissionService } from './application/services/result-submission.service'; import { DistributionManagementService } from './application/services/distribution-management.service'; import { ExpensesManagementService } from './application/services/expenses-management.service'; +import { ProgramExpensesManagementService } from './application/services/program-expenses-management.service'; import { ContributorAccountSyncService } from './application/services/contributor-account-sync.service'; import { CommitSyncService } from './application/syncers/commit-sync.service'; import { CommitDeltaMapper } from './infrastructure/blockchain/mappers/commit-delta.mapper'; @@ -426,6 +427,7 @@ import { VotingResolver } from './application/resolvers/voting.resolver'; import { ResultSubmissionResolver } from './application/resolvers/result-submission.resolver'; import { DistributionManagementResolver } from './application/resolvers/distribution-management.resolver'; import { ExpensesManagementResolver } from './application/resolvers/expenses-management.resolver'; +import { ProgramExpensesResolver } from './application/resolvers/program-expenses.resolver'; import { SegmentsResolver } from './application/resolvers/segments.resolver'; import { LogResolver } from './application/resolvers/log.resolver'; import { MutationLogMapperService } from './application/services/mutation-log-mapper.service'; @@ -710,6 +712,7 @@ IssueIdGenerationService, ResultSubmissionService, DistributionManagementService, ExpensesManagementService, + ProgramExpensesManagementService, ContributorAccountSyncService, SegmentsService, SegmentMapper, @@ -769,6 +772,7 @@ IssueIdGenerationService, ResultSubmissionResolver, DistributionManagementResolver, ExpensesManagementResolver, + ProgramExpensesResolver, SegmentsResolver, TimeTrackerResolver, LogResolver, diff --git a/components/controller/src/extensions/capital/domain/interfaces/capital-blockchain.port.ts b/components/controller/src/extensions/capital/domain/interfaces/capital-blockchain.port.ts index 24f191b0f63..e2f536b3b18 100644 --- a/components/controller/src/extensions/capital/domain/interfaces/capital-blockchain.port.ts +++ b/components/controller/src/extensions/capital/domain/interfaces/capital-blockchain.port.ts @@ -224,6 +224,22 @@ export interface CapitalBlockchainPort { */ createExpense(data: CapitalContract.Actions.CreateExpense.ICreateExpense): Promise; + /** + * Программный расход через шасси: capital резервирует program_expense_pool + * и шлёт inline action в expense::createexp с callback `{capital, onpgexpdone}`. + */ + createProgramExpense( + data: CapitalContract.Actions.CreateProgramExpense.ICreateProgramExpense, + ): Promise; + + /** + * Пополнение пула программных расходов из доступного остатка + * `global_available_invest_pool` (председатель). + */ + topupProgramExpense( + data: CapitalContract.Actions.TopupProgramExpense.ITopupProgramExpense, + ): Promise; + /** * Редактирование участника CAPITAL контракта */ diff --git a/components/controller/src/extensions/capital/infrastructure/blockchain/adapters/capital-blockchain.adapter.ts b/components/controller/src/extensions/capital/infrastructure/blockchain/adapters/capital-blockchain.adapter.ts index 5a3f42832ed..99603932e23 100644 --- a/components/controller/src/extensions/capital/infrastructure/blockchain/adapters/capital-blockchain.adapter.ts +++ b/components/controller/src/extensions/capital/infrastructure/blockchain/adapters/capital-blockchain.adapter.ts @@ -746,6 +746,38 @@ export class CapitalBlockchainAdapter implements CapitalBlockchainPort { }); } + async createProgramExpense( + data: CapitalContract.Actions.CreateProgramExpense.ICreateProgramExpense, + ): Promise { + const wif = await this.vaultDomainService.getWif(data.coopname); + if (!wif) throw new HttpApiError(httpStatus.BAD_GATEWAY, 'Не найден приватный ключ для совершения операции'); + + this.blockchainService.initialize(data.coopname, wif); + + return await this.blockchainService.transact({ + account: CapitalContract.contractName.production, + name: CapitalContract.Actions.CreateProgramExpense.actionName, + authorization: [{ actor: data.coopname, permission: 'active' }], + data, + }); + } + + async topupProgramExpense( + data: CapitalContract.Actions.TopupProgramExpense.ITopupProgramExpense, + ): Promise { + const wif = await this.vaultDomainService.getWif(data.coopname); + if (!wif) throw new HttpApiError(httpStatus.BAD_GATEWAY, 'Не найден приватный ключ для совершения операции'); + + this.blockchainService.initialize(data.coopname, wif); + + return await this.blockchainService.transact({ + account: CapitalContract.contractName.production, + name: CapitalContract.Actions.TopupProgramExpense.actionName, + authorization: [{ actor: data.coopname, permission: 'active' }], + data, + }); + } + /** * Редактирование участника CAPITAL контракта */ diff --git a/components/controller/src/extensions/expenses/README.md b/components/controller/src/extensions/expenses/README.md new file mode 100644 index 00000000000..0a303457492 --- /dev/null +++ b/components/controller/src/extensions/expenses/README.md @@ -0,0 +1,141 @@ +# Expenses Extension — шасси расходов (C28-31) + +**Статус:** implemented (8 actions полного lifecycle, sync-сервис, GraphQL). + +## Скоуп расширения + +Backend-сторона **шасси системы расходов** (MVP-SINGLE) для Цифрового Кооператива. Расширение: + +- хранит и отдаёт **проектные сущности** (`expense_proposals`, `expense_items`) — синхронизация с контрактом `expenses` (TBA в C28-29); +- хранит и отдаёт **первичные файлы** (`expense_files`) — payment proof, отчёт-чеки, return proof. Файлы — в MinIO через готовую `~/infrastructure/file-storage` инфру; +- агрегирует document2 (2010 СЗ-смета, 2011 протокол-1 — см. cooptypes registry 2010/2011) и `expense_files` в единый ответ `expenseProposalDetails` для UI; +- триггерит capital extension при переходе расхода в финальный статус (capitalization of WIP). + +## Зависимости (порядок сборки) + +1. **C28-28** — P0 коды в `OPERATION_REGISTRY` (`o.exp.*`) — БЛОКИРУЕТ контракт. +2. **C28-29** — контракт `expenses` + cooptypes types: `Expenses.payexp`, `Expenses.reportexp`, `Expenses.returnexp` etc. +3. **C28-30** — ✅ document2 2010/2011 (готово, commit `1fa41b2b39a`). +4. **C28-31** (этот модуль) — TypeORM-entities + sync + GraphQL. +5. **C28-32** — UI шасси (потребляет GraphQL отсюда). + +## Планируемая структура файлов + +``` +extensions/expenses/ +├── README.md ← этот файл +├── expenses-extension.module.ts ← NestJS root module (forwardRef в Capital, File-storage) +├── application/ +│ ├── dto/ +│ │ ├── pay-expense-item.input.ts ← { proposal_hash, item_hash, actual_amount, payment_proof: Upload } +│ │ ├── upload-report-file.input.ts ← { proposal_hash, item_hash?, kind, file: Upload } +│ │ ├── submit-report.input.ts ← { proposal_hash, total_actual_amount } +│ │ └── return-expense.input.ts ← { proposal_hash, item_hash, return_proof: Upload } +│ └── resolvers/ +│ ├── expense-proposal.resolver.ts ← Query: expenseProposalDetails(proposal_hash) +│ ├── expense-files.resolver.ts ← Query: expenseFiles(proposal_hash), Mutation: uploadExpenseFile +│ └── expense-mutations.resolver.ts ← Mutation: payExpenseItem / submitReport / returnExpense / authorizeReport +├── domain/ +│ ├── entities/ +│ │ ├── expense-proposal.entity.ts ← domain model (proposal_hash, status, items[], document2_refs) +│ │ ├── expense-item.entity.ts ← domain model (item_hash, recipient_type, mechanics, amounts) +│ │ └── expense-file.entity.ts ← domain model (file_id, proposal_hash, item_hash?, kind, minio_key, checksum, mime, size, uploaded_at, uploaded_by) +│ ├── enums/ +│ │ ├── expense-proposal-status.enum.ts ← DRAFT | APPROVED | PARTIALLY_PAID | REPORTED | AUTHORIZED | CLOSED | DECLINED +│ │ ├── expense-item-status.enum.ts ← APPROVED | PAID | REPORTED | RETURNED | OVERSPENT +│ │ ├── expense-recipient-type.enum.ts ← SELF | MEMBER | ORG (зеркалит cooptypes ExpenseItemRecipientType) +│ │ ├── expense-payment-mechanics.enum.ts ← ADVANCE | DIRECT (зеркалит cooptypes ExpenseItemPaymentMechanics) +│ │ └── expense-file-kind.enum.ts ← PAYMENT_PROOF | REPORT_FILE | RETURN_PROOF +│ ├── interfaces/ +│ │ ├── expense-blockchain.interface.ts ← IExpenseProposalBlockchainData / IExpenseItemBlockchainData +│ │ └── expense-database.interface.ts ← IExpenseProposalDatabaseData / IExpenseItemDatabaseData / IExpenseFileDatabaseData +│ └── repositories/ +│ ├── expense-proposal.repository.ts ← interface +│ ├── expense-item.repository.ts ← interface +│ └── expense-file.repository.ts ← interface +├── infrastructure/ +│ ├── database/ +│ │ └── expenses-database.module.ts ← TypeOrmModule.forFeature([...]) +│ ├── entities/ +│ │ ├── expense-proposal.typeorm-entity.ts ← coopname, proposal_hash, username, status, total_amount, statement_doc, decision_doc, _created_at, _updated_at +│ │ ├── expense-item.typeorm-entity.ts ← proposal_hash, item_hash, recipient_type, mechanics, planned_amount, actual_amount, recipient_account, status +│ │ └── expense-file.typeorm-entity.ts ← file_id (uuid), proposal_hash, item_hash (nullable), kind, minio_key, checksum, mime, size_bytes, uploaded_by, uploaded_at +│ ├── mappers/ +│ │ ├── expense-proposal.mapper.ts ← domain <-> typeorm +│ │ ├── expense-item.mapper.ts +│ │ └── expense-file.mapper.ts +│ ├── repositories/ +│ │ ├── expense-proposal.typeorm-repository.ts ← extends BaseBlockchainRepository +│ │ ├── expense-item.typeorm-repository.ts ← extends BaseBlockchainRepository +│ │ └── expense-file.typeorm-repository.ts ← plain TypeORM (нет блокчейн-якоря) +│ ├── blockchain/ +│ │ └── mappers/ +│ │ ├── expense-proposal-delta.mapper.ts ← parser2 delta → IExpenseProposalBlockchainData +│ │ └── expense-item-delta.mapper.ts +│ └── services/ +│ ├── expense-files.service.ts ← @UseBucket('expenses:files'), put/getReadUrl/delete +│ ├── document-aggregation.service.ts ← собирает 2010+2011 document2 + expense_files в expenseProposalDetails +│ └── capital-trigger.service.ts ← вызывает capital extension при authorizeReport → capitalization +└── constants/ + └── expenses-bucket.ts ← { name: 'expenses:files', maxBytes, allowedMime, metadataSchema } +``` + +## MinIO bucket spec + +```ts +{ + name: 'expenses:files', + maxBytes: 20 * 1024 * 1024, // 20 МБ на файл (чеки/PDF/фото) + allowedMime: [ + 'image/jpeg', 'image/png', 'image/webp', 'image/heic', + 'application/pdf', + ], + metadataSchema: { + proposalHash: 'required', + itemHash: 'optional', + kind: 'required', // PAYMENT_PROOF | REPORT_FILE | RETURN_PROOF + uploadedBy: 'required', // username + }, + defaultUrlTtlSeconds: 600, // 10 мин на signed read URL +} +``` + +Ключ объекта: `{coopname}/expenses/{proposal_hash}/{item_hash|_proposal}/{kind}/{checksum}.{ext}`. + +## GraphQL-схема (предварительная) + +```graphql +type Query { + expenseProposalDetails(coopname: String!, proposal_hash: String!): ExpenseProposalDetails! + expenseFiles(coopname: String!, proposal_hash: String!): [ExpenseFile!]! +} + +type Mutation { + payExpenseItem(input: PayExpenseItemInput!): ExpenseItem! + submitExpenseReport(input: SubmitReportInput!): ExpenseProposal! + authorizeExpenseReport(coopname: String!, proposal_hash: String!): ExpenseProposal! + declineExpenseReport(coopname: String!, proposal_hash: String!, reason: String!): ExpenseProposal! + returnExpenseItem(input: ReturnExpenseInput!): ExpenseItem! + uploadExpenseFile(input: UploadExpenseFileInput!): ExpenseFile! +} + +type ExpenseProposalDetails { + proposal: ExpenseProposal! + items: [ExpenseItem!]! + files: [ExpenseFile!]! + documents: ExpenseProposalDocuments! # signed 2010 + signed 2011 + events: [ExpenseProposalEvent!]! # таймлайн +} +``` + +## Когда расшивается C28-28 + +Каркас выше можно класть в код **только после** согласования имён `o.exp.*` action-codes и моделей wallets/счетов с пользователем (вопросы заданы в issue C28-28). До этого любой код в `infrastructure/blockchain/mappers/*` опирается на несуществующие cooptypes-типы. + +## Ссылки + +- PRD шасси: `13-platforma-tsifrovogo-kooperativa/components/14-versiya-3/requirements/f8-prd-shassi-sistemy-raskhodov-tsifrovogo-kooperativa-v12.md` +- Issue C28-31: `13-platforma-tsifrovogo-kooperativa/components/14-versiya-3/issues/C28-31-shassi-raskhodov-epik-3-backend-extension-expenses.md` +- File-storage инфра: `~/infrastructure/file-storage/README.md` +- Образец extension с blockchain-sync: `~/extensions/capital/` +- Document2 templates: `components/cooptypes/src/cooperative/registry/2010.ExpenseProposalStatement/`, `2011.ExpenseProposalDecision/` diff --git a/components/controller/src/extensions/expenses/application/dto/create-expense-proposal.input.ts b/components/controller/src/extensions/expenses/application/dto/create-expense-proposal.input.ts new file mode 100644 index 00000000000..3f06b5284e3 --- /dev/null +++ b/components/controller/src/extensions/expenses/application/dto/create-expense-proposal.input.ts @@ -0,0 +1,60 @@ +import { Field, InputType } from '@nestjs/graphql'; +import { ArrayMinSize, IsArray, IsNotEmpty, IsOptional, IsString, ValidateNested } from 'class-validator'; +import { Type } from 'class-transformer'; +import { ExpenseItemInputDTO } from './expense-item.input'; +import { ExpenseCallbackInputDTO } from './expense-callback.input'; +import { ExpenseProposalStatementSignedDocumentInputDTO } from '~/application/document/documents-dto/expense-proposal-statement-document.dto'; + +/** + * Input создания и подачи СЗ-расхода (создатель сметы — пайщик / председатель). + * + * Сабмит: `expense::createexp` через `ExpensesBlockchainPort.createExp`. + * Подпись `statement` (registry 2010) сделана на стороне UI через `Classes.Document`. + */ +@InputType('CreateExpenseProposalInput') +export class CreateExpenseProposalInputDTO { + @Field(() => String, { description: 'Имя кооператива.' }) + @IsNotEmpty() + @IsString() + coopname!: string; + + @Field(() => String, { description: 'Имя пайщика-создателя СЗ.' }) + @IsNotEmpty() + @IsString() + username!: string; + + @Field(() => String, { description: 'Хеш сметы расхода (детерминированный, из UI).' }) + @IsNotEmpty() + @IsString() + proposal_hash!: string; + + @Field(() => String, { + description: 'Источник средств (eosio::name кошелька-источника, eg "w.cap.blago").', + }) + @IsNotEmpty() + @IsString() + source_wallet!: string; + + @Field(() => [ExpenseItemInputDTO], { description: 'Строки расхода (массив items).' }) + @IsArray() + @ArrayMinSize(1) + @ValidateNested({ each: true }) + @Type(() => ExpenseItemInputDTO) + items!: ExpenseItemInputDTO[]; + + @Field(() => ExpenseCallbackInputDTO, { + nullable: true, + description: 'Callback на финализацию closeexp (опционально).', + }) + @IsOptional() + @ValidateNested() + @Type(() => ExpenseCallbackInputDTO) + callback?: ExpenseCallbackInputDTO; + + @Field(() => ExpenseProposalStatementSignedDocumentInputDTO, { + description: 'Подписанная СЗ-смета (document2, registry 2010).', + }) + @ValidateNested() + @Type(() => ExpenseProposalStatementSignedDocumentInputDTO) + statement!: ExpenseProposalStatementSignedDocumentInputDTO; +} diff --git a/components/controller/src/extensions/expenses/application/dto/expense-callback.input.ts b/components/controller/src/extensions/expenses/application/dto/expense-callback.input.ts new file mode 100644 index 00000000000..c7eff71238a --- /dev/null +++ b/components/controller/src/extensions/expenses/application/dto/expense-callback.input.ts @@ -0,0 +1,24 @@ +import { Field, InputType } from '@nestjs/graphql'; +import { IsOptional, IsString } from 'class-validator'; + +/** + * Callback на финализацию `expense::closeexp`. Пустой `contract` (`""`) = нет callback. + * Зеркалит `ExpenseDomain::callback_handler` из `expense.hpp`. + */ +@InputType('ExpenseCallbackInput') +export class ExpenseCallbackInputDTO { + @Field(() => String, { nullable: true, description: 'Контракт-целевой' }) + @IsOptional() + @IsString() + contract?: string; + + @Field(() => String, { nullable: true, description: 'Action-метод' }) + @IsOptional() + @IsString() + action?: string; + + @Field(() => String, { nullable: true, description: 'Payload (hex bytes)' }) + @IsOptional() + @IsString() + data?: string; +} diff --git a/components/controller/src/extensions/expenses/application/dto/expense-file.output.ts b/components/controller/src/extensions/expenses/application/dto/expense-file.output.ts new file mode 100644 index 00000000000..7b958a92d31 --- /dev/null +++ b/components/controller/src/extensions/expenses/application/dto/expense-file.output.ts @@ -0,0 +1,69 @@ +import { Field, Int, ObjectType } from '@nestjs/graphql'; +import { ExpenseFileKind } from '../../domain/enums/expense-file-kind.enum'; +import type { IExpenseFileDatabaseData } from '../../domain/interfaces/expense-file-database.interface'; + +/** + * Output DTO записи о первичном файле расхода в MinIO-бакете. + * + * `read_url` — короткоживущий HMAC-signed URL (TTL = `defaultUrlTtlSeconds` бакета). + * Доменный код выдаёт URL уже после проверки ACL; держатель URL может скачать до истечения TTL. + */ +@ObjectType('ExpenseFile', { description: 'Запись о первичном файле расхода (платёжка/чек/возврат).' }) +export class ExpenseFileOutputDTO { + @Field(() => Int, { description: 'Внутренний ID записи.' }) + id!: number; + + @Field(() => String, { description: 'Имя кооператива (scope).' }) + coopname!: string; + + @Field(() => String, { description: 'Хеш сметы расхода.' }) + proposal_hash!: string; + + @Field(() => String, { nullable: true, description: 'Хеш строки расхода (если файл уровня item).' }) + item_hash?: string; + + @Field(() => ExpenseFileKind, { description: 'Назначение файла.' }) + kind!: ExpenseFileKind; + + @Field(() => String, { description: 'SHA-256 содержимого, hex-lowercase.' }) + checksum_sha256!: string; + + @Field(() => String, { description: 'MIME-тип содержимого.' }) + mime_type!: string; + + @Field(() => Int, { description: 'Размер файла в байтах.' }) + size_bytes!: number; + + @Field(() => String, { description: 'MinIO-ключ внутри бакета.' }) + storage_key!: string; + + @Field(() => String, { nullable: true, description: 'Оригинальное имя загруженного файла.' }) + original_filename?: string; + + @Field(() => String, { description: 'Кто загрузил (username).' }) + uploaded_by_username!: string; + + @Field(() => Date, { description: 'Когда загружено.' }) + uploaded_at!: Date; + + @Field(() => String, { nullable: true, description: 'Короткоживущий URL на скачивание (HMAC-signed).' }) + read_url?: string; + + static fromDomain(data: IExpenseFileDatabaseData, readUrl?: string): ExpenseFileOutputDTO { + const dto = new ExpenseFileOutputDTO(); + dto.id = data.id ?? 0; + dto.coopname = data.coopname; + dto.proposal_hash = data.proposal_hash; + dto.item_hash = data.item_hash ?? undefined; + dto.kind = data.kind; + dto.checksum_sha256 = data.checksum_sha256; + dto.mime_type = data.mime_type; + dto.size_bytes = data.size_bytes; + dto.storage_key = data.storage_key; + dto.original_filename = data.original_filename ?? undefined; + dto.uploaded_by_username = data.uploaded_by_username; + dto.uploaded_at = data.uploaded_at; + dto.read_url = readUrl; + return dto; + } +} diff --git a/components/controller/src/extensions/expenses/application/dto/expense-item.input.ts b/components/controller/src/extensions/expenses/application/dto/expense-item.input.ts new file mode 100644 index 00000000000..2adc03ecc74 --- /dev/null +++ b/components/controller/src/extensions/expenses/application/dto/expense-item.input.ts @@ -0,0 +1,62 @@ +import { Field, InputType } from '@nestjs/graphql'; +import { IsEnum, IsNotEmpty, IsOptional, IsString, ValidateIf } from 'class-validator'; +import { ExpenseMechanics } from '../../domain/enums/expense-mechanics.enum'; +import { ExpenseRecipientType } from '../../domain/enums/expense-recipient-type.enum'; + +@InputType('ExpenseItemInput') +export class ExpenseItemInputDTO { + @Field(() => String, { description: 'Хеш строки расхода (детерминированный, из UI).' }) + @IsNotEmpty() + @IsString() + item_hash!: string; + + @Field(() => ExpenseMechanics, { description: 'Способ оплаты (ADVANCE / DIRECT).' }) + @IsEnum(ExpenseMechanics) + mechanics!: ExpenseMechanics; + + @Field(() => ExpenseRecipientType, { description: 'Тип получателя.' }) + @IsEnum(ExpenseRecipientType) + recipient_type!: ExpenseRecipientType; + + @Field(() => String, { + description: 'Получатель: username пайщика; для организации — пустая строка (аккаунта в кооперативе нет).', + }) + @ValidateIf((o) => o.recipient_type !== ExpenseRecipientType.ORG) + @IsNotEmpty() + @IsString() + recipient!: string; + + @Field(() => String, { description: 'Описание назначения расхода.' }) + @IsString() + description!: string; + + @Field(() => String, { description: 'Планируемая сумма (asset, eg "1000.0000 RUB").' }) + @IsNotEmpty() + @IsString() + planned_amount!: string; + + @Field(() => String, { + nullable: true, + description: + 'Идентификатор сохранённых реквизитов получателя-пайщика — реквизиты снимаются в момент создания и прикладываются к платежу.', + }) + @IsOptional() + @IsString() + payment_method_id?: string; + + @Field(() => String, { + nullable: true, + description: 'Реквизиты получателя-организации (вводятся вручную).', + }) + @IsOptional() + @IsString() + requisites?: string; + + @Field(() => String, { + nullable: true, + description: 'Назначение платежа (для оплаты по счёту) — фиксируется в снимке для кассира.', + }) + @IsOptional() + @IsString() + payment_purpose?: string; +} diff --git a/components/controller/src/extensions/expenses/application/dto/expense-item.output.ts b/components/controller/src/extensions/expenses/application/dto/expense-item.output.ts new file mode 100644 index 00000000000..38a1dc16944 --- /dev/null +++ b/components/controller/src/extensions/expenses/application/dto/expense-item.output.ts @@ -0,0 +1,34 @@ +import { Field, ObjectType } from '@nestjs/graphql'; +import { ExpenseItemStatus } from '../../domain/enums/expense-item-status.enum'; +import { ExpenseMechanics } from '../../domain/enums/expense-mechanics.enum'; +import { ExpenseRecipientType } from '../../domain/enums/expense-recipient-type.enum'; + +/** + * Output DTO строки СЗ-расхода. Зеркалит `IExpenseItemBlockchainData`. + */ +@ObjectType('ExpenseItem', { description: 'Строка сметы расхода.' }) +export class ExpenseItemOutputDTO { + @Field(() => String, { description: 'Хеш строки расхода.' }) + item_hash!: string; + + @Field(() => ExpenseMechanics, { description: 'Способ оплаты.' }) + mechanics!: ExpenseMechanics; + + @Field(() => ExpenseRecipientType, { description: 'Тип получателя платежа.' }) + recipient_type!: ExpenseRecipientType; + + @Field(() => String, { nullable: true, description: 'Идентификатор получателя.' }) + recipient?: string; + + @Field(() => String, { description: 'Назначение/описание строки.' }) + description!: string; + + @Field(() => String, { description: 'Планируемая сумма.' }) + planned_amount!: string; + + @Field(() => String, { nullable: true, description: 'Фактическая сумма (после оплаты/отчёта).' }) + actual_amount?: string; + + @Field(() => ExpenseItemStatus, { description: 'Статус строки.' }) + status!: ExpenseItemStatus; +} diff --git a/components/controller/src/extensions/expenses/application/dto/expense-proposal.output.ts b/components/controller/src/extensions/expenses/application/dto/expense-proposal.output.ts new file mode 100644 index 00000000000..5407fd5575c --- /dev/null +++ b/components/controller/src/extensions/expenses/application/dto/expense-proposal.output.ts @@ -0,0 +1,147 @@ +import { Field, Int, ObjectType } from '@nestjs/graphql'; +import { BaseOutputDTO } from '~/shared/dto/base.dto'; +import { DocumentAggregateDTO } from '~/application/document/dto/document-aggregate.dto'; +import { ExpenseProposalStatus } from '../../domain/enums/expense-proposal-status.enum'; +import { ExpenseMechanics } from '../../domain/enums/expense-mechanics.enum'; +import { ExpenseRecipientType } from '../../domain/enums/expense-recipient-type.enum'; +import { ExpenseItemStatus } from '../../domain/enums/expense-item-status.enum'; +import { ExpenseItemOutputDTO } from './expense-item.output'; +import type { ExpenseProposalDomainEntity } from '../../domain/entities/expense-proposal.entity'; +import type { IExpenseItemBlockchainData } from '../../domain/interfaces/expense-proposal-blockchain.interface'; +import type { ExpenseProposalDocumentAggregates } from '../services/expenses-management.service'; + +/** + * Output DTO СЗ-расхода. Сборка из доменной сущности через `fromDomain`. + * + * `statement_doc` / `decision_doc` — полные `DocumentAggregateDTO` (заявление + * пайщика + решение совета), собираются `DocumentAggregationService` из chain-mirror + * `signed_document`-структур. Резолвер вызывает `ExpensesManagementService + * .buildProposalDocumentAggregates(entity)` и передаёт результат в `fromDomain`. + * Если агрегаты не переданы — поля остаются `undefined` (например в spec'ах / + * test-фикстурах, чтобы не тащить полный document-stack). + */ +@ObjectType('ExpenseProposal', { description: 'Смета расхода (СЗ).' }) +export class ExpenseProposalOutputDTO extends BaseOutputDTO { + @Field(() => Int, { nullable: true, description: 'ID в блокчейне.' }) + id?: number; + + @Field(() => String, { description: 'Хеш сметы расхода.' }) + proposal_hash!: string; + + @Field(() => String, { description: 'Имя кооператива (scope).' }) + coopname!: string; + + @Field(() => ExpenseProposalStatus, { description: 'Доменный статус сметы.' }) + status!: ExpenseProposalStatus; + + @Field(() => String, { nullable: true, description: 'Создатель сметы (username).' }) + username?: string; + + @Field(() => String, { nullable: true, description: 'Кошелёк-источник средств.' }) + source_wallet?: string; + + @Field(() => Int, { nullable: true, description: 'Сырой статус из блокчейна (uint8).' }) + blockchain_status?: number; + + @Field(() => [ExpenseItemOutputDTO], { description: 'Строки сметы.' }) + items!: ExpenseItemOutputDTO[]; + + @Field(() => String, { nullable: true, description: 'Сумма всех строк (план).' }) + total_planned?: string; + + @Field(() => String, { nullable: true, description: 'Сумма всех строк (факт).' }) + total_actual?: string; + + @Field(() => String, { nullable: true, description: 'Время создания (chain).' }) + created_at?: string; + + @Field(() => String, { nullable: true, description: 'Время последнего обновления (chain).' }) + updated_at?: string; + + @Field(() => DocumentAggregateDTO, { + nullable: true, + description: 'Заявление пайщика о расходе (DocumentAggregate).', + }) + statement_doc?: DocumentAggregateDTO | null; + + @Field(() => DocumentAggregateDTO, { + nullable: true, + description: 'Решение совета о расходе (DocumentAggregate).', + }) + decision_doc?: DocumentAggregateDTO | null; + + static fromDomain( + entity: ExpenseProposalDomainEntity, + aggregates?: ExpenseProposalDocumentAggregates + ): ExpenseProposalOutputDTO { + const dto = new ExpenseProposalOutputDTO(); + dto._id = (entity as unknown as { _id: string })._id; + dto._created_at = (entity as unknown as { _created_at: Date })._created_at; + dto._updated_at = (entity as unknown as { _updated_at: Date })._updated_at; + dto.present = entity.present ?? false; + dto.block_num = entity.block_num; + dto.id = entity.id; + dto.proposal_hash = entity.proposal_hash; + dto.coopname = entity.coopname; + dto.status = entity.status; + dto.username = entity.username; + dto.source_wallet = entity.source_wallet; + dto.blockchain_status = entity.blockchain_status; + dto.items = (entity.items ?? []).map((i) => ExpenseProposalOutputDTO.itemFromBlockchain(i)); + dto.total_planned = entity.total_planned; + dto.total_actual = entity.total_actual; + dto.created_at = entity.created_at; + dto.updated_at = entity.updated_at; + dto.statement_doc = aggregates?.statement_doc + ? new DocumentAggregateDTO(aggregates.statement_doc) + : aggregates?.statement_doc === null + ? null + : undefined; + dto.decision_doc = aggregates?.decision_doc + ? new DocumentAggregateDTO(aggregates.decision_doc) + : aggregates?.decision_doc === null + ? null + : undefined; + return dto; + } + + private static itemFromBlockchain(item: IExpenseItemBlockchainData): ExpenseItemOutputDTO { + const dto = new ExpenseItemOutputDTO(); + dto.item_hash = String(item.item_hash).toLowerCase(); + dto.mechanics = ExpenseProposalOutputDTO.mapMechanics(item.mechanics); + dto.recipient_type = ExpenseProposalOutputDTO.mapRecipientType(item.recipient_type); + dto.recipient = item.recipient || undefined; + dto.description = item.description; + dto.planned_amount = item.planned_amount; + dto.actual_amount = item.actual_amount || undefined; + dto.status = ExpenseProposalOutputDTO.mapItemStatus(item.status); + return dto; + } + + private static mapMechanics(raw: number): ExpenseMechanics { + return raw === 1 ? ExpenseMechanics.DIRECT : ExpenseMechanics.ADVANCE; + } + + private static mapRecipientType(raw: number): ExpenseRecipientType { + if (raw === 2) return ExpenseRecipientType.ORG; + if (raw === 1) return ExpenseRecipientType.MEMBER; + return ExpenseRecipientType.SELF; + } + + private static mapItemStatus(raw: number): ExpenseItemStatus { + switch (raw) { + case 0: + return ExpenseItemStatus.APPROVED; + case 1: + return ExpenseItemStatus.PAID; + case 2: + return ExpenseItemStatus.REPORTED; + case 3: + return ExpenseItemStatus.RETURNED; + case 4: + return ExpenseItemStatus.OVERSPENT; + default: + return ExpenseItemStatus.UNDEFINED; + } + } +} diff --git a/components/controller/src/extensions/expenses/application/dto/expense-requisite.output.ts b/components/controller/src/extensions/expenses/application/dto/expense-requisite.output.ts new file mode 100644 index 00000000000..ae7149150f3 --- /dev/null +++ b/components/controller/src/extensions/expenses/application/dto/expense-requisite.output.ts @@ -0,0 +1,48 @@ +import { Field, ObjectType } from '@nestjs/graphql'; +import { GraphQLJSON } from 'graphql-type-json'; +import type { ExpenseRequisiteSnapshotTypeormEntity } from '../../infrastructure/entities/expense-requisite-snapshot.typeorm-entity'; + +/** + * Снимок реквизитов получателя по строке расхода. Персональные данные — + * хранятся только в БД шасси (в блокчейн не пишутся), читаются советом + * для сверки «куда и за что платим». + */ +@ObjectType('ExpenseRequisite') +export class ExpenseRequisiteOutputDTO { + @Field(() => String, { description: 'Имя кооператива.' }) + coopname!: string; + + @Field(() => String, { description: 'Хеш сметы расхода (proposal).' }) + proposal_hash!: string; + + @Field(() => String, { description: 'Хеш строки расхода (item).' }) + item_hash!: string; + + @Field(() => String, { description: 'Получатель платежа (аккаунт пайщика или название организации).' }) + recipient!: string; + + @Field(() => String, { nullable: true, description: 'Тип платёжного метода пайщика (СБП / банковский перевод).' }) + method_type?: string | null; + + @Field(() => GraphQLJSON, { nullable: true, description: 'Снимок данных платёжного метода на момент подачи СЗ.' }) + data?: Record | null; + + @Field(() => String, { description: 'Реквизиты строкой — как в документе служебной записки.' }) + requisites!: string; + + @Field(() => String, { nullable: true, description: 'Назначение платежа для поручения кассиру.' }) + payment_purpose?: string | null; + + static fromEntity(entity: ExpenseRequisiteSnapshotTypeormEntity): ExpenseRequisiteOutputDTO { + const dto = new ExpenseRequisiteOutputDTO(); + dto.coopname = entity.coopname; + dto.proposal_hash = entity.proposal_hash; + dto.item_hash = entity.item_hash; + dto.recipient = entity.recipient; + dto.method_type = entity.method_type; + dto.data = entity.data; + dto.requisites = entity.requisites; + dto.payment_purpose = entity.payment_purpose; + return dto; + } +} diff --git a/components/controller/src/extensions/expenses/application/dto/overspend-expense-item.input.ts b/components/controller/src/extensions/expenses/application/dto/overspend-expense-item.input.ts new file mode 100644 index 00000000000..199a558544d --- /dev/null +++ b/components/controller/src/extensions/expenses/application/dto/overspend-expense-item.input.ts @@ -0,0 +1,35 @@ +import { Field, InputType } from '@nestjs/graphql' +import { IsNotEmpty, IsString, Matches } from 'class-validator' + +/** + * Input для action `expense::overspendexp` — доплата при перерасходе (ADVANCE-механика). + * + * Контракт: `Ledger2::apply(OVERSPEND_COMPENSATION)` → сразу `Ledger2::apply(ADVANCE_REPORTED)` + * одной транзакцией. Item переходит из `PAID/REPORTED` в `OVERSPENT`. + */ +@InputType('OverspendExpenseItemInput') +export class OverspendExpenseItemInputDTO { + @Field(() => String, { description: 'Имя кооператива.' }) + @IsNotEmpty() + @IsString() + coopname!: string + + @Field(() => String, { description: 'Хеш сметы расхода (proposal).' }) + @IsNotEmpty() + @IsString() + proposal_hash!: string + + @Field(() => String, { description: 'Хеш строки расхода (item).' }) + @IsNotEmpty() + @IsString() + item_hash!: string + + @Field(() => String, { + description: 'Сумма доплаты сверх planned_amount (asset, например "200.0000 RUB").', + }) + @IsNotEmpty() + @Matches(/^\d+\.\d{1,8} [A-Z]{1,7}$/, { + message: 'overspend_amount должен быть в формате asset (например "200.0000 RUB").', + }) + overspend_amount!: string +} diff --git a/components/controller/src/extensions/expenses/application/dto/pay-expense-item.input.ts b/components/controller/src/extensions/expenses/application/dto/pay-expense-item.input.ts new file mode 100644 index 00000000000..0a5deec8344 --- /dev/null +++ b/components/controller/src/extensions/expenses/application/dto/pay-expense-item.input.ts @@ -0,0 +1,30 @@ +import { Field, InputType } from '@nestjs/graphql'; +import { IsNotEmpty, IsString, Matches } from 'class-validator'; + +/** + * Input для action `expense::payexp` — выдача аванса (ADVANCE) или прямая оплата (DIRECT). + * + * `actual_amount` — `asset` (`"100.0000 RUB"`), формат проверяется регуляркой. + */ +@InputType('PayExpenseItemInput') +export class PayExpenseItemInputDTO { + @Field(() => String, { description: 'Имя кооператива.' }) + @IsNotEmpty() + @IsString() + coopname!: string; + + @Field(() => String, { description: 'Хеш сметы расхода (proposal).' }) + @IsNotEmpty() + @IsString() + proposal_hash!: string; + + @Field(() => String, { description: 'Хеш строки расхода (item).' }) + @IsNotEmpty() + @IsString() + item_hash!: string; + + @Field(() => String, { description: 'Фактическая сумма оплаты (asset, например "100.0000 RUB").' }) + @IsNotEmpty() + @Matches(/^\d+\.\d{1,8} [A-Z]{1,7}$/, { message: 'actual_amount должен быть в формате asset (например "100.0000 RUB").' }) + actual_amount!: string; +} diff --git a/components/controller/src/extensions/expenses/application/dto/report-expense-item.input.ts b/components/controller/src/extensions/expenses/application/dto/report-expense-item.input.ts new file mode 100644 index 00000000000..a49ffaa6d46 --- /dev/null +++ b/components/controller/src/extensions/expenses/application/dto/report-expense-item.input.ts @@ -0,0 +1,40 @@ +import { Field, InputType } from '@nestjs/graphql'; +import { IsNotEmpty, IsOptional, IsString, Matches } from 'class-validator'; + +/** + * Input для отчёта пайщика по строке-авансу (ADVANCE). + * + * `actual_amount` — фактически потраченная сумма по чекам: + * - не указана либо равна выданному авансу → позиция закрывается сразу + * (`expense::reportexp`); + * - меньше аванса (недорасход) → заводится входящая платёжка возврата разницы + * (пайщик возвращает на расчётный счёт), `reportexp` отложен до её приёма; + * - больше аванса (перерасход) → заводится исходящая платёжка-доплата разницы, + * `reportexp` отложен до её выплаты. + */ +@InputType('ReportExpenseItemInput') +export class ReportExpenseItemInputDTO { + @Field(() => String, { description: 'Имя кооператива.' }) + @IsNotEmpty() + @IsString() + coopname!: string; + + @Field(() => String, { description: 'Хеш сметы расхода (proposal).' }) + @IsNotEmpty() + @IsString() + proposal_hash!: string; + + @Field(() => String, { description: 'Хеш строки расхода (item).' }) + @IsNotEmpty() + @IsString() + item_hash!: string; + + @Field(() => String, { + nullable: true, + description: 'Фактически потраченная сумма по чекам (asset, например "800.0000 RUB"). Не указана — равна выданному авансу.', + }) + @IsOptional() + @IsString() + @Matches(/^\d+\.\d{1,8} [A-Z]{1,7}$/, { message: 'actual_amount должен быть в формате asset (например "800.0000 RUB").' }) + actual_amount?: string; +} diff --git a/components/controller/src/extensions/expenses/application/dto/report-expense-item.output.ts b/components/controller/src/extensions/expenses/application/dto/report-expense-item.output.ts new file mode 100644 index 00000000000..f5374a35dd3 --- /dev/null +++ b/components/controller/src/extensions/expenses/application/dto/report-expense-item.output.ts @@ -0,0 +1,43 @@ +import { Field, ObjectType, registerEnumType } from '@nestjs/graphql'; +import { TransactionDTO } from '~/application/common/dto/transaction-result-response.dto'; + +/** + * Исход отчёта пайщика по строке-авансу: совпал ли факт с выданным авансом или + * требуется расчёт разницы через платёжку. + */ +export enum ExpenseReportOutcome { + /** Факт равен авансу — позиция закрыта on-chain (`expense::reportexp`). */ + CLOSED = 'CLOSED', + /** Недорасход — заведена входящая платёжка возврата разницы, ждёт приёма кассиром. */ + RETURN_PENDING = 'RETURN_PENDING', + /** Перерасход — заведена исходящая платёжка-доплата разницы, ждёт выплаты кассиром. */ + OVERSPEND_PENDING = 'OVERSPEND_PENDING', +} + +registerEnumType(ExpenseReportOutcome, { + name: 'ExpenseReportOutcome', + description: 'Исход отчёта по строке-авансу: закрыто либо ожидается расчёт разницы (возврат/доплата).', +}); + +/** + * Результат мутации `reportExpenseItem`. + * + * При `CLOSED` заполнен `transaction` (on-chain reportexp). При `RETURN_PENDING` / + * `OVERSPEND_PENDING` chain-транзакции ещё нет — заполнены `settlement_amount` + * (раз­ница) и `settlement_payment_hash` (хэш заведённой платёжки расчёта); + * `reportexp` пройдёт автоматически после подтверждения платёжки кассиром. + */ +@ObjectType('ExpenseReportResult') +export class ExpenseReportResultDTO { + @Field(() => ExpenseReportOutcome, { description: 'Исход отчёта.' }) + outcome!: ExpenseReportOutcome; + + @Field(() => TransactionDTO, { nullable: true, description: 'Транзакция закрытия позиции (только при CLOSED).' }) + transaction?: TransactionDTO; + + @Field(() => String, { nullable: true, description: 'Сумма разницы к расчёту (asset), при недо-/перерасходе.' }) + settlement_amount?: string; + + @Field(() => String, { nullable: true, description: 'Хэш заведённой платёжки расчёта (возврат/доплата).' }) + settlement_payment_hash?: string; +} diff --git a/components/controller/src/extensions/expenses/application/dto/return-expense-item.input.ts b/components/controller/src/extensions/expenses/application/dto/return-expense-item.input.ts new file mode 100644 index 00000000000..470412a16c0 --- /dev/null +++ b/components/controller/src/extensions/expenses/application/dto/return-expense-item.input.ts @@ -0,0 +1,28 @@ +import { Field, InputType } from '@nestjs/graphql'; +import { IsNotEmpty, IsString, Matches } from 'class-validator'; + +/** + * Input для action `expense::returnexp` — возврат неиспользованного аванса (ADVANCE). + */ +@InputType('ReturnExpenseItemInput') +export class ReturnExpenseItemInputDTO { + @Field(() => String, { description: 'Имя кооператива.' }) + @IsNotEmpty() + @IsString() + coopname!: string; + + @Field(() => String, { description: 'Хеш сметы расхода (proposal).' }) + @IsNotEmpty() + @IsString() + proposal_hash!: string; + + @Field(() => String, { description: 'Хеш строки расхода (item).' }) + @IsNotEmpty() + @IsString() + item_hash!: string; + + @Field(() => String, { description: 'Возвращаемая сумма (asset, например "50.0000 RUB").' }) + @IsNotEmpty() + @Matches(/^\d+\.\d{1,8} [A-Z]{1,7}$/, { message: 'return_amount должен быть в формате asset (например "50.0000 RUB").' }) + return_amount!: string; +} diff --git a/components/controller/src/extensions/expenses/application/dto/submit-expense-report.input.ts b/components/controller/src/extensions/expenses/application/dto/submit-expense-report.input.ts new file mode 100644 index 00000000000..a43b0569a13 --- /dev/null +++ b/components/controller/src/extensions/expenses/application/dto/submit-expense-report.input.ts @@ -0,0 +1,40 @@ +import { Field, InputType } from '@nestjs/graphql'; +import { IsNotEmpty, IsOptional, IsString, Matches, MaxLength } from 'class-validator'; + +/** + * Input для финализации СЗ-отчёта по смете расхода. + * + * Пайщик/председатель закрывают смету, когда все items уже reported/returned. + * На-цепи это эквивалент `expense::closeexp` (см. `contracts/cpp/expense/expense.hpp`). + * Поле `total_actual_amount` опционально — backend в Phase 2 сверит его с суммой + * фактических item-сумм перед submit'ом (защита от UI-расхождения). + */ +@InputType('SubmitExpenseReportInput') +export class SubmitExpenseReportInputDTO { + @Field(() => String, { description: 'Имя кооператива.' }) + @IsNotEmpty() + @IsString() + coopname!: string; + + @Field(() => String, { description: 'Хеш сметы расхода.' }) + @IsNotEmpty() + @IsString() + proposal_hash!: string; + + @Field(() => String, { + nullable: true, + description: 'Итоговая сумма фактических расходов (asset, например "1500.0000 RUB").', + }) + @IsOptional() + @Matches(/^\d+\.\d{1,8} [A-Z]{1,7}$/) + total_actual_amount?: string; + + @Field(() => String, { + nullable: true, + description: 'Комментарий пайщика к финализации отчёта (свободный текст, до 1000 символов).', + }) + @IsOptional() + @IsString() + @MaxLength(1000) + comment?: string; +} diff --git a/components/controller/src/extensions/expenses/application/dto/upload-expense-file.input.ts b/components/controller/src/extensions/expenses/application/dto/upload-expense-file.input.ts new file mode 100644 index 00000000000..7be7870c5c2 --- /dev/null +++ b/components/controller/src/extensions/expenses/application/dto/upload-expense-file.input.ts @@ -0,0 +1,56 @@ +import { Field, InputType } from '@nestjs/graphql'; +import { IsBase64, IsIn, IsInt, IsNotEmpty, IsOptional, IsString, Matches, Max, Min } from 'class-validator'; +import { ExpenseFileKind } from '../../domain/enums/expense-file-kind.enum'; + +const ALLOWED_MIME = ['image/jpeg', 'image/png', 'image/webp', 'image/heic', 'application/pdf'] as const; + +/** + * Input для `uploadExpenseFile` — загрузка платёжки / чека / доказательства возврата. + * + * MVP-передача: base64-содержимое внутри GraphQL-mutation. Размер ограничен `maxBytes` бакета + * (20 MB) — для типичных фото/PDF этого хватает. В Phase 2 при необходимости заменим на + * pre-signed upload URL (для крупных файлов / лучшего UX). + */ +@InputType('UploadExpenseFileInput') +export class UploadExpenseFileInputDTO { + @Field(() => String, { description: 'Имя кооператива.' }) + @IsNotEmpty() + @IsString() + coopname!: string; + + @Field(() => String, { description: 'Хеш сметы расхода.' }) + @IsNotEmpty() + @IsString() + proposal_hash!: string; + + @Field(() => String, { nullable: true, description: 'Хеш строки расхода (пусто — файл уровня сметы).' }) + @IsOptional() + @IsString() + item_hash?: string; + + @Field(() => ExpenseFileKind, { description: 'Назначение файла (платёжка/чек/возврат).' }) + kind!: ExpenseFileKind; + + @Field(() => String, { description: 'MIME-тип содержимого.' }) + @IsIn([...ALLOWED_MIME], { message: 'mime_type должен быть одним из allowed.' }) + mime_type!: string; + + @Field(() => String, { nullable: true, description: 'Оригинальное имя файла — для отображения и поиска.' }) + @IsOptional() + @IsString() + original_filename?: string; + + @Field(() => Number, { description: 'Размер файла в байтах (для серверной валидации).' }) + @IsInt() + @Min(1) + @Max(20 * 1024 * 1024) + size_bytes!: number; + + @Field(() => String, { description: 'SHA-256 содержимого, hex-lowercase (64 hex-символа).' }) + @Matches(/^[a-f0-9]{64}$/, { message: 'checksum_sha256 должен быть 64-символьным lowercase hex.' }) + checksum_sha256!: string; + + @Field(() => String, { description: 'Содержимое файла, base64 без префикса data:.' }) + @IsBase64() + content_base64!: string; +} diff --git a/components/controller/src/extensions/expenses/application/resolvers/expense-files.resolver.ts b/components/controller/src/extensions/expenses/application/resolvers/expense-files.resolver.ts new file mode 100644 index 00000000000..644f0061d43 --- /dev/null +++ b/components/controller/src/extensions/expenses/application/resolvers/expense-files.resolver.ts @@ -0,0 +1,76 @@ +import { Args, Int, Mutation, Query, Resolver } from '@nestjs/graphql'; +import { UseGuards } from '@nestjs/common'; +import { GqlJwtAuthGuard } from '~/application/auth/guards/graphql-jwt-auth.guard'; +import { RolesGuard } from '~/application/auth/guards/roles.guard'; +import { AuthRoles } from '~/application/auth/decorators/auth.decorator'; +import { CurrentUser } from '~/application/auth/decorators/current-user.decorator'; +import type { MonoAccountDomainInterface } from '~/domain/account/interfaces/mono-account-domain.interface'; +import { ExpenseFilesService } from '../services/expense-files.service'; +import { UploadExpenseFileInputDTO } from '../dto/upload-expense-file.input'; +import { ExpenseFileOutputDTO } from '../dto/expense-file.output'; + +/** + * GraphQL для файлов расхода: загрузка (mutation) + чтение списков (queries). + * + * - Загружают пайщик и председатель (всем трём ролям нужно для разных сценариев: + * пайщик — чек/возврат, председатель — платёжка). + * - Списки видят те же роли, при необходимости ACL уточнится после E2E. + */ +@Resolver(() => ExpenseFileOutputDTO) +export class ExpenseFilesResolver { + constructor(private readonly expenseFiles: ExpenseFilesService) {} + + @Mutation(() => ExpenseFileOutputDTO, { + name: 'uploadExpenseFile', + description: 'Загрузить первичный файл расхода (платёжка/чек/возврат) в бакет expenses:files.', + }) + @UseGuards(GqlJwtAuthGuard, RolesGuard) + @AuthRoles(['chairman', 'member', 'user']) + async uploadExpenseFile( + @Args('data', { type: () => UploadExpenseFileInputDTO }) data: UploadExpenseFileInputDTO, + @CurrentUser() user: MonoAccountDomainInterface + ): Promise { + const { data: saved, readUrl } = await this.expenseFiles.uploadFile(data, user.username); + return ExpenseFileOutputDTO.fromDomain(saved, readUrl); + } + + @Query(() => ExpenseFileOutputDTO, { + name: 'expenseFile', + description: 'Получить запись о файле + свежий короткоживущий read-URL.', + }) + @UseGuards(GqlJwtAuthGuard, RolesGuard) + @AuthRoles(['chairman', 'member', 'user']) + async getExpenseFile(@Args('id', { type: () => Int }) id: number): Promise { + const { data, readUrl } = await this.expenseFiles.getReadUrl(id); + return ExpenseFileOutputDTO.fromDomain(data, readUrl); + } + + @Query(() => [ExpenseFileOutputDTO], { + name: 'expenseFilesByProposal', + description: 'Список файлов сметы расхода (без read-URL — запрос отдельно по id).', + }) + @UseGuards(GqlJwtAuthGuard, RolesGuard) + @AuthRoles(['chairman', 'member', 'user']) + async listByProposal( + @Args('coopname', { type: () => String }) coopname: string, + @Args('proposal_hash', { type: () => String }) proposalHash: string + ): Promise { + const items = await this.expenseFiles.listByProposal(coopname, proposalHash); + return items.map((d) => ExpenseFileOutputDTO.fromDomain(d)); + } + + @Query(() => [ExpenseFileOutputDTO], { + name: 'expenseFilesByItem', + description: 'Список файлов строки расхода (без read-URL — запрос отдельно по id).', + }) + @UseGuards(GqlJwtAuthGuard, RolesGuard) + @AuthRoles(['chairman', 'member', 'user']) + async listByItem( + @Args('coopname', { type: () => String }) coopname: string, + @Args('proposal_hash', { type: () => String }) proposalHash: string, + @Args('item_hash', { type: () => String }) itemHash: string + ): Promise { + const items = await this.expenseFiles.listByItem(coopname, proposalHash, itemHash); + return items.map((d) => ExpenseFileOutputDTO.fromDomain(d)); + } +} diff --git a/components/controller/src/extensions/expenses/application/resolvers/expense-mutations.resolver.ts b/components/controller/src/extensions/expenses/application/resolvers/expense-mutations.resolver.ts new file mode 100644 index 00000000000..e3dc346aa91 --- /dev/null +++ b/components/controller/src/extensions/expenses/application/resolvers/expense-mutations.resolver.ts @@ -0,0 +1,173 @@ +import { Args, Mutation, Resolver } from '@nestjs/graphql'; +import { ForbiddenException, UseGuards } from '@nestjs/common'; +import { Throttle } from '@nestjs/throttler'; +import { GqlJwtAuthGuard } from '~/application/auth/guards/graphql-jwt-auth.guard'; +import { RolesGuard } from '~/application/auth/guards/roles.guard'; +import { AuthRoles } from '~/application/auth/decorators/auth.decorator'; +import { CurrentUser } from '~/application/auth/decorators/current-user.decorator'; +import type { MonoAccountDomainInterface } from '~/domain/account/interfaces/mono-account-domain.interface'; +import { TransactionDTO } from '~/application/common/dto/transaction-result-response.dto'; +import { GeneratedDocumentDTO } from '~/application/document/dto/generated-document.dto'; +import { GenerateDocumentOptionsInputDTO } from '~/application/document/dto/generate-document-options-input.dto'; +import { ExpenseProposalStatementGenerateDocumentInputDTO } from '~/application/document/documents-dto/expense-proposal-statement-document.dto'; +import { ExpenseProposalDecisionGenerateDocumentInputDTO } from '~/application/document/documents-dto/expense-proposal-decision-document.dto'; +import { ExpensesMutationsService } from '../services/expenses-mutations.service'; +import { ExpensesManagementService } from '../services/expenses-management.service'; +import { CreateExpenseProposalInputDTO } from '../dto/create-expense-proposal.input'; +import { PayExpenseItemInputDTO } from '../dto/pay-expense-item.input'; +import { ReportExpenseItemInputDTO } from '../dto/report-expense-item.input'; +import { ExpenseReportResultDTO } from '../dto/report-expense-item.output'; +import { ReturnExpenseItemInputDTO } from '../dto/return-expense-item.input'; +import { OverspendExpenseItemInputDTO } from '../dto/overspend-expense-item.input'; +import { SubmitExpenseReportInputDTO } from '../dto/submit-expense-report.input'; + +/** + * GraphQL Mutation-резолвер контракта `expense`. + * + * createExpenseProposal / payExpenseItem / reportExpenseItem / returnExpenseItem / + * overspendExpenseItem / submitExpenseReport. Все маршрутизируются в + * `ExpensesMutationsService`, который сабмитит через `ExpensesBlockchainPort`. + * Авторизация и отклонение СЗ — решение совета (повестка), не мутации backend'а. + */ +@Resolver() +export class ExpenseMutationsResolver { + constructor( + private readonly expensesMutations: ExpensesMutationsService, + private readonly expensesManagement: ExpensesManagementService + ) {} + + /** + * Контракт проверяет только статус/механику строки, но не личность + * отчитывающегося — поэтому отчёт по чужому авансу отсекается здесь: + * пайщик может отчитаться только по своей строке, совет — по любой. + */ + private async assertCanReportItem( + user: MonoAccountDomainInterface, + proposalHash: string, + itemHash: string + ): Promise { + if (user.role === 'chairman' || user.role === 'member') return; + const proposal = await this.expensesManagement.getProposalByHash(proposalHash); + const item = proposal?.items?.find( + (i) => i.item_hash?.toLowerCase() === itemHash.toLowerCase() + ); + if (!item || item.recipient !== user.username) { + throw new ForbiddenException('Отчитаться по авансу может только его получатель'); + } + } + + @Mutation(() => GeneratedDocumentDTO, { + name: 'generateExpenseProposalStatementDocument', + description: 'Сгенерировать документ СЗ-заявления (registry 2010) для последующей подписи.', + }) + @Throttle({ default: { limit: 3, ttl: 60000 } }) + @UseGuards(GqlJwtAuthGuard, RolesGuard) + @AuthRoles(['chairman', 'member']) + async generateExpenseProposalStatementDocument( + @Args('data', { type: () => ExpenseProposalStatementGenerateDocumentInputDTO }) + data: ExpenseProposalStatementGenerateDocumentInputDTO, + @Args('options', { type: () => GenerateDocumentOptionsInputDTO, nullable: true }) + options: GenerateDocumentOptionsInputDTO, + ): Promise { + return this.expensesMutations.generateExpenseProposalStatementDocument(data, options); + } + + @Mutation(() => GeneratedDocumentDTO, { + name: 'generateExpenseProposalDecisionDocument', + description: 'Сгенерировать документ-решение по СЗ (registry 2011) для последующей подписи.', + }) + @Throttle({ default: { limit: 3, ttl: 60000 } }) + @UseGuards(GqlJwtAuthGuard, RolesGuard) + @AuthRoles(['chairman']) + async generateExpenseProposalDecisionDocument( + @Args('data', { type: () => ExpenseProposalDecisionGenerateDocumentInputDTO }) + data: ExpenseProposalDecisionGenerateDocumentInputDTO, + @Args('options', { type: () => GenerateDocumentOptionsInputDTO, nullable: true }) + options: GenerateDocumentOptionsInputDTO, + ): Promise { + return this.expensesMutations.generateExpenseProposalDecisionDocument(data, options); + } + + @Mutation(() => TransactionDTO, { + name: 'createExpenseProposal', + description: 'Подать СЗ-расход (создать смету с подписью пайщика/председателя).', + }) + @UseGuards(GqlJwtAuthGuard, RolesGuard) + @AuthRoles(['chairman', 'member']) + async createExpenseProposal( + @Args('data', { type: () => CreateExpenseProposalInputDTO }) data: CreateExpenseProposalInputDTO, + @CurrentUser() user: MonoAccountDomainInterface + ): Promise { + // RolesGuard пропускает запрос при data.username === user.username независимо + // от роли — здесь это сломало бы ограничение «СЗ подаёт совет», поэтому роль + // проверяется явно. + if (user.role !== 'chairman' && user.role !== 'member') { + throw new ForbiddenException('Создавать служебные записки на расход могут только председатель и члены совета'); + } + return this.expensesMutations.createExpenseProposal(data); + } + + @Mutation(() => TransactionDTO, { + name: 'payExpenseItem', + description: 'Оплатить строку расхода (выдача аванса ADVANCE или прямая оплата DIRECT).', + }) + @UseGuards(GqlJwtAuthGuard, RolesGuard) + @AuthRoles(['chairman']) + async payExpenseItem( + @Args('data', { type: () => PayExpenseItemInputDTO }) data: PayExpenseItemInputDTO + ): Promise { + return this.expensesMutations.payExpenseItem(data); + } + + @Mutation(() => ExpenseReportResultDTO, { + name: 'reportExpenseItem', + description: 'Отчитаться по строке-авансу: при совпадении факта с авансом — закрыть позицию; при недо-/перерасходе — завести платёжку расчёта разницы.', + }) + @UseGuards(GqlJwtAuthGuard, RolesGuard) + @AuthRoles(['chairman', 'member', 'user']) + async reportExpenseItem( + @Args('data', { type: () => ReportExpenseItemInputDTO }) data: ReportExpenseItemInputDTO, + @CurrentUser() user: MonoAccountDomainInterface + ): Promise { + await this.assertCanReportItem(user, data.proposal_hash, data.item_hash); + return this.expensesMutations.reportExpenseItem(data); + } + + @Mutation(() => TransactionDTO, { + name: 'returnExpenseItem', + description: 'Вернуть неиспользованный аванс по строке расхода (ADVANCE-остаток).', + }) + @UseGuards(GqlJwtAuthGuard, RolesGuard) + @AuthRoles(['chairman', 'member', 'user']) + async returnExpenseItem( + @Args('data', { type: () => ReturnExpenseItemInputDTO }) data: ReturnExpenseItemInputDTO + ): Promise { + return this.expensesMutations.returnExpenseItem(data); + } + + @Mutation(() => TransactionDTO, { + name: 'overspendExpenseItem', + description: 'Доплатить сумму перерасхода по строке расхода (ADVANCE-механика).', + }) + @UseGuards(GqlJwtAuthGuard, RolesGuard) + @AuthRoles(['chairman']) + async overspendExpenseItem( + @Args('data', { type: () => OverspendExpenseItemInputDTO }) data: OverspendExpenseItemInputDTO + ): Promise { + return this.expensesMutations.overspendExpenseItem(data); + } + + @Mutation(() => TransactionDTO, { + name: 'submitExpenseReport', + description: 'Финализировать СЗ-отчёт по смете расхода (все items закрыты — оплата/чек/возврат).', + }) + @UseGuards(GqlJwtAuthGuard, RolesGuard) + // Закрытие расхода — финализация СЗ-отчёта советом: председатель / член совета. + @AuthRoles(['chairman', 'member']) + async submitExpenseReport( + @Args('data', { type: () => SubmitExpenseReportInputDTO }) data: SubmitExpenseReportInputDTO + ): Promise { + return this.expensesMutations.submitExpenseReport(data); + } + +} diff --git a/components/controller/src/extensions/expenses/application/resolvers/expense-proposal.resolver.ts b/components/controller/src/extensions/expenses/application/resolvers/expense-proposal.resolver.ts new file mode 100644 index 00000000000..e323ecea098 --- /dev/null +++ b/components/controller/src/extensions/expenses/application/resolvers/expense-proposal.resolver.ts @@ -0,0 +1,111 @@ +import { Args, Query, Resolver } from '@nestjs/graphql'; +import { UseGuards } from '@nestjs/common'; +import { GqlJwtAuthGuard } from '~/application/auth/guards/graphql-jwt-auth.guard'; +import { RolesGuard } from '~/application/auth/guards/roles.guard'; +import { AuthRoles } from '~/application/auth/decorators/auth.decorator'; +import { + createPaginationResult, + PaginationInputDTO, + PaginationResult, +} from '~/application/common/dto/pagination.dto'; +import { ExpensesManagementService } from '../services/expenses-management.service'; +import { ExpenseRequisiteSnapshotsService } from '../services/expense-requisite-snapshots.service'; +import { ExpenseProposalOutputDTO } from '../dto/expense-proposal.output'; +import { ExpenseRequisiteOutputDTO } from '../dto/expense-requisite.output'; +import type { ExpenseProposalDomainEntity } from '../../domain/entities/expense-proposal.entity'; + +const paginatedExpenseProposalsResult = createPaginationResult( + ExpenseProposalOutputDTO, + 'PaginatedExpenseProposals' +); + +/** + * GraphQL-резолвер чтения смет расходов (контракт `expense`). + * + * Read-only: read-path тянется из PG-зеркала (см. ADR-011 в controller/CLAUDE.md). + * Mutation-резолвер (payexp/reportexp/returnexp/submitReport) — `ExpenseMutationsResolver`. + * + * Пагинация — единый паттерн controller'а (см. controller/CLAUDE.md «Пагинация — единый паттерн»): + * input `PaginationInputDTO`, выход `PaginationResult` через `createPaginationResult`. + * + * Документы (заявление + решение совета) выгружаются как полные `DocumentAggregate` + * через `ExpensesManagementService.buildProposalDocumentAggregates(entity)`. + */ +@Resolver(() => ExpenseProposalOutputDTO) +export class ExpenseProposalResolver { + constructor( + private readonly expenses: ExpensesManagementService, + private readonly requisiteSnapshots: ExpenseRequisiteSnapshotsService + ) {} + + @Query(() => [ExpenseRequisiteOutputDTO], { + name: 'expenseRequisitesByProposal', + description: + 'Снимки реквизитов получателей по строкам СЗ (персональные данные — только совету; в блокчейн не пишутся).', + }) + @UseGuards(GqlJwtAuthGuard, RolesGuard) + @AuthRoles(['chairman', 'member']) + async getExpenseRequisitesByProposal( + @Args('coopname', { type: () => String }) coopname: string, + @Args('proposal_hash', { type: () => String }) proposalHash: string + ): Promise { + const items = await this.requisiteSnapshots.listByProposal(coopname, proposalHash); + return items.map((e) => ExpenseRequisiteOutputDTO.fromEntity(e)); + } + + @Query(() => ExpenseProposalOutputDTO, { + name: 'expenseProposal', + description: 'Получить смету расхода по хешу.', + nullable: true, + }) + @UseGuards(GqlJwtAuthGuard, RolesGuard) + @AuthRoles(['chairman', 'member', 'user']) + async getExpenseProposal( + @Args('proposal_hash', { type: () => String }) proposalHash: string + ): Promise { + const entity = await this.expenses.getProposalByHash(proposalHash); + if (!entity) return null; + const aggregates = await this.expenses.buildProposalDocumentAggregates(entity); + return ExpenseProposalOutputDTO.fromDomain(entity, aggregates); + } + + @Query(() => paginatedExpenseProposalsResult, { + name: 'expenseProposalsByCooperative', + description: 'Список смет расходов кооператива (paginated).', + }) + @UseGuards(GqlJwtAuthGuard, RolesGuard) + @AuthRoles(['chairman', 'member']) + async listByCooperative( + @Args('coopname', { type: () => String }) coopname: string, + @Args('options', { type: () => PaginationInputDTO, nullable: true }) options?: PaginationInputDTO + ): Promise> { + const result = await this.expenses.listProposalsByCooperativePaginated(coopname, options); + return { ...result, items: await this.mapItemsWithDocuments(result.items) }; + } + + @Query(() => paginatedExpenseProposalsResult, { + name: 'expenseProposalsByMember', + description: 'Список смет расходов пайщика (свои/созданные им, paginated).', + }) + @UseGuards(GqlJwtAuthGuard, RolesGuard) + @AuthRoles(['chairman', 'member', 'user']) + async listByMember( + @Args('coopname', { type: () => String }) coopname: string, + @Args('username', { type: () => String }) username: string, + @Args('options', { type: () => PaginationInputDTO, nullable: true }) options?: PaginationInputDTO + ): Promise> { + const result = await this.expenses.listProposalsByMemberPaginated(coopname, username, options); + return { ...result, items: await this.mapItemsWithDocuments(result.items) }; + } + + private async mapItemsWithDocuments( + entities: ExpenseProposalDomainEntity[] + ): Promise { + return Promise.all( + entities.map(async (e) => { + const aggregates = await this.expenses.buildProposalDocumentAggregates(e); + return ExpenseProposalOutputDTO.fromDomain(e, aggregates); + }) + ); + } +} diff --git a/components/controller/src/extensions/expenses/application/services/expense-advance-reminder.service.ts b/components/controller/src/extensions/expenses/application/services/expense-advance-reminder.service.ts new file mode 100644 index 00000000000..3e980690c60 --- /dev/null +++ b/components/controller/src/extensions/expenses/application/services/expense-advance-reminder.service.ts @@ -0,0 +1,184 @@ +import { Inject, Injectable, Logger, type OnModuleInit } from '@nestjs/common'; +import { Interval } from '@nestjs/schedule'; +import { Workflows } from '@coopenomics/notifications'; +import { NotificationSenderService } from '~/application/notification/services/notification-sender.service'; +import config from '~/config/config'; +import { + EXPENSE_PROPOSAL_REPOSITORY, + type ExpenseProposalRepository, +} from '../../domain/repositories/expense-proposal.repository'; +import { ExpenseProposalStatus } from '../../domain/enums/expense-proposal-status.enum'; +import type { ExpenseProposalDomainEntity } from '../../domain/entities/expense-proposal.entity'; +import type { IExpenseItemBlockchainData } from '../../domain/interfaces/expense-proposal-blockchain.interface'; + +// Зеркала числовых enum контракта `expense` (cpp/expense/expense.hpp). +const MECHANICS_ADVANCE = 0; // Mechanics::ADVANCE — пайщик-получатель отчитывается чеком. +const RECIPIENT_ORG = 2; // RecipientType::ORG — у организации нет личного кабинета. +const ITEM_STATUS_PAID = 1; // ItemStatus::PAID — аванс выдан, отчёт ещё не подан. + +// Параметры воркера — ENV-sourced с дефолтами (канон: не hardcode magic numbers, +// тюнинг воркера читаем из process.env как outbox-worker Центра уведомлений). +// Тик опроса зеркала расходов. Дайджест шлётся не чаще раза в неделю — частоту +// тика троттлит идемпотентность Центра (поле `period` в payload), поэтому тик +// можно держать редким: он лишь определяет задержку первого письма после грейса. +const TICK_MS = Number(process.env.EXPENSE_ADVANCE_REMINDER_INTERVAL_MS) || 6 * 60 * 60 * 1000; +// Сколько дней аванс может «висеть» без отчёта до первого напоминания. +const GRACE_DAYS = Number(process.env.EXPENSE_ADVANCE_REMINDER_GRACE_DAYS) || 3; +const DAY_MS = 24 * 60 * 60 * 1000; + +interface OutstandingAdvance { + description: string; + amount: string; + proposal_hash: string; +} + +/** + * Фоновый напоминатель об отчёте по авансу под отчёт. + * + * Каждый тик сканирует зеркало расходов кооператива и находит ADVANCE-item-ы в + * статусе PAID (аванс выдан пайщику, отчёт не подан). По каждому пайщику-получателю + * шлёт один агрегированный дайджест со ссылками на расходы. Кассиру/совету ничего + * не шлётся — только получателю аванса. + * + * Троттлинг «раз в неделю» — через идемпотентность Центра уведомлений: поле `period` + * (ISO-неделя) делает payload стабильным внутри недели, поэтому повторные тики гасятся, + * а на новой неделе уходит ровно одно письмо. Своя таблица состояния не нужна. + * + * Точка отсчёта грейса — `proposal.updated_at` (последняя операция со СЗ; для PAID-не- + * REPORTED item-а это фактически момент выплаты), с фолбэком на `created_at`. Срок из + * заявления пайщика (deadline) сейчас не доезжает до цепи — это осознанный fallback, + * точная семантика «просрочен по дате» — отдельный апгрейд контракта (см. README). + */ +@Injectable() +export class ExpenseAdvanceReminderService implements OnModuleInit { + private readonly logger = new Logger(ExpenseAdvanceReminderService.name); + // Гард от наложения тиков (single-instance): длинный тик не запускается повторно. + private isRunning = false; + + constructor( + @Inject(EXPENSE_PROPOSAL_REPOSITORY) + private readonly proposals: ExpenseProposalRepository, + private readonly notificationSender: NotificationSenderService + ) {} + + onModuleInit(): void { + this.logger.log( + `Напоминатель об отчёте по авансу запущен: тик ${Math.round(TICK_MS / 60000)} мин, грейс ${GRACE_DAYS} дн` + ); + } + + @Interval('expense-advance-report-reminder', TICK_MS) + async tick(): Promise { + if (this.isRunning) return; + this.isRunning = true; + try { + await this.processCoop(config.coopname); + } catch (error: any) { + this.logger.error(`Ошибка тика напоминателя авансов: ${error.message}`, error.stack); + } finally { + this.isRunning = false; + } + } + + private async processCoop(coopname: string): Promise { + const proposals = await this.proposals.findByCoopname(coopname); + const now = Date.now(); + + // Группируем «висящие» авансы по пайщику-получателю. + const byUser = new Map(); + for (const proposal of proposals) { + if (!this.isPayablePhase(proposal.status)) continue; + const anchorMs = this.anchorMs(proposal); + if (anchorMs === null || now - anchorMs < GRACE_DAYS * DAY_MS) continue; + + for (const item of proposal.items ?? []) { + if (!this.isOutstandingAdvance(item)) continue; + const username = item.recipient || proposal.username; + if (!username) continue; + const list = byUser.get(username) ?? []; + list.push({ + // Выданная сумма аванса — то, по чему пайщик отчитывается. actual_amount + // дрейфует от возвратов/перерасходов (settlement), поэтому не используем. + description: item.description, + amount: item.planned_amount, + proposal_hash: proposal.proposal_hash, + }); + byUser.set(username, list); + } + } + + if (byUser.size === 0) return; + this.logger.debug(`Напоминания об авансах: пайщиков к уведомлению ${byUser.size}`); + + for (const [username, advances] of byUser) { + await this.sendDigest(coopname, username, advances); + } + } + + private async sendDigest(coopname: string, username: string, advances: OutstandingAdvance[]): Promise { + const base = `${config.frontend_url}/${coopname}/expenses`; + // Один аванс — ведём прямо на сам расход; несколько — на личную страницу + // «Платежи» пайщика (там строки авансов с панелью «приложить чек/отчитаться»). + const link = + advances.length === 1 + ? `${base}/${advances[0].proposal_hash}` + : `${config.frontend_url}/${coopname}/user/payments`; + + const payload: Workflows.ExpenseAdvanceReportReminder.IPayload = { + coopName: coopname, + period: isoWeekBucket(new Date()), + count: advances.length, + link, + advances: advances.map((advance) => ({ + description: advance.description, + amount: advance.amount, + url: `${base}/${advance.proposal_hash}`, + })), + }; + + try { + await this.notificationSender.sendNotificationToUser( + username, + Workflows.ExpenseAdvanceReportReminder.id, + payload + ); + } catch (error: any) { + // Нет subscriber_id/email у аккаунта (системный/служебный) — не валим цикл. + this.logger.warn(`Не удалось отправить напоминание об авансе пайщику ${username}: ${error.message}`); + } + } + + private isPayablePhase(status: ExpenseProposalStatus): boolean { + // Аванс мог быть выдан в AUTHORIZED (одна позиция) или PARTIALLY_PAID (часть позиций). + // REPORT_SUBMITTED/CLOSED/DECLINED — расход уже без открытого подотчёта. + return status === ExpenseProposalStatus.AUTHORIZED || status === ExpenseProposalStatus.PARTIALLY_PAID; + } + + private isOutstandingAdvance(item: IExpenseItemBlockchainData): boolean { + return ( + item.mechanics === MECHANICS_ADVANCE && + item.status === ITEM_STATUS_PAID && + item.recipient_type !== RECIPIENT_ORG + ); + } + + private anchorMs(proposal: ExpenseProposalDomainEntity): number | null { + const raw = proposal.updated_at || proposal.created_at; + if (!raw) return null; + const ms = Date.parse(raw); + return Number.isNaN(ms) ? null : ms; + } +} + +/** + * ISO-8601 номер недели в формате `YYYY-Www` — техническая «корзина» троттлинга. + * Один пайщик + одна неделя → стабильный payload → идемпотентность Центра гасит повтор. + */ +function isoWeekBucket(d: Date): string { + const date = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate())); + const dayNum = date.getUTCDay() || 7; // Пн=1…Вс=7 + date.setUTCDate(date.getUTCDate() + 4 - dayNum); // ближайший четверг определяет ISO-год недели + const yearStart = new Date(Date.UTC(date.getUTCFullYear(), 0, 1)); + const weekNo = Math.ceil(((date.getTime() - yearStart.getTime()) / DAY_MS + 1) / 7); + return `${date.getUTCFullYear()}-W${String(weekNo).padStart(2, '0')}`; +} diff --git a/components/controller/src/extensions/expenses/application/services/expense-files.service.ts b/components/controller/src/extensions/expenses/application/services/expense-files.service.ts new file mode 100644 index 00000000000..8c166cf4d42 --- /dev/null +++ b/components/controller/src/extensions/expenses/application/services/expense-files.service.ts @@ -0,0 +1,159 @@ +import { + BadRequestException, + ConflictException, + Inject, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { createHash } from 'crypto'; +import type { InterFileStorageBucket } from '@coopenomics/inter'; +import { InjectBucket, UseBucket } from '~/infrastructure/file-storage'; +import { PAYMENT_REPOSITORY, type PaymentRepository } from '~/domain/gateway/repositories/payment.repository'; +import { ExpenseFileKind } from '../../domain/enums/expense-file-kind.enum'; +import { + EXPENSE_FILE_REPOSITORY, + type ExpenseFileRepository, +} from '../../domain/repositories/expense-file.repository'; +import type { IExpenseFileDatabaseData } from '../../domain/interfaces/expense-file-database.interface'; +import { EXPENSES_BUCKET } from '../../constants/expenses-bucket'; +import { UploadExpenseFileInputDTO } from '../dto/upload-expense-file.input'; + +const EXTENSION_BY_MIME: Record = { + 'image/jpeg': 'jpg', + 'image/png': 'png', + 'image/webp': 'webp', + 'image/heic': 'heic', + 'application/pdf': 'pdf', +}; + +/** + * Сервис файлов расхода: загрузка в `expenses:files`, выдача read-URL, удаление + БД-метаданные. + * + * Ключ MinIO детерминирован: `{coopname}/{proposal_hash}/{item_hash|_proposal}/{kind}/{checksum}.{ext}`. + * Поэтому повторная загрузка того же содержимого с тем же scope'ом идемпотентна на стороне бакета, + * а в БД защищаемся явным `findByChecksum` (409 при дубле). + */ +@UseBucket(EXPENSES_BUCKET) +@Injectable() +export class ExpenseFilesService { + constructor( + @InjectBucket() private readonly bucket: InterFileStorageBucket, + @Inject(EXPENSE_FILE_REPOSITORY) private readonly files: ExpenseFileRepository, + @Inject(PAYMENT_REPOSITORY) private readonly payments: PaymentRepository + ) {} + + async uploadFile( + input: UploadExpenseFileInputDTO, + uploadedByUsername: string + ): Promise<{ data: IExpenseFileDatabaseData; readUrl: string }> { + const body = Buffer.from(input.content_base64, 'base64'); + if (body.byteLength !== input.size_bytes) { + throw new BadRequestException( + `size_bytes (${input.size_bytes}) не совпадает с фактическим размером base64-контента (${body.byteLength}).` + ); + } + const actualChecksum = createHash('sha256').update(body).digest('hex'); + if (actualChecksum !== input.checksum_sha256.toLowerCase()) { + throw new BadRequestException( + `checksum_sha256 не совпадает с реальным SHA-256 содержимого.` + ); + } + + const existing = await this.files.findByChecksum(input.coopname, actualChecksum); + if (existing) { + throw new ConflictException( + `Файл с таким SHA-256 уже зарегистрирован в этом кооперативе (id=${existing.id}).` + ); + } + + const storageKey = this.buildKey({ + coopname: input.coopname, + proposalHash: input.proposal_hash, + itemHash: input.item_hash ?? null, + kind: input.kind, + checksum: actualChecksum, + mimeType: input.mime_type, + }); + + await this.bucket.put(storageKey, new Uint8Array(body), { + contentType: input.mime_type, + }); + + const saved = await this.files.create({ + coopname: input.coopname, + proposal_hash: input.proposal_hash.toLowerCase(), + item_hash: input.item_hash ? input.item_hash.toLowerCase() : null, + kind: input.kind, + checksum_sha256: actualChecksum, + mime_type: input.mime_type, + size_bytes: body.byteLength, + storage_key: storageKey, + original_filename: input.original_filename ?? null, + uploaded_by_username: uploadedByUsername, + uploaded_at: new Date(), + }); + + await this.syncPaymentProofMark(saved); + + const readUrl = await this.bucket.getReadUrl(storageKey); + return { data: saved, readUrl }; + } + + /** + * Зеркалит число приложенных платёжек в исходящий платёж реестра + * (`blockchain_data.proof_count`) — реестр платежей показывает по нему отметку + * «платёжка приложена», не дёргая хранилище файлов на каждую строку. + * Статус платежа не трогаем — им управляет gateway. + */ + private async syncPaymentProofMark(file: Pick): Promise { + if (file.kind !== ExpenseFileKind.PAYMENT_PROOF || !file.item_hash) return; + const payment = await this.payments.findByHash(file.item_hash); + if (!payment?.id) return; + const proofs = (await this.files.findByItem(file.coopname, file.proposal_hash, file.item_hash)).filter( + (f) => f.kind === ExpenseFileKind.PAYMENT_PROOF + ); + await this.payments.update(payment.id, { + blockchain_data: { ...(payment.blockchain_data ?? {}), proof_count: proofs.length }, + }); + } + + async getReadUrl(fileId: number): Promise<{ data: IExpenseFileDatabaseData; readUrl: string }> { + const file = await this.files.findById(fileId); + if (!file) throw new NotFoundException(`Файл расхода #${fileId} не найден.`); + const readUrl = await this.bucket.getReadUrl(file.storage_key); + return { data: file, readUrl }; + } + + async listByProposal(coopname: string, proposalHash: string): Promise { + return this.files.findByProposal(coopname, proposalHash.toLowerCase()); + } + + async listByItem( + coopname: string, + proposalHash: string, + itemHash: string + ): Promise { + return this.files.findByItem(coopname, proposalHash.toLowerCase(), itemHash.toLowerCase()); + } + + async deleteFile(fileId: number): Promise { + const file = await this.files.findById(fileId); + if (!file) throw new NotFoundException(`Файл расхода #${fileId} не найден.`); + await this.bucket.delete(file.storage_key); + await this.files.delete(fileId); + await this.syncPaymentProofMark(file); + } + + private buildKey(params: { + coopname: string; + proposalHash: string; + itemHash: string | null; + kind: ExpenseFileKind; + checksum: string; + mimeType: string; + }): string { + const ext = EXTENSION_BY_MIME[params.mimeType] ?? 'bin'; + const item = params.itemHash ? params.itemHash.toLowerCase() : '_proposal'; + return `${params.coopname}/${params.proposalHash.toLowerCase()}/${item}/${params.kind}/${params.checksum}.${ext}`; + } +} diff --git a/components/controller/src/extensions/expenses/application/services/expense-payments.listener.ts b/components/controller/src/extensions/expenses/application/services/expense-payments.listener.ts new file mode 100644 index 00000000000..305aba38af7 --- /dev/null +++ b/components/controller/src/extensions/expenses/application/services/expense-payments.listener.ts @@ -0,0 +1,137 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { OnEvent } from '@nestjs/event-emitter'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { WinstonLoggerService } from '~/application/logger/logger-app.service'; +import { PAYMENT_REPOSITORY, PaymentRepository } from '~/domain/gateway/repositories/payment.repository'; +import { PaymentStatusEnum } from '~/domain/gateway/enums/payment-status.enum'; +import { PaymentTypeEnum, PaymentDirectionEnum } from '~/domain/gateway/enums/payment-type.enum'; +import type { PaymentDomainInterface } from '~/domain/gateway/interfaces/payment-domain.interface'; +import { QuantityUtils } from '~/shared/utils/quantity.utils'; +import { generateUniqueHash } from '~/utils/generate-hash.util'; +import { ExpenseProposalDomainEntity } from '../../domain/entities/expense-proposal.entity'; +import { ExpenseProposalStatus } from '../../domain/enums/expense-proposal-status.enum'; +import { ExpenseRequisiteSnapshotTypeormEntity } from '../../infrastructure/entities/expense-requisite-snapshot.typeorm-entity'; +import { EXPENSES_CHASSIS_CONFIG } from '../../domain/expenses-chassis.config'; + +/** Зеркало ExpenseDomain::RecipientType контракта expense. */ +const RECIPIENT_ORG = 2; + +/** + * Заводит исходящие платежи в реестр платежей после авторизации СЗ советом. + * + * Подписан на `entitysynced::expense::proposals` (после save зеркала в PG). + * При переходе СЗ в AUTHORIZED создаёт по одному OUTGOING-платежу на каждую + * позицию расхода: реквизиты и назначение платежа берутся из снимка + * `expense_requisite_snapshots` (зафиксирован при создании СЗ) и НЕ проходят + * через блокчейн — это персональные данные. Кассир видит платёж в реестре + * (PENDING), подтверждает оплату — gateway.interactor по типу EXPENSE проводит + * on-chain `expense::payexp` (см. processOutgoingPayment). + * + * Идемпотентность: hash платежа = item_hash; повторный sync (replay/форк) + * платежей-дублей не создаёт. + */ +@Injectable() +export class ExpensePaymentsListener { + constructor( + @Inject(PAYMENT_REPOSITORY) + private readonly payments: PaymentRepository, + @InjectRepository(ExpenseRequisiteSnapshotTypeormEntity) + private readonly snapshots: Repository, + private readonly logger: WinstonLoggerService + ) { + this.logger.setContext(ExpensePaymentsListener.name); + } + + @OnEvent('entitysynced::expense::proposals') + async handleProposalSynced(payload: { entity: ExpenseProposalDomainEntity; blockNum: number }): Promise { + const { entity } = payload; + + if (entity.status !== ExpenseProposalStatus.AUTHORIZED) return; + if (!entity.items?.length) return; + + for (const item of entity.items) { + const itemHash = item.item_hash.toLowerCase(); + + const existing = await this.payments.findByHash(itemHash); + if (existing) continue; + + const snapshot = await this.snapshots.findOne({ + where: { + coopname: entity.coopname, + proposal_hash: entity.proposal_hash, + item_hash: itemHash, + }, + }); + if (!snapshot) { + // СЗ авторизована, а реквизитов нет — кассир не сможет оплатить позицию. + this.logger.error( + `КРИТИЧНО: СЗ ${entity.proposal_hash} авторизована, но снимок реквизитов позиции ${itemHash} не найден — платёж не создан, нужна ручная сверка` + ); + continue; + } + + const { amount, symbol } = QuantityUtils.parseQuantityString(item.planned_amount); + const isOrganization = item.recipient_type === RECIPIENT_ORG; + const now = new Date(); + + const payment: PaymentDomainInterface = { + id: '', + coopname: entity.coopname, + // Владелец платежа в реестре = тот, к кому платёж относится ЛИЧНО: + // аванс под отчёт — пайщик-получатель (платёж виден в его личном реестре, + // его реквизиты/сертификат). Оплата организации — это платёж самого + // КООПЕРАТИВА (получатель-организация в реквизитах), а НЕ инициатора СЗ: + // инициатор лишь предложил расход, деньги идут не к нему — на его столе + // пайщика такой платёж появляться не должен. Поэтому org-платёж + // принадлежит кооперативу (coopname): в личных реестрах пайщиков он не + // виден (там фильтр по username), кассир видит его в общем реестре + // платежей кооператива (без фильтра по username). На on-chain payexp это + // не влияет — проводка берёт coopname/proposal_hash/item_hash, не username. + username: isOrganization ? entity.coopname : item.recipient, + quantity: amount, + symbol, + type: PaymentTypeEnum.EXPENSE, + direction: PaymentDirectionEnum.OUTGOING, + // Совет уже решил — платёж сразу готов к выплате кассиром. + status: PaymentStatusEnum.PENDING, + // Назначение платежа из снимка: пайщику — всегда «Аванс под отчёт», + // организации — введённый при создании СЗ текст. Кассир копирует as-is. + memo: snapshot.payment_purpose ?? EXPENSES_CHASSIS_CONFIG.advancePaymentPurpose, + secret: generateUniqueHash(), + payment_method_id: snapshot.method_id ?? undefined, + payment_details: { + // Для пайщика — снимок данных платёжного метода (банк/СБП — рендерится + // в реестре как обычные реквизиты); для организации — свободные + // реквизиты строкой + имя получателя. + data: snapshot.data ?? { + recipient_name: snapshot.recipient, + requisites: snapshot.requisites, + }, + amount_plus_fee: String(amount), + amount_without_fee: String(amount), + fee_amount: '0', + fee_percent: 0, + fact_fee_percent: 0, + tolerance_percent: 0, + }, + // Связь с СЗ для processOutgoingPayment (payexp требует proposal_hash) + // и для кассира (что оплачиваем). + blockchain_data: { + proposal_hash: entity.proposal_hash, + item_hash: itemHash, + description: item.description, + }, + expired_at: undefined, // бессрочный — как прочие исходящие платежи + created_at: now, + updated_at: now, + hash: itemHash, + }; + + const created = await this.payments.create(payment); + this.logger.log( + `СЗ ${entity.proposal_hash}: создан исходящий платёж ${created.id} по позиции ${itemHash} на ${item.planned_amount} (${payment.memo})` + ); + } + } +} diff --git a/components/controller/src/extensions/expenses/application/services/expense-requisite-snapshots.service.ts b/components/controller/src/extensions/expenses/application/services/expense-requisite-snapshots.service.ts new file mode 100644 index 00000000000..4f4016aaa1d --- /dev/null +++ b/components/controller/src/extensions/expenses/application/services/expense-requisite-snapshots.service.ts @@ -0,0 +1,154 @@ +import { BadRequestException, Inject, Injectable, Logger } from '@nestjs/common' +import { InjectRepository } from '@nestjs/typeorm' +import { Repository } from 'typeorm' +import type { InterExpenseRequisiteItemInput } from '@coopenomics/inter' +import { PAYMENT_METHOD_REPOSITORY, PaymentMethodRepository } from '~/domain/common/repositories/payment-method.repository' +import { EXPENSES_CHASSIS_CONFIG } from '../../domain/expenses-chassis.config' +import { ExpenseRequisiteSnapshotTypeormEntity } from '../../infrastructure/entities/expense-requisite-snapshot.typeorm-entity' +import { formatPaymentMethodRequisites } from '../../domain/utils/format-requisites.util' + +/** + * Снимки реквизитов получателей по строкам СЗ. Канон gateway prepareWithdraw → + * persistWithdraw: `validate` зовётся ДО on-chain заявки (метод существует), + * `snapshot` — ПОСЛЕ (данные метода копируются на момент создания СЗ). + * Реквизиты в чейн не пишутся — только в БД шасси и в документ 2010. + */ +@Injectable() +export class ExpenseRequisiteSnapshotsService { + private readonly logger = new Logger(ExpenseRequisiteSnapshotsService.name) + + constructor( + @Inject(PAYMENT_METHOD_REPOSITORY) + private readonly paymentMethods: PaymentMethodRepository, + @InjectRepository(ExpenseRequisiteSnapshotTypeormEntity) + private readonly repository: Repository + ) {} + + async validate(coopname: string, items: InterExpenseRequisiteItemInput[]): Promise { + await this.resolve(coopname, items) + } + + async snapshot(coopname: string, items: InterExpenseRequisiteItemInput[]): Promise { + const snapshots = await this.resolve(coopname, items) + if (snapshots.length === 0) return + try { + await this.repository.save(snapshots) + } catch (error: any) { + // On-chain заявка уже создана — без снимка бухгалтер не получит реквизиты + // для оплаты. Требуется ручная сверка. + this.logger.error( + `КРИТИЧНО: СЗ ${items[0]?.proposalHash} создана on-chain, но снимок реквизитов не сохранён: ${error.message}`, + error + ) + throw error + } + } + + /** Снимки реквизитов всех строк СЗ — для сверки советом на странице расхода. */ + async listByProposal(coopname: string, proposalHash: string): Promise { + return this.repository.find({ + where: { coopname, proposal_hash: proposalHash.toLowerCase() }, + order: { id: 'ASC' }, + }) + } + + /** Полные реквизиты платёжного метода пайщика строкой — для документов. */ + async formatForOwner(username: string, methodId: string): Promise { + const method = await this.paymentMethods.get({ username, method_id: methodId }) + return formatPaymentMethodRequisites(method) + } + + /** + * Снимок реквизитов пайщика по позиции СЗ — куда кассир платит при доплате + * перерасхода (тот же платёжный метод, что и при выдаче аванса). + */ + async getItemRequisiteData( + coopname: string, + proposalHash: string, + itemHash: string + ): Promise<{ data: Record | null; requisites: string } | null> { + const snapshot = await this.repository.findOne({ + where: { coopname, proposal_hash: proposalHash.toLowerCase(), item_hash: itemHash.toLowerCase() }, + }) + if (!snapshot) return null + return { data: snapshot.data ?? null, requisites: snapshot.requisites ?? '' } + } + + /** + * Банковские реквизиты самого кооператива — куда пайщик возвращает + * неиспользованный аванс (недорасход). Берём дефолтный (или первый) платёжный + * метод аккаунта кооператива. + */ + async getCooperativeRequisiteData( + coopname: string + ): Promise<{ data: Record | null; requisites: string } | null> { + const list = await this.paymentMethods.list({ username: coopname, page: 1, limit: 50, sortOrder: 'ASC' }) + const methods = list.items ?? [] + const method = methods.find((m) => m.is_default) ?? methods[0] + if (!method) return null + return { + data: method.data as unknown as Record, + requisites: formatPaymentMethodRequisites(method), + } + } + + private async resolve( + coopname: string, + items: InterExpenseRequisiteItemInput[] + ): Promise { + return Promise.all( + items.map(async (it) => { + // Инвариант шасси: пайщику — только аванс под отчёт (средства на личные + // реквизиты, расходом станут после отчёта чеком); организации — только + // прямая оплата по выставленным реквизитам. + if (it.isOrganization && it.mechanics !== 'DIRECT') { + throw new BadRequestException( + 'Организации/ИП доступна только оплата по счёту (DIRECT) — аванс под отчёт ей не выдаётся' + ) + } + if (!it.isOrganization && it.mechanics !== 'ADVANCE') { + throw new BadRequestException( + `Пайщику ${it.recipient} средства передаются только авансом под отчёт (ADVANCE) — оплата по счёту доступна только организации/ИП` + ) + } + + const snapshot = this.repository.create({ + coopname, + proposal_hash: it.proposalHash.toLowerCase(), + item_hash: it.itemHash.toLowerCase(), + recipient: it.recipient, + method_id: null, + method_type: null, + data: null, + requisites: it.requisites ?? '', + payment_purpose: it.paymentPurpose ?? null, + }) + + if (it.isOrganization) { + return snapshot + } + + // Пайщику назначение платежа фиксированное — кассир в платёжке всегда + // указывает «Аванс под отчёт» (суть расхода — в описании позиции) + snapshot.payment_purpose = EXPENSES_CHASSIS_CONFIG.advancePaymentPurpose + + if (!it.paymentMethodId) { + throw new BadRequestException( + `Не указаны реквизиты получателя ${it.recipient} (платёжный метод) по строке расхода` + ) + } + + const method = await this.paymentMethods.get({ + username: it.recipient, + method_id: it.paymentMethodId, + }) + + snapshot.method_id = method.method_id + snapshot.method_type = method.method_type + snapshot.data = method.data as unknown as Record + snapshot.requisites = formatPaymentMethodRequisites(method) + return snapshot + }) + ) + } +} diff --git a/components/controller/src/extensions/expenses/application/services/expenses-capital-trigger.service.spec.ts b/components/controller/src/extensions/expenses/application/services/expenses-capital-trigger.service.spec.ts new file mode 100644 index 00000000000..954251a4503 --- /dev/null +++ b/components/controller/src/extensions/expenses/application/services/expenses-capital-trigger.service.spec.ts @@ -0,0 +1,117 @@ +import type { EventEmitter2 } from '@nestjs/event-emitter'; +import { + ExpensesCapitalTriggerService, + EXPENSE_CAPITALIZATION_REQUIRED, +} from './expenses-capital-trigger.service'; +import { ExpenseProposalDomainEntity } from '../../domain/entities/expense-proposal.entity'; +import { ExpenseProposalStatus } from '../../domain/enums/expense-proposal-status.enum'; +import type { ExpenseProposalRepository } from '../../domain/repositories/expense-proposal.repository'; +import type { WinstonLoggerService } from '~/application/logger/logger-app.service'; + +function makeProposal( + status: ExpenseProposalStatus, + total_actual?: string +): ExpenseProposalDomainEntity { + return { + proposal_hash: '0xabc', + coopname: 'voskhod', + username: 'creator', + status, + total_actual: total_actual ?? '1500.0000 RUB', + } as unknown as ExpenseProposalDomainEntity; +} + +function makeProposalWithoutTotal(status: ExpenseProposalStatus): ExpenseProposalDomainEntity { + return { + proposal_hash: '0xabc', + coopname: 'voskhod', + status, + } as unknown as ExpenseProposalDomainEntity; +} + +function makeLogger(): WinstonLoggerService & { + log: jest.Mock; + warn: jest.Mock; + setContext: jest.Mock; +} { + return { + log: jest.fn(), + warn: jest.fn(), + setContext: jest.fn(), + } as unknown as WinstonLoggerService & { + log: jest.Mock; + warn: jest.Mock; + setContext: jest.Mock; + }; +} + +describe('ExpensesCapitalTriggerService', () => { + let service: ExpensesCapitalTriggerService; + let logger: ReturnType; + let proposals: ExpenseProposalRepository; + let eventEmitter: EventEmitter2 & { emit: jest.Mock }; + + beforeEach(() => { + logger = makeLogger(); + proposals = {} as ExpenseProposalRepository; + eventEmitter = { emit: jest.fn() } as unknown as EventEmitter2 & { emit: jest.Mock }; + service = new ExpensesCapitalTriggerService(proposals, eventEmitter, logger); + }); + + it('ничего не делает для не-CLOSED статуса', async () => { + await service.handleProposalSynced({ + entity: makeProposal(ExpenseProposalStatus.REPORT_SUBMITTED), + blockNum: 100, + }); + expect(logger.log).not.toHaveBeenCalled(); + expect(logger.warn).not.toHaveBeenCalled(); + }); + + it('ничего не делает для AUTHORIZED (не финальный статус)', async () => { + await service.handleProposalSynced({ + entity: makeProposal(ExpenseProposalStatus.AUTHORIZED), + blockNum: 100, + }); + expect(logger.log).not.toHaveBeenCalled(); + }); + + it('warn без total_actual при CLOSED', async () => { + await service.handleProposalSynced({ + entity: makeProposalWithoutTotal(ExpenseProposalStatus.CLOSED), + blockNum: 100, + }); + expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('capitalization не запускается')); + expect(logger.log).not.toHaveBeenCalled(); + }); + + it('эмитит EXPENSE_CAPITALIZATION_REQUIRED для CLOSED + total_actual', async () => { + await service.handleProposalSynced({ + entity: makeProposal(ExpenseProposalStatus.CLOSED, '1500.0000 RUB'), + blockNum: 100, + }); + expect(logger.log).toHaveBeenCalledTimes(1); + const message = logger.log.mock.calls[0][0] as string; + expect(message).toContain('0xabc'); + expect(message).toContain('1500.0000 RUB'); + expect(message).toContain('100'); + expect(eventEmitter.emit).toHaveBeenCalledWith( + EXPENSE_CAPITALIZATION_REQUIRED, + expect.objectContaining({ + coopname: 'voskhod', + proposal_hash: '0xabc', + username: 'creator', + total_actual: '1500.0000 RUB', + block_num: 100, + }) + ); + expect(logger.warn).not.toHaveBeenCalled(); + }); + + it('игнорирует DECLINED (capitalization не запускается)', async () => { + await service.handleProposalSynced({ + entity: makeProposal(ExpenseProposalStatus.DECLINED), + blockNum: 100, + }); + expect(logger.log).not.toHaveBeenCalled(); + }); +}); diff --git a/components/controller/src/extensions/expenses/application/services/expenses-capital-trigger.service.ts b/components/controller/src/extensions/expenses/application/services/expenses-capital-trigger.service.ts new file mode 100644 index 00000000000..1c16466a1fd --- /dev/null +++ b/components/controller/src/extensions/expenses/application/services/expenses-capital-trigger.service.ts @@ -0,0 +1,90 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { EventEmitter2, OnEvent } from '@nestjs/event-emitter'; +import { WinstonLoggerService } from '~/application/logger/logger-app.service'; +import { ExpenseProposalDomainEntity } from '../../domain/entities/expense-proposal.entity'; +import { ExpenseProposalStatus } from '../../domain/enums/expense-proposal-status.enum'; +import { + ExpenseProposalRepository, + EXPENSE_PROPOSAL_REPOSITORY, +} from '../../domain/repositories/expense-proposal.repository'; + +/** + * Payload события `expense::capitalization::required`. Эмитит при переходе сметы + * в CLOSED — подписчик в capital extension выполнит capital::createrid через + * WriteMutationPool под подпись кооператива. + */ +export interface ExpenseCapitalizationRequiredPayload { + coopname: string; + proposal_hash: string; + username: string; + total_actual: string; + block_num: number; +} + +export const EXPENSE_CAPITALIZATION_REQUIRED = 'expense::capitalization::required'; + +/** + * Триггер capitalization при закрытии сметы расхода (CLOSED). + * + * Подписан на `entitysynced::expense::proposals` (emit'ит `ExpenseProposalSyncService` + * после save в PG). При переходе в CLOSED — эмитит платформенное событие + * `expense::capitalization::required` с полным payload. + * + * Capital extension подписывается на это событие и выполняет capital::createrid + * через WriteMutationPool — coupling двух extensions через event-bus, без прямой + * инъекции CapitalExtensionModule. + * + * Идемпотентность: подписан на `entitysynced` (после save в PG), повторная + * capitalization защищается на уровне capital через sync_key dedup в pool. + */ +@Injectable() +export class ExpensesCapitalTriggerService { + constructor( + @Inject(EXPENSE_PROPOSAL_REPOSITORY) + private readonly proposals: ExpenseProposalRepository, + private readonly eventEmitter: EventEmitter2, + private readonly logger: WinstonLoggerService + ) { + this.logger.setContext(ExpensesCapitalTriggerService.name); + } + + @OnEvent('entitysynced::expense::proposals') + async handleProposalSynced(payload: { entity: ExpenseProposalDomainEntity; blockNum: number }): Promise { + const { entity, blockNum } = payload; + + if (entity.status !== ExpenseProposalStatus.CLOSED) { + return; + } + + if (!entity.total_actual) { + this.logger.warn( + `СЗ ${entity.proposal_hash} закрыта без total_actual — capitalization не запускается.` + ); + return; + } + + if (!entity.username) { + this.logger.warn( + `СЗ ${entity.proposal_hash} закрыта без username — capitalization не запускается.` + ); + return; + } + + const event: ExpenseCapitalizationRequiredPayload = { + coopname: entity.coopname, + proposal_hash: entity.proposal_hash, + username: entity.username, + total_actual: entity.total_actual, + block_num: blockNum, + }; + + this.logger.log( + `СЗ ${entity.proposal_hash} закрыта в блоке ${blockNum} (total_actual=${entity.total_actual}). ` + + `Эмит ${EXPENSE_CAPITALIZATION_REQUIRED} для capital extension.` + ); + + this.eventEmitter.emit(EXPENSE_CAPITALIZATION_REQUIRED, event); + + void this.proposals; + } +} diff --git a/components/controller/src/extensions/expenses/application/services/expenses-management.service.ts b/components/controller/src/extensions/expenses/application/services/expenses-management.service.ts new file mode 100644 index 00000000000..846ce7f1ea8 --- /dev/null +++ b/components/controller/src/extensions/expenses/application/services/expenses-management.service.ts @@ -0,0 +1,104 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { + ExpenseProposalRepository, + EXPENSE_PROPOSAL_REPOSITORY, +} from '../../domain/repositories/expense-proposal.repository'; +import { ExpenseProposalDomainEntity } from '../../domain/entities/expense-proposal.entity'; +import { + PaginationInputDTO, + PaginationResult, +} from '~/application/common/dto/pagination.dto'; +import { DocumentAggregationService } from '~/domain/document/services/document-aggregation.service'; +import type { DocumentDomainAggregate } from '~/domain/document/aggregates/document-domain.aggregate'; + +/** + * Агрегаты подписанных документов сметы расхода: заявление + решение совета. + * + * Используется и в одиночном fetch'е, и в paginated-листе (агрегаты собираются + * через batched Promise.all, на типичную страницу 10–20 смет это OK). + */ +export interface ExpenseProposalDocumentAggregates { + statement_doc?: DocumentDomainAggregate | null; + decision_doc?: DocumentDomainAggregate | null; +} + +/** + * Read-сервис расходов: список и просмотр смет. + * + * Mutation-операции (`payexp`/`reportexp`/`returnexp`) — отдельные use-case-сервисы; + * этот сервис только читает зеркало из PG (никаких RPC-fallback'ов, см. ADR-011). + */ +@Injectable() +export class ExpensesManagementService { + constructor( + @Inject(EXPENSE_PROPOSAL_REPOSITORY) + private readonly proposals: ExpenseProposalRepository, + private readonly documentAggregation: DocumentAggregationService + ) {} + + /** + * Собрать агрегаты обоих документов сметы (заявление + решение совета). + * + * Возвращает `undefined` для отсутствующих доков, `null` — если хэш пустой + * (см. `DocumentAggregationService.EMPTY_HASH` контракт). Это разные состояния: + * - `undefined` → доку не приложен / зеркало ещё не пришло; + * - `null` → доку приложен, но пустой хэш (пограничный случай chain-mirror). + */ + async buildProposalDocumentAggregates( + entity: ExpenseProposalDomainEntity + ): Promise { + const [statement_doc, decision_doc] = await Promise.all([ + entity.statement_doc + ? this.documentAggregation.buildDocumentAggregate(entity.statement_doc) + : Promise.resolve(undefined), + entity.decision_doc + ? this.documentAggregation.buildDocumentAggregate(entity.decision_doc) + : Promise.resolve(undefined), + ]); + return { statement_doc, decision_doc }; + } + + async getProposalByHash(proposalHash: string): Promise { + return this.proposals.findByProposalHash(proposalHash); + } + + async listProposalsByCooperative(coopname: string): Promise { + return this.proposals.findByCoopname(coopname); + } + + async listProposalsByMember(coopname: string, username: string): Promise { + return this.proposals.findByUsername(coopname, username); + } + + async listProposalsByCooperativePaginated( + coopname: string, + options?: PaginationInputDTO + ): Promise> { + const { items, totalCount } = await this.proposals.findByCoopnamePaginated(coopname, options); + return this.toPaginationResult(items, totalCount, options); + } + + async listProposalsByMemberPaginated( + coopname: string, + username: string, + options?: PaginationInputDTO + ): Promise> { + const { items, totalCount } = await this.proposals.findByUsernamePaginated(coopname, username, options); + return this.toPaginationResult(items, totalCount, options); + } + + private toPaginationResult( + items: T[], + totalCount: number, + options?: PaginationInputDTO + ): PaginationResult { + const page = Math.max(1, options?.page ?? 1); + const limit = Math.max(1, options?.limit ?? 10); + return { + items, + totalCount, + totalPages: limit > 0 ? Math.ceil(totalCount / limit) : 0, + currentPage: page, + }; + } +} diff --git a/components/controller/src/extensions/expenses/application/services/expenses-mutations.service.spec.ts b/components/controller/src/extensions/expenses/application/services/expenses-mutations.service.spec.ts new file mode 100644 index 00000000000..262808ba6c5 --- /dev/null +++ b/components/controller/src/extensions/expenses/application/services/expenses-mutations.service.spec.ts @@ -0,0 +1,401 @@ +import { ExpensesMutationsService } from './expenses-mutations.service' +import type { ExpensesBlockchainPort } from '../../domain/interfaces/expenses-blockchain.port' +import type { GeneratorInfrastructureService } from '~/infrastructure/generator/generator.service' +import type { ExpenseProposalRepository } from '../../domain/repositories/expense-proposal.repository' +import type { PaymentRepository } from '~/domain/gateway/repositories/payment.repository' +import { PaymentDirectionEnum, PaymentTypeEnum } from '~/domain/gateway/enums/payment-type.enum' +import config from '~/config/config' +import type { PayExpenseItemInputDTO } from '../dto/pay-expense-item.input' +import type { ReportExpenseItemInputDTO } from '../dto/report-expense-item.input' +import { ExpenseReportOutcome } from '../dto/report-expense-item.output' +import type { ReturnExpenseItemInputDTO } from '../dto/return-expense-item.input' +import type { OverspendExpenseItemInputDTO } from '../dto/overspend-expense-item.input' +import type { SubmitExpenseReportInputDTO } from '../dto/submit-expense-report.input' +import type { CreateExpenseProposalInputDTO } from '../dto/create-expense-proposal.input' +import { ExpenseMechanics } from '../../domain/enums/expense-mechanics.enum' +import { ExpenseRecipientType } from '../../domain/enums/expense-recipient-type.enum' +import { ExpenseReportState } from '../../domain/enums/expense-report-state.enum' + +// Символ/precision берём из конфига ноды — тесты расчёта разницы не зависят от +// того, какой именно root_govern_symbol сконфигурирован в окружении CI. +const SYM = config.blockchain.root_govern_symbol +const PREC = config.blockchain.root_govern_precision +const asset = (n: number) => `${n.toFixed(PREC)} ${SYM}` + +/** + * Контракт-тест: backend-mutations пробрасывают payload в `ExpensesBlockchainPort` + * с корректным action-mapping. createExpenseProposal раскладывает подписанный + * document2 через `.toDocument()` и снимает реквизиты получателей-пайщиков. + * authexp/declexp — callbacks решения совета, у backend'а их нет. + */ +describe('ExpensesMutationsService', () => { + let service: ExpensesMutationsService + let chain: jest.Mocked + let generator: jest.Mocked> + let requisiteSnapshots: { + validate: jest.Mock + snapshot: jest.Mock + formatForOwner: jest.Mock + getItemRequisiteData: jest.Mock + getCooperativeRequisiteData: jest.Mock + } + let proposals: { findByProposalHash: jest.Mock } + let payments: { findByHash: jest.Mock; create: jest.Mock; update: jest.Mock } + + const fakeResult = { response: { transaction_id: 'tx_abc' } } as never + + beforeEach(() => { + chain = { + createExp: jest.fn().mockResolvedValue(fakeResult), + payExp: jest.fn().mockResolvedValue(fakeResult), + reportExp: jest.fn().mockResolvedValue(fakeResult), + returnExp: jest.fn().mockResolvedValue(fakeResult), + overspendExp: jest.fn().mockResolvedValue(fakeResult), + closeExp: jest.fn().mockResolvedValue(fakeResult), + } as unknown as jest.Mocked + generator = { + generateDocument: jest.fn().mockResolvedValue({} as never), + } + requisiteSnapshots = { + validate: jest.fn().mockResolvedValue(undefined), + snapshot: jest.fn().mockResolvedValue(undefined), + formatForOwner: jest.fn().mockResolvedValue('Банковский перевод: счёт 40817810000000000000, Банк ВТБ (ПАО)'), + getItemRequisiteData: jest.fn().mockResolvedValue({ data: { bank_name: 'ВТБ' }, requisites: 'счёт пайщика' }), + getCooperativeRequisiteData: jest.fn().mockResolvedValue({ data: { bank_name: 'Банк кооператива' }, requisites: 'счёт кооператива' }), + } + proposals = { findByProposalHash: jest.fn().mockResolvedValue(null) } + payments = { + findByHash: jest.fn().mockResolvedValue(null), + create: jest.fn().mockImplementation((p) => Promise.resolve({ ...p, id: 'pay-1' })), + update: jest.fn().mockResolvedValue(undefined), + } + service = new ExpensesMutationsService( + chain, + generator as unknown as GeneratorInfrastructureService, + requisiteSnapshots as never, + proposals as unknown as ExpenseProposalRepository, + payments as unknown as PaymentRepository + ) + }) + + // СЗ-зеркало с одной строкой-авансом на пайщика, выданный аванс = 1000. + const mockProposalWithAdvance = (advance = 1000) => + proposals.findByProposalHash.mockResolvedValue({ + proposal_hash: '0xabc', + items: [ + { + // Зеркало хранит mechanics/recipient_type числами (ExpenseDomain enum): + // ADVANCE=0, MEMBER=1. PAID=1. + item_hash: '0xdef', + mechanics: 0, + recipient_type: 1, + recipient: 'petrov', + description: 'Закупка кормов', + planned_amount: asset(advance), + actual_amount: asset(advance), + status: 1, + }, + ], + } as never) + + const makeSignedDoc = (overrides: Partial<{ hash: string; doc_hash: string; meta_hash: string }> = {}) => ({ + version: '1', + hash: overrides.hash ?? '0xhash', + doc_hash: overrides.doc_hash ?? '0xdoc', + meta_hash: overrides.meta_hash ?? '0xmeta', + meta: { title: 'Заявление', registry_id: 2010 }, + signatures: [ + { + id: 0, + signed_hash: '0xsignedhash', + signer: 'ivanov', + public_key: 'EOS6...', + signature: 'SIG_K1_...', + signed_at: '2026-06-02T10:00:00', + meta: '', + }, + ], + toDocument(): any { + return { + version: '1', + hash: overrides.hash ?? '0xhash', + doc_hash: overrides.doc_hash ?? '0xdoc', + meta_hash: overrides.meta_hash ?? '0xmeta', + meta: JSON.stringify({ title: 'Заявление', registry_id: 2010 }), + signatures: this.signatures, + } + }, + }) + + it('createExpenseProposal → chain.createExp с раскладкой items + document2', async () => { + const input: CreateExpenseProposalInputDTO = { + coopname: 'voskhod', + username: 'ivanov', + proposal_hash: '0xabc', + source_wallet: 'w.cap.blago', + items: [ + { + item_hash: '0xitem1', + mechanics: ExpenseMechanics.ADVANCE, + recipient_type: ExpenseRecipientType.MEMBER, + recipient: 'petrov', + description: 'Закупка кормов', + planned_amount: '5000.0000 RUB', + payment_method_id: 'pm-1', + }, + ], + statement: makeSignedDoc() as any, + } as CreateExpenseProposalInputDTO + + await service.createExpenseProposal(input) + + expect(chain.createExp).toHaveBeenCalledTimes(1) + const call = chain.createExp.mock.calls[0][0] + expect(call.coopname).toBe('voskhod') + expect(call.username).toBe('ivanov') + expect(call.proposal_hash).toBe('0xabc') + expect(call.source_wallet).toBe('w.cap.blago') + expect(call.items).toHaveLength(1) + expect(call.items[0].item_hash).toBe('0xitem1') + expect(call.items[0].mechanics).toBe(0) + expect(call.items[0].recipient_type).toBe(1) + expect(call.callback).toEqual({ contract: '', action: '', data: '' }) + expect(call.statement.doc_hash).toBe('0xdoc') + + // Реквизиты: валидация до блокчейна, снимок — после. + expect(requisiteSnapshots.validate).toHaveBeenCalledTimes(1) + expect(requisiteSnapshots.snapshot).toHaveBeenCalledTimes(1) + const snapItems = requisiteSnapshots.snapshot.mock.calls[0][1] + expect(snapItems[0]).toMatchObject({ + proposalHash: '0xabc', + itemHash: '0xitem1', + recipient: 'petrov', + isOrganization: false, + paymentMethodId: 'pm-1', + }) + }) + + it('createExpenseProposal: ошибка валидации реквизитов не доходит до блокчейна', async () => { + requisiteSnapshots.validate.mockRejectedValue(new Error('Не указаны реквизиты получателя')) + const input: CreateExpenseProposalInputDTO = { + coopname: 'voskhod', + username: 'ivanov', + proposal_hash: '0xabc', + source_wallet: 'w.cap.blago', + items: [ + { + item_hash: '0xitem1', + mechanics: ExpenseMechanics.ADVANCE, + recipient_type: ExpenseRecipientType.SELF, + recipient: 'ivanov', + description: 'Канцелярия', + planned_amount: '1000.0000 RUB', + }, + ], + statement: makeSignedDoc() as any, + } as CreateExpenseProposalInputDTO + + await expect(service.createExpenseProposal(input)).rejects.toThrow('Не указаны реквизиты') + expect(chain.createExp).not.toHaveBeenCalled() + }) + + it('payExpenseItem → chain.payExp({coopname, proposal_hash, item_hash, actual_amount})', async () => { + const input = { + coopname: 'voskhod', + proposal_hash: '0xabc', + item_hash: '0xdef', + actual_amount: '100.0000 RUB', + } as PayExpenseItemInputDTO + + await service.payExpenseItem(input) + + expect(chain.payExp).toHaveBeenCalledWith({ + coopname: 'voskhod', + proposal_hash: '0xabc', + item_hash: '0xdef', + actual_amount: '100.0000 RUB', + }) + }) + + it('reportExpenseItem → chain.reportExp({coopname, proposal_hash, item_hash})', async () => { + const input = { + coopname: 'voskhod', + proposal_hash: '0xabc', + item_hash: '0xdef', + } as ReportExpenseItemInputDTO + + const result = await service.reportExpenseItem(input) + + expect(chain.reportExp).toHaveBeenCalledWith({ + coopname: 'voskhod', + proposal_hash: '0xabc', + item_hash: '0xdef', + }) + expect(result.outcome).toBe(ExpenseReportOutcome.CLOSED) + expect(payments.create).not.toHaveBeenCalled() + }) + + it('reportExpenseItem: факт == аванс → CLOSED, платёжка расчёта не заводится', async () => { + mockProposalWithAdvance(1000) + // Платёж выдачи аванса (hash = item_hash) — для зеркалирования report_state. + payments.findByHash.mockResolvedValue({ id: 'adv-1', hash: '0xdef', blockchain_data: { proposal_hash: '0xabc' } }) + const result = await service.reportExpenseItem({ + coopname: 'voskhod', + proposal_hash: '0xabc', + item_hash: '0xdef', + actual_amount: asset(1000), + } as ReportExpenseItemInputDTO) + + expect(result.outcome).toBe(ExpenseReportOutcome.CLOSED) + expect(chain.reportExp).toHaveBeenCalledTimes(1) + expect(payments.create).not.toHaveBeenCalled() + // Отчёт принят → зеркалим CLOSED в платёж аванса. + expect(payments.update).toHaveBeenCalledWith('adv-1', { + blockchain_data: { proposal_hash: '0xabc', report_state: ExpenseReportState.CLOSED }, + }) + }) + + it('reportExpenseItem: недорасход → входящая платёжка EXPENSE_RETURN на разницу, reportexp отложен', async () => { + mockProposalWithAdvance(1000) + // Платёжка расчёта (хэш settlement) ещё не заведена → null (чтобы create сработал); + // платёж выдачи аванса (item_hash '0xdef') существует → для зеркалирования report_state. + payments.findByHash.mockImplementation((h: string) => + Promise.resolve(h === '0xdef' ? { id: 'adv-1', hash: '0xdef', blockchain_data: {} } : null), + ) + const result = await service.reportExpenseItem({ + coopname: 'voskhod', + proposal_hash: '0xabc', + item_hash: '0xdef', + actual_amount: asset(800), + } as ReportExpenseItemInputDTO) + + expect(result.outcome).toBe(ExpenseReportOutcome.RETURN_PENDING) + expect(result.settlement_amount).toBe(asset(200)) + expect(chain.reportExp).not.toHaveBeenCalled() + expect(payments.create).toHaveBeenCalledTimes(1) + // Отчёт подан, ждём расчёт → зеркалим SETTLEMENT_PENDING + заявленный факт + // (reported_amount) в платёж аванса; факт нужен реестру вместо выданного аванса. + expect(payments.update).toHaveBeenCalledWith('adv-1', { + blockchain_data: { + report_state: ExpenseReportState.SETTLEMENT_PENDING, + reported_amount: asset(800), + }, + }) + const payment = payments.create.mock.calls[0][0] + expect(payment.type).toBe(PaymentTypeEnum.EXPENSE_RETURN) + expect(payment.direction).toBe(PaymentDirectionEnum.INCOMING) + expect(payment.username).toBe('petrov') + expect(payment.quantity).toBe(200) + expect(payment.blockchain_data).toMatchObject({ proposal_hash: '0xabc', item_hash: '0xdef' }) + // Недорасход: реквизиты для оплаты — банк кооператива (пайщик возвращает ему). + expect(requisiteSnapshots.getCooperativeRequisiteData).toHaveBeenCalledWith('voskhod') + expect(payment.payment_details?.data).toMatchObject({ bank_name: 'Банк кооператива' }) + }) + + it('reportExpenseItem: перерасход → исходящая платёжка EXPENSE_OVERSPEND на разницу, reportexp отложен', async () => { + mockProposalWithAdvance(1000) + const result = await service.reportExpenseItem({ + coopname: 'voskhod', + proposal_hash: '0xabc', + item_hash: '0xdef', + actual_amount: asset(1200), + } as ReportExpenseItemInputDTO) + + expect(result.outcome).toBe(ExpenseReportOutcome.OVERSPEND_PENDING) + expect(result.settlement_amount).toBe(asset(200)) + expect(chain.reportExp).not.toHaveBeenCalled() + expect(payments.create).toHaveBeenCalledTimes(1) + const payment = payments.create.mock.calls[0][0] + expect(payment.type).toBe(PaymentTypeEnum.EXPENSE_OVERSPEND) + expect(payment.direction).toBe(PaymentDirectionEnum.OUTGOING) + expect(payment.quantity).toBe(200) + // Перерасход: реквизиты для выплаты — снимок реквизитов пайщика по позиции. + expect(requisiteSnapshots.getItemRequisiteData).toHaveBeenCalledWith('voskhod', '0xabc', '0xdef') + expect(payment.payment_details?.data).toMatchObject({ bank_name: 'ВТБ' }) + }) + + it('reportExpenseItem: повторный отчёт по той же позиции платёжку не дублирует (идемпотентность)', async () => { + mockProposalWithAdvance(1000) + payments.findByHash.mockResolvedValue({ hash: 'existing-hash' }) + const result = await service.reportExpenseItem({ + coopname: 'voskhod', + proposal_hash: '0xabc', + item_hash: '0xdef', + actual_amount: asset(800), + } as ReportExpenseItemInputDTO) + + expect(result.outcome).toBe(ExpenseReportOutcome.RETURN_PENDING) + expect(result.settlement_payment_hash).toBe('existing-hash') + expect(payments.create).not.toHaveBeenCalled() + }) + + it('reportExpenseItem: повторный отчёт после поданного (SETTLEMENT_PENDING) запрещён', async () => { + mockProposalWithAdvance(1000) + // Платёж аванса уже несёт report_state SETTLEMENT_PENDING — отчёт подан ранее, + // позиция on-chain ещё PAID (расчёт не подтверждён). Повторный отчёт обязан + // отбиться на бэке, иначе плодятся дубль-платёжки расчёта. + payments.findByHash.mockResolvedValue({ + id: 'adv-1', + hash: '0xdef', + blockchain_data: { proposal_hash: '0xabc', report_state: ExpenseReportState.SETTLEMENT_PENDING }, + }) + await expect( + service.reportExpenseItem({ + coopname: 'voskhod', + proposal_hash: '0xabc', + item_hash: '0xdef', + actual_amount: asset(800), + } as ReportExpenseItemInputDTO), + ).rejects.toThrow('Отчёт по этой позиции уже подан') + expect(payments.create).not.toHaveBeenCalled() + }) + + it('returnExpenseItem → chain.returnExp({coopname, proposal_hash, item_hash, return_amount})', async () => { + const input = { + coopname: 'voskhod', + proposal_hash: '0xabc', + item_hash: '0xdef', + return_amount: '50.0000 RUB', + } as ReturnExpenseItemInputDTO + + await service.returnExpenseItem(input) + + expect(chain.returnExp).toHaveBeenCalledWith({ + coopname: 'voskhod', + proposal_hash: '0xabc', + item_hash: '0xdef', + return_amount: '50.0000 RUB', + }) + }) + + it('overspendExpenseItem → chain.overspendExp({coopname, proposal_hash, item_hash, overspend_amount})', async () => { + const input = { + coopname: 'voskhod', + proposal_hash: '0xabc', + item_hash: '0xdef', + overspend_amount: '200.0000 RUB', + } as OverspendExpenseItemInputDTO + + await service.overspendExpenseItem(input) + + expect(chain.overspendExp).toHaveBeenCalledWith({ + coopname: 'voskhod', + proposal_hash: '0xabc', + item_hash: '0xdef', + overspend_amount: '200.0000 RUB', + }) + }) + + it('submitExpenseReport → chain.closeExp({coopname, proposal_hash})', async () => { + const input = { + coopname: 'voskhod', + proposal_hash: '0xabc', + total_actual_amount: '1500.0000 RUB', + comment: 'пиар-кампания апрель', + } as SubmitExpenseReportInputDTO + + await service.submitExpenseReport(input) + + expect(chain.closeExp).toHaveBeenCalledWith({ coopname: 'voskhod', proposal_hash: '0xabc' }) + }) +}) diff --git a/components/controller/src/extensions/expenses/application/services/expenses-mutations.service.ts b/components/controller/src/extensions/expenses/application/services/expenses-mutations.service.ts new file mode 100644 index 00000000000..22ad3c13325 --- /dev/null +++ b/components/controller/src/extensions/expenses/application/services/expenses-mutations.service.ts @@ -0,0 +1,424 @@ +import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common' +import type { TransactResult } from '@wharfkit/session' +import type { InterExpenseRequisiteItemInput } from '@coopenomics/inter' +import { Cooperative } from 'cooptypes' +import { GeneratorInfrastructureService } from '~/infrastructure/generator/generator.service' +import type { DocumentDomainEntity } from '~/domain/document/entity/document-domain.entity' +import { ExpenseProposalStatementGenerateDocumentInputDTO } from '~/application/document/documents-dto/expense-proposal-statement-document.dto' +import { ExpenseProposalDecisionGenerateDocumentInputDTO } from '~/application/document/documents-dto/expense-proposal-decision-document.dto' +import { PAYMENT_REPOSITORY, PaymentRepository } from '~/domain/gateway/repositories/payment.repository' +import type { PaymentDomainInterface } from '~/domain/gateway/interfaces/payment-domain.interface' +import { PaymentStatusEnum } from '~/domain/gateway/enums/payment-status.enum' +import { PaymentDirectionEnum, PaymentTypeEnum } from '~/domain/gateway/enums/payment-type.enum' +import { QuantityUtils } from '~/shared/utils/quantity.utils' +import { generateHashFromString, generateUniqueHash } from '~/utils/generate-hash.util' +import { CreateExpenseProposalInputDTO } from '../dto/create-expense-proposal.input' +import type { ExpenseItemInputDTO } from '../dto/expense-item.input' +import { PayExpenseItemInputDTO } from '../dto/pay-expense-item.input' +import { ReportExpenseItemInputDTO } from '../dto/report-expense-item.input' +import { ExpenseReportOutcome, ExpenseReportResultDTO } from '../dto/report-expense-item.output' +import { ReturnExpenseItemInputDTO } from '../dto/return-expense-item.input' +import { OverspendExpenseItemInputDTO } from '../dto/overspend-expense-item.input' +import { SubmitExpenseReportInputDTO } from '../dto/submit-expense-report.input' +import { + EXPENSES_BLOCKCHAIN_PORT, + ExpensesBlockchainPort, +} from '../../domain/interfaces/expenses-blockchain.port' +import { + EXPENSE_PROPOSAL_REPOSITORY, + ExpenseProposalRepository, +} from '../../domain/repositories/expense-proposal.repository' +import type { IExpenseItemBlockchainData } from '../../domain/interfaces/expense-proposal-blockchain.interface' +import { ExpenseMechanics } from '../../domain/enums/expense-mechanics.enum' +import { ExpenseRecipientType } from '../../domain/enums/expense-recipient-type.enum' +import { ExpenseReportState } from '../../domain/enums/expense-report-state.enum' +import { EXPENSES_CHASSIS_CONFIG } from '../../domain/expenses-chassis.config' +import { ExpenseRequisiteSnapshotsService } from './expense-requisite-snapshots.service' + +/** Зеркало ExpenseDomain::Mechanics::ADVANCE контракта expense. */ +const MECHANICS_ADVANCE = 0 + +/** + * Write-сервис расходов. + * + * Подписывает ключом кооператива (`active`), `account = expense`. Для пайщик-actions + * (`reportexp` / `returnexp`) — тот же канон, что в capital (`createCommit` пайщика + * сервисно подписан кооперативом). Авторизация / отклонение СЗ идут через решение + * совета (callbacks authexp/declexp от контракта soviet) — здесь их нет. + */ +@Injectable() +export class ExpensesMutationsService { + constructor( + @Inject(EXPENSES_BLOCKCHAIN_PORT) + private readonly chain: ExpensesBlockchainPort, + private readonly generator: GeneratorInfrastructureService, + private readonly requisiteSnapshots: ExpenseRequisiteSnapshotsService, + @Inject(EXPENSE_PROPOSAL_REPOSITORY) + private readonly proposals: ExpenseProposalRepository, + @Inject(PAYMENT_REPOSITORY) + private readonly payments: PaymentRepository + ) {} + + async generateExpenseProposalStatementDocument( + data: ExpenseProposalStatementGenerateDocumentInputDTO, + options: Cooperative.Document.IGenerationOptions + ): Promise { + const registry_id = Cooperative.Registry.ExpenseProposalStatement.registry_id + + // Фонд списания — параметр шасси, фронт его не передаёт + const proposal = { ...data.proposal, fund_name: EXPENSES_CHASSIS_CONFIG.fundNameDative } + + // Приватная часть позиций — имя/реквизиты/назначение платежа. Полные реквизиты + // получателей подставляет сервер по payment_method_id (фронт знает только + // сокращённое представление). Эти данные НЕ публикуются в блокчейн: они + // сохраняются off-chain в doc_data фабрики, а в meta документа едет только + // doc_data_hash. Корреляция с публичной позицией — по number. + const privateItems = await Promise.all( + data.items.map(async (item) => { + let requisites = item.requisites + if (item.payment_method_id) { + const owner = item.recipient_username || data.username + requisites = await this.requisiteSnapshots.formatForOwner(owner, item.payment_method_id) + } + return { + number: item.number, + recipient_name: item.recipient_name, + requisites, + // Пайщику назначение платежа не вводится — всегда «Аванс под отчёт» + payment_purpose: + item.recipient_type === 'ORG' ? item.payment_purpose : EXPENSES_CHASSIS_CONFIG.advancePaymentPurpose, + } + }) + ) + + const privatePayload: Cooperative.Registry.ExpenseProposalStatement.PrivateData = { items: privateItems } + const { hash: doc_data_hash } = await this.generator.saveDocData( + privatePayload as unknown as Record, + registry_id + ) + + // Публичные позиции — без реквизитов/имени/назначения (только структура и суммы). + const items: Cooperative.Registry.ExpenseProposalStatement.IExpenseItem[] = data.items.map((item) => ({ + number: item.number, + description: item.description, + amount: item.amount, + recipient_type: item.recipient_type, + mechanics: item.mechanics, + })) + + const action = { + ...data, + registry_id, + proposal, + items, + doc_data_hash, + } as unknown as Cooperative.Registry.ExpenseProposalStatement.Action + + return this.generator.generateDocument({ data: action, options: options || {} }) + } + + async generateExpenseProposalDecisionDocument( + data: ExpenseProposalDecisionGenerateDocumentInputDTO, + options: Cooperative.Document.IGenerationOptions + ): Promise { + data.registry_id = Cooperative.Registry.ExpenseProposalDecision.registry_id + return this.generator.generateDocument({ data: data as unknown as Cooperative.Registry.ExpenseProposalDecision.Action, options: options || {} }) + } + + async createExpenseProposal(input: CreateExpenseProposalInputDTO): Promise { + // Валидация реквизитов ДО блокчейна, снимок — ПОСЛЕ (канон gateway + // prepareWithdraw → persistWithdraw). Реквизиты в чейн не пишутся. + const requisiteItems = toRequisiteItems(input.proposal_hash, input.items) + await this.requisiteSnapshots.validate(input.coopname, requisiteItems) + + const items = input.items.map((it) => ({ + item_hash: it.item_hash, + mechanics: it.mechanics === ExpenseMechanics.DIRECT ? 1 : 0, + recipient_type: + it.recipient_type === ExpenseRecipientType.SELF + ? 0 + : it.recipient_type === ExpenseRecipientType.MEMBER + ? 1 + : 2, + recipient: it.recipient, + description: it.description, + planned_amount: it.planned_amount, + actual_amount: it.planned_amount, + status: 0, + })) + + const statement = input.statement.toDocument() + + const result = await this.chain.createExp({ + coopname: input.coopname, + username: input.username, + proposal_hash: input.proposal_hash, + source_wallet: input.source_wallet, + items, + callback: { + contract: input.callback?.contract ?? '', + action: input.callback?.action ?? '', + data: input.callback?.data ?? '', + }, + statement: { + version: statement.version, + hash: statement.hash, + doc_hash: statement.doc_hash, + meta_hash: statement.meta_hash, + meta: statement.meta, + signatures: statement.signatures, + } as any, + }) + + await this.requisiteSnapshots.snapshot(input.coopname, requisiteItems) + + return result + } + + async payExpenseItem(input: PayExpenseItemInputDTO): Promise { + return this.chain.payExp({ + coopname: input.coopname, + proposal_hash: input.proposal_hash, + item_hash: input.item_hash, + actual_amount: input.actual_amount, + }) + } + + /** + * Отчёт пайщика по строке-авансу. Если факт совпал с выданным авансом (или не + * указан) — закрываем позицию on-chain (`reportexp`). Если факт расходится — + * заводим платёжку расчёта разницы (возврат/доплата) и откладываем `reportexp` + * до её подтверждения кассиром (контракт принимает returnexp/overspendexp + * только пока позиция в статусе PAID, поэтому расчёт обязан пройти ДО закрытия). + */ + async reportExpenseItem(input: ReportExpenseItemInputDTO): Promise { + // Защита от повторного отчёта. Позиция on-chain остаётся PAID до подтверждения + // расчётной платёжки кассой (reportexp отложен), поэтому без этого guard'а + // пайщик/кассир могли отчитываться многократно на разные суммы, плодя дубль- + // платёжки расчёта (идемпотентность createSettlementPayment — лишь по виду + // возврат/доплата, не по факту «отчёт уже подан»). Источник истины — + // report_state в зеркале платежа выдачи аванса. + const reportState = await this.getAdvanceReportState(input.item_hash) + if (reportState === ExpenseReportState.SETTLEMENT_PENDING || reportState === ExpenseReportState.CLOSED) { + throw new BadRequestException('Отчёт по этой позиции уже подан — повторный отчёт недоступен') + } + + if (!input.actual_amount) { + const transaction = await this.chain.reportExp({ + coopname: input.coopname, + proposal_hash: input.proposal_hash, + item_hash: input.item_hash, + }) + await this.markAdvanceReportState(input.item_hash, ExpenseReportState.CLOSED) + return { outcome: ExpenseReportOutcome.CLOSED, transaction: transaction as any } + } + + const proposal = await this.proposals.findByProposalHash(input.proposal_hash) + const item = proposal?.items?.find( + (i) => i.item_hash?.toLowerCase() === input.item_hash.toLowerCase() + ) + if (!proposal || !item) { + throw new NotFoundException('Строка расхода не найдена') + } + if (item.mechanics !== MECHANICS_ADVANCE) { + throw new BadRequestException('Отчёт о фактической сумме применим только к авансу под отчёт') + } + + // База расчёта — фактически ВЫДАННЫЙ аванс (item.actual_amount после payexp), + // а не план: устойчиво к частичной выдаче аванса кассиром. + const advance = parseAssetToMinor(item.actual_amount) + const factual = parseAssetToMinor(input.actual_amount) + if (advance.symbol !== factual.symbol) { + throw new BadRequestException('Символ фактической суммы не совпадает с символом аванса') + } + + const deltaMinor = factual.minor - advance.minor + if (deltaMinor === 0) { + const transaction = await this.chain.reportExp({ + coopname: input.coopname, + proposal_hash: input.proposal_hash, + item_hash: input.item_hash, + }) + await this.markAdvanceReportState(input.item_hash, ExpenseReportState.CLOSED) + return { outcome: ExpenseReportOutcome.CLOSED, transaction: transaction as any } + } + + const isUnderspend = deltaMinor < 0 + const diffAmount = Math.abs(deltaMinor) / 10 ** advance.precision + const diffAsset = QuantityUtils.formatQuantityForBlockchain(diffAmount, advance.symbol) + const paymentHash = await this.createSettlementPayment( + input.coopname, + input.proposal_hash, + item, + isUnderspend, + diffAmount, + advance.symbol + ) + + // Отчёт подан, но позиция закроется (reportexp) только после подтверждения + // платёжки расчёта кассой — фиксируем промежуточное состояние для реестра. + // reported_amount храним рядом, чтобы реестр показывал заявленный факт + // (а не выданный аванс) и не предлагал отчитаться повторно. + await this.markAdvanceReportState(input.item_hash, ExpenseReportState.SETTLEMENT_PENDING, { + reported_amount: input.actual_amount, + }) + + return { + outcome: isUnderspend ? ExpenseReportOutcome.RETURN_PENDING : ExpenseReportOutcome.OVERSPEND_PENDING, + settlement_amount: diffAsset, + settlement_payment_hash: paymentHash, + } + } + + /** + * Зеркалит состояние отчёта в платёж выдачи аванса (hash платежа = item_hash) — + * реестр платежей показывает «Требуется отчёт / подан / принят» рядом со + * статусом платежа, не дёргая цепь. Статус платежа при этом не трогаем. + */ + private async markAdvanceReportState( + itemHash: string, + state: ExpenseReportState, + extra?: Record + ): Promise { + const payment = await this.payments.findByHash(itemHash.toLowerCase()) + if (!payment?.id) return + await this.payments.update(payment.id, { + blockchain_data: { ...(payment.blockchain_data ?? {}), report_state: state, ...(extra ?? {}) }, + }) + } + + /** + * Текущее состояние отчёта по строке-авансу — из зеркала в платеже выдачи + * (`blockchain_data.report_state`). AWAITING по умолчанию: отчёт ещё не подан. + * Нужен guard'у reportExpenseItem против повторной подачи. + */ + private async getAdvanceReportState(itemHash: string): Promise { + const payment = await this.payments.findByHash(itemHash.toLowerCase()) + const state = (payment?.blockchain_data as { report_state?: ExpenseReportState } | undefined)?.report_state + return state ?? ExpenseReportState.AWAITING + } + + /** + * Заводит manual-confirm платёжку расчёта разницы в общем реестре платежей + * (как платёж выдачи аванса) — кассир подтверждает по факту движения денег, и + * gateway проводит on-chain returnexp/overspendexp + reportexp. Идемпотентно по + * детерминированному хэшу: повторный отчёт по той же позиции платёжку не плодит. + */ + private async createSettlementPayment( + coopname: string, + proposalHash: string, + item: IExpenseItemBlockchainData, + isUnderspend: boolean, + amount: number, + symbol: string + ): Promise { + const itemHash = item.item_hash.toLowerCase() + const kind = isUnderspend ? 'return' : 'overspend' + const hash = generateHashFromString(`expense-settlement:${coopname}:${itemHash}:${kind}`) + + const existing = await this.payments.findByHash(hash) + if (existing) return existing.hash + + // Реквизиты для платёжки: при недорасходе пайщик платит КООПЕРАТИВУ (его банк), + // при перерасходе кооператив платит ПАЙЩИКУ (снимок реквизитов позиции — тот + // же метод, что при выдаче аванса). Кассир/пайщик видят их в деталях платежа. + const requisite = isUnderspend + ? await this.requisiteSnapshots.getCooperativeRequisiteData(coopname) + : await this.requisiteSnapshots.getItemRequisiteData(coopname, proposalHash, itemHash) + + const now = new Date() + const amountStr = amount.toFixed(2) + const payment: PaymentDomainInterface = { + id: '', + coopname, + // Позиция-аванс всегда оформлена на пайщика-получателя — расчёт разницы + // относится лично к нему (виден в его личном реестре платежей). + username: item.recipient, + quantity: amount, + symbol, + type: isUnderspend ? PaymentTypeEnum.EXPENSE_RETURN : PaymentTypeEnum.EXPENSE_OVERSPEND, + direction: isUnderspend ? PaymentDirectionEnum.INCOMING : PaymentDirectionEnum.OUTGOING, + status: PaymentStatusEnum.PENDING, + // Назначение фиксированное (суть расхода — в описании позиции, поле + // blockchain_data.description показывается отдельно как «Что оплачиваем»). + memo: isUnderspend + ? 'Возврат неиспользованных средств, выданных авансом под отчёт' + : 'Доплата по перерасходу аванса под отчёт', + secret: generateUniqueHash(), + payment_method_id: undefined, + payment_details: { + // Полные реквизиты (банк/СБП) — те же поля, что у платежа выдачи аванса; + // деталь платежа рендерит их строками с копированием. + data: requisite?.data ?? { requisites: requisite?.requisites ?? '' }, + amount_plus_fee: amountStr, + amount_without_fee: amountStr, + fee_amount: '0', + fee_percent: 0, + fact_fee_percent: 0, + tolerance_percent: 0, + }, + // proposal_hash + item_hash нужны gateway для returnexp/overspendexp; hash + // платёжки уникальный (не item_hash) — не пересекается с платежом выдачи. + blockchain_data: { + proposal_hash: proposalHash.toLowerCase(), + item_hash: itemHash, + description: item.description, + }, + expired_at: undefined, + created_at: now, + updated_at: now, + hash, + } + + const created = await this.payments.create(payment) + return created.hash + } + + async returnExpenseItem(input: ReturnExpenseItemInputDTO): Promise { + return this.chain.returnExp({ + coopname: input.coopname, + proposal_hash: input.proposal_hash, + item_hash: input.item_hash, + return_amount: input.return_amount, + }) + } + + async overspendExpenseItem(input: OverspendExpenseItemInputDTO): Promise { + return this.chain.overspendExp({ + coopname: input.coopname, + proposal_hash: input.proposal_hash, + item_hash: input.item_hash, + overspend_amount: input.overspend_amount, + }) + } + + async submitExpenseReport(input: SubmitExpenseReportInputDTO): Promise { + return this.chain.closeExp({ + coopname: input.coopname, + proposal_hash: input.proposal_hash, + }) + } +} + +/** + * Парсит asset-строку ("800.0000 RUB") в целые минорные единицы по precision + * символа — сравнение/вычитание сумм без ошибок плавающей точки. + */ +function parseAssetToMinor(asset: string): { minor: number; symbol: string; precision: number } { + const { amount, symbol } = QuantityUtils.parseQuantityString(asset) + const precision = QuantityUtils.getPrecisionForSymbol(symbol) + return { minor: Math.round(amount * 10 ** precision), symbol, precision } +} + +function toRequisiteItems(proposalHash: string, items: ExpenseItemInputDTO[]): InterExpenseRequisiteItemInput[] { + return items.map((it) => ({ + proposalHash, + itemHash: it.item_hash, + recipient: it.recipient, + isOrganization: it.recipient_type === ExpenseRecipientType.ORG, + mechanics: it.mechanics === ExpenseMechanics.DIRECT ? 'DIRECT' : 'ADVANCE', + paymentMethodId: it.payment_method_id, + requisites: it.requisites, + paymentPurpose: it.payment_purpose, + })) +} diff --git a/components/controller/src/extensions/expenses/application/syncers/expense-proposal-sync.service.ts b/components/controller/src/extensions/expenses/application/syncers/expense-proposal-sync.service.ts new file mode 100644 index 00000000000..aec468376f7 --- /dev/null +++ b/components/controller/src/extensions/expenses/application/syncers/expense-proposal-sync.service.ts @@ -0,0 +1,90 @@ +import { Injectable, OnModuleInit, Inject } from '@nestjs/common'; +import { OnEvent, EventEmitter2 } from '@nestjs/event-emitter'; +import { WinstonLoggerService } from '~/application/logger/logger-app.service'; +import { AbstractEntitySyncService } from '~/shared/services/abstract-entity-sync.service'; +import type { ISyncResult } from '~/shared/interfaces/blockchain-sync.interface'; +import { ExpenseProposalDomainEntity } from '../../domain/entities/expense-proposal.entity'; +import { + ExpenseProposalRepository, + EXPENSE_PROPOSAL_REPOSITORY, +} from '../../domain/repositories/expense-proposal.repository'; +import { ExpenseProposalDeltaMapper } from '../../infrastructure/blockchain/mappers/expense-proposal-delta.mapper'; +import type { IExpenseProposalBlockchainData } from '../../domain/interfaces/expense-proposal-blockchain.interface'; + +/** + * Подписка на дельты `expense::proposals` и зеркалирование расходов в локальную БД. + * + * Канон capital-style flat (ADR-008 v2 декораторов в репозитории нет). + * После регистрации контракта `expense` в `cooptypes` маппер автоматически + * получит точный тип `IProposal` — изменений в syncer'е не потребуется. + */ +@Injectable() +export class ExpenseProposalSyncService + extends AbstractEntitySyncService + implements OnModuleInit +{ + protected readonly entityName = 'ExpenseProposal'; + + constructor( + @Inject(EXPENSE_PROPOSAL_REPOSITORY) + proposalRepository: ExpenseProposalRepository, + proposalDeltaMapper: ExpenseProposalDeltaMapper, + logger: WinstonLoggerService, + private readonly eventEmitter: EventEmitter2 + ) { + super(proposalRepository, proposalDeltaMapper, logger); + } + + async onModuleInit() { + const supportedVersions = this.getSupportedVersions(); + this.logger.debug( + `Сервис синхронизации смет расходов инициализирован. Контракты: [${supportedVersions.contracts.join( + ', ' + )}], таблицы: [${supportedVersions.tables.join(', ')}]` + ); + + const allPatterns = this.getAllEventPatterns(); + this.logger.debug(`Подписка на ${allPatterns.length} паттернов: ${allPatterns.join(', ')}`); + allPatterns.forEach((pattern) => { + this.eventEmitter.on(pattern, this.processDelta.bind(this)); + }); + + this.logger.debug('ExpenseProposalSyncService готов принимать дельты parser2'); + } + + @OnEvent('fork::*') + async handleExpenseProposalFork(forkData: { block_num: number }): Promise { + await this.handleFork(forkData.block_num); + } + + /** + * Emit `entitysynced::expense::proposals` после успешного save в PG. + * + * Канал per-contract (см. controller/CLAUDE.md «Subscription anti-patterns»): + * pubsub-канал расширения отделён от глобальной шины `delta::*`. Слушают + * business-side-effect listeners (capital-trigger → capitalization Благороста, + * notification, audit). + */ + public override async handleSyncDelta( + syncKey: string, + syncValue: string, + blockchainData: IExpenseProposalBlockchainData, + blockNum: number, + present = true + ): Promise { + const result = await super.handleSyncDelta(syncKey, syncValue, blockchainData, blockNum, present); + + if (result.created || result.updated) { + const entity = await (this.repository as ExpenseProposalRepository).findBySyncKey(syncKey, syncValue); + if (entity) { + this.eventEmitter.emit('entitysynced::expense::proposals', { + entity, + blockNum, + syncResult: result, + }); + } + } + + return result; + } +} diff --git a/components/controller/src/extensions/expenses/constants/expenses-bucket.ts b/components/controller/src/extensions/expenses/constants/expenses-bucket.ts new file mode 100644 index 00000000000..327d2909467 --- /dev/null +++ b/components/controller/src/extensions/expenses/constants/expenses-bucket.ts @@ -0,0 +1,25 @@ +/** + * Spec MinIO-бакета `expenses:files` — хранилище первичных файлов СЗ-расхода. + * + * Содержимое: + * - платёжки (PAYMENT_PROOF) — подтверждение оплаты item-а; + * - чеки / акты (REPORT_FILE) — подтверждение фактического расхода (ADVANCE); + * - возвраты (RETURN_PROOF) — подтверждение возврата неиспользованного аванса. + * + * Ключ объекта: + * `{coopname}/expenses/{proposal_hash}/{item_hash|_proposal}/{kind}/{checksum}.{ext}` + */ +export const EXPENSES_BUCKET = { + name: 'expenses:files', + maxBytes: 20 * 1024 * 1024, + allowedMime: [ + 'image/jpeg', + 'image/png', + 'image/webp', + 'image/heic', + 'application/pdf', + ] as const, + defaultUrlTtlSeconds: 600, +} as const; + +export type ExpensesBucketAllowedMime = (typeof EXPENSES_BUCKET.allowedMime)[number]; diff --git a/components/controller/src/extensions/expenses/domain/entities/expense-proposal.entity.ts b/components/controller/src/extensions/expenses/domain/entities/expense-proposal.entity.ts new file mode 100644 index 00000000000..d0870d8b3a7 --- /dev/null +++ b/components/controller/src/extensions/expenses/domain/entities/expense-proposal.entity.ts @@ -0,0 +1,139 @@ +import { BaseDomainEntity } from '~/shared/sync/entities/base-domain.entity'; +import type { IBlockchainSynchronizable } from '~/shared/interfaces/blockchain-sync.interface'; +import type { ISignedDocumentDomainInterface } from '~/domain/document/interfaces/signed-document-domain.interface'; +import type { IExpenseProposalDatabaseData } from '../interfaces/expense-proposal-database.interface'; +import type { + IExpenseProposalBlockchainData, + IExpenseProposalCallbackHandler, + IExpenseItemBlockchainData, +} from '../interfaces/expense-proposal-blockchain.interface'; +import { ExpenseProposalStatus } from '../enums/expense-proposal-status.enum'; + +/** + * Доменная сущность СЗ-расхода. + * + * Зеркалит строку `proposals` контракта `expense` (scope = coopname). + * Композиция БД + блокчейна: БД хранит идентификацию + нормализованный + * статус; блокчейн — экономику items, ссылки на документы и колбэк + * закрытия. Конструктор бросает при рассинхроне `proposal_hash`. + */ +export class ExpenseProposalDomainEntity + extends BaseDomainEntity + implements IBlockchainSynchronizable +{ + private static primary_key = 'id'; + private static sync_key = 'proposal_hash'; + + public id?: number; + public proposal_hash: string; + public coopname: string; + public status: ExpenseProposalStatus; + + public username?: string; + public source_wallet?: string; + public blockchain_status?: number; + public items?: IExpenseItemBlockchainData[]; + public total_planned?: string; + public total_actual?: string; + public callback?: IExpenseProposalCallbackHandler; + public statement_doc?: ISignedDocumentDomainInterface; + public decision_doc?: ISignedDocumentDomainInterface; + public created_at?: string; + public updated_at?: string; + + constructor(databaseData: IExpenseProposalDatabaseData, blockchainData?: IExpenseProposalBlockchainData) { + super(databaseData, ExpenseProposalStatus.UNDEFINED); + + this.proposal_hash = databaseData.proposal_hash.toLowerCase(); + this.coopname = databaseData.coopname; + this.status = databaseData.status ?? ExpenseProposalStatus.UNDEFINED; + + if (blockchainData) { + if (this.proposal_hash !== blockchainData.proposal_hash.toLowerCase()) { + throw new Error( + `Expense proposal hash mismatch: db=${this.proposal_hash}, bc=${blockchainData.proposal_hash.toLowerCase()}` + ); + } + + this.id = Number(blockchainData.id); + this.coopname = blockchainData.coopname; + this.username = blockchainData.username; + this.source_wallet = blockchainData.source_wallet; + this.blockchain_status = blockchainData.status; + this.items = blockchainData.items; + this.total_planned = blockchainData.total_planned; + this.total_actual = blockchainData.total_actual; + this.callback = blockchainData.callback; + this.statement_doc = blockchainData.statement_doc; + this.decision_doc = blockchainData.decision_doc; + this.created_at = blockchainData.created_at; + this.updated_at = blockchainData.updated_at; + + this.status = ExpenseProposalDomainEntity.mapStatusToDomain(blockchainData.status); + } + } + + getBlockNum(): number | undefined { + return this.block_num; + } + + public static getPrimaryKey(): string { + return ExpenseProposalDomainEntity.primary_key; + } + + public static getSyncKey(): string { + return ExpenseProposalDomainEntity.sync_key; + } + + getPrimaryKey(): string { + return ExpenseProposalDomainEntity.primary_key; + } + + getSyncKey(): string { + return ExpenseProposalDomainEntity.sync_key; + } + + updateFromBlockchain(blockchainData: IExpenseProposalBlockchainData, blockNum: number, present = true): void { + if (this.proposal_hash !== blockchainData.proposal_hash.toLowerCase()) { + throw new Error('Expense proposal hash mismatch on updateFromBlockchain'); + } + this.id = Number(blockchainData.id); + this.username = blockchainData.username; + this.source_wallet = blockchainData.source_wallet; + this.blockchain_status = blockchainData.status; + this.items = blockchainData.items; + this.total_planned = blockchainData.total_planned; + this.total_actual = blockchainData.total_actual; + this.callback = blockchainData.callback; + this.statement_doc = blockchainData.statement_doc; + this.decision_doc = blockchainData.decision_doc; + this.created_at = blockchainData.created_at; + this.updated_at = blockchainData.updated_at; + this.status = ExpenseProposalDomainEntity.mapStatusToDomain(blockchainData.status); + this.block_num = blockNum; + this.present = present; + } + + /** + * Маппит `uint8_t status` (`ExpenseDomain::ProposalStatus`) → enum. + * Неизвестное значение → UNDEFINED (alert на schema drift по канону). + */ + private static mapStatusToDomain(raw: number): ExpenseProposalStatus { + switch (raw) { + case 0: + return ExpenseProposalStatus.CREATED; + case 1: + return ExpenseProposalStatus.AUTHORIZED; + case 2: + return ExpenseProposalStatus.PARTIALLY_PAID; + case 3: + return ExpenseProposalStatus.REPORT_SUBMITTED; + case 4: + return ExpenseProposalStatus.CLOSED; + case 5: + return ExpenseProposalStatus.DECLINED; + default: + return ExpenseProposalStatus.UNDEFINED; + } + } +} diff --git a/components/controller/src/extensions/expenses/domain/enums/expense-file-kind.enum.ts b/components/controller/src/extensions/expenses/domain/enums/expense-file-kind.enum.ts new file mode 100644 index 00000000000..3a521e848f7 --- /dev/null +++ b/components/controller/src/extensions/expenses/domain/enums/expense-file-kind.enum.ts @@ -0,0 +1,22 @@ +import { registerEnumType } from '@nestjs/graphql'; + +/** + * Назначение файла в MinIO-бакете `expenses:files`. + */ +export enum ExpenseFileKind { + PAYMENT_PROOF = 'PAYMENT_PROOF', + REPORT_FILE = 'REPORT_FILE', + RETURN_PROOF = 'RETURN_PROOF', + CLOSING_DOC = 'CLOSING_DOC', +} + +registerEnumType(ExpenseFileKind, { + name: 'ExpenseFileKind', + description: 'Тип первичного файла расхода.', + valuesMap: { + PAYMENT_PROOF: { description: 'Платёжка/квитанция кассира об исполненной оплате.' }, + REPORT_FILE: { description: 'Чек/первичка пайщика по отчёту об авансе под отчёт.' }, + RETURN_PROOF: { description: 'Подтверждение возврата неиспользованного аванса.' }, + CLOSING_DOC: { description: 'Закрывающий документ по оплате организации (акт/счёт-фактура/накладная).' }, + }, +}); diff --git a/components/controller/src/extensions/expenses/domain/enums/expense-item-status.enum.ts b/components/controller/src/extensions/expenses/domain/enums/expense-item-status.enum.ts new file mode 100644 index 00000000000..89e208ec3cb --- /dev/null +++ b/components/controller/src/extensions/expenses/domain/enums/expense-item-status.enum.ts @@ -0,0 +1,19 @@ +import { registerEnumType } from '@nestjs/graphql'; + +/** + * Доменный статус item-а в СЗ-расходе. Зеркалит `ExpenseDomain::ItemStatus` + * из `components/contracts/cpp/expense/expense.hpp`. + */ +export enum ExpenseItemStatus { + APPROVED = 'APPROVED', + PAID = 'PAID', + REPORTED = 'REPORTED', + RETURNED = 'RETURNED', + OVERSPENT = 'OVERSPENT', + UNDEFINED = 'UNDEFINED', +} + +registerEnumType(ExpenseItemStatus, { + name: 'ExpenseItemStatus', + description: 'Статус строки расхода.', +}); diff --git a/components/controller/src/extensions/expenses/domain/enums/expense-mechanics.enum.ts b/components/controller/src/extensions/expenses/domain/enums/expense-mechanics.enum.ts new file mode 100644 index 00000000000..a2efc7e3bbf --- /dev/null +++ b/components/controller/src/extensions/expenses/domain/enums/expense-mechanics.enum.ts @@ -0,0 +1,18 @@ +import { registerEnumType } from '@nestjs/graphql'; + +/** + * Способ оплаты item-а в СЗ-расходе. + * - ADVANCE — пайщик-получатель платит и приносит чек. + * - DIRECT — кассир/председатель платит организации напрямую. + * + * Зеркалит `ExpenseDomain::Mechanics` из `expense.hpp`. + */ +export enum ExpenseMechanics { + ADVANCE = 'ADVANCE', + DIRECT = 'DIRECT', +} + +registerEnumType(ExpenseMechanics, { + name: 'ExpenseMechanics', + description: 'Способ оплаты строки расхода.', +}); diff --git a/components/controller/src/extensions/expenses/domain/enums/expense-proposal-status.enum.ts b/components/controller/src/extensions/expenses/domain/enums/expense-proposal-status.enum.ts new file mode 100644 index 00000000000..a1083e85b7e --- /dev/null +++ b/components/controller/src/extensions/expenses/domain/enums/expense-proposal-status.enum.ts @@ -0,0 +1,20 @@ +import { registerEnumType } from '@nestjs/graphql'; + +/** + * Доменный статус СЗ-расхода. Зеркалит `ExpenseDomain::ProposalStatus` + * из `components/contracts/cpp/expense/expense.hpp`. + */ +export enum ExpenseProposalStatus { + CREATED = 'CREATED', + AUTHORIZED = 'AUTHORIZED', + PARTIALLY_PAID = 'PARTIALLY_PAID', + REPORT_SUBMITTED = 'REPORT_SUBMITTED', + CLOSED = 'CLOSED', + DECLINED = 'DECLINED', + UNDEFINED = 'UNDEFINED', +} + +registerEnumType(ExpenseProposalStatus, { + name: 'ExpenseProposalStatus', + description: 'Статус сметы расхода.', +}); diff --git a/components/controller/src/extensions/expenses/domain/enums/expense-recipient-type.enum.ts b/components/controller/src/extensions/expenses/domain/enums/expense-recipient-type.enum.ts new file mode 100644 index 00000000000..0b97070a790 --- /dev/null +++ b/components/controller/src/extensions/expenses/domain/enums/expense-recipient-type.enum.ts @@ -0,0 +1,20 @@ +import { registerEnumType } from '@nestjs/graphql'; + +/** + * Тип получателя платежа в item-е СЗ-расхода. + * - SELF — сам создатель СЗ. + * - MEMBER — другой пайщик кооператива. + * - ORG — внешняя организация / поставщик. + * + * Зеркалит `ExpenseDomain::RecipientType` из `expense.hpp`. + */ +export enum ExpenseRecipientType { + SELF = 'SELF', + MEMBER = 'MEMBER', + ORG = 'ORG', +} + +registerEnumType(ExpenseRecipientType, { + name: 'ExpenseRecipientType', + description: 'Тип получателя платежа.', +}); diff --git a/components/controller/src/extensions/expenses/domain/enums/expense-report-state.enum.ts b/components/controller/src/extensions/expenses/domain/enums/expense-report-state.enum.ts new file mode 100644 index 00000000000..118064ae25d --- /dev/null +++ b/components/controller/src/extensions/expenses/domain/enums/expense-report-state.enum.ts @@ -0,0 +1,24 @@ +/** + * Состояние отчёта по строке-авансу под отчёт — зеркалится в платёж выдачи + * аванса (`payment.blockchain_data.report_state`), чтобы реестр платежей + * показывал статус отчёта рядом со статусом платежа без чтения цепи. + * + * Не путать со статусом ПЛАТЕЖА (`PaymentStatusEnum`, движение денег) и статусом + * ПОЗИЦИИ в контракте (`ItemStatus`). Это отдельная UI-ось «надо ли пайщику + * отчитаться»: + * - AWAITING — аванс выдан, отчёт ещё не подан (дефолт; отдельно не пишется); + * - SETTLEMENT_PENDING — отчёт подан с недо-/перерасходом, заведена платёжка + * расчёта разницы, ждём её подтверждения кассой; + * - CLOSED — отчёт принят on-chain (reportexp); + * - NOT_REQUIRED — отчёт пайщика не требуется (прямая оплата организации). + * + * Значения дублируются строковыми константами на фронте + * (`desktop/src/shared/lib/expenses`) — поле живёт в JSON-скаляре blockchain_data, + * а не в типизированной GraphQL-схеме. + */ +export enum ExpenseReportState { + NOT_REQUIRED = 'NOT_REQUIRED', + AWAITING = 'AWAITING', + SETTLEMENT_PENDING = 'SETTLEMENT_PENDING', + CLOSED = 'CLOSED', +} diff --git a/components/controller/src/extensions/expenses/domain/expenses-chassis.config.ts b/components/controller/src/extensions/expenses/domain/expenses-chassis.config.ts new file mode 100644 index 00000000000..ba6127f90a2 --- /dev/null +++ b/components/controller/src/extensions/expenses/domain/expenses-chassis.config.ts @@ -0,0 +1,19 @@ +/** + * Параметры шасси расходов. + * + * `fundNameDative` — фонд, по которому совет согласует списание затрат + * (строка в служебной записке 2010: «списание затрат по …»). Хранится + * в дательном падеже, т.к. подставляется после предлога «по». Дефолт — + * фонд хозяйственной деятельности; другой фонд (развития кооперации, + * материальной помощи и т.п.) задаётся через env EXPENSES_FUND_NAME. + */ +export const EXPENSES_CHASSIS_CONFIG = { + fundNameDative: process.env.EXPENSES_FUND_NAME ?? 'Фонду хозяйственной деятельности', + /** + * Назначение платежа для выплат пайщику (SELF/MEMBER) — всегда одно и то же, + * подставляется автоматически: в платёжке кассира неизменно «аванс под отчёт», + * а на что он — видно из описания позиции. Для организации/ИП назначение + * вводится при создании расхода. + */ + advancePaymentPurpose: 'Аванс под отчёт', +}; diff --git a/components/controller/src/extensions/expenses/domain/interfaces/expense-file-database.interface.ts b/components/controller/src/extensions/expenses/domain/interfaces/expense-file-database.interface.ts new file mode 100644 index 00000000000..f6f773d1a93 --- /dev/null +++ b/components/controller/src/extensions/expenses/domain/interfaces/expense-file-database.interface.ts @@ -0,0 +1,24 @@ +import { ExpenseFileKind } from '../enums/expense-file-kind.enum'; + +/** + * Запись о первичном файле расхода — платёжке, чеке, доказательстве возврата. + * Сам бинарь лежит в MinIO-бакете `expenses:files`. В блокчейне файла нет — + * это чистая БД-сущность; для on-chain валидации используется `checksum_sha256`. + * + * Ключ MinIO: `{coopname}/expenses/{proposal_hash}/{item_hash|_proposal}/{kind}/{checksum}.{ext}`. + */ +export interface IExpenseFileDatabaseData { + id?: number; + coopname: string; + proposal_hash: string; + item_hash?: string | null; + kind: ExpenseFileKind; + checksum_sha256: string; + mime_type: string; + size_bytes: number; + storage_key: string; + /** Оригинальное имя загруженного файла — для отображения и поиска */ + original_filename?: string | null; + uploaded_by_username: string; + uploaded_at: Date; +} diff --git a/components/controller/src/extensions/expenses/domain/interfaces/expense-item-database.interface.ts b/components/controller/src/extensions/expenses/domain/interfaces/expense-item-database.interface.ts new file mode 100644 index 00000000000..514b463f11d --- /dev/null +++ b/components/controller/src/extensions/expenses/domain/interfaces/expense-item-database.interface.ts @@ -0,0 +1,13 @@ +import type { IBaseDatabaseData } from '~/shared/sync/interfaces/base-database.interface'; +import { ExpenseItemStatus } from '../enums/expense-item-status.enum'; + +/** + * Зеркало строки `items[]` СЗ-расхода. Хранится отдельной таблицей + * (`expense_items`) ради per-item фильтров и присоединения файлов. + */ +export interface IExpenseItemDatabaseData extends IBaseDatabaseData { + id?: number; + proposal_hash: string; + item_hash: string; + status: ExpenseItemStatus; +} diff --git a/components/controller/src/extensions/expenses/domain/interfaces/expense-proposal-blockchain.interface.ts b/components/controller/src/extensions/expenses/domain/interfaces/expense-proposal-blockchain.interface.ts new file mode 100644 index 00000000000..d029fcdc954 --- /dev/null +++ b/components/controller/src/extensions/expenses/domain/interfaces/expense-proposal-blockchain.interface.ts @@ -0,0 +1,47 @@ +import type { ISignedDocumentDomainInterface } from '~/domain/document/interfaces/signed-document-domain.interface'; + +/** + * Колбэк, который контракт `expense` опубликует в `dispatcher` при закрытии СЗ. + * `contract.value === 0` означает «колбэка нет». + */ +export interface IExpenseProposalCallbackHandler { + contract: string; + action: string; + data: string; +} + +/** + * Item — одна строка СЗ-расхода в `proposals.items` контракта `expense`. + * Зеркалит `ExpenseDomain::item`. + */ +export interface IExpenseItemBlockchainData { + item_hash: string; + mechanics: number; + recipient_type: number; + recipient: string; + description: string; + planned_amount: string; + actual_amount: string; + status: number; +} + +/** + * СЗ-расход — строка `proposals` контракта `expense` (scope = coopname). + * Зеркалит `ExpenseDomain::proposal`. + */ +export interface IExpenseProposalBlockchainData { + id: string; + proposal_hash: string; + coopname: string; + username: string; + source_wallet: string; + status: number; + items: IExpenseItemBlockchainData[]; + total_planned: string; + total_actual: string; + callback?: IExpenseProposalCallbackHandler; + statement_doc?: ISignedDocumentDomainInterface; + decision_doc?: ISignedDocumentDomainInterface; + created_at: string; + updated_at: string; +} diff --git a/components/controller/src/extensions/expenses/domain/interfaces/expense-proposal-database.interface.ts b/components/controller/src/extensions/expenses/domain/interfaces/expense-proposal-database.interface.ts new file mode 100644 index 00000000000..41522ba265d --- /dev/null +++ b/components/controller/src/extensions/expenses/domain/interfaces/expense-proposal-database.interface.ts @@ -0,0 +1,13 @@ +import type { IBaseDatabaseData } from '~/shared/sync/interfaces/base-database.interface'; +import { ExpenseProposalStatus } from '../enums/expense-proposal-status.enum'; + +/** + * Поля, которые живут в собственной таблице зеркала СЗ-расхода + * (`expense_proposals`). Идентификация и нормализованный статус. + */ +export interface IExpenseProposalDatabaseData extends IBaseDatabaseData { + id?: number; + proposal_hash: string; + coopname: string; + status: ExpenseProposalStatus; +} diff --git a/components/controller/src/extensions/expenses/domain/interfaces/expenses-blockchain.port.ts b/components/controller/src/extensions/expenses/domain/interfaces/expenses-blockchain.port.ts new file mode 100644 index 00000000000..454d07cce85 --- /dev/null +++ b/components/controller/src/extensions/expenses/domain/interfaces/expenses-blockchain.port.ts @@ -0,0 +1,29 @@ +import { ExpenseContract } from 'cooptypes' +import type { TransactResult } from '@wharfkit/session' + +/** + * Блокчейн-порт контракта `expense`. Hexagonal: domain видит интерфейс, + * implementation в `infrastructure/blockchain/adapters`. + * + * 6 actions, доступных backend'у. Авторизация (`authexp`) и отклонение + * (`declexp`) СЗ исполняются контрактом soviet как callbacks решения совета — + * backend их не вызывает. + * + * - `createexp` — создание + подача СЗ (signact1 statement_doc, type=2010); + * ставит вопрос в повестку совета + * - `payexp` — оплата item (ADVANCE/DIRECT) + * - `reportexp` — закрытие item чеком (ADVANCE) + * - `returnexp` — возврат неиспользованного аванса + * - `overspendexp` — доплата при перерасходе (ADVANCE) + * - `closeexp` — финализация СЗ-отчёта (REPORT_SUBMITTED → CLOSED) + */ +export interface ExpensesBlockchainPort { + createExp(data: ExpenseContract.Actions.CreateExp.ICreateExp): Promise + payExp(data: ExpenseContract.Actions.PayExp.IPayExp): Promise + reportExp(data: ExpenseContract.Actions.ReportExp.IReportExp): Promise + returnExp(data: ExpenseContract.Actions.ReturnExp.IReturnExp): Promise + overspendExp(data: ExpenseContract.Actions.OverspendExp.IOverspendExp): Promise + closeExp(data: ExpenseContract.Actions.CloseExp.ICloseExp): Promise +} + +export const EXPENSES_BLOCKCHAIN_PORT = Symbol('ExpensesBlockchainPort') diff --git a/components/controller/src/extensions/expenses/domain/repositories/expense-file.repository.ts b/components/controller/src/extensions/expenses/domain/repositories/expense-file.repository.ts new file mode 100644 index 00000000000..8551e7d6a0f --- /dev/null +++ b/components/controller/src/extensions/expenses/domain/repositories/expense-file.repository.ts @@ -0,0 +1,16 @@ +import type { IExpenseFileDatabaseData } from '../interfaces/expense-file-database.interface'; + +/** + * Реестр первичных файлов расхода. Файл — чистая БД-сущность (MinIO + метаданные); + * блокчейна за ним нет, поэтому интерфейс не наследует `IBlockchainSyncRepository`. + */ +export interface ExpenseFileRepository { + create(data: IExpenseFileDatabaseData): Promise; + findById(id: number): Promise; + findByChecksum(coopname: string, checksum: string): Promise; + findByProposal(coopname: string, proposalHash: string): Promise; + findByItem(coopname: string, proposalHash: string, itemHash: string): Promise; + delete(id: number): Promise; +} + +export const EXPENSE_FILE_REPOSITORY = Symbol('ExpenseFileRepository'); diff --git a/components/controller/src/extensions/expenses/domain/repositories/expense-proposal.repository.ts b/components/controller/src/extensions/expenses/domain/repositories/expense-proposal.repository.ts new file mode 100644 index 00000000000..71ac0b59c41 --- /dev/null +++ b/components/controller/src/extensions/expenses/domain/repositories/expense-proposal.repository.ts @@ -0,0 +1,29 @@ +import type { IBlockchainSyncRepository } from '~/shared/interfaces/blockchain-sync.interface'; +import type { PaginationInputDTO } from '~/application/common/dto/pagination.dto'; +import type { ExpenseProposalDomainEntity } from '../entities/expense-proposal.entity'; + +/** + * Репозиторий зеркала СЗ-расходов. Чтение строго из Postgres + * (ADR-011 — read-path); chain RPC только для reconciliation / forensic. + * + * `*Paginated` методы — каноничный путь для resolver'ов (см. controller/CLAUDE.md + * «Пагинация — единый паттерн»). Неснабжённые пагинацией методы оставлены для + * внутреннего DocumentAggregation / e2e и постепенно вытесняются. + */ +export interface ExpenseProposalRepository extends IBlockchainSyncRepository { + findByProposalHash(proposalHash: string): Promise; + findByCoopname(coopname: string): Promise; + findByUsername(coopname: string, username: string): Promise; + + findByCoopnamePaginated( + coopname: string, + options?: PaginationInputDTO + ): Promise<{ items: ExpenseProposalDomainEntity[]; totalCount: number }>; + findByUsernamePaginated( + coopname: string, + username: string, + options?: PaginationInputDTO + ): Promise<{ items: ExpenseProposalDomainEntity[]; totalCount: number }>; +} + +export const EXPENSE_PROPOSAL_REPOSITORY = Symbol('ExpenseProposalRepository'); diff --git a/components/controller/src/extensions/expenses/domain/utils/format-requisites.util.ts b/components/controller/src/extensions/expenses/domain/utils/format-requisites.util.ts new file mode 100644 index 00000000000..0c9fc6b8f27 --- /dev/null +++ b/components/controller/src/extensions/expenses/domain/utils/format-requisites.util.ts @@ -0,0 +1,24 @@ +import type { PaymentMethodDomainEntity } from '~/domain/payment-method/entities/method-domain.entity'; +import type { + SBPDataDomainInterface, + BankTransferDataDomainInterface, +} from '~/domain/payment-method/interfaces/payment-methods-domain.interface'; + +/** + * Полная строка реквизитов платёжного метода — для документов (СЗ в совет, + * поручение бухгалтеру). Сокращённое представление для UI делает фронт. + */ +export function formatPaymentMethodRequisites(method: PaymentMethodDomainEntity): string { + if (method.method_type === 'sbp') { + const data = method.data as SBPDataDomainInterface; + return `СБП, телефон ${data.phone}`; + } + + const data = method.data as BankTransferDataDomainInterface; + const parts = [`Банковский перевод: счёт ${data.account_number}`]; + if (data.bank_name) parts.push(data.bank_name); + if (data.details?.bik) parts.push(`БИК ${data.details.bik}`); + if (data.details?.corr) parts.push(`к/с ${data.details.corr}`); + if (data.card_number) parts.push(`карта ${data.card_number}`); + return parts.join(', '); +} diff --git a/components/controller/src/extensions/expenses/expenses-extension.module.ts b/components/controller/src/extensions/expenses/expenses-extension.module.ts new file mode 100644 index 00000000000..9aeb2209c7f --- /dev/null +++ b/components/controller/src/extensions/expenses/expenses-extension.module.ts @@ -0,0 +1,95 @@ +import { Module } from '@nestjs/common'; +import { FileStorageInfrastructureModule } from '~/infrastructure/file-storage'; +import { DocumentDomainModule } from '~/domain/document/document.module'; +import { BlockchainModule } from '~/infrastructure/blockchain/blockchain.module'; +import { VaultDomainModule } from '~/domain/vault/vault-domain.module'; +import { GeneratorInfrastructureModule } from '~/infrastructure/generator/generator.module'; +import { ExpensesDatabaseModule } from './infrastructure/database/expenses-database.module'; +import { ExpenseContractInfoService } from './infrastructure/services/expense-contract-info.service'; +import { ExpenseProposalDeltaMapper } from './infrastructure/blockchain/mappers/expense-proposal-delta.mapper'; +import { ExpensesBlockchainAdapter } from './infrastructure/blockchain/adapters/expenses-blockchain.adapter'; +import { ExpenseProposalTypeormRepository } from './infrastructure/repositories/expense-proposal.typeorm-repository'; +import { ExpenseFileTypeormRepository } from './infrastructure/repositories/expense-file.typeorm-repository'; +import { EXPENSE_PROPOSAL_REPOSITORY } from './domain/repositories/expense-proposal.repository'; +import { EXPENSE_FILE_REPOSITORY } from './domain/repositories/expense-file.repository'; +import { EXPENSES_BLOCKCHAIN_PORT } from './domain/interfaces/expenses-blockchain.port'; +import { ExpenseProposalSyncService } from './application/syncers/expense-proposal-sync.service'; +import { ExpensesManagementService } from './application/services/expenses-management.service'; +import { ExpensesMutationsService } from './application/services/expenses-mutations.service'; +import { ExpenseRequisiteSnapshotsService } from './application/services/expense-requisite-snapshots.service'; +import { ExpensesCapitalTriggerService } from './application/services/expenses-capital-trigger.service'; +import { ExpensePaymentsListener } from './application/services/expense-payments.listener'; +import { ExpenseAdvanceReminderService } from './application/services/expense-advance-reminder.service'; +import { ExpenseFilesService } from './application/services/expense-files.service'; +import { ExpenseProposalResolver } from './application/resolvers/expense-proposal.resolver'; +import { ExpenseMutationsResolver } from './application/resolvers/expense-mutations.resolver'; +import { ExpenseFilesResolver } from './application/resolvers/expense-files.resolver'; +import { ExpensesInterExpenseChassisAdapter } from './infrastructure/inter/expenses-inter-expense-chassis.adapter'; + +/** + * Шасси расходов цифрового кооператива (MVP — Благорост). + * + * Backend-сторона контракта `expense`: + * - TypeORM-зеркало on-chain proposals + items; + * - MinIO-bucket `expenses:files` (платёжки/чеки/возвраты); + * - GraphQL Query/Mutation для UI; + * - sync через parser2 (после ABI regen). + * + * Расширение НЕ предоставляет собственного «стола» — расход живёт в Столе + * Благороста и Председателя. Поэтому в `AppRegistry` запись не заводится, + * модуль подключается напрямую в `ExtensionsModule.register()`. + * + * Подробности и план реализации — см. `README.md` рядом. + */ +@Module({ + imports: [ + ExpensesDatabaseModule, + DocumentDomainModule, + BlockchainModule, + VaultDomainModule, + GeneratorInfrastructureModule, + FileStorageInfrastructureModule.forFeature([ExpenseFilesService]), + ], + providers: [ + ExpenseContractInfoService, + ExpenseProposalDeltaMapper, + ExpenseProposalTypeormRepository, + ExpenseFileTypeormRepository, + { + provide: EXPENSE_PROPOSAL_REPOSITORY, + useClass: ExpenseProposalTypeormRepository, + }, + { + provide: EXPENSE_FILE_REPOSITORY, + useClass: ExpenseFileTypeormRepository, + }, + { + provide: EXPENSES_BLOCKCHAIN_PORT, + useClass: ExpensesBlockchainAdapter, + }, + ExpenseProposalSyncService, + ExpensesManagementService, + ExpenseRequisiteSnapshotsService, + ExpensesMutationsService, + ExpensesCapitalTriggerService, + ExpensePaymentsListener, + ExpenseAdvanceReminderService, + ExpenseFilesService, + ExpenseProposalResolver, + ExpenseMutationsResolver, + ExpenseFilesResolver, + ExpensesInterExpenseChassisAdapter, + ], + exports: [ + ExpenseContractInfoService, + ExpenseProposalDeltaMapper, + EXPENSE_PROPOSAL_REPOSITORY, + EXPENSE_FILE_REPOSITORY, + ExpenseProposalSyncService, + ExpensesManagementService, + ExpensesMutationsService, + ExpenseFilesService, + ExpensesInterExpenseChassisAdapter, + ], +}) +export class ExpensesExtensionModule {} diff --git a/components/controller/src/extensions/expenses/infrastructure/blockchain/adapters/expenses-blockchain.adapter.spec.ts b/components/controller/src/extensions/expenses/infrastructure/blockchain/adapters/expenses-blockchain.adapter.spec.ts new file mode 100644 index 00000000000..5ab8b4ce9ab --- /dev/null +++ b/components/controller/src/extensions/expenses/infrastructure/blockchain/adapters/expenses-blockchain.adapter.spec.ts @@ -0,0 +1,121 @@ +import { ExpenseContract } from 'cooptypes' +import { ExpensesBlockchainAdapter } from './expenses-blockchain.adapter' +import type { BlockchainService } from '~/infrastructure/blockchain/blockchain.service' +import type { VaultDomainService } from '~/domain/vault/services/vault-domain.service' + +/** + * Контракт-тест адаптера: каждое действие подписывается ключом кооператива + * на `account = expense`, `permission = active`, `actor = coopname`. + * + * Гарантия — не вырастет дрейф между cooptypes/expense actionName'ами и тем, + * что реально уходит в `BlockchainService.transact`. + */ +describe('ExpensesBlockchainAdapter', () => { + let blockchain: jest.Mocked> + let vault: jest.Mocked> + let adapter: ExpensesBlockchainAdapter + + const fakeResult = { response: { transaction_id: 'tx_zzz' } } as never + + beforeEach(() => { + blockchain = { + initialize: jest.fn(), + transact: jest.fn().mockResolvedValue(fakeResult), + } + vault = { + getWif: jest.fn().mockResolvedValue('5KQwrPbwdL6Ph...'), + } + adapter = new ExpensesBlockchainAdapter( + blockchain as unknown as BlockchainService, + vault as unknown as VaultDomainService + ) + }) + + const coopname = 'voskhod' + + function assertTransactShape(name: string): void { + expect(vault.getWif).toHaveBeenCalledWith(coopname) + expect(blockchain.initialize).toHaveBeenCalledWith(coopname, '5KQwrPbwdL6Ph...') + expect(blockchain.transact).toHaveBeenCalledWith( + expect.objectContaining({ + account: ExpenseContract.contractName.production, + name, + authorization: [{ actor: coopname, permission: 'active' }], + }) + ) + } + + const fakeDoc2 = { + version: '1', + hash: '0xh', + doc_hash: '0xd', + meta_hash: '0xm', + meta: '{}', + signatures: [], + } as any + + it('createExp → transact(expense, createexp, [{coopname, active}])', async () => { + await adapter.createExp({ + coopname, + username: 'ivanov', + proposal_hash: '0xabc', + source_wallet: 'w.cap.blago', + items: [], + callback: { contract: '', action: '', data: '' }, + statement: fakeDoc2, + }) + assertTransactShape(ExpenseContract.Actions.CreateExp.actionName) + }) + + // authexp/declexp исполняет контракт soviet как callbacks решения совета — + // в backend-адаптере их нет по дизайну (см. expenses-blockchain.port.ts). + + it('payExp → transact(expense, payexp, [{coopname, active}])', async () => { + await adapter.payExp({ + coopname, + proposal_hash: '0xabc', + item_hash: '0xdef', + actual_amount: '100.0000 RUB', + }) + assertTransactShape(ExpenseContract.Actions.PayExp.actionName) + }) + + it('reportExp → transact(expense, reportexp, ...)', async () => { + await adapter.reportExp({ coopname, proposal_hash: '0xabc', item_hash: '0xdef' }) + assertTransactShape(ExpenseContract.Actions.ReportExp.actionName) + }) + + it('returnExp → transact(expense, returnexp, ...)', async () => { + await adapter.returnExp({ + coopname, + proposal_hash: '0xabc', + item_hash: '0xdef', + return_amount: '50.0000 RUB', + }) + assertTransactShape(ExpenseContract.Actions.ReturnExp.actionName) + }) + + it('overspendExp → transact(expense, overspendexp, ...)', async () => { + await adapter.overspendExp({ + coopname, + proposal_hash: '0xabc', + item_hash: '0xdef', + overspend_amount: '200.0000 RUB', + }) + assertTransactShape(ExpenseContract.Actions.OverspendExp.actionName) + }) + + it('closeExp → transact(expense, closeexp, ...)', async () => { + await adapter.closeExp({ coopname, proposal_hash: '0xabc' }) + assertTransactShape(ExpenseContract.Actions.CloseExp.actionName) + }) + + it('бросает 502, если vault не отдал ключ', async () => { + vault.getWif.mockResolvedValueOnce(null) + await expect(adapter.closeExp({ coopname, proposal_hash: '0xabc' })).rejects.toThrow( + 'Не найден приватный ключ' + ) + expect(blockchain.initialize).not.toHaveBeenCalled() + expect(blockchain.transact).not.toHaveBeenCalled() + }) +}) diff --git a/components/controller/src/extensions/expenses/infrastructure/blockchain/adapters/expenses-blockchain.adapter.ts b/components/controller/src/extensions/expenses/infrastructure/blockchain/adapters/expenses-blockchain.adapter.ts new file mode 100644 index 00000000000..06f7ee31253 --- /dev/null +++ b/components/controller/src/extensions/expenses/infrastructure/blockchain/adapters/expenses-blockchain.adapter.ts @@ -0,0 +1,93 @@ +import { Inject, Injectable } from '@nestjs/common' +import { ExpenseContract } from 'cooptypes' +import type { TransactResult } from '@wharfkit/session' +import httpStatus from 'http-status' +import { BlockchainService } from '~/infrastructure/blockchain/blockchain.service' +import { VAULT_DOMAIN_SERVICE, VaultDomainService } from '~/domain/vault/services/vault-domain.service' +import { HttpApiError } from '~/utils/httpApiError' +import { ExpensesBlockchainPort } from '../../../domain/interfaces/expenses-blockchain.port' + +/** + * Адаптер блокчейн-порта `expense`. Канон взят с `CapitalBlockchainAdapter`: + * подпись ключом кооператива (`active` permission), `account = contractName.production`. + * + * 6 backend-actions: createexp / payexp / reportexp / returnexp / overspendexp / + * closeexp (authexp/declexp вызывает контракт soviet по решению совета). + * Документы document2 (statement) идут как часть `data` action'а — wharfkit + * сериализует поля по C++ struct document2. + */ +@Injectable() +export class ExpensesBlockchainAdapter implements ExpensesBlockchainPort { + constructor( + private readonly blockchainService: BlockchainService, + @Inject(VAULT_DOMAIN_SERVICE) private readonly vaultDomainService: VaultDomainService + ) {} + + private async initWithCoopKey(coopname: string): Promise { + const wif = await this.vaultDomainService.getWif(coopname) + if (!wif) { + throw new HttpApiError(httpStatus.BAD_GATEWAY, 'Не найден приватный ключ для совершения операции') + } + this.blockchainService.initialize(coopname, wif) + } + + async createExp(data: ExpenseContract.Actions.CreateExp.ICreateExp): Promise { + await this.initWithCoopKey(data.coopname) + return this.blockchainService.transact({ + account: ExpenseContract.contractName.production, + name: ExpenseContract.Actions.CreateExp.actionName, + authorization: [{ actor: data.coopname, permission: 'active' }], + data, + }) + } + + async payExp(data: ExpenseContract.Actions.PayExp.IPayExp): Promise { + await this.initWithCoopKey(data.coopname) + return this.blockchainService.transact({ + account: ExpenseContract.contractName.production, + name: ExpenseContract.Actions.PayExp.actionName, + authorization: [{ actor: data.coopname, permission: 'active' }], + data, + }) + } + + async reportExp(data: ExpenseContract.Actions.ReportExp.IReportExp): Promise { + await this.initWithCoopKey(data.coopname) + return this.blockchainService.transact({ + account: ExpenseContract.contractName.production, + name: ExpenseContract.Actions.ReportExp.actionName, + authorization: [{ actor: data.coopname, permission: 'active' }], + data, + }) + } + + async returnExp(data: ExpenseContract.Actions.ReturnExp.IReturnExp): Promise { + await this.initWithCoopKey(data.coopname) + return this.blockchainService.transact({ + account: ExpenseContract.contractName.production, + name: ExpenseContract.Actions.ReturnExp.actionName, + authorization: [{ actor: data.coopname, permission: 'active' }], + data, + }) + } + + async overspendExp(data: ExpenseContract.Actions.OverspendExp.IOverspendExp): Promise { + await this.initWithCoopKey(data.coopname) + return this.blockchainService.transact({ + account: ExpenseContract.contractName.production, + name: ExpenseContract.Actions.OverspendExp.actionName, + authorization: [{ actor: data.coopname, permission: 'active' }], + data, + }) + } + + async closeExp(data: ExpenseContract.Actions.CloseExp.ICloseExp): Promise { + await this.initWithCoopKey(data.coopname) + return this.blockchainService.transact({ + account: ExpenseContract.contractName.production, + name: ExpenseContract.Actions.CloseExp.actionName, + authorization: [{ actor: data.coopname, permission: 'active' }], + data, + }) + } +} diff --git a/components/controller/src/extensions/expenses/infrastructure/blockchain/mappers/expense-proposal-delta.mapper.ts b/components/controller/src/extensions/expenses/infrastructure/blockchain/mappers/expense-proposal-delta.mapper.ts new file mode 100644 index 00000000000..f90de80eb4c --- /dev/null +++ b/components/controller/src/extensions/expenses/infrastructure/blockchain/mappers/expense-proposal-delta.mapper.ts @@ -0,0 +1,90 @@ +import { Injectable } from '@nestjs/common'; +import type { IDelta } from '~/types/common'; +import { ExpenseProposalDomainEntity } from '../../../domain/entities/expense-proposal.entity'; +import type { IExpenseProposalBlockchainData } from '../../../domain/interfaces/expense-proposal-blockchain.interface'; +import { WinstonLoggerService } from '~/application/logger/logger-app.service'; +import { ExpenseContractInfoService } from '../../services/expense-contract-info.service'; +import { AbstractBlockchainDeltaMapper } from '~/shared/abstract-blockchain-delta.mapper'; +import { DomainToBlockchainUtils } from '~/shared/utils/domain-to-blockchain.utils'; + +/** + * Маппер дельт таблицы `expense::proposals` в блокчейн-данные расхода. + * + * NB: типизация `delta.value` сейчас идёт через локальный интерфейс — после + * регистрации контракта `expense` в `cooptypes` сменить на + * `ExpenseContract.Tables.Proposals.IProposal` без правки логики. + */ +@Injectable() +export class ExpenseProposalDeltaMapper extends AbstractBlockchainDeltaMapper< + IExpenseProposalBlockchainData, + ExpenseProposalDomainEntity +> { + constructor( + private readonly logger: WinstonLoggerService, + private readonly contractInfo: ExpenseContractInfoService + ) { + super(); + this.logger.setContext(ExpenseProposalDeltaMapper.name); + } + + mapDeltaToBlockchainData(delta: IDelta): IExpenseProposalBlockchainData | null { + try { + const value = delta.value as IExpenseProposalBlockchainData; + if (!value) { + this.logger.warn(`Delta has no value: table=${delta.table}, key=${delta.primary_key}`); + return null; + } + + const statement_doc = value.statement_doc + ? DomainToBlockchainUtils.convertChainDocumentToDomainFormat(value.statement_doc as never) + : undefined; + const decision_doc = value.decision_doc + ? DomainToBlockchainUtils.convertChainDocumentToDomainFormat(value.decision_doc as never) + : undefined; + + // callback.data on-chain — vector; парсер десериализует его в + // Uint8Array, который в JSON становится {} / {"0":..}. GraphQL-поле — + // String, поэтому нормализуем в hex-строку ещё на записи в зеркало. + const callback = value.callback + ? { ...value.callback, data: bytesToHex(value.callback.data) } + : undefined; + + return { + ...value, + proposal_hash: String(value.proposal_hash).toLowerCase(), + callback, + statement_doc, + decision_doc, + }; + } catch (error: any) { + this.logger.error(`Error mapping delta to blockchain data: ${error.message}`, error.stack); + return null; + } + } + + extractSyncValue(delta: IDelta): string { + if (!delta.value || !delta.value[this.extractSyncKey()]) { + throw new Error(`Delta has no value: table=${delta.table}, key=${this.extractSyncKey()}`); + } + return String(delta.value[this.extractSyncKey()]).toLowerCase(); + } + + extractSyncKey(): string { + return ExpenseProposalDomainEntity.getSyncKey(); + } + + getSupportedContractNames(): string[] { + return this.contractInfo.getSupportedContractNames(); + } + + getSupportedTableNames(): string[] { + return this.contractInfo.getTablePatterns('proposals'); + } +} + +function bytesToHex(data: unknown): string { + if (typeof data === 'string') return data; + if (data == null) return ''; + const bytes = Array.isArray(data) ? data : Object.values(data as Record); + return bytes.map((b) => Number(b).toString(16).padStart(2, '0')).join(''); +} diff --git a/components/controller/src/extensions/expenses/infrastructure/database/expenses-database.module.ts b/components/controller/src/extensions/expenses/infrastructure/database/expenses-database.module.ts new file mode 100644 index 00000000000..ad8d8f22520 --- /dev/null +++ b/components/controller/src/extensions/expenses/infrastructure/database/expenses-database.module.ts @@ -0,0 +1,23 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { ExpenseProposalTypeormEntity } from '../entities/expense-proposal.typeorm-entity'; +import { ExpenseFileTypeormEntity } from '../entities/expense-file.typeorm-entity'; +import { ExpenseRequisiteSnapshotTypeormEntity } from '../entities/expense-requisite-snapshot.typeorm-entity'; +import { EntityVersionTypeormEntity } from '~/shared/sync/entities/entity-version.typeorm-entity'; + +/** + * TypeORM-модуль расширения `expense` — зеркало SP-расходов + реестр файлов + * + снимки реквизитов получателей. + */ +@Module({ + imports: [ + TypeOrmModule.forFeature([ + ExpenseProposalTypeormEntity, + ExpenseFileTypeormEntity, + ExpenseRequisiteSnapshotTypeormEntity, + EntityVersionTypeormEntity, + ]), + ], + exports: [TypeOrmModule], +}) +export class ExpensesDatabaseModule {} diff --git a/components/controller/src/extensions/expenses/infrastructure/entities/expense-file.typeorm-entity.ts b/components/controller/src/extensions/expenses/infrastructure/entities/expense-file.typeorm-entity.ts new file mode 100644 index 00000000000..f2016755ab9 --- /dev/null +++ b/components/controller/src/extensions/expenses/infrastructure/entities/expense-file.typeorm-entity.ts @@ -0,0 +1,50 @@ +import { Entity, Column, Index, PrimaryGeneratedColumn, CreateDateColumn } from 'typeorm'; +import { ExpenseFileKind } from '../../domain/enums/expense-file-kind.enum'; + +export const EntityName = 'expense_files'; + +@Entity(EntityName) +@Index(`uq_${EntityName}_checksum`, ['coopname', 'checksum_sha256'], { unique: true }) +@Index(`idx_${EntityName}_proposal`, ['coopname', 'proposal_hash']) +@Index(`idx_${EntityName}_item`, ['coopname', 'proposal_hash', 'item_hash']) +export class ExpenseFileTypeormEntity { + static getTableName(): string { + return EntityName; + } + + @PrimaryGeneratedColumn() + id!: number; + + @Column({ type: 'varchar' }) + coopname!: string; + + @Column({ type: 'varchar' }) + proposal_hash!: string; + + @Column({ type: 'varchar', nullable: true }) + item_hash!: string | null; + + @Column({ type: 'enum', enum: ExpenseFileKind }) + kind!: ExpenseFileKind; + + @Column({ type: 'varchar', length: 64 }) + checksum_sha256!: string; + + @Column({ type: 'varchar', length: 120 }) + mime_type!: string; + + @Column({ type: 'integer' }) + size_bytes!: number; + + @Column({ type: 'varchar', length: 512 }) + storage_key!: string; + + @Column({ type: 'varchar', length: 255, nullable: true, comment: 'Оригинальное имя загруженного файла — для отображения и поиска' }) + original_filename!: string | null; + + @Column({ type: 'varchar' }) + uploaded_by_username!: string; + + @CreateDateColumn({ type: 'timestamp' }) + uploaded_at!: Date; +} diff --git a/components/controller/src/extensions/expenses/infrastructure/entities/expense-proposal.typeorm-entity.ts b/components/controller/src/extensions/expenses/infrastructure/entities/expense-proposal.typeorm-entity.ts new file mode 100644 index 00000000000..73df8f20278 --- /dev/null +++ b/components/controller/src/extensions/expenses/infrastructure/entities/expense-proposal.typeorm-entity.ts @@ -0,0 +1,71 @@ +import { Entity, Column, Index } from 'typeorm'; +import { BaseTypeormEntity } from '~/shared/sync/entities/base-typeorm.entity'; +import type { ISignedDocumentDomainInterface } from '~/domain/document/interfaces/signed-document-domain.interface'; +import type { + IExpenseItemBlockchainData, + IExpenseProposalCallbackHandler, +} from '../../domain/interfaces/expense-proposal-blockchain.interface'; +import { ExpenseProposalStatus } from '../../domain/enums/expense-proposal-status.enum'; + +export const EntityName = 'expense_proposals'; + +@Entity(EntityName) +@Index(`idx_${EntityName}_blockchain_id`, ['id']) +@Index(`idx_${EntityName}_proposal_hash`, ['proposal_hash']) +@Index(`idx_${EntityName}_coopname_username`, ['coopname', 'username']) +@Index(`idx_${EntityName}_status`, ['status']) +@Index(`idx_${EntityName}_created_at`, ['_created_at']) +export class ExpenseProposalTypeormEntity extends BaseTypeormEntity { + static getTableName(): string { + return EntityName; + } + + @Column({ type: 'integer', nullable: true, unique: true }) + id!: number; + + @Column({ type: 'varchar' }) + proposal_hash!: string; + + @Column({ type: 'varchar' }) + coopname!: string; + + @Column({ type: 'varchar' }) + username!: string; + + @Column({ type: 'varchar' }) + source_wallet!: string; + + @Column({ type: 'integer', nullable: true }) + blockchain_status!: number; + + @Column({ type: 'jsonb', nullable: true }) + items!: IExpenseItemBlockchainData[]; + + @Column({ type: 'varchar' }) + total_planned!: string; + + @Column({ type: 'varchar' }) + total_actual!: string; + + @Column({ type: 'jsonb', nullable: true }) + callback!: IExpenseProposalCallbackHandler | null; + + @Column({ type: 'jsonb', nullable: true }) + statement_doc!: ISignedDocumentDomainInterface | null; + + @Column({ type: 'jsonb', nullable: true }) + decision_doc!: ISignedDocumentDomainInterface | null; + + @Column({ type: 'timestamp', nullable: true }) + created_at!: Date; + + @Column({ type: 'timestamp', nullable: true }) + updated_at!: Date; + + @Column({ + type: 'enum', + enum: ExpenseProposalStatus, + default: ExpenseProposalStatus.UNDEFINED, + }) + status!: ExpenseProposalStatus; +} diff --git a/components/controller/src/extensions/expenses/infrastructure/entities/expense-requisite-snapshot.typeorm-entity.ts b/components/controller/src/extensions/expenses/infrastructure/entities/expense-requisite-snapshot.typeorm-entity.ts new file mode 100644 index 00000000000..52b3c191319 --- /dev/null +++ b/components/controller/src/extensions/expenses/infrastructure/entities/expense-requisite-snapshot.typeorm-entity.ts @@ -0,0 +1,51 @@ +import { Entity, Column, Index, PrimaryGeneratedColumn, CreateDateColumn } from 'typeorm'; + +export const EntityName = 'expense_requisite_snapshots'; + +/** + * Снимок реквизитов получателя по строке расхода — фиксируется в момент + * создания СЗ (как payment_details в gateway-платежах): последующее изменение + * или удаление платёжного метода пайщиком не меняет то, куда платить по уже + * поданной смете. Источник реквизитов для поручения бухгалтеру (payexp). + */ +@Entity(EntityName) +@Index(`uq_${EntityName}_item`, ['coopname', 'proposal_hash', 'item_hash'], { unique: true }) +@Index(`idx_${EntityName}_proposal`, ['coopname', 'proposal_hash']) +export class ExpenseRequisiteSnapshotTypeormEntity { + static getTableName(): string { + return EntityName; + } + + @PrimaryGeneratedColumn() + id!: number; + + @Column({ type: 'varchar' }) + coopname!: string; + + @Column({ type: 'varchar' }) + proposal_hash!: string; + + @Column({ type: 'varchar' }) + item_hash!: string; + + @Column({ type: 'varchar', comment: 'Получатель платежа (username пайщика или имя организации)' }) + recipient!: string; + + @Column({ type: 'varchar', nullable: true, comment: 'Идентификатор платёжного метода получателя (для пайщиков)' }) + method_id!: string | null; + + @Column({ type: 'varchar', nullable: true, comment: 'Тип метода (sbp / bank_transfer)' }) + method_type!: string | null; + + @Column({ type: 'jsonb', nullable: true, comment: 'Снимок данных платёжного метода на момент создания СЗ' }) + data!: Record | null; + + @Column({ type: 'text', comment: 'Реквизиты строкой — как в документе СЗ' }) + requisites!: string; + + @Column({ type: 'text', nullable: true, comment: 'Назначение платежа (оплата по счёту) — для поручения кассиру' }) + payment_purpose!: string | null; + + @CreateDateColumn() + created_at!: Date; +} diff --git a/components/controller/src/extensions/expenses/infrastructure/inter/expenses-inter-expense-chassis.adapter.ts b/components/controller/src/extensions/expenses/infrastructure/inter/expenses-inter-expense-chassis.adapter.ts new file mode 100644 index 00000000000..7f8ff9ba367 --- /dev/null +++ b/components/controller/src/extensions/expenses/infrastructure/inter/expenses-inter-expense-chassis.adapter.ts @@ -0,0 +1,182 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { In, Repository } from 'typeorm'; +import type { + InterExpenseChassisPort, + InterExpenseItem, + InterExpensePagination, + InterExpensePaginatedResult, + InterExpenseProposalRead, + InterExpenseProposalStatus, + InterExpenseRequisiteItemInput, +} from '@coopenomics/inter'; +import { PAYMENT_REPOSITORY, PaymentRepository } from '~/domain/gateway/repositories/payment.repository'; +import { ExpenseProposalTypeormEntity } from '../entities/expense-proposal.typeorm-entity'; +import { ExpenseProposalStatus } from '../../domain/enums/expense-proposal-status.enum'; +import { ExpenseReportState } from '../../domain/enums/expense-report-state.enum'; +import { ExpenseRequisiteSnapshotsService } from '../../application/services/expense-requisite-snapshots.service'; +import { + EXPENSES_BLOCKCHAIN_PORT, + ExpensesBlockchainPort, +} from '../../domain/interfaces/expenses-blockchain.port'; + +/** + * Реализация `InterExpenseChassisPort` для consumer-расширений (capital, marketplace, EMP) + * и реестра платежей (gateway). + * + * Adapter — тонкий: читает из локального TypeORM-зеркала proposals + items, проектирует + * во внешний нейтральный DTO `@coopenomics/inter`. Write-методы (`payItem` / + * `returnItem` / `overspendItem` / `reportItem`) — on-chain действия шасси при + * подтверждении кассиром платежей расчёта (payexp / returnexp / overspendexp / + * reportexp), под подписью кооператива. + */ +@Injectable() +export class ExpensesInterExpenseChassisAdapter implements InterExpenseChassisPort { + constructor( + @InjectRepository(ExpenseProposalTypeormEntity) + private readonly repository: Repository, + private readonly requisiteSnapshots: ExpenseRequisiteSnapshotsService, + @Inject(EXPENSES_BLOCKCHAIN_PORT) + private readonly chain: ExpensesBlockchainPort, + @Inject(PAYMENT_REPOSITORY) + private readonly payments: PaymentRepository, + ) {} + + async payItem(coopname: string, proposalHash: string, itemHash: string, actualAmount: string): Promise { + await this.chain.payExp({ + coopname, + proposal_hash: proposalHash.toLowerCase(), + item_hash: itemHash.toLowerCase(), + actual_amount: actualAmount, + }); + } + + async returnItem(coopname: string, proposalHash: string, itemHash: string, returnAmount: string): Promise { + await this.chain.returnExp({ + coopname, + proposal_hash: proposalHash.toLowerCase(), + item_hash: itemHash.toLowerCase(), + return_amount: returnAmount, + }); + } + + async overspendItem(coopname: string, proposalHash: string, itemHash: string, overspendAmount: string): Promise { + await this.chain.overspendExp({ + coopname, + proposal_hash: proposalHash.toLowerCase(), + item_hash: itemHash.toLowerCase(), + overspend_amount: overspendAmount, + }); + } + + async reportItem(coopname: string, proposalHash: string, itemHash: string): Promise { + await this.chain.reportExp({ + coopname, + proposal_hash: proposalHash.toLowerCase(), + item_hash: itemHash.toLowerCase(), + }); + // Зеркалим закрытие отчёта в платёж выдачи аванса (hash платежа = item_hash): + // реестр платежей показывает «Отчёт принят» сразу после проводки расчёта кассой. + const payment = await this.payments.findByHash(itemHash.toLowerCase()); + if (payment?.id) { + await this.payments.update(payment.id, { + blockchain_data: { ...(payment.blockchain_data ?? {}), report_state: ExpenseReportState.CLOSED }, + }); + } + } + + async validateRequisites(coopname: string, items: InterExpenseRequisiteItemInput[]): Promise { + await this.requisiteSnapshots.validate(coopname, items); + } + + async snapshotRequisites(coopname: string, items: InterExpenseRequisiteItemInput[]): Promise { + await this.requisiteSnapshots.snapshot(coopname, items); + } + + async readProposalByHash(coopname: string, proposalHash: string): Promise { + const entity = await this.repository.findOne({ + where: { coopname, proposal_hash: proposalHash.toLowerCase() }, + }); + return entity ? this.toRead(entity) : null; + } + + async readProposalsByHashes(coopname: string, proposalHashes: string[]): Promise { + if (proposalHashes.length === 0) return []; + const normalized = proposalHashes.map((h) => h.toLowerCase()); + const entities = await this.repository.find({ + where: { coopname, proposal_hash: In(normalized) }, + }); + return entities.map((e) => this.toRead(e)); + } + + async listProposalsByOwner( + coopname: string, + ownerContract: string, + ownerAction?: string, + pagination?: InterExpensePagination, + ): Promise> { + const sortBy = pagination?.sortBy === 'createdAt' ? 'expense_proposal.created_at' : 'expense_proposal.updated_at'; + const sortOrder = pagination?.sortOrder === 'ASC' ? 'ASC' : 'DESC'; + const limit = pagination?.limit ?? 50; + const offset = pagination?.offset ?? 0; + + const qb = this.repository + .createQueryBuilder('expense_proposal') + .where('expense_proposal.coopname = :coopname', { coopname }) + .andWhere(`expense_proposal.callback ->> 'contract' = :ownerContract`, { ownerContract }); + + if (ownerAction) { + qb.andWhere(`expense_proposal.callback ->> 'action' = :ownerAction`, { ownerAction }); + } + + const totalCount = await qb.getCount(); + const entities = await qb.orderBy(sortBy, sortOrder).skip(offset).take(limit).getMany(); + return { items: entities.map((e) => this.toRead(e)), totalCount }; + } + + private toRead(e: ExpenseProposalTypeormEntity): InterExpenseProposalRead { + return { + coopname: e.coopname, + proposalHash: e.proposal_hash, + sourceWalletCode: e.source_wallet ?? '', + creator: e.username ?? '', + status: this.mapStatus(e.status), + callback: e.callback + ? { contract: e.callback.contract, action: e.callback.action, data: e.callback.data } + : undefined, + items: (e.items ?? []).map((it): InterExpenseItem => ({ + itemHash: it.item_hash, + mechanics: it.mechanics, + recipientType: it.recipient_type, + recipient: it.recipient, + description: it.description, + plannedAmount: it.planned_amount, + actualAmount: it.actual_amount, + status: it.status, + })), + totalPlanned: e.total_planned ?? '', + totalActual: e.total_actual ?? '', + createdAt: e.created_at?.toISOString?.() ?? '', + updatedAt: e.updated_at?.toISOString?.() ?? '', + }; + } + + private mapStatus(status: ExpenseProposalStatus | undefined): InterExpenseProposalStatus { + switch (status) { + case ExpenseProposalStatus.CREATED: + return 'CREATED'; + case ExpenseProposalStatus.AUTHORIZED: + return 'AUTHORIZED'; + case ExpenseProposalStatus.PARTIALLY_PAID: + return 'PARTIALLY_PAID'; + case ExpenseProposalStatus.REPORT_SUBMITTED: + return 'REPORT_SUBMITTED'; + case ExpenseProposalStatus.CLOSED: + return 'CLOSED'; + case ExpenseProposalStatus.DECLINED: + return 'DECLINED'; + default: + return 'UNDEFINED'; + } + } +} diff --git a/components/controller/src/extensions/expenses/infrastructure/mappers/expense-file.mapper.ts b/components/controller/src/extensions/expenses/infrastructure/mappers/expense-file.mapper.ts new file mode 100644 index 00000000000..9b6cd87395c --- /dev/null +++ b/components/controller/src/extensions/expenses/infrastructure/mappers/expense-file.mapper.ts @@ -0,0 +1,41 @@ +import { ExpenseFileTypeormEntity } from '../entities/expense-file.typeorm-entity'; +import type { IExpenseFileDatabaseData } from '../../domain/interfaces/expense-file-database.interface'; + +/** + * Маппер для записей файлов расхода. БД-only сущность — symmetric copy без + * дополнительной логики. + */ +export class ExpenseFileMapper { + static toDomain(entity: ExpenseFileTypeormEntity): IExpenseFileDatabaseData { + return { + id: entity.id, + coopname: entity.coopname, + proposal_hash: entity.proposal_hash, + item_hash: entity.item_hash, + kind: entity.kind, + checksum_sha256: entity.checksum_sha256, + mime_type: entity.mime_type, + size_bytes: entity.size_bytes, + storage_key: entity.storage_key, + original_filename: entity.original_filename, + uploaded_by_username: entity.uploaded_by_username, + uploaded_at: entity.uploaded_at, + }; + } + + static toEntity(data: IExpenseFileDatabaseData): Partial { + return { + coopname: data.coopname, + proposal_hash: data.proposal_hash, + item_hash: data.item_hash ?? null, + kind: data.kind, + checksum_sha256: data.checksum_sha256, + mime_type: data.mime_type, + size_bytes: data.size_bytes, + storage_key: data.storage_key, + original_filename: data.original_filename ?? null, + uploaded_by_username: data.uploaded_by_username, + uploaded_at: data.uploaded_at, + }; + } +} diff --git a/components/controller/src/extensions/expenses/infrastructure/mappers/expense-proposal.mapper.ts b/components/controller/src/extensions/expenses/infrastructure/mappers/expense-proposal.mapper.ts new file mode 100644 index 00000000000..3f9351b0497 --- /dev/null +++ b/components/controller/src/extensions/expenses/infrastructure/mappers/expense-proposal.mapper.ts @@ -0,0 +1,91 @@ +import { ExpenseProposalDomainEntity } from '../../domain/entities/expense-proposal.entity'; +import { ExpenseProposalTypeormEntity } from '../entities/expense-proposal.typeorm-entity'; +import type { IExpenseProposalDatabaseData } from '../../domain/interfaces/expense-proposal-database.interface'; +import type { IExpenseProposalBlockchainData } from '../../domain/interfaces/expense-proposal-blockchain.interface'; +import { ExpenseProposalStatus } from '../../domain/enums/expense-proposal-status.enum'; + +/** + * Маппер `ExpenseProposalTypeormEntity` ↔ `ExpenseProposalDomainEntity`. + * + * Логика идентична capital-стилю: статус/идентификация хранится локально, + * экономика (items, total_*, документы) — образ блокчейна и обновляется + * только через `AbstractEntitySyncService`. Поэтому `toUpdateEntity` + * пробрасывает наверх только `block_num`/`present`. + */ +export class ExpenseProposalMapper { + static toDomain(entity: ExpenseProposalTypeormEntity): ExpenseProposalDomainEntity { + const databaseData: IExpenseProposalDatabaseData = { + _id: entity._id, + block_num: entity.block_num, + present: entity.present, + proposal_hash: entity.proposal_hash, + coopname: entity.coopname, + status: entity.status ?? ExpenseProposalStatus.UNDEFINED, + _created_at: entity._created_at, + _updated_at: entity._updated_at, + }; + + let blockchainData: IExpenseProposalBlockchainData | undefined; + + if (entity.id) { + blockchainData = { + id: String(entity.id), + proposal_hash: entity.proposal_hash, + coopname: entity.coopname, + username: entity.username, + source_wallet: entity.source_wallet, + status: entity.blockchain_status, + items: entity.items ?? [], + total_planned: entity.total_planned, + total_actual: entity.total_actual, + callback: entity.callback ?? undefined, + statement_doc: entity.statement_doc ?? undefined, + decision_doc: entity.decision_doc ?? undefined, + created_at: entity.created_at?.toISOString?.() ?? '', + updated_at: entity.updated_at?.toISOString?.() ?? '', + }; + } + + return new ExpenseProposalDomainEntity(databaseData, blockchainData); + } + + static toEntity(domain: ExpenseProposalDomainEntity): Partial { + const dbPart: Partial = { + _id: domain._id, + block_num: domain.block_num ?? 0, + present: domain.present, + proposal_hash: domain.proposal_hash, + coopname: domain.coopname, + status: domain.status, + _created_at: domain._created_at as Date, + _updated_at: domain._updated_at as Date, + }; + + if (domain.id !== undefined) { + const bcPart: Partial = { + id: domain.id, + username: domain.username, + source_wallet: domain.source_wallet, + blockchain_status: domain.blockchain_status, + items: domain.items ?? [], + total_planned: domain.total_planned, + total_actual: domain.total_actual, + callback: domain.callback ?? null, + statement_doc: domain.statement_doc ?? null, + decision_doc: domain.decision_doc ?? null, + created_at: domain.created_at ? new Date(domain.created_at) : new Date(), + updated_at: domain.updated_at ? new Date(domain.updated_at) : new Date(), + }; + Object.assign(dbPart, bcPart); + } + + return dbPart; + } + + static toUpdateEntity(domain: Partial): Partial { + const updateData: Partial = {}; + if (domain.block_num !== undefined) updateData.block_num = domain.block_num; + if (domain.present !== undefined) updateData.present = domain.present; + return updateData; + } +} diff --git a/components/controller/src/extensions/expenses/infrastructure/repositories/expense-file.typeorm-repository.ts b/components/controller/src/extensions/expenses/infrastructure/repositories/expense-file.typeorm-repository.ts new file mode 100644 index 00000000000..15eb90d47ab --- /dev/null +++ b/components/controller/src/extensions/expenses/infrastructure/repositories/expense-file.typeorm-repository.ts @@ -0,0 +1,57 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { ExpenseFileTypeormEntity } from '../entities/expense-file.typeorm-entity'; +import { ExpenseFileMapper } from '../mappers/expense-file.mapper'; +import type { ExpenseFileRepository } from '../../domain/repositories/expense-file.repository'; +import type { IExpenseFileDatabaseData } from '../../domain/interfaces/expense-file-database.interface'; + +@Injectable() +export class ExpenseFileTypeormRepository implements ExpenseFileRepository { + constructor( + @InjectRepository(ExpenseFileTypeormEntity) + private readonly repository: Repository + ) {} + + async create(data: IExpenseFileDatabaseData): Promise { + const entity = this.repository.create(ExpenseFileMapper.toEntity(data)); + const saved = await this.repository.save(entity); + return ExpenseFileMapper.toDomain(saved); + } + + async findById(id: number): Promise { + const entity = await this.repository.findOne({ where: { id } }); + return entity ? ExpenseFileMapper.toDomain(entity) : null; + } + + async findByChecksum(coopname: string, checksum: string): Promise { + const entity = await this.repository.findOne({ + where: { coopname, checksum_sha256: checksum }, + }); + return entity ? ExpenseFileMapper.toDomain(entity) : null; + } + + async findByProposal(coopname: string, proposalHash: string): Promise { + const entities = await this.repository.find({ + where: { coopname, proposal_hash: proposalHash.toLowerCase() }, + order: { uploaded_at: 'DESC' }, + }); + return entities.map((e) => ExpenseFileMapper.toDomain(e)); + } + + async findByItem(coopname: string, proposalHash: string, itemHash: string): Promise { + const entities = await this.repository.find({ + where: { + coopname, + proposal_hash: proposalHash.toLowerCase(), + item_hash: itemHash.toLowerCase(), + }, + order: { uploaded_at: 'DESC' }, + }); + return entities.map((e) => ExpenseFileMapper.toDomain(e)); + } + + async delete(id: number): Promise { + await this.repository.delete({ id }); + } +} diff --git a/components/controller/src/extensions/expenses/infrastructure/repositories/expense-proposal.typeorm-repository.ts b/components/controller/src/extensions/expenses/infrastructure/repositories/expense-proposal.typeorm-repository.ts new file mode 100644 index 00000000000..d9c045d82bd --- /dev/null +++ b/components/controller/src/extensions/expenses/infrastructure/repositories/expense-proposal.typeorm-repository.ts @@ -0,0 +1,102 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { BaseBlockchainRepository } from '~/shared/sync/repositories/base-blockchain.repository'; +import { EntityVersioningService } from '~/shared/sync/services/entity-versioning.service'; +import type { IBlockchainSyncRepository } from '~/shared/interfaces/blockchain-sync.interface'; +import type { PaginationInputDTO } from '~/application/common/dto/pagination.dto'; +import { ExpenseProposalDomainEntity } from '../../domain/entities/expense-proposal.entity'; +import { ExpenseProposalTypeormEntity } from '../entities/expense-proposal.typeorm-entity'; +import { ExpenseProposalMapper } from '../mappers/expense-proposal.mapper'; +import type { ExpenseProposalRepository } from '../../domain/repositories/expense-proposal.repository'; +import type { IExpenseProposalDatabaseData } from '../../domain/interfaces/expense-proposal-database.interface'; +import type { IExpenseProposalBlockchainData } from '../../domain/interfaces/expense-proposal-blockchain.interface'; + +@Injectable() +export class ExpenseProposalTypeormRepository + extends BaseBlockchainRepository + implements ExpenseProposalRepository, IBlockchainSyncRepository +{ + constructor( + @InjectRepository(ExpenseProposalTypeormEntity) + repository: Repository, + entityVersioningService: EntityVersioningService + ) { + super(repository, entityVersioningService); + } + + protected getMapper() { + return { + toDomain: ExpenseProposalMapper.toDomain, + toEntity: ExpenseProposalMapper.toEntity, + }; + } + + protected createDomainEntity( + databaseData: IExpenseProposalDatabaseData, + blockchainData: IExpenseProposalBlockchainData + ): ExpenseProposalDomainEntity { + return new ExpenseProposalDomainEntity(databaseData, blockchainData); + } + + protected getSyncKey(): string { + return ExpenseProposalDomainEntity.getSyncKey(); + } + + async create(proposal: ExpenseProposalDomainEntity): Promise { + const entity = this.repository.create(ExpenseProposalMapper.toEntity(proposal)); + const saved = await this.repository.save(entity); + return ExpenseProposalMapper.toDomain(saved); + } + + async findByProposalHash(proposalHash: string): Promise { + const entity = await this.repository.findOne({ where: { proposal_hash: proposalHash.toLowerCase() } }); + return entity ? ExpenseProposalMapper.toDomain(entity) : null; + } + + async findByCoopname(coopname: string): Promise { + const entities = await this.repository.find({ where: { coopname } }); + return entities.map((e) => ExpenseProposalMapper.toDomain(e)); + } + + async findByUsername(coopname: string, username: string): Promise { + const entities = await this.repository.find({ where: { coopname, username } }); + return entities.map((e) => ExpenseProposalMapper.toDomain(e)); + } + + async findByCoopnamePaginated( + coopname: string, + options?: PaginationInputDTO + ): Promise<{ items: ExpenseProposalDomainEntity[]; totalCount: number }> { + return this.paginate({ coopname }, options); + } + + async findByUsernamePaginated( + coopname: string, + username: string, + options?: PaginationInputDTO + ): Promise<{ items: ExpenseProposalDomainEntity[]; totalCount: number }> { + return this.paginate({ coopname, username }, options); + } + + private async paginate( + where: Record, + options?: PaginationInputDTO + ): Promise<{ items: ExpenseProposalDomainEntity[]; totalCount: number }> { + const page = Math.max(1, options?.page ?? 1); + const limit = Math.max(1, Math.min(200, options?.limit ?? 10)); + const sortBy = options?.sortBy ?? '_created_at'; + const sortOrder = options?.sortOrder ?? 'DESC'; + + const [entities, totalCount] = await this.repository.findAndCount({ + where, + order: { [sortBy]: sortOrder }, + skip: (page - 1) * limit, + take: limit, + }); + return { + items: entities.map((e) => ExpenseProposalMapper.toDomain(e)), + totalCount, + }; + } +} diff --git a/components/controller/src/extensions/expenses/infrastructure/services/expense-contract-info.service.ts b/components/controller/src/extensions/expenses/infrastructure/services/expense-contract-info.service.ts new file mode 100644 index 00000000000..a9d77ebd1c4 --- /dev/null +++ b/components/controller/src/extensions/expenses/infrastructure/services/expense-contract-info.service.ts @@ -0,0 +1,29 @@ +import { Injectable } from '@nestjs/common'; + +/** + * Сервис паттернов имён контрактов и таблиц для расширения `expense`. + * Аналог `CapitalContractInfoService` — централизует масочные паттерны + * для parser2 subscribe. + */ +@Injectable() +export class ExpenseContractInfoService { + private readonly supportedContractNames: string[] = ['expense']; + + private readonly tablePatterns: Record = { + proposals: ['proposals', 'proposals*'], + }; + + getSupportedContractNames(): string[] { + return [...this.supportedContractNames]; + } + + getTablePatterns(entityName: string): string[] { + const patterns = this.tablePatterns[entityName]; + if (!patterns) { + throw new Error( + `Unknown entity name: ${entityName}. Supported entities: ${Object.keys(this.tablePatterns).join(', ')}` + ); + } + return [...patterns]; + } +} diff --git a/components/controller/src/extensions/extensions.module.ts b/components/controller/src/extensions/extensions.module.ts index 92af08ac052..0ca39881642 100644 --- a/components/controller/src/extensions/extensions.module.ts +++ b/components/controller/src/extensions/extensions.module.ts @@ -11,6 +11,7 @@ import { ParticipantPluginModule } from './participant/participant-extension.mod import { ChatCoopPluginModule } from './chatcoop/chatcoop-extension.module'; import { OneCoopPluginModule } from './1ccoop/oneccoop-extension.module'; import { ReportsExtensionModule } from './reports/reports-extension.module'; +import { ExpensesExtensionModule } from './expenses/expenses-extension.module'; import { ExtensionDomainModule } from '~/domain/extension/extension-domain.module'; import { GatewayDomainModule } from '~/domain/gateway/gateway-domain.module'; @@ -36,6 +37,7 @@ export class ExtensionsModule { InterCommunicationBridgeModule, OneCoopPluginModule, ReportsExtensionModule, + ExpensesExtensionModule, ], providers: [], // Экспортируем все модули расширений, чтобы их провайдеры были доступны @@ -53,6 +55,7 @@ export class ExtensionsModule { InterCommunicationBridgeModule, OneCoopPluginModule, ReportsExtensionModule, + ExpensesExtensionModule, ], }; } diff --git a/components/controller/src/extensions/inter-communication-bridge.module.ts b/components/controller/src/extensions/inter-communication-bridge.module.ts index 409f4057328..ba0206aef01 100644 --- a/components/controller/src/extensions/inter-communication-bridge.module.ts +++ b/components/controller/src/extensions/inter-communication-bridge.module.ts @@ -1,24 +1,28 @@ import { Global, Module } from '@nestjs/common'; import { INTER_CHATCOOP_CALENDAR, + INTER_EXPENSE_CHASSIS, INTER_MATRIX_ROOM_MESSAGING, INTER_PROJECT_CAPITAL_CLEARANCE, INTER_PROJECT_COMMUNICATION_ARTIFACTS, } from '@coopenomics/inter'; import { ChatCoopPluginModule } from './chatcoop/chatcoop-extension.module'; import { CapitalPluginModule } from './capital/capital-extension.module'; +import { ExpensesExtensionModule } from './expenses/expenses-extension.module'; import { ChatcoopInterProjectCommunicationArtifactsAdapter } from './chatcoop/infrastructure/inter/chatcoop-inter-project-communication-artifacts.adapter'; import { ChatcoopInterMatrixRoomMessagingAdapter } from './chatcoop/infrastructure/inter/chatcoop-inter-matrix-room-messaging.adapter'; import { ChatcoopInterChatCoopCalendarAdapter } from './chatcoop/infrastructure/inter/chatcoop-inter-chatcoop-calendar.adapter'; import { CapitalInterProjectCapitalClearanceAdapter } from './capital/infrastructure/inter/capital-inter-project-capital-clearance.adapter'; +import { ExpensesInterExpenseChassisAdapter } from './expenses/infrastructure/inter/expenses-inter-expense-chassis.adapter'; /** - * Глобальная привязка порта @coopenomics/inter к реализации ChatCoop. - * Capital и другие расширения инжектят токен без импорта ChatCoop. + * Глобальная привязка портов @coopenomics/inter к их реализациям-расширениям. + * Consumer-extension'ы (capital, marketplace, EMP) инжектят токен без знания + * о конкретной реализации. */ @Global() @Module({ - imports: [CapitalPluginModule, ChatCoopPluginModule], + imports: [CapitalPluginModule, ChatCoopPluginModule, ExpensesExtensionModule], providers: [ { provide: INTER_PROJECT_COMMUNICATION_ARTIFACTS, @@ -36,12 +40,17 @@ import { CapitalInterProjectCapitalClearanceAdapter } from './capital/infrastruc provide: INTER_PROJECT_CAPITAL_CLEARANCE, useExisting: CapitalInterProjectCapitalClearanceAdapter, }, + { + provide: INTER_EXPENSE_CHASSIS, + useExisting: ExpensesInterExpenseChassisAdapter, + }, ], exports: [ INTER_PROJECT_COMMUNICATION_ARTIFACTS, INTER_MATRIX_ROOM_MESSAGING, INTER_CHATCOOP_CALENDAR, INTER_PROJECT_CAPITAL_CLEARANCE, + INTER_EXPENSE_CHASSIS, ], }) export class InterCommunicationBridgeModule {} diff --git a/components/controller/src/infrastructure/database/typeorm/repositories/typeorm-action.repository.ts b/components/controller/src/infrastructure/database/typeorm/repositories/typeorm-action.repository.ts index 4999270d8c4..9c604093647 100644 --- a/components/controller/src/infrastructure/database/typeorm/repositories/typeorm-action.repository.ts +++ b/components/controller/src/infrastructure/database/typeorm/repositories/typeorm-action.repository.ts @@ -24,7 +24,21 @@ export class TypeOrmActionRepository implements ActionRepositoryPort { */ async save(actionData: Omit): Promise { const entity = this.actionRepository.create(actionData); - return await this.actionRepository.save(entity); + try { + return await this.actionRepository.save(entity); + } catch (err: any) { + // global_sequence — глобально-уникальный монотонный id действия в истории + // цепи. Дубль (23505) означает повторную доставку того же действия + // (re-scan/replay стрима) — оно уже сохранено. Идемпотентно: не бросаем, + // иначе consumer не ACK'ает сообщение и зацикливает recoverOwnPending. + if (err?.code === '23505' || err?.driverError?.code === '23505') { + const existing = await this.actionRepository.findOne({ + where: { global_sequence: actionData.global_sequence }, + }); + if (existing) return existing; + } + throw err; + } } /** diff --git a/components/controller/src/infrastructure/database/typeorm/repositories/typeorm-payment.repository.ts b/components/controller/src/infrastructure/database/typeorm/repositories/typeorm-payment.repository.ts index 033d1ceee6d..8340dcf849d 100644 --- a/components/controller/src/infrastructure/database/typeorm/repositories/typeorm-payment.repository.ts +++ b/components/controller/src/infrastructure/database/typeorm/repositories/typeorm-payment.repository.ts @@ -167,6 +167,15 @@ export class TypeOrmPaymentRepository implements PaymentRepository { queryBuilder.andWhere('payment.hash = :hash', { hash: filters.hash }); } + // Все платежи, связанные с расходом (служебной запиской): proposal_hash + // лежит в json-поле blockchain_data у платежа выдачи аванса/оплаты и у + // расчётных платёжек (возврат/доплата). Извлекаем через ->> (json-оператор). + if (filters.proposal_hash) { + queryBuilder.andWhere("payment.blockchain_data ->> 'proposal_hash' = :proposalHash", { + proposalHash: filters.proposal_hash.toLowerCase(), + }); + } + queryBuilder.orderBy('payment.created_at', 'DESC'); const limit = options.limit || 10; diff --git a/components/controller/src/infrastructure/generator/generator.service.ts b/components/controller/src/infrastructure/generator/generator.service.ts index 834dba62159..509bc533c41 100644 --- a/components/controller/src/infrastructure/generator/generator.service.ts +++ b/components/controller/src/infrastructure/generator/generator.service.ts @@ -63,6 +63,14 @@ export class GeneratorInfrastructureService implements GeneratorPort, OnModuleIn return await this.generator.search(query); } + async saveDocData

>(payload: P, registry_id: number): Promise<{ hash: string }> { + return await this.generator.saveDocData(payload, registry_id); + } + + async getDocData

>(hash: string): Promise

{ + return await this.generator.getDocData

(hash); + } + async generateDocument(body: GenerateDocumentDomainInterfaceWithOptions): Promise { try { const generated = await this.generate(body.data, body.options); diff --git a/components/controller/zeus/const.ts b/components/controller/zeus/const.ts index 63d4ca66150..2aef7280b32 100644 --- a/components/controller/zeus/const.ts +++ b/components/controller/zeus/const.ts @@ -149,6 +149,10 @@ export const AllTypesProps: Record = { CapitalContributorFilter:{ status:"ContributorStatus" }, + CapitalCreateProgramExpenseInput:{ + items:"ExpenseItemInput", + statement:"ExpenseProposalStatementSignedDocumentInput" + }, CapitalCycleFilter:{ status:"CycleStatus" }, @@ -185,6 +189,9 @@ export const AllTypesProps: Record = { }, CapitalTimeStatsInput:{ + }, + CapitalTopupProgramExpenseInput:{ + }, ChairmanOnboardingAgendaInput:{ step:"ChairmanOnboardingAgendaStep" @@ -296,6 +303,11 @@ export const AllTypesProps: Record = { CreateExpenseInput:{ statement:"SignedDigitalDocumentInput" }, + CreateExpenseProposalInput:{ + callback:"ExpenseCallbackInput", + items:"ExpenseItemInput", + statement:"ExpenseProposalStatementSignedDocumentInput" + }, CreateIndividualDataInput:{ passport:"PassportInput" }, @@ -431,9 +443,57 @@ export const AllTypesProps: Record = { EntrepreneurDetailsInput:{ }, + ExpenseCallbackInput:{ + + }, + ExpenseFileKind: "enum" as const, ExpenseFilter:{ }, + ExpenseItemInput:{ + mechanics:"ExpenseMechanics", + recipient_type:"ExpenseRecipientType" + }, + ExpenseItemStatus: "enum" as const, + ExpenseMechanics: "enum" as const, + ExpenseProposalDecisionBodyInput:{ + + }, + ExpenseProposalDecisionGenerateDocumentInput:{ + items:"ExpenseProposalDecisionItemInput", + proposal:"ExpenseProposalDecisionHeaderInput", + resolution:"ExpenseProposalDecisionBodyInput" + }, + ExpenseProposalDecisionHeaderInput:{ + + }, + ExpenseProposalDecisionItemInput:{ + + }, + ExpenseProposalHeaderInput:{ + + }, + ExpenseProposalItemInput:{ + + }, + ExpenseProposalSignedItemInput:{ + + }, + ExpenseProposalStatementGenerateDocumentInput:{ + items:"ExpenseProposalItemInput", + proposal:"ExpenseProposalHeaderInput" + }, + ExpenseProposalStatementSignedDocumentInput:{ + meta:"ExpenseProposalStatementSignedMetaDocumentInput", + signatures:"SignatureInfoInput" + }, + ExpenseProposalStatementSignedMetaDocumentInput:{ + items:"ExpenseProposalSignedItemInput", + proposal:"ExpenseProposalHeaderInput" + }, + ExpenseProposalStatus: "enum" as const, + ExpenseRecipientType: "enum" as const, + ExpenseReportOutcome: "enum" as const, ExpenseStatus: "enum" as const, ExtendedMeetStatus: "enum" as const, ExtensionInput:{ @@ -721,6 +781,9 @@ export const AllTypesProps: Record = { capitalCreateProcessTemplate:{ data:"CreateProcessTemplateInput" }, + capitalCreateProgramExpense:{ + data:"CapitalCreateProgramExpenseInput" + }, capitalCreateProgramInvest:{ data:"CreateProgramInvestInput" }, @@ -914,6 +977,9 @@ export const AllTypesProps: Record = { capitalSubmitVote:{ data:"SubmitVoteInput" }, + capitalTopupProgramExpensePool:{ + data:"CapitalTopupProgramExpenseInput" + }, capitalUpdateIssue:{ data:"UpdateIssueInput" }, @@ -989,6 +1055,9 @@ export const AllTypesProps: Record = { createDepositPayment:{ data:"CreateDepositPaymentInput" }, + createExpenseProposal:{ + data:"CreateExpenseProposalInput" + }, createInitialPayment:{ data:"CreateInitialPaymentInput" }, @@ -1078,6 +1147,14 @@ export const AllTypesProps: Record = { generateDocument:{ input:"GenerateAnyDocumentInput" }, + generateExpenseProposalDecisionDocument:{ + data:"ExpenseProposalDecisionGenerateDocumentInput", + options:"GenerateDocumentOptionsInput" + }, + generateExpenseProposalStatementDocument:{ + data:"ExpenseProposalStatementGenerateDocumentInput", + options:"GenerateDocumentOptionsInput" + }, generateFreeDecision:{ data:"FreeDecisionGenerateDocumentInput", options:"GenerateDocumentOptionsInput" @@ -1182,6 +1259,12 @@ export const AllTypesProps: Record = { notifyOnAnnualGeneralMeet:{ data:"NotifyOnAnnualGeneralMeetInput" }, + overspendExpenseItem:{ + data:"OverspendExpenseItemInput" + }, + payExpenseItem:{ + data:"PayExpenseItemInput" + }, processConvertToAxonStatement:{ data:"ProcessConvertToAxonStatementInput" }, @@ -1206,6 +1289,9 @@ export const AllTypesProps: Record = { registerParticipant:{ data:"RegisterParticipantInput" }, + reportExpenseItem:{ + data:"ReportExpenseItemInput" + }, resendNotification:{ }, @@ -1215,6 +1301,9 @@ export const AllTypesProps: Record = { restartAnnualGeneralMeet:{ data:"RestartAnnualGeneralMeetInput" }, + returnExpenseItem:{ + data:"ReturnExpenseItemInput" + }, saveReportDraft:{ input:"SaveReportDraftInput" }, @@ -1242,6 +1331,9 @@ export const AllTypesProps: Record = { startResetKey:{ data:"StartResetKeyInput" }, + submitExpenseReport:{ + data:"SubmitExpenseReportInput" + }, supplyOnRequest:{ data:"SupplyOnRequestInput" }, @@ -1275,6 +1367,9 @@ export const AllTypesProps: Record = { updateSystem:{ data:"Update" }, + uploadExpenseFile:{ + data:"UploadExpenseFileInput" + }, verifyEmail:{ data:"VerifyEmailInputDTO" }, @@ -1306,6 +1401,9 @@ export const AllTypesProps: Record = { }, OrganizationType: "enum" as const, + OverspendExpenseItemInput:{ + + }, PaginationInput:{ }, @@ -1324,6 +1422,9 @@ export const AllTypesProps: Record = { }, PassportInput:{ + }, + PayExpenseItemInput:{ + }, PaymentDirection: "enum" as const, PaymentFiltersInput:{ @@ -1469,6 +1570,12 @@ export const AllTypesProps: Record = { filter:"CapitalIssueFilter", options:"PaginationInput" }, + capitalProgramExpense:{ + + }, + capitalProgramExpenses:{ + options:"PaginationInput" + }, capitalProject:{ data:"GetProjectInput" }, @@ -1558,6 +1665,27 @@ export const AllTypesProps: Record = { }, cooperativePrograms:{ + }, + expenseFile:{ + + }, + expenseFilesByItem:{ + + }, + expenseFilesByProposal:{ + + }, + expenseProposal:{ + + }, + expenseProposalsByCooperative:{ + options:"PaginationInput" + }, + expenseProposalsByMember:{ + options:"PaginationInput" + }, + expenseRequisitesByProposal:{ + }, getAccount:{ data:"GetAccountInput" @@ -1744,6 +1872,9 @@ export const AllTypesProps: Record = { }, RemoveSecretaryRoomInput:{ + }, + ReportExpenseItemInput:{ + }, ReportHistoryFilterInput:{ reportType:"ReportType" @@ -1813,6 +1944,9 @@ export const AllTypesProps: Record = { }, ReturnByMoneySignedMetaDocumentInput:{ + }, + ReturnExpenseItemInput:{ + }, RoomMessageKind: "enum" as const, SaveReportDraftInput:{ @@ -1911,6 +2045,9 @@ export const AllTypesProps: Record = { }, StoryStatus: "enum" as const, + SubmitExpenseReportInput:{ + + }, SubmitVoteInput:{ votes:"VoteDistributionInput" }, @@ -1981,6 +2118,9 @@ export const AllTypesProps: Record = { content_format:"CapitalStoryContentFormat", status:"StoryStatus" }, + UploadExpenseFileInput:{ + kind:"ExpenseFileKind" + }, UserStatus: "enum" as const, VarsInput:{ coopenomics_agreement:"AgreementInput", @@ -2649,6 +2789,36 @@ export const ReturnTypes: Record = { onboarding_generator_program_template_hash:"String", onboarding_init_at:"String" }, + CapitalProgramExpense:{ + callback:"CapitalProgramExpenseCallback", + coopname:"String", + created_at:"String", + creator:"String", + creator_name:"String", + expense_hash:"String", + items:"CapitalProgramExpenseItem", + source_wallet:"String", + status:"ExpenseProposalStatus", + total_actual:"String", + total_planned:"String", + updated_at:"String" + }, + CapitalProgramExpenseCallback:{ + action:"String", + contract:"String", + data:"String" + }, + CapitalProgramExpenseItem:{ + actual_amount:"String", + description:"String", + item_hash:"String", + mechanics:"ExpenseMechanics", + planned_amount:"String", + recipient:"String", + recipient_name:"String", + recipient_type:"ExpenseRecipientType", + status:"ExpenseItemStatus" + }, CapitalProject:{ _created_at:"DateTime", _id:"String", @@ -2904,6 +3074,8 @@ export const ReturnTypes: Record = { coopname:"String", global_available_invest_pool:"String", present:"Boolean", + program_expense_pool:"String", + program_expense_reserved:"String", program_membership_available:"String", program_membership_cumulative_reward_per_share:"Float", program_membership_distributed:"String", @@ -3220,6 +3392,68 @@ export const ReturnTypes: Record = { inn:"String", ogrn:"String" }, + ExpenseFile:{ + checksum_sha256:"String", + coopname:"String", + id:"Int", + item_hash:"String", + kind:"ExpenseFileKind", + mime_type:"String", + original_filename:"String", + proposal_hash:"String", + read_url:"String", + size_bytes:"Int", + storage_key:"String", + uploaded_at:"DateTime", + uploaded_by_username:"String" + }, + ExpenseItem:{ + actual_amount:"String", + description:"String", + item_hash:"String", + mechanics:"ExpenseMechanics", + planned_amount:"String", + recipient:"String", + recipient_type:"ExpenseRecipientType", + status:"ExpenseItemStatus" + }, + ExpenseProposal:{ + _created_at:"DateTime", + _id:"String", + _updated_at:"DateTime", + block_num:"Float", + blockchain_status:"Int", + coopname:"String", + created_at:"String", + decision_doc:"DocumentAggregate", + id:"Int", + items:"ExpenseItem", + present:"Boolean", + proposal_hash:"String", + source_wallet:"String", + statement_doc:"DocumentAggregate", + status:"ExpenseProposalStatus", + total_actual:"String", + total_planned:"String", + updated_at:"String", + username:"String" + }, + ExpenseReportResult:{ + outcome:"ExpenseReportOutcome", + settlement_amount:"String", + settlement_payment_hash:"String", + transaction:"Transaction" + }, + ExpenseRequisite:{ + coopname:"String", + data:"JSON", + item_hash:"String", + method_type:"String", + payment_purpose:"String", + proposal_hash:"String", + recipient:"String", + requisites:"String" + }, ExtendedBlockchainAction:{ account:"String", account_ram_deltas:"AccountRamDelta", @@ -3643,6 +3877,7 @@ export const ReturnTypes: Record = { capitalCreateExpense:"Transaction", capitalCreateIssue:"CapitalIssue", capitalCreateProcessTemplate:"ProcessTemplate", + capitalCreateProgramExpense:"Transaction", capitalCreateProgramInvest:"Transaction", capitalCreateProgramProperty:"Transaction", capitalCreateProject:"Transaction", @@ -3700,6 +3935,7 @@ export const ReturnTypes: Record = { capitalStartVoting:"Transaction", capitalStopProject:"CapitalProject", capitalSubmitVote:"Transaction", + capitalTopupProgramExpensePool:"Transaction", capitalUpdateIssue:"CapitalIssue", capitalUpdateProcessTemplate:"ProcessTemplate", capitalUpdateStory:"CapitalStory", @@ -3726,6 +3962,7 @@ export const ReturnTypes: Record = { createBranch:"Branch", createChildOrder:"Transaction", createDepositPayment:"GatewayPayment", + createExpenseProposal:"Transaction", createInitialPayment:"GatewayPayment", createMembershipExit:"MembershipExitResult", createParentOffer:"Transaction", @@ -3753,6 +3990,8 @@ export const ReturnTypes: Record = { generateBallotForAnnualGeneralMeetDocument:"GeneratedDocument", generateConvertToAxonStatement:"GeneratedDocument", generateDocument:"GeneratedDocument", + generateExpenseProposalDecisionDocument:"GeneratedDocument", + generateExpenseProposalStatementDocument:"GeneratedDocument", generateFreeDecision:"GeneratedDocument", generateMembershipExitApplication:"GeneratedDocument", generateMembershipExitDecision:"GeneratedDocument", @@ -3782,6 +4021,8 @@ export const ReturnTypes: Record = { markReportPeriod:"Boolean", moderateRequest:"Transaction", notifyOnAnnualGeneralMeet:"MeetAggregate", + overspendExpenseItem:"Transaction", + payExpenseItem:"Transaction", processConvertToAxonStatement:"Boolean", prohibitRequest:"Transaction", publishProjectOfFreeDecision:"AgendaWithDocuments", @@ -3790,10 +4031,12 @@ export const ReturnTypes: Record = { refresh:"RegisteredAccount", registerAccount:"RegisteredAccount", registerParticipant:"Account", + reportExpenseItem:"ExpenseReportResult", resendNotification:"Notification", resetKey:"Boolean", resetRegistration:"Account", restartAnnualGeneralMeet:"MeetAggregate", + returnExpenseItem:"Transaction", saveReportDraft:"ReportDraft", selectBranch:"Boolean", sendAgreement:"Transaction", @@ -3803,6 +4046,7 @@ export const ReturnTypes: Record = { signBySecretaryOnAnnualGeneralMeet:"MeetAggregate", startInstall:"StartInstallResult", startResetKey:"Boolean", + submitExpenseReport:"Transaction", supplyOnRequest:"Transaction", triggerNotificationWorkflow:"Boolean", uninstallExtension:"Boolean", @@ -3814,6 +4058,7 @@ export const ReturnTypes: Record = { updateRequest:"Transaction", updateSettings:"Settings", updateSystem:"SystemInfo", + uploadExpenseFile:"ExpenseFile", verifyEmail:"Boolean", voteOnAnnualGeneralMeet:"MeetAggregate", walmoveWallets:"Ledger2AdjustmentResult" @@ -3987,6 +4232,12 @@ export const ReturnTypes: Record = { totalCount:"Int", totalPages:"Int" }, + PaginatedCapitalProgramExpensesPaginationResult:{ + currentPage:"Int", + items:"CapitalProgramExpense", + totalCount:"Int", + totalPages:"Int" + }, PaginatedCapitalProjectsPaginationResult:{ currentPage:"Int", items:"CapitalProject", @@ -4047,6 +4298,12 @@ export const ReturnTypes: Record = { totalCount:"Int", totalPages:"Int" }, + PaginatedExpenseProposalsPaginationResult:{ + currentPage:"Int", + items:"ExpenseProposal", + totalCount:"Int", + totalPages:"Int" + }, PaginatedGatewayPaymentsPaginationResult:{ currentPage:"Int", items:"GatewayPayment", @@ -4305,6 +4562,8 @@ export const ReturnTypes: Record = { capitalInvests:"PaginatedCapitalInvestsPaginationResult", capitalIssue:"CapitalIssue", capitalIssues:"PaginatedCapitalIssuesPaginationResult", + capitalProgramExpense:"CapitalProgramExpense", + capitalProgramExpenses:"PaginatedCapitalProgramExpensesPaginationResult", capitalProject:"CapitalProject", capitalProjectWithRelations:"CapitalProject", capitalProjects:"PaginatedCapitalProjectsPaginationResult", @@ -4337,6 +4596,13 @@ export const ReturnTypes: Record = { checkReportReadiness:"ReportReadinessView", cooperativeAgreements:"CoopAgreement", cooperativePrograms:"CooperativeProgram", + expenseFile:"ExpenseFile", + expenseFilesByItem:"ExpenseFile", + expenseFilesByProposal:"ExpenseFile", + expenseProposal:"ExpenseProposal", + expenseProposalsByCooperative:"PaginatedExpenseProposalsPaginationResult", + expenseProposalsByMember:"PaginatedExpenseProposalsPaginationResult", + expenseRequisitesByProposal:"ExpenseRequisite", getAccount:"Account", getAccounts:"AccountsPaginationResult", getActions:"PaginatedActionsPaginationResult", diff --git a/components/controller/zeus/index.ts b/components/controller/zeus/index.ts index 30d88b8e550..99a7dbe823f 100644 --- a/components/controller/zeus/index.ts +++ b/components/controller/zeus/index.ts @@ -2634,6 +2634,20 @@ export type ValueTypes = { status?: ValueTypes["ContributorStatus"] | undefined | null | Variable, /** Фильтр по имени пользователя */ username?: string | undefined | null | Variable +}; + ["CapitalCreateProgramExpenseInput"]: { + /** Имя кооператива. */ + coopname: string | Variable, + /** Имя пайщика-создателя СЗ (председатель). */ + creator: string | Variable, + /** Описание программного расхода. */ + description: string | Variable, + /** Хэш СЗ-расхода (детерминированный, из UI). Он же станет proposal_hash в шасси. */ + expense_hash: string | Variable, + /** Строки расхода. Способ оплаты (аванс под отчёт / оплата по счёту) задаётся на каждой строке отдельно. */ + items: Array | Variable, + /** Подписанная СЗ-смета (document2, registry 2010). */ + statement: ValueTypes["ExpenseProposalStatementSignedDocumentInput"] | Variable }; /** Цикл разработки в системе CAPITAL */ ["CapitalCycle"]: AliasType<{ @@ -3015,6 +3029,44 @@ export type ValueTypes = { step: ValueTypes["CapitalOnboardingStep"] | Variable, title?: string | undefined | null | Variable }; + ["CapitalProgramExpense"]: AliasType<{ + callback?:ValueTypes["CapitalProgramExpenseCallback"], + coopname?:boolean | `@${string}`, + created_at?:boolean | `@${string}`, + creator?:boolean | `@${string}`, + /** Имя инициатора (ФИО пайщика или название организации) */ + creator_name?:boolean | `@${string}`, + expense_hash?:boolean | `@${string}`, + items?:ValueTypes["CapitalProgramExpenseItem"], + source_wallet?:boolean | `@${string}`, + status?:boolean | `@${string}`, + total_actual?:boolean | `@${string}`, + total_planned?:boolean | `@${string}`, + updated_at?:boolean | `@${string}`, + __typename?: boolean | `@${string}`, + ['...on CapitalProgramExpense']?: Omit +}>; + ["CapitalProgramExpenseCallback"]: AliasType<{ + action?:boolean | `@${string}`, + contract?:boolean | `@${string}`, + data?:boolean | `@${string}`, + __typename?: boolean | `@${string}`, + ['...on CapitalProgramExpenseCallback']?: Omit +}>; + ["CapitalProgramExpenseItem"]: AliasType<{ + actual_amount?:boolean | `@${string}`, + description?:boolean | `@${string}`, + item_hash?:boolean | `@${string}`, + mechanics?:boolean | `@${string}`, + planned_amount?:boolean | `@${string}`, + recipient?:boolean | `@${string}`, + /** Имя получателя (ФИО пайщика или название организации) */ + recipient_name?:boolean | `@${string}`, + recipient_type?:boolean | `@${string}`, + status?:boolean | `@${string}`, + __typename?: boolean | `@${string}`, + ['...on CapitalProgramExpenseItem']?: Omit +}>; /** Проект в системе CAPITAL с компонентами */ ["CapitalProject"]: AliasType<{ /** Дата создания записи */ @@ -3593,6 +3645,10 @@ export type ValueTypes = { global_available_invest_pool?:boolean | `@${string}`, /** Флаг присутствия записи в блокчейне */ present?:boolean | `@${string}`, + /** Пул программных расходов — средства, переведённые под целевые расходы программы */ + program_expense_pool?:boolean | `@${string}`, + /** Зарезервировано в пуле программных расходов под активные расходы (служебные записки в работе) */ + program_expense_reserved?:boolean | `@${string}`, /** Доступная сумма членских взносов по программе */ program_membership_available?:boolean | `@${string}`, /** Накопительное вознаграждение на долю в членских взносах */ @@ -3758,6 +3814,12 @@ export type ValueTypes = { project_hash?: string | undefined | null | Variable, /** Имя пользователя (опционально) */ username?: string | undefined | null | Variable +}; + ["CapitalTopupProgramExpenseInput"]: { + /** Сумма пополнения (asset, eg "10000.0000 RUB"). */ + amount: string | Variable, + /** Имя кооператива. */ + coopname: string | Variable }; /** Голос в системе CAPITAL */ ["CapitalVote"]: AliasType<{ @@ -4439,6 +4501,22 @@ export type ValueTypes = { project_hash: string | Variable, /** Служебная записка о расходе */ statement: ValueTypes["SignedDigitalDocumentInput"] | Variable +}; + ["CreateExpenseProposalInput"]: { + /** Callback на финализацию closeexp (опционально). */ + callback?: ValueTypes["ExpenseCallbackInput"] | undefined | null | Variable, + /** Имя кооператива. */ + coopname: string | Variable, + /** Строки расхода (массив items). */ + items: Array | Variable, + /** Хеш сметы расхода (детерминированный, из UI). */ + proposal_hash: string | Variable, + /** Источник средств (eosio::name кошелька-источника, eg "w.cap.blago"). */ + source_wallet: string | Variable, + /** Подписанная СЗ-смета (document2, registry 2010). */ + statement: ValueTypes["ExpenseProposalStatementSignedDocumentInput"] | Variable, + /** Имя пайщика-создателя СЗ. */ + username: string | Variable }; ["CreateIndividualDataInput"]: { /** Дата рождения */ @@ -4517,16 +4595,6 @@ export type ValueTypes = { ["CreateMatrixAccountInputDTO"]: { password: string | Variable, username: string | Variable -}; - ["CreateMembershipExitInput"]: { - /** Имя аккаунта кооператива */ - coopname: string | Variable, - /** Хеш процесса выхода (генерируется на клиенте) */ - exit_hash: string | Variable, - /** Подписанное пайщиком заявление о выходе из кооператива */ - statement: ValueTypes["MembershipExitApplicationSignedDocumentInput"] | Variable, - /** Имя пайщика, выходящего из кооператива */ - username: string | Variable }; ["CreateOrganizationDataInput"]: { /** Банковский счет организации */ @@ -5137,6 +5205,47 @@ export type ValueTypes = { /** ОГРН */ ogrn: string | Variable }; + ["ExpenseCallbackInput"]: { + /** Action-метод */ + action?: string | undefined | null | Variable, + /** Контракт-целевой */ + contract?: string | undefined | null | Variable, + /** Payload (hex bytes) */ + data?: string | undefined | null | Variable +}; + /** Запись о первичном файле расхода (платёжка/чек/возврат). */ +["ExpenseFile"]: AliasType<{ + /** SHA-256 содержимого, hex-lowercase. */ + checksum_sha256?:boolean | `@${string}`, + /** Имя кооператива (scope). */ + coopname?:boolean | `@${string}`, + /** Внутренний ID записи. */ + id?:boolean | `@${string}`, + /** Хеш строки расхода (если файл уровня item). */ + item_hash?:boolean | `@${string}`, + /** Назначение файла. */ + kind?:boolean | `@${string}`, + /** MIME-тип содержимого. */ + mime_type?:boolean | `@${string}`, + /** Оригинальное имя загруженного файла. */ + original_filename?:boolean | `@${string}`, + /** Хеш сметы расхода. */ + proposal_hash?:boolean | `@${string}`, + /** Короткоживущий URL на скачивание (HMAC-signed). */ + read_url?:boolean | `@${string}`, + /** Размер файла в байтах. */ + size_bytes?:boolean | `@${string}`, + /** MinIO-ключ внутри бакета. */ + storage_key?:boolean | `@${string}`, + /** Когда загружено. */ + uploaded_at?:boolean | `@${string}`, + /** Кто загрузил (username). */ + uploaded_by_username?:boolean | `@${string}`, + __typename?: boolean | `@${string}`, + ['...on ExpenseFile']?: Omit +}>; + /** Тип первичного файла расхода. */ +["ExpenseFileKind"]:ExpenseFileKind; ["ExpenseFilter"]: { /** Фильтр по ID фонда */ fundId?: string | undefined | null | Variable, @@ -5147,6 +5256,307 @@ export type ValueTypes = { /** Фильтр по имени пользователя */ username?: string | undefined | null | Variable }; + /** Строка сметы расхода. */ +["ExpenseItem"]: AliasType<{ + /** Фактическая сумма (после оплаты/отчёта). */ + actual_amount?:boolean | `@${string}`, + /** Назначение/описание строки. */ + description?:boolean | `@${string}`, + /** Хеш строки расхода. */ + item_hash?:boolean | `@${string}`, + /** Способ оплаты. */ + mechanics?:boolean | `@${string}`, + /** Планируемая сумма. */ + planned_amount?:boolean | `@${string}`, + /** Идентификатор получателя. */ + recipient?:boolean | `@${string}`, + /** Тип получателя платежа. */ + recipient_type?:boolean | `@${string}`, + /** Статус строки. */ + status?:boolean | `@${string}`, + __typename?: boolean | `@${string}`, + ['...on ExpenseItem']?: Omit +}>; + ["ExpenseItemInput"]: { + /** Описание назначения расхода. */ + description: string | Variable, + /** Хеш строки расхода (детерминированный, из UI). */ + item_hash: string | Variable, + /** Способ оплаты (ADVANCE / DIRECT). */ + mechanics: ValueTypes["ExpenseMechanics"] | Variable, + /** Идентификатор сохранённых реквизитов получателя-пайщика — реквизиты снимаются в момент создания и прикладываются к платежу. */ + payment_method_id?: string | undefined | null | Variable, + /** Назначение платежа (для оплаты по счёту) — фиксируется в снимке для кассира. */ + payment_purpose?: string | undefined | null | Variable, + /** Планируемая сумма (asset, eg "1000.0000 RUB"). */ + planned_amount: string | Variable, + /** Получатель: username пайщика; для организации — пустая строка (аккаунта в кооперативе нет). */ + recipient: string | Variable, + /** Тип получателя. */ + recipient_type: ValueTypes["ExpenseRecipientType"] | Variable, + /** Реквизиты получателя-организации (вводятся вручную). */ + requisites?: string | undefined | null | Variable +}; + /** Статус строки расхода. */ +["ExpenseItemStatus"]:ExpenseItemStatus; + /** Способ оплаты строки расхода. */ +["ExpenseMechanics"]:ExpenseMechanics; + /** Смета расхода (СЗ). */ +["ExpenseProposal"]: AliasType<{ + /** Дата создания записи */ + _created_at?:boolean | `@${string}`, + /** Внутренний ID базы данных */ + _id?:boolean | `@${string}`, + /** Дата последнего обновления записи */ + _updated_at?:boolean | `@${string}`, + /** Номер блока крайней синхронизации с блокчейном */ + block_num?:boolean | `@${string}`, + /** Сырой статус из блокчейна (uint8). */ + blockchain_status?:boolean | `@${string}`, + /** Имя кооператива (scope). */ + coopname?:boolean | `@${string}`, + /** Время создания (chain). */ + created_at?:boolean | `@${string}`, + /** Решение совета о расходе (DocumentAggregate). */ + decision_doc?:ValueTypes["DocumentAggregate"], + /** ID в блокчейне. */ + id?:boolean | `@${string}`, + /** Строки сметы. */ + items?:ValueTypes["ExpenseItem"], + /** Флаг присутствия записи в блокчейне */ + present?:boolean | `@${string}`, + /** Хеш сметы расхода. */ + proposal_hash?:boolean | `@${string}`, + /** Кошелёк-источник средств. */ + source_wallet?:boolean | `@${string}`, + /** Заявление пайщика о расходе (DocumentAggregate). */ + statement_doc?:ValueTypes["DocumentAggregate"], + /** Доменный статус сметы. */ + status?:boolean | `@${string}`, + /** Сумма всех строк (факт). */ + total_actual?:boolean | `@${string}`, + /** Сумма всех строк (план). */ + total_planned?:boolean | `@${string}`, + /** Время последнего обновления (chain). */ + updated_at?:boolean | `@${string}`, + /** Создатель сметы (username). */ + username?:boolean | `@${string}`, + __typename?: boolean | `@${string}`, + ['...on ExpenseProposal']?: Omit +}>; + ["ExpenseProposalDecisionBodyInput"]: { + /** Род решения (approve / decline) */ + kind: string | Variable, + /** Причина отказа (для decline) */ + reason?: string | undefined | null | Variable +}; + ["ExpenseProposalDecisionGenerateDocumentInput"]: { + /** Номер блока, на котором был создан документ */ + block_num?: number | undefined | null | Variable, + /** Название кооператива, связанное с документом */ + coopname: string | Variable, + /** Дата и время создания документа */ + created_at?: string | undefined | null | Variable, + /** Идентификатор решения совета (повестка) — источник данных голосования */ + decision_id: number | Variable, + /** Имя генератора, использованного для создания документа */ + generator?: string | undefined | null | Variable, + /** Позиции расхода */ + items: Array | Variable, + /** Язык документа */ + lang?: string | undefined | null | Variable, + /** Ссылки, связанные с документом */ + links?: Array | undefined | null | Variable, + /** Шапка СЗ */ + proposal: ValueTypes["ExpenseProposalDecisionHeaderInput"] | Variable, + /** Хеш сметы расхода */ + proposal_hash: string | Variable, + /** Резолюция совета (утвердить / отказать) */ + resolution: ValueTypes["ExpenseProposalDecisionBodyInput"] | Variable, + /** Часовой пояс, в котором был создан документ */ + timezone?: string | undefined | null | Variable, + /** Название документа */ + title?: string | undefined | null | Variable, + /** Имя пользователя, создавшего документ */ + username: string | Variable, + /** Версия генератора, использованного для создания документа */ + version?: string | undefined | null | Variable +}; + ["ExpenseProposalDecisionHeaderInput"]: { + deadline?: string | undefined | null | Variable, + description: string | Variable, + fund_name?: string | undefined | null | Variable, + items_count: number | Variable, + source_wallet: string | Variable, + total_amount: string | Variable +}; + ["ExpenseProposalDecisionItemInput"]: { + amount: string | Variable, + description: string | Variable, + mechanics: string | Variable, + number: string | Variable, + recipient_type: string | Variable +}; + ["ExpenseProposalHeaderInput"]: { + /** Срок исполнения («в срок до»), формат DD.MM.YYYY */ + deadline?: string | undefined | null | Variable, + /** Описание цели расходов */ + description: string | Variable, + /** Фонд списания — подставляется сервером из параметров шасси расходов, передавать не нужно */ + fund_name?: string | undefined | null | Variable, + /** Количество позиций */ + items_count: number | Variable, + /** Кошелёк-источник */ + source_wallet: string | Variable, + /** Итоговая сумма расходов */ + total_amount: string | Variable +}; + ["ExpenseProposalItemInput"]: { + /** Сумма строки */ + amount: string | Variable, + /** Описание расхода */ + description: string | Variable, + /** Способ оплаты (ADVANCE / DIRECT) */ + mechanics: string | Variable, + /** Порядковый номер строки */ + number: string | Variable, + /** Идентификатор сохранённых реквизитов получателя-пайщика — сервер подставит полные реквизиты в документ. */ + payment_method_id?: string | undefined | null | Variable, + /** Назначение платежа — отдельной строкой после реквизитов */ + payment_purpose?: string | undefined | null | Variable, + /** Имя получателя */ + recipient_name?: string | undefined | null | Variable, + /** Тип получателя (SELF / MEMBER / ORG) */ + recipient_type: string | Variable, + /** Имя аккаунта получателя-пайщика (владелец реквизитов). */ + recipient_username?: string | undefined | null | Variable, + /** Реквизиты получателя */ + requisites?: string | undefined | null | Variable +}; + ["ExpenseProposalSignedItemInput"]: { + /** Сумма строки */ + amount: string | Variable, + /** Описание расхода */ + description: string | Variable, + /** Способ оплаты (ADVANCE / DIRECT) */ + mechanics: string | Variable, + /** Порядковый номер строки */ + number: string | Variable, + /** Тип получателя (SELF / MEMBER / ORG) */ + recipient_type: string | Variable +}; + ["ExpenseProposalStatementGenerateDocumentInput"]: { + /** Номер блока, на котором был создан документ */ + block_num?: number | undefined | null | Variable, + /** Название кооператива, связанное с документом */ + coopname: string | Variable, + /** Дата и время создания документа */ + created_at?: string | undefined | null | Variable, + /** Имя генератора, использованного для создания документа */ + generator?: string | undefined | null | Variable, + /** Позиции расхода */ + items: Array | Variable, + /** Язык документа */ + lang?: string | undefined | null | Variable, + /** Ссылки, связанные с документом */ + links?: Array | undefined | null | Variable, + /** Шапка СЗ */ + proposal: ValueTypes["ExpenseProposalHeaderInput"] | Variable, + /** Хеш сметы расхода (детерминированный) */ + proposal_hash: string | Variable, + /** Часовой пояс, в котором был создан документ */ + timezone?: string | undefined | null | Variable, + /** Название документа */ + title?: string | undefined | null | Variable, + /** Имя пользователя, создавшего документ */ + username: string | Variable, + /** Версия генератора, использованного для создания документа */ + version?: string | undefined | null | Variable +}; + ["ExpenseProposalStatementSignedDocumentInput"]: { + /** Хэш содержимого документа */ + doc_hash: string | Variable, + /** Общий хэш (doc_hash + meta_hash) */ + hash: string | Variable, + /** Метаинформация СЗ-заявления */ + meta: ValueTypes["ExpenseProposalStatementSignedMetaDocumentInput"] | Variable, + /** Хэш мета-данных */ + meta_hash: string | Variable, + /** Вектор подписей */ + signatures: Array | Variable, + /** Версия стандарта документа */ + version: string | Variable +}; + ["ExpenseProposalStatementSignedMetaDocumentInput"]: { + /** Номер блока, на котором был создан документ */ + block_num: number | Variable, + /** Название кооператива, связанное с документом */ + coopname: string | Variable, + /** Дата и время создания документа */ + created_at: string | Variable, + /** Идентификатор приватных данных документа off-chain (реквизиты/имя/назначение) */ + doc_data_hash: string | Variable, + /** Имя генератора, использованного для создания документа */ + generator: string | Variable, + /** Публичные позиции расхода (без реквизитов) */ + items: Array | Variable, + /** Язык документа */ + lang: string | Variable, + /** Ссылки, связанные с документом */ + links: Array | Variable, + /** Шапка СЗ */ + proposal: ValueTypes["ExpenseProposalHeaderInput"] | Variable, + /** Хеш сметы расхода (детерминированный) */ + proposal_hash: string | Variable, + /** ID документа в реестре */ + registry_id: number | Variable, + /** Часовой пояс, в котором был создан документ */ + timezone: string | Variable, + /** Название документа */ + title: string | Variable, + /** Имя пользователя, создавшего документ */ + username: string | Variable, + /** Версия генератора, использованного для создания документа */ + version: string | Variable +}; + /** Статус сметы расхода. */ +["ExpenseProposalStatus"]:ExpenseProposalStatus; + /** Тип получателя платежа. */ +["ExpenseRecipientType"]:ExpenseRecipientType; + /** Исход отчёта по строке-авансу: закрыто либо ожидается расчёт разницы (возврат/доплата). */ +["ExpenseReportOutcome"]:ExpenseReportOutcome; + ["ExpenseReportResult"]: AliasType<{ + /** Исход отчёта. */ + outcome?:boolean | `@${string}`, + /** Сумма разницы к расчёту (asset), при недо-/перерасходе. */ + settlement_amount?:boolean | `@${string}`, + /** Хэш заведённой платёжки расчёта (возврат/доплата). */ + settlement_payment_hash?:boolean | `@${string}`, + /** Транзакция закрытия позиции (только при CLOSED). */ + transaction?:ValueTypes["Transaction"], + __typename?: boolean | `@${string}`, + ['...on ExpenseReportResult']?: Omit +}>; + ["ExpenseRequisite"]: AliasType<{ + /** Имя кооператива. */ + coopname?:boolean | `@${string}`, + /** Снимок данных платёжного метода на момент подачи СЗ. */ + data?:boolean | `@${string}`, + /** Хеш строки расхода (item). */ + item_hash?:boolean | `@${string}`, + /** Тип платёжного метода пайщика (СБП / банковский перевод). */ + method_type?:boolean | `@${string}`, + /** Назначение платежа для поручения кассиру. */ + payment_purpose?:boolean | `@${string}`, + /** Хеш сметы расхода (proposal). */ + proposal_hash?:boolean | `@${string}`, + /** Получатель платежа (аккаунт пайщика или название организации). */ + recipient?:boolean | `@${string}`, + /** Реквизиты строкой — как в документе служебной записки. */ + requisites?:boolean | `@${string}`, + __typename?: boolean | `@${string}`, + ['...on ExpenseRequisite']?: Omit +}>; /** Статус расхода в системе CAPITAL */ ["ExpenseStatus"]:ExpenseStatus; /** Расширенное действие блокчейна с сертификатом пользователя, совершившего его. */ @@ -6422,125 +6832,6 @@ export type ValueTypes = { __typename?: boolean | `@${string}`, ['...on MeetQuestionResult']?: Omit }>; - ["MembershipExit"]: AliasType<{ - /** Дата подачи заявления на выход */ - created_at?:boolean | `@${string}`, - /** Хеш процесса выхода */ - exit_hash?:boolean | `@${string}`, - /** Сумма к возврату (фиксируется советом при одобрении; до одобрения — 0) */ - quantity?:boolean | `@${string}`, - /** Статус процесса выхода */ - status?:boolean | `@${string}`, - __typename?: boolean | `@${string}`, - ['...on MembershipExit']?: Omit -}>; - ["MembershipExitApplicationGenerateDocumentInput"]: { - /** Номер блока, на котором был создан документ */ - block_num?: number | undefined | null | Variable, - /** Название кооператива, связанное с документом */ - coopname: string | Variable, - /** Дата и время создания документа */ - created_at?: string | undefined | null | Variable, - /** Имя генератора, использованного для создания документа */ - generator?: string | undefined | null | Variable, - /** Язык документа */ - lang?: string | undefined | null | Variable, - /** Ссылки, связанные с документом */ - links?: Array | undefined | null | Variable, - /** Флаг пропуска сохранения документа (используется для предварительной генерации и демонстрации пользователю) */ - skip_save: boolean | Variable, - /** Часовой пояс, в котором был создан документ */ - timezone?: string | undefined | null | Variable, - /** Название документа */ - title?: string | undefined | null | Variable, - /** Имя пользователя, создавшего документ */ - username: string | Variable, - /** Версия генератора, использованного для создания документа */ - version?: string | undefined | null | Variable -}; - ["MembershipExitApplicationSignedDocumentInput"]: { - /** Хэш содержимого документа */ - doc_hash: string | Variable, - /** Общий хэш (doc_hash + meta_hash) */ - hash: string | Variable, - meta: ValueTypes["MembershipExitApplicationSignedMetaDocumentInput"] | Variable, - /** Хэш мета-данных */ - meta_hash: string | Variable, - /** Вектор подписей */ - signatures: Array | Variable, - /** Версия стандарта документа */ - version: string | Variable -}; - ["MembershipExitApplicationSignedMetaDocumentInput"]: { - /** Номер блока, на котором был создан документ */ - block_num: number | Variable, - /** Название кооператива, связанное с документом */ - coopname: string | Variable, - /** Дата и время создания документа */ - created_at: string | Variable, - /** Имя генератора, использованного для создания документа */ - generator: string | Variable, - /** Язык документа */ - lang: string | Variable, - /** Ссылки, связанные с документом */ - links: Array | Variable, - /** ID документа в реестре */ - registry_id: number | Variable, - /** Флаг пропуска сохранения документа (используется для предварительной генерации и демонстрации пользователю) */ - skip_save: boolean | Variable, - /** Часовой пояс, в котором был создан документ */ - timezone: string | Variable, - /** Название документа */ - title: string | Variable, - /** Имя пользователя, создавшего документ */ - username: string | Variable, - /** Версия генератора, использованного для создания документа */ - version: string | Variable -}; - ["MembershipExitDecisionGenerateDocumentInput"]: { - /** Номер блока, на котором был создан документ */ - block_num?: number | undefined | null | Variable, - /** Название кооператива, связанное с документом */ - coopname: string | Variable, - /** Дата и время создания документа */ - created_at?: string | undefined | null | Variable, - /** Идентификатор протокола решения собрания совета */ - decision_id: number | Variable, - /** Имя генератора, использованного для создания документа */ - generator?: string | undefined | null | Variable, - /** Язык документа */ - lang?: string | undefined | null | Variable, - /** Ссылки, связанные с документом */ - links?: Array | undefined | null | Variable, - /** Часовой пояс, в котором был создан документ */ - timezone?: string | undefined | null | Variable, - /** Название документа */ - title?: string | undefined | null | Variable, - /** Имя пользователя, создавшего документ */ - username: string | Variable, - /** Версия генератора, использованного для создания документа */ - version?: string | undefined | null | Variable -}; - ["MembershipExitResult"]: AliasType<{ - /** Хеш созданного процесса выхода */ - exit_hash?:boolean | `@${string}`, - /** Статус процесса выхода после подачи (ожидает подтверждения по ссылке из письма) */ - status?:boolean | `@${string}`, - __typename?: boolean | `@${string}`, - ['...on MembershipExitResult']?: Omit -}>; - ["MembershipExitReturnPreview"]: AliasType<{ - /** Минимальный паевой взнос пайщика */ - minimum_contribution?:boolean | `@${string}`, - /** Целевой паевой взнос пайщика */ - share_contribution?:boolean | `@${string}`, - /** Итоговая сумма к возврату (минимальный + целевой паевой) */ - total?:boolean | `@${string}`, - __typename?: boolean | `@${string}`, - ['...on MembershipExitReturnPreview']?: Omit -}>; - /** Статус процесса выхода пайщика из кооператива */ -["MembershipExitStatus"]:MembershipExitStatus; ["MissingRequisiteField"]: AliasType<{ key?:boolean | `@${string}`, label?:boolean | `@${string}`, @@ -6603,7 +6894,6 @@ addParticipant?: [{ data: ValueTypes["AddParticipantInput"] | Variable},ValueTypes["PaymentMethod"]], addTrustedAccount?: [{ data: ValueTypes["AddTrustedAccountInput"] | Variable},ValueTypes["Branch"]], authorizeDecision?: [{ data: ValueTypes["AuthorizeDecisionInput"] | Variable},ValueTypes["Transaction"]], -cancelMembershipExit?: [{ coopname: string | Variable, username: string | Variable},boolean | `@${string}`], cancelRequest?: [{ data: ValueTypes["CancelRequestInput"] | Variable},ValueTypes["Transaction"]], capitalAddAuthor?: [{ data: ValueTypes["AddAuthorInput"] | Variable},ValueTypes["CapitalProject"]], capitalApproveCommit?: [{ data: ValueTypes["CommitApproveInput"] | Variable},ValueTypes["CapitalCommit"]], @@ -6619,6 +6909,7 @@ capitalCreateDebt?: [{ data: ValueTypes["CreateDebtInput"] | Variable},ValueTypes["Transaction"]], capitalCreateIssue?: [{ data: ValueTypes["CreateIssueInput"] | Variable},ValueTypes["CapitalIssue"]], capitalCreateProcessTemplate?: [{ data: ValueTypes["CreateProcessTemplateInput"] | Variable},ValueTypes["ProcessTemplate"]], +capitalCreateProgramExpense?: [{ data: ValueTypes["CapitalCreateProgramExpenseInput"] | Variable},ValueTypes["Transaction"]], capitalCreateProgramInvest?: [{ data: ValueTypes["CreateProgramInvestInput"] | Variable},ValueTypes["Transaction"]], capitalCreateProgramProperty?: [{ data: ValueTypes["CreateProgramPropertyInput"] | Variable},ValueTypes["Transaction"]], capitalCreateProject?: [{ data: ValueTypes["CreateProjectInput"] | Variable},ValueTypes["Transaction"]], @@ -6676,6 +6967,7 @@ capitalStartProject?: [{ data: ValueTypes["StartProjectInput"] | Variable},ValueTypes["Transaction"]], capitalStopProject?: [{ data: ValueTypes["StopProjectInput"] | Variable},ValueTypes["CapitalProject"]], capitalSubmitVote?: [{ data: ValueTypes["SubmitVoteInput"] | Variable},ValueTypes["Transaction"]], +capitalTopupProgramExpensePool?: [{ data: ValueTypes["CapitalTopupProgramExpenseInput"] | Variable},ValueTypes["Transaction"]], capitalUpdateIssue?: [{ data: ValueTypes["UpdateIssueInput"] | Variable},ValueTypes["CapitalIssue"]], capitalUpdateProcessTemplate?: [{ data: ValueTypes["UpdateProcessTemplateInput"] | Variable},ValueTypes["ProcessTemplate"]], capitalUpdateStory?: [{ data: ValueTypes["UpdateStoryInput"] | Variable},ValueTypes["CapitalStory"]], @@ -6698,15 +6990,14 @@ completeChairmanGeneralMeetStep?: [{ data: ValueTypes["ChairmanOnboardingGeneral completeExtensionOnboardingStep?: [{ data: ValueTypes["CompleteExtensionOnboardingStepInput"] | Variable},ValueTypes["ExtensionOnboardingState"]], completeRequest?: [{ data: ValueTypes["CompleteRequestInput"] | Variable},ValueTypes["Transaction"]], confirmAgreement?: [{ data: ValueTypes["ConfirmAgreementInput"] | Variable},ValueTypes["Transaction"]], -confirmMembershipExit?: [{ token: string | Variable},ValueTypes["MembershipExitResult"]], confirmReceiveOnRequest?: [{ data: ValueTypes["ConfirmReceiveOnRequestInput"] | Variable},ValueTypes["Transaction"]], confirmSupplyOnRequest?: [{ data: ValueTypes["ConfirmSupplyOnRequestInput"] | Variable},ValueTypes["Transaction"]], createAnnualGeneralMeet?: [{ data: ValueTypes["CreateAnnualGeneralMeetInput"] | Variable},ValueTypes["MeetAggregate"]], createBranch?: [{ data: ValueTypes["CreateBranchInput"] | Variable},ValueTypes["Branch"]], createChildOrder?: [{ data: ValueTypes["CreateChildOrderInput"] | Variable},ValueTypes["Transaction"]], createDepositPayment?: [{ data: ValueTypes["CreateDepositPaymentInput"] | Variable},ValueTypes["GatewayPayment"]], +createExpenseProposal?: [{ data: ValueTypes["CreateExpenseProposalInput"] | Variable},ValueTypes["Transaction"]], createInitialPayment?: [{ data: ValueTypes["CreateInitialPaymentInput"] | Variable},ValueTypes["GatewayPayment"]], -createMembershipExit?: [{ data: ValueTypes["CreateMembershipExitInput"] | Variable},ValueTypes["MembershipExitResult"]], createParentOffer?: [{ data: ValueTypes["CreateParentOfferInput"] | Variable},ValueTypes["Transaction"]], createProjectOfFreeDecision?: [{ data: ValueTypes["CreateProjectFreeDecisionInput"] | Variable},ValueTypes["CreatedProjectFreeDecision"]], createWebPushSubscription?: [{ data: ValueTypes["CreateSubscriptionInput"] | Variable},ValueTypes["CreateSubscriptionResponse"]], @@ -6732,9 +7023,9 @@ generateAssetContributionStatement?: [{ data: ValueTypes["AssetContributionState generateBallotForAnnualGeneralMeetDocument?: [{ data: ValueTypes["AnnualGeneralMeetingVotingBallotGenerateDocumentInput"] | Variable, options?: ValueTypes["GenerateDocumentOptionsInput"] | undefined | null | Variable},ValueTypes["GeneratedDocument"]], generateConvertToAxonStatement?: [{ data: ValueTypes["ConvertToAxonStatementGenerateDocumentInput"] | Variable, options?: ValueTypes["GenerateDocumentOptionsInput"] | undefined | null | Variable},ValueTypes["GeneratedDocument"]], generateDocument?: [{ input: ValueTypes["GenerateAnyDocumentInput"] | Variable},ValueTypes["GeneratedDocument"]], +generateExpenseProposalDecisionDocument?: [{ data: ValueTypes["ExpenseProposalDecisionGenerateDocumentInput"] | Variable, options?: ValueTypes["GenerateDocumentOptionsInput"] | undefined | null | Variable},ValueTypes["GeneratedDocument"]], +generateExpenseProposalStatementDocument?: [{ data: ValueTypes["ExpenseProposalStatementGenerateDocumentInput"] | Variable, options?: ValueTypes["GenerateDocumentOptionsInput"] | undefined | null | Variable},ValueTypes["GeneratedDocument"]], generateFreeDecision?: [{ data: ValueTypes["FreeDecisionGenerateDocumentInput"] | Variable, options?: ValueTypes["GenerateDocumentOptionsInput"] | undefined | null | Variable},ValueTypes["GeneratedDocument"]], -generateMembershipExitApplication?: [{ data: ValueTypes["MembershipExitApplicationGenerateDocumentInput"] | Variable, options?: ValueTypes["GenerateDocumentOptionsInput"] | undefined | null | Variable},ValueTypes["GeneratedDocument"]], -generateMembershipExitDecision?: [{ data: ValueTypes["MembershipExitDecisionGenerateDocumentInput"] | Variable, options?: ValueTypes["GenerateDocumentOptionsInput"] | undefined | null | Variable},ValueTypes["GeneratedDocument"]], generateParticipantApplication?: [{ data: ValueTypes["ParticipantApplicationGenerateDocumentInput"] | Variable, options?: ValueTypes["GenerateDocumentOptionsInput"] | undefined | null | Variable},ValueTypes["GeneratedDocument"]], generateParticipantApplicationDecision?: [{ data: ValueTypes["ParticipantApplicationDecisionGenerateDocumentInput"] | Variable, options?: ValueTypes["GenerateDocumentOptionsInput"] | undefined | null | Variable},ValueTypes["GeneratedDocument"]], generatePrivacyAgreement?: [{ data: ValueTypes["GenerateDocumentInput"] | Variable, options?: ValueTypes["GenerateDocumentOptionsInput"] | undefined | null | Variable},ValueTypes["GeneratedDocument"]], @@ -6761,6 +7052,8 @@ markNotificationRead?: [{ id: string | Variable},ValueTypes["InboxN markReportPeriod?: [{ data: ValueTypes["MarkReportPeriodInput"] | Variable},boolean | `@${string}`], moderateRequest?: [{ data: ValueTypes["ModerateRequestInput"] | Variable},ValueTypes["Transaction"]], notifyOnAnnualGeneralMeet?: [{ data: ValueTypes["NotifyOnAnnualGeneralMeetInput"] | Variable},ValueTypes["MeetAggregate"]], +overspendExpenseItem?: [{ data: ValueTypes["OverspendExpenseItemInput"] | Variable},ValueTypes["Transaction"]], +payExpenseItem?: [{ data: ValueTypes["PayExpenseItemInput"] | Variable},ValueTypes["Transaction"]], processConvertToAxonStatement?: [{ data: ValueTypes["ProcessConvertToAxonStatementInput"] | Variable},boolean | `@${string}`], prohibitRequest?: [{ data: ValueTypes["ProhibitRequestInput"] | Variable},ValueTypes["Transaction"]], publishProjectOfFreeDecision?: [{ data: ValueTypes["PublishProjectFreeDecisionInput"] | Variable},ValueTypes["AgendaWithDocuments"]], @@ -6769,11 +7062,13 @@ receiveOnRequest?: [{ data: ValueTypes["ReceiveOnRequestInput"] | Variable},ValueTypes["RegisteredAccount"]], registerAccount?: [{ data: ValueTypes["RegisterAccountInput"] | Variable},ValueTypes["RegisteredAccount"]], registerParticipant?: [{ data: ValueTypes["RegisterParticipantInput"] | Variable},ValueTypes["Account"]], +reportExpenseItem?: [{ data: ValueTypes["ReportExpenseItemInput"] | Variable},ValueTypes["ExpenseReportResult"]], resendNotification?: [{ id: string | Variable},ValueTypes["Notification"]], resetKey?: [{ data: ValueTypes["ResetKeyInput"] | Variable},boolean | `@${string}`], /** Откатить собственную незавершённую регистрацию к редактированию данных: снимает заморозку профиля и e-mail, сбрасывает подписанное заявление и непринятую попытку вступительного платежа. Доступно только до отправки регистрации в блокчейн; если взнос уже принят — требуется возврат средств. */ resetRegistration?:ValueTypes["Account"], restartAnnualGeneralMeet?: [{ data: ValueTypes["RestartAnnualGeneralMeetInput"] | Variable},ValueTypes["MeetAggregate"]], +returnExpenseItem?: [{ data: ValueTypes["ReturnExpenseItemInput"] | Variable},ValueTypes["Transaction"]], saveReportDraft?: [{ input: ValueTypes["SaveReportDraftInput"] | Variable},ValueTypes["ReportDraft"]], selectBranch?: [{ data: ValueTypes["SelectBranchInput"] | Variable},boolean | `@${string}`], sendAgreement?: [{ data: ValueTypes["SendAgreementInput"] | Variable},ValueTypes["Transaction"]], @@ -6783,6 +7078,7 @@ signByPresiderOnAnnualGeneralMeet?: [{ data: ValueTypes["SignByPresiderOnAnnualG signBySecretaryOnAnnualGeneralMeet?: [{ data: ValueTypes["SignBySecretaryOnAnnualGeneralMeetInput"] | Variable},ValueTypes["MeetAggregate"]], startInstall?: [{ data: ValueTypes["StartInstallInput"] | Variable},ValueTypes["StartInstallResult"]], startResetKey?: [{ data: ValueTypes["StartResetKeyInput"] | Variable},boolean | `@${string}`], +submitExpenseReport?: [{ data: ValueTypes["SubmitExpenseReportInput"] | Variable},ValueTypes["Transaction"]], supplyOnRequest?: [{ data: ValueTypes["SupplyOnRequestInput"] | Variable},ValueTypes["Transaction"]], triggerNotificationWorkflow?: [{ data: ValueTypes["TriggerNotificationWorkflowInput"] | Variable},boolean | `@${string}`], uninstallExtension?: [{ data: ValueTypes["UninstallExtensionInput"] | Variable},boolean | `@${string}`], @@ -6794,6 +7090,7 @@ updateReportRequisites?: [{ input: ValueTypes["UpdateReportRequisitesInput"] | V updateRequest?: [{ data: ValueTypes["UpdateRequestInput"] | Variable},ValueTypes["Transaction"]], updateSettings?: [{ data: ValueTypes["UpdateSettingsInput"] | Variable},ValueTypes["Settings"]], updateSystem?: [{ data: ValueTypes["Update"] | Variable},ValueTypes["SystemInfo"]], +uploadExpenseFile?: [{ data: ValueTypes["UploadExpenseFileInput"] | Variable},ValueTypes["ExpenseFile"]], verifyEmail?: [{ data: ValueTypes["VerifyEmailInputDTO"] | Variable},boolean | `@${string}`], voteOnAnnualGeneralMeet?: [{ data: ValueTypes["VoteOnAnnualGeneralMeetInput"] | Variable},ValueTypes["MeetAggregate"]], walmoveWallets?: [{ input: ValueTypes["WalmoveInput"] | Variable},ValueTypes["Ledger2AdjustmentResult"]], @@ -7037,6 +7334,16 @@ walmoveWallets?: [{ input: ValueTypes["WalmoveInput"] | Variable},V __typename?: boolean | `@${string}`, ['...on OrganizationWithBankAccount']?: Omit }>; + ["OverspendExpenseItemInput"]: { + /** Имя кооператива. */ + coopname: string | Variable, + /** Хеш строки расхода (item). */ + item_hash: string | Variable, + /** Сумма доплаты сверх planned_amount (asset, например "200.0000 RUB"). */ + overspend_amount: string | Variable, + /** Хеш сметы расхода (proposal). */ + proposal_hash: string | Variable +}; ["PaginatedActionsPaginationResult"]: AliasType<{ /** Текущая страница */ currentPage?:boolean | `@${string}`, @@ -7180,6 +7487,18 @@ walmoveWallets?: [{ input: ValueTypes["WalmoveInput"] | Variable},V totalPages?:boolean | `@${string}`, __typename?: boolean | `@${string}`, ['...on PaginatedCapitalLogsPaginationResult']?: Omit +}>; + ["PaginatedCapitalProgramExpensesPaginationResult"]: AliasType<{ + /** Текущая страница */ + currentPage?:boolean | `@${string}`, + /** Элементы текущей страницы */ + items?:ValueTypes["CapitalProgramExpense"], + /** Общее количество элементов */ + totalCount?:boolean | `@${string}`, + /** Общее количество страниц */ + totalPages?:boolean | `@${string}`, + __typename?: boolean | `@${string}`, + ['...on PaginatedCapitalProgramExpensesPaginationResult']?: Omit }>; ["PaginatedCapitalProjectsPaginationResult"]: AliasType<{ /** Текущая страница */ @@ -7300,6 +7619,18 @@ walmoveWallets?: [{ input: ValueTypes["WalmoveInput"] | Variable},V totalPages?:boolean | `@${string}`, __typename?: boolean | `@${string}`, ['...on PaginatedDeltasPaginationResult']?: Omit +}>; + ["PaginatedExpenseProposalsPaginationResult"]: AliasType<{ + /** Текущая страница */ + currentPage?:boolean | `@${string}`, + /** Элементы текущей страницы */ + items?:ValueTypes["ExpenseProposal"], + /** Общее количество элементов */ + totalCount?:boolean | `@${string}`, + /** Общее количество страниц */ + totalPages?:boolean | `@${string}`, + __typename?: boolean | `@${string}`, + ['...on PaginatedExpenseProposalsPaginationResult']?: Omit }>; ["PaginatedGatewayPaymentsPaginationResult"]: AliasType<{ /** Текущая страница */ @@ -7466,6 +7797,16 @@ walmoveWallets?: [{ input: ValueTypes["WalmoveInput"] | Variable},V issued_by: string | Variable, number: number | Variable, series: number | Variable +}; + ["PayExpenseItemInput"]: { + /** Фактическая сумма оплаты (asset, например "100.0000 RUB"). */ + actual_amount: string | Variable, + /** Имя кооператива. */ + coopname: string | Variable, + /** Хеш строки расхода (item). */ + item_hash: string | Variable, + /** Хеш сметы расхода (proposal). */ + proposal_hash: string | Variable }; ["PaymentDetails"]: AliasType<{ /** Сумма платежа с учетом комиссии */ @@ -7494,6 +7835,8 @@ walmoveWallets?: [{ input: ValueTypes["WalmoveInput"] | Variable},V direction?: ValueTypes["PaymentDirection"] | undefined | null | Variable, /** Хэш платежа */ hash?: string | undefined | null | Variable, + /** Хэш расхода (служебной записки): вернёт все платежи, связанные с этим расходом */ + proposal_hash?: string | undefined | null | Variable, /** Провайдер платежа */ provider?: string | undefined | null | Variable, /** Статус платежа */ @@ -8076,6 +8419,8 @@ capitalInvest?: [{ data: ValueTypes["GetInvestInput"] | Variable},V capitalInvests?: [{ filter?: ValueTypes["CapitalInvestFilter"] | undefined | null | Variable, options?: ValueTypes["PaginationInput"] | undefined | null | Variable},ValueTypes["PaginatedCapitalInvestsPaginationResult"]], capitalIssue?: [{ data: ValueTypes["GetCapitalIssueByHashInput"] | Variable},ValueTypes["CapitalIssue"]], capitalIssues?: [{ filter?: ValueTypes["CapitalIssueFilter"] | undefined | null | Variable, options?: ValueTypes["PaginationInput"] | undefined | null | Variable},ValueTypes["PaginatedCapitalIssuesPaginationResult"]], +capitalProgramExpense?: [{ coopname: string | Variable, expense_hash: string | Variable},ValueTypes["CapitalProgramExpense"]], +capitalProgramExpenses?: [{ coopname: string | Variable, options?: ValueTypes["PaginationInput"] | undefined | null | Variable},ValueTypes["PaginatedCapitalProgramExpensesPaginationResult"]], capitalProject?: [{ data: ValueTypes["GetProjectInput"] | Variable},ValueTypes["CapitalProject"]], capitalProjectWithRelations?: [{ data: ValueTypes["GetProjectWithRelationsInput"] | Variable},ValueTypes["CapitalProject"]], capitalProjects?: [{ filter?: ValueTypes["CapitalProjectFilter"] | undefined | null | Variable, options?: ValueTypes["PaginationInput"] | undefined | null | Variable},ValueTypes["PaginatedCapitalProjectsPaginationResult"]], @@ -8123,6 +8468,13 @@ chatcoopListUtcDatesWithNewRoomMessages?: [{ data: ValueTypes["ListUtcDatesWithN checkReportReadiness?: [{ reportType: ValueTypes["ReportType"] | Variable},ValueTypes["ReportReadinessView"]], cooperativeAgreements?: [{ coopname: string | Variable},ValueTypes["CoopAgreement"]], cooperativePrograms?: [{ coopname: string | Variable},ValueTypes["CooperativeProgram"]], +expenseFile?: [{ id: number | Variable},ValueTypes["ExpenseFile"]], +expenseFilesByItem?: [{ coopname: string | Variable, item_hash: string | Variable, proposal_hash: string | Variable},ValueTypes["ExpenseFile"]], +expenseFilesByProposal?: [{ coopname: string | Variable, proposal_hash: string | Variable},ValueTypes["ExpenseFile"]], +expenseProposal?: [{ proposal_hash: string | Variable},ValueTypes["ExpenseProposal"]], +expenseProposalsByCooperative?: [{ coopname: string | Variable, options?: ValueTypes["PaginationInput"] | undefined | null | Variable},ValueTypes["PaginatedExpenseProposalsPaginationResult"]], +expenseProposalsByMember?: [{ coopname: string | Variable, options?: ValueTypes["PaginationInput"] | undefined | null | Variable, username: string | Variable},ValueTypes["PaginatedExpenseProposalsPaginationResult"]], +expenseRequisitesByProposal?: [{ coopname: string | Variable, proposal_hash: string | Variable},ValueTypes["ExpenseRequisite"]], getAccount?: [{ data: ValueTypes["GetAccountInput"] | Variable},ValueTypes["Account"]], getAccounts?: [{ data?: ValueTypes["GetAccountsInput"] | undefined | null | Variable, options?: ValueTypes["PaginationInput"] | undefined | null | Variable},ValueTypes["AccountsPaginationResult"]], getActions?: [{ filters?: ValueTypes["ActionFiltersInput"] | undefined | null | Variable, pagination?: ValueTypes["PaginationInput"] | undefined | null | Variable},ValueTypes["PaginatedActionsPaginationResult"]], @@ -8198,8 +8550,6 @@ getUserWebPushSubscriptions?: [{ data: ValueTypes["GetUserSubscriptionsInput"] | Требуемые роли: chairman. */ getWebPushSubscriptionStats?:ValueTypes["SubscriptionStatsDto"], listReportDrafts?: [{ filter?: ValueTypes["ListReportDraftsFilterInput"] | undefined | null | Variable},ValueTypes["ReportDraft"]], -membershipExit?: [{ coopname: string | Variable, username: string | Variable},ValueTypes["MembershipExit"]], -membershipExitReturnPreview?: [{ coopname: string | Variable, username: string | Variable},ValueTypes["MembershipExitReturnPreview"]], onecoopGetDocuments?: [{ data: ValueTypes["GetOneCoopDocumentsInput"] | Variable},ValueTypes["OneCoopDocumentsResponse"]], process?: [{ coopname: string | Variable, hash: string | Variable},ValueTypes["ProcessView"]], processes?: [{ filter: ValueTypes["ProcessesFilter"] | Variable, pagination: ValueTypes["PaginationInput"] | Variable},ValueTypes["ProcessSummaryPaginationResult"]], @@ -8446,6 +8796,16 @@ validateReportEdits?: [{ editsJson: string | Variable, reportType: __typename?: boolean | `@${string}`, ['...on ReportDraft']?: Omit }>; + ["ReportExpenseItemInput"]: { + /** Фактически потраченная сумма по чекам (asset, например "800.0000 RUB"). Не указана — равна выданному авансу. */ + actual_amount?: string | undefined | null | Variable, + /** Имя кооператива. */ + coopname: string | Variable, + /** Хеш строки расхода (item). */ + item_hash: string | Variable, + /** Хеш сметы расхода (proposal). */ + proposal_hash: string | Variable +}; ["ReportHistoryFilterInput"]: { /** Лимит (макс 100, по умолчанию 20) */ limit?: number | undefined | null | Variable, @@ -8914,6 +9274,16 @@ validateReportEdits?: [{ editsJson: string | Variable, reportType: username: string | Variable, /** Версия генератора, использованного для создания документа */ version: string | Variable +}; + ["ReturnExpenseItemInput"]: { + /** Имя кооператива. */ + coopname: string | Variable, + /** Хеш строки расхода (item). */ + item_hash: string | Variable, + /** Хеш сметы расхода (proposal). */ + proposal_hash: string | Variable, + /** Возвращаемая сумма (asset, например "50.0000 RUB"). */ + return_amount: string | Variable }; /** Тип сообщения в истории комнаты Matrix (текст или расшифрованное аудио) */ ["RoomMessageKind"]:RoomMessageKind; @@ -9301,6 +9671,16 @@ validateReportEdits?: [{ editsJson: string | Variable, reportType: }; /** Статус истории в системе CAPITAL */ ["StoryStatus"]:StoryStatus; + ["SubmitExpenseReportInput"]: { + /** Комментарий пайщика к финализации отчёта (свободный текст, до 1000 символов). */ + comment?: string | undefined | null | Variable, + /** Имя кооператива. */ + coopname: string | Variable, + /** Хеш сметы расхода. */ + proposal_hash: string | Variable, + /** Итоговая сумма фактических расходов (asset, например "1500.0000 RUB"). */ + total_actual_amount?: string | undefined | null | Variable +}; ["SubmitVoteInput"]: { /** Имя аккаунта кооператива */ coopname: string | Variable, @@ -9676,6 +10056,26 @@ validateReportEdits?: [{ editsJson: string | Variable, reportType: story_hash: string | Variable, /** Название истории */ title?: string | undefined | null | Variable +}; + ["UploadExpenseFileInput"]: { + /** SHA-256 содержимого, hex-lowercase (64 hex-символа). */ + checksum_sha256: string | Variable, + /** Содержимое файла, base64 без префикса data:. */ + content_base64: string | Variable, + /** Имя кооператива. */ + coopname: string | Variable, + /** Хеш строки расхода (пусто — файл уровня сметы). */ + item_hash?: string | undefined | null | Variable, + /** Назначение файла (платёжка/чек/возврат). */ + kind: ValueTypes["ExpenseFileKind"] | Variable, + /** MIME-тип содержимого. */ + mime_type: string | Variable, + /** Оригинальное имя файла — для отображения и поиска. */ + original_filename?: string | undefined | null | Variable, + /** Хеш сметы расхода. */ + proposal_hash: string | Variable, + /** Размер файла в байтах (для серверной валидации). */ + size_bytes: number | Variable }; ["UserAccount"]: AliasType<{ /** Метаинформация */ @@ -11388,6 +11788,20 @@ export type ResolverInputTypes = { status?: ResolverInputTypes["ContributorStatus"] | undefined | null, /** Фильтр по имени пользователя */ username?: string | undefined | null +}; + ["CapitalCreateProgramExpenseInput"]: { + /** Имя кооператива. */ + coopname: string, + /** Имя пайщика-создателя СЗ (председатель). */ + creator: string, + /** Описание программного расхода. */ + description: string, + /** Хэш СЗ-расхода (детерминированный, из UI). Он же станет proposal_hash в шасси. */ + expense_hash: string, + /** Строки расхода. Способ оплаты (аванс под отчёт / оплата по счёту) задаётся на каждой строке отдельно. */ + items: Array, + /** Подписанная СЗ-смета (document2, registry 2010). */ + statement: ResolverInputTypes["ExpenseProposalStatementSignedDocumentInput"] }; /** Цикл разработки в системе CAPITAL */ ["CapitalCycle"]: AliasType<{ @@ -11759,6 +12173,41 @@ export type ResolverInputTypes = { step: ResolverInputTypes["CapitalOnboardingStep"], title?: string | undefined | null }; + ["CapitalProgramExpense"]: AliasType<{ + callback?:ResolverInputTypes["CapitalProgramExpenseCallback"], + coopname?:boolean | `@${string}`, + created_at?:boolean | `@${string}`, + creator?:boolean | `@${string}`, + /** Имя инициатора (ФИО пайщика или название организации) */ + creator_name?:boolean | `@${string}`, + expense_hash?:boolean | `@${string}`, + items?:ResolverInputTypes["CapitalProgramExpenseItem"], + source_wallet?:boolean | `@${string}`, + status?:boolean | `@${string}`, + total_actual?:boolean | `@${string}`, + total_planned?:boolean | `@${string}`, + updated_at?:boolean | `@${string}`, + __typename?: boolean | `@${string}` +}>; + ["CapitalProgramExpenseCallback"]: AliasType<{ + action?:boolean | `@${string}`, + contract?:boolean | `@${string}`, + data?:boolean | `@${string}`, + __typename?: boolean | `@${string}` +}>; + ["CapitalProgramExpenseItem"]: AliasType<{ + actual_amount?:boolean | `@${string}`, + description?:boolean | `@${string}`, + item_hash?:boolean | `@${string}`, + mechanics?:boolean | `@${string}`, + planned_amount?:boolean | `@${string}`, + recipient?:boolean | `@${string}`, + /** Имя получателя (ФИО пайщика или название организации) */ + recipient_name?:boolean | `@${string}`, + recipient_type?:boolean | `@${string}`, + status?:boolean | `@${string}`, + __typename?: boolean | `@${string}` +}>; /** Проект в системе CAPITAL с компонентами */ ["CapitalProject"]: AliasType<{ /** Дата создания записи */ @@ -12325,6 +12774,10 @@ export type ResolverInputTypes = { global_available_invest_pool?:boolean | `@${string}`, /** Флаг присутствия записи в блокчейне */ present?:boolean | `@${string}`, + /** Пул программных расходов — средства, переведённые под целевые расходы программы */ + program_expense_pool?:boolean | `@${string}`, + /** Зарезервировано в пуле программных расходов под активные расходы (служебные записки в работе) */ + program_expense_reserved?:boolean | `@${string}`, /** Доступная сумма членских взносов по программе */ program_membership_available?:boolean | `@${string}`, /** Накопительное вознаграждение на долю в членских взносах */ @@ -12485,6 +12938,12 @@ export type ResolverInputTypes = { project_hash?: string | undefined | null, /** Имя пользователя (опционально) */ username?: string | undefined | null +}; + ["CapitalTopupProgramExpenseInput"]: { + /** Сумма пополнения (asset, eg "10000.0000 RUB"). */ + amount: string, + /** Имя кооператива. */ + coopname: string }; /** Голос в системе CAPITAL */ ["CapitalVote"]: AliasType<{ @@ -13151,6 +13610,22 @@ export type ResolverInputTypes = { project_hash: string, /** Служебная записка о расходе */ statement: ResolverInputTypes["SignedDigitalDocumentInput"] +}; + ["CreateExpenseProposalInput"]: { + /** Callback на финализацию closeexp (опционально). */ + callback?: ResolverInputTypes["ExpenseCallbackInput"] | undefined | null, + /** Имя кооператива. */ + coopname: string, + /** Строки расхода (массив items). */ + items: Array, + /** Хеш сметы расхода (детерминированный, из UI). */ + proposal_hash: string, + /** Источник средств (eosio::name кошелька-источника, eg "w.cap.blago"). */ + source_wallet: string, + /** Подписанная СЗ-смета (document2, registry 2010). */ + statement: ResolverInputTypes["ExpenseProposalStatementSignedDocumentInput"], + /** Имя пайщика-создателя СЗ. */ + username: string }; ["CreateIndividualDataInput"]: { /** Дата рождения */ @@ -13229,16 +13704,6 @@ export type ResolverInputTypes = { ["CreateMatrixAccountInputDTO"]: { password: string, username: string -}; - ["CreateMembershipExitInput"]: { - /** Имя аккаунта кооператива */ - coopname: string, - /** Хеш процесса выхода (генерируется на клиенте) */ - exit_hash: string, - /** Подписанное пайщиком заявление о выходе из кооператива */ - statement: ResolverInputTypes["MembershipExitApplicationSignedDocumentInput"], - /** Имя пайщика, выходящего из кооператива */ - username: string }; ["CreateOrganizationDataInput"]: { /** Банковский счет организации */ @@ -13833,6 +14298,46 @@ export type ResolverInputTypes = { /** ОГРН */ ogrn: string }; + ["ExpenseCallbackInput"]: { + /** Action-метод */ + action?: string | undefined | null, + /** Контракт-целевой */ + contract?: string | undefined | null, + /** Payload (hex bytes) */ + data?: string | undefined | null +}; + /** Запись о первичном файле расхода (платёжка/чек/возврат). */ +["ExpenseFile"]: AliasType<{ + /** SHA-256 содержимого, hex-lowercase. */ + checksum_sha256?:boolean | `@${string}`, + /** Имя кооператива (scope). */ + coopname?:boolean | `@${string}`, + /** Внутренний ID записи. */ + id?:boolean | `@${string}`, + /** Хеш строки расхода (если файл уровня item). */ + item_hash?:boolean | `@${string}`, + /** Назначение файла. */ + kind?:boolean | `@${string}`, + /** MIME-тип содержимого. */ + mime_type?:boolean | `@${string}`, + /** Оригинальное имя загруженного файла. */ + original_filename?:boolean | `@${string}`, + /** Хеш сметы расхода. */ + proposal_hash?:boolean | `@${string}`, + /** Короткоживущий URL на скачивание (HMAC-signed). */ + read_url?:boolean | `@${string}`, + /** Размер файла в байтах. */ + size_bytes?:boolean | `@${string}`, + /** MinIO-ключ внутри бакета. */ + storage_key?:boolean | `@${string}`, + /** Когда загружено. */ + uploaded_at?:boolean | `@${string}`, + /** Кто загрузил (username). */ + uploaded_by_username?:boolean | `@${string}`, + __typename?: boolean | `@${string}` +}>; + /** Тип первичного файла расхода. */ +["ExpenseFileKind"]:ExpenseFileKind; ["ExpenseFilter"]: { /** Фильтр по ID фонда */ fundId?: string | undefined | null, @@ -13843,6 +14348,303 @@ export type ResolverInputTypes = { /** Фильтр по имени пользователя */ username?: string | undefined | null }; + /** Строка сметы расхода. */ +["ExpenseItem"]: AliasType<{ + /** Фактическая сумма (после оплаты/отчёта). */ + actual_amount?:boolean | `@${string}`, + /** Назначение/описание строки. */ + description?:boolean | `@${string}`, + /** Хеш строки расхода. */ + item_hash?:boolean | `@${string}`, + /** Способ оплаты. */ + mechanics?:boolean | `@${string}`, + /** Планируемая сумма. */ + planned_amount?:boolean | `@${string}`, + /** Идентификатор получателя. */ + recipient?:boolean | `@${string}`, + /** Тип получателя платежа. */ + recipient_type?:boolean | `@${string}`, + /** Статус строки. */ + status?:boolean | `@${string}`, + __typename?: boolean | `@${string}` +}>; + ["ExpenseItemInput"]: { + /** Описание назначения расхода. */ + description: string, + /** Хеш строки расхода (детерминированный, из UI). */ + item_hash: string, + /** Способ оплаты (ADVANCE / DIRECT). */ + mechanics: ResolverInputTypes["ExpenseMechanics"], + /** Идентификатор сохранённых реквизитов получателя-пайщика — реквизиты снимаются в момент создания и прикладываются к платежу. */ + payment_method_id?: string | undefined | null, + /** Назначение платежа (для оплаты по счёту) — фиксируется в снимке для кассира. */ + payment_purpose?: string | undefined | null, + /** Планируемая сумма (asset, eg "1000.0000 RUB"). */ + planned_amount: string, + /** Получатель: username пайщика; для организации — пустая строка (аккаунта в кооперативе нет). */ + recipient: string, + /** Тип получателя. */ + recipient_type: ResolverInputTypes["ExpenseRecipientType"], + /** Реквизиты получателя-организации (вводятся вручную). */ + requisites?: string | undefined | null +}; + /** Статус строки расхода. */ +["ExpenseItemStatus"]:ExpenseItemStatus; + /** Способ оплаты строки расхода. */ +["ExpenseMechanics"]:ExpenseMechanics; + /** Смета расхода (СЗ). */ +["ExpenseProposal"]: AliasType<{ + /** Дата создания записи */ + _created_at?:boolean | `@${string}`, + /** Внутренний ID базы данных */ + _id?:boolean | `@${string}`, + /** Дата последнего обновления записи */ + _updated_at?:boolean | `@${string}`, + /** Номер блока крайней синхронизации с блокчейном */ + block_num?:boolean | `@${string}`, + /** Сырой статус из блокчейна (uint8). */ + blockchain_status?:boolean | `@${string}`, + /** Имя кооператива (scope). */ + coopname?:boolean | `@${string}`, + /** Время создания (chain). */ + created_at?:boolean | `@${string}`, + /** Решение совета о расходе (DocumentAggregate). */ + decision_doc?:ResolverInputTypes["DocumentAggregate"], + /** ID в блокчейне. */ + id?:boolean | `@${string}`, + /** Строки сметы. */ + items?:ResolverInputTypes["ExpenseItem"], + /** Флаг присутствия записи в блокчейне */ + present?:boolean | `@${string}`, + /** Хеш сметы расхода. */ + proposal_hash?:boolean | `@${string}`, + /** Кошелёк-источник средств. */ + source_wallet?:boolean | `@${string}`, + /** Заявление пайщика о расходе (DocumentAggregate). */ + statement_doc?:ResolverInputTypes["DocumentAggregate"], + /** Доменный статус сметы. */ + status?:boolean | `@${string}`, + /** Сумма всех строк (факт). */ + total_actual?:boolean | `@${string}`, + /** Сумма всех строк (план). */ + total_planned?:boolean | `@${string}`, + /** Время последнего обновления (chain). */ + updated_at?:boolean | `@${string}`, + /** Создатель сметы (username). */ + username?:boolean | `@${string}`, + __typename?: boolean | `@${string}` +}>; + ["ExpenseProposalDecisionBodyInput"]: { + /** Род решения (approve / decline) */ + kind: string, + /** Причина отказа (для decline) */ + reason?: string | undefined | null +}; + ["ExpenseProposalDecisionGenerateDocumentInput"]: { + /** Номер блока, на котором был создан документ */ + block_num?: number | undefined | null, + /** Название кооператива, связанное с документом */ + coopname: string, + /** Дата и время создания документа */ + created_at?: string | undefined | null, + /** Идентификатор решения совета (повестка) — источник данных голосования */ + decision_id: number, + /** Имя генератора, использованного для создания документа */ + generator?: string | undefined | null, + /** Позиции расхода */ + items: Array, + /** Язык документа */ + lang?: string | undefined | null, + /** Ссылки, связанные с документом */ + links?: Array | undefined | null, + /** Шапка СЗ */ + proposal: ResolverInputTypes["ExpenseProposalDecisionHeaderInput"], + /** Хеш сметы расхода */ + proposal_hash: string, + /** Резолюция совета (утвердить / отказать) */ + resolution: ResolverInputTypes["ExpenseProposalDecisionBodyInput"], + /** Часовой пояс, в котором был создан документ */ + timezone?: string | undefined | null, + /** Название документа */ + title?: string | undefined | null, + /** Имя пользователя, создавшего документ */ + username: string, + /** Версия генератора, использованного для создания документа */ + version?: string | undefined | null +}; + ["ExpenseProposalDecisionHeaderInput"]: { + deadline?: string | undefined | null, + description: string, + fund_name?: string | undefined | null, + items_count: number, + source_wallet: string, + total_amount: string +}; + ["ExpenseProposalDecisionItemInput"]: { + amount: string, + description: string, + mechanics: string, + number: string, + recipient_type: string +}; + ["ExpenseProposalHeaderInput"]: { + /** Срок исполнения («в срок до»), формат DD.MM.YYYY */ + deadline?: string | undefined | null, + /** Описание цели расходов */ + description: string, + /** Фонд списания — подставляется сервером из параметров шасси расходов, передавать не нужно */ + fund_name?: string | undefined | null, + /** Количество позиций */ + items_count: number, + /** Кошелёк-источник */ + source_wallet: string, + /** Итоговая сумма расходов */ + total_amount: string +}; + ["ExpenseProposalItemInput"]: { + /** Сумма строки */ + amount: string, + /** Описание расхода */ + description: string, + /** Способ оплаты (ADVANCE / DIRECT) */ + mechanics: string, + /** Порядковый номер строки */ + number: string, + /** Идентификатор сохранённых реквизитов получателя-пайщика — сервер подставит полные реквизиты в документ. */ + payment_method_id?: string | undefined | null, + /** Назначение платежа — отдельной строкой после реквизитов */ + payment_purpose?: string | undefined | null, + /** Имя получателя */ + recipient_name?: string | undefined | null, + /** Тип получателя (SELF / MEMBER / ORG) */ + recipient_type: string, + /** Имя аккаунта получателя-пайщика (владелец реквизитов). */ + recipient_username?: string | undefined | null, + /** Реквизиты получателя */ + requisites?: string | undefined | null +}; + ["ExpenseProposalSignedItemInput"]: { + /** Сумма строки */ + amount: string, + /** Описание расхода */ + description: string, + /** Способ оплаты (ADVANCE / DIRECT) */ + mechanics: string, + /** Порядковый номер строки */ + number: string, + /** Тип получателя (SELF / MEMBER / ORG) */ + recipient_type: string +}; + ["ExpenseProposalStatementGenerateDocumentInput"]: { + /** Номер блока, на котором был создан документ */ + block_num?: number | undefined | null, + /** Название кооператива, связанное с документом */ + coopname: string, + /** Дата и время создания документа */ + created_at?: string | undefined | null, + /** Имя генератора, использованного для создания документа */ + generator?: string | undefined | null, + /** Позиции расхода */ + items: Array, + /** Язык документа */ + lang?: string | undefined | null, + /** Ссылки, связанные с документом */ + links?: Array | undefined | null, + /** Шапка СЗ */ + proposal: ResolverInputTypes["ExpenseProposalHeaderInput"], + /** Хеш сметы расхода (детерминированный) */ + proposal_hash: string, + /** Часовой пояс, в котором был создан документ */ + timezone?: string | undefined | null, + /** Название документа */ + title?: string | undefined | null, + /** Имя пользователя, создавшего документ */ + username: string, + /** Версия генератора, использованного для создания документа */ + version?: string | undefined | null +}; + ["ExpenseProposalStatementSignedDocumentInput"]: { + /** Хэш содержимого документа */ + doc_hash: string, + /** Общий хэш (doc_hash + meta_hash) */ + hash: string, + /** Метаинформация СЗ-заявления */ + meta: ResolverInputTypes["ExpenseProposalStatementSignedMetaDocumentInput"], + /** Хэш мета-данных */ + meta_hash: string, + /** Вектор подписей */ + signatures: Array, + /** Версия стандарта документа */ + version: string +}; + ["ExpenseProposalStatementSignedMetaDocumentInput"]: { + /** Номер блока, на котором был создан документ */ + block_num: number, + /** Название кооператива, связанное с документом */ + coopname: string, + /** Дата и время создания документа */ + created_at: string, + /** Идентификатор приватных данных документа off-chain (реквизиты/имя/назначение) */ + doc_data_hash: string, + /** Имя генератора, использованного для создания документа */ + generator: string, + /** Публичные позиции расхода (без реквизитов) */ + items: Array, + /** Язык документа */ + lang: string, + /** Ссылки, связанные с документом */ + links: Array, + /** Шапка СЗ */ + proposal: ResolverInputTypes["ExpenseProposalHeaderInput"], + /** Хеш сметы расхода (детерминированный) */ + proposal_hash: string, + /** ID документа в реестре */ + registry_id: number, + /** Часовой пояс, в котором был создан документ */ + timezone: string, + /** Название документа */ + title: string, + /** Имя пользователя, создавшего документ */ + username: string, + /** Версия генератора, использованного для создания документа */ + version: string +}; + /** Статус сметы расхода. */ +["ExpenseProposalStatus"]:ExpenseProposalStatus; + /** Тип получателя платежа. */ +["ExpenseRecipientType"]:ExpenseRecipientType; + /** Исход отчёта по строке-авансу: закрыто либо ожидается расчёт разницы (возврат/доплата). */ +["ExpenseReportOutcome"]:ExpenseReportOutcome; + ["ExpenseReportResult"]: AliasType<{ + /** Исход отчёта. */ + outcome?:boolean | `@${string}`, + /** Сумма разницы к расчёту (asset), при недо-/перерасходе. */ + settlement_amount?:boolean | `@${string}`, + /** Хэш заведённой платёжки расчёта (возврат/доплата). */ + settlement_payment_hash?:boolean | `@${string}`, + /** Транзакция закрытия позиции (только при CLOSED). */ + transaction?:ResolverInputTypes["Transaction"], + __typename?: boolean | `@${string}` +}>; + ["ExpenseRequisite"]: AliasType<{ + /** Имя кооператива. */ + coopname?:boolean | `@${string}`, + /** Снимок данных платёжного метода на момент подачи СЗ. */ + data?:boolean | `@${string}`, + /** Хеш строки расхода (item). */ + item_hash?:boolean | `@${string}`, + /** Тип платёжного метода пайщика (СБП / банковский перевод). */ + method_type?:boolean | `@${string}`, + /** Назначение платежа для поручения кассиру. */ + payment_purpose?:boolean | `@${string}`, + /** Хеш сметы расхода (proposal). */ + proposal_hash?:boolean | `@${string}`, + /** Получатель платежа (аккаунт пайщика или название организации). */ + recipient?:boolean | `@${string}`, + /** Реквизиты строкой — как в документе служебной записки. */ + requisites?:boolean | `@${string}`, + __typename?: boolean | `@${string}` +}>; /** Статус расхода в системе CAPITAL */ ["ExpenseStatus"]:ExpenseStatus; /** Расширенное действие блокчейна с сертификатом пользователя, совершившего его. */ @@ -15081,122 +15883,6 @@ export type ResolverInputTypes = { votes_for?:boolean | `@${string}`, __typename?: boolean | `@${string}` }>; - ["MembershipExit"]: AliasType<{ - /** Дата подачи заявления на выход */ - created_at?:boolean | `@${string}`, - /** Хеш процесса выхода */ - exit_hash?:boolean | `@${string}`, - /** Сумма к возврату (фиксируется советом при одобрении; до одобрения — 0) */ - quantity?:boolean | `@${string}`, - /** Статус процесса выхода */ - status?:boolean | `@${string}`, - __typename?: boolean | `@${string}` -}>; - ["MembershipExitApplicationGenerateDocumentInput"]: { - /** Номер блока, на котором был создан документ */ - block_num?: number | undefined | null, - /** Название кооператива, связанное с документом */ - coopname: string, - /** Дата и время создания документа */ - created_at?: string | undefined | null, - /** Имя генератора, использованного для создания документа */ - generator?: string | undefined | null, - /** Язык документа */ - lang?: string | undefined | null, - /** Ссылки, связанные с документом */ - links?: Array | undefined | null, - /** Флаг пропуска сохранения документа (используется для предварительной генерации и демонстрации пользователю) */ - skip_save: boolean, - /** Часовой пояс, в котором был создан документ */ - timezone?: string | undefined | null, - /** Название документа */ - title?: string | undefined | null, - /** Имя пользователя, создавшего документ */ - username: string, - /** Версия генератора, использованного для создания документа */ - version?: string | undefined | null -}; - ["MembershipExitApplicationSignedDocumentInput"]: { - /** Хэш содержимого документа */ - doc_hash: string, - /** Общий хэш (doc_hash + meta_hash) */ - hash: string, - meta: ResolverInputTypes["MembershipExitApplicationSignedMetaDocumentInput"], - /** Хэш мета-данных */ - meta_hash: string, - /** Вектор подписей */ - signatures: Array, - /** Версия стандарта документа */ - version: string -}; - ["MembershipExitApplicationSignedMetaDocumentInput"]: { - /** Номер блока, на котором был создан документ */ - block_num: number, - /** Название кооператива, связанное с документом */ - coopname: string, - /** Дата и время создания документа */ - created_at: string, - /** Имя генератора, использованного для создания документа */ - generator: string, - /** Язык документа */ - lang: string, - /** Ссылки, связанные с документом */ - links: Array, - /** ID документа в реестре */ - registry_id: number, - /** Флаг пропуска сохранения документа (используется для предварительной генерации и демонстрации пользователю) */ - skip_save: boolean, - /** Часовой пояс, в котором был создан документ */ - timezone: string, - /** Название документа */ - title: string, - /** Имя пользователя, создавшего документ */ - username: string, - /** Версия генератора, использованного для создания документа */ - version: string -}; - ["MembershipExitDecisionGenerateDocumentInput"]: { - /** Номер блока, на котором был создан документ */ - block_num?: number | undefined | null, - /** Название кооператива, связанное с документом */ - coopname: string, - /** Дата и время создания документа */ - created_at?: string | undefined | null, - /** Идентификатор протокола решения собрания совета */ - decision_id: number, - /** Имя генератора, использованного для создания документа */ - generator?: string | undefined | null, - /** Язык документа */ - lang?: string | undefined | null, - /** Ссылки, связанные с документом */ - links?: Array | undefined | null, - /** Часовой пояс, в котором был создан документ */ - timezone?: string | undefined | null, - /** Название документа */ - title?: string | undefined | null, - /** Имя пользователя, создавшего документ */ - username: string, - /** Версия генератора, использованного для создания документа */ - version?: string | undefined | null -}; - ["MembershipExitResult"]: AliasType<{ - /** Хеш созданного процесса выхода */ - exit_hash?:boolean | `@${string}`, - /** Статус процесса выхода после подачи (ожидает подтверждения по ссылке из письма) */ - status?:boolean | `@${string}`, - __typename?: boolean | `@${string}` -}>; - ["MembershipExitReturnPreview"]: AliasType<{ - /** Минимальный паевой взнос пайщика */ - minimum_contribution?:boolean | `@${string}`, - /** Целевой паевой взнос пайщика */ - share_contribution?:boolean | `@${string}`, - /** Итоговая сумма к возврату (минимальный + целевой паевой) */ - total?:boolean | `@${string}`, - __typename?: boolean | `@${string}` -}>; - /** Статус процесса выхода пайщика из кооператива */ -["MembershipExitStatus"]:MembershipExitStatus; ["MissingRequisiteField"]: AliasType<{ key?:boolean | `@${string}`, label?:boolean | `@${string}`, @@ -15257,7 +15943,6 @@ addParticipant?: [{ data: ResolverInputTypes["AddParticipantInput"]},ResolverInp addPaymentMethod?: [{ data: ResolverInputTypes["AddPaymentMethodInput"]},ResolverInputTypes["PaymentMethod"]], addTrustedAccount?: [{ data: ResolverInputTypes["AddTrustedAccountInput"]},ResolverInputTypes["Branch"]], authorizeDecision?: [{ data: ResolverInputTypes["AuthorizeDecisionInput"]},ResolverInputTypes["Transaction"]], -cancelMembershipExit?: [{ coopname: string, username: string},boolean | `@${string}`], cancelRequest?: [{ data: ResolverInputTypes["CancelRequestInput"]},ResolverInputTypes["Transaction"]], capitalAddAuthor?: [{ data: ResolverInputTypes["AddAuthorInput"]},ResolverInputTypes["CapitalProject"]], capitalApproveCommit?: [{ data: ResolverInputTypes["CommitApproveInput"]},ResolverInputTypes["CapitalCommit"]], @@ -15273,6 +15958,7 @@ capitalCreateDebt?: [{ data: ResolverInputTypes["CreateDebtInput"]},ResolverInpu capitalCreateExpense?: [{ data: ResolverInputTypes["CreateExpenseInput"]},ResolverInputTypes["Transaction"]], capitalCreateIssue?: [{ data: ResolverInputTypes["CreateIssueInput"]},ResolverInputTypes["CapitalIssue"]], capitalCreateProcessTemplate?: [{ data: ResolverInputTypes["CreateProcessTemplateInput"]},ResolverInputTypes["ProcessTemplate"]], +capitalCreateProgramExpense?: [{ data: ResolverInputTypes["CapitalCreateProgramExpenseInput"]},ResolverInputTypes["Transaction"]], capitalCreateProgramInvest?: [{ data: ResolverInputTypes["CreateProgramInvestInput"]},ResolverInputTypes["Transaction"]], capitalCreateProgramProperty?: [{ data: ResolverInputTypes["CreateProgramPropertyInput"]},ResolverInputTypes["Transaction"]], capitalCreateProject?: [{ data: ResolverInputTypes["CreateProjectInput"]},ResolverInputTypes["Transaction"]], @@ -15330,6 +16016,7 @@ capitalStartProject?: [{ data: ResolverInputTypes["StartProjectInput"]},Resolver capitalStartVoting?: [{ data: ResolverInputTypes["StartVotingInput"]},ResolverInputTypes["Transaction"]], capitalStopProject?: [{ data: ResolverInputTypes["StopProjectInput"]},ResolverInputTypes["CapitalProject"]], capitalSubmitVote?: [{ data: ResolverInputTypes["SubmitVoteInput"]},ResolverInputTypes["Transaction"]], +capitalTopupProgramExpensePool?: [{ data: ResolverInputTypes["CapitalTopupProgramExpenseInput"]},ResolverInputTypes["Transaction"]], capitalUpdateIssue?: [{ data: ResolverInputTypes["UpdateIssueInput"]},ResolverInputTypes["CapitalIssue"]], capitalUpdateProcessTemplate?: [{ data: ResolverInputTypes["UpdateProcessTemplateInput"]},ResolverInputTypes["ProcessTemplate"]], capitalUpdateStory?: [{ data: ResolverInputTypes["UpdateStoryInput"]},ResolverInputTypes["CapitalStory"]], @@ -15352,15 +16039,14 @@ completeChairmanGeneralMeetStep?: [{ data: ResolverInputTypes["ChairmanOnboardin completeExtensionOnboardingStep?: [{ data: ResolverInputTypes["CompleteExtensionOnboardingStepInput"]},ResolverInputTypes["ExtensionOnboardingState"]], completeRequest?: [{ data: ResolverInputTypes["CompleteRequestInput"]},ResolverInputTypes["Transaction"]], confirmAgreement?: [{ data: ResolverInputTypes["ConfirmAgreementInput"]},ResolverInputTypes["Transaction"]], -confirmMembershipExit?: [{ token: string},ResolverInputTypes["MembershipExitResult"]], confirmReceiveOnRequest?: [{ data: ResolverInputTypes["ConfirmReceiveOnRequestInput"]},ResolverInputTypes["Transaction"]], confirmSupplyOnRequest?: [{ data: ResolverInputTypes["ConfirmSupplyOnRequestInput"]},ResolverInputTypes["Transaction"]], createAnnualGeneralMeet?: [{ data: ResolverInputTypes["CreateAnnualGeneralMeetInput"]},ResolverInputTypes["MeetAggregate"]], createBranch?: [{ data: ResolverInputTypes["CreateBranchInput"]},ResolverInputTypes["Branch"]], createChildOrder?: [{ data: ResolverInputTypes["CreateChildOrderInput"]},ResolverInputTypes["Transaction"]], createDepositPayment?: [{ data: ResolverInputTypes["CreateDepositPaymentInput"]},ResolverInputTypes["GatewayPayment"]], +createExpenseProposal?: [{ data: ResolverInputTypes["CreateExpenseProposalInput"]},ResolverInputTypes["Transaction"]], createInitialPayment?: [{ data: ResolverInputTypes["CreateInitialPaymentInput"]},ResolverInputTypes["GatewayPayment"]], -createMembershipExit?: [{ data: ResolverInputTypes["CreateMembershipExitInput"]},ResolverInputTypes["MembershipExitResult"]], createParentOffer?: [{ data: ResolverInputTypes["CreateParentOfferInput"]},ResolverInputTypes["Transaction"]], createProjectOfFreeDecision?: [{ data: ResolverInputTypes["CreateProjectFreeDecisionInput"]},ResolverInputTypes["CreatedProjectFreeDecision"]], createWebPushSubscription?: [{ data: ResolverInputTypes["CreateSubscriptionInput"]},ResolverInputTypes["CreateSubscriptionResponse"]], @@ -15386,9 +16072,9 @@ generateAssetContributionStatement?: [{ data: ResolverInputTypes["AssetContribut generateBallotForAnnualGeneralMeetDocument?: [{ data: ResolverInputTypes["AnnualGeneralMeetingVotingBallotGenerateDocumentInput"], options?: ResolverInputTypes["GenerateDocumentOptionsInput"] | undefined | null},ResolverInputTypes["GeneratedDocument"]], generateConvertToAxonStatement?: [{ data: ResolverInputTypes["ConvertToAxonStatementGenerateDocumentInput"], options?: ResolverInputTypes["GenerateDocumentOptionsInput"] | undefined | null},ResolverInputTypes["GeneratedDocument"]], generateDocument?: [{ input: ResolverInputTypes["GenerateAnyDocumentInput"]},ResolverInputTypes["GeneratedDocument"]], +generateExpenseProposalDecisionDocument?: [{ data: ResolverInputTypes["ExpenseProposalDecisionGenerateDocumentInput"], options?: ResolverInputTypes["GenerateDocumentOptionsInput"] | undefined | null},ResolverInputTypes["GeneratedDocument"]], +generateExpenseProposalStatementDocument?: [{ data: ResolverInputTypes["ExpenseProposalStatementGenerateDocumentInput"], options?: ResolverInputTypes["GenerateDocumentOptionsInput"] | undefined | null},ResolverInputTypes["GeneratedDocument"]], generateFreeDecision?: [{ data: ResolverInputTypes["FreeDecisionGenerateDocumentInput"], options?: ResolverInputTypes["GenerateDocumentOptionsInput"] | undefined | null},ResolverInputTypes["GeneratedDocument"]], -generateMembershipExitApplication?: [{ data: ResolverInputTypes["MembershipExitApplicationGenerateDocumentInput"], options?: ResolverInputTypes["GenerateDocumentOptionsInput"] | undefined | null},ResolverInputTypes["GeneratedDocument"]], -generateMembershipExitDecision?: [{ data: ResolverInputTypes["MembershipExitDecisionGenerateDocumentInput"], options?: ResolverInputTypes["GenerateDocumentOptionsInput"] | undefined | null},ResolverInputTypes["GeneratedDocument"]], generateParticipantApplication?: [{ data: ResolverInputTypes["ParticipantApplicationGenerateDocumentInput"], options?: ResolverInputTypes["GenerateDocumentOptionsInput"] | undefined | null},ResolverInputTypes["GeneratedDocument"]], generateParticipantApplicationDecision?: [{ data: ResolverInputTypes["ParticipantApplicationDecisionGenerateDocumentInput"], options?: ResolverInputTypes["GenerateDocumentOptionsInput"] | undefined | null},ResolverInputTypes["GeneratedDocument"]], generatePrivacyAgreement?: [{ data: ResolverInputTypes["GenerateDocumentInput"], options?: ResolverInputTypes["GenerateDocumentOptionsInput"] | undefined | null},ResolverInputTypes["GeneratedDocument"]], @@ -15415,6 +16101,8 @@ markNotificationRead?: [{ id: string},ResolverInputTypes["InboxNotification"]], markReportPeriod?: [{ data: ResolverInputTypes["MarkReportPeriodInput"]},boolean | `@${string}`], moderateRequest?: [{ data: ResolverInputTypes["ModerateRequestInput"]},ResolverInputTypes["Transaction"]], notifyOnAnnualGeneralMeet?: [{ data: ResolverInputTypes["NotifyOnAnnualGeneralMeetInput"]},ResolverInputTypes["MeetAggregate"]], +overspendExpenseItem?: [{ data: ResolverInputTypes["OverspendExpenseItemInput"]},ResolverInputTypes["Transaction"]], +payExpenseItem?: [{ data: ResolverInputTypes["PayExpenseItemInput"]},ResolverInputTypes["Transaction"]], processConvertToAxonStatement?: [{ data: ResolverInputTypes["ProcessConvertToAxonStatementInput"]},boolean | `@${string}`], prohibitRequest?: [{ data: ResolverInputTypes["ProhibitRequestInput"]},ResolverInputTypes["Transaction"]], publishProjectOfFreeDecision?: [{ data: ResolverInputTypes["PublishProjectFreeDecisionInput"]},ResolverInputTypes["AgendaWithDocuments"]], @@ -15423,11 +16111,13 @@ receiveOnRequest?: [{ data: ResolverInputTypes["ReceiveOnRequestInput"]},Resolve refresh?: [{ data: ResolverInputTypes["RefreshInput"]},ResolverInputTypes["RegisteredAccount"]], registerAccount?: [{ data: ResolverInputTypes["RegisterAccountInput"]},ResolverInputTypes["RegisteredAccount"]], registerParticipant?: [{ data: ResolverInputTypes["RegisterParticipantInput"]},ResolverInputTypes["Account"]], +reportExpenseItem?: [{ data: ResolverInputTypes["ReportExpenseItemInput"]},ResolverInputTypes["ExpenseReportResult"]], resendNotification?: [{ id: string},ResolverInputTypes["Notification"]], resetKey?: [{ data: ResolverInputTypes["ResetKeyInput"]},boolean | `@${string}`], /** Откатить собственную незавершённую регистрацию к редактированию данных: снимает заморозку профиля и e-mail, сбрасывает подписанное заявление и непринятую попытку вступительного платежа. Доступно только до отправки регистрации в блокчейн; если взнос уже принят — требуется возврат средств. */ resetRegistration?:ResolverInputTypes["Account"], restartAnnualGeneralMeet?: [{ data: ResolverInputTypes["RestartAnnualGeneralMeetInput"]},ResolverInputTypes["MeetAggregate"]], +returnExpenseItem?: [{ data: ResolverInputTypes["ReturnExpenseItemInput"]},ResolverInputTypes["Transaction"]], saveReportDraft?: [{ input: ResolverInputTypes["SaveReportDraftInput"]},ResolverInputTypes["ReportDraft"]], selectBranch?: [{ data: ResolverInputTypes["SelectBranchInput"]},boolean | `@${string}`], sendAgreement?: [{ data: ResolverInputTypes["SendAgreementInput"]},ResolverInputTypes["Transaction"]], @@ -15437,6 +16127,7 @@ signByPresiderOnAnnualGeneralMeet?: [{ data: ResolverInputTypes["SignByPresiderO signBySecretaryOnAnnualGeneralMeet?: [{ data: ResolverInputTypes["SignBySecretaryOnAnnualGeneralMeetInput"]},ResolverInputTypes["MeetAggregate"]], startInstall?: [{ data: ResolverInputTypes["StartInstallInput"]},ResolverInputTypes["StartInstallResult"]], startResetKey?: [{ data: ResolverInputTypes["StartResetKeyInput"]},boolean | `@${string}`], +submitExpenseReport?: [{ data: ResolverInputTypes["SubmitExpenseReportInput"]},ResolverInputTypes["Transaction"]], supplyOnRequest?: [{ data: ResolverInputTypes["SupplyOnRequestInput"]},ResolverInputTypes["Transaction"]], triggerNotificationWorkflow?: [{ data: ResolverInputTypes["TriggerNotificationWorkflowInput"]},boolean | `@${string}`], uninstallExtension?: [{ data: ResolverInputTypes["UninstallExtensionInput"]},boolean | `@${string}`], @@ -15448,6 +16139,7 @@ updateReportRequisites?: [{ input: ResolverInputTypes["UpdateReportRequisitesInp updateRequest?: [{ data: ResolverInputTypes["UpdateRequestInput"]},ResolverInputTypes["Transaction"]], updateSettings?: [{ data: ResolverInputTypes["UpdateSettingsInput"]},ResolverInputTypes["Settings"]], updateSystem?: [{ data: ResolverInputTypes["Update"]},ResolverInputTypes["SystemInfo"]], +uploadExpenseFile?: [{ data: ResolverInputTypes["UploadExpenseFileInput"]},ResolverInputTypes["ExpenseFile"]], verifyEmail?: [{ data: ResolverInputTypes["VerifyEmailInputDTO"]},boolean | `@${string}`], voteOnAnnualGeneralMeet?: [{ data: ResolverInputTypes["VoteOnAnnualGeneralMeetInput"]},ResolverInputTypes["MeetAggregate"]], walmoveWallets?: [{ input: ResolverInputTypes["WalmoveInput"]},ResolverInputTypes["Ledger2AdjustmentResult"]], @@ -15680,6 +16372,16 @@ walmoveWallets?: [{ input: ResolverInputTypes["WalmoveInput"]},ResolverInputType username?:boolean | `@${string}`, __typename?: boolean | `@${string}` }>; + ["OverspendExpenseItemInput"]: { + /** Имя кооператива. */ + coopname: string, + /** Хеш строки расхода (item). */ + item_hash: string, + /** Сумма доплаты сверх planned_amount (asset, например "200.0000 RUB"). */ + overspend_amount: string, + /** Хеш сметы расхода (proposal). */ + proposal_hash: string +}; ["PaginatedActionsPaginationResult"]: AliasType<{ /** Текущая страница */ currentPage?:boolean | `@${string}`, @@ -15811,6 +16513,17 @@ walmoveWallets?: [{ input: ResolverInputTypes["WalmoveInput"]},ResolverInputType /** Общее количество страниц */ totalPages?:boolean | `@${string}`, __typename?: boolean | `@${string}` +}>; + ["PaginatedCapitalProgramExpensesPaginationResult"]: AliasType<{ + /** Текущая страница */ + currentPage?:boolean | `@${string}`, + /** Элементы текущей страницы */ + items?:ResolverInputTypes["CapitalProgramExpense"], + /** Общее количество элементов */ + totalCount?:boolean | `@${string}`, + /** Общее количество страниц */ + totalPages?:boolean | `@${string}`, + __typename?: boolean | `@${string}` }>; ["PaginatedCapitalProjectsPaginationResult"]: AliasType<{ /** Текущая страница */ @@ -15921,6 +16634,17 @@ walmoveWallets?: [{ input: ResolverInputTypes["WalmoveInput"]},ResolverInputType /** Общее количество страниц */ totalPages?:boolean | `@${string}`, __typename?: boolean | `@${string}` +}>; + ["PaginatedExpenseProposalsPaginationResult"]: AliasType<{ + /** Текущая страница */ + currentPage?:boolean | `@${string}`, + /** Элементы текущей страницы */ + items?:ResolverInputTypes["ExpenseProposal"], + /** Общее количество элементов */ + totalCount?:boolean | `@${string}`, + /** Общее количество страниц */ + totalPages?:boolean | `@${string}`, + __typename?: boolean | `@${string}` }>; ["PaginatedGatewayPaymentsPaginationResult"]: AliasType<{ /** Текущая страница */ @@ -16084,6 +16808,16 @@ walmoveWallets?: [{ input: ResolverInputTypes["WalmoveInput"]},ResolverInputType issued_by: string, number: number, series: number +}; + ["PayExpenseItemInput"]: { + /** Фактическая сумма оплаты (asset, например "100.0000 RUB"). */ + actual_amount: string, + /** Имя кооператива. */ + coopname: string, + /** Хеш строки расхода (item). */ + item_hash: string, + /** Хеш сметы расхода (proposal). */ + proposal_hash: string }; ["PaymentDetails"]: AliasType<{ /** Сумма платежа с учетом комиссии */ @@ -16111,6 +16845,8 @@ walmoveWallets?: [{ input: ResolverInputTypes["WalmoveInput"]},ResolverInputType direction?: ResolverInputTypes["PaymentDirection"] | undefined | null, /** Хэш платежа */ hash?: string | undefined | null, + /** Хэш расхода (служебной записки): вернёт все платежи, связанные с этим расходом */ + proposal_hash?: string | undefined | null, /** Провайдер платежа */ provider?: string | undefined | null, /** Статус платежа */ @@ -16671,6 +17407,8 @@ capitalInvest?: [{ data: ResolverInputTypes["GetInvestInput"]},ResolverInputType capitalInvests?: [{ filter?: ResolverInputTypes["CapitalInvestFilter"] | undefined | null, options?: ResolverInputTypes["PaginationInput"] | undefined | null},ResolverInputTypes["PaginatedCapitalInvestsPaginationResult"]], capitalIssue?: [{ data: ResolverInputTypes["GetCapitalIssueByHashInput"]},ResolverInputTypes["CapitalIssue"]], capitalIssues?: [{ filter?: ResolverInputTypes["CapitalIssueFilter"] | undefined | null, options?: ResolverInputTypes["PaginationInput"] | undefined | null},ResolverInputTypes["PaginatedCapitalIssuesPaginationResult"]], +capitalProgramExpense?: [{ coopname: string, expense_hash: string},ResolverInputTypes["CapitalProgramExpense"]], +capitalProgramExpenses?: [{ coopname: string, options?: ResolverInputTypes["PaginationInput"] | undefined | null},ResolverInputTypes["PaginatedCapitalProgramExpensesPaginationResult"]], capitalProject?: [{ data: ResolverInputTypes["GetProjectInput"]},ResolverInputTypes["CapitalProject"]], capitalProjectWithRelations?: [{ data: ResolverInputTypes["GetProjectWithRelationsInput"]},ResolverInputTypes["CapitalProject"]], capitalProjects?: [{ filter?: ResolverInputTypes["CapitalProjectFilter"] | undefined | null, options?: ResolverInputTypes["PaginationInput"] | undefined | null},ResolverInputTypes["PaginatedCapitalProjectsPaginationResult"]], @@ -16718,6 +17456,13 @@ chatcoopListUtcDatesWithNewRoomMessages?: [{ data: ResolverInputTypes["ListUtcDa checkReportReadiness?: [{ reportType: ResolverInputTypes["ReportType"]},ResolverInputTypes["ReportReadinessView"]], cooperativeAgreements?: [{ coopname: string},ResolverInputTypes["CoopAgreement"]], cooperativePrograms?: [{ coopname: string},ResolverInputTypes["CooperativeProgram"]], +expenseFile?: [{ id: number},ResolverInputTypes["ExpenseFile"]], +expenseFilesByItem?: [{ coopname: string, item_hash: string, proposal_hash: string},ResolverInputTypes["ExpenseFile"]], +expenseFilesByProposal?: [{ coopname: string, proposal_hash: string},ResolverInputTypes["ExpenseFile"]], +expenseProposal?: [{ proposal_hash: string},ResolverInputTypes["ExpenseProposal"]], +expenseProposalsByCooperative?: [{ coopname: string, options?: ResolverInputTypes["PaginationInput"] | undefined | null},ResolverInputTypes["PaginatedExpenseProposalsPaginationResult"]], +expenseProposalsByMember?: [{ coopname: string, options?: ResolverInputTypes["PaginationInput"] | undefined | null, username: string},ResolverInputTypes["PaginatedExpenseProposalsPaginationResult"]], +expenseRequisitesByProposal?: [{ coopname: string, proposal_hash: string},ResolverInputTypes["ExpenseRequisite"]], getAccount?: [{ data: ResolverInputTypes["GetAccountInput"]},ResolverInputTypes["Account"]], getAccounts?: [{ data?: ResolverInputTypes["GetAccountsInput"] | undefined | null, options?: ResolverInputTypes["PaginationInput"] | undefined | null},ResolverInputTypes["AccountsPaginationResult"]], getActions?: [{ filters?: ResolverInputTypes["ActionFiltersInput"] | undefined | null, pagination?: ResolverInputTypes["PaginationInput"] | undefined | null},ResolverInputTypes["PaginatedActionsPaginationResult"]], @@ -16793,8 +17538,6 @@ getUserWebPushSubscriptions?: [{ data: ResolverInputTypes["GetUserSubscriptionsI Требуемые роли: chairman. */ getWebPushSubscriptionStats?:ResolverInputTypes["SubscriptionStatsDto"], listReportDrafts?: [{ filter?: ResolverInputTypes["ListReportDraftsFilterInput"] | undefined | null},ResolverInputTypes["ReportDraft"]], -membershipExit?: [{ coopname: string, username: string},ResolverInputTypes["MembershipExit"]], -membershipExitReturnPreview?: [{ coopname: string, username: string},ResolverInputTypes["MembershipExitReturnPreview"]], onecoopGetDocuments?: [{ data: ResolverInputTypes["GetOneCoopDocumentsInput"]},ResolverInputTypes["OneCoopDocumentsResponse"]], process?: [{ coopname: string, hash: string},ResolverInputTypes["ProcessView"]], processes?: [{ filter: ResolverInputTypes["ProcessesFilter"], pagination: ResolverInputTypes["PaginationInput"]},ResolverInputTypes["ProcessSummaryPaginationResult"]], @@ -17030,6 +17773,16 @@ validateReportEdits?: [{ editsJson: string, reportType: ResolverInputTypes["Repo year?:boolean | `@${string}`, __typename?: boolean | `@${string}` }>; + ["ReportExpenseItemInput"]: { + /** Фактически потраченная сумма по чекам (asset, например "800.0000 RUB"). Не указана — равна выданному авансу. */ + actual_amount?: string | undefined | null, + /** Имя кооператива. */ + coopname: string, + /** Хеш строки расхода (item). */ + item_hash: string, + /** Хеш сметы расхода (proposal). */ + proposal_hash: string +}; ["ReportHistoryFilterInput"]: { /** Лимит (макс 100, по умолчанию 20) */ limit?: number | undefined | null, @@ -17487,6 +18240,16 @@ validateReportEdits?: [{ editsJson: string, reportType: ResolverInputTypes["Repo username: string, /** Версия генератора, использованного для создания документа */ version: string +}; + ["ReturnExpenseItemInput"]: { + /** Имя кооператива. */ + coopname: string, + /** Хеш строки расхода (item). */ + item_hash: string, + /** Хеш сметы расхода (proposal). */ + proposal_hash: string, + /** Возвращаемая сумма (asset, например "50.0000 RUB"). */ + return_amount: string }; /** Тип сообщения в истории комнаты Matrix (текст или расшифрованное аудио) */ ["RoomMessageKind"]:RoomMessageKind; @@ -17866,6 +18629,16 @@ validateReportEdits?: [{ editsJson: string, reportType: ResolverInputTypes["Repo }; /** Статус истории в системе CAPITAL */ ["StoryStatus"]:StoryStatus; + ["SubmitExpenseReportInput"]: { + /** Комментарий пайщика к финализации отчёта (свободный текст, до 1000 символов). */ + comment?: string | undefined | null, + /** Имя кооператива. */ + coopname: string, + /** Хеш сметы расхода. */ + proposal_hash: string, + /** Итоговая сумма фактических расходов (asset, например "1500.0000 RUB"). */ + total_actual_amount?: string | undefined | null +}; ["SubmitVoteInput"]: { /** Имя аккаунта кооператива */ coopname: string, @@ -18232,6 +19005,26 @@ validateReportEdits?: [{ editsJson: string, reportType: ResolverInputTypes["Repo story_hash: string, /** Название истории */ title?: string | undefined | null +}; + ["UploadExpenseFileInput"]: { + /** SHA-256 содержимого, hex-lowercase (64 hex-символа). */ + checksum_sha256: string, + /** Содержимое файла, base64 без префикса data:. */ + content_base64: string, + /** Имя кооператива. */ + coopname: string, + /** Хеш строки расхода (пусто — файл уровня сметы). */ + item_hash?: string | undefined | null, + /** Назначение файла (платёжка/чек/возврат). */ + kind: ResolverInputTypes["ExpenseFileKind"], + /** MIME-тип содержимого. */ + mime_type: string, + /** Оригинальное имя файла — для отображения и поиска. */ + original_filename?: string | undefined | null, + /** Хеш сметы расхода. */ + proposal_hash: string, + /** Размер файла в байтах (для серверной валидации). */ + size_bytes: number }; ["UserAccount"]: AliasType<{ /** Метаинформация */ @@ -19896,6 +20689,20 @@ export type ModelTypes = { status?: ModelTypes["ContributorStatus"] | undefined | null, /** Фильтр по имени пользователя */ username?: string | undefined | null +}; + ["CapitalCreateProgramExpenseInput"]: { + /** Имя кооператива. */ + coopname: string, + /** Имя пайщика-создателя СЗ (председатель). */ + creator: string, + /** Описание программного расхода. */ + description: string, + /** Хэш СЗ-расхода (детерминированный, из UI). Он же станет proposal_hash в шасси. */ + expense_hash: string, + /** Строки расхода. Способ оплаты (аванс под отчёт / оплата по счёту) задаётся на каждой строке отдельно. */ + items: Array, + /** Подписанная СЗ-смета (document2, registry 2010). */ + statement: ModelTypes["ExpenseProposalStatementSignedDocumentInput"] }; /** Цикл разработки в системе CAPITAL */ ["CapitalCycle"]: { @@ -20256,6 +21063,38 @@ export type ModelTypes = { question: string, step: ModelTypes["CapitalOnboardingStep"], title?: string | undefined | null +}; + ["CapitalProgramExpense"]: { + callback?: ModelTypes["CapitalProgramExpenseCallback"] | undefined | null, + coopname: string, + created_at: string, + creator: string, + /** Имя инициатора (ФИО пайщика или название организации) */ + creator_name: string, + expense_hash: string, + items: Array, + source_wallet: string, + status: ModelTypes["ExpenseProposalStatus"], + total_actual: string, + total_planned: string, + updated_at: string +}; + ["CapitalProgramExpenseCallback"]: { + action: string, + contract: string, + data: string +}; + ["CapitalProgramExpenseItem"]: { + actual_amount: string, + description: string, + item_hash: string, + mechanics: ModelTypes["ExpenseMechanics"], + planned_amount: string, + recipient: string, + /** Имя получателя (ФИО пайщика или название организации) */ + recipient_name: string, + recipient_type: ModelTypes["ExpenseRecipientType"], + status: ModelTypes["ExpenseItemStatus"] }; /** Проект в системе CAPITAL с компонентами */ ["CapitalProject"]: { @@ -20811,6 +21650,10 @@ export type ModelTypes = { global_available_invest_pool: string, /** Флаг присутствия записи в блокчейне */ present: boolean, + /** Пул программных расходов — средства, переведённые под целевые расходы программы */ + program_expense_pool?: string | undefined | null, + /** Зарезервировано в пуле программных расходов под активные расходы (служебные записки в работе) */ + program_expense_reserved?: string | undefined | null, /** Доступная сумма членских взносов по программе */ program_membership_available: string, /** Накопительное вознаграждение на долю в членских взносах */ @@ -20965,6 +21808,12 @@ export type ModelTypes = { project_hash?: string | undefined | null, /** Имя пользователя (опционально) */ username?: string | undefined | null +}; + ["CapitalTopupProgramExpenseInput"]: { + /** Сумма пополнения (asset, eg "10000.0000 RUB"). */ + amount: string, + /** Имя кооператива. */ + coopname: string }; /** Голос в системе CAPITAL */ ["CapitalVote"]: { @@ -21613,6 +22462,22 @@ export type ModelTypes = { project_hash: string, /** Служебная записка о расходе */ statement: ModelTypes["SignedDigitalDocumentInput"] +}; + ["CreateExpenseProposalInput"]: { + /** Callback на финализацию closeexp (опционально). */ + callback?: ModelTypes["ExpenseCallbackInput"] | undefined | null, + /** Имя кооператива. */ + coopname: string, + /** Строки расхода (массив items). */ + items: Array, + /** Хеш сметы расхода (детерминированный, из UI). */ + proposal_hash: string, + /** Источник средств (eosio::name кошелька-источника, eg "w.cap.blago"). */ + source_wallet: string, + /** Подписанная СЗ-смета (document2, registry 2010). */ + statement: ModelTypes["ExpenseProposalStatementSignedDocumentInput"], + /** Имя пайщика-создателя СЗ. */ + username: string }; ["CreateIndividualDataInput"]: { /** Дата рождения */ @@ -21691,16 +22556,6 @@ export type ModelTypes = { ["CreateMatrixAccountInputDTO"]: { password: string, username: string -}; - ["CreateMembershipExitInput"]: { - /** Имя аккаунта кооператива */ - coopname: string, - /** Хеш процесса выхода (генерируется на клиенте) */ - exit_hash: string, - /** Подписанное пайщиком заявление о выходе из кооператива */ - statement: ModelTypes["MembershipExitApplicationSignedDocumentInput"], - /** Имя пайщика, выходящего из кооператива */ - username: string }; ["CreateOrganizationDataInput"]: { /** Банковский счет организации */ @@ -22276,6 +23131,44 @@ export type ModelTypes = { /** ОГРН */ ogrn: string }; + ["ExpenseCallbackInput"]: { + /** Action-метод */ + action?: string | undefined | null, + /** Контракт-целевой */ + contract?: string | undefined | null, + /** Payload (hex bytes) */ + data?: string | undefined | null +}; + /** Запись о первичном файле расхода (платёжка/чек/возврат). */ +["ExpenseFile"]: { + /** SHA-256 содержимого, hex-lowercase. */ + checksum_sha256: string, + /** Имя кооператива (scope). */ + coopname: string, + /** Внутренний ID записи. */ + id: number, + /** Хеш строки расхода (если файл уровня item). */ + item_hash?: string | undefined | null, + /** Назначение файла. */ + kind: ModelTypes["ExpenseFileKind"], + /** MIME-тип содержимого. */ + mime_type: string, + /** Оригинальное имя загруженного файла. */ + original_filename?: string | undefined | null, + /** Хеш сметы расхода. */ + proposal_hash: string, + /** Короткоживущий URL на скачивание (HMAC-signed). */ + read_url?: string | undefined | null, + /** Размер файла в байтах. */ + size_bytes: number, + /** MinIO-ключ внутри бакета. */ + storage_key: string, + /** Когда загружено. */ + uploaded_at: ModelTypes["DateTime"], + /** Кто загрузил (username). */ + uploaded_by_username: string +}; + ["ExpenseFileKind"]:ExpenseFileKind; ["ExpenseFilter"]: { /** Фильтр по ID фонда */ fundId?: string | undefined | null, @@ -22285,6 +23178,294 @@ export type ModelTypes = { status?: string | undefined | null, /** Фильтр по имени пользователя */ username?: string | undefined | null +}; + /** Строка сметы расхода. */ +["ExpenseItem"]: { + /** Фактическая сумма (после оплаты/отчёта). */ + actual_amount?: string | undefined | null, + /** Назначение/описание строки. */ + description: string, + /** Хеш строки расхода. */ + item_hash: string, + /** Способ оплаты. */ + mechanics: ModelTypes["ExpenseMechanics"], + /** Планируемая сумма. */ + planned_amount: string, + /** Идентификатор получателя. */ + recipient?: string | undefined | null, + /** Тип получателя платежа. */ + recipient_type: ModelTypes["ExpenseRecipientType"], + /** Статус строки. */ + status: ModelTypes["ExpenseItemStatus"] +}; + ["ExpenseItemInput"]: { + /** Описание назначения расхода. */ + description: string, + /** Хеш строки расхода (детерминированный, из UI). */ + item_hash: string, + /** Способ оплаты (ADVANCE / DIRECT). */ + mechanics: ModelTypes["ExpenseMechanics"], + /** Идентификатор сохранённых реквизитов получателя-пайщика — реквизиты снимаются в момент создания и прикладываются к платежу. */ + payment_method_id?: string | undefined | null, + /** Назначение платежа (для оплаты по счёту) — фиксируется в снимке для кассира. */ + payment_purpose?: string | undefined | null, + /** Планируемая сумма (asset, eg "1000.0000 RUB"). */ + planned_amount: string, + /** Получатель: username пайщика; для организации — пустая строка (аккаунта в кооперативе нет). */ + recipient: string, + /** Тип получателя. */ + recipient_type: ModelTypes["ExpenseRecipientType"], + /** Реквизиты получателя-организации (вводятся вручную). */ + requisites?: string | undefined | null +}; + ["ExpenseItemStatus"]:ExpenseItemStatus; + ["ExpenseMechanics"]:ExpenseMechanics; + /** Смета расхода (СЗ). */ +["ExpenseProposal"]: { + /** Дата создания записи */ + _created_at: ModelTypes["DateTime"], + /** Внутренний ID базы данных */ + _id: string, + /** Дата последнего обновления записи */ + _updated_at: ModelTypes["DateTime"], + /** Номер блока крайней синхронизации с блокчейном */ + block_num?: number | undefined | null, + /** Сырой статус из блокчейна (uint8). */ + blockchain_status?: number | undefined | null, + /** Имя кооператива (scope). */ + coopname: string, + /** Время создания (chain). */ + created_at?: string | undefined | null, + /** Решение совета о расходе (DocumentAggregate). */ + decision_doc?: ModelTypes["DocumentAggregate"] | undefined | null, + /** ID в блокчейне. */ + id?: number | undefined | null, + /** Строки сметы. */ + items: Array, + /** Флаг присутствия записи в блокчейне */ + present: boolean, + /** Хеш сметы расхода. */ + proposal_hash: string, + /** Кошелёк-источник средств. */ + source_wallet?: string | undefined | null, + /** Заявление пайщика о расходе (DocumentAggregate). */ + statement_doc?: ModelTypes["DocumentAggregate"] | undefined | null, + /** Доменный статус сметы. */ + status: ModelTypes["ExpenseProposalStatus"], + /** Сумма всех строк (факт). */ + total_actual?: string | undefined | null, + /** Сумма всех строк (план). */ + total_planned?: string | undefined | null, + /** Время последнего обновления (chain). */ + updated_at?: string | undefined | null, + /** Создатель сметы (username). */ + username?: string | undefined | null +}; + ["ExpenseProposalDecisionBodyInput"]: { + /** Род решения (approve / decline) */ + kind: string, + /** Причина отказа (для decline) */ + reason?: string | undefined | null +}; + ["ExpenseProposalDecisionGenerateDocumentInput"]: { + /** Номер блока, на котором был создан документ */ + block_num?: number | undefined | null, + /** Название кооператива, связанное с документом */ + coopname: string, + /** Дата и время создания документа */ + created_at?: string | undefined | null, + /** Идентификатор решения совета (повестка) — источник данных голосования */ + decision_id: number, + /** Имя генератора, использованного для создания документа */ + generator?: string | undefined | null, + /** Позиции расхода */ + items: Array, + /** Язык документа */ + lang?: string | undefined | null, + /** Ссылки, связанные с документом */ + links?: Array | undefined | null, + /** Шапка СЗ */ + proposal: ModelTypes["ExpenseProposalDecisionHeaderInput"], + /** Хеш сметы расхода */ + proposal_hash: string, + /** Резолюция совета (утвердить / отказать) */ + resolution: ModelTypes["ExpenseProposalDecisionBodyInput"], + /** Часовой пояс, в котором был создан документ */ + timezone?: string | undefined | null, + /** Название документа */ + title?: string | undefined | null, + /** Имя пользователя, создавшего документ */ + username: string, + /** Версия генератора, использованного для создания документа */ + version?: string | undefined | null +}; + ["ExpenseProposalDecisionHeaderInput"]: { + deadline?: string | undefined | null, + description: string, + fund_name?: string | undefined | null, + items_count: number, + source_wallet: string, + total_amount: string +}; + ["ExpenseProposalDecisionItemInput"]: { + amount: string, + description: string, + mechanics: string, + number: string, + recipient_type: string +}; + ["ExpenseProposalHeaderInput"]: { + /** Срок исполнения («в срок до»), формат DD.MM.YYYY */ + deadline?: string | undefined | null, + /** Описание цели расходов */ + description: string, + /** Фонд списания — подставляется сервером из параметров шасси расходов, передавать не нужно */ + fund_name?: string | undefined | null, + /** Количество позиций */ + items_count: number, + /** Кошелёк-источник */ + source_wallet: string, + /** Итоговая сумма расходов */ + total_amount: string +}; + ["ExpenseProposalItemInput"]: { + /** Сумма строки */ + amount: string, + /** Описание расхода */ + description: string, + /** Способ оплаты (ADVANCE / DIRECT) */ + mechanics: string, + /** Порядковый номер строки */ + number: string, + /** Идентификатор сохранённых реквизитов получателя-пайщика — сервер подставит полные реквизиты в документ. */ + payment_method_id?: string | undefined | null, + /** Назначение платежа — отдельной строкой после реквизитов */ + payment_purpose?: string | undefined | null, + /** Имя получателя */ + recipient_name?: string | undefined | null, + /** Тип получателя (SELF / MEMBER / ORG) */ + recipient_type: string, + /** Имя аккаунта получателя-пайщика (владелец реквизитов). */ + recipient_username?: string | undefined | null, + /** Реквизиты получателя */ + requisites?: string | undefined | null +}; + ["ExpenseProposalSignedItemInput"]: { + /** Сумма строки */ + amount: string, + /** Описание расхода */ + description: string, + /** Способ оплаты (ADVANCE / DIRECT) */ + mechanics: string, + /** Порядковый номер строки */ + number: string, + /** Тип получателя (SELF / MEMBER / ORG) */ + recipient_type: string +}; + ["ExpenseProposalStatementGenerateDocumentInput"]: { + /** Номер блока, на котором был создан документ */ + block_num?: number | undefined | null, + /** Название кооператива, связанное с документом */ + coopname: string, + /** Дата и время создания документа */ + created_at?: string | undefined | null, + /** Имя генератора, использованного для создания документа */ + generator?: string | undefined | null, + /** Позиции расхода */ + items: Array, + /** Язык документа */ + lang?: string | undefined | null, + /** Ссылки, связанные с документом */ + links?: Array | undefined | null, + /** Шапка СЗ */ + proposal: ModelTypes["ExpenseProposalHeaderInput"], + /** Хеш сметы расхода (детерминированный) */ + proposal_hash: string, + /** Часовой пояс, в котором был создан документ */ + timezone?: string | undefined | null, + /** Название документа */ + title?: string | undefined | null, + /** Имя пользователя, создавшего документ */ + username: string, + /** Версия генератора, использованного для создания документа */ + version?: string | undefined | null +}; + ["ExpenseProposalStatementSignedDocumentInput"]: { + /** Хэш содержимого документа */ + doc_hash: string, + /** Общий хэш (doc_hash + meta_hash) */ + hash: string, + /** Метаинформация СЗ-заявления */ + meta: ModelTypes["ExpenseProposalStatementSignedMetaDocumentInput"], + /** Хэш мета-данных */ + meta_hash: string, + /** Вектор подписей */ + signatures: Array, + /** Версия стандарта документа */ + version: string +}; + ["ExpenseProposalStatementSignedMetaDocumentInput"]: { + /** Номер блока, на котором был создан документ */ + block_num: number, + /** Название кооператива, связанное с документом */ + coopname: string, + /** Дата и время создания документа */ + created_at: string, + /** Идентификатор приватных данных документа off-chain (реквизиты/имя/назначение) */ + doc_data_hash: string, + /** Имя генератора, использованного для создания документа */ + generator: string, + /** Публичные позиции расхода (без реквизитов) */ + items: Array, + /** Язык документа */ + lang: string, + /** Ссылки, связанные с документом */ + links: Array, + /** Шапка СЗ */ + proposal: ModelTypes["ExpenseProposalHeaderInput"], + /** Хеш сметы расхода (детерминированный) */ + proposal_hash: string, + /** ID документа в реестре */ + registry_id: number, + /** Часовой пояс, в котором был создан документ */ + timezone: string, + /** Название документа */ + title: string, + /** Имя пользователя, создавшего документ */ + username: string, + /** Версия генератора, использованного для создания документа */ + version: string +}; + ["ExpenseProposalStatus"]:ExpenseProposalStatus; + ["ExpenseRecipientType"]:ExpenseRecipientType; + ["ExpenseReportOutcome"]:ExpenseReportOutcome; + ["ExpenseReportResult"]: { + /** Исход отчёта. */ + outcome: ModelTypes["ExpenseReportOutcome"], + /** Сумма разницы к расчёту (asset), при недо-/перерасходе. */ + settlement_amount?: string | undefined | null, + /** Хэш заведённой платёжки расчёта (возврат/доплата). */ + settlement_payment_hash?: string | undefined | null, + /** Транзакция закрытия позиции (только при CLOSED). */ + transaction?: ModelTypes["Transaction"] | undefined | null +}; + ["ExpenseRequisite"]: { + /** Имя кооператива. */ + coopname: string, + /** Снимок данных платёжного метода на момент подачи СЗ. */ + data?: ModelTypes["JSON"] | undefined | null, + /** Хеш строки расхода (item). */ + item_hash: string, + /** Тип платёжного метода пайщика (СБП / банковский перевод). */ + method_type?: string | undefined | null, + /** Назначение платежа для поручения кассиру. */ + payment_purpose?: string | undefined | null, + /** Хеш сметы расхода (proposal). */ + proposal_hash: string, + /** Получатель платежа (аккаунт пайщика или название организации). */ + recipient: string, + /** Реквизиты строкой — как в документе служебной записки. */ + requisites: string }; ["ExpenseStatus"]:ExpenseStatus; /** Расширенное действие блокчейна с сертификатом пользователя, совершившего его. */ @@ -23478,118 +24659,6 @@ export type ModelTypes = { /** Количество голосов за */ votes_for: number }; - ["MembershipExit"]: { - /** Дата подачи заявления на выход */ - created_at: string, - /** Хеш процесса выхода */ - exit_hash: string, - /** Сумма к возврату (фиксируется советом при одобрении; до одобрения — 0) */ - quantity: string, - /** Статус процесса выхода */ - status: ModelTypes["MembershipExitStatus"] -}; - ["MembershipExitApplicationGenerateDocumentInput"]: { - /** Номер блока, на котором был создан документ */ - block_num?: number | undefined | null, - /** Название кооператива, связанное с документом */ - coopname: string, - /** Дата и время создания документа */ - created_at?: string | undefined | null, - /** Имя генератора, использованного для создания документа */ - generator?: string | undefined | null, - /** Язык документа */ - lang?: string | undefined | null, - /** Ссылки, связанные с документом */ - links?: Array | undefined | null, - /** Флаг пропуска сохранения документа (используется для предварительной генерации и демонстрации пользователю) */ - skip_save: boolean, - /** Часовой пояс, в котором был создан документ */ - timezone?: string | undefined | null, - /** Название документа */ - title?: string | undefined | null, - /** Имя пользователя, создавшего документ */ - username: string, - /** Версия генератора, использованного для создания документа */ - version?: string | undefined | null -}; - ["MembershipExitApplicationSignedDocumentInput"]: { - /** Хэш содержимого документа */ - doc_hash: string, - /** Общий хэш (doc_hash + meta_hash) */ - hash: string, - meta: ModelTypes["MembershipExitApplicationSignedMetaDocumentInput"], - /** Хэш мета-данных */ - meta_hash: string, - /** Вектор подписей */ - signatures: Array, - /** Версия стандарта документа */ - version: string -}; - ["MembershipExitApplicationSignedMetaDocumentInput"]: { - /** Номер блока, на котором был создан документ */ - block_num: number, - /** Название кооператива, связанное с документом */ - coopname: string, - /** Дата и время создания документа */ - created_at: string, - /** Имя генератора, использованного для создания документа */ - generator: string, - /** Язык документа */ - lang: string, - /** Ссылки, связанные с документом */ - links: Array, - /** ID документа в реестре */ - registry_id: number, - /** Флаг пропуска сохранения документа (используется для предварительной генерации и демонстрации пользователю) */ - skip_save: boolean, - /** Часовой пояс, в котором был создан документ */ - timezone: string, - /** Название документа */ - title: string, - /** Имя пользователя, создавшего документ */ - username: string, - /** Версия генератора, использованного для создания документа */ - version: string -}; - ["MembershipExitDecisionGenerateDocumentInput"]: { - /** Номер блока, на котором был создан документ */ - block_num?: number | undefined | null, - /** Название кооператива, связанное с документом */ - coopname: string, - /** Дата и время создания документа */ - created_at?: string | undefined | null, - /** Идентификатор протокола решения собрания совета */ - decision_id: number, - /** Имя генератора, использованного для создания документа */ - generator?: string | undefined | null, - /** Язык документа */ - lang?: string | undefined | null, - /** Ссылки, связанные с документом */ - links?: Array | undefined | null, - /** Часовой пояс, в котором был создан документ */ - timezone?: string | undefined | null, - /** Название документа */ - title?: string | undefined | null, - /** Имя пользователя, создавшего документ */ - username: string, - /** Версия генератора, использованного для создания документа */ - version?: string | undefined | null -}; - ["MembershipExitResult"]: { - /** Хеш созданного процесса выхода */ - exit_hash: string, - /** Статус процесса выхода после подачи (ожидает подтверждения по ссылке из письма) */ - status: ModelTypes["MembershipExitStatus"] -}; - ["MembershipExitReturnPreview"]: { - /** Минимальный паевой взнос пайщика */ - minimum_contribution: string, - /** Целевой паевой взнос пайщика */ - share_contribution: string, - /** Итоговая сумма к возврату (минимальный + целевой паевой) */ - total: string -}; - ["MembershipExitStatus"]:MembershipExitStatus; ["MissingRequisiteField"]: { key: string, label: string, @@ -23659,8 +24728,6 @@ export type ModelTypes = { Требуемые роли: chairman. */ authorizeDecision: ModelTypes["Transaction"], - /** Отменить заявление на выход до подтверждения по email. */ - cancelMembershipExit: boolean, /** Отменить заявку */ cancelRequest: ModelTypes["Transaction"], /** Добавление автора проекта в CAPITAL контракте @@ -23719,6 +24786,10 @@ export type ModelTypes = { Требуемые роли: chairman, member. */ capitalCreateProcessTemplate: ModelTypes["ProcessTemplate"], + /** Создание программного расхода капитала через шасси expense. + +Требуемые роли: chairman, member. */ + capitalCreateProgramExpense: ModelTypes["Transaction"], /** Инвестирование в программу благорост (денежная программная инвестиция) Требуемые роли: participant. */ @@ -23943,6 +25014,10 @@ export type ModelTypes = { Требуемые роли: chairman, member, user. */ capitalSubmitVote: ModelTypes["Transaction"], + /** Пополнение пула программных расходов капитала из инвестиционного пула. + +Требуемые роли: chairman. */ + capitalTopupProgramExpensePool: ModelTypes["Transaction"], /** Обновление задачи в CAPITAL контракте Требуемые роли: chairman, member, user. */ @@ -24017,8 +25092,6 @@ export type ModelTypes = { Требуемые роли: chairman, member. */ confirmAgreement: ModelTypes["Transaction"], - /** Подтвердить выход из кооператива по ссылке из письма. Проверяет токен и отправляет ранее подписанное заявление в блокчейн. */ - confirmMembershipExit: ModelTypes["MembershipExitResult"], /** Подтвердить получение имущества Уполномоченным лицом от Заказчика по новации и акту приёмки-передачи */ confirmReceiveOnRequest: ModelTypes["Transaction"], /** Подтвердить поставку имущества Поставщиком по заявке Заказчика и акту приёма-передачи */ @@ -24037,12 +25110,14 @@ export type ModelTypes = { Требуемые роли: chairman, member. */ createDepositPayment: ModelTypes["GatewayPayment"], + /** Подать СЗ-расход (создать смету с подписью пайщика/председателя). + +Требуемые роли: chairman, member. */ + createExpenseProposal: ModelTypes["Transaction"], /** Создание объекта регистрационного платежа производится мутацией createInitialPayment. Выполнение мутации возвращает идентификатор платежа и данные для его совершения в зависимости от выбранного платежного провайдера. Требуемые роли: chairman, member. */ createInitialPayment: ModelTypes["GatewayPayment"], - /** Подать подписанное заявление на выход из кооператива. Запускает рассмотрение советом и последующий возврат паевого взноса. */ - createMembershipExit: ModelTypes["MembershipExitResult"], /** Создать предложение на поставку имущества */ createParentOffer: ModelTypes["Transaction"], /** Создать повестку дня и проект решения, и сохранить в хранилище для дальнейшей генерации документа и его публикации @@ -24131,16 +25206,18 @@ export type ModelTypes = { generateConvertToAxonStatement: ModelTypes["GeneratedDocument"], /** Универсальная генерация документа с произвольными данными (только для председателя) */ generateDocument: ModelTypes["GeneratedDocument"], + /** Сгенерировать документ-решение по СЗ (registry 2011) для последующей подписи. + +Требуемые роли: chairman. */ + generateExpenseProposalDecisionDocument: ModelTypes["GeneratedDocument"], + /** Сгенерировать документ СЗ-заявления (registry 2010) для последующей подписи. + +Требуемые роли: chairman, member. */ + generateExpenseProposalStatementDocument: ModelTypes["GeneratedDocument"], /** Сгенерировать протокол решения по предложенной повестке Требуемые роли: chairman, member. */ generateFreeDecision: ModelTypes["GeneratedDocument"], - /** Сгенерировать документ заявления о выходе из кооператива. */ - generateMembershipExitApplication: ModelTypes["GeneratedDocument"], - /** Сгенерировать документ решения собрания совета о выходе пайщика. - -Требуемые роли: chairman, member. */ - generateMembershipExitDecision: ModelTypes["GeneratedDocument"], /** Сгенерировать документ заявления о вступлении в кооператив. Требуемые роли: chairman, member. */ @@ -24235,6 +25312,14 @@ export type ModelTypes = { Требуемые роли: chairman, member. */ notifyOnAnnualGeneralMeet: ModelTypes["MeetAggregate"], + /** Доплатить сумму перерасхода по строке расхода (ADVANCE-механика). + +Требуемые роли: chairman. */ + overspendExpenseItem: ModelTypes["Transaction"], + /** Оплатить строку расхода (выдача аванса ADVANCE или прямая оплата DIRECT). + +Требуемые роли: chairman. */ + payExpenseItem: ModelTypes["Transaction"], /** Обрабатывает подписанное заявление на конвертацию и выполняет блокчейн-транзакцию Требуемые роли: member, chairman. */ @@ -24257,6 +25342,10 @@ export type ModelTypes = { Требуемые роли: chairman, member. */ registerParticipant: ModelTypes["Account"], + /** Отчитаться по строке-авансу: при совпадении факта с авансом — закрыть позицию; при недо-/перерасходе — завести платёжку расчёта разницы. + +Требуемые роли: chairman, member, user. */ + reportExpenseItem: ModelTypes["ExpenseReportResult"], /** Переотправить уведомление (force-постановка новой строки в очередь доставки) Требуемые роли: chairman. */ @@ -24269,6 +25358,10 @@ export type ModelTypes = { Требуемые роли: chairman. */ restartAnnualGeneralMeet: ModelTypes["MeetAggregate"], + /** Вернуть неиспользованный аванс по строке расхода (ADVANCE-остаток). + +Требуемые роли: chairman, member, user. */ + returnExpenseItem: ModelTypes["Transaction"], /** Сохранить/обновить черновик формы отчёта (upsert по owner+type+year+period) Требуемые роли: chairman. */ @@ -24297,6 +25390,10 @@ export type ModelTypes = { startInstall: ModelTypes["StartInstallResult"], /** Выслать токен для замены приватного ключа аккаунта на электронную почту */ startResetKey: boolean, + /** Финализировать СЗ-отчёт по смете расхода (все items закрыты — оплата/чек/возврат). + +Требуемые роли: chairman, member. */ + submitExpenseReport: ModelTypes["Transaction"], /** Подтвердить поставку имущества Поставщиком по заявке Заказчика и акту приёма-передачи */ supplyOnRequest: ModelTypes["Transaction"], /** Запустить воркфлоу уведомлений (только для председателя или server-secret) @@ -24333,6 +25430,10 @@ export type ModelTypes = { Требуемые роли: chairman. */ updateSystem: ModelTypes["SystemInfo"], + /** Загрузить первичный файл расхода (платёжка/чек/возврат) в бакет expenses:files. + +Требуемые роли: chairman, member, user. */ + uploadExpenseFile: ModelTypes["ExpenseFile"], /** Подтвердить email адрес пользователя */ verifyEmail: boolean, /** Голосование на общем собрании пайщиков @@ -24555,6 +25656,16 @@ export type ModelTypes = { type: string, /** Имя аккаунта организации */ username: string +}; + ["OverspendExpenseItemInput"]: { + /** Имя кооператива. */ + coopname: string, + /** Хеш строки расхода (item). */ + item_hash: string, + /** Сумма доплаты сверх planned_amount (asset, например "200.0000 RUB"). */ + overspend_amount: string, + /** Хеш сметы расхода (proposal). */ + proposal_hash: string }; ["PaginatedActionsPaginationResult"]: { /** Текущая страница */ @@ -24675,6 +25786,16 @@ export type ModelTypes = { totalCount: number, /** Общее количество страниц */ totalPages: number +}; + ["PaginatedCapitalProgramExpensesPaginationResult"]: { + /** Текущая страница */ + currentPage: number, + /** Элементы текущей страницы */ + items: Array, + /** Общее количество элементов */ + totalCount: number, + /** Общее количество страниц */ + totalPages: number }; ["PaginatedCapitalProjectsPaginationResult"]: { /** Текущая страница */ @@ -24775,6 +25896,16 @@ export type ModelTypes = { totalCount: number, /** Общее количество страниц */ totalPages: number +}; + ["PaginatedExpenseProposalsPaginationResult"]: { + /** Текущая страница */ + currentPage: number, + /** Элементы текущей страницы */ + items: Array, + /** Общее количество элементов */ + totalCount: number, + /** Общее количество страниц */ + totalPages: number }; ["PaginatedGatewayPaymentsPaginationResult"]: { /** Текущая страница */ @@ -24935,6 +26066,16 @@ export type ModelTypes = { issued_by: string, number: number, series: number +}; + ["PayExpenseItemInput"]: { + /** Фактическая сумма оплаты (asset, например "100.0000 RUB"). */ + actual_amount: string, + /** Имя кооператива. */ + coopname: string, + /** Хеш строки расхода (item). */ + item_hash: string, + /** Хеш сметы расхода (proposal). */ + proposal_hash: string }; ["PaymentDetails"]: { /** Сумма платежа с учетом комиссии */ @@ -24960,6 +26101,8 @@ export type ModelTypes = { direction?: ModelTypes["PaymentDirection"] | undefined | null, /** Хэш платежа */ hash?: string | undefined | null, + /** Хэш расхода (служебной записки): вернёт все платежи, связанные с этим расходом */ + proposal_hash?: string | undefined | null, /** Провайдер платежа */ provider?: string | undefined | null, /** Статус платежа */ @@ -25516,6 +26659,14 @@ export type ModelTypes = { Требуемые роли: chairman, member, user. */ capitalIssues: ModelTypes["PaginatedCapitalIssuesPaginationResult"], + /** Программный расход по expense_hash. + +Требуемые роли: chairman, member. */ + capitalProgramExpense?: ModelTypes["CapitalProgramExpense"] | undefined | null, + /** Список программных расходов капитала (через шасси expense). + +Требуемые роли: chairman, member. */ + capitalProgramExpenses: ModelTypes["PaginatedCapitalProgramExpensesPaginationResult"], /** Получение проекта по хешу с компонентами */ capitalProject?: ModelTypes["CapitalProject"] | undefined | null, /** Получение проекта с полными отношениями по хешу проекта */ @@ -25620,6 +26771,34 @@ export type ModelTypes = { cooperativeAgreements: Array, /** Целевые потребительские программы кооператива (id, тип, активность, draft_id) */ cooperativePrograms: Array, + /** Получить запись о файле + свежий короткоживущий read-URL. + +Требуемые роли: chairman, member, user. */ + expenseFile: ModelTypes["ExpenseFile"], + /** Список файлов строки расхода (без read-URL — запрос отдельно по id). + +Требуемые роли: chairman, member, user. */ + expenseFilesByItem: Array, + /** Список файлов сметы расхода (без read-URL — запрос отдельно по id). + +Требуемые роли: chairman, member, user. */ + expenseFilesByProposal: Array, + /** Получить смету расхода по хешу. + +Требуемые роли: chairman, member, user. */ + expenseProposal?: ModelTypes["ExpenseProposal"] | undefined | null, + /** Список смет расходов кооператива (paginated). + +Требуемые роли: chairman, member. */ + expenseProposalsByCooperative: ModelTypes["PaginatedExpenseProposalsPaginationResult"], + /** Список смет расходов пайщика (свои/созданные им, paginated). + +Требуемые роли: chairman, member, user. */ + expenseProposalsByMember: ModelTypes["PaginatedExpenseProposalsPaginationResult"], + /** Снимки реквизитов получателей по строкам СЗ (персональные данные — только совету; в блокчейн не пишутся). + +Требуемые роли: chairman, member. */ + expenseRequisitesByProposal: Array, /** Получить сводную информацию о аккаунте Требуемые роли: chairman, member. */ @@ -25800,10 +26979,6 @@ export type ModelTypes = { Требуемые роли: chairman. */ listReportDrafts: Array, - /** Текущий процесс выхода пайщика из кооператива (статус заявления и планируемая сумма возврата). null — активного выхода нет. */ - membershipExit?: ModelTypes["MembershipExit"] | undefined | null, - /** Предварительный расчёт суммы возврата паевого взноса при выходе пайщика (минимальный + целевой паевой). Ориентир для пайщика; итог фиксирует совет. */ - membershipExitReturnPreview: ModelTypes["MembershipExitReturnPreview"], /** Получение документов кооператива для синхронизации с 1С. Требует секретный ключ в заголовке x-onecoop-secret-key. */ onecoopGetDocuments: ModelTypes["OneCoopDocumentsResponse"], /** Получить полную картину процесса ledger2 по process_hash @@ -26041,6 +27216,16 @@ export type ModelTypes = { reportType: ModelTypes["ReportType"], updatedAt: ModelTypes["DateTime"], year: number +}; + ["ReportExpenseItemInput"]: { + /** Фактически потраченная сумма по чекам (asset, например "800.0000 RUB"). Не указана — равна выданному авансу. */ + actual_amount?: string | undefined | null, + /** Имя кооператива. */ + coopname: string, + /** Хеш строки расхода (item). */ + item_hash: string, + /** Хеш сметы расхода (proposal). */ + proposal_hash: string }; ["ReportHistoryFilterInput"]: { /** Лимит (макс 100, по умолчанию 20) */ @@ -26486,6 +27671,16 @@ export type ModelTypes = { username: string, /** Версия генератора, использованного для создания документа */ version: string +}; + ["ReturnExpenseItemInput"]: { + /** Имя кооператива. */ + coopname: string, + /** Хеш строки расхода (item). */ + item_hash: string, + /** Хеш сметы расхода (proposal). */ + proposal_hash: string, + /** Возвращаемая сумма (asset, например "50.0000 RUB"). */ + return_amount: string }; ["RoomMessageKind"]:RoomMessageKind; ["SaveReportDraftInput"]: { @@ -26854,6 +28049,16 @@ export type ModelTypes = { project_hash: string }; ["StoryStatus"]:StoryStatus; + ["SubmitExpenseReportInput"]: { + /** Комментарий пайщика к финализации отчёта (свободный текст, до 1000 символов). */ + comment?: string | undefined | null, + /** Имя кооператива. */ + coopname: string, + /** Хеш сметы расхода. */ + proposal_hash: string, + /** Итоговая сумма фактических расходов (asset, например "1500.0000 RUB"). */ + total_actual_amount?: string | undefined | null +}; ["SubmitVoteInput"]: { /** Имя аккаунта кооператива */ coopname: string, @@ -27209,6 +28414,26 @@ export type ModelTypes = { story_hash: string, /** Название истории */ title?: string | undefined | null +}; + ["UploadExpenseFileInput"]: { + /** SHA-256 содержимого, hex-lowercase (64 hex-символа). */ + checksum_sha256: string, + /** Содержимое файла, base64 без префикса data:. */ + content_base64: string, + /** Имя кооператива. */ + coopname: string, + /** Хеш строки расхода (пусто — файл уровня сметы). */ + item_hash?: string | undefined | null, + /** Назначение файла (платёжка/чек/возврат). */ + kind: ModelTypes["ExpenseFileKind"], + /** MIME-тип содержимого. */ + mime_type: string, + /** Оригинальное имя файла — для отображения и поиска. */ + original_filename?: string | undefined | null, + /** Хеш сметы расхода. */ + proposal_hash: string, + /** Размер файла в байтах (для серверной валидации). */ + size_bytes: number }; ["UserAccount"]: { /** Метаинформация */ @@ -27417,7 +28642,10 @@ export type ModelTypes = { } export type GraphQLTypes = { - ["AcceptChildOrderInput"]: { + // ------------------------------------------------------; + // THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY); + // ------------------------------------------------------; + ["AcceptChildOrderInput"]: { /** Имя аккаунта кооператива */ coopname: string, /** Подписанное заявление на имущественный паевый взнос */ @@ -28944,6 +30172,20 @@ export type GraphQLTypes = { status?: GraphQLTypes["ContributorStatus"] | undefined | null, /** Фильтр по имени пользователя */ username?: string | undefined | null +}; + ["CapitalCreateProgramExpenseInput"]: { + /** Имя кооператива. */ + coopname: string, + /** Имя пайщика-создателя СЗ (председатель). */ + creator: string, + /** Описание программного расхода. */ + description: string, + /** Хэш СЗ-расхода (детерминированный, из UI). Он же станет proposal_hash в шасси. */ + expense_hash: string, + /** Строки расхода. Способ оплаты (аванс под отчёт / оплата по счёту) задаётся на каждой строке отдельно. */ + items: Array, + /** Подписанная СЗ-смета (document2, registry 2010). */ + statement: GraphQLTypes["ExpenseProposalStatementSignedDocumentInput"] }; /** Цикл разработки в системе CAPITAL */ ["CapitalCycle"]: { @@ -29324,6 +30566,44 @@ export type GraphQLTypes = { question: string, step: GraphQLTypes["CapitalOnboardingStep"], title?: string | undefined | null +}; + ["CapitalProgramExpense"]: { + __typename: "CapitalProgramExpense", + callback?: GraphQLTypes["CapitalProgramExpenseCallback"] | undefined | null, + coopname: string, + created_at: string, + creator: string, + /** Имя инициатора (ФИО пайщика или название организации) */ + creator_name: string, + expense_hash: string, + items: Array, + source_wallet: string, + status: GraphQLTypes["ExpenseProposalStatus"], + total_actual: string, + total_planned: string, + updated_at: string, + ['...on CapitalProgramExpense']: Omit +}; + ["CapitalProgramExpenseCallback"]: { + __typename: "CapitalProgramExpenseCallback", + action: string, + contract: string, + data: string, + ['...on CapitalProgramExpenseCallback']: Omit +}; + ["CapitalProgramExpenseItem"]: { + __typename: "CapitalProgramExpenseItem", + actual_amount: string, + description: string, + item_hash: string, + mechanics: GraphQLTypes["ExpenseMechanics"], + planned_amount: string, + recipient: string, + /** Имя получателя (ФИО пайщика или название организации) */ + recipient_name: string, + recipient_type: GraphQLTypes["ExpenseRecipientType"], + status: GraphQLTypes["ExpenseItemStatus"], + ['...on CapitalProgramExpenseItem']: Omit }; /** Проект в системе CAPITAL с компонентами */ ["CapitalProject"]: { @@ -29904,6 +31184,10 @@ export type GraphQLTypes = { global_available_invest_pool: string, /** Флаг присутствия записи в блокчейне */ present: boolean, + /** Пул программных расходов — средства, переведённые под целевые расходы программы */ + program_expense_pool?: string | undefined | null, + /** Зарезервировано в пуле программных расходов под активные расходы (служебные записки в работе) */ + program_expense_reserved?: string | undefined | null, /** Доступная сумма членских взносов по программе */ program_membership_available: string, /** Накопительное вознаграждение на долю в членских взносах */ @@ -30068,6 +31352,12 @@ export type GraphQLTypes = { project_hash?: string | undefined | null, /** Имя пользователя (опционально) */ username?: string | undefined | null +}; + ["CapitalTopupProgramExpenseInput"]: { + /** Сумма пополнения (asset, eg "10000.0000 RUB"). */ + amount: string, + /** Имя кооператива. */ + coopname: string }; /** Голос в системе CAPITAL */ ["CapitalVote"]: { @@ -30749,6 +32039,22 @@ export type GraphQLTypes = { project_hash: string, /** Служебная записка о расходе */ statement: GraphQLTypes["SignedDigitalDocumentInput"] +}; + ["CreateExpenseProposalInput"]: { + /** Callback на финализацию closeexp (опционально). */ + callback?: GraphQLTypes["ExpenseCallbackInput"] | undefined | null, + /** Имя кооператива. */ + coopname: string, + /** Строки расхода (массив items). */ + items: Array, + /** Хеш сметы расхода (детерминированный, из UI). */ + proposal_hash: string, + /** Источник средств (eosio::name кошелька-источника, eg "w.cap.blago"). */ + source_wallet: string, + /** Подписанная СЗ-смета (document2, registry 2010). */ + statement: GraphQLTypes["ExpenseProposalStatementSignedDocumentInput"], + /** Имя пайщика-создателя СЗ. */ + username: string }; ["CreateIndividualDataInput"]: { /** Дата рождения */ @@ -30827,16 +32133,6 @@ export type GraphQLTypes = { ["CreateMatrixAccountInputDTO"]: { password: string, username: string -}; - ["CreateMembershipExitInput"]: { - /** Имя аккаунта кооператива */ - coopname: string, - /** Хеш процесса выхода (генерируется на клиенте) */ - exit_hash: string, - /** Подписанное пайщиком заявление о выходе из кооператива */ - statement: GraphQLTypes["MembershipExitApplicationSignedDocumentInput"], - /** Имя пайщика, выходящего из кооператива */ - username: string }; ["CreateOrganizationDataInput"]: { /** Банковский счет организации */ @@ -31447,6 +32743,47 @@ export type GraphQLTypes = { /** ОГРН */ ogrn: string }; + ["ExpenseCallbackInput"]: { + /** Action-метод */ + action?: string | undefined | null, + /** Контракт-целевой */ + contract?: string | undefined | null, + /** Payload (hex bytes) */ + data?: string | undefined | null +}; + /** Запись о первичном файле расхода (платёжка/чек/возврат). */ +["ExpenseFile"]: { + __typename: "ExpenseFile", + /** SHA-256 содержимого, hex-lowercase. */ + checksum_sha256: string, + /** Имя кооператива (scope). */ + coopname: string, + /** Внутренний ID записи. */ + id: number, + /** Хеш строки расхода (если файл уровня item). */ + item_hash?: string | undefined | null, + /** Назначение файла. */ + kind: GraphQLTypes["ExpenseFileKind"], + /** MIME-тип содержимого. */ + mime_type: string, + /** Оригинальное имя загруженного файла. */ + original_filename?: string | undefined | null, + /** Хеш сметы расхода. */ + proposal_hash: string, + /** Короткоживущий URL на скачивание (HMAC-signed). */ + read_url?: string | undefined | null, + /** Размер файла в байтах. */ + size_bytes: number, + /** MinIO-ключ внутри бакета. */ + storage_key: string, + /** Когда загружено. */ + uploaded_at: GraphQLTypes["DateTime"], + /** Кто загрузил (username). */ + uploaded_by_username: string, + ['...on ExpenseFile']: Omit +}; + /** Тип первичного файла расхода. */ +["ExpenseFileKind"]: ExpenseFileKind; ["ExpenseFilter"]: { /** Фильтр по ID фонда */ fundId?: string | undefined | null, @@ -31456,6 +32793,307 @@ export type GraphQLTypes = { status?: string | undefined | null, /** Фильтр по имени пользователя */ username?: string | undefined | null +}; + /** Строка сметы расхода. */ +["ExpenseItem"]: { + __typename: "ExpenseItem", + /** Фактическая сумма (после оплаты/отчёта). */ + actual_amount?: string | undefined | null, + /** Назначение/описание строки. */ + description: string, + /** Хеш строки расхода. */ + item_hash: string, + /** Способ оплаты. */ + mechanics: GraphQLTypes["ExpenseMechanics"], + /** Планируемая сумма. */ + planned_amount: string, + /** Идентификатор получателя. */ + recipient?: string | undefined | null, + /** Тип получателя платежа. */ + recipient_type: GraphQLTypes["ExpenseRecipientType"], + /** Статус строки. */ + status: GraphQLTypes["ExpenseItemStatus"], + ['...on ExpenseItem']: Omit +}; + ["ExpenseItemInput"]: { + /** Описание назначения расхода. */ + description: string, + /** Хеш строки расхода (детерминированный, из UI). */ + item_hash: string, + /** Способ оплаты (ADVANCE / DIRECT). */ + mechanics: GraphQLTypes["ExpenseMechanics"], + /** Идентификатор сохранённых реквизитов получателя-пайщика — реквизиты снимаются в момент создания и прикладываются к платежу. */ + payment_method_id?: string | undefined | null, + /** Назначение платежа (для оплаты по счёту) — фиксируется в снимке для кассира. */ + payment_purpose?: string | undefined | null, + /** Планируемая сумма (asset, eg "1000.0000 RUB"). */ + planned_amount: string, + /** Получатель: username пайщика; для организации — пустая строка (аккаунта в кооперативе нет). */ + recipient: string, + /** Тип получателя. */ + recipient_type: GraphQLTypes["ExpenseRecipientType"], + /** Реквизиты получателя-организации (вводятся вручную). */ + requisites?: string | undefined | null +}; + /** Статус строки расхода. */ +["ExpenseItemStatus"]: ExpenseItemStatus; + /** Способ оплаты строки расхода. */ +["ExpenseMechanics"]: ExpenseMechanics; + /** Смета расхода (СЗ). */ +["ExpenseProposal"]: { + __typename: "ExpenseProposal", + /** Дата создания записи */ + _created_at: GraphQLTypes["DateTime"], + /** Внутренний ID базы данных */ + _id: string, + /** Дата последнего обновления записи */ + _updated_at: GraphQLTypes["DateTime"], + /** Номер блока крайней синхронизации с блокчейном */ + block_num?: number | undefined | null, + /** Сырой статус из блокчейна (uint8). */ + blockchain_status?: number | undefined | null, + /** Имя кооператива (scope). */ + coopname: string, + /** Время создания (chain). */ + created_at?: string | undefined | null, + /** Решение совета о расходе (DocumentAggregate). */ + decision_doc?: GraphQLTypes["DocumentAggregate"] | undefined | null, + /** ID в блокчейне. */ + id?: number | undefined | null, + /** Строки сметы. */ + items: Array, + /** Флаг присутствия записи в блокчейне */ + present: boolean, + /** Хеш сметы расхода. */ + proposal_hash: string, + /** Кошелёк-источник средств. */ + source_wallet?: string | undefined | null, + /** Заявление пайщика о расходе (DocumentAggregate). */ + statement_doc?: GraphQLTypes["DocumentAggregate"] | undefined | null, + /** Доменный статус сметы. */ + status: GraphQLTypes["ExpenseProposalStatus"], + /** Сумма всех строк (факт). */ + total_actual?: string | undefined | null, + /** Сумма всех строк (план). */ + total_planned?: string | undefined | null, + /** Время последнего обновления (chain). */ + updated_at?: string | undefined | null, + /** Создатель сметы (username). */ + username?: string | undefined | null, + ['...on ExpenseProposal']: Omit +}; + ["ExpenseProposalDecisionBodyInput"]: { + /** Род решения (approve / decline) */ + kind: string, + /** Причина отказа (для decline) */ + reason?: string | undefined | null +}; + ["ExpenseProposalDecisionGenerateDocumentInput"]: { + /** Номер блока, на котором был создан документ */ + block_num?: number | undefined | null, + /** Название кооператива, связанное с документом */ + coopname: string, + /** Дата и время создания документа */ + created_at?: string | undefined | null, + /** Идентификатор решения совета (повестка) — источник данных голосования */ + decision_id: number, + /** Имя генератора, использованного для создания документа */ + generator?: string | undefined | null, + /** Позиции расхода */ + items: Array, + /** Язык документа */ + lang?: string | undefined | null, + /** Ссылки, связанные с документом */ + links?: Array | undefined | null, + /** Шапка СЗ */ + proposal: GraphQLTypes["ExpenseProposalDecisionHeaderInput"], + /** Хеш сметы расхода */ + proposal_hash: string, + /** Резолюция совета (утвердить / отказать) */ + resolution: GraphQLTypes["ExpenseProposalDecisionBodyInput"], + /** Часовой пояс, в котором был создан документ */ + timezone?: string | undefined | null, + /** Название документа */ + title?: string | undefined | null, + /** Имя пользователя, создавшего документ */ + username: string, + /** Версия генератора, использованного для создания документа */ + version?: string | undefined | null +}; + ["ExpenseProposalDecisionHeaderInput"]: { + deadline?: string | undefined | null, + description: string, + fund_name?: string | undefined | null, + items_count: number, + source_wallet: string, + total_amount: string +}; + ["ExpenseProposalDecisionItemInput"]: { + amount: string, + description: string, + mechanics: string, + number: string, + recipient_type: string +}; + ["ExpenseProposalHeaderInput"]: { + /** Срок исполнения («в срок до»), формат DD.MM.YYYY */ + deadline?: string | undefined | null, + /** Описание цели расходов */ + description: string, + /** Фонд списания — подставляется сервером из параметров шасси расходов, передавать не нужно */ + fund_name?: string | undefined | null, + /** Количество позиций */ + items_count: number, + /** Кошелёк-источник */ + source_wallet: string, + /** Итоговая сумма расходов */ + total_amount: string +}; + ["ExpenseProposalItemInput"]: { + /** Сумма строки */ + amount: string, + /** Описание расхода */ + description: string, + /** Способ оплаты (ADVANCE / DIRECT) */ + mechanics: string, + /** Порядковый номер строки */ + number: string, + /** Идентификатор сохранённых реквизитов получателя-пайщика — сервер подставит полные реквизиты в документ. */ + payment_method_id?: string | undefined | null, + /** Назначение платежа — отдельной строкой после реквизитов */ + payment_purpose?: string | undefined | null, + /** Имя получателя */ + recipient_name?: string | undefined | null, + /** Тип получателя (SELF / MEMBER / ORG) */ + recipient_type: string, + /** Имя аккаунта получателя-пайщика (владелец реквизитов). */ + recipient_username?: string | undefined | null, + /** Реквизиты получателя */ + requisites?: string | undefined | null +}; + ["ExpenseProposalSignedItemInput"]: { + /** Сумма строки */ + amount: string, + /** Описание расхода */ + description: string, + /** Способ оплаты (ADVANCE / DIRECT) */ + mechanics: string, + /** Порядковый номер строки */ + number: string, + /** Тип получателя (SELF / MEMBER / ORG) */ + recipient_type: string +}; + ["ExpenseProposalStatementGenerateDocumentInput"]: { + /** Номер блока, на котором был создан документ */ + block_num?: number | undefined | null, + /** Название кооператива, связанное с документом */ + coopname: string, + /** Дата и время создания документа */ + created_at?: string | undefined | null, + /** Имя генератора, использованного для создания документа */ + generator?: string | undefined | null, + /** Позиции расхода */ + items: Array, + /** Язык документа */ + lang?: string | undefined | null, + /** Ссылки, связанные с документом */ + links?: Array | undefined | null, + /** Шапка СЗ */ + proposal: GraphQLTypes["ExpenseProposalHeaderInput"], + /** Хеш сметы расхода (детерминированный) */ + proposal_hash: string, + /** Часовой пояс, в котором был создан документ */ + timezone?: string | undefined | null, + /** Название документа */ + title?: string | undefined | null, + /** Имя пользователя, создавшего документ */ + username: string, + /** Версия генератора, использованного для создания документа */ + version?: string | undefined | null +}; + ["ExpenseProposalStatementSignedDocumentInput"]: { + /** Хэш содержимого документа */ + doc_hash: string, + /** Общий хэш (doc_hash + meta_hash) */ + hash: string, + /** Метаинформация СЗ-заявления */ + meta: GraphQLTypes["ExpenseProposalStatementSignedMetaDocumentInput"], + /** Хэш мета-данных */ + meta_hash: string, + /** Вектор подписей */ + signatures: Array, + /** Версия стандарта документа */ + version: string +}; + ["ExpenseProposalStatementSignedMetaDocumentInput"]: { + /** Номер блока, на котором был создан документ */ + block_num: number, + /** Название кооператива, связанное с документом */ + coopname: string, + /** Дата и время создания документа */ + created_at: string, + /** Идентификатор приватных данных документа off-chain (реквизиты/имя/назначение) */ + doc_data_hash: string, + /** Имя генератора, использованного для создания документа */ + generator: string, + /** Публичные позиции расхода (без реквизитов) */ + items: Array, + /** Язык документа */ + lang: string, + /** Ссылки, связанные с документом */ + links: Array, + /** Шапка СЗ */ + proposal: GraphQLTypes["ExpenseProposalHeaderInput"], + /** Хеш сметы расхода (детерминированный) */ + proposal_hash: string, + /** ID документа в реестре */ + registry_id: number, + /** Часовой пояс, в котором был создан документ */ + timezone: string, + /** Название документа */ + title: string, + /** Имя пользователя, создавшего документ */ + username: string, + /** Версия генератора, использованного для создания документа */ + version: string +}; + /** Статус сметы расхода. */ +["ExpenseProposalStatus"]: ExpenseProposalStatus; + /** Тип получателя платежа. */ +["ExpenseRecipientType"]: ExpenseRecipientType; + /** Исход отчёта по строке-авансу: закрыто либо ожидается расчёт разницы (возврат/доплата). */ +["ExpenseReportOutcome"]: ExpenseReportOutcome; + ["ExpenseReportResult"]: { + __typename: "ExpenseReportResult", + /** Исход отчёта. */ + outcome: GraphQLTypes["ExpenseReportOutcome"], + /** Сумма разницы к расчёту (asset), при недо-/перерасходе. */ + settlement_amount?: string | undefined | null, + /** Хэш заведённой платёжки расчёта (возврат/доплата). */ + settlement_payment_hash?: string | undefined | null, + /** Транзакция закрытия позиции (только при CLOSED). */ + transaction?: GraphQLTypes["Transaction"] | undefined | null, + ['...on ExpenseReportResult']: Omit +}; + ["ExpenseRequisite"]: { + __typename: "ExpenseRequisite", + /** Имя кооператива. */ + coopname: string, + /** Снимок данных платёжного метода на момент подачи СЗ. */ + data?: GraphQLTypes["JSON"] | undefined | null, + /** Хеш строки расхода (item). */ + item_hash: string, + /** Тип платёжного метода пайщика (СБП / банковский перевод). */ + method_type?: string | undefined | null, + /** Назначение платежа для поручения кассиру. */ + payment_purpose?: string | undefined | null, + /** Хеш сметы расхода (proposal). */ + proposal_hash: string, + /** Получатель платежа (аккаунт пайщика или название организации). */ + recipient: string, + /** Реквизиты строкой — как в документе служебной записки. */ + requisites: string, + ['...on ExpenseRequisite']: Omit }; /** Статус расхода в системе CAPITAL */ ["ExpenseStatus"]: ExpenseStatus; @@ -32732,125 +34370,6 @@ export type GraphQLTypes = { votes_for: number, ['...on MeetQuestionResult']: Omit }; - ["MembershipExit"]: { - __typename: "MembershipExit", - /** Дата подачи заявления на выход */ - created_at: string, - /** Хеш процесса выхода */ - exit_hash: string, - /** Сумма к возврату (фиксируется советом при одобрении; до одобрения — 0) */ - quantity: string, - /** Статус процесса выхода */ - status: GraphQLTypes["MembershipExitStatus"], - ['...on MembershipExit']: Omit -}; - ["MembershipExitApplicationGenerateDocumentInput"]: { - /** Номер блока, на котором был создан документ */ - block_num?: number | undefined | null, - /** Название кооператива, связанное с документом */ - coopname: string, - /** Дата и время создания документа */ - created_at?: string | undefined | null, - /** Имя генератора, использованного для создания документа */ - generator?: string | undefined | null, - /** Язык документа */ - lang?: string | undefined | null, - /** Ссылки, связанные с документом */ - links?: Array | undefined | null, - /** Флаг пропуска сохранения документа (используется для предварительной генерации и демонстрации пользователю) */ - skip_save: boolean, - /** Часовой пояс, в котором был создан документ */ - timezone?: string | undefined | null, - /** Название документа */ - title?: string | undefined | null, - /** Имя пользователя, создавшего документ */ - username: string, - /** Версия генератора, использованного для создания документа */ - version?: string | undefined | null -}; - ["MembershipExitApplicationSignedDocumentInput"]: { - /** Хэш содержимого документа */ - doc_hash: string, - /** Общий хэш (doc_hash + meta_hash) */ - hash: string, - meta: GraphQLTypes["MembershipExitApplicationSignedMetaDocumentInput"], - /** Хэш мета-данных */ - meta_hash: string, - /** Вектор подписей */ - signatures: Array, - /** Версия стандарта документа */ - version: string -}; - ["MembershipExitApplicationSignedMetaDocumentInput"]: { - /** Номер блока, на котором был создан документ */ - block_num: number, - /** Название кооператива, связанное с документом */ - coopname: string, - /** Дата и время создания документа */ - created_at: string, - /** Имя генератора, использованного для создания документа */ - generator: string, - /** Язык документа */ - lang: string, - /** Ссылки, связанные с документом */ - links: Array, - /** ID документа в реестре */ - registry_id: number, - /** Флаг пропуска сохранения документа (используется для предварительной генерации и демонстрации пользователю) */ - skip_save: boolean, - /** Часовой пояс, в котором был создан документ */ - timezone: string, - /** Название документа */ - title: string, - /** Имя пользователя, создавшего документ */ - username: string, - /** Версия генератора, использованного для создания документа */ - version: string -}; - ["MembershipExitDecisionGenerateDocumentInput"]: { - /** Номер блока, на котором был создан документ */ - block_num?: number | undefined | null, - /** Название кооператива, связанное с документом */ - coopname: string, - /** Дата и время создания документа */ - created_at?: string | undefined | null, - /** Идентификатор протокола решения собрания совета */ - decision_id: number, - /** Имя генератора, использованного для создания документа */ - generator?: string | undefined | null, - /** Язык документа */ - lang?: string | undefined | null, - /** Ссылки, связанные с документом */ - links?: Array | undefined | null, - /** Часовой пояс, в котором был создан документ */ - timezone?: string | undefined | null, - /** Название документа */ - title?: string | undefined | null, - /** Имя пользователя, создавшего документ */ - username: string, - /** Версия генератора, использованного для создания документа */ - version?: string | undefined | null -}; - ["MembershipExitResult"]: { - __typename: "MembershipExitResult", - /** Хеш созданного процесса выхода */ - exit_hash: string, - /** Статус процесса выхода после подачи (ожидает подтверждения по ссылке из письма) */ - status: GraphQLTypes["MembershipExitStatus"], - ['...on MembershipExitResult']: Omit -}; - ["MembershipExitReturnPreview"]: { - __typename: "MembershipExitReturnPreview", - /** Минимальный паевой взнос пайщика */ - minimum_contribution: string, - /** Целевой паевой взнос пайщика */ - share_contribution: string, - /** Итоговая сумма к возврату (минимальный + целевой паевой) */ - total: string, - ['...on MembershipExitReturnPreview']: Omit -}; - /** Статус процесса выхода пайщика из кооператива */ -["MembershipExitStatus"]: MembershipExitStatus; ["MissingRequisiteField"]: { __typename: "MissingRequisiteField", key: string, @@ -32925,8 +34444,6 @@ export type GraphQLTypes = { Требуемые роли: chairman. */ authorizeDecision: GraphQLTypes["Transaction"], - /** Отменить заявление на выход до подтверждения по email. */ - cancelMembershipExit: boolean, /** Отменить заявку */ cancelRequest: GraphQLTypes["Transaction"], /** Добавление автора проекта в CAPITAL контракте @@ -32985,6 +34502,10 @@ export type GraphQLTypes = { Требуемые роли: chairman, member. */ capitalCreateProcessTemplate: GraphQLTypes["ProcessTemplate"], + /** Создание программного расхода капитала через шасси expense. + +Требуемые роли: chairman, member. */ + capitalCreateProgramExpense: GraphQLTypes["Transaction"], /** Инвестирование в программу благорост (денежная программная инвестиция) Требуемые роли: participant. */ @@ -33209,6 +34730,10 @@ export type GraphQLTypes = { Требуемые роли: chairman, member, user. */ capitalSubmitVote: GraphQLTypes["Transaction"], + /** Пополнение пула программных расходов капитала из инвестиционного пула. + +Требуемые роли: chairman. */ + capitalTopupProgramExpensePool: GraphQLTypes["Transaction"], /** Обновление задачи в CAPITAL контракте Требуемые роли: chairman, member, user. */ @@ -33283,8 +34808,6 @@ export type GraphQLTypes = { Требуемые роли: chairman, member. */ confirmAgreement: GraphQLTypes["Transaction"], - /** Подтвердить выход из кооператива по ссылке из письма. Проверяет токен и отправляет ранее подписанное заявление в блокчейн. */ - confirmMembershipExit: GraphQLTypes["MembershipExitResult"], /** Подтвердить получение имущества Уполномоченным лицом от Заказчика по новации и акту приёмки-передачи */ confirmReceiveOnRequest: GraphQLTypes["Transaction"], /** Подтвердить поставку имущества Поставщиком по заявке Заказчика и акту приёма-передачи */ @@ -33303,12 +34826,14 @@ export type GraphQLTypes = { Требуемые роли: chairman, member. */ createDepositPayment: GraphQLTypes["GatewayPayment"], + /** Подать СЗ-расход (создать смету с подписью пайщика/председателя). + +Требуемые роли: chairman, member. */ + createExpenseProposal: GraphQLTypes["Transaction"], /** Создание объекта регистрационного платежа производится мутацией createInitialPayment. Выполнение мутации возвращает идентификатор платежа и данные для его совершения в зависимости от выбранного платежного провайдера. Требуемые роли: chairman, member. */ createInitialPayment: GraphQLTypes["GatewayPayment"], - /** Подать подписанное заявление на выход из кооператива. Запускает рассмотрение советом и последующий возврат паевого взноса. */ - createMembershipExit: GraphQLTypes["MembershipExitResult"], /** Создать предложение на поставку имущества */ createParentOffer: GraphQLTypes["Transaction"], /** Создать повестку дня и проект решения, и сохранить в хранилище для дальнейшей генерации документа и его публикации @@ -33397,16 +34922,18 @@ export type GraphQLTypes = { generateConvertToAxonStatement: GraphQLTypes["GeneratedDocument"], /** Универсальная генерация документа с произвольными данными (только для председателя) */ generateDocument: GraphQLTypes["GeneratedDocument"], + /** Сгенерировать документ-решение по СЗ (registry 2011) для последующей подписи. + +Требуемые роли: chairman. */ + generateExpenseProposalDecisionDocument: GraphQLTypes["GeneratedDocument"], + /** Сгенерировать документ СЗ-заявления (registry 2010) для последующей подписи. + +Требуемые роли: chairman, member. */ + generateExpenseProposalStatementDocument: GraphQLTypes["GeneratedDocument"], /** Сгенерировать протокол решения по предложенной повестке Требуемые роли: chairman, member. */ generateFreeDecision: GraphQLTypes["GeneratedDocument"], - /** Сгенерировать документ заявления о выходе из кооператива. */ - generateMembershipExitApplication: GraphQLTypes["GeneratedDocument"], - /** Сгенерировать документ решения собрания совета о выходе пайщика. - -Требуемые роли: chairman, member. */ - generateMembershipExitDecision: GraphQLTypes["GeneratedDocument"], /** Сгенерировать документ заявления о вступлении в кооператив. Требуемые роли: chairman, member. */ @@ -33501,6 +35028,14 @@ export type GraphQLTypes = { Требуемые роли: chairman, member. */ notifyOnAnnualGeneralMeet: GraphQLTypes["MeetAggregate"], + /** Доплатить сумму перерасхода по строке расхода (ADVANCE-механика). + +Требуемые роли: chairman. */ + overspendExpenseItem: GraphQLTypes["Transaction"], + /** Оплатить строку расхода (выдача аванса ADVANCE или прямая оплата DIRECT). + +Требуемые роли: chairman. */ + payExpenseItem: GraphQLTypes["Transaction"], /** Обрабатывает подписанное заявление на конвертацию и выполняет блокчейн-транзакцию Требуемые роли: member, chairman. */ @@ -33523,6 +35058,10 @@ export type GraphQLTypes = { Требуемые роли: chairman, member. */ registerParticipant: GraphQLTypes["Account"], + /** Отчитаться по строке-авансу: при совпадении факта с авансом — закрыть позицию; при недо-/перерасходе — завести платёжку расчёта разницы. + +Требуемые роли: chairman, member, user. */ + reportExpenseItem: GraphQLTypes["ExpenseReportResult"], /** Переотправить уведомление (force-постановка новой строки в очередь доставки) Требуемые роли: chairman. */ @@ -33535,6 +35074,10 @@ export type GraphQLTypes = { Требуемые роли: chairman. */ restartAnnualGeneralMeet: GraphQLTypes["MeetAggregate"], + /** Вернуть неиспользованный аванс по строке расхода (ADVANCE-остаток). + +Требуемые роли: chairman, member, user. */ + returnExpenseItem: GraphQLTypes["Transaction"], /** Сохранить/обновить черновик формы отчёта (upsert по owner+type+year+period) Требуемые роли: chairman. */ @@ -33563,6 +35106,10 @@ export type GraphQLTypes = { startInstall: GraphQLTypes["StartInstallResult"], /** Выслать токен для замены приватного ключа аккаунта на электронную почту */ startResetKey: boolean, + /** Финализировать СЗ-отчёт по смете расхода (все items закрыты — оплата/чек/возврат). + +Требуемые роли: chairman, member. */ + submitExpenseReport: GraphQLTypes["Transaction"], /** Подтвердить поставку имущества Поставщиком по заявке Заказчика и акту приёма-передачи */ supplyOnRequest: GraphQLTypes["Transaction"], /** Запустить воркфлоу уведомлений (только для председателя или server-secret) @@ -33599,6 +35146,10 @@ export type GraphQLTypes = { Требуемые роли: chairman. */ updateSystem: GraphQLTypes["SystemInfo"], + /** Загрузить первичный файл расхода (платёжка/чек/возврат) в бакет expenses:files. + +Требуемые роли: chairman, member, user. */ + uploadExpenseFile: GraphQLTypes["ExpenseFile"], /** Подтвердить email адрес пользователя */ verifyEmail: boolean, /** Голосование на общем собрании пайщиков @@ -33847,6 +35398,16 @@ export type GraphQLTypes = { /** Имя аккаунта организации */ username: string, ['...on OrganizationWithBankAccount']: Omit +}; + ["OverspendExpenseItemInput"]: { + /** Имя кооператива. */ + coopname: string, + /** Хеш строки расхода (item). */ + item_hash: string, + /** Сумма доплаты сверх planned_amount (asset, например "200.0000 RUB"). */ + overspend_amount: string, + /** Хеш сметы расхода (proposal). */ + proposal_hash: string }; ["PaginatedActionsPaginationResult"]: { __typename: "PaginatedActionsPaginationResult", @@ -33991,6 +35552,18 @@ export type GraphQLTypes = { /** Общее количество страниц */ totalPages: number, ['...on PaginatedCapitalLogsPaginationResult']: Omit +}; + ["PaginatedCapitalProgramExpensesPaginationResult"]: { + __typename: "PaginatedCapitalProgramExpensesPaginationResult", + /** Текущая страница */ + currentPage: number, + /** Элементы текущей страницы */ + items: Array, + /** Общее количество элементов */ + totalCount: number, + /** Общее количество страниц */ + totalPages: number, + ['...on PaginatedCapitalProgramExpensesPaginationResult']: Omit }; ["PaginatedCapitalProjectsPaginationResult"]: { __typename: "PaginatedCapitalProjectsPaginationResult", @@ -34111,6 +35684,18 @@ export type GraphQLTypes = { /** Общее количество страниц */ totalPages: number, ['...on PaginatedDeltasPaginationResult']: Omit +}; + ["PaginatedExpenseProposalsPaginationResult"]: { + __typename: "PaginatedExpenseProposalsPaginationResult", + /** Текущая страница */ + currentPage: number, + /** Элементы текущей страницы */ + items: Array, + /** Общее количество элементов */ + totalCount: number, + /** Общее количество страниц */ + totalPages: number, + ['...on PaginatedExpenseProposalsPaginationResult']: Omit }; ["PaginatedGatewayPaymentsPaginationResult"]: { __typename: "PaginatedGatewayPaymentsPaginationResult", @@ -34277,6 +35862,16 @@ export type GraphQLTypes = { issued_by: string, number: number, series: number +}; + ["PayExpenseItemInput"]: { + /** Фактическая сумма оплаты (asset, например "100.0000 RUB"). */ + actual_amount: string, + /** Имя кооператива. */ + coopname: string, + /** Хеш строки расхода (item). */ + item_hash: string, + /** Хеш сметы расхода (proposal). */ + proposal_hash: string }; ["PaymentDetails"]: { __typename: "PaymentDetails", @@ -34305,6 +35900,8 @@ export type GraphQLTypes = { direction?: GraphQLTypes["PaymentDirection"] | undefined | null, /** Хэш платежа */ hash?: string | undefined | null, + /** Хэш расхода (служебной записки): вернёт все платежи, связанные с этим расходом */ + proposal_hash?: string | undefined | null, /** Провайдер платежа */ provider?: string | undefined | null, /** Статус платежа */ @@ -34924,6 +36521,14 @@ export type GraphQLTypes = { Требуемые роли: chairman, member, user. */ capitalIssues: GraphQLTypes["PaginatedCapitalIssuesPaginationResult"], + /** Программный расход по expense_hash. + +Требуемые роли: chairman, member. */ + capitalProgramExpense?: GraphQLTypes["CapitalProgramExpense"] | undefined | null, + /** Список программных расходов капитала (через шасси expense). + +Требуемые роли: chairman, member. */ + capitalProgramExpenses: GraphQLTypes["PaginatedCapitalProgramExpensesPaginationResult"], /** Получение проекта по хешу с компонентами */ capitalProject?: GraphQLTypes["CapitalProject"] | undefined | null, /** Получение проекта с полными отношениями по хешу проекта */ @@ -35028,6 +36633,34 @@ export type GraphQLTypes = { cooperativeAgreements: Array, /** Целевые потребительские программы кооператива (id, тип, активность, draft_id) */ cooperativePrograms: Array, + /** Получить запись о файле + свежий короткоживущий read-URL. + +Требуемые роли: chairman, member, user. */ + expenseFile: GraphQLTypes["ExpenseFile"], + /** Список файлов строки расхода (без read-URL — запрос отдельно по id). + +Требуемые роли: chairman, member, user. */ + expenseFilesByItem: Array, + /** Список файлов сметы расхода (без read-URL — запрос отдельно по id). + +Требуемые роли: chairman, member, user. */ + expenseFilesByProposal: Array, + /** Получить смету расхода по хешу. + +Требуемые роли: chairman, member, user. */ + expenseProposal?: GraphQLTypes["ExpenseProposal"] | undefined | null, + /** Список смет расходов кооператива (paginated). + +Требуемые роли: chairman, member. */ + expenseProposalsByCooperative: GraphQLTypes["PaginatedExpenseProposalsPaginationResult"], + /** Список смет расходов пайщика (свои/созданные им, paginated). + +Требуемые роли: chairman, member, user. */ + expenseProposalsByMember: GraphQLTypes["PaginatedExpenseProposalsPaginationResult"], + /** Снимки реквизитов получателей по строкам СЗ (персональные данные — только совету; в блокчейн не пишутся). + +Требуемые роли: chairman, member. */ + expenseRequisitesByProposal: Array, /** Получить сводную информацию о аккаунте Требуемые роли: chairman, member. */ @@ -35208,10 +36841,6 @@ export type GraphQLTypes = { Требуемые роли: chairman. */ listReportDrafts: Array, - /** Текущий процесс выхода пайщика из кооператива (статус заявления и планируемая сумма возврата). null — активного выхода нет. */ - membershipExit?: GraphQLTypes["MembershipExit"] | undefined | null, - /** Предварительный расчёт суммы возврата паевого взноса при выходе пайщика (минимальный + целевой паевой). Ориентир для пайщика; итог фиксирует совет. */ - membershipExitReturnPreview: GraphQLTypes["MembershipExitReturnPreview"], /** Получение документов кооператива для синхронизации с 1С. Требует секретный ключ в заголовке x-onecoop-secret-key. */ onecoopGetDocuments: GraphQLTypes["OneCoopDocumentsResponse"], /** Получить полную картину процесса ledger2 по process_hash @@ -35470,6 +37099,16 @@ export type GraphQLTypes = { updatedAt: GraphQLTypes["DateTime"], year: number, ['...on ReportDraft']: Omit +}; + ["ReportExpenseItemInput"]: { + /** Фактически потраченная сумма по чекам (asset, например "800.0000 RUB"). Не указана — равна выданному авансу. */ + actual_amount?: string | undefined | null, + /** Имя кооператива. */ + coopname: string, + /** Хеш строки расхода (item). */ + item_hash: string, + /** Хеш сметы расхода (proposal). */ + proposal_hash: string }; ["ReportHistoryFilterInput"]: { /** Лимит (макс 100, по умолчанию 20) */ @@ -35939,6 +37578,16 @@ export type GraphQLTypes = { username: string, /** Версия генератора, использованного для создания документа */ version: string +}; + ["ReturnExpenseItemInput"]: { + /** Имя кооператива. */ + coopname: string, + /** Хеш строки расхода (item). */ + item_hash: string, + /** Хеш сметы расхода (proposal). */ + proposal_hash: string, + /** Возвращаемая сумма (asset, например "50.0000 RUB"). */ + return_amount: string }; /** Тип сообщения в истории комнаты Matrix (текст или расшифрованное аудио) */ ["RoomMessageKind"]: RoomMessageKind; @@ -36326,6 +37975,16 @@ export type GraphQLTypes = { }; /** Статус истории в системе CAPITAL */ ["StoryStatus"]: StoryStatus; + ["SubmitExpenseReportInput"]: { + /** Комментарий пайщика к финализации отчёта (свободный текст, до 1000 символов). */ + comment?: string | undefined | null, + /** Имя кооператива. */ + coopname: string, + /** Хеш сметы расхода. */ + proposal_hash: string, + /** Итоговая сумма фактических расходов (asset, например "1500.0000 RUB"). */ + total_actual_amount?: string | undefined | null +}; ["SubmitVoteInput"]: { /** Имя аккаунта кооператива */ coopname: string, @@ -36701,6 +38360,26 @@ export type GraphQLTypes = { story_hash: string, /** Название истории */ title?: string | undefined | null +}; + ["UploadExpenseFileInput"]: { + /** SHA-256 содержимого, hex-lowercase (64 hex-символа). */ + checksum_sha256: string, + /** Содержимое файла, base64 без префикса data:. */ + content_base64: string, + /** Имя кооператива. */ + coopname: string, + /** Хеш строки расхода (пусто — файл уровня сметы). */ + item_hash?: string | undefined | null, + /** Назначение файла (платёжка/чек/возврат). */ + kind: GraphQLTypes["ExpenseFileKind"], + /** MIME-тип содержимого. */ + mime_type: string, + /** Оригинальное имя файла — для отображения и поиска. */ + original_filename?: string | undefined | null, + /** Хеш сметы расхода. */ + proposal_hash: string, + /** Размер файла в байтах (для серверной валидации). */ + size_bytes: number }; ["UserAccount"]: { __typename: "UserAccount", @@ -37028,6 +38707,49 @@ export enum DocumentAction { REGCOOP = "REGCOOP", WITHDRAW = "WITHDRAW" } +/** Тип первичного файла расхода. */ +export enum ExpenseFileKind { + CLOSING_DOC = "CLOSING_DOC", + PAYMENT_PROOF = "PAYMENT_PROOF", + REPORT_FILE = "REPORT_FILE", + RETURN_PROOF = "RETURN_PROOF" +} +/** Статус строки расхода. */ +export enum ExpenseItemStatus { + APPROVED = "APPROVED", + OVERSPENT = "OVERSPENT", + PAID = "PAID", + REPORTED = "REPORTED", + RETURNED = "RETURNED", + UNDEFINED = "UNDEFINED" +} +/** Способ оплаты строки расхода. */ +export enum ExpenseMechanics { + ADVANCE = "ADVANCE", + DIRECT = "DIRECT" +} +/** Статус сметы расхода. */ +export enum ExpenseProposalStatus { + AUTHORIZED = "AUTHORIZED", + CLOSED = "CLOSED", + CREATED = "CREATED", + DECLINED = "DECLINED", + PARTIALLY_PAID = "PARTIALLY_PAID", + REPORT_SUBMITTED = "REPORT_SUBMITTED", + UNDEFINED = "UNDEFINED" +} +/** Тип получателя платежа. */ +export enum ExpenseRecipientType { + MEMBER = "MEMBER", + ORG = "ORG", + SELF = "SELF" +} +/** Исход отчёта по строке-авансу: закрыто либо ожидается расчёт разницы (возврат/доплата). */ +export enum ExpenseReportOutcome { + CLOSED = "CLOSED", + OVERSPEND_PENDING = "OVERSPEND_PENDING", + RETURN_PENDING = "RETURN_PENDING" +} /** Статус расхода в системе CAPITAL */ export enum ExpenseStatus { APPROVED = "APPROVED", @@ -37150,12 +38872,6 @@ export enum ManagedRoomKind { MEMBERS = "MEMBERS", SECRETARY = "SECRETARY" } -/** Статус процесса выхода пайщика из кооператива */ -export enum MembershipExitStatus { - AUTHORIZED = "AUTHORIZED", - AWAITING_CONFIRMATION = "AWAITING_CONFIRMATION", - PENDING = "PENDING" -} /** Тип комнаты вне проекта: пайщики, совет, комната секретаря */ export enum NonProjectRoomKind { COUNCIL = "COUNCIL", @@ -37211,7 +38927,9 @@ export enum PaymentStatus { /** Тип платежа по назначению */ export enum PaymentType { DEPOSIT = "DEPOSIT", - MEMBERSHIP_EXIT = "MEMBERSHIP_EXIT", + EXPENSE = "EXPENSE", + EXPENSE_OVERSPEND = "EXPENSE_OVERSPEND", + EXPENSE_RETURN = "EXPENSE_RETURN", REGISTRATION = "REGISTRATION", REGISTRATION_REFUND = "REGISTRATION_REFUND", WITHDRAWAL = "WITHDRAWAL" @@ -37390,6 +39108,7 @@ type ZEUS_VARIABLES = { ["CandidateStatus"]: ValueTypes["CandidateStatus"]; ["CapitalCommitFilter"]: ValueTypes["CapitalCommitFilter"]; ["CapitalContributorFilter"]: ValueTypes["CapitalContributorFilter"]; + ["CapitalCreateProgramExpenseInput"]: ValueTypes["CapitalCreateProgramExpenseInput"]; ["CapitalCycleFilter"]: ValueTypes["CapitalCycleFilter"]; ["CapitalInvestFilter"]: ValueTypes["CapitalInvestFilter"]; ["CapitalIssueFilter"]: ValueTypes["CapitalIssueFilter"]; @@ -37402,6 +39121,7 @@ type ZEUS_VARIABLES = { ["CapitalStoryFilter"]: ValueTypes["CapitalStoryFilter"]; ["CapitalTimeEntriesFilter"]: ValueTypes["CapitalTimeEntriesFilter"]; ["CapitalTimeStatsInput"]: ValueTypes["CapitalTimeStatsInput"]; + ["CapitalTopupProgramExpenseInput"]: ValueTypes["CapitalTopupProgramExpenseInput"]; ["ChairmanOnboardingAgendaInput"]: ValueTypes["ChairmanOnboardingAgendaInput"]; ["ChairmanOnboardingAgendaStep"]: ValueTypes["ChairmanOnboardingAgendaStep"]; ["ChairmanOnboardingGeneralMeetInput"]: ValueTypes["ChairmanOnboardingGeneralMeetInput"]; @@ -37438,12 +39158,12 @@ type ZEUS_VARIABLES = { ["CreateDepositPaymentInput"]: ValueTypes["CreateDepositPaymentInput"]; ["CreateEntrepreneurDataInput"]: ValueTypes["CreateEntrepreneurDataInput"]; ["CreateExpenseInput"]: ValueTypes["CreateExpenseInput"]; + ["CreateExpenseProposalInput"]: ValueTypes["CreateExpenseProposalInput"]; ["CreateIndividualDataInput"]: ValueTypes["CreateIndividualDataInput"]; ["CreateInitOrganizationDataInput"]: ValueTypes["CreateInitOrganizationDataInput"]; ["CreateInitialPaymentInput"]: ValueTypes["CreateInitialPaymentInput"]; ["CreateIssueInput"]: ValueTypes["CreateIssueInput"]; ["CreateMatrixAccountInputDTO"]: ValueTypes["CreateMatrixAccountInputDTO"]; - ["CreateMembershipExitInput"]: ValueTypes["CreateMembershipExitInput"]; ["CreateOrganizationDataInput"]: ValueTypes["CreateOrganizationDataInput"]; ["CreateParentOfferInput"]: ValueTypes["CreateParentOfferInput"]; ["CreateProcessTemplateInput"]: ValueTypes["CreateProcessTemplateInput"]; @@ -37483,7 +39203,25 @@ type ZEUS_VARIABLES = { ["EditContributorInput"]: ValueTypes["EditContributorInput"]; ["EditProjectInput"]: ValueTypes["EditProjectInput"]; ["EntrepreneurDetailsInput"]: ValueTypes["EntrepreneurDetailsInput"]; + ["ExpenseCallbackInput"]: ValueTypes["ExpenseCallbackInput"]; + ["ExpenseFileKind"]: ValueTypes["ExpenseFileKind"]; ["ExpenseFilter"]: ValueTypes["ExpenseFilter"]; + ["ExpenseItemInput"]: ValueTypes["ExpenseItemInput"]; + ["ExpenseItemStatus"]: ValueTypes["ExpenseItemStatus"]; + ["ExpenseMechanics"]: ValueTypes["ExpenseMechanics"]; + ["ExpenseProposalDecisionBodyInput"]: ValueTypes["ExpenseProposalDecisionBodyInput"]; + ["ExpenseProposalDecisionGenerateDocumentInput"]: ValueTypes["ExpenseProposalDecisionGenerateDocumentInput"]; + ["ExpenseProposalDecisionHeaderInput"]: ValueTypes["ExpenseProposalDecisionHeaderInput"]; + ["ExpenseProposalDecisionItemInput"]: ValueTypes["ExpenseProposalDecisionItemInput"]; + ["ExpenseProposalHeaderInput"]: ValueTypes["ExpenseProposalHeaderInput"]; + ["ExpenseProposalItemInput"]: ValueTypes["ExpenseProposalItemInput"]; + ["ExpenseProposalSignedItemInput"]: ValueTypes["ExpenseProposalSignedItemInput"]; + ["ExpenseProposalStatementGenerateDocumentInput"]: ValueTypes["ExpenseProposalStatementGenerateDocumentInput"]; + ["ExpenseProposalStatementSignedDocumentInput"]: ValueTypes["ExpenseProposalStatementSignedDocumentInput"]; + ["ExpenseProposalStatementSignedMetaDocumentInput"]: ValueTypes["ExpenseProposalStatementSignedMetaDocumentInput"]; + ["ExpenseProposalStatus"]: ValueTypes["ExpenseProposalStatus"]; + ["ExpenseRecipientType"]: ValueTypes["ExpenseRecipientType"]; + ["ExpenseReportOutcome"]: ValueTypes["ExpenseReportOutcome"]; ["ExpenseStatus"]: ValueTypes["ExpenseStatus"]; ["ExtendedMeetStatus"]: ValueTypes["ExtendedMeetStatus"]; ["ExtensionInput"]: ValueTypes["ExtensionInput"]; @@ -37555,11 +39293,6 @@ type ZEUS_VARIABLES = { ["MakeClearanceInput"]: ValueTypes["MakeClearanceInput"]; ["ManagedRoomKind"]: ValueTypes["ManagedRoomKind"]; ["MarkReportPeriodInput"]: ValueTypes["MarkReportPeriodInput"]; - ["MembershipExitApplicationGenerateDocumentInput"]: ValueTypes["MembershipExitApplicationGenerateDocumentInput"]; - ["MembershipExitApplicationSignedDocumentInput"]: ValueTypes["MembershipExitApplicationSignedDocumentInput"]; - ["MembershipExitApplicationSignedMetaDocumentInput"]: ValueTypes["MembershipExitApplicationSignedMetaDocumentInput"]; - ["MembershipExitDecisionGenerateDocumentInput"]: ValueTypes["MembershipExitDecisionGenerateDocumentInput"]; - ["MembershipExitStatus"]: ValueTypes["MembershipExitStatus"]; ["ModerateRequestInput"]: ValueTypes["ModerateRequestInput"]; ["MoveCapitalIssueToComponentInput"]: ValueTypes["MoveCapitalIssueToComponentInput"]; ["NonProjectRoomKind"]: ValueTypes["NonProjectRoomKind"]; @@ -37572,12 +39305,14 @@ type ZEUS_VARIABLES = { ["OpenProjectInput"]: ValueTypes["OpenProjectInput"]; ["OrganizationDetailsInput"]: ValueTypes["OrganizationDetailsInput"]; ["OrganizationType"]: ValueTypes["OrganizationType"]; + ["OverspendExpenseItemInput"]: ValueTypes["OverspendExpenseItemInput"]; ["PaginationInput"]: ValueTypes["PaginationInput"]; ["ParticipantApplicationDecisionGenerateDocumentInput"]: ValueTypes["ParticipantApplicationDecisionGenerateDocumentInput"]; ["ParticipantApplicationGenerateDocumentInput"]: ValueTypes["ParticipantApplicationGenerateDocumentInput"]; ["ParticipantApplicationSignedDocumentInput"]: ValueTypes["ParticipantApplicationSignedDocumentInput"]; ["ParticipantApplicationSignedMetaDocumentInput"]: ValueTypes["ParticipantApplicationSignedMetaDocumentInput"]; ["PassportInput"]: ValueTypes["PassportInput"]; + ["PayExpenseItemInput"]: ValueTypes["PayExpenseItemInput"]; ["PaymentDirection"]: ValueTypes["PaymentDirection"]; ["PaymentFiltersInput"]: ValueTypes["PaymentFiltersInput"]; ["PaymentStatus"]: ValueTypes["PaymentStatus"]; @@ -37613,6 +39348,7 @@ type ZEUS_VARIABLES = { ["RegisterContributorInput"]: ValueTypes["RegisterContributorInput"]; ["RegisterParticipantInput"]: ValueTypes["RegisterParticipantInput"]; ["RemoveSecretaryRoomInput"]: ValueTypes["RemoveSecretaryRoomInput"]; + ["ReportExpenseItemInput"]: ValueTypes["ReportExpenseItemInput"]; ["ReportHistoryFilterInput"]: ValueTypes["ReportHistoryFilterInput"]; ["ReportPreviewInput"]: ValueTypes["ReportPreviewInput"]; ["ReportSubmissionMark"]: ValueTypes["ReportSubmissionMark"]; @@ -37637,6 +39373,7 @@ type ZEUS_VARIABLES = { ["ReturnByMoneyGenerateDocumentInput"]: ValueTypes["ReturnByMoneyGenerateDocumentInput"]; ["ReturnByMoneySignedDocumentInput"]: ValueTypes["ReturnByMoneySignedDocumentInput"]; ["ReturnByMoneySignedMetaDocumentInput"]: ValueTypes["ReturnByMoneySignedMetaDocumentInput"]; + ["ReturnExpenseItemInput"]: ValueTypes["ReturnExpenseItemInput"]; ["RoomMessageKind"]: ValueTypes["RoomMessageKind"]; ["SaveReportDraftInput"]: ValueTypes["SaveReportDraftInput"]; ["SbpDataInput"]: ValueTypes["SbpDataInput"]; @@ -37669,6 +39406,7 @@ type ZEUS_VARIABLES = { ["StartVotingInput"]: ValueTypes["StartVotingInput"]; ["StopProjectInput"]: ValueTypes["StopProjectInput"]; ["StoryStatus"]: ValueTypes["StoryStatus"]; + ["SubmitExpenseReportInput"]: ValueTypes["SubmitExpenseReportInput"]; ["SubmitVoteInput"]: ValueTypes["SubmitVoteInput"]; ["SupplyOnRequestInput"]: ValueTypes["SupplyOnRequestInput"]; ["SystemStatus"]: ValueTypes["SystemStatus"]; @@ -37690,6 +39428,7 @@ type ZEUS_VARIABLES = { ["UpdateRequestInput"]: ValueTypes["UpdateRequestInput"]; ["UpdateSettingsInput"]: ValueTypes["UpdateSettingsInput"]; ["UpdateStoryInput"]: ValueTypes["UpdateStoryInput"]; + ["UploadExpenseFileInput"]: ValueTypes["UploadExpenseFileInput"]; ["UserStatus"]: ValueTypes["UserStatus"]; ["VarsInput"]: ValueTypes["VarsInput"]; ["VerifyEmailInputDTO"]: ValueTypes["VerifyEmailInputDTO"]; diff --git a/components/cooptypes/src/common/names/index.ts b/components/cooptypes/src/common/names/index.ts index 8e3944b63fd..cab6165cd89 100644 --- a/components/cooptypes/src/common/names/index.ts +++ b/components/cooptypes/src/common/names/index.ts @@ -82,3 +82,8 @@ export const _apps = { production: 'apps', testnet: 'apps', } as const + +export const _expense = { + production: 'expense', + testnet: 'expense', +} as const diff --git a/components/cooptypes/src/contracts/capital/actions/createProgramExpense.ts b/components/cooptypes/src/contracts/capital/actions/createProgramExpense.ts new file mode 100644 index 00000000000..3665d916e40 --- /dev/null +++ b/components/cooptypes/src/contracts/capital/actions/createProgramExpense.ts @@ -0,0 +1,17 @@ +import * as Permissions from '../../../common/permissions' +import type * as Capital from '../../../interfaces/capital' +import { Actors } from '../../../common' + +export const authorizations = [{ permissions: [Permissions.active], actor: Actors._chairman }] as const + +/** + * Имя действия. Инициатор программного расхода: резервирует + * `program_expense_pool` и шлёт inline action в шасси `expense::createexp` + * с callback handler `{capital, onpgexpdone}`. + */ +export const actionName = 'createpgexp' + +/** + * @interface + */ +export type ICreateProgramExpense = Capital.ICreatepgexp diff --git a/components/cooptypes/src/contracts/capital/actions/index.ts b/components/cooptypes/src/contracts/capital/actions/index.ts index 1f0ae3acffa..07207e3bc1e 100644 --- a/components/cooptypes/src/contracts/capital/actions/index.ts +++ b/components/cooptypes/src/contracts/capital/actions/index.ts @@ -48,6 +48,12 @@ export * as CommitDecline from './commitDecline' // Долги export * as CreateDebt from './createDebt' +// Расходы программы через шасси expense (Эпик 5 шасси) +// capital — инициатор+callback, весь flow обслуживает контракт expense. +export * as TopupProgramExpense from './topupProgramExpense' +export * as CreateProgramExpense from './createProgramExpense' +export * as OnProgramExpenseDone from './onProgramExpenseDone' + // Регистрация участников export * as RegisterContributor from './registerContributor' diff --git a/components/cooptypes/src/contracts/capital/actions/onProgramExpenseDone.ts b/components/cooptypes/src/contracts/capital/actions/onProgramExpenseDone.ts new file mode 100644 index 00000000000..e602744f4c0 --- /dev/null +++ b/components/cooptypes/src/contracts/capital/actions/onProgramExpenseDone.ts @@ -0,0 +1,25 @@ +import * as Permissions from '../../../common/permissions' +import type * as Capital from '../../../interfaces/capital' +import { Actors } from '../../../common' + +/** + * Self-callback контракта `capital`. Шасси `expense` шлёт inline action + * на capital после терминального перехода (CLOSED либо DECLINED). + * Authority — `capital@active`, не пользователь. + */ +export const authorizations = [{ permissions: [Permissions.active], actor: Actors._contract }] as const + +/** + * Имя действия. Callback от шасси expense на финализацию program-расхода. + * status == ExpenseProposalStatus.CLOSED (4) → списать `total_actual` из + * `program_expense_reserved`, остаток вернуть в `program_expense_pool`, + * удалить запись `progexpenses`. + * status == ExpenseProposalStatus.DECLINED (5) → вернуть весь резерв в пул, + * удалить запись. + */ +export const actionName = 'onpgexpdone' + +/** + * @interface + */ +export type IOnProgramExpenseDone = Capital.IOnpgexpdone diff --git a/components/cooptypes/src/contracts/capital/actions/topupProgramExpense.ts b/components/cooptypes/src/contracts/capital/actions/topupProgramExpense.ts new file mode 100644 index 00000000000..ac714e4c759 --- /dev/null +++ b/components/cooptypes/src/contracts/capital/actions/topupProgramExpense.ts @@ -0,0 +1,17 @@ +import * as Permissions from '../../../common/permissions' +import type * as Capital from '../../../interfaces/capital' +import { Actors } from '../../../common' + +export const authorizations = [{ permissions: [Permissions.active], actor: Actors._chairman }] as const + +/** + * Имя действия. Пополнение пула программных расходов из доступного остатка + * `global_available_invest_pool`. Председатель решает сколько денег из + * инвестиционного пула программы перевести в пул, доступный для расходов. + */ +export const actionName = 'topupprogexp' + +/** + * @interface + */ +export type ITopupProgramExpense = Capital.ITopupprogexp diff --git a/components/cooptypes/src/contracts/capital/tables/index.ts b/components/cooptypes/src/contracts/capital/tables/index.ts index 3757171c9b0..5e96d997b2e 100644 --- a/components/cooptypes/src/contracts/capital/tables/index.ts +++ b/components/cooptypes/src/contracts/capital/tables/index.ts @@ -6,6 +6,7 @@ export * as Commits from './commits' export * as Contributors from './contributors' export * as Debts from './debts' export * as Expenses from './expenses' +export * as ProgramExpenses from './programExpenses' export * as Invests from './invests' export * as ProgramInvests from './programInvests' export * as ProgramProperties from './programProperties' diff --git a/components/cooptypes/src/contracts/capital/tables/programExpenses.ts b/components/cooptypes/src/contracts/capital/tables/programExpenses.ts new file mode 100644 index 00000000000..210d6d40623 --- /dev/null +++ b/components/cooptypes/src/contracts/capital/tables/programExpenses.ts @@ -0,0 +1,18 @@ +import type * as Capital from '../../../interfaces/capital' +import { Actors } from '../../../common' + +/** + * Имя таблицы + */ +export const tableName = 'progexpenses' + +/** + * Таблица хранится в {@link Actors._coopname | области памяти кооператива}. + */ +export const scope = Actors._coopname + +/** + * @interface + * Таблица содержит информацию о расходах программы (Благорост Эпик B). + */ +export type IProgramExpense = Capital.IProgramExpense diff --git a/components/cooptypes/src/contracts/expense/actions/authExp.ts b/components/cooptypes/src/contracts/expense/actions/authExp.ts new file mode 100644 index 00000000000..c9c4d1534ed --- /dev/null +++ b/components/cooptypes/src/contracts/expense/actions/authExp.ts @@ -0,0 +1,14 @@ +import * as Permissions from '../../../common/permissions' +import type * as Expense from '../../../interfaces/expense' +import { Actors } from '../../../common' + +/** + * authexp — авторизация СЗ советом (signact2 decision_doc, type=2011). + * CREATED → AUTHORIZED. См. expense.hpp::authexp. + */ +export const authorizations = [{ permissions: [Permissions.active], actor: Actors._chairman }] as const + +export const actionName = 'authexp' + +/** @interface */ +export type IAuthExp = Expense.IAuthexp diff --git a/components/cooptypes/src/contracts/expense/actions/closeExp.ts b/components/cooptypes/src/contracts/expense/actions/closeExp.ts new file mode 100644 index 00000000000..0a37d20543b --- /dev/null +++ b/components/cooptypes/src/contracts/expense/actions/closeExp.ts @@ -0,0 +1,15 @@ +import * as Permissions from '../../../common/permissions' +import type * as Expense from '../../../interfaces/expense' +import { Actors } from '../../../common' + +/** + * closeexp — финализация СЗ-отчёта советом. REPORT_SUBMITTED → CLOSED. + * При заполненном callback — inline action (capital::createrid для Благороста). + * См. expense.hpp::closeexp. + */ +export const authorizations = [{ permissions: [Permissions.active], actor: Actors._chairman }] as const + +export const actionName = 'closeexp' + +/** @interface */ +export type ICloseExp = Expense.ICloseexp diff --git a/components/cooptypes/src/contracts/expense/actions/createExp.ts b/components/cooptypes/src/contracts/expense/actions/createExp.ts new file mode 100644 index 00000000000..c0b7de15586 --- /dev/null +++ b/components/cooptypes/src/contracts/expense/actions/createExp.ts @@ -0,0 +1,14 @@ +import * as Permissions from '../../../common/permissions' +import type * as Expense from '../../../interfaces/expense' +import { Actors } from '../../../common' + +/** + * createexp — создаёт и подаёт СЗ-расход. Подписывает создатель + * (signact1 statement_doc, type=2010). См. expense.hpp::createexp. + */ +export const authorizations = [{ permissions: [Permissions.active], actor: Actors._username }] as const + +export const actionName = 'createexp' + +/** @interface */ +export type ICreateExp = Expense.ICreateexp diff --git a/components/cooptypes/src/contracts/expense/actions/declineExp.ts b/components/cooptypes/src/contracts/expense/actions/declineExp.ts new file mode 100644 index 00000000000..0c38d372035 --- /dev/null +++ b/components/cooptypes/src/contracts/expense/actions/declineExp.ts @@ -0,0 +1,14 @@ +import * as Permissions from '../../../common/permissions' +import type * as Expense from '../../../interfaces/expense' +import { Actors } from '../../../common' + +/** + * declexp — отклонение СЗ-предложения. CREATED/REPORT_SUBMITTED → DECLINED. + * Capitalization Благороста НЕ запускается. См. expense.hpp::declexp. + */ +export const authorizations = [{ permissions: [Permissions.active], actor: Actors._chairman }] as const + +export const actionName = 'declexp' + +/** @interface */ +export type IDeclineExp = Expense.IDeclexp diff --git a/components/cooptypes/src/contracts/expense/actions/index.ts b/components/cooptypes/src/contracts/expense/actions/index.ts new file mode 100644 index 00000000000..c6e079a25f6 --- /dev/null +++ b/components/cooptypes/src/contracts/expense/actions/index.ts @@ -0,0 +1,11 @@ +// Actions контракта expense (см. components/contracts/cpp/expense/expense.hpp). +// MVP: 8 actions для шасси расходов цифрового кооператива. + +export * as CreateExp from './createExp' +export * as AuthExp from './authExp' +export * as DeclineExp from './declineExp' +export * as PayExp from './payExp' +export * as ReportExp from './reportExp' +export * as CloseExp from './closeExp' +export * as ReturnExp from './returnExp' +export * as OverspendExp from './overspendExp' diff --git a/components/cooptypes/src/contracts/expense/actions/overspendExp.ts b/components/cooptypes/src/contracts/expense/actions/overspendExp.ts new file mode 100644 index 00000000000..85a3a496d6f --- /dev/null +++ b/components/cooptypes/src/contracts/expense/actions/overspendExp.ts @@ -0,0 +1,15 @@ +import * as Permissions from '../../../common/permissions' +import type * as Expense from '../../../interfaces/expense' +import { Actors } from '../../../common' + +/** + * overspendexp — доплата при перерасходе ADVANCE. + * Контракт зовёт Ledger2::apply(OVERSPEND) + сразу Ledger2::apply(ADVANCE_REPORT) одной транзакцией. + * См. expense.hpp::overspendexp. + */ +export const authorizations = [{ permissions: [Permissions.active], actor: Actors._chairman }] as const + +export const actionName = 'overspendexp' + +/** @interface */ +export type IOverspendExp = Expense.IOverspendexp diff --git a/components/cooptypes/src/contracts/expense/actions/payExp.ts b/components/cooptypes/src/contracts/expense/actions/payExp.ts new file mode 100644 index 00000000000..8883d32762f --- /dev/null +++ b/components/cooptypes/src/contracts/expense/actions/payExp.ts @@ -0,0 +1,15 @@ +import * as Permissions from '../../../common/permissions' +import type * as Expense from '../../../interfaces/expense' +import { Actors } from '../../../common' + +/** + * payexp — оплата item: ADVANCE (выдача аванса) или DIRECT (прямая оплата). + * Контракт зовёт Ledger2::apply(operation_code, actual_amount). + * Для DIRECT сразу за payexp идёт фоновый reportexp. См. expense.hpp::payexp. + */ +export const authorizations = [{ permissions: [Permissions.active], actor: Actors._chairman }] as const + +export const actionName = 'payexp' + +/** @interface */ +export type IPayExp = Expense.IPayexp diff --git a/components/cooptypes/src/contracts/expense/actions/reportExp.ts b/components/cooptypes/src/contracts/expense/actions/reportExp.ts new file mode 100644 index 00000000000..a0e74368f54 --- /dev/null +++ b/components/cooptypes/src/contracts/expense/actions/reportExp.ts @@ -0,0 +1,14 @@ +import * as Permissions from '../../../common/permissions' +import type * as Expense from '../../../interfaces/expense' +import { Actors } from '../../../common' + +/** + * reportexp — закрытие ADVANCE-item чеком. Контракт зовёт Ledger2::apply(ADVANCE_REPORT). + * Все items REPORTED → proposal становится REPORT_SUBMITTED. См. expense.hpp::reportexp. + */ +export const authorizations = [{ permissions: [Permissions.active], actor: Actors._username }] as const + +export const actionName = 'reportexp' + +/** @interface */ +export type IReportExp = Expense.IReportexp diff --git a/components/cooptypes/src/contracts/expense/actions/returnExp.ts b/components/cooptypes/src/contracts/expense/actions/returnExp.ts new file mode 100644 index 00000000000..62d4f55b38f --- /dev/null +++ b/components/cooptypes/src/contracts/expense/actions/returnExp.ts @@ -0,0 +1,14 @@ +import * as Permissions from '../../../common/permissions' +import type * as Expense from '../../../interfaces/expense' +import { Actors } from '../../../common' + +/** + * returnexp — возврат неиспользованного ADVANCE-остатка. + * Контракт зовёт Ledger2::apply(ADVANCE_RETURN). См. expense.hpp::returnexp. + */ +export const authorizations = [{ permissions: [Permissions.active], actor: Actors._username }] as const + +export const actionName = 'returnexp' + +/** @interface */ +export type IReturnExp = Expense.IReturnexp diff --git a/components/cooptypes/src/contracts/expense/index.ts b/components/cooptypes/src/contracts/expense/index.ts new file mode 100644 index 00000000000..b75dbdb5da1 --- /dev/null +++ b/components/cooptypes/src/contracts/expense/index.ts @@ -0,0 +1,9 @@ +import * as contractNames from '../../common/names' + +export * as Actions from './actions' +export * as Tables from './tables' + +/** @private */ +export * as Interfaces from '../../interfaces/expense' + +export const contractName = contractNames._expense diff --git a/components/cooptypes/src/contracts/expense/tables/index.ts b/components/cooptypes/src/contracts/expense/tables/index.ts new file mode 100644 index 00000000000..cbcaaffd7a8 --- /dev/null +++ b/components/cooptypes/src/contracts/expense/tables/index.ts @@ -0,0 +1 @@ +export * as Proposals from './proposals' diff --git a/components/cooptypes/src/contracts/expense/tables/proposals.ts b/components/cooptypes/src/contracts/expense/tables/proposals.ts new file mode 100644 index 00000000000..87f94f44afe --- /dev/null +++ b/components/cooptypes/src/contracts/expense/tables/proposals.ts @@ -0,0 +1,13 @@ +import type * as Expense from '../../../interfaces/expense' +import { Actors } from '../../../common' + +/** + * Таблица СЗ-расходов. Scope = coopname. + * On-chain индексы: byhash (proposal_hash), byusername (username), bystatus (status). + */ +export const tableName = 'proposals' + +export const scope = Actors._coopname + +/** @interface */ +export type IProposal = Expense.IProposal diff --git a/components/cooptypes/src/contracts/index.ts b/components/cooptypes/src/contracts/index.ts index 213d4339e60..957c8b2af10 100644 --- a/components/cooptypes/src/contracts/index.ts +++ b/components/cooptypes/src/contracts/index.ts @@ -94,3 +94,13 @@ export * as MeetContract from './meet' * Деплоится на корневой KE-цепи и обслуживает несколько подсетей одновременно. */ export * as AppsContract from './apps' + +/** + * Смарт-контракт шасси расходов цифрового кооператива (MVP — Благорост). + * + * Универсальный движок СЗ-расходов: principal storage = proposal с массивом items, + * operation_code из ledger2 OPERATION_REGISTRY передаётся в payexp, + * callback на финализацию задаётся при createexp (контракт+action+data). + * См. components/contracts/cpp/expense/expense.hpp (C28-29 v3). + */ +export * as ExpenseContract from './expense' diff --git a/components/cooptypes/src/cooperative/document/index.ts b/components/cooptypes/src/cooperative/document/index.ts index 60de9fdca21..25cc5628b41 100644 --- a/components/cooptypes/src/cooperative/document/index.ts +++ b/components/cooptypes/src/cooperative/document/index.ts @@ -112,6 +112,18 @@ export interface IGenerate extends Omit, 'title'> { [key: string]: any } +/** + * Зарезервированная ссылка на приватный payload документа. + * Action конкретного документа расширяет этот интерфейс, если хочет + * хранить персональные/приватные данные off-chain — payload лежит в + * коллекции `doc_private_data` фабрики, on-chain публикуется только + * `doc_data_hash`. См. раздел «Document Generation Pattern: doc_data» + * в архитектуре. + */ +export interface IDocDataRef { + doc_data_hash?: string +} + /** * Интерфейс генерации заявления на вступление в кооператив */ diff --git a/components/cooptypes/src/cooperative/registry/2010.ExpenseProposalStatement/index.ts b/components/cooptypes/src/cooperative/registry/2010.ExpenseProposalStatement/index.ts new file mode 100644 index 00000000000..061aec04680 --- /dev/null +++ b/components/cooptypes/src/cooperative/registry/2010.ExpenseProposalStatement/index.ts @@ -0,0 +1,157 @@ +import type { IDocDataRef, IGenerate, IMetaDocument } from '../../document' +import type { ICommonUser, ICooperativeData, IVars } from '../../model' + +export const registry_id = 2010 + +export type ExpenseItemRecipientType = 'SELF' | 'MEMBER' | 'ORG' +export type ExpenseItemPaymentMechanics = 'ADVANCE' | 'DIRECT' + +/** + * Публичная часть позиции расхода — единственное, что публикуется в meta + * документа on-chain. Реквизиты получателя, его имя и назначение платежа + * сюда НЕ входят — они приватны (см. {@link IExpensePrivateItem}). + */ +export interface IExpenseItem { + number: string + description: string + amount: string + recipient_type: ExpenseItemRecipientType + mechanics: ExpenseItemPaymentMechanics +} + +/** + * Приватная часть позиции — НЕ публикуется в блокчейн. Имя получателя, + * его банковские реквизиты и назначение платежа сохраняются off-chain в + * `doc_private_data` фабрики; в meta документа едет только `doc_data_hash`. + * Корреляция с публичной позицией — по `number`. + */ +export interface IExpensePrivateItem { + number: string + recipient_name?: string + requisites?: string + /** Назначение платежа — отдельной строкой после реквизитов (для оплаты по счёту) */ + payment_purpose?: string +} + +/** + * Приватный payload СЗ-сметы целиком (off-chain). Сохраняется в + * `DocDataService` фабрики, адресуется публичным `doc_data_hash`. + * См. раздел «Document Generation Pattern: doc_data» в архитектуре. + */ +export interface PrivateData { + items: IExpensePrivateItem[] +} + +/** + * Позиция в модели рендера — публичная + приватная части, склеенные фабрикой + * по `number` при генерации документа. Шаблон обращается к полям как `item.*`. + */ +export interface IExpenseRenderItem extends IExpenseItem { + recipient_name?: string + requisites?: string + payment_purpose?: string +} + +export interface IExpenseProposalHeader { + description: string + total_amount: string + items_count: number + source_wallet: string + /** Срок исполнения («в срок до»), формат DD.MM.YYYY — передаётся при создании расхода */ + deadline?: string + /** Фонд списания в дательном падеже («по …») — подставляет сервер из параметров шасси расходов */ + fund_name?: string +} + +export interface Action extends IGenerate, IDocDataRef { + proposal_hash: string + proposal: IExpenseProposalHeader + /** Публичные позиции расхода — без реквизитов/имени/назначения (они в doc_data). */ + items: IExpenseItem[] + /** sha256 приватного payload (PrivateData) — реквизиты/имя/назначение off-chain. */ + doc_data_hash: string +} + +export type Meta = IMetaDocument & Action + +export interface Model { + meta: IMetaDocument + coop: ICooperativeData + user: ICommonUser + vars: IVars + proposal_hash: string + /** Короткий идентификатор СЗ (первые 16 символов хэша, uppercase) — для шапки документа */ + proposal_short_hash: string + proposal: IExpenseProposalHeader + /** Позиции с подмешанной приватной частью (фабрика склеивает по number из doc_data). */ + items: IExpenseRenderItem[] +} + +export const title = 'Служебная записка-смета о расходах' +export const description = 'Заявка-смета председателю кооператива на финансирование расходов по программе. Содержит описание цели и массив позиций расхода.' + +// Вёрстка по канону документов реестра (см. 1040): без \n\n

{% trans \'protocol_number\', decision.id %}

\n

{% trans \'council_meeting_name\' %} {{vars.full_abbr_genitive}} "{{vars.name}}"

\n

{{ meta.created_at }}, {{ coop.city }}

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
{% trans \'meeting_format\' %}{% trans \'meeting_format_value\' %}
{% trans \'meeting_place\' %}{{ coop.full_address }}
{% trans \'meeting_date\' %}{{ decision.date }}
{% trans \'opening_time\' %}{{ decision.time }}
\n

{% trans \'council_members\' %}

\n\n\n{% for member in coop.members %}\n\n\n\n\n{% endfor %}\n\n
{% if member.is_chairman %}{% trans \'chairman_of_the_council\' %}{% else %}{% trans \'member_of_the_council\' %}{% endif %}{{ member.last_name }} {{ member.first_name }} {{ member.middle_name }}
\n

{% trans \'meeting_legality\' %}

\n

{% trans \'voting_results\', decision.voters_percent %} {% trans \'quorum\' %} {% trans \'chairman_of_the_meeting\', coop.chairman.last_name, coop.chairman.first_name, coop.chairman.middle_name %}.

\n

{% trans \'agenda\' %}

\n\n\n\n\n\n\n\n\n\n\n\n
{% trans \'question\' %}
1{% trans \'question_body\', proposal_short_hash, proposal.total_amount %} {% trans \'question_purpose\' %}: {{ proposal.description }}
\n

{% trans \'voting\' %}

\n

{% trans \'vote_results\' %}

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
{% trans \'votes_for\' %}{{ decision.votes_for }}
{% trans \'votes_against\' %}{{ decision.votes_against }}
{% trans \'votes_abstained\' %}{{ decision.votes_abstained }}
\n

{% trans \'decision_made\' %}

\n\n\n\n\n\n\n\n\n\n\n\n
{% trans \'decision\' %}
1{% if resolution.kind == \'approve\' %}

{% trans \'decision_approve\', proposal_short_hash, proposal.total_amount %}{% if proposal.deadline %} {% trans \'decision_deadline\', proposal.deadline %}{% endif %}{% trans \'decision_namely\' %}:

{% for item in items %}

{{ item.number }}. {{ item.description }} — {{ item.amount }} ({% if item.mechanics == \'ADVANCE\' %}{% trans \'mech_advance\' %}{% else %}{% trans \'mech_direct\' %}{% endif %})

{% endfor %}{% else %}

{% trans \'decision_decline\', proposal_short_hash %}{% if resolution.reason %} {% trans \'decline_reason\' %}: {{ resolution.reason }}{% endif %}

{% endif %}
\n
\n

{% trans \'closing_time\', decision.time %}

\n

{% trans \'signature\' %}

{% trans \'chairman\' %} {{ coop.chairman.last_name }} {{ coop.chairman.first_name }} {{ coop.chairman.middle_name }}

\n' + +export const translations = { + ru: { + meeting_format: 'Форма', + meeting_date: 'Дата', + meeting_place: 'Место', + opening_time: 'Время открытия', + council_members: 'ЧЛЕНЫ СОВЕТА', + voting_results: 'Количество голосов составляет {0}% от общего числа членов Совета.', + meeting_legality: 'СОБРАНИЕ ПРАВОМОЧНО', + chairman_of_the_meeting: 'Председатель собрания совета: {0} {1} {2}', + agenda: 'ПОВЕСТКА ДНЯ', + vote_results: 'По первому вопросу повестки дня проголосовали:', + decision_made: 'РЕШИЛИ', + closing_time: 'Время закрытия собрания совета: {0}.', + protocol_number: 'ПРОТОКОЛ № {0}', + council_meeting_name: 'Собрания Совета', + chairman_of_the_council: 'Председатель совета', + signature: 'Документ подписан электронной подписью.', + chairman: 'Председатель', + quorum: 'Кворум для решения поставленных на повестку дня вопросов имеется.', + voting: 'ГОЛОСОВАНИЕ', + meeting_format_value: 'Заочная', + member_of_the_council: 'Член совета', + question: 'Вопрос', + votes_for: 'ЗА', + votes_against: 'ПРОТИВ', + votes_abstained: 'ВОЗДЕРЖАЛСЯ', + decision: 'Решение', + question_body: 'Рассмотрение служебной записки-сметы о расходах № {0} на общую сумму {1}.', + question_purpose: 'Цель расходов', + decision_approve: 'Утвердить расходы по служебной записке № {0} в общей сумме {1}', + decision_deadline: 'со сроком исполнения до {0}', + decision_namely: ', а именно', + mech_advance: 'аванс под отчёт', + mech_direct: 'оплата по счёту', + decision_decline: 'Отказать в утверждении расходов по служебной записке № {0}.', + decline_reason: 'Основание отказа', + }, +} + +export const exampleData = { + meta: { + created_at: '03.06.2026 11:00', + }, + coop: { + city: 'Москва', + full_address: 'г. Москва, ул. Смольная, д. 3, оф. 84', + chairman: { + last_name: 'Муравьев', + first_name: 'Алексей', + middle_name: 'Николаевич', + }, + }, + decision: { + time: '11:00', + date: '03.06.2026', + votes_for: '3', + votes_against: '0', + votes_abstained: '0', + voters_percent: '100', + id: '15', + }, + member: { + is_chairman: true, + last_name: 'Муравьев', + first_name: 'Алексей', + middle_name: 'Николаевич', + }, + vars: { + full_abbr_genitive: 'потребительского кооператива', + name: 'ВОСХОД', + }, + proposal_hash: '55c470039a8c53ce1b4b6e842fe8063ab3d5b85ba2ba8ab0ae6e30be3ad328b7', + proposal_short_hash: '55C470039A8C53CE', + proposal: { + description: 'Закупка хостинга и бухгалтерских услуг на июнь 2026', + total_amount: '15000.00 RUB', + items_count: 2, + source_wallet: 'w.cap.bla', + deadline: '30.06.2026', + fund_name: 'Фонду хозяйственной деятельности', + }, + items: [ + { number: '1', description: 'Аренда серверов Yandex Cloud', amount: '12000.00 RUB', recipient_type: 'ORG', mechanics: 'DIRECT' }, + { number: '2', description: 'Канцелярия для офиса', amount: '3000.00 RUB', recipient_type: 'SELF', mechanics: 'ADVANCE' }, + ], + resolution: { + kind: 'approve', + }, +} diff --git a/components/cooptypes/src/cooperative/registry/index.ts b/components/cooptypes/src/cooperative/registry/index.ts index ca85ce6311a..dcabb437a4c 100644 --- a/components/cooptypes/src/cooperative/registry/index.ts +++ b/components/cooptypes/src/cooperative/registry/index.ts @@ -44,6 +44,10 @@ export * as InitProjectDecision from './1006.InitProjectDecision' export * as ExpenseStatement from './1010.ExpenseStatement' export * as ExpenseDecision from './1011.ExpenseDecision' +// Шасси расходов (волна 6, MVP-SINGLE) — C28-30 +export * as ExpenseProposalStatement from './2010.ExpenseProposalStatement' +export * as ExpenseProposalDecision from './2011.ExpenseProposalDecision' + export * as GenerationMoneyInvestStatement from './1020.GenerationMoneyInvestStatement' export * as GenerationMoneyReturnUnusedStatement from './1025.GenerationMoneyReturnUnusedStatement' diff --git a/components/cooptypes/src/interfaces/capital.ts b/components/cooptypes/src/interfaces/capital.ts index 12731d5adb2..f3329b121be 100644 --- a/components/cooptypes/src/interfaces/capital.ts +++ b/components/cooptypes/src/interfaces/capital.ts @@ -1,11 +1,14 @@ // Generated by eosio-abi2ts 1.2.2 - eosio::abi/1.2 +import type { IItem as IExpenseProposalItem } from './expense' + export type IAsset = string export type IName = string export type IChecksum256 = string export type IPublicKey = string export type ISignature = string export type ITimePointSec = string +export type IUint8 = number export type IUint32 = number export type IUint64 = number | string export type IFloat64 = number @@ -359,6 +362,8 @@ export interface IDebt { approved_statement: IDocument2 authorization: IDocument2 memo: string + last_pay_error?: string + due_at?: ITimePointSec } export interface IDebtauthcnfr { @@ -556,6 +561,8 @@ export interface IGlobalState { program_membership_distributed: IAsset program_membership_cumulative_reward_per_share: IFloat64 config: IConfig + program_expense_pool?: IAsset + program_expense_reserved?: IAsset } export interface IImportcontrib { @@ -803,6 +810,9 @@ export interface ISegment { available_for_program: IAsset available_for_wallet: IAsset share_percent: IFloat64 + approved_rate_per_hour?: IAsset + approved_hours_per_day?: IUint64 + active_debts_count?: IUint32 } export interface ISetconfig { @@ -827,7 +837,7 @@ export interface ISetplan { export interface ISettledebt { coopname: IName - username: IName + debt_hash: IChecksum256 amount: IAsset statement: IDocument2 } @@ -912,3 +922,63 @@ export interface IVotingData { amounts: IVotingAmounts voting_deadline: ITimePointSec } + +// ─── Эпик 5 (шасси расходов): програмные расходы через шасси expense ────── + +/** + * Инициация программного расхода через шасси expense. + * capital резервирует пул, шлёт inline action в expense::createexp с + * callback handler `{capital, onpgexpdone}`. Шасси обслуживает весь flow + * расхода (auth/pay/report/close|decline) и шлёт callback назад. + */ +export interface ICreatepgexp { + coopname: IName + expense_hash: IChecksum256 + creator: IName + /** Позиции СЗ-расхода (Expense.IItem из шасси); механика оплаты — per-item (mechanics: ADVANCE | DIRECT). */ + items: IExpenseProposalItem[] + description: string + statement: IDocument2 +} + +/** + * Self-callback `capital`. Шасси `expense` дёргает после CLOSED/DECLINED. + * status == ExpenseProposalStatus.CLOSED (4) → consume + release остаток + delete. + * status == ExpenseProposalStatus.DECLINED (5) → release полностью + delete. + */ +export interface IOnpgexpdone { + coopname: IName + expense_hash: IChecksum256 + status: IUint8 + total_actual: IAsset + /** Произвольный payload от инициатора (на MVP не используется). */ + data: string +} + +export interface ITopupprogexp { + coopname: IName + amount: IAsset +} + +// Таблицы + +/** + * Таблица `progexpenses` — целевые расходы программы «Благорост», не привязанные + * к проекту. Создаётся в `createpgexp`, удаляется в `onpgexpdone` после + * терминального перехода шасси. На MVP approved_statement / authorization + * остаются пустыми (флоу подписания делает шасси expense). + */ +export interface IProgramExpense { + id: IUint64 + coopname: IName + username: IName + status: IName + expense_hash: IChecksum256 + fund_id: IUint64 + amount: IAsset + description: string + expense_statement: IDocument2 + approved_statement: IDocument2 + authorization: IDocument2 + spended_at: ITimePointSec +} diff --git a/components/cooptypes/src/interfaces/expense.ts b/components/cooptypes/src/interfaces/expense.ts new file mode 100644 index 00000000000..e8b8798a15e --- /dev/null +++ b/components/cooptypes/src/interfaces/expense.ts @@ -0,0 +1,177 @@ +// Hand-written from components/contracts/cpp/expense/expense.hpp (C28-29 v3, MVP — Благорост). +// До появления codegen из ABI этот файл = SoT для cooptypes/expense. +// При regen ABI заменить ровно тем же набором имён интерфейсов, не ломая публичный API. + +export type IAsset = string +export type IName = string +export type IChecksum256 = string +export type ITimePointSec = string +export type IUint8 = number +export type IUint32 = number +export type IUint64 = number | string + +/** + * Документ document2 (signact1/signact2 — двухподписные акты). + * Точная копия C++ struct document2 из components/contracts/cpp/lib/domain/document_core.hpp. + */ +export interface IDocument2 { + version: string + hash: IChecksum256 + doc_hash: IChecksum256 + meta_hash: IChecksum256 + meta: string + signatures: ISignature2[] +} + +export interface ISignature2 { + id: IUint32 + signed_hash: IChecksum256 + signer: IName + public_key: string + signature: string + signed_at: ITimePointSec + meta: string +} + +/** + * Перечисления expense::* — в TS экспонируются как `number` (uint8), сами литералы + * (`enum`) задаются на стороне backend extension (`expense-proposal-status.enum.ts` + * + `expense-item-status.enum.ts` + `expense-mechanics.enum.ts` + `expense-recipient-type.enum.ts`). + */ + +export interface IItem { + item_hash: IChecksum256 + /** Mechanics (ADVANCE=0, DIRECT=1). */ + mechanics: IUint8 + /** RecipientType (SELF=0, MEMBER=1, ORG=2). */ + recipient_type: IUint8 + recipient: IName + description: string + planned_amount: IAsset + actual_amount: IAsset + /** ItemStatus (APPROVED=0, PAID=1, REPORTED=2, RETURNED=3, OVERSPENT=4). */ + status: IUint8 +} + +/** + * Callback на финализацию closeexp. Пустой `contract` (`""`) = нет callback. + * `data` — base16/hex (eosio bytes) либо base64 в зависимости от транспорта. + */ +export interface ICallbackHandler { + contract: IName + action: IName + data: string +} + +// ─────────────────────────── Actions ─────────────────────────── + +/** + * createexp — создание и подача СЗ-расхода (signact1 statement_doc, type=2010). + */ +export interface ICreateexp { + coopname: IName + username: IName + proposal_hash: IChecksum256 + source_wallet: IName + items: IItem[] + callback: ICallbackHandler + statement: IDocument2 +} + +/** + * authexp — авторизация СЗ советом (signact2 decision_doc, type=2011). + * После — CREATED → AUTHORIZED. + */ +export interface IAuthexp { + coopname: IName + proposal_hash: IChecksum256 + decision: IDocument2 +} + +/** + * declexp — отклонение СЗ-предложения (CREATED/REPORT_SUBMITTED → DECLINED). + * Capitalization Благороста НЕ запускается. + */ +export interface IDeclexp { + coopname: IName + proposal_hash: IChecksum256 + reason: string +} + +/** + * payexp — оплата item (ADVANCE = выдача аванса / DIRECT = прямая оплата). + * Контракт: Ledger2::apply(operation_code, actual_amount); для DIRECT сразу за payexp — фоновый reportexp. + */ +export interface IPayexp { + coopname: IName + proposal_hash: IChecksum256 + item_hash: IChecksum256 + actual_amount: IAsset +} + +/** + * reportexp — закрытие item чеком (только ADVANCE). + * Контракт: Ledger2::apply(ADVANCE_REPORT). Все items reported → PARTIALLY_PAID/CLOSED-эскалация + * выносится в closeexp. + */ +export interface IReportexp { + coopname: IName + proposal_hash: IChecksum256 + item_hash: IChecksum256 +} + +/** + * closeexp — финализация СЗ-отчёта советом. REPORT_SUBMITTED → CLOSED. + * При наличии callback — inline action на (callback.contract, callback.action, callback.data). + * **Триггерит capitalization Благороста** (см. capital::createrid через callback). + */ +export interface ICloseexp { + coopname: IName + proposal_hash: IChecksum256 +} + +/** + * returnexp — возврат неиспользованного аванса (ADVANCE-остаток). + * Контракт: Ledger2::apply(ADVANCE_RETURN). + */ +export interface IReturnexp { + coopname: IName + proposal_hash: IChecksum256 + item_hash: IChecksum256 + return_amount: IAsset +} + +/** + * overspendexp — доплата при перерасходе (только ADVANCE). + * Контракт: Ledger2::apply(OVERSPEND) → сразу Ledger2::apply(ADVANCE_REPORT) одной транзакцией. + */ +export interface IOverspendexp { + coopname: IName + proposal_hash: IChecksum256 + item_hash: IChecksum256 + overspend_amount: IAsset +} + +// ─────────────────────────── Tables ─────────────────────────── + +/** + * Таблица `proposals` (scope = coopname). + * Индексы on-chain: byhash (proposal_hash), byusername (username), bystatus (status). + */ +export interface IProposal { + id: IUint64 + coopname: IName + proposal_hash: IChecksum256 + username: IName + source_wallet: IName + /** ProposalStatus (CREATED=0, AUTHORIZED=1, PARTIALLY_PAID=2, REPORT_SUBMITTED=3, CLOSED=4, DECLINED=5). */ + status: IUint8 + items: IItem[] + total_planned: IAsset + total_actual: IAsset + callback: ICallbackHandler + statement_doc: IDocument2 + decision_doc: IDocument2 + created_at: ITimePointSec + updated_at: ITimePointSec +} diff --git a/components/cooptypes/src/interfaces/index.ts b/components/cooptypes/src/interfaces/index.ts index 0455dc95e9f..ef1d62e7615 100644 --- a/components/cooptypes/src/interfaces/index.ts +++ b/components/cooptypes/src/interfaces/index.ts @@ -1,6 +1,7 @@ export * as Branch from './branch' export * as Capital from './capital' export * as Draft from './draft' +export * as Expense from './expense' export * as Fund from './fund' export * as Gateway from './gateway' export * as Ledger from './ledger' diff --git a/components/cooptypes/src/ledger2/operations.ts b/components/cooptypes/src/ledger2/operations.ts index dca2c6d5aa6..eaa9cf68939 100644 --- a/components/cooptypes/src/ledger2/operations.ts +++ b/components/cooptypes/src/ledger2/operations.ts @@ -108,6 +108,37 @@ export const LEDGER2_OPERATION_REGISTRY: readonly OperationMeta[] = [ { code: 'o.cap.cnvbl', process_type: 'p.cap.rid', contract: 'capital', name: 'CONVERT_TO_BLAGO', wallet_op: 'TRANSFER', wallet_from: 'w.cap.gen', wallet_to: 'w.cap.blago', debit: null, credit: null, human_name: 'Конвертация сегмента: РИД → ЦПП «Благорост»' }, + { code: 'o.cap.pgtop', process_type: 'p.cap.pgexp', contract: 'capital', + name: 'PROGRAM_EXPENSE_TOPUP', wallet_op: 'ISSUE', wallet_from: null, wallet_to: 'w.cap.pgexp', + debit: null, credit: null, + human_name: 'Пополнение пула программных расходов ЦПП «Благорост»' }, + + // expense — шасси расходов (оплата СЗ из кооперативного пула расходов) + { code: 'o.exp.blgadv', process_type: 'p.exp.expns', contract: 'expense', + name: 'BLAGO_ADVANCE', wallet_op: 'TRANSFER', wallet_from: 'w.cap.pgexp', wallet_to: 'w.exp.adv', + debit: 8, credit: 51, + human_name: 'Выдача подотчётных из пула расходов ЦПП «Благорост»' }, + + { code: 'o.exp.blgdir', process_type: 'p.exp.expns', contract: 'expense', + name: 'BLAGO_DIRECT', wallet_op: 'BURN', wallet_from: 'w.cap.pgexp', wallet_to: null, + debit: 8, credit: 51, + human_name: 'Прямая оплата из пула расходов ЦПП «Благорост»' }, + + { code: 'o.exp.advrpt', process_type: 'p.exp.expns', contract: 'expense', + name: 'ADVANCE_REPORT', wallet_op: 'BURN', wallet_from: 'w.exp.adv', wallet_to: null, + debit: null, credit: null, + human_name: 'Закрытие подотчёта пайщика по отчёту' }, + + { code: 'o.exp.advret', process_type: 'p.exp.expns', contract: 'expense', + name: 'ADVANCE_RETURN', wallet_op: 'TRANSFER', wallet_from: 'w.exp.adv', wallet_to: 'w.cap.pgexp', + debit: 51, credit: 8, + human_name: 'Возврат неиспользованного подотчёта в пул расходов' }, + + { code: 'o.exp.over', process_type: 'p.exp.expns', contract: 'expense', + name: 'OVERSPEND', wallet_op: 'TRANSFER', wallet_from: 'w.cap.pgexp', wallet_to: 'w.exp.adv', + debit: 8, credit: 51, + human_name: 'Доплата сверх подотчёта (перерасход)' }, + // marketplace { code: 'o.mkt.supply', process_type: 'p.mkt.reqst', contract: 'marketplace', name: 'CONFIRM_SUPPLY', wallet_op: 'ISSUE', wallet_from: null, wallet_to: 'w.wal.share', debit: 51, credit: 80, human_name: 'Подтверждение поставки' }, diff --git a/components/cooptypes/src/ledger2/processes.ts b/components/cooptypes/src/ledger2/processes.ts index 7735829b889..d976d558da0 100644 --- a/components/cooptypes/src/ledger2/processes.ts +++ b/components/cooptypes/src/ledger2/processes.ts @@ -36,6 +36,10 @@ export const LEDGER2_PROCESS_REGISTRY: readonly ProcessMeta[] = [ { type: 'p.cap.prop', contract: 'capital', name: 'PROPERTY', human_name: 'Приём имущественного паевого взноса' }, { type: 'p.cap.preimp', contract: 'capital', name: 'PREIMP', human_name: 'Первичный учёт РИД-взноса до перехода на электронный учёт' }, { type: 'p.cap.wthcap', contract: 'capital', name: 'WTHCAP', human_name: 'Возврат паевого из ЦПП «Благорост» в кошелёк' }, + { type: 'p.cap.pgexp', contract: 'capital', name: 'PGEXP', human_name: 'Пополнение пула программных расходов' }, + + // expense + { type: 'p.exp.expns', contract: 'expense', name: 'PROPOSAL', human_name: 'Расход по служебной записке' }, // marketplace { type: 'p.mkt.reqst', contract: 'marketplace', name: 'REQUEST', human_name: 'Запрос маркетплейса' }, diff --git a/components/cooptypes/src/ledger2/wallets.generated.ts b/components/cooptypes/src/ledger2/wallets.generated.ts index 8363b66eaaf..332fbc56443 100644 --- a/components/cooptypes/src/ledger2/wallets.generated.ts +++ b/components/cooptypes/src/ledger2/wallets.generated.ts @@ -24,6 +24,7 @@ export const LEDGER2_WALLET_REGISTRY: readonly WalletMeta[] = [ { name: "w.wal.member", human_name: "ЦК — членская часть пайщика", kind: "USER_SHARED" }, { name: "w.cap.blago", human_name: "ЦПП «Благорост» — единый кошелёк программы у пайщика", kind: "USER_SHARED" }, { name: "w.cap.preimp", human_name: "Первичный учёт РИД-взносов до перехода на электронный учёт", kind: "USER_SHARED" }, + { name: "w.exp.adv", human_name: "Подотчётные средства пайщика", kind: "USER_SHARED" }, { name: "w.reg.pend", human_name: "Регистрационный взнос в ожидании решения совета", kind: "USER_SHARED" }, { name: "w.cap.gen", human_name: "ЦПП «Генератор» — единый кошелёк программы", kind: "COOPERATIVE" }, { name: "w.reg.entry", human_name: "Вступительные взносы", kind: "COOPERATIVE" }, @@ -35,6 +36,7 @@ export const LEDGER2_WALLET_REGISTRY: readonly WalletMeta[] = [ { name: "w.sov.mnused", human_name: "Использованные минимальные паевые взносы", kind: "COOPERATIVE" }, { name: "w.cap.loan", human_name: "Выданные пайщикам беспроцентные займы", kind: "COOPERATIVE" }, { name: "w.mkt.payout", human_name: "Выплаты поставщикам", kind: "COOPERATIVE" }, + { name: "w.cap.pgexp", human_name: "Пул программных расходов ЦПП «Благорост»", kind: "COOPERATIVE" }, ] as const export interface ProgramWalletMapping { @@ -57,5 +59,6 @@ export const LEDGER2_USER_SHARED_PROGRAM_MAPPING: readonly ProgramWalletMapping[ { wallet_name: "w.cap.blago", required_program_id: 4, program_label: "Благорост" }, { wallet_name: "w.cap.gen", required_program_id: 3, program_label: "Генератор" }, { wallet_name: "w.cap.preimp", required_program_id: 0, program_label: null }, + { wallet_name: "w.exp.adv", required_program_id: 0, program_label: null }, { wallet_name: "w.reg.pend", required_program_id: 0, program_label: null }, ] as const diff --git a/components/desktop/extensions/capital/app/extensions.ts b/components/desktop/extensions/capital/app/extensions.ts index fbf9b1283e6..22851480615 100644 --- a/components/desktop/extensions/capital/app/extensions.ts +++ b/components/desktop/extensions/capital/app/extensions.ts @@ -1,4 +1,8 @@ +import type { Cooperative } from 'cooptypes'; import { decisionFactory } from 'src/shared/lib/decision-factory'; +import { useSystemStore } from 'src/entities/System/model'; +import { useSessionStore } from 'src/entities/Session'; +import { generateExpenseProposalDecisionDocument } from 'app/extensions/expenses/api'; import { useGenerateResultContributionDecision } from '../features/Result/GenerateResultContributionDecision/model'; import { CreateResultDecisionInfoWidget } from '../widgets/CreateResultDecisionInfoWidget'; @@ -37,4 +41,41 @@ export function registerCapitalDecisionHandlers() { // Компонент для отображения дополнительной информации infoComponent: CreateResultDecisionInfoWidget, }); + + // Обработчик для createexp (служебная записка-смета о расходах, шасси expense). + // Живёт в capital, пока расширение expenses не подключено в extensions-registry: + // программные расходы создаются со стола capital. + decisionFactory.registerHandler('createexp', { + generateHandler: async ({ decision_id, row }) => { + if (!row.table?.statement?.meta) { + throw new Error('Отсутствуют метаданные заявления для решения createexp'); + } + + const parsedDocumentMeta = JSON.parse( + row.table.statement.meta, + ) as Cooperative.Registry.ExpenseProposalStatement.Action; + + if (!parsedDocumentMeta.proposal_hash || !parsedDocumentMeta.items?.length) { + throw new Error('Некорректные метаданные заявления для решения createexp'); + } + + const { info } = useSystemStore(); + const session = useSessionStore(); + + // Протокол решения подписывает председатель — документ генерируется на его + // имя. Данные собрания (кворум, голоса, № протокола) фабрика берёт сама + // по decision_id — канон протоколов решений совета. + const generated = await generateExpenseProposalDecisionDocument({ + coopname: info.coopname, + username: session.username, + proposal_hash: parsedDocumentMeta.proposal_hash, + decision_id, + proposal: parsedDocumentMeta.proposal, + items: parsedDocumentMeta.items, + resolution: { kind: 'approve' }, + } as any); + + return (generated as any).generateExpenseProposalDecisionDocument; + }, + }); } diff --git a/components/desktop/extensions/capital/entities/ProgramExpense/api/index.ts b/components/desktop/extensions/capital/entities/ProgramExpense/api/index.ts new file mode 100644 index 00000000000..d8eb0b71948 --- /dev/null +++ b/components/desktop/extensions/capital/entities/ProgramExpense/api/index.ts @@ -0,0 +1,96 @@ +import { client } from 'src/shared/api/client'; +import { Mutations, Queries } from '@coopenomics/sdk'; +import type { + IProgramExpense, + IProgramExpensesPagination, + IGetProgramExpenseInput, + IGetProgramExpensesInput, + IExpenseProposalAggregate, + IExpenseProposalFile, + IExpenseRequisite, +} from '../model/types'; + +async function loadProgramExpenses( + data: IGetProgramExpensesInput, +): Promise { + const { [Queries.Capital.GetProgramExpenses.name]: output } = await client.Query( + Queries.Capital.GetProgramExpenses.query, + { variables: data }, + ); + return output; +} + +async function loadProgramExpense( + data: IGetProgramExpenseInput, +): Promise { + const { [Queries.Capital.GetProgramExpense.name]: output } = await client.Query( + Queries.Capital.GetProgramExpense.query, + { variables: data }, + ); + return output; +} + +// Документы СЗ (заявление + решение совета) — из шасси expense: capital-проекция +// отдаёт только зеркальные данные таблицы, агрегаты документов живут в proposal. +async function loadExpenseProposal( + proposalHash: string, +): Promise { + const { [Queries.Expense.ExpenseProposal.name]: output } = await client.Query( + Queries.Expense.ExpenseProposal.query, + { variables: { proposal_hash: proposalHash } }, + ); + return output ?? null; +} + +async function loadExpenseFiles( + coopname: string, + proposalHash: string, +): Promise { + const { [Queries.Expense.ExpenseFilesByProposal.name]: output } = await client.Query( + Queries.Expense.ExpenseFilesByProposal.query, + { variables: { coopname, proposal_hash: proposalHash } }, + ); + return output; +} + +async function loadExpenseRequisites( + coopname: string, + proposalHash: string, +): Promise { + const { [Queries.Expense.ExpenseRequisitesByProposal.name]: output } = await client.Query( + Queries.Expense.ExpenseRequisitesByProposal.query, + { variables: { coopname, proposal_hash: proposalHash } }, + ); + return output; +} + +// Закрытие расхода (финализация СЗ-отчёта советом) — все позиции оплачены и +// отчитаны, председатель/член совета закрывает расход целиком. +async function closeExpenseProposal( + coopname: string, + proposalHash: string, +): Promise { + await client.Mutation(Mutations.Expense.SubmitExpenseReport.mutation, { + variables: { data: { coopname, proposal_hash: proposalHash } }, + }); +} + +// Списочные запросы файлов отдают записи без read_url (он короткоживущий) — +// свежая ссылка запрашивается по id в момент клика. +async function getExpenseFileReadUrl(id: number): Promise { + const { [Queries.Expense.ExpenseFile.name]: result } = await client.Query( + Queries.Expense.ExpenseFile.query, + { variables: { id } }, + ); + return result.read_url ?? undefined; +} + +export const api = { + loadProgramExpenses, + loadProgramExpense, + loadExpenseProposal, + loadExpenseFiles, + loadExpenseRequisites, + closeExpenseProposal, + getExpenseFileReadUrl, +}; diff --git a/components/desktop/extensions/capital/entities/ProgramExpense/model/index.ts b/components/desktop/extensions/capital/entities/ProgramExpense/model/index.ts new file mode 100644 index 00000000000..7a742e8d7f1 --- /dev/null +++ b/components/desktop/extensions/capital/entities/ProgramExpense/model/index.ts @@ -0,0 +1,3 @@ +export * from './types'; +export * from './store'; +export * from './labels'; diff --git a/components/desktop/extensions/capital/entities/ProgramExpense/model/labels.ts b/components/desktop/extensions/capital/entities/ProgramExpense/model/labels.ts new file mode 100644 index 00000000000..549bc8f4ec4 --- /dev/null +++ b/components/desktop/extensions/capital/entities/ProgramExpense/model/labels.ts @@ -0,0 +1,15 @@ +/** + * Подписи статусов шасси расходов — общие для всех пулов, живут в + * `src/shared/lib/expenses`. Здесь — реэкспорт для существующих импортов capital. + */ +export { + proposalStatusLabel, + proposalStatusVariant, + itemStatusLabel, + itemStatusVariant, + mechanicsLabel, + fileKindLabel, + shortExpenseId, + type ExpenseBadgeVariant, + type ExpenseBadgeVariant as ProgramExpenseBadgeVariant, +} from 'src/shared/lib/expenses'; diff --git a/components/desktop/extensions/capital/entities/ProgramExpense/model/store.ts b/components/desktop/extensions/capital/entities/ProgramExpense/model/store.ts new file mode 100644 index 00000000000..e9ab881e5b8 --- /dev/null +++ b/components/desktop/extensions/capital/entities/ProgramExpense/model/store.ts @@ -0,0 +1,38 @@ +import { defineStore } from 'pinia'; +import { ref, type Ref } from 'vue'; +import { api } from '../api'; +import type { + IProgramExpense, + IProgramExpensesPagination, + IGetProgramExpenseInput, + IGetProgramExpensesInput, +} from './types'; + +const namespace = 'programExpenseStore'; + +interface IProgramExpenseStore { + programExpenses: Ref; + loadProgramExpenses: (data: IGetProgramExpensesInput) => Promise; + programExpense: Ref; + loadProgramExpense: (data: IGetProgramExpenseInput) => Promise; +} + +export const useProgramExpenseStore = defineStore(namespace, (): IProgramExpenseStore => { + const programExpenses = ref(null); + const programExpense = ref(null); + + const loadProgramExpenses = async (data: IGetProgramExpensesInput): Promise => { + programExpenses.value = await api.loadProgramExpenses(data); + }; + + const loadProgramExpense = async (data: IGetProgramExpenseInput): Promise => { + programExpense.value = await api.loadProgramExpense(data); + }; + + return { + programExpenses, + programExpense, + loadProgramExpenses, + loadProgramExpense, + }; +}); diff --git a/components/desktop/extensions/capital/entities/ProgramExpense/model/types.ts b/components/desktop/extensions/capital/entities/ProgramExpense/model/types.ts new file mode 100644 index 00000000000..c95d291c918 --- /dev/null +++ b/components/desktop/extensions/capital/entities/ProgramExpense/model/types.ts @@ -0,0 +1,28 @@ +import type { Queries, Mutations } from '@coopenomics/sdk'; + +export type IProgramExpense = + Queries.Capital.GetProgramExpense.IOutput[typeof Queries.Capital.GetProgramExpense.name]; +export type IProgramExpensesPagination = + Queries.Capital.GetProgramExpenses.IOutput[typeof Queries.Capital.GetProgramExpenses.name]; + +export type IGetProgramExpenseInput = Queries.Capital.GetProgramExpense.IInput; +export type IGetProgramExpensesInput = Queries.Capital.GetProgramExpenses.IInput; + +export type IProgramExpenseItem = NonNullable['items'][number]; + +export type IExpenseProposalAggregate = + Queries.Expense.ExpenseProposal.IOutput[typeof Queries.Expense.ExpenseProposal.name]; +export type IExpenseProposalFile = + Queries.Expense.ExpenseFilesByProposal.IOutput[typeof Queries.Expense.ExpenseFilesByProposal.name][number]; +export type IExpenseRequisite = + Queries.Expense.ExpenseRequisitesByProposal.IOutput[typeof Queries.Expense.ExpenseRequisitesByProposal.name][number]; + +export type ICreateProgramExpenseInput = + Mutations.Capital.CreateProgramExpense.IInput['data']; +export type ICreateProgramExpenseOutput = + Mutations.Capital.CreateProgramExpense.IOutput[typeof Mutations.Capital.CreateProgramExpense.name]; + +export type ITopupProgramExpenseInput = + Mutations.Capital.TopupProgramExpense.IInput['data']; +export type ITopupProgramExpenseOutput = + Mutations.Capital.TopupProgramExpense.IOutput[typeof Mutations.Capital.TopupProgramExpense.name]; diff --git a/components/desktop/extensions/capital/features/ProgramExpense/CreateProgramExpense/api/index.ts b/components/desktop/extensions/capital/features/ProgramExpense/CreateProgramExpense/api/index.ts new file mode 100644 index 00000000000..388930a4524 --- /dev/null +++ b/components/desktop/extensions/capital/features/ProgramExpense/CreateProgramExpense/api/index.ts @@ -0,0 +1,16 @@ +import { client } from 'src/shared/api/client'; +import { Mutations } from '@coopenomics/sdk'; +import type { + ICreateProgramExpenseInput, + ICreateProgramExpenseOutput, +} from 'app/extensions/capital/entities/ProgramExpense/model/types'; + +export async function createProgramExpense( + data: ICreateProgramExpenseInput, +): Promise { + const { [Mutations.Capital.CreateProgramExpense.name]: result } = + await client.Mutation(Mutations.Capital.CreateProgramExpense.mutation, { + variables: { data }, + }); + return result; +} diff --git a/components/desktop/extensions/capital/features/ProgramExpense/CreateProgramExpense/model/index.ts b/components/desktop/extensions/capital/features/ProgramExpense/CreateProgramExpense/model/index.ts new file mode 100644 index 00000000000..b8d4c786490 --- /dev/null +++ b/components/desktop/extensions/capital/features/ProgramExpense/CreateProgramExpense/model/index.ts @@ -0,0 +1,41 @@ +import { useSystemStore } from 'src/entities/System/model'; +import { useSessionStore } from 'src/entities/Session'; +import type { ExpenseCreatePayload } from 'src/shared/ui/domain/ExpenseCreateDialog'; +import { createProgramExpense } from '../api'; +import { useProgramExpenseStore } from 'app/extensions/capital/entities/ProgramExpense/model'; + +/** + * Кошелёк-источник программных расходов capital — кооперативный пул расходов + * (его же на чейн ставит capital::createpgexp; здесь — для текста СЗ-документа). + */ +export const PROGRAM_EXPENSE_SOURCE_WALLET = 'w.cap.pgexp'; + +/** Ключ черновика формы создания программного расхода. */ +export const PROGRAM_EXPENSE_DRAFT_KEY = 'mp:capital:create-program-expense:draft'; + +/** + * Программная подача СЗ на чейн: форму, документ и подпись собирает общий + * виджет шасси `ExpenseCreateDialog` — здесь только capital-мутация + * (инициирует capital::createpgexp: счётчики пула + inline expense::createexp). + */ +export function useCreateProgramExpense() { + const { info } = useSystemStore(); + const session = useSessionStore(); + const store = useProgramExpenseStore(); + + async function submitProgramExpense(payload: ExpenseCreatePayload) { + const result = await createProgramExpense({ + coopname: info.coopname, + expense_hash: payload.expense_hash, + creator: session.username, + items: payload.items, + description: payload.description, + statement: payload.statement, + } as any); + + await store.loadProgramExpenses({ coopname: info.coopname }); + return result; + } + + return { submitProgramExpense }; +} diff --git a/components/desktop/extensions/capital/features/ProgramExpense/CreateProgramExpense/ui/CreateProgramExpenseDialog.vue b/components/desktop/extensions/capital/features/ProgramExpense/CreateProgramExpense/ui/CreateProgramExpenseDialog.vue new file mode 100644 index 00000000000..351a00199e4 --- /dev/null +++ b/components/desktop/extensions/capital/features/ProgramExpense/CreateProgramExpense/ui/CreateProgramExpenseDialog.vue @@ -0,0 +1,30 @@ + + + diff --git a/components/desktop/extensions/capital/features/ProgramExpense/CreateProgramExpense/ui/index.ts b/components/desktop/extensions/capital/features/ProgramExpense/CreateProgramExpense/ui/index.ts new file mode 100644 index 00000000000..d5bc11f9466 --- /dev/null +++ b/components/desktop/extensions/capital/features/ProgramExpense/CreateProgramExpense/ui/index.ts @@ -0,0 +1 @@ +export { default as CreateProgramExpenseDialog } from './CreateProgramExpenseDialog.vue'; diff --git a/components/desktop/extensions/capital/features/ProgramExpense/TopupPool/api/index.ts b/components/desktop/extensions/capital/features/ProgramExpense/TopupPool/api/index.ts new file mode 100644 index 00000000000..84122ec50f4 --- /dev/null +++ b/components/desktop/extensions/capital/features/ProgramExpense/TopupPool/api/index.ts @@ -0,0 +1,16 @@ +import { client } from 'src/shared/api/client'; +import { Mutations } from '@coopenomics/sdk'; +import type { + ITopupProgramExpenseInput, + ITopupProgramExpenseOutput, +} from 'app/extensions/capital/entities/ProgramExpense/model/types'; + +export async function topupProgramExpense( + data: ITopupProgramExpenseInput, +): Promise { + const { [Mutations.Capital.TopupProgramExpense.name]: result } = + await client.Mutation(Mutations.Capital.TopupProgramExpense.mutation, { + variables: { data }, + }); + return result; +} diff --git a/components/desktop/extensions/capital/features/ProgramExpense/TopupPool/model/index.ts b/components/desktop/extensions/capital/features/ProgramExpense/TopupPool/model/index.ts new file mode 100644 index 00000000000..d6e05451a0f --- /dev/null +++ b/components/desktop/extensions/capital/features/ProgramExpense/TopupPool/model/index.ts @@ -0,0 +1,19 @@ +import { useSystemStore } from 'src/entities/System/model'; +import { formatToAsset } from 'src/shared/lib/utils/formatToAsset'; +import { topupProgramExpense } from '../api'; + +export function useTopupProgramExpensePool() { + const { info } = useSystemStore(); + + async function submitTopup(rawAmount: string) { + const symbol = info.symbols.root_govern_symbol; + const precision = info.symbols.root_govern_precision; + const amount = formatToAsset(rawAmount, symbol, precision); + return topupProgramExpense({ + coopname: info.coopname, + amount, + }); + } + + return { submitTopup }; +} diff --git a/components/desktop/extensions/capital/features/ProgramExpense/TopupPool/ui/TopupProgramExpensePoolDialog.vue b/components/desktop/extensions/capital/features/ProgramExpense/TopupPool/ui/TopupProgramExpensePoolDialog.vue new file mode 100644 index 00000000000..46e61ccd08d --- /dev/null +++ b/components/desktop/extensions/capital/features/ProgramExpense/TopupPool/ui/TopupProgramExpensePoolDialog.vue @@ -0,0 +1,98 @@ + + + + + diff --git a/components/desktop/extensions/capital/features/ProgramExpense/TopupPool/ui/index.ts b/components/desktop/extensions/capital/features/ProgramExpense/TopupPool/ui/index.ts new file mode 100644 index 00000000000..86bc08737fa --- /dev/null +++ b/components/desktop/extensions/capital/features/ProgramExpense/TopupPool/ui/index.ts @@ -0,0 +1 @@ +export { default as TopupProgramExpensePoolDialog } from './TopupProgramExpensePoolDialog.vue'; diff --git a/components/desktop/extensions/capital/install.ts b/components/desktop/extensions/capital/install.ts index 5c6b2c7fb49..3d6fea07ec6 100644 --- a/components/desktop/extensions/capital/install.ts +++ b/components/desktop/extensions/capital/install.ts @@ -1,7 +1,7 @@ import { markRaw } from 'vue'; import { agreementsBase } from 'src/shared/lib/consts/workspaces'; import type { IWorkspaceConfig } from 'src/shared/lib/types/workspace'; -import { ContributorsPage } from './pages'; +import { ContributorsPage, ProgramExpensesPage, ProgramExpensePage } from './pages'; import { CapitalBase } from './pages/CapitalBase'; import { ProjectsListPage } from './pages/ProjectsListPage'; import { ProjectPage } from './pages/ProjectPage'; @@ -32,10 +32,21 @@ import { ProjectHistoryPage } from './pages/ProjectHistoryPage'; import { ComponentHistoryPage } from './pages/ComponentHistoryPage'; import { ActivityFeedPage } from './pages/ActivityFeedPage'; import { registerCapitalDecisionHandlers } from './app/extensions'; +import { registerExpenseWallet } from 'src/shared/lib/expense-wallets'; export default async function (): Promise { // Регистрируем обработчики решений для расширения capital registerCapitalDecisionHandlers(); + // Пул расходов программы — в общесистемный реестр кошельков расходов + // (страница «Расходы» стола совета собирает пулы всех расширений). + registerExpenseWallet({ + wallet: 'w.cap.pgexp', + title: 'Пул расходов программы «Благорост»', + subtitle: 'Программные расходы ЦПП «Благорост»', + icon: 'receipt_long', + program: 'blagorost', + route: { name: 'capital-program-expenses' }, + }); return [{ workspace: 'capital', extension_name: 'capital', @@ -189,6 +200,33 @@ export default async function (): Promise { }, children: [], }, + { + path: 'program-expenses', + name: 'capital-program-expenses', + component: markRaw(ProgramExpensesPage), + meta: { + title: 'Расходы', + icon: 'receipt_long', + roles: ['chairman', 'member'], + agreements: agreementsBase, + requiresAuth: true, + }, + children: [], + }, + { + path: 'program-expenses/:expense_hash', + name: 'capital-program-expense', + component: markRaw(ProgramExpensePage), + meta: { + title: 'Расход', + icon: 'receipt_long', + roles: ['chairman', 'member'], + agreements: agreementsBase, + requiresAuth: true, + hidden: true, + }, + children: [], + }, { path: 'activity', name: 'activity-feed', diff --git a/components/desktop/extensions/capital/pages/ProgramExpensePage/ProgramExpensePage.vue b/components/desktop/extensions/capital/pages/ProgramExpensePage/ProgramExpensePage.vue new file mode 100644 index 00000000000..93c341f271b --- /dev/null +++ b/components/desktop/extensions/capital/pages/ProgramExpensePage/ProgramExpensePage.vue @@ -0,0 +1,534 @@ + + + + + diff --git a/components/desktop/extensions/capital/pages/ProgramExpensePage/index.ts b/components/desktop/extensions/capital/pages/ProgramExpensePage/index.ts new file mode 100644 index 00000000000..95574ae3e49 --- /dev/null +++ b/components/desktop/extensions/capital/pages/ProgramExpensePage/index.ts @@ -0,0 +1 @@ +export { default as ProgramExpensePage } from './ProgramExpensePage.vue'; diff --git a/components/desktop/extensions/capital/pages/ProgramExpensesPage/ProgramExpensesPage.vue b/components/desktop/extensions/capital/pages/ProgramExpensesPage/ProgramExpensesPage.vue new file mode 100644 index 00000000000..0461bab4e5d --- /dev/null +++ b/components/desktop/extensions/capital/pages/ProgramExpensesPage/ProgramExpensesPage.vue @@ -0,0 +1,166 @@ + + + + + diff --git a/components/desktop/extensions/capital/pages/ProgramExpensesPage/index.ts b/components/desktop/extensions/capital/pages/ProgramExpensesPage/index.ts new file mode 100644 index 00000000000..cfe460d8edd --- /dev/null +++ b/components/desktop/extensions/capital/pages/ProgramExpensesPage/index.ts @@ -0,0 +1 @@ +export { default as ProgramExpensesPage } from './ProgramExpensesPage.vue'; diff --git a/components/desktop/extensions/capital/pages/index.ts b/components/desktop/extensions/capital/pages/index.ts index 4e0d4f12b09..df51fc82e94 100644 --- a/components/desktop/extensions/capital/pages/index.ts +++ b/components/desktop/extensions/capital/pages/index.ts @@ -24,3 +24,5 @@ export * from './ComponentVotingPage'; export * from './ComponentResultsPage'; export * from './InvitationsPage'; export * from './ProcessesPage'; +export * from './ProgramExpensesPage'; +export * from './ProgramExpensePage'; diff --git a/components/desktop/extensions/expenses/E2E.md b/components/desktop/extensions/expenses/E2E.md new file mode 100644 index 00000000000..c97e5edc7d6 --- /dev/null +++ b/components/desktop/extensions/expenses/E2E.md @@ -0,0 +1,106 @@ +# Expenses · E2E Golden Path (C28-34) + +**Статус:** scaffold-stub. На момент создания в `components/desktop/` нет e2e-фреймворка (ни Cypress, ни Playwright — `grep cypress|playwright` по package.json пусто). Документ фиксирует план сценариев и матрицу assertions; реальные spec'и пишутся после закрытия [C28-28..C28-33] и развёртывания e2e-инфраструктуры в desktop. + +## 0. Прежде чем писать spec'и + +1. Согласовать с пользователем выбор фреймворка (Cypress vs Playwright). Канон `feedback_kanon_naming_soglasovyvat`: спрашивать ДО реализации. +2. Завести `components/desktop/test-e2e/` (или `cypress/`/`tests/e2e/`), подключить через quasar config / package.json scripts. +3. Добавить `pnpm test:e2e` в корне (требование Acceptance Criteria из C28-34). +4. Завести fixture-кооператив `voskhod-e2e` с предзаведёнными ролями: `igor` (chairman), `master` (chairman, отдельная личность для Journey B), `svetlana` (member). +5. Завести seed для `BLAGOROST_POOL` (capital::topupprogexp на достаточную сумму). + +## 1. Journey A — DIRECT-оплата организации + +**Контекст:** Игорь (председатель) платит Yandex Cloud из BLAGOROST_POOL. + +| Шаг | Actor | Действие | Ожидание | +|---|---|---|---| +| 1 | `igor` | login → `/voskhod/expenses` → «Создать расход» | dialog открыт | +| 2 | `igor` | items: `[{amount: 8000, description: 'Yandex Cloud, май 2026', mechanics: 'DIRECT', payee: 'Yandex Cloud LLC', requisites: {...}}]`, source=`BLAGOROST_POOL` | форма валидна | +| 3 | `igor` | submit | proposal.status = `created`, СЗ document2 type=2010 в реестре | +| 4 | `igor` | подписать СЗ (2010, signact1) | document2 signed_by_first=true | +| 5 | `council[]` | mock authorize (signact2) | proposal.status = `authorized`, document2 type=2011 | +| 6 | `igor` | `/voskhod/expenses/cashier`, таб «Готово к оплате» | строка видна | +| 7 | `igor` | «Оплатил» → upload платёжки (файл, не document2) | item.status = `paid`, ledger2 op = `o.exp.blgdir` (Дт 08 / Кт 51) | +| 8 | `igor` | submit «Отчёт о расходе» (UI-форма с числами + файлом-чеком) | proposal.status = `report_submitted` | +| 9 | `council[]` | authorize | proposal.status = `closed` | + +**Ledger2 assertions (по hash расхода):** +- `o.exp.blgdir` × 1, amount=8000, Дт 08 / Кт 51 + +**Document2 assertions:** +- type=2010 (СЗ-смета), signact1=igor, signact2= +- type=2011 (протокол-1 авторизации) + +**Файлы-приложения (MinIO, НЕ document2):** +- платёжное поручение / чек оплаты на item +- (отчёт о расходе закрывается одним UI-шагом без отдельного протокола закрытия) + +## 2. Journey B — ADVANCE другому пайщику + +**Контекст:** мастер оформляет 2 аванса (себе + Светлане). + +| Шаг | Actor | Действие | Ожидание | +|---|---|---|---| +| 1 | `master` | login → «Создать расход» → items: `[{amount: 5000, description: 'Сбер-кабинет', mechanics: 'ADVANCE', recipient: 'self'}, {amount: 3000, description: 'Light Lab', mechanics: 'ADVANCE', recipient: 'svetlana'}]`, source=BLAGOROST_POOL | форма валидна | +| 2 | `master` | submit + signact1 | proposal.status = `created` | +| 3 | `council[]` | authorize | proposal.status = `authorized` | +| 4 | `cashier` | `/voskhod/expenses/cashier`, оплатить item(a) → upload файл-платёжка | item(a).status = `paid`, ledger2 op = `o.exp.blgadv` (Дт 08 / Кт 51), notification → master | +| 5 | `cashier` | оплатить item(b) → файл-платёжка | item(b).status = `paid`, ledger2 op = `o.exp.blgadv`, notification → svetlana | +| 6 | `master` | login → `/voskhod/user/payments` → item(a) → «Приложить чек» (FileUploader → MinIO → `reportexp`) | item(a).status = `reported`, ledger2 op = `o.exp.advrpt` (BURN, без проводки) | +| 7 | `svetlana` | login → `/voskhod/user/payments` → item(b) → чек → reportexp | item(b).status = `reported`, ledger2 op = `o.exp.advrpt` | +| 8 | `master` | submitreport | proposal.status = `report_submitted` | +| 9 | `council[]` | authorize | proposal.status = `closed` | + +**Ledger2 assertions:** +- `o.exp.blgadv` × 2 (5000 master, 3000 svetlana), Дт 08 / Кт 51 +- `o.exp.advrpt` × 2, без проводки (только BURN `w.exp.adv`) + +**Document2 assertions:** +- type=2010, items=2 +- type=2011 + +**Файлы-приложения (MinIO, НЕ document2):** +- платёжки × 2 (от кассира при оплате каждому) +- чеки × 2 (от master и svetlana при ADVANCE-отчёте) + +## 3. Common helpers + +```ts +// cy.task / page.evaluate стороне +// 1. loginAs(account: 'igor' | 'master' | 'svetlana') +// 2. seedBlagorostPool(amount: number) +// 3. mockCouncilAuthorize(proposal_hash: string) +// 4. fixturePaymentSlip(): File // файл-платёжка (MinIO) +// 5. fixtureReceipt(): File // файл-чек ADVANCE-отчёта (MinIO) +// 6. assertLedgerOp({ action_code, amount, debit, credit }) +// 7. assertDocument2({ type, signed_by, ... }) +``` + +## 4. Performance gates + +- Каждый сценарий ≤ 3 мин на CI runner (Acceptance Criteria). +- Локально `pnpm test:e2e` запускает оба сценария параллельно (если фреймворк поддерживает) или последовательно с reuse-session. + +## 5. Зависимости (порядок написания spec'ов) + +| # | Должно быть зелёным до старта C28-34 | +|---|---| +| C28-28 | ledger2 action-codes зарегистрированы (иначе assertion `DIRECT_PAYMENT`/`ADVANCE_PAYOUT`/`ADVANCE_REPORTED` падает) | +| C28-29 | контракт `expenses` + capital trigger | +| C28-30 | ✅ document2 2010/2011 (готово); 2012/2013/2014 НЕ заводим — это файлы (MinIO), не шаблоны | +| C28-31 | backend GraphQL — без него Cypress не получит данные для assertion | +| C28-32 | UI расшит (страницы из stub'а → реальные таблицы/формы) | +| C28-33 | Notification Center подключён к 9 событиям шасси | + +## 6. Когда расшивается C28-34 + +1. Получить ack на выбор фреймворка (Cypress vs Playwright). +2. Поднять test-e2e инфраструктуру в desktop (config, runner, fixtures, helpers). +3. Перенести §1 и §2 в spec-файлы: + - `test-e2e/expenses/journey-a-direct.cy.ts` (или `.spec.ts`) + - `test-e2e/expenses/journey-b-advance.cy.ts` +4. Завести seed-фикстуру `voskhod-e2e` с тремя аккаунтами + пул. +5. CI workflow `e2e-expenses` в Gitea-runner (DinD), артефакт — видеозаписи (на падении). +6. Прогон в CI на каждый PR в `dev`. diff --git a/components/desktop/extensions/expenses/INTEGRATIONS.md b/components/desktop/extensions/expenses/INTEGRATIONS.md new file mode 100644 index 00000000000..99b37466a5e --- /dev/null +++ b/components/desktop/extensions/expenses/INTEGRATIONS.md @@ -0,0 +1,123 @@ +# Expenses · Integrations & Cleanup (C28-33) + +**Статус:** doc-scaffold. Файл фиксирует план интеграции Благороста с шасси и cleanup legacy program-expense; реализация после закрытия [C28-29](../../../../_blago/.../C28-29.md), [C28-31](../../../../_blago/.../C28-31.md), [C28-32](../../../../_blago/.../C28-32.md). + +## 1. Благорост как первый потребитель шасси + +### Точка входа + +- Расход из Благороста = СЗ с `source_wallet = BLAGOROST_POOL`. +- Создание идёт через **существующий** `capital::createprogexp` (никаких новых contract-actions; этот action уже из Эпика 1 Благороста). +- Действие `capital::createprogexp` валидирует: + - `source_wallet` принадлежит пайщику-creator'у (chair / member-of-program); + - на пуле достаточно остатка; + - `BLAGOROST_POOL` зарегистрирован в `capital_programs`. + +### Пополнение пула + +Существующий `capital::topupprogexp` (вход в пул), не трогаем; **оставляем единственной обёрткой** из старой пачки `topupProgramExpense` в SDK. + +### UI-вход для Благороста + +`ExpenseProposalCreateDialog` (C28-32) принимает `?source=BLAGOROST_POOL` или эквивалентный prop. Селектор пула фильтруется по правам пайщика. Дальнейшие шаги (approve / authorize / pay / report) — общий канон шасси, без отдельных Благорост-страниц. + +## 2. Каталог уведомлений (универсальный) + +Заводим в Notification Center (C28-22..C28-27). События **универсальны** для всех потребителей шасси, не Благорост-специфичны. + +| Event | Тип | Получатели | +|---|---|---| +| `exp.proposal.created` | action_required | председатель | +| `exp.proposal.approved` | action_required | совет | +| | info | creator | +| `exp.proposal.authorized` | action_required | кассир | +| | info | creator, члены совета | +| `exp.proposal.declined` | info | creator | +| `exp.item.paid` | action_required | получатель аванса (CTA «приложить чек») | +| | info | creator | +| `exp.item.reported` | action_required | кассир | +| `exp.proposal.report.submitted` | action_required | совет | +| `exp.proposal.closed` | info | все участники | +| `exp.item.return` | info | все участники | + +Источник публикации — backend `expenses` extension (C28-31) через NestJS EventEmitter2 → Notification Center. + +## 3. Backward-compat роутов + +| Старый маршрут | Действие | Новый маршрут | +|---|---|---| +| `/:coopname/programs/blagorost/expenses` | редирект (301-аналог в Vue Router) | `/:coopname/expenses?source=BLAGOROST_POOL` | +| `/:coopname/programs/blagorost/expenses/:id` | редирект по hash | `/:coopname/expenses/:hash` | +| `/:coopname/program-expenses/*` (из PR #59) | удалить (см. §4) | — | + +Реализация — `routes.ts` расширения `capital`, `beforeEnter`-guard с `next({ name: 'expenses', query: { source: 'BLAGOROST_POOL' } })`. + +## 4. Cleanup legacy program-expense + +**Текущее состояние ветки `feat/expense-chassis-mvp` @ `6e0fb185b6a`:** + +```bash +grep -rl "ProgramExpense\|program-expense\|programExpense" \ + components/desktop/src components/desktop/extensions \ + components/coopback/src components/cooptypes/src +# → пусто (нет ни одного файла) +``` + +PR #59 не залит в `dev`/`feat/expense-chassis-mvp`. Удалять нечего. При появлении (если merge произойдёт извне) — список таргетов: + +### Desktop + +- `extensions/capital/entities/ProgramExpense/` +- `extensions/capital/features/ProgramExpense/` +- `extensions/capital/widgets/ProgramExpenses/` +- `extensions/capital/pages/ProgramExpensesPage.vue` +- `extensions/capital/pages/ProgramExpenseDetailPage.vue` + +### SDK + +`components/sdk/src/mutations/capital/`: +- `createProgramExpense` — DELETE +- `approveProgramExpense` — DELETE +- `authorizeProgramExpense` — DELETE +- `confirmProgramExpense` — DELETE +- `declineProgramExpense` — DELETE +- `topupProgramExpense` — **KEEP** (вход в пул) + +### Cooptypes + +`components/cooptypes/src/contracts/capital/actions/`: +- `approveProgramExpense` — DELETE +- `authProgramExpense` — DELETE +- `createProgramExpense` — DELETE +- `declineProgramExpense` — DELETE +- `payProgramExpense` — DELETE +- `topupProgramExpense` — **KEEP** + +После удаления — `pnpm -w build` cooptypes + sdk, коммит ZEUS-клиента (canon `feedback_codegen_build_commit`). + +## 5. Шаблоны document2 1010/1011 + +**Состояние:** не существует. + +```bash +find components -path '*document2*' \( -name '*1010*' -o -name '*1011*' \) 2>/dev/null +# → пусто +``` + +Их роль закрывают новые 2010/2011 (C28-30, ✅ commit `1fa41b2b39a`). Удалять нечего. Если найдутся в будущих merge — DELETE без миграции (legacy одиночные program-expense не выходили в прод). + +## 6. Acceptance Criteria + +- [ ] Из BLAGOROST_POOL делается полный путь create → approve → authorize → pay → report → close через UI (после C28-32 расшивки). +- [ ] Уведомления приходят правильным ролям (каталог §2). +- [ ] Старые роуты редиректят, новые работают (§3). +- [ ] Мёртвый код Благороста удалён (§4 — на момент написания: уже чисто). + +## 7. Когда расшивается C28-33 + +1. Закрыть C28-29 (`capital::createprogexp` уже есть; добавить hook на `expenses::createexp` если source=BLAGOROST_POOL). +2. Закрыть C28-31 (backend `expenses` extension + EventEmitter события). +3. C28-32 расшивается до канон-страниц. +4. Подключить Notification Center handlers к 9 событиям (отдельные NotificationProducer в `coopback/src/extensions/expenses/notifications/`). +5. Добавить `beforeEnter`-guard в `extensions/capital/install.ts` для редиректов §3. +6. Финальный re-grep `ProgramExpense` — должен оставаться пустым. diff --git a/components/desktop/extensions/expenses/NAMING-C28-28.md b/components/desktop/extensions/expenses/NAMING-C28-28.md new file mode 100644 index 00000000000..2c9c4c0500f --- /dev/null +++ b/components/desktop/extensions/expenses/NAMING-C28-28.md @@ -0,0 +1,193 @@ +# C28-28 · Naming Proposal v3 — универсальное шасси расхода + +**Изменения v3 (2026-06-02, второй раунд голосового):** (1) контракт `expense` ничего не знает про `capital` — callback на финализацию устанавливается при создании СЗ как **переменная** (`callback_contract` + `callback_action` + опц. `callback_data`), хардкод запрещён; (2) проводка расхода для ОБЕИХ механик Благороста — **Дт 08 / Кт 51** (не 80) — паевой фонд при расходе не трогается, меняется только форма актива 51 → 08; (3) `o.exp.advrpt` без бухпроводки — она уже сделана при выдаче (canal 08/51 второй раз не строим); (4) document2 — только 2010/2011, 2012/2013/2014 = файлы MinIO. + +**Изменения v2 (первый раунд):** контракт делается универсальным — не вводим новых счетов (60/71 отвергнуты), используем существующие 04/08/51/58/80/86; источник средств и operation_code передаются **извне** в payload, контракт ничего не «знает» про Благорост или хозрасходы; матрица 5 операций (вместо 6). + +**Канон:** `feedback_kanon_naming_soglasovyvat` — имена в ontology согласовываются ДО реализации; прецедент 2026-05-19. + +**Источник правды:** `components/contracts/cpp/lib/core/ledger2/{accounts,wallets,operations}.hpp` mono-ai-2 (`feat/expense-chassis-mvp`). + +--- + +## 0. Что изменилось v1 → v2 + +| Точка | v1 | v2 | Причина | +|---|---|---|---| +| Имя контракта | `expenses` (мн.ч.) | **`expense`** (ед.ч.) | пользователь §1 | +| Счета 60/71 | новые account_id | **не вводим** | пользователь §2 — план счетов фиксирован 04/08/51/58/80/86 | +| Кошелёк `w.exp.sup` | COOPERATIVE для расчётов с поставщиком | **отменён** | `w.mkt.payout` занят, `w.exp.sup` дублирует семантику без выгоды | +| Operation codes | 6 в реестре с хардкод-источниками | **5**, по матрице source × mechanic; источник определяется при выборе кода | универсальность шасси, расширение без правки контракта | +| `o.exp.compen` (12 char) | основной | **`o.exp.over`** | пользователь §7.1 — короче, понятнее | +| capwip-флаг | отдельная политика | **payload-флаг `wip_project_hash`** | пользователь §5 — capital trigger только если payload содержит project_hash | +| Расширение реестра | ADD COLUMN | реестр расширяемый — каждая новая пара (source, mechanic) = новая `o.exp.*` запись | хардкод-only constraint OPERATION_REGISTRY | + +--- + +## 1. Контракт-владелец + +`expense` (ед.ч.). Префиксы: +- `[[eosio::action]]`: `expense::createexp`, `submitexp`, `authexp`, `payexp`, `reportexp`, `closeexp`, `declexp`, `returnexp`, `overspendexp`. +- Operation codes: `o.exp.*` (§3). +- Wallets: `w.exp.adv` (один — §4). +- Processes: `p.exp.expns`. + +## 2. План счетов — без новых записей + +Существующие из `accounts.hpp`: +- **04** НМА (А) — если расход капитализируется в РИД +- **08** Вложения во внеоборотные активы (А) — WIP-проект РИД +- **51** Расчётный счёт (А) +- **58** Финансовые вложения (А) — займы +- **80** Паевой фонд (П) +- **86** Целевое финансирование (П) — членские, хозрасходы + +**60/71 отвергнуты.** Подотчёт пайщика моделируется кошельком-резервом (`w.exp.adv` USER_SHARED) аналогично `w.wal.wpend` — без отдельного бух.счёта. + +## 3. Operation codes — 5 в реестре, матрица source × mechanic + +В `OPERATION_REGISTRY[]`: + +| # | C++ константа | eosio::name | WalletOp | wallet_from → wallet_to | Dr / Cr | human_name | +|---|---|---|---|---|---|---| +| 3.1 | `operations::expense::BLAGO_ADVANCE` | `o.exp.blgadv` | TRANSFER | `PROGRAM_EXPENSE_POOL` → `w.exp.adv` | **08 / 51** | «Выдача подотчётных из пула расходов ЦПП «Благорост»» | +| 3.2 | `operations::expense::BLAGO_DIRECT` | `o.exp.blgdir` | BURN | `PROGRAM_EXPENSE_POOL` → ∅ | **08 / 51** | «Прямая оплата из пула расходов ЦПП «Благорост»» | +| 3.3 | `operations::expense::ADVANCE_REPORT` | `o.exp.advrpt` | BURN | `w.exp.adv` → ∅ | 0 / 0 | «Закрытие подотчёта пайщика по отчёту (canal 08/51 уже сделан на blgadv)» | +| 3.4 | `operations::expense::ADVANCE_RETURN` | `o.exp.advret` | TRANSFER | `w.exp.adv` → `PROGRAM_EXPENSE_POOL` | **51 / 08** | «Возврат неиспользованного подотчёта в пул расходов (зеркало blgadv)» | +| 3.5 | `operations::expense::OVERSPEND` | `o.exp.over` | TRANSFER | `PROGRAM_EXPENSE_POOL` → `w.exp.adv` | **08 / 51** | «Доплата сверх подотчёта (перерасход)» | + +**Изменение v4 (2026-06-12, голосовое):** источник оплат — НЕ `w.cap.blago` (он USER_SHARED: списание уменьшало личный паевой кошелёк пайщика-инициатора — категорически неверно). Введён КООПЕРАТИВНЫЙ кошелёк `w.cap.pgexp` (`PROGRAM_EXPENSE_POOL`, «Пул программных расходов ЦПП «Благорост»»): пополняется `capital::topupprogexp` операцией `o.cap.pgtop` (ISSUE, без Dr/Cr — деньги физически на 51 со взносов, выделяется назначение), из него `expense::payexp` платит обе механики. `username` в ledger2-проводках шасси — получатель позиции (подотчёт `w.exp.adv` числится на получателе аванса), не автор СЗ. + +**Изменение v5 (2026-06-12):** фабричная настройка пулов — в `ledger2/operations.hpp` введена таблица `EXPENSE_OPERATION_SETS` (`source_wallet → {advance, direct, report, refund, overspend}` с compile-time валидацией согласованности с `OPERATION_REGISTRY`). Контракт `expense` больше не содержит захардкоженных operation_code: коды всех пяти операций жизненного цикла выводятся из `proposal.source_wallet`. Подключение шасси к новому пулу (например, кошельку членских взносов КУ) = 5 операций в `OPERATION_REGISTRY` + 1 строка в `EXPENSE_OPERATION_SETS`, без правок expense. На фронте пулы регистрируются фабрично: `registerExpenseWallet(...)` из `install.ts` расширения → страница «Расходы» стола совета (`src/shared/lib/expense-wallets`). Общие UI-виджеты шасси — в `src/shared/ui/domain`: `ExpenseCreateDialog` (форма + черновик + генерация и подпись СЗ; подача на чейн — проп `submit`, который задаёт расширение-потребитель) и `ExpenseProposalList` (список СЗ пула, данные подаёт страница); подписи статусов — `src/shared/lib/expenses`. capital — первый потребитель (тонкие обёртки в `features/ProgramExpense`); стол КУ собирает свою страницу из тех же виджетов: `source-wallet` + `draft-key` + своя мутация подачи. + +**Process_type:** один — `processes::expense::PROPOSAL = "p.exp.expns"_n`. Все 5 кодов привязаны к нему. + +### Объяснение проводок + +**Базовое состояние Благороста ДО расхода** (поднял `p.cap.invest.standard.yaml` и `operations.hpp`): + +- Денежная инвестиция (`o.cap.invest`): TRANSFER `w.wal.share` → `w.cap.blago`, **без бухпроводки** (оба счёт 80). Деньги физически на 51 с момента изначального `o.wal.depcpl` (Dr 51 / Cr 80). +- РИД-имущество (`o.cap.import`/`actprp`): Dr 04 / Cr 80, наполняет НМА. +- **08 пустой** на момент инвестиции деньгами — никто туда ничего не клал. + +**Принцип расхода:** деньги физически уходят с 51 → Cr 51. Появляется WIP-стоимость проекта → Dr 08. Паевой фонд (80) **не меняется** — это бух-инвариант (форма актива переходит 51 → 08, ценность остаётся в фонде). + +- **3.1 BLAGO_ADVANCE (Дт 08 / Кт 51):** деньги физически уходят пайщику (Cr 51), стоимость капитализируется в WIP проекта (Dr 08). Кошелёк-резерв `w.exp.adv` фиксирует ответственность пайщика (USER_SHARED) до отчёта. +- **3.2 BLAGO_DIRECT (Дт 08 / Кт 51):** прямая оплата организации — Cr 51 как раз эта оплата, Dr 08 капитализирует стоимость. BURN `w.cap.blago` — резерв пула. +- **3.3 ADVANCE_REPORT (0/0):** проводка 08/51 **уже сделана** на blgadv. При отчёте — только BURN подотчётного кошелька. Никаких canal 08/51 второй раз. +- **3.4 ADVANCE_RETURN (Дт 51 / Кт 08):** деньги возвращаются на банк, WIP-стоимость уменьшается. Зеркало blgadv. Кошелёк-резерв TRANSFER обратно в `w.cap.blago`. +- **3.5 OVERSPEND (Дт 08 / Кт 51):** добавочная выдача после `OVERSPEND` сразу закрывается `o.exp.advrpt` — это две последовательные записи в `expense::overspendexp`. + +### Расширяемость + +Новый источник (хозрасходы из членских 86, фонд развития, AXN) — это **новая пара кодов** в реестре: + +| Будущее | source | target | пример имени | +|---|---|---|---| +| Хозрасходы DIRECT | `w.sov.expns` (86) | банк | `o.exp.sovdir` (Dr 26\*/Cr 51 или прямой Dr 86/Cr 51) | +| Хозрасходы ADVANCE | `w.sov.expns` | пайщик-подотчёт | `o.exp.sovadv` | +| Фонд развития DIRECT | `w.sov.devfnd` (новый) | банк | `o.exp.devdir` | + +Контракт `expense` не меняется — только реестр ledger2 расширяется. Сейчас в MVP — только Благорост (3.1–3.5). + +\* Счёт 26 в плане отсутствует. Если когда-то понадобится — добавится отдельным эпиком; для MVP хозрасходов из членских делаем прямо Dr 86 / Cr 51 без транзита через 26. + +## 4. Кошельки — один новый + +```cpp +// w.exp.* — Шасси расходов (подотчёт пайщика) +static constexpr eosio::name ADVANCE_HOLD = "w.exp.adv"_n; ///< Подотчётные средства у пайщика (USER_SHARED, резерв-pattern; зеркало w.wal.wpend) +``` + +`LEDGER2_WALLET_REGISTRY`: +- `ADVANCE_HOLD` — «Подотчётные средства пайщика» — **USER_SHARED** (разрез по пайщику-получателю; именно он держит ответственность за отчёт). + +`w.exp.sup` (расчёты с поставщиком) **не заводим** — DIRECT обходится без транзитного кошелька. + +`w.mkt.payout` **не трогаем** — занят `o.mkt.recv` (Dr 80 / Cr 51, TRANSFER SHARE_FUND_PAY → SUPPLIER_PAYMENTS). + +## 5. Callback на финализацию — переменная при создании СЗ + +Контракт `expense` **не знает** о `capital`. Никаких импортов `capital::*`, никакого хардкода имени action. + +При создании СЗ (`expense::createexp`) в payload передаётся опциональный блок-callback: + +```cpp +struct callback_handler { + eosio::name contract; ///< кто слушает финализацию (например, "capital"_n) + eosio::name action; ///< что вызвать (например, "onexpreport"_n) + std::vector data; ///< опц. payload (например, packed wip_project_hash) +}; +``` + +Поле хранится в таблице `expense.proposals` рядом с `proposal_hash`. При финализации (`expense::reportexp` или `closeexp`) контракт: + +```cpp +if (proposal.callback.contract != name{}) { + eosio::action::send_inline( + proposal.callback.contract, + proposal.callback.action, + {{ self, "active"_n }}, + std::tuple(proposal_hash, amount, proposal.callback.data) + ); +} +``` + +`expense` дёргает callback **как переменную** — куда сказали при создании, туда и шлёт. Capital у себя сам распарсит `data` (там его `wip_project_hash`) и решит что делать. + +**Никаких:** +- Хардкода имени `capital::onexpreport` в коде `expense`. +- Полей с capital-семантикой (`wip_project_hash`) в схеме `expense`. +- Условных веток «если source = Благорост, то...» — `expense` агностичен к источнику. + +**Альтернативы (отвергнуты):** +- Флаг в payload reportexp — отказ: не масштабируется (один флаг на один контракт-слушатель). +- Event-bus с подписчиками — отказ: для on-chain контрактов EOSIO нет такого паттерна без отдельного event-router'а; перекладывает сложность вне scope MVP. +- Хардкод `capital::onexpreport` — отказ пользователем явно. + +## 6. Универсальный поток + +``` +1. UI собирает СЗ. Выбор источника (UI dropdown → BLAGOROST_FUND / SOV_EXPENSES / ...). +2. UI определяет operation_code по матрице (source × mechanic): + BLAGOROST_FUND + ADVANCE → o.exp.blgadv + BLAGOROST_FUND + DIRECT → o.exp.blgdir + SOV_EXPENSES + DIRECT → o.exp.sovdir (будущее) +3. expense::createexp(proposal_hash, items[], ?callback{contract, action, data}) + — для расходов в РИД-проект Благороста UI/backend заполняет callback = {"capital", "onexpreport", packed(wip_project_hash)}. +4. expense::payexp(proposal_hash, item_hash, operation_code, amount) +5. Контракт валидирует: operation_code ∈ OPERATION_REGISTRY, process_type == p.exp.expns. +6. Контракт зовёт ledger2::apply(operation_code, sum, from?, to?). Реестр сам подставляет правильные wallets и Dr/Cr. +7. Для ADVANCE — фронт пайщика прикладывает чек → expense::reportexp → o.exp.advrpt. +8. Для DIRECT — backend сразу после payexp вызывает reportexp (внутренне, без UI). +9. Если в proposal сохранён callback — после reportexp inline action на (callback.contract, callback.action, data). +``` + +Контракт `expense` агностичен: +- к **источнику** (передаётся operation_code, который сам несёт `wallet_from` в реестре); +- к **программе-получателю** (callback-handler — переменная); +- к **бухгалтерским счетам** (выводятся из реестра по operation_code). + +## 7. Acceptance Criteria для C28-28 v3 — все ack получены + +- [x] §1: контракт `expense` (ед.ч.). Ack 2026-06-02. +- [x] §2: 60/71 не вводим, существующие 04/08/51/58/80/86. Ack 2026-06-02. +- [x] §3 матрица: 5 op-кодов; обе механики Благороста Дт 08 / Кт 51 (не 08/80); хозрасходы (sov-источники) — отдельный эпик. Ack 2026-06-02. +- [x] §4 кошельки: один новый `w.exp.adv` USER_SHARED; `w.exp.sup` отменён; `w.mkt.payout` не трогаем. Ack 2026-06-02. +- [x] §5 callback: не payload-флаг, а **переменная** `callback{contract, action, data}` в `expense.proposals`, заполняется при createexp; `expense` ничего не знает про `capital`. Ack 2026-06-02 («если так тогда ок»). +- [x] §6: контракт универсальный, operation_code определяется фронтом/backend. +- [x] Document2: только 2010/2011; 2012/2013/2014 — файлы MinIO, не шаблоны. Ack 2026-06-02. + +--- + +## 9. Roadmap после ack + +1. C28-28 → C++: + - `accounts.hpp` — без изменений + - `wallets.hpp` — `+w.exp.adv` + - `processes.hpp` — `+p.exp.expns` + - `operations.hpp` — `+5 entries` в `OPERATION_REGISTRY[]` под namespace `operations::expense` +2. CI-тест `ledger2_actions_registry_test` — ожидать 5 новых кодов. +3. `pnpm -w build` cooptypes — auto-regen enum `OperationCode`. +4. C28-29 расшивается на ledger2-инструкции (имена операций ↔ имена C++ actions контракта). +5. C28-31 backend `expense` extension использует cooptypes enums. diff --git a/components/desktop/extensions/expenses/README.md b/components/desktop/extensions/expenses/README.md new file mode 100644 index 00000000000..548454c5864 --- /dev/null +++ b/components/desktop/extensions/expenses/README.md @@ -0,0 +1,129 @@ +# Expenses Extension — UI шасси расходов (C28-32) + +**Статус:** живое — зарегистрировано в `extensions-registry`, backend C28-31 и контракт работают; первый прикладной потребитель — Благорост (`extensions/capital`, пул `w.cap.pgexp`). + +## Как подключить пул расходов на новый стол (рецепт, 2026-06-12) + +Шасси фабричное: контракт `expense`, backend и UI-виджеты переиспользуются без правок — настраивается только пул. + +1. **Контракт/ledger2** (`cpp/lib/core/ledger2/`): кошелёк-пул (COOPERATIVE) в `wallets.hpp`; 5 операций жизненного цикла (аванс/прямая/отчёт/возврат/перерасход) в `OPERATION_REGISTRY` + одна строка в `EXPENSE_OPERATION_SETS` (`operations.hpp`) — коды операций контракт `expense` выводит из `source_wallet` сам. Зеркала имён — `cooptypes/src/ledger2/{operations,processes}.ts` + локатор `controller/.../process-hash-locator.ts`. +2. **Создание СЗ**: либо generic-мутация `Mutations.Expense.CreateExpenseProposal`, либо своя обвязка по образцу `capital::createpgexp` (счётчики/резерв пула + inline `expense::createexp` с callback на финализацию). +3. **Страница стола**: собирается из общих доменных виджетов `ExpenseCreateDialog` (пропы `source-wallet` / `draft-key` / `submit`) + `ExpenseProposalList` — см. `src/shared/ui/domain/ExpenseCreateDialog/README.md`; эталон — `extensions/capital/pages/ProgramExpensesPage` (+ детальная `ProgramExpensePage`). +4. **Стол совета**: `registerExpenseWallet({ wallet, title, route, ... })` в `install.ts` расширения (`src/shared/lib/expense-wallets`) — этим имя пула резолвится в колонке «Кошелёк (пул)» реестра расходов совета (`ExpensesRegistryPage`, маршрут `soviet-expenses-registry`). Реестр показывает ВСЕ расходы кооператива по всем пулам без фильтра; фильтр по конкретному пулу — на странице расходов программы. +5. **Оплата/отчёты** работают сразу: реестр платежей, панели платёжки кассира (`features/Payment/AttachExpenseProof`) и отчёта пайщика (`features/Payment/ReportExpenseAdvance`), закрытие расхода советом — всё привязано к `proposal_hash`/`item_hash`, не к пулу. + +## Фоновые сервисы (backend) + +**Напоминатель об отчёте по авансу под отчёт** — `controller/src/extensions/expenses/application/services/expense-advance-reminder.service.ts`. + +`@Interval`-воркер периодически сканирует зеркало расходов кооператива и находит `ADVANCE`-позиции в статусе `PAID` (аванс выдан пайщику, отчёт не подан). По каждому пайщику-получателю шлёт **один агрегированный недельный дайджест** (workflow `Workflows.ExpenseAdvanceReportReminder` в `@coopenomics/notifications`) со ссылками — на сам расход `/:coopname/expenses/:hash`, если он один, иначе на личную страницу «Платежи» `/:coopname/user/payments`. **Только получателю аванса** (`item.recipient`), кассиру/совету ничего; ORG-получатели пропускаются. + +- Троттлинг «раз в неделю» — через идемпотентность Центра уведомлений: поле `period` (ISO-неделя) в payload делает его стабильным внутри недели, повторные тики гасятся, на новой неделе уходит ровно одно письмо. Своя таблица состояния не нужна. +- Точка отсчёта грейса — `proposal.updated_at` (для `PAID`-не-`REPORTED` item-а это фактически момент выплаты), фолбэк `created_at`. +- Конфиг (env, с дефолтами): `EXPENSE_ADVANCE_REMINDER_INTERVAL_MS` (тик, дефолт 6 ч), `EXPENSE_ADVANCE_REMINDER_GRACE_DAYS` (дней до первого напоминания, дефолт 3). +- **Follow-up (не сделано намеренно):** `deadline` из формы создания СЗ сейчас не доезжает до контракта (живёт только в подписанном документе). Для строгой семантики «просрочен по сроку из заявления» нужен апгрейд: аргумент `deadline` в `createexp` → поле в on-chain `proposals` → проекция парсера; тогда грейс заменяется сравнением с фактическим сроком. + +## Скоуп расширения + +Desktop UI для **шасси системы расходов** (MVP-SINGLE) Цифрового Кооператива. Расширение наполняет 7 страниц + 1 dialog по [issue C28-32](../../../../_blago/.../C28-32...md). + +## Структура страниц + +| Маршрут | Компонент | Назначение | Аудитория | +|---|---|---|---| +| `/:coopname/expenses` | `ExpensesRegistryPage` | Глобальный реестр расходов с фильтрами | все | +| `/:coopname/expenses/:hash` | `ExpenseDetailPage` | Экран доказательств: document2 + expense_files + ledger2 timeline | все | +| `/:coopname/expenses/admin/approve` | `ExpensesAdminApprovePage` | Очередь «Ждут одобрения председателя» | председатель | +| `/:coopname/expenses/admin/authorize` | `ExpensesAdminAuthorizePage` | Очередь «На авторизацию совета» | председатель | +| `/:coopname/expenses/cashier` | `CashierPage` | 4 таба кассира (готово к оплате / ждут отчёта / ждут утверждения / закрыто) | председатель | +| `/:coopname/expenses/my/advances` | `MyAdvancesPage` | Мои авансы — приложить чек (каркас, пока не выведен) | пайщик | +| dialog (без маршрута) | `ExpenseProposalCreateDialog` | Форма-конструктор СЗ с массивом items | все | + +## Зависимости (порядок сборки) + +1. **C28-28** — P0 ledger2 коды (БЛОКИРУЕТ контракт). +2. **C28-29** — контракт `expenses` + cooptypes types. +3. **C28-30** — ✅ document2 2010/2011 (готово). +4. **C28-31** — backend extension `expenses` + GraphQL → SDK Zeus generation. +5. **C28-32** (этот модуль) — UI scaffold готов, страницы-stub; **дозаливка содержимого после C28-31**. +6. **C28-33** — интеграция Благороста. +7. **C28-34** — e2e Cypress. + +## Регистрация в `extensions-registry` + +Зарегистрировано в `src/processes/init-installed-extensions/extensions-registry.ts` (ключ `expenses`). Помимо собственного воркспейса, расширение отдаёт страницу `ExpensesRegistryPage` (реестр всех расходов кооператива) — её монтирует стол совета как «Реестр расходов» (`extensions/soviet/install.ts`, маршрут `soviet-expenses-registry`). Страница пулов `ExpenseWalletsPage` остаётся в barrel'е, но на столах больше не монтируется (источник пулов — реестр `expense-wallets` для резолва имени кошелька). + +## Канон вёрстки (`/home/admin/.claude/skills/mono-desktop-canon/`) + +Каждая страница строится **строго** из `shared/ui/{base,domain,layout}`: + +- **base:** `BaseTable`, `BaseChip`/`BaseBadge`, `BaseBanner`/`.banner--info`, `BaseDialog`, `BaseInput`, `BaseSelect`, `BaseForm`, `BaseButton`, `BaseCard`, `EmptyState`, `TableSkeleton`. +- **domain:** ``, ``, ``, ``, ``, ``, `` (новый, под C28-32 — TBA в `shared/ui/domain/RequisitesPicker/`). +- **layout:** ``, ``, ``. + +Иконки **только Material** `q-icon`: `receipt_long`, `payments`, `construction`, `gavel`, `upload_file`. FontAwesome (`fa-*`) запрещён (отличие от capital/reports extensions, где FA — legacy). + +Spacing/радиусы/цвета — токены `--p-*` и utility (`bg-primary`/`text-positive`). Никаких hex/inline px. + +## Структура файлов + +``` +extensions/expenses/ +├── README.md ← этот файл +├── install.ts ← IWorkspaceConfig (routes + meta) +├── index.ts ← экспорты для extensions-registry +└── pages/ + ├── index.ts + ├── ExpensesRegistryPage.vue ← /expenses + ├── ExpenseDetailPage.vue ← /expenses/:hash + ├── ExpensesAdminApprovePage.vue ← /expenses/admin/approve + ├── ExpensesAdminAuthorizePage.vue ← /expenses/admin/authorize + ├── CashierPage.vue ← /expenses/cashier + ├── MyAdvancesPage.vue ← /expenses/my/advances (каркас) + └── ExpenseProposalCreateDialog.vue ← dialog (вызывается из ExpensesRegistryPage) +``` + +Дальше внутри каждой страницы — стандартная FSD-структура (`api/`, `model/`, `ui/`), но **до C28-31 запрещено** добавлять `api/index.ts` с raw GraphQL — канон monorepo (`mono-ai-2/CLAUDE.md`, секция «Никаких raw GraphQL-строк в desktop»). + +## GraphQL-зависимости (из C28-31) + +UI-страницы будут потреблять (через SDK Zeus, после C28-31): + +```ts +import { Queries, Mutations } from '@coopenomics/sdk'; + +// Реестр + детали +Queries.Expenses.ExpenseProposals.query // /expenses +Queries.Expenses.ExpenseProposalDetails.query // /expenses/:hash +Queries.Expenses.ExpenseFiles.query + +// Кассир + админ +Mutations.Expenses.PayExpenseItem.mutation +Mutations.Expenses.SubmitExpenseReport.mutation +// Авторизация/отклонение СЗ — решение совета (повестка), мутаций нет +Mutations.Expenses.ReturnExpenseItem.mutation +Mutations.Expenses.UploadExpenseFile.mutation + +// Создание (через capital extension, не expenses!) +Mutations.Capital.CreateProgramExpenseProposal.mutation +``` + +## Когда расшивается C28-31 + +Каждая страница-stub расшивается по списку: + +1. **`ExpensesRegistryPage`** — `BaseTable` с фильтр-баром ``, polling через `useDataPoller(POLL_INTERVALS.MEDIUM)`, статусы через ``, низ — `.table-foot` с «Загрузить ещё». +2. **`ExpenseDetailPage`** — `` + статус-чип, секции: проект-карточка → item-table → цепочка артефактов (`` 2010/2011 + ``-показ файлов) → `` lifecycle. +3. **`ExpensesAdminApprovePage`** / **`ExpensesAdminAuthorizePage`** — canon-таблицы со столбцом действий (одобрить/отклонить/авторизовать/декланировать). Decline-модалка: `` с `` для reason. +4. **`CashierPage`** — `` с 4 вкладками; каждая = `BaseTable`. Действие «Оплатил, приложить чек» = `` с `` (multipart → MinIO через `Mutations.Expenses.UploadExpenseFile` → следом `Mutations.Expenses.PayExpenseItem`). +5. **`MyAdvancesPage`** — КАРКАС будущего стола кассира (роут `expenses-my-advances`), пока НЕ выведен в меню и ничего на него не ведёт. До сборки стола кассира авансы пайщика-получателя видны на его личной странице «Платежи» (`/:coopname/user/payments`, `ListOfPaymentsWidget`): строки авансов с панелью `ReportExpenseAdvancePanel` (приложить чек / отчитаться). Рабочие ссылки (напоминатель об авансах) ведут туда, а не на эту страницу. +6. **`ExpenseProposalCreateDialog`** — `` + `` с массивом items, добавление/удаление item'ов через ``. Per-item — `` для recipient_type {SELF/MEMBER/ORG}, `` для mechanics {ADVANCE/DIRECT}, `` для сумм, `` (новый компонент в `shared/ui/domain/`) для ADVANCE-получателя. + +## Ссылки + +- PRD шасси: `13-platforma-tsifrovogo-kooperativa/components/14-versiya-3/requirements/f8-prd-shassi-sistemy-raskhodov-tsifrovogo-kooperativa-v12.md` +- Issue C28-32: `13-platforma-tsifrovogo-kooperativa/components/14-versiya-3/issues/C28-32-shassi-raskhodov-epik-4-ui-shassi-canon-mono-platform-v2.md` +- Канон вёрстки: `/home/admin/.claude/skills/mono-desktop-canon/` + `src/pages/_dev/ui/index.vue` +- HTML SoT: `/home/admin/blago/production/shared/MONO Design System.html` +- Backend плейн: `components/controller/src/extensions/expenses/README.md` (C28-31) +- Document2 templates: 2010 ExpenseProposalStatement, 2011 ExpenseProposalDecision (C28-30 ✅) diff --git a/components/desktop/extensions/expenses/api/index.ts b/components/desktop/extensions/expenses/api/index.ts new file mode 100644 index 00000000000..895efae7ac1 --- /dev/null +++ b/components/desktop/extensions/expenses/api/index.ts @@ -0,0 +1,27 @@ +export { getExpenseProposalsByCooperative } from './queries/getExpenseProposalsByCooperative'; +export type { + IGetExpenseProposalsByCooperativeInput, + IExpenseProposalsByCooperativeResult, +} from './queries/getExpenseProposalsByCooperative'; +export { getExpenseProposalsByMember } from './queries/getExpenseProposalsByMember'; +export type { + IGetExpenseProposalsByMemberInput, + IExpenseProposalsByMemberResult, +} from './queries/getExpenseProposalsByMember'; +export { getExpenseProposal } from './queries/getExpenseProposal'; +export type { + IGetExpenseProposalInput, + IExpenseProposalResult, +} from './queries/getExpenseProposal'; +export { getExpenseFilesByProposal } from './queries/getExpenseFilesByProposal'; +export type { + IGetExpenseFilesByProposalInput, + IExpenseFilesByProposalResult, +} from './queries/getExpenseFilesByProposal'; +export { getExpenseFileReadUrl } from './queries/getExpenseFileReadUrl'; +export { generateExpenseProposalStatementDocument } from './mutations/generateExpenseProposalStatementDocument'; +export type { IGenerateExpenseProposalStatementDocumentInput } from './mutations/generateExpenseProposalStatementDocument'; +export { generateExpenseProposalDecisionDocument } from './mutations/generateExpenseProposalDecisionDocument'; +export type { IGenerateExpenseProposalDecisionDocumentInput } from './mutations/generateExpenseProposalDecisionDocument'; +export { createExpenseProposal } from './mutations/createExpenseProposal'; +export type { ICreateExpenseProposalInput } from './mutations/createExpenseProposal'; diff --git a/components/desktop/extensions/expenses/api/mutations/createExpenseProposal.ts b/components/desktop/extensions/expenses/api/mutations/createExpenseProposal.ts new file mode 100644 index 00000000000..767541823a7 --- /dev/null +++ b/components/desktop/extensions/expenses/api/mutations/createExpenseProposal.ts @@ -0,0 +1,10 @@ +import { client } from 'src/shared/api/client'; +import { Mutations } from '@coopenomics/sdk'; + +export type ICreateExpenseProposalInput = Mutations.Expense.CreateExpenseProposal.IInput['data']; + +export async function createExpenseProposal(data: ICreateExpenseProposalInput) { + return client.Mutation(Mutations.Expense.CreateExpenseProposal.mutation, { + variables: { data }, + }); +} diff --git a/components/desktop/extensions/expenses/api/mutations/generateExpenseProposalDecisionDocument.ts b/components/desktop/extensions/expenses/api/mutations/generateExpenseProposalDecisionDocument.ts new file mode 100644 index 00000000000..a8ac0b43dfb --- /dev/null +++ b/components/desktop/extensions/expenses/api/mutations/generateExpenseProposalDecisionDocument.ts @@ -0,0 +1,19 @@ +import { client } from 'src/shared/api/client'; +import { Mutations } from '@coopenomics/sdk'; + +export type IGenerateExpenseProposalDecisionDocumentInput = + Mutations.Expense.GenerateExpenseProposalDecisionDocument.IInput['data']; + +export async function generateExpenseProposalDecisionDocument( + data: IGenerateExpenseProposalDecisionDocumentInput, +) { + return client.Mutation( + Mutations.Expense.GenerateExpenseProposalDecisionDocument.mutation, + { + variables: { + data, + options: { lang: 'ru' }, + }, + }, + ); +} diff --git a/components/desktop/extensions/expenses/api/mutations/generateExpenseProposalStatementDocument.ts b/components/desktop/extensions/expenses/api/mutations/generateExpenseProposalStatementDocument.ts new file mode 100644 index 00000000000..22a0cfc606f --- /dev/null +++ b/components/desktop/extensions/expenses/api/mutations/generateExpenseProposalStatementDocument.ts @@ -0,0 +1,19 @@ +import { client } from 'src/shared/api/client'; +import { Mutations } from '@coopenomics/sdk'; + +export type IGenerateExpenseProposalStatementDocumentInput = + Mutations.Expense.GenerateExpenseProposalStatementDocument.IInput['data']; + +export async function generateExpenseProposalStatementDocument( + data: IGenerateExpenseProposalStatementDocumentInput, +) { + return client.Mutation( + Mutations.Expense.GenerateExpenseProposalStatementDocument.mutation, + { + variables: { + data, + options: { lang: 'ru' }, + }, + }, + ); +} diff --git a/components/desktop/extensions/expenses/api/queries/getExpenseFileReadUrl.ts b/components/desktop/extensions/expenses/api/queries/getExpenseFileReadUrl.ts new file mode 100644 index 00000000000..446a31bc647 --- /dev/null +++ b/components/desktop/extensions/expenses/api/queries/getExpenseFileReadUrl.ts @@ -0,0 +1,13 @@ +import { client } from 'src/shared/api/client'; +import { Queries } from '@coopenomics/sdk'; + +// Списочные запросы файлов отдают короткоживущий read_url — свежую ссылку на +// файл (чек/подтверждение) запрашиваем по id в момент клика. Тот же приём, что +// на странице расхода программы (capital ProgramExpense.api). +export async function getExpenseFileReadUrl(id: number): Promise { + const { [Queries.Expense.ExpenseFile.name]: result } = await client.Query( + Queries.Expense.ExpenseFile.query, + { variables: { id } }, + ); + return result.read_url ?? undefined; +} diff --git a/components/desktop/extensions/expenses/api/queries/getExpenseFilesByProposal.ts b/components/desktop/extensions/expenses/api/queries/getExpenseFilesByProposal.ts new file mode 100644 index 00000000000..4efde529a3c --- /dev/null +++ b/components/desktop/extensions/expenses/api/queries/getExpenseFilesByProposal.ts @@ -0,0 +1,18 @@ +import { client } from 'src/shared/api/client'; +import { Queries } from '@coopenomics/sdk'; + +export type IGetExpenseFilesByProposalInput = + Queries.Expense.ExpenseFilesByProposal.IInput; + +export type IExpenseFilesByProposalResult = + Queries.Expense.ExpenseFilesByProposal.IOutput[typeof Queries.Expense.ExpenseFilesByProposal.name]; + +export async function getExpenseFilesByProposal( + data: IGetExpenseFilesByProposalInput, +): Promise { + const { [Queries.Expense.ExpenseFilesByProposal.name]: result } = + await client.Query(Queries.Expense.ExpenseFilesByProposal.query, { + variables: data, + }); + return result; +} diff --git a/components/desktop/extensions/expenses/api/queries/getExpenseProposal.ts b/components/desktop/extensions/expenses/api/queries/getExpenseProposal.ts new file mode 100644 index 00000000000..29f8b6cdbd2 --- /dev/null +++ b/components/desktop/extensions/expenses/api/queries/getExpenseProposal.ts @@ -0,0 +1,17 @@ +import { client } from 'src/shared/api/client'; +import { Queries } from '@coopenomics/sdk'; + +export type IGetExpenseProposalInput = Queries.Expense.ExpenseProposal.IInput; + +export type IExpenseProposalResult = + Queries.Expense.ExpenseProposal.IOutput[typeof Queries.Expense.ExpenseProposal.name]; + +export async function getExpenseProposal( + data: IGetExpenseProposalInput, +): Promise { + const { [Queries.Expense.ExpenseProposal.name]: result } = await client.Query( + Queries.Expense.ExpenseProposal.query, + { variables: data }, + ); + return result; +} diff --git a/components/desktop/extensions/expenses/api/queries/getExpenseProposalsByCooperative.ts b/components/desktop/extensions/expenses/api/queries/getExpenseProposalsByCooperative.ts new file mode 100644 index 00000000000..230d42f14b9 --- /dev/null +++ b/components/desktop/extensions/expenses/api/queries/getExpenseProposalsByCooperative.ts @@ -0,0 +1,18 @@ +import { client } from 'src/shared/api/client'; +import { Queries } from '@coopenomics/sdk'; + +export type IGetExpenseProposalsByCooperativeInput = + Queries.Expense.ExpenseProposalsByCooperative.IInput; + +export type IExpenseProposalsByCooperativeResult = + Queries.Expense.ExpenseProposalsByCooperative.IOutput[typeof Queries.Expense.ExpenseProposalsByCooperative.name]; + +export async function getExpenseProposalsByCooperative( + data: IGetExpenseProposalsByCooperativeInput, +): Promise { + const { [Queries.Expense.ExpenseProposalsByCooperative.name]: result } = + await client.Query(Queries.Expense.ExpenseProposalsByCooperative.query, { + variables: data, + }); + return result; +} diff --git a/components/desktop/extensions/expenses/api/queries/getExpenseProposalsByMember.ts b/components/desktop/extensions/expenses/api/queries/getExpenseProposalsByMember.ts new file mode 100644 index 00000000000..13270d272f7 --- /dev/null +++ b/components/desktop/extensions/expenses/api/queries/getExpenseProposalsByMember.ts @@ -0,0 +1,18 @@ +import { client } from 'src/shared/api/client'; +import { Queries } from '@coopenomics/sdk'; + +export type IGetExpenseProposalsByMemberInput = + Queries.Expense.ExpenseProposalsByMember.IInput; + +export type IExpenseProposalsByMemberResult = + Queries.Expense.ExpenseProposalsByMember.IOutput[typeof Queries.Expense.ExpenseProposalsByMember.name]; + +export async function getExpenseProposalsByMember( + data: IGetExpenseProposalsByMemberInput, +): Promise { + const { [Queries.Expense.ExpenseProposalsByMember.name]: result } = + await client.Query(Queries.Expense.ExpenseProposalsByMember.query, { + variables: data, + }); + return result; +} diff --git a/components/desktop/extensions/expenses/index.ts b/components/desktop/extensions/expenses/index.ts new file mode 100644 index 00000000000..27dc3f9323e --- /dev/null +++ b/components/desktop/extensions/expenses/index.ts @@ -0,0 +1,3 @@ +// Expenses extension — UI шасси расходов (C28-32, scaffold-stub). +// Полноценная регистрация в src/processes/init-installed-extensions/extensions-registry.ts +// будет добавлена после закрытия C28-31 (backend GraphQL + SDK Zeus). См. README.md. diff --git a/components/desktop/extensions/expenses/install.ts b/components/desktop/extensions/expenses/install.ts new file mode 100644 index 00000000000..95d01d39bdc --- /dev/null +++ b/components/desktop/extensions/expenses/install.ts @@ -0,0 +1,112 @@ +import { markRaw } from 'vue'; +import { agreementsBase } from 'src/shared/lib/consts/workspaces'; +import type { IWorkspaceConfig } from 'src/shared/lib/types/workspace'; +import { + ExpensesRegistryPage, + ExpenseDetailPage, + ExpensesAdminApprovePage, + CashierPage, + MyAdvancesPage, +} from './pages'; + +// Шасси расходов — UI scaffold (C28-32). Зарегистрировано в +// src/processes/init-installed-extensions/extensions-registry.ts после +// расшивки 5 mutations C28-31 (b4dd4b335ac). См. README.md. +export default async function (): Promise { + return [{ + workspace: 'expenses', + extension_name: 'expenses', + title: 'Расходы', + icon: 'receipt_long', + defaultRoute: 'expenses-registry', + routes: [ + { + meta: { + title: 'Расходы', + icon: 'receipt_long', + roles: [], + }, + path: '/:coopname/expenses', + name: 'expenses', + // ВНИМАНИЕ: воркспейс `expenses` пока НЕ привязан ни к одному столу + // (меню столов приходит с бэкенда; setRoutes вешает пункт лишь если + // воркспейс есть в desktop-конфиге). Поэтому страницы ниже — рабочий + // КАРКАС будущего «стола кассира» (касса/одобрение/мои авансы): + // открываются только по прямому URL, в навигации их нет, и НИЧЕГО на + // них не ведёт намеренно. НЕ удалять как «мёртвый код» — соберём стол + // кассира из них позже. Реально используются сейчас только + // expenses-registry (монтируется на столе совета как «Реестр расходов») + // и expenses-detail (деталь, куда проваливается реестр). Рабочие ссылки + // (напоминатель об авансах и т.п.) ведут на личные «Платежи» + // /:coopname/user/payments, НЕ на эти каркасные страницы. + children: [ + { + path: '', + name: 'expenses-registry', + component: markRaw(ExpensesRegistryPage), + meta: { + title: 'Реестр расходов', + icon: 'receipt_long', + roles: [], + agreements: agreementsBase, + requiresAuth: true, + }, + children: [], + }, + { + path: 'admin/approve', + name: 'expenses-admin-approve', + component: markRaw(ExpensesAdminApprovePage), + meta: { + title: 'На одобрение председателя', + icon: 'gavel', + roles: ['chairman'], + agreements: agreementsBase, + requiresAuth: true, + }, + children: [], + }, + { + path: 'cashier', + name: 'expenses-cashier', + component: markRaw(CashierPage), + meta: { + title: 'Касса', + icon: 'payments', + roles: ['chairman'], + agreements: agreementsBase, + requiresAuth: true, + }, + children: [], + }, + { + path: 'my/advances', + name: 'expenses-my-advances', + component: markRaw(MyAdvancesPage), + meta: { + title: 'Мои авансы', + icon: 'account_balance_wallet', + roles: [], + agreements: agreementsBase, + requiresAuth: true, + }, + children: [], + }, + { + path: ':hash', + name: 'expenses-detail', + component: markRaw(ExpenseDetailPage), + meta: { + title: 'Расход', + roles: [], + agreements: agreementsBase, + requiresAuth: true, + hidden: true, + }, + children: [], + }, + ], + }, + ], + }]; +} diff --git a/components/desktop/extensions/expenses/model/expenseItemStatus.ts b/components/desktop/extensions/expenses/model/expenseItemStatus.ts new file mode 100644 index 00000000000..1f15c8d8baf --- /dev/null +++ b/components/desktop/extensions/expenses/model/expenseItemStatus.ts @@ -0,0 +1,42 @@ +import { Zeus } from '@coopenomics/sdk'; +import type { BaseBadgeVariant } from 'src/shared/ui/base/BaseBadge'; + +export const expenseItemStatusLabel: Record = { + [Zeus.ExpenseItemStatus.UNDEFINED]: 'Не определена', + [Zeus.ExpenseItemStatus.APPROVED]: 'Одобрена', + [Zeus.ExpenseItemStatus.PAID]: 'Оплачена', + [Zeus.ExpenseItemStatus.REPORTED]: 'Отчёт подан', + [Zeus.ExpenseItemStatus.RETURNED]: 'Возврат', + [Zeus.ExpenseItemStatus.OVERSPENT]: 'Перерасход', +}; + +export const expenseItemStatusVariant: Record = { + [Zeus.ExpenseItemStatus.UNDEFINED]: 'neutral', + [Zeus.ExpenseItemStatus.APPROVED]: 'info', + [Zeus.ExpenseItemStatus.PAID]: 'warn', + [Zeus.ExpenseItemStatus.REPORTED]: 'warn', + [Zeus.ExpenseItemStatus.RETURNED]: 'pos', + [Zeus.ExpenseItemStatus.OVERSPENT]: 'neg', +}; + +export function getExpenseItemStatusLabel(status?: Zeus.ExpenseItemStatus | null): string { + if (!status) return expenseItemStatusLabel[Zeus.ExpenseItemStatus.UNDEFINED]; + return expenseItemStatusLabel[status] || expenseItemStatusLabel[Zeus.ExpenseItemStatus.UNDEFINED]; +} + +export function getExpenseItemStatusVariant( + status?: Zeus.ExpenseItemStatus | null, +): BaseBadgeVariant { + if (!status) return 'neutral'; + return expenseItemStatusVariant[status] || 'neutral'; +} + +export const expenseMechanicsLabel: Record = { + [Zeus.ExpenseMechanics.ADVANCE]: 'Аванс', + [Zeus.ExpenseMechanics.DIRECT]: 'Прямая оплата', +}; + +export function getExpenseMechanicsLabel(mechanics?: Zeus.ExpenseMechanics | null): string { + if (!mechanics) return '—'; + return expenseMechanicsLabel[mechanics] || '—'; +} diff --git a/components/desktop/extensions/expenses/model/expenseProposalStatus.ts b/components/desktop/extensions/expenses/model/expenseProposalStatus.ts new file mode 100644 index 00000000000..83fa5517d2a --- /dev/null +++ b/components/desktop/extensions/expenses/model/expenseProposalStatus.ts @@ -0,0 +1,34 @@ +import { Zeus } from '@coopenomics/sdk'; +import type { BaseBadgeVariant } from 'src/shared/ui/base/BaseBadge'; + +export const expenseProposalStatusLabel: Record = { + [Zeus.ExpenseProposalStatus.UNDEFINED]: 'Не определён', + [Zeus.ExpenseProposalStatus.CREATED]: 'Подан', + [Zeus.ExpenseProposalStatus.AUTHORIZED]: 'Утверждён советом', + [Zeus.ExpenseProposalStatus.PARTIALLY_PAID]: 'Частично оплачен', + [Zeus.ExpenseProposalStatus.REPORT_SUBMITTED]: 'Отчёт подан', + [Zeus.ExpenseProposalStatus.CLOSED]: 'Закрыт', + [Zeus.ExpenseProposalStatus.DECLINED]: 'Отклонён', +}; + +export const expenseProposalStatusVariant: Record = { + [Zeus.ExpenseProposalStatus.UNDEFINED]: 'neutral', + [Zeus.ExpenseProposalStatus.CREATED]: 'info', + [Zeus.ExpenseProposalStatus.AUTHORIZED]: 'info', + [Zeus.ExpenseProposalStatus.PARTIALLY_PAID]: 'warn', + [Zeus.ExpenseProposalStatus.REPORT_SUBMITTED]: 'warn', + [Zeus.ExpenseProposalStatus.CLOSED]: 'pos', + [Zeus.ExpenseProposalStatus.DECLINED]: 'neg', +}; + +export function getExpenseProposalStatusLabel(status?: Zeus.ExpenseProposalStatus | null): string { + if (!status) return expenseProposalStatusLabel[Zeus.ExpenseProposalStatus.UNDEFINED]; + return expenseProposalStatusLabel[status] || expenseProposalStatusLabel[Zeus.ExpenseProposalStatus.UNDEFINED]; +} + +export function getExpenseProposalStatusVariant( + status?: Zeus.ExpenseProposalStatus | null, +): BaseBadgeVariant { + if (!status) return 'neutral'; + return expenseProposalStatusVariant[status] || 'neutral'; +} diff --git a/components/desktop/extensions/expenses/model/index.ts b/components/desktop/extensions/expenses/model/index.ts new file mode 100644 index 00000000000..99bf53accb4 --- /dev/null +++ b/components/desktop/extensions/expenses/model/index.ts @@ -0,0 +1,19 @@ +export { + expenseProposalStatusLabel, + expenseProposalStatusVariant, + getExpenseProposalStatusLabel, + getExpenseProposalStatusVariant, +} from './expenseProposalStatus'; +export { + expenseItemStatusLabel, + expenseItemStatusVariant, + expenseMechanicsLabel, + getExpenseItemStatusLabel, + getExpenseItemStatusVariant, + getExpenseMechanicsLabel, +} from './expenseItemStatus'; +export { useExpenseProposalActions } from './useExpenseProposalActions'; +export type { + ICreateProposalDraft, + ICreateProposalDraftItem, +} from './useExpenseProposalActions'; diff --git a/components/desktop/extensions/expenses/model/useExpenseProposalActions.ts b/components/desktop/extensions/expenses/model/useExpenseProposalActions.ts new file mode 100644 index 00000000000..6d25e802043 --- /dev/null +++ b/components/desktop/extensions/expenses/model/useExpenseProposalActions.ts @@ -0,0 +1,110 @@ +import { useSystemStore } from 'src/entities/System/model'; +import { useSessionStore } from 'src/entities/Session'; +import { DigitalDocument } from 'src/shared/lib/document'; +import type { Cooperative } from 'cooptypes'; +import { generateUniqueHash } from 'src/shared/lib/utils/generateUniqueHash'; +import { + generateExpenseProposalStatementDocument, + createExpenseProposal, +} from '../api'; + +export interface ICreateProposalDraftItem { + number: string; + description: string; + amount: string; + recipient_type: 'SELF' | 'MEMBER' | 'ORG'; + mechanics: 'ADVANCE' | 'DIRECT'; + recipient_name?: string; + requisites?: string; + /** Назначение платежа (оплата по счёту организации/ИП). */ + payment_purpose?: string; + item_hash?: string; + recipient_account?: string; +} + +export interface ICreateProposalDraft { + source_wallet: string; + description: string; + /** Срок исполнения («в срок до»), значение date-input `YYYY-MM-DD`. */ + deadline: string; + items: ICreateProposalDraftItem[]; +} + +export function useExpenseProposalActions() { + const { info } = useSystemStore(); + const session = useSessionStore(); + + async function submitProposal(draft: ICreateProposalDraft) { + const proposal_hash = await generateUniqueHash(); + + const totalAmount = draft.items.reduce((sum, it) => sum + parseFloat(it.amount || '0'), 0); + const total_amount = `${totalAmount.toFixed(4)} RUB`; + + const itemsForDoc = draft.items.map((it, idx) => ({ + number: String(idx + 1), + description: it.description, + amount: `${parseFloat(it.amount || '0').toFixed(4)} RUB`, + recipient_type: it.recipient_type, + mechanics: it.mechanics, + recipient_name: it.recipient_name ?? '', + requisites: it.requisites ?? '', + payment_purpose: it.payment_purpose || undefined, + })); + + const proposalHeader = { + description: draft.description, + total_amount, + items_count: draft.items.length, + source_wallet: draft.source_wallet, + deadline: formatDeadline(draft.deadline), + }; + + const generated = await generateExpenseProposalStatementDocument({ + coopname: info.coopname, + username: session.username, + proposal_hash, + proposal: proposalHeader, + items: itemsForDoc, + } as any); + + const docKey = 'generateExpenseProposalStatementDocument' as const; + const generatedDoc = (generated as any)[docKey]; + + const digital = new DigitalDocument(generatedDoc); + const signed = await digital.sign( + session.username, + 1, + ); + + const itemsForChain = draft.items.map((it, idx) => ({ + item_hash: it.item_hash ?? generateItemHash(proposal_hash, idx), + mechanics: it.mechanics, + recipient_type: it.recipient_type, + recipient: it.recipient_account ?? it.recipient_name ?? session.username, + description: it.description, + planned_amount: it.amount, + payment_purpose: it.payment_purpose || undefined, + })); + + return createExpenseProposal({ + coopname: info.coopname, + username: session.username, + proposal_hash, + source_wallet: draft.source_wallet, + items: itemsForChain, + statement: signed as any, + } as any); + } + + return { submitProposal }; +} + +function generateItemHash(proposal_hash: string, idx: number): string { + return `${proposal_hash.slice(0, 56)}${idx.toString(16).padStart(8, '0')}`; +} + +/** `YYYY-MM-DD` (date-input) → `DD.MM.YYYY` (документ). */ +function formatDeadline(value: string): string { + const [year, month, day] = value.split('-'); + return `${day}.${month}.${year}`; +} diff --git a/components/desktop/extensions/expenses/pages/CashierPage.vue b/components/desktop/extensions/expenses/pages/CashierPage.vue new file mode 100644 index 00000000000..15db96aa4e1 --- /dev/null +++ b/components/desktop/extensions/expenses/pages/CashierPage.vue @@ -0,0 +1,263 @@ + + + + + diff --git a/components/desktop/extensions/expenses/pages/ExpenseDetailPage.vue b/components/desktop/extensions/expenses/pages/ExpenseDetailPage.vue new file mode 100644 index 00000000000..6c2cbe995b0 --- /dev/null +++ b/components/desktop/extensions/expenses/pages/ExpenseDetailPage.vue @@ -0,0 +1,717 @@ + + + + + diff --git a/components/desktop/extensions/expenses/pages/ExpenseProposalCreateDialog.vue b/components/desktop/extensions/expenses/pages/ExpenseProposalCreateDialog.vue new file mode 100644 index 00000000000..adb0a0b1fcb --- /dev/null +++ b/components/desktop/extensions/expenses/pages/ExpenseProposalCreateDialog.vue @@ -0,0 +1,273 @@ + + + + + diff --git a/components/desktop/extensions/expenses/pages/ExpenseWalletsPage.vue b/components/desktop/extensions/expenses/pages/ExpenseWalletsPage.vue new file mode 100644 index 00000000000..0bcc16919f6 --- /dev/null +++ b/components/desktop/extensions/expenses/pages/ExpenseWalletsPage.vue @@ -0,0 +1,118 @@ + + + + + diff --git a/components/desktop/extensions/expenses/pages/ExpensesAdminApprovePage.vue b/components/desktop/extensions/expenses/pages/ExpensesAdminApprovePage.vue new file mode 100644 index 00000000000..2848d8f1522 --- /dev/null +++ b/components/desktop/extensions/expenses/pages/ExpensesAdminApprovePage.vue @@ -0,0 +1,206 @@ + + + + + diff --git a/components/desktop/extensions/expenses/pages/ExpensesRegistryPage.vue b/components/desktop/extensions/expenses/pages/ExpensesRegistryPage.vue new file mode 100644 index 00000000000..88b92342e8d --- /dev/null +++ b/components/desktop/extensions/expenses/pages/ExpensesRegistryPage.vue @@ -0,0 +1,287 @@ + + + + + diff --git a/components/desktop/extensions/expenses/pages/MyAdvancesPage.vue b/components/desktop/extensions/expenses/pages/MyAdvancesPage.vue new file mode 100644 index 00000000000..c12d72d29cf --- /dev/null +++ b/components/desktop/extensions/expenses/pages/MyAdvancesPage.vue @@ -0,0 +1,205 @@ + + + + + diff --git a/components/desktop/extensions/expenses/pages/index.ts b/components/desktop/extensions/expenses/pages/index.ts new file mode 100644 index 00000000000..86e4c70bdf5 --- /dev/null +++ b/components/desktop/extensions/expenses/pages/index.ts @@ -0,0 +1,7 @@ +export { default as ExpenseWalletsPage } from './ExpenseWalletsPage.vue'; +export { default as ExpensesRegistryPage } from './ExpensesRegistryPage.vue'; +export { default as ExpenseDetailPage } from './ExpenseDetailPage.vue'; +export { default as ExpensesAdminApprovePage } from './ExpensesAdminApprovePage.vue'; +export { default as CashierPage } from './CashierPage.vue'; +export { default as MyAdvancesPage } from './MyAdvancesPage.vue'; +export { default as ExpenseProposalCreateDialog } from './ExpenseProposalCreateDialog.vue'; diff --git a/components/desktop/extensions/soviet/install.ts b/components/desktop/extensions/soviet/install.ts index 6ba70e0e6db..59ff09fb76e 100644 --- a/components/desktop/extensions/soviet/install.ts +++ b/components/desktop/extensions/soviet/install.ts @@ -7,6 +7,7 @@ import { PaymentsPage } from 'src/pages/Cooperative/Payments'; import { ListOfMeetsPage } from 'src/pages/Cooperative/ListOfMeets'; import { MeetDetailsPage } from 'src/pages/Cooperative/MeetDetails'; import { UnionPageListOfCooperatives } from 'src/pages/Union/ListOfCooperatives'; +import { ExpensesRegistryPage } from 'app/extensions/expenses/pages'; import type { IWorkspaceConfig } from 'src/shared/lib/types/workspace'; export default async function (): Promise { @@ -77,6 +78,21 @@ export default async function (): Promise { roles: ['chairman', 'member'], }, }, + { + // Реестр расходов кооператива: единая таблица всех расходов по всем + // пулам-кошелькам (без фильтра по кошельку — колонка «Кошелёк (пул)» + // показывает источник списания). Совет наблюдает; клик по строке + // открывает деталь расхода (generic `expenses-detail`). Фильтр по + // конкретному пулу — на странице расходов программы (capital). + path: 'expenses', + name: 'soviet-expenses-registry', + component: markRaw(ExpensesRegistryPage), + meta: { + title: 'Реестр расходов', + icon: 'receipt_long', + roles: ['chairman', 'member'], + }, + }, { path: 'meets', name: 'meets', diff --git a/components/desktop/src/features/Payment/AttachExpenseProof/api/index.ts b/components/desktop/src/features/Payment/AttachExpenseProof/api/index.ts new file mode 100644 index 00000000000..dc43e24c1e5 --- /dev/null +++ b/components/desktop/src/features/Payment/AttachExpenseProof/api/index.ts @@ -0,0 +1,60 @@ +import { client } from 'src/shared/api/client'; +import { Mutations, Queries, Zeus } from '@coopenomics/sdk'; + +export type IUploadExpenseFileInput = Mutations.Expense.UploadExpenseFile.IInput['data']; +export type IExpenseFile = + Queries.Expense.ExpenseFilesByItem.IOutput[typeof Queries.Expense.ExpenseFilesByItem.name][number]; + +async function uploadExpenseFile(data: IUploadExpenseFileInput): Promise { + const { [Mutations.Expense.UploadExpenseFile.name]: result } = await client.Mutation( + Mutations.Expense.UploadExpenseFile.mutation, + { variables: { data } }, + ); + return result; +} + +async function loadExpenseFilesByItem( + coopname: string, + proposal_hash: string, + item_hash: string, +): Promise { + const { [Queries.Expense.ExpenseFilesByItem.name]: result } = await client.Query( + Queries.Expense.ExpenseFilesByItem.query, + { variables: { coopname, proposal_hash, item_hash } }, + ); + return result; +} + +// Механика строки расхода (ADVANCE/DIRECT) нужна, чтобы решить, какие документы +// прикладывает кассир: по авансу — только платёжка, по оплате организации — +// ещё и закрывающие документы (акт/счёт-фактура/накладная). +async function loadItemMechanics( + proposal_hash: string, + item_hash: string, +): Promise { + const { [Queries.Expense.ExpenseProposal.name]: proposal } = await client.Query( + Queries.Expense.ExpenseProposal.query, + { variables: { proposal_hash } }, + ); + const item = proposal?.items?.find( + (i) => i.item_hash?.toLowerCase() === item_hash.toLowerCase(), + ); + return item?.mechanics ?? null; +} + +// Списочные запросы файлы отдают без read_url — свежий короткоживущий URL +// запрашивается по id в момент клика. +async function getExpenseFileReadUrl(id: number): Promise { + const { [Queries.Expense.ExpenseFile.name]: result } = await client.Query( + Queries.Expense.ExpenseFile.query, + { variables: { id } }, + ); + return result.read_url ?? undefined; +} + +export const api = { + uploadExpenseFile, + loadExpenseFilesByItem, + loadItemMechanics, + getExpenseFileReadUrl, +}; diff --git a/components/desktop/src/features/Payment/AttachExpenseProof/index.ts b/components/desktop/src/features/Payment/AttachExpenseProof/index.ts new file mode 100644 index 00000000000..399c4336904 --- /dev/null +++ b/components/desktop/src/features/Payment/AttachExpenseProof/index.ts @@ -0,0 +1,2 @@ +export { default as AttachExpenseProofPanel } from './ui/AttachExpenseProofPanel.vue'; +export { api as attachExpenseProofApi, type IExpenseFile } from './api'; diff --git a/components/desktop/src/features/Payment/AttachExpenseProof/ui/AttachExpenseProofPanel.vue b/components/desktop/src/features/Payment/AttachExpenseProof/ui/AttachExpenseProofPanel.vue new file mode 100644 index 00000000000..f607cc3f6be --- /dev/null +++ b/components/desktop/src/features/Payment/AttachExpenseProof/ui/AttachExpenseProofPanel.vue @@ -0,0 +1,260 @@ + + + + + diff --git a/components/desktop/src/features/Payment/ExpenseSettlementBasis/index.ts b/components/desktop/src/features/Payment/ExpenseSettlementBasis/index.ts new file mode 100644 index 00000000000..4edfbea7011 --- /dev/null +++ b/components/desktop/src/features/Payment/ExpenseSettlementBasis/index.ts @@ -0,0 +1 @@ +export { default as ExpenseSettlementBasisPanel } from './ui/ExpenseSettlementBasisPanel.vue'; diff --git a/components/desktop/src/features/Payment/ExpenseSettlementBasis/ui/ExpenseSettlementBasisPanel.vue b/components/desktop/src/features/Payment/ExpenseSettlementBasis/ui/ExpenseSettlementBasisPanel.vue new file mode 100644 index 00000000000..9234d07d7a5 --- /dev/null +++ b/components/desktop/src/features/Payment/ExpenseSettlementBasis/ui/ExpenseSettlementBasisPanel.vue @@ -0,0 +1,254 @@ + + + + + diff --git a/components/desktop/src/features/Payment/ReportExpenseAdvance/api/index.ts b/components/desktop/src/features/Payment/ReportExpenseAdvance/api/index.ts new file mode 100644 index 00000000000..ccbd26745ff --- /dev/null +++ b/components/desktop/src/features/Payment/ReportExpenseAdvance/api/index.ts @@ -0,0 +1,31 @@ +import { client } from 'src/shared/api/client'; +import { Mutations, Queries } from '@coopenomics/sdk'; + +export type IExpenseProposal = + Queries.Expense.ExpenseProposal.IOutput[typeof Queries.Expense.ExpenseProposal.name]; +export type IExpenseProposalItem = NonNullable['items'][number]; +export type IReportExpenseItemInput = Mutations.Expense.ReportExpenseItem.IInput['data']; +export type IReportExpenseItemResult = + Mutations.Expense.ReportExpenseItem.IOutput[typeof Mutations.Expense.ReportExpenseItem.name]; + +async function loadExpenseProposal( + proposalHash: string, +): Promise { + const { [Queries.Expense.ExpenseProposal.name]: result } = await client.Query( + Queries.Expense.ExpenseProposal.query, + { variables: { proposal_hash: proposalHash } }, + ); + return result ?? null; +} + +async function reportExpenseItem( + data: IReportExpenseItemInput, +): Promise { + const { [Mutations.Expense.ReportExpenseItem.name]: result } = await client.Mutation( + Mutations.Expense.ReportExpenseItem.mutation, + { variables: { data } }, + ); + return result; +} + +export const api = { loadExpenseProposal, reportExpenseItem }; diff --git a/components/desktop/src/features/Payment/ReportExpenseAdvance/index.ts b/components/desktop/src/features/Payment/ReportExpenseAdvance/index.ts new file mode 100644 index 00000000000..3db6094112d --- /dev/null +++ b/components/desktop/src/features/Payment/ReportExpenseAdvance/index.ts @@ -0,0 +1,2 @@ +export { default as ReportExpenseAdvancePanel } from './ui/ReportExpenseAdvancePanel.vue'; +export { api as reportExpenseAdvanceApi, type IExpenseProposalItem } from './api'; diff --git a/components/desktop/src/features/Payment/ReportExpenseAdvance/ui/ReportExpenseAdvancePanel.vue b/components/desktop/src/features/Payment/ReportExpenseAdvance/ui/ReportExpenseAdvancePanel.vue new file mode 100644 index 00000000000..aea66057660 --- /dev/null +++ b/components/desktop/src/features/Payment/ReportExpenseAdvance/ui/ReportExpenseAdvancePanel.vue @@ -0,0 +1,391 @@ + + + + + diff --git a/components/desktop/src/pages/Cooperative/Payments/ui/PaymentsPage.vue b/components/desktop/src/pages/Cooperative/Payments/ui/PaymentsPage.vue index 63348f08373..d67cc2918e6 100644 --- a/components/desktop/src/pages/Cooperative/Payments/ui/PaymentsPage.vue +++ b/components/desktop/src/pages/Cooperative/Payments/ui/PaymentsPage.vue @@ -1,10 +1,17 @@ diff --git a/components/desktop/src/shared/ui/domain/ExpenseCreateDialog/README.md b/components/desktop/src/shared/ui/domain/ExpenseCreateDialog/README.md new file mode 100644 index 00000000000..061a79bd3a9 --- /dev/null +++ b/components/desktop/src/shared/ui/domain/ExpenseCreateDialog/README.md @@ -0,0 +1,55 @@ +# UI-виджеты шасси расходов + +Общие компоненты для любого стола, подключающего шасси расходов (контракт +`expense`): Благорост — первый потребитель, стол кооперативного участка — +следующий. Полный рецепт подключения нового пула (контракт + backend + стол +совета) — `extensions/expenses/README.md`. + +## Состав + +| Что | Где | Зачем | +|---|---|---| +| `ExpenseCreateDialog` | `shared/ui/domain/ExpenseCreateDialog` | Создание СЗ: форма с позициями, черновик в localStorage, валидация, генерация и подпись заявления (document2 type=2010) | +| `ExpenseProposalList` | `shared/ui/domain/ExpenseProposalList` | Список СЗ пула карточками (статус, сумма, №, инициатор) | +| Подписи статусов | `src/shared/lib/expenses` | `proposalStatusLabel/Variant`, `itemStatusLabel/Variant`, `mechanicsLabel`, `fileKindLabel`, `shortExpenseId` | +| Реестр пулов | `src/shared/lib/expense-wallets` | `registerExpenseWallet(...)` из install.ts расширения → страница «Расходы» стола совета | + +## ExpenseCreateDialog + +Виджет владеет всем процессом до чейна; на чейн подаёт потребитель через проп +`submit` — у каждого пула своя мутация (capital → `createProgramExpense`, +у КУ будет своя). + +```pug +ExpenseCreateDialog( + v-model='createOpen', + title='Создание расхода КУ', + source-wallet='w.mkt.kuexp', //- ledger2-пул, фиксируется в тексте СЗ + draft-key='mp:market:create-expense:draft', //- свой ключ черновика + :submit='submitToChain', //- (payload: ExpenseCreatePayload) => Promise + @created='refresh' +) +``` + +`ExpenseCreatePayload` (см. `ExpenseCreateDialog.types.ts`): `expense_hash`, +`description`, `deadline` (`YYYY-MM-DD`), `items` (готовые для +`expense::createexp`, с `payment_method_id`/`requisites`), `statement` +(подписанный документ). Эталон обвязки — тонкая обёртка capital: +`extensions/capital/features/ProgramExpense/CreateProgramExpense/`. + +## ExpenseProposalList + +Презентационный: страница сама грузит СЗ своим запросом и отдаёт строки. + +```pug +ExpenseProposalList( + :rows='listRows', //- ExpenseProposalListRow[]: expense_hash, title, status, total_planned, creator_name?, created_at? + :loading='loading', + @open='openExpense' //- проваливание в детальную страницу расхода +) +``` + +Эталон сборки страницы (виджеты + WalletCard пулов + кнопка пополнения) — +`extensions/capital/pages/ProgramExpensesPage`. Эталон детальной страницы +(позиции, реквизиты, файлы, история состояний, кнопка «Закрыть расход») — +`extensions/capital/pages/ProgramExpensePage`. diff --git a/components/desktop/src/shared/ui/domain/ExpenseCreateDialog/index.ts b/components/desktop/src/shared/ui/domain/ExpenseCreateDialog/index.ts new file mode 100644 index 00000000000..0c3a40db835 --- /dev/null +++ b/components/desktop/src/shared/ui/domain/ExpenseCreateDialog/index.ts @@ -0,0 +1,7 @@ +export { default as ExpenseCreateDialog } from './ExpenseCreateDialog.vue'; +export type { + ExpenseCreateDialogProps, + ExpenseCreateDraftItem, + ExpenseCreateChainItem, + ExpenseCreatePayload, +} from './ExpenseCreateDialog.types'; diff --git a/components/desktop/src/shared/ui/domain/ExpenseProposalDocuments/ExpenseProposalDocuments.types.ts b/components/desktop/src/shared/ui/domain/ExpenseProposalDocuments/ExpenseProposalDocuments.types.ts new file mode 100644 index 00000000000..688979b1b5b --- /dev/null +++ b/components/desktop/src/shared/ui/domain/ExpenseProposalDocuments/ExpenseProposalDocuments.types.ts @@ -0,0 +1,8 @@ +import type { IDocumentAggregate } from 'src/entities/Document/model'; + +export interface ExpenseProposalDocumentsProps { + /** Заявление пайщика о расходе (служебная записка, 2010). */ + statement?: IDocumentAggregate | null; + /** Решение совета о расходе (протокол, 2011). */ + decision?: IDocumentAggregate | null; +} diff --git a/components/desktop/src/shared/ui/domain/ExpenseProposalDocuments/ExpenseProposalDocuments.vue b/components/desktop/src/shared/ui/domain/ExpenseProposalDocuments/ExpenseProposalDocuments.vue new file mode 100644 index 00000000000..38d6d125699 --- /dev/null +++ b/components/desktop/src/shared/ui/domain/ExpenseProposalDocuments/ExpenseProposalDocuments.vue @@ -0,0 +1,101 @@ + + + + + diff --git a/components/desktop/src/shared/ui/domain/ExpenseProposalDocuments/index.ts b/components/desktop/src/shared/ui/domain/ExpenseProposalDocuments/index.ts new file mode 100644 index 00000000000..b9b0f14ba8b --- /dev/null +++ b/components/desktop/src/shared/ui/domain/ExpenseProposalDocuments/index.ts @@ -0,0 +1,2 @@ +export { default as ExpenseProposalDocuments } from './ExpenseProposalDocuments.vue'; +export type { ExpenseProposalDocumentsProps } from './ExpenseProposalDocuments.types'; diff --git a/components/desktop/src/shared/ui/domain/ExpenseProposalList/ExpenseProposalList.types.ts b/components/desktop/src/shared/ui/domain/ExpenseProposalList/ExpenseProposalList.types.ts new file mode 100644 index 00000000000..59c28eb97e5 --- /dev/null +++ b/components/desktop/src/shared/ui/domain/ExpenseProposalList/ExpenseProposalList.types.ts @@ -0,0 +1,22 @@ +import type { Zeus } from '@coopenomics/sdk'; + +/** Строка списка СЗ-расходов — минимум, который отдаёт любой пул шасси. */ +export interface ExpenseProposalListRow { + expense_hash: string; + /** Заголовок карточки (обычно описание первой позиции или цель расхода). */ + title: string; + status: Zeus.ExpenseProposalStatus; + /** Отформатированная плановая сумма (asset-строка). */ + total_planned: string; + /** ФИО инициатора (или аккаунт, если имя не отрезолвлено). */ + creator_name?: string; + created_at?: string; +} + +export interface ExpenseProposalListProps { + rows: ExpenseProposalListRow[]; + /** Скелетон на время первой загрузки (когда rows ещё пуст). */ + loading?: boolean; + emptyTitle?: string; + emptyBody?: string; +} diff --git a/components/desktop/src/shared/ui/domain/ExpenseProposalList/ExpenseProposalList.vue b/components/desktop/src/shared/ui/domain/ExpenseProposalList/ExpenseProposalList.vue new file mode 100644 index 00000000000..062992c38c7 --- /dev/null +++ b/components/desktop/src/shared/ui/domain/ExpenseProposalList/ExpenseProposalList.vue @@ -0,0 +1,125 @@ + + + + + diff --git a/components/desktop/src/shared/ui/domain/ExpenseProposalList/index.ts b/components/desktop/src/shared/ui/domain/ExpenseProposalList/index.ts new file mode 100644 index 00000000000..8a1fdfc70fc --- /dev/null +++ b/components/desktop/src/shared/ui/domain/ExpenseProposalList/index.ts @@ -0,0 +1,5 @@ +export { default as ExpenseProposalList } from './ExpenseProposalList.vue'; +export type { + ExpenseProposalListProps, + ExpenseProposalListRow, +} from './ExpenseProposalList.types'; diff --git a/components/desktop/src/shared/ui/domain/PaymentMethodSelect/PaymentMethodSelect.vue b/components/desktop/src/shared/ui/domain/PaymentMethodSelect/PaymentMethodSelect.vue new file mode 100644 index 00000000000..e090029b74e --- /dev/null +++ b/components/desktop/src/shared/ui/domain/PaymentMethodSelect/PaymentMethodSelect.vue @@ -0,0 +1,82 @@ + + + diff --git a/components/desktop/src/shared/ui/domain/PaymentMethodSelect/formatPaymentMethod.ts b/components/desktop/src/shared/ui/domain/PaymentMethodSelect/formatPaymentMethod.ts new file mode 100644 index 00000000000..493b286385f --- /dev/null +++ b/components/desktop/src/shared/ui/domain/PaymentMethodSelect/formatPaymentMethod.ts @@ -0,0 +1,22 @@ +/** + * Сокращённое отображение реквизитов платёжного метода для UI: телефон СБП — + * со звёздочками, банковский счёт — банк + последние 4 цифры. Полные реквизиты + * в интерфейсе не показываются — они подставляются сервером в документы. + */ +export interface IPaymentMethodLike { + method_id: string | number; + method_type: string; + data: Record; +} + +export function formatPaymentMethodShort(method: IPaymentMethodLike): string { + if (method.method_type === 'sbp') { + const phone = String(method.data?.phone ?? ''); + const masked = phone.length > 4 ? `${phone.slice(0, 2)}***${phone.slice(-2)}` : phone; + return `СБП (${masked})`; + } + const account = String(method.data?.account_number ?? ''); + const bank = String(method.data?.bank_name ?? ''); + const last4 = account.slice(-4); + return bank ? `${bank} (***${last4})` : `Банковский счёт ***${last4}`; +} diff --git a/components/desktop/src/shared/ui/domain/PaymentMethodSelect/index.ts b/components/desktop/src/shared/ui/domain/PaymentMethodSelect/index.ts new file mode 100644 index 00000000000..cf9d4e86612 --- /dev/null +++ b/components/desktop/src/shared/ui/domain/PaymentMethodSelect/index.ts @@ -0,0 +1,2 @@ +export { default as PaymentMethodSelect } from './PaymentMethodSelect.vue'; +export { formatPaymentMethodShort, type IPaymentMethodLike } from './formatPaymentMethod'; diff --git a/components/desktop/src/shared/ui/domain/WalletCard/WalletCard.vue b/components/desktop/src/shared/ui/domain/WalletCard/WalletCard.vue index edf77a831e5..2c956acddac 100644 --- a/components/desktop/src/shared/ui/domain/WalletCard/WalletCard.vue +++ b/components/desktop/src/shared/ui/domain/WalletCard/WalletCard.vue @@ -8,23 +8,8 @@
-
- {{ resolvedTitle }} -
-
- {{ subtitle }} -
+
{{ resolvedTitle }}
+
{{ subtitle }}
@@ -47,7 +32,7 @@ diff --git a/components/desktop/src/shared/ui/domain/index.ts b/components/desktop/src/shared/ui/domain/index.ts index c38b4cc0e47..2477805d460 100644 --- a/components/desktop/src/shared/ui/domain/index.ts +++ b/components/desktop/src/shared/ui/domain/index.ts @@ -9,11 +9,15 @@ export * from './DetailsDrawer'; export * from './DocumentPreview'; export * from './DocumentRow'; export * from './DocumentSignatures'; +export * from './ExpenseCreateDialog'; +export * from './ExpenseProposalDocuments'; +export * from './ExpenseProposalList'; export * from './FileUploader'; export * from './FilterBar'; export * from './IdentityPanel'; export * from './NotificationCenter'; export * from './OtpInput'; +export * from './PaymentMethodSelect'; export * from './PersonCard'; export * from './RailUserCard'; export * from './SignatureCard'; diff --git a/components/desktop/src/widgets/Cooperative/Payments/ListOfPayments/ui/ListOfPaymentsWidget.vue b/components/desktop/src/widgets/Cooperative/Payments/ListOfPayments/ui/ListOfPaymentsWidget.vue index 82efc4634bf..ee8f02105a4 100644 --- a/components/desktop/src/widgets/Cooperative/Payments/ListOfPayments/ui/ListOfPaymentsWidget.vue +++ b/components/desktop/src/widgets/Cooperative/Payments/ListOfPayments/ui/ListOfPaymentsWidget.vue @@ -4,7 +4,7 @@ v-if='onLoading && !items.length', :columns='skeletonColumns', :rows='6', - :min-width='hideActions ? "880px" : "1000px"' + :min-width='hideActions ? "940px" : "1000px"' ) template(v-else-if='items.length') //- Десктоп: канон-таблица. На мобиле скрыта — таблица из 7–8 колонок @@ -20,12 +20,15 @@ th.col-sort.col-date(@click='onSort("created_at")') Дата создания {{ sortMark('created_at') }} th.col-sort.col-num(@click='onSort("quantity")') Сумма {{ sortMark('quantity') }} th Тип платежа + //- «Направление» — относительно того, чей это стол: на столе совета + //- относительно кооператива (в/из кооператива), на личном столе + //- пайщика относительно пайщика (поступление/списание) — см. displayDirection. th Направление th.col-sort(@click='onSort("status")') Статус {{ sortMark('status') }} th.col-action(v-if='!hideActions') Действия tbody template(v-for='row in items', :key='row.id') - tr.data-row(@click='toggleExpand(row.id)') + tr.data-row(:id='`pay-row-${row.id}`', @click='toggleExpand(row.id)') td.col-toggle button.icon-btn( type='button', @@ -35,14 +38,25 @@ q-icon(:name='expanded.get(row.id) ? "expand_more" : "chevron_right"') td.cell-name {{ getShortNameFromCertificate(row.username_certificate) || row.username }} td {{ formatDateToHumanDateTime(row.created_at) }} - td.col-num {{ row.quantity }} {{ row.symbol }} + td.col-num {{ amountLabel(row) }} td {{ row.type_label }} td - span.dir(:class='isIncoming(row.direction) ? "dir--in" : "dir--out"') - q-icon.q-mr-xs(:name='getDirectionIcon(row.direction)', size='16px') - span {{ row.direction_label }} + span.dir(:class='isIncoming(displayDirection(row)) ? "dir--in" : "dir--out"') + q-icon.q-mr-xs(:name='getDirectionIcon(displayDirection(row))', size='16px') + span {{ directionLabel(row) }} + q-tooltip {{ directionHint(row) }} td - BaseBadge(:variant='getStatusVariant(row.status)') {{ row.status_label }} + .cell-status + BaseBadge(:variant='getStatusVariant(row.status)') {{ row.status_label }} + //- Статус отчёта по авансу (личный стол пайщика) — отдельная ось + //- рядом со статусом платежа: требуется / подан / принят. + BaseBadge(v-if='reportBadge(row)', :variant='reportBadge(row)?.variant') {{ reportBadge(row)?.label }} + //- Кассирская отметка «платёжка приложена» — только на столе совета + //- (пайщику чужая бухгалтерия не показывается). + q-icon.proof-icon.proof-icon--ok(v-if='!hideActions && proofState(row) === "attached"', name='receipt_long', size='16px') + q-tooltip Платёжка приложена + q-icon.proof-icon.proof-icon--missing(v-else-if='!hideActions && proofState(row) === "missing"', name='receipt_long', size='16px') + q-tooltip Платёжка не приложена td.col-action(v-if='!hideActions', @click.stop) .cell-actions(v-if='["EXPIRED", "PENDING", "FAILED"].includes(row.status)') SetOrderPaidStatusButton(:id='row.id') @@ -54,6 +68,45 @@ tr.expand-row(v-if='expanded.get(row.id)') td(:colspan='hideActions ? 7 : 8') PaymentDetails(:payment='row') + //- Стол совета: кассир прикладывает платёжку и закрывающие документы; + //- при личной передаче чеков — отчитывается за пайщика (панель + //- отчёта сама скрывается для прямой оплаты организации, DIRECT). + .expense-flow.q-mt-sm(v-if='!hideActions && expenseProofRef(row)') + AttachExpenseProofPanel( + :proposal-hash='expenseProofRef(row)?.proposal_hash ?? ""', + :item-hash='expenseProofRef(row)?.item_hash ?? ""', + :step='{ number: 1, title: "Подтвердите оплату" }', + @uploaded='onProofUploaded(row)' + ) + ReportExpenseAdvancePanel( + :proposal-hash='expenseProofRef(row)?.proposal_hash ?? ""', + :item-hash='expenseProofRef(row)?.item_hash ?? ""', + :on-behalf='true', + :report-state='advanceReportState(row)', + :reported-amount='advanceReportedAmount(row)', + :step='{ number: 2, title: "Отчёт пайщика" }', + @reported='onReported' + ) + //- Личный стол: пайщик-получатель отчитывается чеком по своей строке. + ReportExpenseAdvancePanel.q-mt-sm( + v-else-if='advanceReportRef(row)', + :proposal-hash='advanceReportRef(row)?.proposal_hash ?? ""', + :item-hash='advanceReportRef(row)?.item_hash ?? ""', + :report-state='advanceReportState(row)', + :reported-amount='advanceReportedAmount(row)', + @reported='onReported' + ) + //- Расчётная платёжка (возврат/доплата): кассиру/пайщику видно + //- основание — исходный аванс, заявленный факт и документы. + ExpenseSettlementBasisPanel.q-mt-sm( + v-if='settlementRef(row)', + :proposal-hash='settlementRef(row)?.proposal_hash ?? ""', + :item-hash='settlementRef(row)?.item_hash ?? ""', + :settlement-amount='`${row.quantity} ${row.symbol}`', + :is-return='settlementRef(row)?.isReturn ?? false', + :description='settlementRef(row)?.description ?? ""', + @open-source='openSourcePayment' + ) .table-foot span {{ rangeLabel }} @@ -67,22 +120,28 @@ //- Мобайл: карточки вместо таблицы. Видны только на узких экранах. .payments-cards.pmt-mobile - .pay-card(v-for='row in items', :key='row.id') + .pay-card(v-for='row in items', :key='row.id', :id='`pay-card-${row.id}`') .pay-card__main(@click='toggleExpand(row.id)') .pay-card__row span.pay-card__name {{ getShortNameFromCertificate(row.username_certificate) || row.username }} - BaseBadge(:variant='getStatusVariant(row.status)') {{ row.status_label }} + .cell-status + BaseBadge(:variant='getStatusVariant(row.status)') {{ row.status_label }} + BaseBadge(v-if='reportBadge(row)', :variant='reportBadge(row)?.variant') {{ reportBadge(row)?.label }} + q-icon.proof-icon.proof-icon--ok(v-if='!hideActions && proofState(row) === "attached"', name='receipt_long', size='16px') + q-tooltip Платёжка приложена + q-icon.proof-icon.proof-icon--missing(v-else-if='!hideActions && proofState(row) === "missing"', name='receipt_long', size='16px') + q-tooltip Платёжка не приложена .pay-card__row span.pay-card__amount q-icon.q-mr-xs( - :name='getDirectionIcon(row.direction)', - :class='isIncoming(row.direction) ? "dir--in" : "dir--out"', + :name='getDirectionIcon(displayDirection(row))', + :class='isIncoming(displayDirection(row)) ? "dir--in" : "dir--out"', size='16px' ) - | {{ row.quantity }} {{ row.symbol }} + | {{ amountLabel(row) }} span.pay-card__type {{ row.type_label }} .pay-card__row.pay-card__row--meta - span {{ row.direction_label }} + span {{ directionLabel(row) }} span {{ formatDateToHumanDateTime(row.created_at) }} .pay-card__actions( v-if='!hideActions && ["EXPIRED", "PENDING", "FAILED"].includes(row.status)', @@ -90,7 +149,41 @@ ) SetOrderPaidStatusButton(:id='row.id') SetOrderRefundedStatusButton(v-if='!isRefundType(row.type)', :id='row.id') - PaymentDetails.pay-card__details(v-if='expanded.get(row.id)', :payment='row') + template(v-if='expanded.get(row.id)') + PaymentDetails.pay-card__details(:payment='row') + .expense-flow.q-mt-sm(v-if='!hideActions && expenseProofRef(row)') + AttachExpenseProofPanel( + :proposal-hash='expenseProofRef(row)?.proposal_hash ?? ""', + :item-hash='expenseProofRef(row)?.item_hash ?? ""', + :step='{ number: 1, title: "Подтвердите оплату" }', + @uploaded='onProofUploaded(row)' + ) + ReportExpenseAdvancePanel( + :proposal-hash='expenseProofRef(row)?.proposal_hash ?? ""', + :item-hash='expenseProofRef(row)?.item_hash ?? ""', + :on-behalf='true', + :report-state='advanceReportState(row)', + :reported-amount='advanceReportedAmount(row)', + :step='{ number: 2, title: "Отчёт пайщика" }', + @reported='onReported' + ) + ReportExpenseAdvancePanel.q-mt-sm( + v-else-if='advanceReportRef(row)', + :proposal-hash='advanceReportRef(row)?.proposal_hash ?? ""', + :item-hash='advanceReportRef(row)?.item_hash ?? ""', + :report-state='advanceReportState(row)', + :reported-amount='advanceReportedAmount(row)', + @reported='onReported' + ) + ExpenseSettlementBasisPanel.q-mt-sm( + v-if='settlementRef(row)', + :proposal-hash='settlementRef(row)?.proposal_hash ?? ""', + :item-hash='settlementRef(row)?.item_hash ?? ""', + :settlement-amount='`${row.quantity} ${row.symbol}`', + :is-return='settlementRef(row)?.isReturn ?? false', + :description='settlementRef(row)?.description ?? ""', + @open-source='openSourcePayment' + ) .table-foot span {{ rangeLabel }} @@ -112,11 +205,16 @@ @@ -420,6 +681,19 @@ onMounted(() => { display: inline-flex; align-items: center; } + +.cell-status { + display: inline-flex; + align-items: center; + gap: var(--p-2, 8px); +} + +.proof-icon--ok { + color: var(--p-pos); +} +.proof-icon--missing { + color: var(--p-warn); +} .dir--in { color: var(--p-pos); } @@ -439,4 +713,17 @@ onMounted(() => { padding: 0 20px 16px; background: var(--p-surface-2); } + +/* Последовательные этапы кассира (подтверждение оплаты → отчёт пайщика): + разделяем хайрлайном, чтобы видно было «сейчас этот этап, потом следующий». */ +.expense-flow { + display: flex; + flex-direction: column; + gap: var(--p-4); +} + +.expense-flow > * + * { + padding-top: var(--p-4); + border-top: 1px solid var(--p-line); +} diff --git a/components/factory/AGENTS.md b/components/factory/AGENTS.md index 508283eda9b..d15ee378b86 100644 --- a/components/factory/AGENTS.md +++ b/components/factory/AGENTS.md @@ -47,6 +47,20 @@ src/ `PDFService` в `Services/Generator/` записывает HTML во временный файл, вызывает `weasyprint` через `child_process.exec`, читает результат. Требует установленного WeasyPrint в системе. +### Приватные данные документа: `doc_data` + +Зарезервированный механизм для документов, содержащих персональные данные, которые не должны попадать в блокчейн. + +- `Services/DocData/index.ts` — `DocDataService` с `save(payload, registry_id) → { hash }` (sha256, идемпотентный upsert) и `get(hash) → payload | null`. +- Коллекция Mongo `doc_private_data` со схемой `{ hash, registry_id, payload, _created_at }`; уникальный индекс по `hash`. +- В Action документа — зарезервированное поле `doc_data_hash: string` (см. `Cooperative.Document.IDocDataRef` в cooptypes). +- `DocFactory.loadDocData(data)` — хелпер для конкретной фабрики: подгружает payload, фабрика кладёт в `combinedData.doc_data`. +- Шаблон обращается как `{{ doc_data. }}` — `doc_data` это зарезервированная переменная шаблона наравне с `meta`, `coop`, `vars`, `user`, `request`, `program`, `decision`, `branch`. +- Generator-фасад: `generator.saveDocData(payload, registry_id)` / `generator.getDocData(hash)`. +- Graceful degradation: если payload удалён из коллекции, документ остаётся верифицируемым по хэшу подписи (on-chain), но не регенерируется. + +Применяется в новых документах с персональными данными (1103.MarketplaceTransportNote — данные экспедитора). Старые документы не переводим. + ### Mock-система - `src/Utils/testMocks.ts` — экспортирует `testMocks` (массив моков из `mocks/tables/` и `mocks/actions/`) diff --git a/components/factory/README.md b/components/factory/README.md index a0bebd38fa5..64e7210e400 100644 --- a/components/factory/README.md +++ b/components/factory/README.md @@ -113,6 +113,74 @@ test/ # Тесты (Vitest) 6. Подпись хеша документа EOSIO-ключом 7. Генерация PDF через WeasyPrint +## Приватные данные документа: `doc_data` + +Зарезервированный механизм для документов, содержащих персональные данные (ФИО экспедитора, паспорт, госномер, телефон и т.п.), которые **не должны попадать в блокчейн**. + +### Поток + +1. Контроллер собирает приватный payload и вызывает `generator.saveDocData(payload, registry_id)` — возвращается `{ hash }` (sha256, идемпотентный). +2. `hash` передаётся on-chain в Action под зарезервированным именем `doc_data_hash`. +3. При генерации/регенерации фабрика подгружает payload из `doc_private_data` через `generator.getDocData(hash)` и подставляет в `combinedData.doc_data`. +4. Шаблон документа использует приватные поля как `{{ doc_data. }}`. + +### Пример + +Контроллер: + +```ts +const { hash } = await generator.saveDocData({ + expeditor_full_name: 'Иванов Иван Иванович', + passport: '4509 123456', + phone: '+79991234567', + vehicle_registration_number: 'А123ВВ77', +}, 1103) + +// в Action документа 1103 (MarketplaceTransportNote): +await generator.generate({ + registry_id: 1103, + coopname: 'voskhod', + username: operatorAccount, + shipment_id, + doc_data_hash: hash, +}) +``` + +Фабрика (`src/Actions/1103.MarketplaceTransportNote.ts`): + +```ts +const doc_data = await this.loadDocData(data) +const combinedData = { ...base, doc_data } +``` + +Шаблон (`Templates/1103.MarketplaceTransportNote/index.ts`): + +```html +Экспедитор{{ doc_data.expeditor_full_name }} +Госномер{{ doc_data.vehicle_registration_number }} +``` + +### Зарезервированные имена шаблона + +| Имя | Источник | Применение | +|---|---|---| +| `meta` | factory | заголовки, дата, lang, hash | +| `coop` | factory | реквизиты кооператива | +| `vars` | factory | переменные кооператива | +| `user` | factory | подписант (по сертификату) | +| `request` / `program` / `decision` | factory | контекст ЦПП | +| `branch` | factory | данные КУ | +| **`doc_data`** | **DocDataService** | **приватные данные документа** | + +### Graceful degradation + +Если запись в `doc_private_data` удалена (например, по запросу пайщика), документ остаётся **верифицируемым по хэшу подписи** (хэш зафиксирован on-chain), но не регенерируется. Это явное архитектурное решение: ценность блокчейн-подписи сохраняется; восстановление PDF — обратимо deprecated. + +### Когда применять + +- **Новые документы** с персональными данными — сразу через `doc_data`. +- **Существующие документы** (700-1099, 300-304 и т.д.) — остаются без изменений. Паттерн прогрессивный, переводить старые документы при отсутствии необходимости не нужно. + ## Ключевые зависимости - **nunjucks / handlebars** — шаблонизаторы diff --git a/components/factory/src/Actions/2010.ExpenseProposalStatement.ts b/components/factory/src/Actions/2010.ExpenseProposalStatement.ts new file mode 100644 index 00000000000..23c9116480d --- /dev/null +++ b/components/factory/src/Actions/2010.ExpenseProposalStatement.ts @@ -0,0 +1,65 @@ +import { DraftContract } from 'cooptypes' +import { ExpenseProposalStatement } from '../Templates' +import { DocFactory } from '../Factory' +import type { IGeneratedDocument, IGenerationOptions, IMetaDocument, ITemplate } from '../Interfaces' +import type { MongoDBConnector } from '../Services/Databazor' + +export { ExpenseProposalStatement as Template } from '../Templates' + +export class Factory extends DocFactory { + constructor(storage: MongoDBConnector) { + super(storage) + } + + async generateDocument(data: ExpenseProposalStatement.Action, options?: IGenerationOptions): Promise { + // Независимые источники тянем параллельно (см. resolveParallel в DocFactory). + // Приватная часть позиций (реквизиты/имя/назначение) — из `doc_private_data` + // по `doc_data_hash`; в meta документа эти данные НЕ попадают. См. раздел + // «Document Generation Pattern: doc_data» в архитектуре. + const { template, coop, vars, userData, priv } = await this.resolveParallel({ + template: () => process.env.SOURCE === 'local' + ? Promise.resolve(ExpenseProposalStatement.Template as ITemplate) + : this.getTemplate(DraftContract.contractName.production, ExpenseProposalStatement.registry_id, data.block_num), + coop: () => super.getCooperative(data.coopname, data.block_num), + vars: () => super.getVars(data.coopname, data.block_num), + userData: () => super.getUser(data.username, data.block_num), + priv: () => this.loadDocData(data), + }) + + // meta зависит от template.title — считаем после батча. meta строится из + // публичной `data` (без приватных позиций) → реквизиты не уходят on-chain. + const meta: IMetaDocument = await this.getMeta({ title: template.title, ...data }) + + // Подмешиваем приватную часть в позиции по `number` — только для рендера. + const privByNumber = new Map( + (priv?.items ?? []).map((p): [string, ExpenseProposalStatement.PrivateData['items'][number]] => [p.number, p]), + ) + + const combinedData: ExpenseProposalStatement.Model = { + meta, + coop, + user: super.getCommonUser(userData), + vars, + proposal_hash: data.proposal_hash, + proposal_short_hash: this.getShortHash(data.proposal_hash), + proposal: { ...data.proposal, total_amount: this.formatAsset(data.proposal.total_amount) }, + items: data.items.map((item) => { + const p = privByNumber.get(item.number) + return { + ...item, + amount: this.formatAsset(item.amount), + recipient_name: p?.recipient_name, + requisites: p?.requisites, + payment_purpose: p?.payment_purpose, + } + }), + } + + await this.validate(combinedData, template.model) + + const translation = template.translations[meta.lang] + const document: IGeneratedDocument = await this.generatePDF('', template.context, combinedData, translation, meta, options?.skip_save) + + return document + } +} diff --git a/components/factory/src/Actions/2011.ExpenseProposalDecision.ts b/components/factory/src/Actions/2011.ExpenseProposalDecision.ts new file mode 100644 index 00000000000..15d98d1ed5e --- /dev/null +++ b/components/factory/src/Actions/2011.ExpenseProposalDecision.ts @@ -0,0 +1,57 @@ +import { type Cooperative, DraftContract } from 'cooptypes' +import { ExpenseProposalDecision } from '../Templates' +import { DocFactory } from '../Factory' +import type { IGeneratedDocument, IGenerationOptions, IMetaDocument, ITemplate } from '../Interfaces' +import type { MongoDBConnector } from '../Services/Databazor' + +export { ExpenseProposalDecision as Template } from '../Templates' + +export class Factory extends DocFactory { + constructor(storage: MongoDBConnector) { + super(storage) + } + + async generateDocument(data: ExpenseProposalDecision.Action, options?: IGenerationOptions): Promise { + if (!data.decision_id) + throw new Error('Идентификатор решения совета не установлен') + + // Независимые источники тянем параллельно (см. resolveParallel в DocFactory) + const { template, coop, vars } = await this.resolveParallel({ + template: () => process.env.SOURCE === 'local' + ? Promise.resolve(ExpenseProposalDecision.Template as ITemplate) + : this.getTemplate(DraftContract.contractName.production, ExpenseProposalDecision.registry_id, data.block_num), + coop: () => super.getCooperative(data.coopname, data.block_num), + vars: () => super.getVars(data.coopname, data.block_num), + }) + + // meta зависит от template.title — считаем после батча + const meta: IMetaDocument = await this.getMeta({ title: template.title, ...data }) + + // данные собрания совета (кворум, голоса) — канон протоколов решений (600) + const decision: Cooperative.Document.IDecisionData = await this.getDecision( + coop, + data.coopname, + data.decision_id, + meta.created_at, + ) + + const combinedData: ExpenseProposalDecision.Model = { + meta, + coop, + vars, + decision, + proposal_hash: data.proposal_hash, + proposal_short_hash: this.getShortHash(data.proposal_hash), + proposal: { ...data.proposal, total_amount: this.formatAsset(data.proposal.total_amount) }, + items: data.items.map(item => ({ ...item, amount: this.formatAsset(item.amount) })), + resolution: data.resolution, + } + + await this.validate(combinedData, template.model) + + const translation = template.translations[meta.lang] + const document: IGeneratedDocument = await this.generatePDF('', template.context, combinedData, translation, meta, options?.skip_save) + + return document + } +} diff --git a/components/factory/src/Actions/index.ts b/components/factory/src/Actions/index.ts index ebec6f31235..b6b52ea02ce 100644 --- a/components/factory/src/Actions/index.ts +++ b/components/factory/src/Actions/index.ts @@ -34,6 +34,10 @@ export * as InitProjectDecision from './1006.InitProjectDecision' export * as ExpenseStatement from './1010.ExpenseStatement' export * as ExpenseDecision from './1011.ExpenseDecision' +// Шасси расходов (волна 6, MVP-SINGLE) — C28-30 +export * as ExpenseProposalStatement from './2010.ExpenseProposalStatement' +export * as ExpenseProposalDecision from './2011.ExpenseProposalDecision' + export * as GenerationMoneyInvestStatement from './1020.GenerationMoneyInvestStatement' export * as GenerationMoneyReturnUnusedStatement from './1025.GenerationMoneyReturnUnusedStatement' diff --git a/components/factory/src/Factory/index.ts b/components/factory/src/Factory/index.ts index 05472d7ab94..0e8e0011efd 100644 --- a/components/factory/src/Factory/index.ts +++ b/components/factory/src/Factory/index.ts @@ -7,6 +7,7 @@ import type { MongoDBConnector } from '../Services/Databazor' import { type ExternalEntrepreneurData, type ExternalIndividualData, type ExternalOrganizationData, type IVars, Individual, type InternalProjectData, Organization, PaymentMethod, Project, Vars } from '../Models' import type { IGenerate, IGenerationOptions } from '../Interfaces/Documents' import { PDFService } from '../Services/Generator' +import { DocDataService } from '../Services/DocData' import packageJson from '../../package.json' import { Validator } from '../Services/Validator' import type { CooperativeData } from '../Models/Cooperative' @@ -26,9 +27,11 @@ export abstract class DocFactory { abstract generateDocument(data: T, options?: IGenerationOptions): Promise public storage: MongoDBConnector + public docData: DocDataService constructor(storage: MongoDBConnector) { this.storage = storage + this.docData = new DocDataService(storage) } /** @@ -65,6 +68,37 @@ export abstract class DocFactory { return new Validator(schema, combinedData).validate() } + /** + * Сохранить приватный payload документа и получить его hash. + * Hash безопасно публикуется on-chain как `doc_data_hash`; сам payload + * остаётся off-chain в коллекции `doc_private_data`. + * См. раздел «Document Generation Pattern: doc_data» в архитектуре. + */ + async saveDocData

>(payload: P, registry_id: number): Promise<{ hash: string }> { + return this.docData.save(payload, registry_id) + } + + /** + * Прочитать приватный payload по `doc_data_hash`. + * Возвращает null если запись удалена (документ остаётся верифицируемым + * по хэшу подписи, но не регенерируется — graceful degradation). + */ + async getDocData

>(hash: string): Promise

{ + return this.docData.get

(hash) + } + + /** + * Помощник для конкретных фабрик: подгружает приватный payload по + * `data.doc_data_hash` и возвращает его (или null, если хэш не задан / + * запись удалена). Фабрика кладёт результат в `combinedData.doc_data` — + * шаблон документа обращается к полям как `{{ doc_data. }}`. + */ + async loadDocData

>(data: { doc_data_hash?: string }): Promise

{ + if (!data.doc_data_hash) + return null + return this.getDocData

(data.doc_data_hash) + } + async getOrganization(username: string, block_num?: number): Promise { const block_filter = block_num ? { block_num: { $lte: block_num } } : {} diff --git a/components/factory/src/Schema/ExpenseItemSchema.ts b/components/factory/src/Schema/ExpenseItemSchema.ts new file mode 100644 index 00000000000..4e7a0c176ee --- /dev/null +++ b/components/factory/src/Schema/ExpenseItemSchema.ts @@ -0,0 +1,86 @@ +import type { JSONSchemaType } from 'ajv' + +/** Публичная позиция расхода — то, что едет в meta документа on-chain. */ +export interface IExpenseItem { + number: string + description: string + amount: string + recipient_type: 'SELF' | 'MEMBER' | 'ORG' + mechanics: 'ADVANCE' | 'DIRECT' +} + +/** Позиция в модели рендера — публичная + приватная часть (склеена фабрикой из doc_data). */ +export interface IExpenseRenderItem extends IExpenseItem { + recipient_name?: string + requisites?: string + payment_purpose?: string +} + +export interface IExpenseProposalHeader { + description: string + total_amount: string + items_count: number + source_wallet: string + deadline?: string + fund_name?: string +} + +export interface IExpenseProposalDecisionBody { + kind: 'approve' | 'decline' + reason?: string +} + +/** Схема публичной позиции (2011 «Решение совета» — без приватных полей). */ +export const ExpenseItemSchema: JSONSchemaType = { + type: 'object', + properties: { + number: { type: 'string' }, + description: { type: 'string' }, + amount: { type: 'string' }, + recipient_type: { type: 'string', enum: ['SELF', 'MEMBER', 'ORG'] }, + mechanics: { type: 'string', enum: ['ADVANCE', 'DIRECT'] }, + }, + required: ['number', 'description', 'amount', 'recipient_type', 'mechanics'], + additionalProperties: true, +} + +/** Схема позиции рендера (2010 СЗ-смета — с подмешанной приватной частью). */ +export const ExpenseRenderItemSchema: JSONSchemaType = { + type: 'object', + properties: { + number: { type: 'string' }, + description: { type: 'string' }, + amount: { type: 'string' }, + recipient_type: { type: 'string', enum: ['SELF', 'MEMBER', 'ORG'] }, + mechanics: { type: 'string', enum: ['ADVANCE', 'DIRECT'] }, + recipient_name: { type: 'string', nullable: true }, + requisites: { type: 'string', nullable: true }, + payment_purpose: { type: 'string', nullable: true }, + }, + required: ['number', 'description', 'amount', 'recipient_type', 'mechanics'], + additionalProperties: true, +} + +export const ExpenseProposalHeaderSchema: JSONSchemaType = { + type: 'object', + properties: { + description: { type: 'string' }, + total_amount: { type: 'string' }, + items_count: { type: 'number' }, + source_wallet: { type: 'string' }, + deadline: { type: 'string', nullable: true }, + fund_name: { type: 'string', nullable: true }, + }, + required: ['description', 'total_amount', 'items_count', 'source_wallet'], + additionalProperties: true, +} + +export const ExpenseProposalDecisionBodySchema: JSONSchemaType = { + type: 'object', + properties: { + kind: { type: 'string', enum: ['approve', 'decline'] }, + reason: { type: 'string', nullable: true }, + }, + required: ['kind'], + additionalProperties: true, +} diff --git a/components/factory/src/Schema/index.ts b/components/factory/src/Schema/index.ts index 5bde705068f..9b6998c089d 100644 --- a/components/factory/src/Schema/index.ts +++ b/components/factory/src/Schema/index.ts @@ -10,3 +10,4 @@ export * from './MeetSchema' export * from './CommonUserSchema' export * from './AgendaMeetSchema' export * from './UdataSchema' +export * from './ExpenseItemSchema' diff --git a/components/factory/src/Services/Databazor/index.ts b/components/factory/src/Services/Databazor/index.ts index 0705d34b70d..f5597b68ca3 100644 --- a/components/factory/src/Services/Databazor/index.ts +++ b/components/factory/src/Services/Databazor/index.ts @@ -1,3 +1,4 @@ export { MongoDBConnector } from './MongoDBConnector' export { default as DataService } from './DataService' export { SearchService, type ISearchResult } from './SearchService' +export { DocDataService, type IDocDataRecord } from '../DocData' diff --git a/components/factory/src/Services/DocData/index.ts b/components/factory/src/Services/DocData/index.ts new file mode 100644 index 00000000000..8c2cd6b7902 --- /dev/null +++ b/components/factory/src/Services/DocData/index.ts @@ -0,0 +1,102 @@ +import { Buffer } from 'node:buffer' +import type { Collection } from 'mongodb' +import type { MongoDBConnector } from '../Databazor' +import { calculateSha256 } from '../../Utils/calculateSHA' + +/** + * Хранилище приватных данных документа. + * + * On-chain в meta попадает только `doc_data_hash` (sha256 от стабильно + * сериализованного payload). Сам payload (ФИО, паспорт, телефон, госномер + * и прочие персональные поля экспедитора / подписанта) лежит в этой + * коллекции отдельно и не публикуется в блокчейн. + * + * При генерации документа `DocFactory.generateDocument` сам подгружает + * payload по `doc_data_hash` и подставляет под зарезервированной + * переменной `doc_data` в combinedData — шаблон обращается как + * `{{ doc_data. }}`. + */ + +export interface IDocDataRecord

> { + hash: string + registry_id: number + payload: P + _created_at: Date +} + +const COLLECTION_NAME = 'doc_private_data' + +function stableStringify(value: unknown): string { + if (value === null || typeof value !== 'object') + return JSON.stringify(value) + + if (Array.isArray(value)) + return `[${value.map(item => stableStringify(item)).join(',')}]` + + const entries = Object.keys(value as Record) + .sort() + .map((key) => { + const v = (value as Record)[key] + return `${JSON.stringify(key)}:${stableStringify(v)}` + }) + + return `{${entries.join(',')}}` +} + +export class DocDataService { + private readonly collection: Collection + private indexEnsured = false + + constructor(storage: MongoDBConnector) { + this.collection = storage.getCollection(COLLECTION_NAME) + } + + private async ensureIndex(): Promise { + if (this.indexEnsured) + return + await this.collection.createIndex({ hash: 1 }, { unique: true }) + this.indexEnsured = true + } + + /** + * Сохраняет приватный payload и возвращает его hash. + * Идемпотентно: один и тот же payload даёт один и тот же hash, + * повторный вызов не создаёт дублей. + */ + async save

>( + payload: P, + registry_id: number, + ): Promise<{ hash: string }> { + await this.ensureIndex() + + const serialized = stableStringify(payload) + const hash = calculateSha256(Buffer.from(serialized, 'utf8')) + + await this.collection.updateOne( + { hash }, + { + $setOnInsert: { + hash, + registry_id, + payload, + _created_at: new Date(), + }, + }, + { upsert: true }, + ) + + return { hash } + } + + /** + * Получает приватный payload по хэшу. Возвращает null если запись + * удалена (документ остаётся верифицируемым по хэшу подписи, но не + * регенерируется — graceful degradation). + */ + async get

>(hash: string): Promise

{ + const record = await this.collection.findOne({ hash }) + if (!record) + return null + return record.payload as P + } +} diff --git a/components/factory/src/Templates/2010.ExpenseProposalStatement.ts b/components/factory/src/Templates/2010.ExpenseProposalStatement.ts new file mode 100644 index 00000000000..a9f0fac4dbc --- /dev/null +++ b/components/factory/src/Templates/2010.ExpenseProposalStatement.ts @@ -0,0 +1,44 @@ +import type { JSONSchemaType } from 'ajv' +import { Cooperative } from 'cooptypes' +import type { ITemplate } from '../Interfaces' +import { IMetaJSONSchema } from '../Schema/MetaSchema' +import { + CommonUserSchema, + CooperativeSchema, + ExpenseProposalHeaderSchema, + ExpenseRenderItemSchema, + VarsSchema, +} from '../Schema' + +export const registry_id = Cooperative.Registry.ExpenseProposalStatement.registry_id + +export type Action = Cooperative.Registry.ExpenseProposalStatement.Action +export type Model = Cooperative.Registry.ExpenseProposalStatement.Model +export type PrivateData = Cooperative.Registry.ExpenseProposalStatement.PrivateData + +export const Schema: JSONSchemaType = { + type: 'object', + properties: { + meta: IMetaJSONSchema, + coop: CooperativeSchema, + user: CommonUserSchema, + vars: VarsSchema, + proposal_hash: { type: 'string' }, + proposal_short_hash: { type: 'string' }, + proposal: ExpenseProposalHeaderSchema, + items: { + type: 'array', + items: ExpenseRenderItemSchema, + }, + }, + required: ['meta', 'coop', 'user', 'vars', 'proposal_hash', 'proposal_short_hash', 'proposal', 'items'], + additionalProperties: true, +} + +export const Template: ITemplate = { + title: Cooperative.Registry.ExpenseProposalStatement.title, + description: Cooperative.Registry.ExpenseProposalStatement.description, + model: Schema, + context: Cooperative.Registry.ExpenseProposalStatement.context, + translations: Cooperative.Registry.ExpenseProposalStatement.translations, +} diff --git a/components/factory/src/Templates/2011.ExpenseProposalDecision.ts b/components/factory/src/Templates/2011.ExpenseProposalDecision.ts new file mode 100644 index 00000000000..01f9e51a242 --- /dev/null +++ b/components/factory/src/Templates/2011.ExpenseProposalDecision.ts @@ -0,0 +1,52 @@ +import type { JSONSchemaType } from 'ajv' +import { Cooperative } from 'cooptypes' +import type { ITemplate } from '../Interfaces' +import { IMetaJSONSchema } from '../Schema/MetaSchema' +import { decisionSchema } from '../Schema/DecisionSchema' +import { + CooperativeSchema, + ExpenseItemSchema, + ExpenseProposalDecisionBodySchema, + ExpenseProposalHeaderSchema, + VarsSchema, +} from '../Schema' + +export const registry_id = Cooperative.Registry.ExpenseProposalDecision.registry_id + +export type Action = Cooperative.Registry.ExpenseProposalDecision.Action +export type Model = Cooperative.Registry.ExpenseProposalDecision.Model + +export const Schema: JSONSchemaType = { + type: 'object', + properties: { + meta: IMetaJSONSchema, + coop: CooperativeSchema, + vars: VarsSchema, + decision: { + type: 'object', + properties: { + ...decisionSchema.properties, + }, + required: [...decisionSchema.required], + additionalProperties: true, + }, + proposal_hash: { type: 'string' }, + proposal_short_hash: { type: 'string' }, + proposal: ExpenseProposalHeaderSchema, + items: { + type: 'array', + items: ExpenseItemSchema, + }, + resolution: ExpenseProposalDecisionBodySchema, + }, + required: ['meta', 'coop', 'vars', 'decision', 'proposal_hash', 'proposal_short_hash', 'proposal', 'items', 'resolution'], + additionalProperties: true, +} + +export const Template: ITemplate = { + title: Cooperative.Registry.ExpenseProposalDecision.title, + description: Cooperative.Registry.ExpenseProposalDecision.description, + model: Schema, + context: Cooperative.Registry.ExpenseProposalDecision.context, + translations: Cooperative.Registry.ExpenseProposalDecision.translations, +} diff --git a/components/factory/src/Templates/index.ts b/components/factory/src/Templates/index.ts index 75df95b3323..52bab6d783e 100644 --- a/components/factory/src/Templates/index.ts +++ b/components/factory/src/Templates/index.ts @@ -43,6 +43,10 @@ export * as InitProjectDecision from './1006.InitProjectDecision' export * as ExpenseStatement from './1010.ExpenseStatement' export * as ExpenseDecision from './1011.ExpenseDecision' +// Шасси расходов (волна 6, MVP-SINGLE) — C28-30 +export * as ExpenseProposalStatement from './2010.ExpenseProposalStatement' +export * as ExpenseProposalDecision from './2011.ExpenseProposalDecision' + export * as GenerationMoneyInvestStatement from './1020.GenerationMoneyInvestStatement' export * as GenerationMoneyReturnUnusedStatement from './1025.GenerationMoneyReturnUnusedStatement' diff --git a/components/factory/src/Templates/registry.ts b/components/factory/src/Templates/registry.ts index 2ddb6a08fcb..1838257a46e 100644 --- a/components/factory/src/Templates/registry.ts +++ b/components/factory/src/Templates/registry.ts @@ -31,6 +31,9 @@ import * as InitProjectDecision from './1006.InitProjectDecision' import * as ExpenseStatement from './1010.ExpenseStatement' import * as ExpenseDecision from './1011.ExpenseDecision' +import * as ExpenseProposalStatement from './2010.ExpenseProposalStatement' +import * as ExpenseProposalDecision from './2011.ExpenseProposalDecision' + import * as GenerationMoneyInvestStatement from './1020.GenerationMoneyInvestStatement' import * as GenerationMoneyReturnUnusedStatement from './1025.GenerationMoneyReturnUnusedStatement' @@ -131,4 +134,6 @@ export const Registry = { 1072: CapitalizationPropertyInvestAct, 1080: GenerationConvertStatement, 1090: CapitalizationToMainWalletConvertStatement, + 2010: ExpenseProposalStatement, + 2011: ExpenseProposalDecision, } diff --git a/components/factory/src/index.ts b/components/factory/src/index.ts index 9a073d058a0..3f9da4f26f6 100644 --- a/components/factory/src/index.ts +++ b/components/factory/src/index.ts @@ -8,7 +8,7 @@ import type { IFilterDocuments, IGeneratedDocument, Numbers, externalDataTypes, import type { IGenerate, IGenerationOptions } from './Interfaces/Documents' import * as Actions from './Actions' -import { type ISearchResult, MongoDBConnector, SearchService } from './Services/Databazor' +import { DocDataService, type ISearchResult, MongoDBConnector, SearchService } from './Services/Databazor' import type { ExternalIndividualData } from './Models/Individual' import { Individual } from './Models/Individual' import type { ExternalEntrepreneurData, ExternalOrganizationData, IVars } from './Models' @@ -39,6 +39,16 @@ export interface IGenerator { generate: (data: IGenerate, options?: IGenerationOptions) => Promise getDocument: (filter: Filter) => Promise + /** + * Сохранить приватные данные документа off-chain; возвращает hash для + * публикации on-chain как `doc_data_hash`. См. раздел + * «Document Generation Pattern: doc_data» в архитектуре. + */ + saveDocData:

>(payload: P, registry_id: number) => Promise<{ hash: string }> + + /** Прочитать приватный payload по `doc_data_hash`. null — если запись удалена. */ + getDocData:

>(hash: string) => Promise

+ constructCooperative: (username: string, block_num?: number) => Promise save: ((type: 'individual', data: ExternalIndividualData) => Promise) & ((type: 'entrepreneur', data: ExternalEntrepreneurData) => Promise) & ((type: 'organization', data: ExternalOrganizationData) => Promise) & ((type: 'paymentMethod', data: PaymentData) => Promise) & ((type: 'vars', data: IVars) => Promise) & ((type: 'project', data: ExternalProjectData) => Promise) & ((type: 'udata', data: ExternalUdata) => Promise) get: (type: dataTypes, filter: Filter) => Promise @@ -64,6 +74,9 @@ export class Generator implements IGenerator { // Сервис поиска private searchService!: SearchService + // Сервис приватных данных документов (off-chain payload + on-chain hash) + private docDataService!: DocDataService + // Метод подключения к хранилищу async connect(mongoUri: string): Promise { this.storage = new MongoDBConnector(mongoUri) @@ -71,6 +84,9 @@ export class Generator implements IGenerator { // Инициализация сервиса поиска this.searchService = new SearchService(this.storage) + // Инициализация сервиса приватных данных документов + this.docDataService = new DocDataService(this.storage) + // Инициализация фабрик документов this.factories = { [Actions.WalletAgreement.Template.registry_id]: new Actions.WalletAgreement.Factory(this.storage), // 1 @@ -128,6 +144,10 @@ export class Generator implements IGenerator { [Actions.ExpenseStatement.Template.registry_id]: new Actions.ExpenseStatement.Factory(this.storage), // 1010 [Actions.ExpenseDecision.Template.registry_id]: new Actions.ExpenseDecision.Factory(this.storage), // 1011 + // Шасси расходов — программные расходы (служебные записки) + [Actions.ExpenseProposalStatement.Template.registry_id]: new Actions.ExpenseProposalStatement.Factory(this.storage), // 2010 + [Actions.ExpenseProposalDecision.Template.registry_id]: new Actions.ExpenseProposalDecision.Factory(this.storage), // 2011 + [Actions.GenerationMoneyInvestStatement.Template.registry_id]: new Actions.GenerationMoneyInvestStatement.Factory(this.storage), // 1020 [Actions.GenerationMoneyReturnUnusedStatement.Template.registry_id]: new Actions.GenerationMoneyReturnUnusedStatement.Factory(this.storage), // 1025 @@ -175,6 +195,14 @@ export class Generator implements IGenerator { return await this.storage.getDocument(filter) } + async saveDocData

>(payload: P, registry_id: number): Promise<{ hash: string }> { + return this.docDataService.save(payload, registry_id) + } + + async getDocData

>(hash: string): Promise

{ + return this.docDataService.get

(hash) + } + async save(type: 'individual', data: ExternalIndividualData): Promise async save(type: 'entrepreneur', data: ExternalEntrepreneurData): Promise async save(type: 'organization', data: ExternalOrganizationData): Promise diff --git a/components/factory/test/doc-data.test.ts b/components/factory/test/doc-data.test.ts new file mode 100644 index 00000000000..9df944855a2 --- /dev/null +++ b/components/factory/test/doc-data.test.ts @@ -0,0 +1,73 @@ +import { afterAll, beforeAll, describe, expect, it } from 'vitest' +import { MongoDBConnector } from '../src/Services/Databazor' +import { DocDataService } from '../src/Services/DocData' +import { generator, mongoUri } from './utils' + +describe('DocDataService — приватные данные документов', () => { + let storage: MongoDBConnector + let service: DocDataService + + beforeAll(async () => { + storage = new MongoDBConnector(mongoUri) + await storage.connect() + service = new DocDataService(storage) + await storage.getCollection('doc_private_data').deleteMany({}) + }) + + afterAll(async () => { + await storage.disconnect() + }) + + it('save → возвращает sha256 hash и сохраняет payload', async () => { + const payload = { + expeditor_full_name: 'Иванов Иван Иванович', + passport: '4509 123456', + phone: '+79991234567', + vehicle_registration_number: 'А123ВВ77', + } + + const { hash } = await service.save(payload, 1103) + expect(hash).toMatch(/^[A-F0-9]{64}$/) + + const restored = await service.get(hash) + expect(restored).toEqual(payload) + }) + + it('save идемпотентен: одинаковый payload → одинаковый hash, без дублей', async () => { + const payload = { foo: 'bar', baz: 42 } + + const r1 = await service.save(payload, 1102) + const r2 = await service.save(payload, 1102) + expect(r1.hash).toEqual(r2.hash) + + const count = await storage.getCollection('doc_private_data').countDocuments({ hash: r1.hash }) + expect(count).toBe(1) + }) + + it('save стабилен по порядку ключей: {a,b} и {b,a} → один и тот же hash', async () => { + const r1 = await service.save({ a: 1, b: 2 }, 1102) + const r2 = await service.save({ b: 2, a: 1 }, 1102) + expect(r1.hash).toEqual(r2.hash) + }) + + it('get → null если запись удалена (graceful degradation)', async () => { + const { hash } = await service.save({ tmp: 'будет удалён' }, 1102) + await storage.getCollection('doc_private_data').deleteOne({ hash }) + + const restored = await service.get(hash) + expect(restored).toBeNull() + }) + + it('Generator-фасад: saveDocData / getDocData делегируются', async () => { + await generator.connect(mongoUri) + try { + const payload = { signer_full_name: 'Петров Пётр Петрович' } + const { hash } = await generator.saveDocData(payload, 1102) + const restored = await generator.getDocData(hash) + expect(restored).toEqual(payload) + } + finally { + await generator.disconnect() + } + }) +}) diff --git a/components/factory/test/documents-1000-plus.test.ts b/components/factory/test/documents-1000-plus.test.ts index e58ab2cad5f..24ece6c28cd 100644 --- a/components/factory/test/documents-1000-plus.test.ts +++ b/components/factory/test/documents-1000-plus.test.ts @@ -219,6 +219,91 @@ describe('тест генератора документов с registry_id >= 1 }) }) + // Шасси расходов (волна 6, MVP-SINGLE) — C28-30 + it('генерируем СЗ-смету о расходах 2010 (шасси, массив items)', async () => { + await testDocumentGeneration({ + registry_id: 2010, + coopname: 'voskhod', + username: 'ant', + lang: 'ru', + proposal: { + description: 'Закупка хостинга и бухгалтерских услуг на июнь 2026', + total_amount: '15000.00 RUB', + items_count: 2, + source_wallet: 'w.cap.bla', + }, + items: [ + { + number: '1', + description: 'Аренда серверов Yandex Cloud (тариф S, июнь)', + amount: '12000.00 RUB', + recipient_type: 'ORG', + mechanics: 'DIRECT', + recipient_name: 'ООО «Яндекс.Облако»', + requisites: 'ИНН 7704414297, р/с 40702810000000000000, БИК 044525000', + }, + { + number: '2', + description: 'Канцелярия для офиса', + amount: '3000.00 RUB', + recipient_type: 'SELF', + mechanics: 'ADVANCE', + }, + ], + }) + }) + + it('генерируем протокол-1 утверждения СЗ 2011 (шасси, approve)', async () => { + await testDocumentGeneration({ + registry_id: 2011, + coopname: 'voskhod', + username: 'ant', + lang: 'ru', + proposal_hash: '0xabc123def4560000000000000000000000000000000000000000000000000000', + proposal: { + description: 'Закупка хостинга и бухгалтерских услуг на июнь 2026', + total_amount: '15000.00 RUB', + items_count: 2, + source_wallet: 'w.cap.bla', + }, + items: [ + { number: '1', description: 'Аренда серверов Yandex Cloud', amount: '12000.00 RUB', recipient_type: 'ORG', mechanics: 'DIRECT' }, + { number: '2', description: 'Канцелярия для офиса', amount: '3000.00 RUB', recipient_type: 'SELF', mechanics: 'ADVANCE' }, + ], + decision: { + kind: 'approve', + protocol_number: '15', + protocol_date: '03.06.2026', + }, + }) + }) + + it('генерируем протокол-1 ОТКАЗА по СЗ 2011 (шасси, decline + reason)', async () => { + await testDocumentGeneration({ + registry_id: 2011, + coopname: 'voskhod', + username: 'ant', + lang: 'ru', + proposal_hash: '0xfedcba9876540000000000000000000000000000000000000000000000000000', + proposal: { + description: 'Закупка хостинга и бухгалтерских услуг на июнь 2026', + total_amount: '15000.00 RUB', + items_count: 2, + source_wallet: 'w.cap.bla', + }, + items: [ + { number: '1', description: 'Аренда серверов Yandex Cloud', amount: '12000.00 RUB', recipient_type: 'ORG', mechanics: 'DIRECT' }, + { number: '2', description: 'Канцелярия для офиса', amount: '3000.00 RUB', recipient_type: 'SELF', mechanics: 'ADVANCE' }, + ], + decision: { + kind: 'decline', + reason: 'Просьба разделить позиции на отдельные СЗ — Yandex Cloud и канцелярия имеют разный контур контроля.', + protocol_number: '16', + protocol_date: '03.06.2026', + }, + }) + }) + // Документы инвестиций в генерацию it('генерируем заявление об инвестициях денег в генерацию', async () => { await testDocumentGeneration({ diff --git a/components/factory/test/utils/index.ts b/components/factory/test/utils/index.ts index 94e53996966..b6bbec62fc7 100644 --- a/components/factory/test/utils/index.ts +++ b/components/factory/test/utils/index.ts @@ -48,6 +48,7 @@ export async function preLoading() { 'udatas', 'deltas', 'actions', + 'doc_private_data', ] for (const collectionName of collectionsToClear) { diff --git a/components/inter/src/expense-chassis.port.ts b/components/inter/src/expense-chassis.port.ts new file mode 100644 index 00000000000..38b6ec724b7 --- /dev/null +++ b/components/inter/src/expense-chassis.port.ts @@ -0,0 +1,176 @@ +/** + * Шасси расходов (контракт `expense`): read-side для приложений-потребителей + * (capital, marketplace, EMP). Реализация — расширение `expense-chassis`; + * токен `INTER_EXPENSE_CHASSIS` регистрируется в InterCommunicationBridgeModule. + * + * Mutations approve/authorize/pay/report/close/decline — это прямые GraphQL + * endpoints шасси; consumer-extension'ы их НЕ оборачивают, чтобы не дублировать + * lifecycle. Порт нужен только для чтения proposals из инфраструктуры шасси + * без межсервисных вызовов. + */ + +/** Lifecycle-статус proposal'а шасси (зеркало `ExpenseProposalStatus` в `extensions/expenses`). */ +export type InterExpenseProposalStatus = + | 'CREATED' + | 'AUTHORIZED' + | 'PARTIALLY_PAID' + | 'REPORT_SUBMITTED' + | 'CLOSED' + | 'DECLINED' + | 'UNDEFINED'; + +/** Item — одна строка СЗ-расхода (зеркало `IExpenseItemBlockchainData`). */ +export interface InterExpenseItem { + itemHash: string; + /** ExpenseMechanics: 0=ADVANCE / 1=DIRECT / 2=POST_FACT (узнать у consumer'а). */ + mechanics: number; + /** ExpenseRecipientType: 0=USER / 1=ORG / 2=EXTERNAL. */ + recipientType: number; + /** Получатель: username (USER) / inn (ORG) / свободная строка (EXTERNAL). */ + recipient: string; + description: string; + /** Сумма-резерв позиции (raw asset, например "1500.0000 RUB"). */ + plannedAmount: string; + /** Сумма-факт после `reportexp` (0 пока не отчитались). */ + actualAmount: string; + /** ExpenseItemStatus blockchain enum. */ + status: number; +} + +export interface InterExpenseCallbackHandler { + /** Owner-контракт consumer'а (для capital'а — `capital`). Пусто = standalone. */ + contract: string; + action: string; + data: string; +} + +export interface InterExpenseProposalRead { + coopname: string; + /** Хэш СЗ-расхода (он же `expense_hash` в capital::progexpenses). */ + proposalHash: string; + /** Кошелёк-источник (например `BLAGOROST_FUND`). */ + sourceWalletCode: string; + /** Инициатор (creator) — username председателя, оформившего расход. */ + creator: string; + status: InterExpenseProposalStatus; + /** Callback consumer-контракта: пустой `contract` ⇒ standalone-расход без owner'а. */ + callback?: InterExpenseCallbackHandler; + items: InterExpenseItem[]; + /** Сумма-резерв по items (raw asset). */ + totalPlanned: string; + /** Фактически потрачено после `reportexp` (raw asset; "0.0000 RUB" пока не отчитались). */ + totalActual: string; + /** ISO timestamp создания. */ + createdAt: string; + /** ISO timestamp последнего перехода lifecycle. */ + updatedAt: string; +} + +/** Пагинация — нейтральная (не зависит от Nest/TypeORM DTO). */ +export interface InterExpensePagination { + limit?: number; + offset?: number; + /** Поле сортировки. По умолчанию `updatedAt`. */ + sortBy?: 'createdAt' | 'updatedAt'; + /** Направление. По умолчанию `DESC`. */ + sortOrder?: 'ASC' | 'DESC'; +} + +export interface InterExpensePaginatedResult { + items: T[]; + totalCount: number; +} + + +/** Строка СЗ для снимка реквизитов получателя (фиксация «куда платить» на момент создания). */ +export interface InterExpenseRequisiteItemInput { + proposalHash: string; + itemHash: string; + /** Получатель: username пайщика (владелец платёжного метода) либо пусто для организации. */ + recipient: string; + /** true — получатель-организация: реквизиты приходят строкой `requisites`. */ + isOrganization: boolean; + /** + * Способ оплаты строки. Инвариант шасси: пайщик (SELF/MEMBER) получает только + * ADVANCE (аванс под отчёт на личные реквизиты, расходом станет после отчёта + * чеком), организация — только DIRECT (прямая оплата по выставленным реквизитам). + */ + mechanics: 'ADVANCE' | 'DIRECT'; + /** Идентификатор платёжного метода получателя-пайщика. */ + paymentMethodId?: string; + /** Реквизиты строкой (организации — ручной ввод из формы). */ + requisites?: string; + /** Назначение платежа (оплата по счёту) — фиксируется в снимке для кассира. */ + paymentPurpose?: string; +} + +export interface InterExpenseChassisPort { + /** + * Чтение proposal'а по хэшу. Возвращает null, если шасси не видит + * (либо хэш чужого кооператива, либо ещё не долетел из parser-stream'а). + */ + readProposalByHash(coopname: string, proposalHash: string): Promise; + + /** + * Batch-чтение для list-view (страница «Управление расходами программы» в Капитале). + * Order не гарантируется — вызывающий сам индексирует по proposalHash. + */ + readProposalsByHashes(coopname: string, proposalHashes: string[]): Promise; + + /** + * Список proposals по owner'у (значение `callback.contract`) + опц. action. + * Используется list-view'ями consumer-extension'ов: «дай мне все программные + * расходы Капитала» = listProposalsByOwner(coopname, 'capital', 'onpgexpdone'). + */ + listProposalsByOwner( + coopname: string, + ownerContract: string, + ownerAction?: string, + pagination?: InterExpensePagination, + ): Promise>; + + /** + * Валидация реквизитов строк СЗ ДО постановки on-chain заявки: у каждой + * строки с получателем-пайщиком должен существовать указанный платёжный метод. + */ + validateRequisites(coopname: string, items: InterExpenseRequisiteItemInput[]): Promise; + + /** + * Снимок реквизитов ПОСЛЕ успешной on-chain заявки: данные платёжного метода + * копируются в хранилище шасси на момент создания СЗ — последующее изменение + * метода пайщиком не меняет то, куда платить по уже поданной смете. + */ + snapshotRequisites(coopname: string, items: InterExpenseRequisiteItemInput[]): Promise; + + /** + * On-chain оплата позиции СЗ (`expense::payexp`) под подписью кооператива. + * Вызывается реестром платежей (gateway) при подтверждении кассиром + * исходящего платежа типа EXPENSE. `actualAmount` — raw asset + * (например "1000.0000 RUB"). + */ + payItem(coopname: string, proposalHash: string, itemHash: string, actualAmount: string): Promise; + + /** + * On-chain возврат неиспользованного аванса (`expense::returnexp`) под подписью + * кооператива. Вызывается реестром платежей при подтверждении кассиром приёма + * ВХОДЯЩЕГО платежа типа EXPENSE_RETURN (пайщик вернул разницу недорасхода). + * `returnAmount` — raw asset (модуль разницы факт−аванс, например "200.0000 RUB"). + */ + returnItem(coopname: string, proposalHash: string, itemHash: string, returnAmount: string): Promise; + + /** + * On-chain доплата при перерасходе (`expense::overspendexp`) под подписью + * кооператива. Вызывается реестром платежей при подтверждении кассиром выплаты + * ИСХОДЯЩЕГО платежа типа EXPENSE_OVERSPEND (кооператив доплатил перерасход). + * `overspendAmount` — raw asset (разница факт−аванс, например "200.0000 RUB"). + */ + overspendItem(coopname: string, proposalHash: string, itemHash: string, overspendAmount: string): Promise; + + /** + * On-chain закрытие позиции отчётом (`expense::reportexp`) под подписью + * кооператива. Вызывается реестром платежей после того, как расчёт разницы + * проведён (returnexp/overspendexp), либо напрямую сервисом отчёта, когда факт + * совпал с авансом и расчёт разницы не нужен. + */ + reportItem(coopname: string, proposalHash: string, itemHash: string): Promise; +} diff --git a/components/inter/src/index.ts b/components/inter/src/index.ts index 009b3abe2d9..649611c15d5 100644 --- a/components/inter/src/index.ts +++ b/components/inter/src/index.ts @@ -1,6 +1,7 @@ export { INTER_CHATCOOP_CALENDAR, INTER_COOP_CALENDAR_EVENT_NOTIFICATION, + INTER_EXPENSE_CHASSIS, INTER_FILE_STORAGE, INTER_MATRIX_ROOM_MESSAGING, INTER_PROJECT_CAPITAL_CLEARANCE, @@ -59,3 +60,14 @@ export type { } from './coop-calendar-event-notification.port'; export type { InterProjectCapitalClearancePort } from './project-capital-clearance.port'; + +export type { + InterExpenseCallbackHandler, + InterExpenseChassisPort, + InterExpenseItem, + InterExpensePagination, + InterExpensePaginatedResult, + InterExpenseProposalRead, + InterExpenseProposalStatus, + InterExpenseRequisiteItemInput, +} from './expense-chassis.port'; diff --git a/components/inter/src/tokens.ts b/components/inter/src/tokens.ts index 22c1afa2849..dfd7495211e 100644 --- a/components/inter/src/tokens.ts +++ b/components/inter/src/tokens.ts @@ -19,3 +19,6 @@ export const INTER_PROJECT_CAPITAL_CLEARANCE = Symbol.for('InterProjectCapitalCl /** FileStorage: универсальное файловое хранилище контура кооператива (MinIO в v1, S3 в v2). */ export const INTER_FILE_STORAGE = Symbol.for('InterFileStorage'); + +/** Шасси расходов (contract `expense`): read-side proposals для consumer-extension'ов (capital, marketplace, EMP). */ +export const INTER_EXPENSE_CHASSIS = Symbol.for('InterExpenseChassis'); diff --git a/components/notifications/src/workflows/expense-advance-report-reminder/index.ts b/components/notifications/src/workflows/expense-advance-report-reminder/index.ts new file mode 100644 index 00000000000..5587fc53ab1 --- /dev/null +++ b/components/notifications/src/workflows/expense-advance-report-reminder/index.ts @@ -0,0 +1,58 @@ +import { z } from 'zod'; +import { WorkflowDefinition } from '../../types'; +import { WorkflowBuilder } from '../../base/workflow-builder'; +import { createEmailStep, createInAppStep } from '../../base/defaults'; +import { BaseWorkflowPayload } from '../../types'; +import { slugify } from '../../utils'; + +// Одна строка-аванс в дайджесте напоминания. +const advanceItemSchema = z.object({ + description: z.string(), // Назначение расхода (item.description) + amount: z.string(), // Сумма выданного аванса (asset-строка) + url: z.string(), // Прямая ссылка на страницу этого расхода +}); + +// Напоминание пайщику отчитаться по выданным авансам под отчёт. +// `period` — техническое поле (ISO-неделя): входит в payload, чтобы +// идемпотентность Центра уведомлений гасила повторы в пределах недели и +// пропускала ровно одно письмо на следующей неделе. В шаблоне не рендерится. +export const expenseAdvanceReportReminderPayloadSchema = z.object({ + coopName: z.string(), + period: z.string(), + count: z.number(), + link: z.string(), // Основная CTA-ссылка (на сам расход, если он один, иначе на личную страницу «Платежи») + advances: z.array(advanceItemSchema), +}); +export type IPayload = z.infer; +export interface IWorkflow extends BaseWorkflowPayload, IPayload {} + +export const name = 'Напоминание об отчёте по авансу'; +export const id = slugify(name); + +export const workflow: WorkflowDefinition = WorkflowBuilder + .create() + .name(name) + .workflowId(id) + .description('Еженедельное напоминание пайщику предоставить отчёт по выданному авансу под отчёт') + .payloadSchema(expenseAdvanceReportReminderPayloadSchema) + .tags(['expense', 'financial']) + .addSteps([ + createEmailStep( + 'expense-advance-report-reminder-email', + 'Ожидается отчёт по авансу под отчёт — {{payload.coopName}}', + 'Уважаемый пайщик!

' + + 'По {% if payload.count > 1 %}следующим авансам под отчёт{% else %}авансу под отчёт{% endif %}, ' + + 'полученным в кооперативе «{{payload.coopName}}», ещё не предоставлен отчёт:

' + + '{% for advance in payload.advances %}• {{advance.description}} — {{advance.amount}} — ' + + 'открыть расход
{% endfor %}' + + '
Пожалуйста, приложите подтверждающие документы (чек, акт) на странице расхода в личном ' + + 'кабинете — после этого отчёт будет подан автоматически.

' + + 'Перейти к моим авансам' + ), + createInAppStep( + 'expense-advance-report-reminder-inapp', + 'Ожидается отчёт по авансу под отчёт', + 'По вашим авансам под отчёт ещё не предоставлен отчёт. Приложите подтверждающие документы в личном кабинете.' + ), + ]) + .build(); diff --git a/components/notifications/src/workflows/index.ts b/components/notifications/src/workflows/index.ts index 8cff257d122..06ebeb3f87f 100644 --- a/components/notifications/src/workflows/index.ts +++ b/components/notifications/src/workflows/index.ts @@ -25,6 +25,7 @@ import { workflow as serverProvisionedWorkflow } from './server-provisioned'; import { workflow as decisionExpiredWorkflow } from './decision-expired'; import { workflow as chatcoopCalendarEventCreatedWorkflow } from './chatcoop-calendar-event-created'; import { workflow as chatcoopCalendarEventUpdatedWorkflow } from './chatcoop-calendar-event-updated'; +import { workflow as expenseAdvanceReportReminderWorkflow } from './expense-advance-report-reminder'; // Импортируем все воркфлоу export * as Welcome from './welcome'; @@ -52,6 +53,7 @@ export * as ServerProvisioned from './server-provisioned'; export * as DecisionExpired from './decision-expired'; export * as ChatCoopCalendarEventCreated from './chatcoop-calendar-event-created'; export * as ChatCoopCalendarEventUpdated from './chatcoop-calendar-event-updated'; +export * as ExpenseAdvanceReportReminder from './expense-advance-report-reminder'; // Массив всех воркфлоу для автоматической регистрации export const allWorkflows: WorkflowDefinition[] = [ @@ -80,6 +82,7 @@ export const allWorkflows: WorkflowDefinition[] = [ decisionExpiredWorkflow, chatcoopCalendarEventCreatedWorkflow, chatcoopCalendarEventUpdatedWorkflow, + expenseAdvanceReportReminderWorkflow, ]; // Экспортируем воркфлоу по ID для удобного доступа diff --git a/components/notifications/src/workflows/payment-refunded/index.ts b/components/notifications/src/workflows/payment-refunded/index.ts index 0c49700f8c8..1b7cef5c4df 100644 --- a/components/notifications/src/workflows/payment-refunded/index.ts +++ b/components/notifications/src/workflows/payment-refunded/index.ts @@ -6,7 +6,9 @@ import { BaseWorkflowPayload } from '../../types'; import { createEmailStep, createInAppStep, createPushStep } from '../../base/defaults'; import { slugify } from '../../utils'; -// Схема для payment-refunded воркфлоу (исходящий платёж-возврат пайщику) +// Схема исходящего платежа пайщику. Это НЕ обязательно возврат взноса — тем же +// каналом идут оплата аванса под отчёт, доплата по перерасходу и пр., поэтому +// текст универсальный «Платёж выполнен», без привязки к типу платежа. export const paymentRefundedPayloadSchema = z.object({ userName: z.string(), paymentAmount: z.string(), @@ -19,31 +21,31 @@ export type IPayload = z.infer; export interface IWorkflow extends BaseWorkflowPayload, IPayload {} -export const name = 'Возврат взноса выполнен'; +export const name = 'Платёж выполнен'; export const id = slugify(name); export const workflow: WorkflowDefinition = WorkflowBuilder .create() .name(name) .workflowId(id) - .description('Уведомление о выполненном возврате взноса пайщику') + .description('Уведомление о выполненном исходящем платеже пайщику (аванс, возврат, доплата и пр.)') .payloadSchema(paymentRefundedPayloadSchema) .tags(['user']) // Для всех пользователей .addSteps([ createEmailStep( 'payment-refunded-email', - 'Возврат взноса выполнен', - 'Уважаемый {{payload.userName}}!

Ваш взнос возвращён.

Сумма: {{payload.paymentAmount}} {{payload.paymentCurrency}}

Дата: {{payload.paymentDate}}

Подробная информация доступна по ссылке: {{payload.paymentUrl}}' + 'Платёж выполнен', + 'Уважаемый {{payload.userName}}!

Вам выполнен платёж.

Сумма: {{payload.paymentAmount}} {{payload.paymentCurrency}}

Дата: {{payload.paymentDate}}

Подробная информация доступна по ссылке: {{payload.paymentUrl}}' ), createInAppStep( 'payment-refunded-notification', - 'Возврат взноса выполнен', - 'Возврат на сумму {{payload.paymentAmount}} {{payload.paymentCurrency}} выполнен' + 'Платёж выполнен', + 'Платёж на сумму {{payload.paymentAmount}} {{payload.paymentCurrency}} выполнен' ), createPushStep( 'payment-refunded-push', - 'Возврат взноса выполнен', - 'Возврат {{payload.paymentAmount}} {{payload.paymentCurrency}} выполнен' + 'Платёж выполнен', + 'Платёж {{payload.paymentAmount}} {{payload.paymentCurrency}} выполнен' ), ]) .build(); diff --git a/components/parser/src/config.ts b/components/parser/src/config.ts index dae9d492d8d..e2d0b8a6765 100644 --- a/components/parser/src/config.ts +++ b/components/parser/src/config.ts @@ -33,6 +33,7 @@ export const subscribedContracts: string[] = [ 'ledger', 'ledger2', 'marketplace', + 'expense', ] // Автоматически генерируем действия для всех контрактов из списка diff --git a/components/sdk/src/mutations/capital/createProgramExpense.ts b/components/sdk/src/mutations/capital/createProgramExpense.ts new file mode 100644 index 00000000000..e84c1602193 --- /dev/null +++ b/components/sdk/src/mutations/capital/createProgramExpense.ts @@ -0,0 +1,19 @@ +import { rawTransactionSelector } from '../../selectors' +import { $, type GraphQLTypes, type InputType, type ModelTypes, Selector } from '../../zeus/index' + +export const name = 'capitalCreateProgramExpense' + +export const mutation = Selector('Mutation')({ + [name]: [{ data: $('data', 'CapitalCreateProgramExpenseInput!') }, rawTransactionSelector], +}) + +export interface IInput { + /** + * @private + */ + [key: string]: unknown + + data: ModelTypes['CapitalCreateProgramExpenseInput'] +} + +export type IOutput = InputType diff --git a/components/sdk/src/mutations/capital/index.ts b/components/sdk/src/mutations/capital/index.ts index eb6a5797004..fa1d6a38f85 100644 --- a/components/sdk/src/mutations/capital/index.ts +++ b/components/sdk/src/mutations/capital/index.ts @@ -11,6 +11,7 @@ export * as CreateCycle from './createCycle' export * as CreateDebt from './createDebt' export * as CreateExpense from './createExpense' export * as CreateIssue from './createIssue' +export * as CreateProgramExpense from './createProgramExpense' export * as CreateProgramInvest from './createProgramInvest' export * as CreateProgramProperty from './createProgramProperty' export * as CreateProject from './createProject' @@ -67,6 +68,7 @@ export * as StartProject from './startProject' export * as StartVoting from './startVoting' export * as StopProject from './stopProject' export * as SubmitVote from './submitVote' +export * as TopupProgramExpense from './topupProgramExpense' export * as UpdateIssue from './updateIssue' export * as UpdateStory from './updateStory' diff --git a/components/sdk/src/mutations/capital/topupProgramExpense.ts b/components/sdk/src/mutations/capital/topupProgramExpense.ts new file mode 100644 index 00000000000..2f1bd686473 --- /dev/null +++ b/components/sdk/src/mutations/capital/topupProgramExpense.ts @@ -0,0 +1,19 @@ +import { rawTransactionSelector } from '../../selectors' +import { $, type GraphQLTypes, type InputType, type ModelTypes, Selector } from '../../zeus/index' + +export const name = 'capitalTopupProgramExpensePool' + +export const mutation = Selector('Mutation')({ + [name]: [{ data: $('data', 'CapitalTopupProgramExpenseInput!') }, rawTransactionSelector], +}) + +export interface IInput { + /** + * @private + */ + [key: string]: unknown + + data: ModelTypes['CapitalTopupProgramExpenseInput'] +} + +export type IOutput = InputType diff --git a/components/sdk/src/mutations/expense/createExpenseProposal.ts b/components/sdk/src/mutations/expense/createExpenseProposal.ts new file mode 100644 index 00000000000..56b62794c4a --- /dev/null +++ b/components/sdk/src/mutations/expense/createExpenseProposal.ts @@ -0,0 +1,19 @@ +import { rawTransactionSelector } from '../../selectors' +import { $, type GraphQLTypes, type InputType, type ModelTypes, Selector } from '../../zeus/index' + +export const name = 'createExpenseProposal' + +export const mutation = Selector('Mutation')({ + [name]: [{ data: $('data', 'CreateExpenseProposalInput!') }, rawTransactionSelector], +}) + +export interface IInput { + /** + * @private + */ + [key: string]: unknown + + data: ModelTypes['CreateExpenseProposalInput'] +} + +export type IOutput = InputType diff --git a/components/sdk/src/mutations/expense/generateExpenseProposalDecisionDocument.ts b/components/sdk/src/mutations/expense/generateExpenseProposalDecisionDocument.ts new file mode 100644 index 00000000000..e0bdbb02ad0 --- /dev/null +++ b/components/sdk/src/mutations/expense/generateExpenseProposalDecisionDocument.ts @@ -0,0 +1,23 @@ +import { $, Selector, type ModelTypes } from '../../zeus/index' +import { rawGeneratedDocumentSelector } from '../../selectors/documents/documentAggregateSelector' + +export const name = 'generateExpenseProposalDecisionDocument' + +export const mutation = Selector('Mutation')({ + [name]: [ + { + data: $('data', 'ExpenseProposalDecisionGenerateDocumentInput!'), + options: $('options', 'GenerateDocumentOptionsInput'), + }, + rawGeneratedDocumentSelector, + ], +}) + +export interface IInput { + data: ModelTypes['ExpenseProposalDecisionGenerateDocumentInput'] + options?: ModelTypes['GenerateDocumentOptionsInput'] +} + +export type IOutput = { + [name]: ModelTypes['GeneratedDocument'] +} diff --git a/components/sdk/src/mutations/expense/generateExpenseProposalStatementDocument.ts b/components/sdk/src/mutations/expense/generateExpenseProposalStatementDocument.ts new file mode 100644 index 00000000000..4382b94167b --- /dev/null +++ b/components/sdk/src/mutations/expense/generateExpenseProposalStatementDocument.ts @@ -0,0 +1,23 @@ +import { $, Selector, type ModelTypes } from '../../zeus/index' +import { rawGeneratedDocumentSelector } from '../../selectors/documents/documentAggregateSelector' + +export const name = 'generateExpenseProposalStatementDocument' + +export const mutation = Selector('Mutation')({ + [name]: [ + { + data: $('data', 'ExpenseProposalStatementGenerateDocumentInput!'), + options: $('options', 'GenerateDocumentOptionsInput'), + }, + rawGeneratedDocumentSelector, + ], +}) + +export interface IInput { + data: ModelTypes['ExpenseProposalStatementGenerateDocumentInput'] + options?: ModelTypes['GenerateDocumentOptionsInput'] +} + +export type IOutput = { + [name]: ModelTypes['GeneratedDocument'] +} diff --git a/components/sdk/src/mutations/expense/index.ts b/components/sdk/src/mutations/expense/index.ts new file mode 100644 index 00000000000..0c259b8d258 --- /dev/null +++ b/components/sdk/src/mutations/expense/index.ts @@ -0,0 +1,9 @@ +export * as GenerateExpenseProposalStatementDocument from './generateExpenseProposalStatementDocument' +export * as GenerateExpenseProposalDecisionDocument from './generateExpenseProposalDecisionDocument' +export * as CreateExpenseProposal from './createExpenseProposal' +export * as PayExpenseItem from './payExpenseItem' +export * as ReportExpenseItem from './reportExpenseItem' +export * as ReturnExpenseItem from './returnExpenseItem' +export * as OverspendExpenseItem from './overspendExpenseItem' +export * as SubmitExpenseReport from './submitExpenseReport' +export * as UploadExpenseFile from './uploadExpenseFile' diff --git a/components/sdk/src/mutations/expense/overspendExpenseItem.ts b/components/sdk/src/mutations/expense/overspendExpenseItem.ts new file mode 100644 index 00000000000..6da5b0549e0 --- /dev/null +++ b/components/sdk/src/mutations/expense/overspendExpenseItem.ts @@ -0,0 +1,19 @@ +import { rawTransactionSelector } from '../../selectors' +import { $, type GraphQLTypes, type InputType, type ModelTypes, Selector } from '../../zeus/index' + +export const name = 'overspendExpenseItem' + +export const mutation = Selector('Mutation')({ + [name]: [{ data: $('data', 'OverspendExpenseItemInput!') }, rawTransactionSelector], +}) + +export interface IInput { + /** + * @private + */ + [key: string]: unknown + + data: ModelTypes['OverspendExpenseItemInput'] +} + +export type IOutput = InputType diff --git a/components/sdk/src/mutations/expense/payExpenseItem.ts b/components/sdk/src/mutations/expense/payExpenseItem.ts new file mode 100644 index 00000000000..5b600d5cadd --- /dev/null +++ b/components/sdk/src/mutations/expense/payExpenseItem.ts @@ -0,0 +1,19 @@ +import { rawTransactionSelector } from '../../selectors' +import { $, type GraphQLTypes, type InputType, type ModelTypes, Selector } from '../../zeus/index' + +export const name = 'payExpenseItem' + +export const mutation = Selector('Mutation')({ + [name]: [{ data: $('data', 'PayExpenseItemInput!') }, rawTransactionSelector], +}) + +export interface IInput { + /** + * @private + */ + [key: string]: unknown + + data: ModelTypes['PayExpenseItemInput'] +} + +export type IOutput = InputType diff --git a/components/sdk/src/mutations/expense/reportExpenseItem.ts b/components/sdk/src/mutations/expense/reportExpenseItem.ts new file mode 100644 index 00000000000..997a42cb4ea --- /dev/null +++ b/components/sdk/src/mutations/expense/reportExpenseItem.ts @@ -0,0 +1,19 @@ +import { rawExpenseReportResultSelector } from '../../selectors' +import { $, type GraphQLTypes, type InputType, type ModelTypes, Selector } from '../../zeus/index' + +export const name = 'reportExpenseItem' + +export const mutation = Selector('Mutation')({ + [name]: [{ data: $('data', 'ReportExpenseItemInput!') }, rawExpenseReportResultSelector], +}) + +export interface IInput { + /** + * @private + */ + [key: string]: unknown + + data: ModelTypes['ReportExpenseItemInput'] +} + +export type IOutput = InputType diff --git a/components/sdk/src/mutations/expense/returnExpenseItem.ts b/components/sdk/src/mutations/expense/returnExpenseItem.ts new file mode 100644 index 00000000000..140b7288e89 --- /dev/null +++ b/components/sdk/src/mutations/expense/returnExpenseItem.ts @@ -0,0 +1,19 @@ +import { rawTransactionSelector } from '../../selectors' +import { $, type GraphQLTypes, type InputType, type ModelTypes, Selector } from '../../zeus/index' + +export const name = 'returnExpenseItem' + +export const mutation = Selector('Mutation')({ + [name]: [{ data: $('data', 'ReturnExpenseItemInput!') }, rawTransactionSelector], +}) + +export interface IInput { + /** + * @private + */ + [key: string]: unknown + + data: ModelTypes['ReturnExpenseItemInput'] +} + +export type IOutput = InputType diff --git a/components/sdk/src/mutations/expense/submitExpenseReport.ts b/components/sdk/src/mutations/expense/submitExpenseReport.ts new file mode 100644 index 00000000000..f58bfee5034 --- /dev/null +++ b/components/sdk/src/mutations/expense/submitExpenseReport.ts @@ -0,0 +1,19 @@ +import { rawTransactionSelector } from '../../selectors' +import { $, type GraphQLTypes, type InputType, type ModelTypes, Selector } from '../../zeus/index' + +export const name = 'submitExpenseReport' + +export const mutation = Selector('Mutation')({ + [name]: [{ data: $('data', 'SubmitExpenseReportInput!') }, rawTransactionSelector], +}) + +export interface IInput { + /** + * @private + */ + [key: string]: unknown + + data: ModelTypes['SubmitExpenseReportInput'] +} + +export type IOutput = InputType diff --git a/components/sdk/src/mutations/expense/uploadExpenseFile.ts b/components/sdk/src/mutations/expense/uploadExpenseFile.ts new file mode 100644 index 00000000000..5a85b486584 --- /dev/null +++ b/components/sdk/src/mutations/expense/uploadExpenseFile.ts @@ -0,0 +1,19 @@ +import { rawExpenseFileSelector } from '../../selectors/expense' +import { $, type GraphQLTypes, type InputType, type ModelTypes, Selector } from '../../zeus/index' + +export const name = 'uploadExpenseFile' + +export const mutation = Selector('Mutation')({ + [name]: [{ data: $('data', 'UploadExpenseFileInput!') }, rawExpenseFileSelector], +}) + +export interface IInput { + /** + * @private + */ + [key: string]: unknown + + data: ModelTypes['UploadExpenseFileInput'] +} + +export type IOutput = InputType diff --git a/components/sdk/src/mutations/index.ts b/components/sdk/src/mutations/index.ts index 0d1323625b6..b8032b975f5 100644 --- a/components/sdk/src/mutations/index.ts +++ b/components/sdk/src/mutations/index.ts @@ -8,6 +8,7 @@ export * as ChatCoop from './chatcoop' export * as Cooplace from './cooplace' export * as Decisions from './decisions' export * as Documents from './documents' +export * as Expense from './expense' export * as Extensions from './extensions' export * as FreeDecisions from './freeDecisions' export * as Gateway from './gateway' diff --git a/components/sdk/src/queries/capital/getProgramExpense.ts b/components/sdk/src/queries/capital/getProgramExpense.ts new file mode 100644 index 00000000000..2414207cd00 --- /dev/null +++ b/components/sdk/src/queries/capital/getProgramExpense.ts @@ -0,0 +1,23 @@ +import { programExpenseSelector } from '../../selectors/capital/programExpenseSelector' +import { $, type GraphQLTypes, type InputType, Selector } from '../../zeus/index' + +export const name = 'capitalProgramExpense' + +/** + * Программный расход по expense_hash. + */ +export const query = Selector('Query')({ + [name]: [{ coopname: $('coopname', 'String!'), expense_hash: $('expense_hash', 'String!') }, programExpenseSelector], +}) + +export interface IInput { + /** + * @private + */ + [key: string]: unknown + + coopname: string + expense_hash: string +} + +export type IOutput = InputType diff --git a/components/sdk/src/queries/capital/getProgramExpenses.ts b/components/sdk/src/queries/capital/getProgramExpenses.ts new file mode 100644 index 00000000000..fe30579449d --- /dev/null +++ b/components/sdk/src/queries/capital/getProgramExpenses.ts @@ -0,0 +1,26 @@ +import { programExpensesPaginationSelector } from '../../selectors/capital/paginationSelectors' +import { $, type GraphQLTypes, type InputType, type ModelTypes, Selector } from '../../zeus/index' + +export const name = 'capitalProgramExpenses' + +/** + * Список программных расходов капитала через шасси expense. + */ +export const query = Selector('Query')({ + [name]: [ + { coopname: $('coopname', 'String!'), options: $('options', 'PaginationInput') }, + programExpensesPaginationSelector, + ], +}) + +export interface IInput { + /** + * @private + */ + [key: string]: unknown + + coopname: string + options?: ModelTypes['PaginationInput'] +} + +export type IOutput = InputType diff --git a/components/sdk/src/queries/capital/index.ts b/components/sdk/src/queries/capital/index.ts index defbb8a9f31..3e30a89d007 100644 --- a/components/sdk/src/queries/capital/index.ts +++ b/components/sdk/src/queries/capital/index.ts @@ -18,6 +18,10 @@ export * as GetExpense from './getExpense' export * as GetExpenses from './getExpenses' +export * as GetProgramExpense from './getProgramExpense' + +export * as GetProgramExpenses from './getProgramExpenses' + export * as GetInvest from './getInvest' export * as GetInvests from './getInvests' diff --git a/components/sdk/src/queries/expense/expenseFile.ts b/components/sdk/src/queries/expense/expenseFile.ts new file mode 100644 index 00000000000..94e05adc54e --- /dev/null +++ b/components/sdk/src/queries/expense/expenseFile.ts @@ -0,0 +1,19 @@ +import { expenseFileSelector } from '../../selectors/expense' +import { $, type GraphQLTypes, type InputType, Selector } from '../../zeus/index' + +export const name = 'expenseFile' + +export const query = Selector('Query')({ + [name]: [{ id: $('id', 'Int!') }, expenseFileSelector], +}) + +export interface IInput { + /** + * @private + */ + [key: string]: unknown + + id: number +} + +export type IOutput = InputType diff --git a/components/sdk/src/queries/expense/expenseFilesByItem.ts b/components/sdk/src/queries/expense/expenseFilesByItem.ts new file mode 100644 index 00000000000..6c4577bf256 --- /dev/null +++ b/components/sdk/src/queries/expense/expenseFilesByItem.ts @@ -0,0 +1,28 @@ +import { rawExpenseFileSelector } from '../../selectors/expense' +import { $, type GraphQLTypes, type InputType, Selector } from '../../zeus/index' + +export const name = 'expenseFilesByItem' + +export const query = Selector('Query')({ + [name]: [ + { + coopname: $('coopname', 'String!'), + proposal_hash: $('proposal_hash', 'String!'), + item_hash: $('item_hash', 'String!'), + }, + rawExpenseFileSelector, + ], +}) + +export interface IInput { + /** + * @private + */ + [key: string]: unknown + + coopname: string + proposal_hash: string + item_hash: string +} + +export type IOutput = InputType diff --git a/components/sdk/src/queries/expense/expenseFilesByProposal.ts b/components/sdk/src/queries/expense/expenseFilesByProposal.ts new file mode 100644 index 00000000000..f6d2e2fe2b3 --- /dev/null +++ b/components/sdk/src/queries/expense/expenseFilesByProposal.ts @@ -0,0 +1,23 @@ +import { rawExpenseFileSelector } from '../../selectors/expense' +import { $, type GraphQLTypes, type InputType, Selector } from '../../zeus/index' + +export const name = 'expenseFilesByProposal' + +export const query = Selector('Query')({ + [name]: [ + { coopname: $('coopname', 'String!'), proposal_hash: $('proposal_hash', 'String!') }, + rawExpenseFileSelector, + ], +}) + +export interface IInput { + /** + * @private + */ + [key: string]: unknown + + coopname: string + proposal_hash: string +} + +export type IOutput = InputType diff --git a/components/sdk/src/queries/expense/expenseProposal.ts b/components/sdk/src/queries/expense/expenseProposal.ts new file mode 100644 index 00000000000..10777955c3c --- /dev/null +++ b/components/sdk/src/queries/expense/expenseProposal.ts @@ -0,0 +1,19 @@ +import { expenseProposalSelector } from '../../selectors/expense' +import { $, type GraphQLTypes, type InputType, Selector } from '../../zeus/index' + +export const name = 'expenseProposal' + +export const query = Selector('Query')({ + [name]: [{ proposal_hash: $('proposal_hash', 'String!') }, expenseProposalSelector], +}) + +export interface IInput { + /** + * @private + */ + [key: string]: unknown + + proposal_hash: string +} + +export type IOutput = InputType diff --git a/components/sdk/src/queries/expense/expenseProposalsByCooperative.ts b/components/sdk/src/queries/expense/expenseProposalsByCooperative.ts new file mode 100644 index 00000000000..b4a4a3f33ed --- /dev/null +++ b/components/sdk/src/queries/expense/expenseProposalsByCooperative.ts @@ -0,0 +1,23 @@ +import { expenseProposalsPaginationSelector } from '../../selectors/expense' +import { $, type GraphQLTypes, type InputType, type ModelTypes, Selector } from '../../zeus/index' + +export const name = 'expenseProposalsByCooperative' + +export const query = Selector('Query')({ + [name]: [ + { coopname: $('coopname', 'String!'), options: $('options', 'PaginationInput') }, + expenseProposalsPaginationSelector, + ], +}) + +export interface IInput { + /** + * @private + */ + [key: string]: unknown + + coopname: string + options?: ModelTypes['PaginationInput'] +} + +export type IOutput = InputType diff --git a/components/sdk/src/queries/expense/expenseProposalsByMember.ts b/components/sdk/src/queries/expense/expenseProposalsByMember.ts new file mode 100644 index 00000000000..b13d07d5486 --- /dev/null +++ b/components/sdk/src/queries/expense/expenseProposalsByMember.ts @@ -0,0 +1,28 @@ +import { expenseProposalsPaginationSelector } from '../../selectors/expense' +import { $, type GraphQLTypes, type InputType, type ModelTypes, Selector } from '../../zeus/index' + +export const name = 'expenseProposalsByMember' + +export const query = Selector('Query')({ + [name]: [ + { + coopname: $('coopname', 'String!'), + username: $('username', 'String!'), + options: $('options', 'PaginationInput'), + }, + expenseProposalsPaginationSelector, + ], +}) + +export interface IInput { + /** + * @private + */ + [key: string]: unknown + + coopname: string + username: string + options?: ModelTypes['PaginationInput'] +} + +export type IOutput = InputType diff --git a/components/sdk/src/queries/expense/expenseRequisitesByProposal.ts b/components/sdk/src/queries/expense/expenseRequisitesByProposal.ts new file mode 100644 index 00000000000..94ef576d3ed --- /dev/null +++ b/components/sdk/src/queries/expense/expenseRequisitesByProposal.ts @@ -0,0 +1,27 @@ +import { rawExpenseRequisiteSelector } from '../../selectors/expense' +import { $, type GraphQLTypes, type InputType, Selector } from '../../zeus/index' + +export const name = 'expenseRequisitesByProposal' + +/** + * Снимки реквизитов получателей по строкам СЗ (только совет; персональные + * данные хранятся офчейн в БД шасси). + */ +export const query = Selector('Query')({ + [name]: [ + { coopname: $('coopname', 'String!'), proposal_hash: $('proposal_hash', 'String!') }, + rawExpenseRequisiteSelector, + ], +}) + +export interface IInput { + /** + * @private + */ + [key: string]: unknown + + coopname: string + proposal_hash: string +} + +export type IOutput = InputType diff --git a/components/sdk/src/queries/expense/index.ts b/components/sdk/src/queries/expense/index.ts new file mode 100644 index 00000000000..20a28ccd620 --- /dev/null +++ b/components/sdk/src/queries/expense/index.ts @@ -0,0 +1,7 @@ +export * as ExpenseProposal from './expenseProposal' +export * as ExpenseProposalsByCooperative from './expenseProposalsByCooperative' +export * as ExpenseProposalsByMember from './expenseProposalsByMember' +export * as ExpenseFile from './expenseFile' +export * as ExpenseFilesByItem from './expenseFilesByItem' +export * as ExpenseFilesByProposal from './expenseFilesByProposal' +export * as ExpenseRequisitesByProposal from './expenseRequisitesByProposal' diff --git a/components/sdk/src/queries/index.ts b/components/sdk/src/queries/index.ts index e3d7aecd195..5117465ffff 100644 --- a/components/sdk/src/queries/index.ts +++ b/components/sdk/src/queries/index.ts @@ -8,6 +8,7 @@ export * as Chairman from './chairman' export * as ChatCoop from './chatcoop' export * as Desktop from './desktop' export * as Documents from './documents' +export * as Expense from './expense' export * as Extensions from './extensions' export * as Gateway from './gateway' export * as Ledger from './ledger' diff --git a/components/sdk/src/selectors/capital/configSelector.ts b/components/sdk/src/selectors/capital/configSelector.ts index f2304881e3e..b23f4d3355f 100644 --- a/components/sdk/src/selectors/capital/configSelector.ts +++ b/components/sdk/src/selectors/capital/configSelector.ts @@ -6,6 +6,8 @@ const rawConfigSelector = { ...baseCapitalSelector, coopname: true, global_available_invest_pool: true, + program_expense_pool: true, + program_expense_reserved: true, program_membership_available: true, program_membership_cumulative_reward_per_share: true, program_membership_distributed: true, diff --git a/components/sdk/src/selectors/capital/index.ts b/components/sdk/src/selectors/capital/index.ts index f2f83f7863b..8993a185c39 100644 --- a/components/sdk/src/selectors/capital/index.ts +++ b/components/sdk/src/selectors/capital/index.ts @@ -5,6 +5,7 @@ export * from './contributorSelector' export * from './cycleSelector' export * from './debtSelector' export * from './expenseSelector' +export * from './programExpenseSelector' export * from './generateCapitalRegistrationDocumentsOutputSelector' export * from './investSelector' export * from './issueSelector' diff --git a/components/sdk/src/selectors/capital/paginationSelectors.ts b/components/sdk/src/selectors/capital/paginationSelectors.ts index f2d3a5e73e4..800245f6b83 100644 --- a/components/sdk/src/selectors/capital/paginationSelectors.ts +++ b/components/sdk/src/selectors/capital/paginationSelectors.ts @@ -7,6 +7,7 @@ import { rawContributorSelector } from './contributorSelector' import { rawCycleSelector } from './cycleSelector' import { rawDebtSelector } from './debtSelector' import { rawExpenseSelector } from './expenseSelector' +import { rawProgramExpenseSelector } from './programExpenseSelector' import { rawInvestSelector } from './investSelector' import { rawIssueSelector } from './issueSelector' import { rawLogSelector } from './logSelector' @@ -95,6 +96,12 @@ const _validateExpenses: MakeAllFieldsRequired = rawProgramExpensesPaginationSelector +export type programExpensesPaginationModel = ModelTypes['PaginatedCapitalProgramExpensesPaginationResult'] +export const programExpensesPaginationSelector = Selector('PaginatedCapitalProgramExpensesPaginationResult')(rawProgramExpensesPaginationSelector) + // Пагинированный селектор для записей времени const rawTimeEntriesPaginationSelector = { ...paginationSelector, items: rawTimeEntrySelector } const _validateTimeEntries: MakeAllFieldsRequired = rawTimeEntriesPaginationSelector diff --git a/components/sdk/src/selectors/capital/programExpenseSelector.ts b/components/sdk/src/selectors/capital/programExpenseSelector.ts new file mode 100644 index 00000000000..aec2b870752 --- /dev/null +++ b/components/sdk/src/selectors/capital/programExpenseSelector.ts @@ -0,0 +1,50 @@ +import type { MakeAllFieldsRequired } from '../../utils/MakeAllFieldsRequired' +import { type ModelTypes, Selector, type ValueTypes } from '../../zeus/index' + +const rawProgramExpenseItemSelector = { + item_hash: true, + mechanics: true, + recipient_type: true, + recipient: true, + recipient_name: true, + description: true, + planned_amount: true, + actual_amount: true, + status: true, +} + +const _validateItem: MakeAllFieldsRequired = rawProgramExpenseItemSelector + +export const programExpenseItemSelector = Selector('CapitalProgramExpenseItem')(rawProgramExpenseItemSelector) + +const rawProgramExpenseCallbackSelector = { + contract: true, + action: true, + data: true, +} + +const _validateCallback: MakeAllFieldsRequired = rawProgramExpenseCallbackSelector + +export const programExpenseCallbackSelector = Selector('CapitalProgramExpenseCallback')(rawProgramExpenseCallbackSelector) + +const rawProgramExpenseSelector = { + coopname: true, + expense_hash: true, + creator: true, + creator_name: true, + source_wallet: true, + status: true, + callback: rawProgramExpenseCallbackSelector, + items: rawProgramExpenseItemSelector, + total_planned: true, + total_actual: true, + created_at: true, + updated_at: true, +} + +const _validate: MakeAllFieldsRequired = rawProgramExpenseSelector + +export type programExpenseModel = ModelTypes['CapitalProgramExpense'] + +export const programExpenseSelector = Selector('CapitalProgramExpense')(rawProgramExpenseSelector) +export { rawProgramExpenseSelector } diff --git a/components/sdk/src/selectors/expense/expenseFileSelector.ts b/components/sdk/src/selectors/expense/expenseFileSelector.ts new file mode 100644 index 00000000000..336a70f86bb --- /dev/null +++ b/components/sdk/src/selectors/expense/expenseFileSelector.ts @@ -0,0 +1,22 @@ +import type { MakeAllFieldsRequired } from '../../utils/MakeAllFieldsRequired' +import { Selector, type ValueTypes } from '../../zeus/index' + +export const rawExpenseFileSelector = { + id: true, + coopname: true, + proposal_hash: true, + item_hash: true, + kind: true, + storage_key: true, + mime_type: true, + size_bytes: true, + checksum_sha256: true, + original_filename: true, + read_url: true, + uploaded_at: true, + uploaded_by_username: true, +} + +const _validate: MakeAllFieldsRequired = rawExpenseFileSelector + +export const expenseFileSelector = Selector('ExpenseFile')(rawExpenseFileSelector) diff --git a/components/sdk/src/selectors/expense/expenseItemSelector.ts b/components/sdk/src/selectors/expense/expenseItemSelector.ts new file mode 100644 index 00000000000..f4caa25d55b --- /dev/null +++ b/components/sdk/src/selectors/expense/expenseItemSelector.ts @@ -0,0 +1,17 @@ +import type { MakeAllFieldsRequired } from '../../utils/MakeAllFieldsRequired' +import { Selector, type ValueTypes } from '../../zeus/index' + +export const rawExpenseItemSelector = { + item_hash: true, + description: true, + planned_amount: true, + actual_amount: true, + mechanics: true, + recipient: true, + recipient_type: true, + status: true, +} + +const _validate: MakeAllFieldsRequired = rawExpenseItemSelector + +export const expenseItemSelector = Selector('ExpenseItem')(rawExpenseItemSelector) diff --git a/components/sdk/src/selectors/expense/expenseProposalSelector.ts b/components/sdk/src/selectors/expense/expenseProposalSelector.ts new file mode 100644 index 00000000000..2eed526461b --- /dev/null +++ b/components/sdk/src/selectors/expense/expenseProposalSelector.ts @@ -0,0 +1,30 @@ +import type { MakeAllFieldsRequired } from '../../utils/MakeAllFieldsRequired' +import { Selector, type ValueTypes } from '../../zeus/index' +import { rawDocumentAggregateSelector } from '../documents/documentAggregateSelector' +import { rawExpenseItemSelector } from './expenseItemSelector' + +export const rawExpenseProposalSelector = { + _id: true, + _created_at: true, + _updated_at: true, + proposal_hash: true, + coopname: true, + username: true, + source_wallet: true, + status: true, + blockchain_status: true, + total_planned: true, + total_actual: true, + created_at: true, + updated_at: true, + id: true, + block_num: true, + present: true, + items: rawExpenseItemSelector, + statement_doc: rawDocumentAggregateSelector, + decision_doc: rawDocumentAggregateSelector, +} + +const _validate: MakeAllFieldsRequired = rawExpenseProposalSelector + +export const expenseProposalSelector = Selector('ExpenseProposal')(rawExpenseProposalSelector) diff --git a/components/sdk/src/selectors/expense/expenseProposalsPaginationSelector.ts b/components/sdk/src/selectors/expense/expenseProposalsPaginationSelector.ts new file mode 100644 index 00000000000..f742ec8b6b6 --- /dev/null +++ b/components/sdk/src/selectors/expense/expenseProposalsPaginationSelector.ts @@ -0,0 +1,18 @@ +import type { MakeAllFieldsRequired } from '../../utils/MakeAllFieldsRequired' +import { paginationSelector } from '../../utils/paginationSelector' +import { type ModelTypes, Selector, type ValueTypes } from '../../zeus/index' +import { rawExpenseProposalSelector } from './expenseProposalSelector' + +export const rawExpenseProposalsPaginationSelector = { + ...paginationSelector, + items: rawExpenseProposalSelector, +} + +const _validate: MakeAllFieldsRequired + = rawExpenseProposalsPaginationSelector + +export type expenseProposalsPaginationModel = ModelTypes['PaginatedExpenseProposalsPaginationResult'] + +export const expenseProposalsPaginationSelector = Selector('PaginatedExpenseProposalsPaginationResult')( + rawExpenseProposalsPaginationSelector, +) diff --git a/components/sdk/src/selectors/expense/expenseReportResultSelector.ts b/components/sdk/src/selectors/expense/expenseReportResultSelector.ts new file mode 100644 index 00000000000..e86a8aa985c --- /dev/null +++ b/components/sdk/src/selectors/expense/expenseReportResultSelector.ts @@ -0,0 +1,19 @@ +import { rawTransactionSelector } from '../common/transactionSelector' +import { type GraphQLTypes, type InputType, Selector } from '../../zeus/index' + +// Результат отчёта по строке-авансу: исход (закрыто / ожидается возврат / доплата), +// сумма разницы к расчёту и хэш заведённой платёжки расчёта; transaction заполнен +// только при CLOSED (on-chain reportexp). +export const rawExpenseReportResultSelector = { + outcome: true, + settlement_amount: true, + settlement_payment_hash: true, + transaction: rawTransactionSelector, +} + +export const expenseReportResultSelector = Selector('ExpenseReportResult')(rawExpenseReportResultSelector) + +export type ExpenseReportResultModel = InputType< + GraphQLTypes['ExpenseReportResult'], + typeof rawExpenseReportResultSelector +> diff --git a/components/sdk/src/selectors/expense/expenseRequisiteSelector.ts b/components/sdk/src/selectors/expense/expenseRequisiteSelector.ts new file mode 100644 index 00000000000..20ea83b23e8 --- /dev/null +++ b/components/sdk/src/selectors/expense/expenseRequisiteSelector.ts @@ -0,0 +1,17 @@ +import type { MakeAllFieldsRequired } from '../../utils/MakeAllFieldsRequired' +import { Selector, type ValueTypes } from '../../zeus/index' + +export const rawExpenseRequisiteSelector = { + coopname: true, + proposal_hash: true, + item_hash: true, + recipient: true, + method_type: true, + data: true, + requisites: true, + payment_purpose: true, +} + +const _validate: MakeAllFieldsRequired = rawExpenseRequisiteSelector + +export const expenseRequisiteSelector = Selector('ExpenseRequisite')(rawExpenseRequisiteSelector) diff --git a/components/sdk/src/selectors/expense/index.ts b/components/sdk/src/selectors/expense/index.ts new file mode 100644 index 00000000000..2718ac0391c --- /dev/null +++ b/components/sdk/src/selectors/expense/index.ts @@ -0,0 +1,6 @@ +export * from './expenseFileSelector' +export * from './expenseItemSelector' +export * from './expenseRequisiteSelector' +export * from './expenseProposalSelector' +export * from './expenseProposalsPaginationSelector' +export * from './expenseReportResultSelector' diff --git a/components/sdk/src/selectors/index.ts b/components/sdk/src/selectors/index.ts index 14dcdd2877d..6dbb7327618 100644 --- a/components/sdk/src/selectors/index.ts +++ b/components/sdk/src/selectors/index.ts @@ -5,6 +5,7 @@ export * from './capital' export * from './chatcoop' export * from './common' export * from './cooplace' +export * from './expense' export * from './extensions' export * from './freeDecisions' export * from './gateway' diff --git a/components/sdk/src/zeus/const.ts b/components/sdk/src/zeus/const.ts index 63d4ca66150..2aef7280b32 100644 --- a/components/sdk/src/zeus/const.ts +++ b/components/sdk/src/zeus/const.ts @@ -149,6 +149,10 @@ export const AllTypesProps: Record = { CapitalContributorFilter:{ status:"ContributorStatus" }, + CapitalCreateProgramExpenseInput:{ + items:"ExpenseItemInput", + statement:"ExpenseProposalStatementSignedDocumentInput" + }, CapitalCycleFilter:{ status:"CycleStatus" }, @@ -185,6 +189,9 @@ export const AllTypesProps: Record = { }, CapitalTimeStatsInput:{ + }, + CapitalTopupProgramExpenseInput:{ + }, ChairmanOnboardingAgendaInput:{ step:"ChairmanOnboardingAgendaStep" @@ -296,6 +303,11 @@ export const AllTypesProps: Record = { CreateExpenseInput:{ statement:"SignedDigitalDocumentInput" }, + CreateExpenseProposalInput:{ + callback:"ExpenseCallbackInput", + items:"ExpenseItemInput", + statement:"ExpenseProposalStatementSignedDocumentInput" + }, CreateIndividualDataInput:{ passport:"PassportInput" }, @@ -431,9 +443,57 @@ export const AllTypesProps: Record = { EntrepreneurDetailsInput:{ }, + ExpenseCallbackInput:{ + + }, + ExpenseFileKind: "enum" as const, ExpenseFilter:{ }, + ExpenseItemInput:{ + mechanics:"ExpenseMechanics", + recipient_type:"ExpenseRecipientType" + }, + ExpenseItemStatus: "enum" as const, + ExpenseMechanics: "enum" as const, + ExpenseProposalDecisionBodyInput:{ + + }, + ExpenseProposalDecisionGenerateDocumentInput:{ + items:"ExpenseProposalDecisionItemInput", + proposal:"ExpenseProposalDecisionHeaderInput", + resolution:"ExpenseProposalDecisionBodyInput" + }, + ExpenseProposalDecisionHeaderInput:{ + + }, + ExpenseProposalDecisionItemInput:{ + + }, + ExpenseProposalHeaderInput:{ + + }, + ExpenseProposalItemInput:{ + + }, + ExpenseProposalSignedItemInput:{ + + }, + ExpenseProposalStatementGenerateDocumentInput:{ + items:"ExpenseProposalItemInput", + proposal:"ExpenseProposalHeaderInput" + }, + ExpenseProposalStatementSignedDocumentInput:{ + meta:"ExpenseProposalStatementSignedMetaDocumentInput", + signatures:"SignatureInfoInput" + }, + ExpenseProposalStatementSignedMetaDocumentInput:{ + items:"ExpenseProposalSignedItemInput", + proposal:"ExpenseProposalHeaderInput" + }, + ExpenseProposalStatus: "enum" as const, + ExpenseRecipientType: "enum" as const, + ExpenseReportOutcome: "enum" as const, ExpenseStatus: "enum" as const, ExtendedMeetStatus: "enum" as const, ExtensionInput:{ @@ -721,6 +781,9 @@ export const AllTypesProps: Record = { capitalCreateProcessTemplate:{ data:"CreateProcessTemplateInput" }, + capitalCreateProgramExpense:{ + data:"CapitalCreateProgramExpenseInput" + }, capitalCreateProgramInvest:{ data:"CreateProgramInvestInput" }, @@ -914,6 +977,9 @@ export const AllTypesProps: Record = { capitalSubmitVote:{ data:"SubmitVoteInput" }, + capitalTopupProgramExpensePool:{ + data:"CapitalTopupProgramExpenseInput" + }, capitalUpdateIssue:{ data:"UpdateIssueInput" }, @@ -989,6 +1055,9 @@ export const AllTypesProps: Record = { createDepositPayment:{ data:"CreateDepositPaymentInput" }, + createExpenseProposal:{ + data:"CreateExpenseProposalInput" + }, createInitialPayment:{ data:"CreateInitialPaymentInput" }, @@ -1078,6 +1147,14 @@ export const AllTypesProps: Record = { generateDocument:{ input:"GenerateAnyDocumentInput" }, + generateExpenseProposalDecisionDocument:{ + data:"ExpenseProposalDecisionGenerateDocumentInput", + options:"GenerateDocumentOptionsInput" + }, + generateExpenseProposalStatementDocument:{ + data:"ExpenseProposalStatementGenerateDocumentInput", + options:"GenerateDocumentOptionsInput" + }, generateFreeDecision:{ data:"FreeDecisionGenerateDocumentInput", options:"GenerateDocumentOptionsInput" @@ -1182,6 +1259,12 @@ export const AllTypesProps: Record = { notifyOnAnnualGeneralMeet:{ data:"NotifyOnAnnualGeneralMeetInput" }, + overspendExpenseItem:{ + data:"OverspendExpenseItemInput" + }, + payExpenseItem:{ + data:"PayExpenseItemInput" + }, processConvertToAxonStatement:{ data:"ProcessConvertToAxonStatementInput" }, @@ -1206,6 +1289,9 @@ export const AllTypesProps: Record = { registerParticipant:{ data:"RegisterParticipantInput" }, + reportExpenseItem:{ + data:"ReportExpenseItemInput" + }, resendNotification:{ }, @@ -1215,6 +1301,9 @@ export const AllTypesProps: Record = { restartAnnualGeneralMeet:{ data:"RestartAnnualGeneralMeetInput" }, + returnExpenseItem:{ + data:"ReturnExpenseItemInput" + }, saveReportDraft:{ input:"SaveReportDraftInput" }, @@ -1242,6 +1331,9 @@ export const AllTypesProps: Record = { startResetKey:{ data:"StartResetKeyInput" }, + submitExpenseReport:{ + data:"SubmitExpenseReportInput" + }, supplyOnRequest:{ data:"SupplyOnRequestInput" }, @@ -1275,6 +1367,9 @@ export const AllTypesProps: Record = { updateSystem:{ data:"Update" }, + uploadExpenseFile:{ + data:"UploadExpenseFileInput" + }, verifyEmail:{ data:"VerifyEmailInputDTO" }, @@ -1306,6 +1401,9 @@ export const AllTypesProps: Record = { }, OrganizationType: "enum" as const, + OverspendExpenseItemInput:{ + + }, PaginationInput:{ }, @@ -1324,6 +1422,9 @@ export const AllTypesProps: Record = { }, PassportInput:{ + }, + PayExpenseItemInput:{ + }, PaymentDirection: "enum" as const, PaymentFiltersInput:{ @@ -1469,6 +1570,12 @@ export const AllTypesProps: Record = { filter:"CapitalIssueFilter", options:"PaginationInput" }, + capitalProgramExpense:{ + + }, + capitalProgramExpenses:{ + options:"PaginationInput" + }, capitalProject:{ data:"GetProjectInput" }, @@ -1558,6 +1665,27 @@ export const AllTypesProps: Record = { }, cooperativePrograms:{ + }, + expenseFile:{ + + }, + expenseFilesByItem:{ + + }, + expenseFilesByProposal:{ + + }, + expenseProposal:{ + + }, + expenseProposalsByCooperative:{ + options:"PaginationInput" + }, + expenseProposalsByMember:{ + options:"PaginationInput" + }, + expenseRequisitesByProposal:{ + }, getAccount:{ data:"GetAccountInput" @@ -1744,6 +1872,9 @@ export const AllTypesProps: Record = { }, RemoveSecretaryRoomInput:{ + }, + ReportExpenseItemInput:{ + }, ReportHistoryFilterInput:{ reportType:"ReportType" @@ -1813,6 +1944,9 @@ export const AllTypesProps: Record = { }, ReturnByMoneySignedMetaDocumentInput:{ + }, + ReturnExpenseItemInput:{ + }, RoomMessageKind: "enum" as const, SaveReportDraftInput:{ @@ -1911,6 +2045,9 @@ export const AllTypesProps: Record = { }, StoryStatus: "enum" as const, + SubmitExpenseReportInput:{ + + }, SubmitVoteInput:{ votes:"VoteDistributionInput" }, @@ -1981,6 +2118,9 @@ export const AllTypesProps: Record = { content_format:"CapitalStoryContentFormat", status:"StoryStatus" }, + UploadExpenseFileInput:{ + kind:"ExpenseFileKind" + }, UserStatus: "enum" as const, VarsInput:{ coopenomics_agreement:"AgreementInput", @@ -2649,6 +2789,36 @@ export const ReturnTypes: Record = { onboarding_generator_program_template_hash:"String", onboarding_init_at:"String" }, + CapitalProgramExpense:{ + callback:"CapitalProgramExpenseCallback", + coopname:"String", + created_at:"String", + creator:"String", + creator_name:"String", + expense_hash:"String", + items:"CapitalProgramExpenseItem", + source_wallet:"String", + status:"ExpenseProposalStatus", + total_actual:"String", + total_planned:"String", + updated_at:"String" + }, + CapitalProgramExpenseCallback:{ + action:"String", + contract:"String", + data:"String" + }, + CapitalProgramExpenseItem:{ + actual_amount:"String", + description:"String", + item_hash:"String", + mechanics:"ExpenseMechanics", + planned_amount:"String", + recipient:"String", + recipient_name:"String", + recipient_type:"ExpenseRecipientType", + status:"ExpenseItemStatus" + }, CapitalProject:{ _created_at:"DateTime", _id:"String", @@ -2904,6 +3074,8 @@ export const ReturnTypes: Record = { coopname:"String", global_available_invest_pool:"String", present:"Boolean", + program_expense_pool:"String", + program_expense_reserved:"String", program_membership_available:"String", program_membership_cumulative_reward_per_share:"Float", program_membership_distributed:"String", @@ -3220,6 +3392,68 @@ export const ReturnTypes: Record = { inn:"String", ogrn:"String" }, + ExpenseFile:{ + checksum_sha256:"String", + coopname:"String", + id:"Int", + item_hash:"String", + kind:"ExpenseFileKind", + mime_type:"String", + original_filename:"String", + proposal_hash:"String", + read_url:"String", + size_bytes:"Int", + storage_key:"String", + uploaded_at:"DateTime", + uploaded_by_username:"String" + }, + ExpenseItem:{ + actual_amount:"String", + description:"String", + item_hash:"String", + mechanics:"ExpenseMechanics", + planned_amount:"String", + recipient:"String", + recipient_type:"ExpenseRecipientType", + status:"ExpenseItemStatus" + }, + ExpenseProposal:{ + _created_at:"DateTime", + _id:"String", + _updated_at:"DateTime", + block_num:"Float", + blockchain_status:"Int", + coopname:"String", + created_at:"String", + decision_doc:"DocumentAggregate", + id:"Int", + items:"ExpenseItem", + present:"Boolean", + proposal_hash:"String", + source_wallet:"String", + statement_doc:"DocumentAggregate", + status:"ExpenseProposalStatus", + total_actual:"String", + total_planned:"String", + updated_at:"String", + username:"String" + }, + ExpenseReportResult:{ + outcome:"ExpenseReportOutcome", + settlement_amount:"String", + settlement_payment_hash:"String", + transaction:"Transaction" + }, + ExpenseRequisite:{ + coopname:"String", + data:"JSON", + item_hash:"String", + method_type:"String", + payment_purpose:"String", + proposal_hash:"String", + recipient:"String", + requisites:"String" + }, ExtendedBlockchainAction:{ account:"String", account_ram_deltas:"AccountRamDelta", @@ -3643,6 +3877,7 @@ export const ReturnTypes: Record = { capitalCreateExpense:"Transaction", capitalCreateIssue:"CapitalIssue", capitalCreateProcessTemplate:"ProcessTemplate", + capitalCreateProgramExpense:"Transaction", capitalCreateProgramInvest:"Transaction", capitalCreateProgramProperty:"Transaction", capitalCreateProject:"Transaction", @@ -3700,6 +3935,7 @@ export const ReturnTypes: Record = { capitalStartVoting:"Transaction", capitalStopProject:"CapitalProject", capitalSubmitVote:"Transaction", + capitalTopupProgramExpensePool:"Transaction", capitalUpdateIssue:"CapitalIssue", capitalUpdateProcessTemplate:"ProcessTemplate", capitalUpdateStory:"CapitalStory", @@ -3726,6 +3962,7 @@ export const ReturnTypes: Record = { createBranch:"Branch", createChildOrder:"Transaction", createDepositPayment:"GatewayPayment", + createExpenseProposal:"Transaction", createInitialPayment:"GatewayPayment", createMembershipExit:"MembershipExitResult", createParentOffer:"Transaction", @@ -3753,6 +3990,8 @@ export const ReturnTypes: Record = { generateBallotForAnnualGeneralMeetDocument:"GeneratedDocument", generateConvertToAxonStatement:"GeneratedDocument", generateDocument:"GeneratedDocument", + generateExpenseProposalDecisionDocument:"GeneratedDocument", + generateExpenseProposalStatementDocument:"GeneratedDocument", generateFreeDecision:"GeneratedDocument", generateMembershipExitApplication:"GeneratedDocument", generateMembershipExitDecision:"GeneratedDocument", @@ -3782,6 +4021,8 @@ export const ReturnTypes: Record = { markReportPeriod:"Boolean", moderateRequest:"Transaction", notifyOnAnnualGeneralMeet:"MeetAggregate", + overspendExpenseItem:"Transaction", + payExpenseItem:"Transaction", processConvertToAxonStatement:"Boolean", prohibitRequest:"Transaction", publishProjectOfFreeDecision:"AgendaWithDocuments", @@ -3790,10 +4031,12 @@ export const ReturnTypes: Record = { refresh:"RegisteredAccount", registerAccount:"RegisteredAccount", registerParticipant:"Account", + reportExpenseItem:"ExpenseReportResult", resendNotification:"Notification", resetKey:"Boolean", resetRegistration:"Account", restartAnnualGeneralMeet:"MeetAggregate", + returnExpenseItem:"Transaction", saveReportDraft:"ReportDraft", selectBranch:"Boolean", sendAgreement:"Transaction", @@ -3803,6 +4046,7 @@ export const ReturnTypes: Record = { signBySecretaryOnAnnualGeneralMeet:"MeetAggregate", startInstall:"StartInstallResult", startResetKey:"Boolean", + submitExpenseReport:"Transaction", supplyOnRequest:"Transaction", triggerNotificationWorkflow:"Boolean", uninstallExtension:"Boolean", @@ -3814,6 +4058,7 @@ export const ReturnTypes: Record = { updateRequest:"Transaction", updateSettings:"Settings", updateSystem:"SystemInfo", + uploadExpenseFile:"ExpenseFile", verifyEmail:"Boolean", voteOnAnnualGeneralMeet:"MeetAggregate", walmoveWallets:"Ledger2AdjustmentResult" @@ -3987,6 +4232,12 @@ export const ReturnTypes: Record = { totalCount:"Int", totalPages:"Int" }, + PaginatedCapitalProgramExpensesPaginationResult:{ + currentPage:"Int", + items:"CapitalProgramExpense", + totalCount:"Int", + totalPages:"Int" + }, PaginatedCapitalProjectsPaginationResult:{ currentPage:"Int", items:"CapitalProject", @@ -4047,6 +4298,12 @@ export const ReturnTypes: Record = { totalCount:"Int", totalPages:"Int" }, + PaginatedExpenseProposalsPaginationResult:{ + currentPage:"Int", + items:"ExpenseProposal", + totalCount:"Int", + totalPages:"Int" + }, PaginatedGatewayPaymentsPaginationResult:{ currentPage:"Int", items:"GatewayPayment", @@ -4305,6 +4562,8 @@ export const ReturnTypes: Record = { capitalInvests:"PaginatedCapitalInvestsPaginationResult", capitalIssue:"CapitalIssue", capitalIssues:"PaginatedCapitalIssuesPaginationResult", + capitalProgramExpense:"CapitalProgramExpense", + capitalProgramExpenses:"PaginatedCapitalProgramExpensesPaginationResult", capitalProject:"CapitalProject", capitalProjectWithRelations:"CapitalProject", capitalProjects:"PaginatedCapitalProjectsPaginationResult", @@ -4337,6 +4596,13 @@ export const ReturnTypes: Record = { checkReportReadiness:"ReportReadinessView", cooperativeAgreements:"CoopAgreement", cooperativePrograms:"CooperativeProgram", + expenseFile:"ExpenseFile", + expenseFilesByItem:"ExpenseFile", + expenseFilesByProposal:"ExpenseFile", + expenseProposal:"ExpenseProposal", + expenseProposalsByCooperative:"PaginatedExpenseProposalsPaginationResult", + expenseProposalsByMember:"PaginatedExpenseProposalsPaginationResult", + expenseRequisitesByProposal:"ExpenseRequisite", getAccount:"Account", getAccounts:"AccountsPaginationResult", getActions:"PaginatedActionsPaginationResult", diff --git a/components/sdk/src/zeus/index.ts b/components/sdk/src/zeus/index.ts index 30d88b8e550..99a7dbe823f 100644 --- a/components/sdk/src/zeus/index.ts +++ b/components/sdk/src/zeus/index.ts @@ -2634,6 +2634,20 @@ export type ValueTypes = { status?: ValueTypes["ContributorStatus"] | undefined | null | Variable, /** Фильтр по имени пользователя */ username?: string | undefined | null | Variable +}; + ["CapitalCreateProgramExpenseInput"]: { + /** Имя кооператива. */ + coopname: string | Variable, + /** Имя пайщика-создателя СЗ (председатель). */ + creator: string | Variable, + /** Описание программного расхода. */ + description: string | Variable, + /** Хэш СЗ-расхода (детерминированный, из UI). Он же станет proposal_hash в шасси. */ + expense_hash: string | Variable, + /** Строки расхода. Способ оплаты (аванс под отчёт / оплата по счёту) задаётся на каждой строке отдельно. */ + items: Array | Variable, + /** Подписанная СЗ-смета (document2, registry 2010). */ + statement: ValueTypes["ExpenseProposalStatementSignedDocumentInput"] | Variable }; /** Цикл разработки в системе CAPITAL */ ["CapitalCycle"]: AliasType<{ @@ -3015,6 +3029,44 @@ export type ValueTypes = { step: ValueTypes["CapitalOnboardingStep"] | Variable, title?: string | undefined | null | Variable }; + ["CapitalProgramExpense"]: AliasType<{ + callback?:ValueTypes["CapitalProgramExpenseCallback"], + coopname?:boolean | `@${string}`, + created_at?:boolean | `@${string}`, + creator?:boolean | `@${string}`, + /** Имя инициатора (ФИО пайщика или название организации) */ + creator_name?:boolean | `@${string}`, + expense_hash?:boolean | `@${string}`, + items?:ValueTypes["CapitalProgramExpenseItem"], + source_wallet?:boolean | `@${string}`, + status?:boolean | `@${string}`, + total_actual?:boolean | `@${string}`, + total_planned?:boolean | `@${string}`, + updated_at?:boolean | `@${string}`, + __typename?: boolean | `@${string}`, + ['...on CapitalProgramExpense']?: Omit +}>; + ["CapitalProgramExpenseCallback"]: AliasType<{ + action?:boolean | `@${string}`, + contract?:boolean | `@${string}`, + data?:boolean | `@${string}`, + __typename?: boolean | `@${string}`, + ['...on CapitalProgramExpenseCallback']?: Omit +}>; + ["CapitalProgramExpenseItem"]: AliasType<{ + actual_amount?:boolean | `@${string}`, + description?:boolean | `@${string}`, + item_hash?:boolean | `@${string}`, + mechanics?:boolean | `@${string}`, + planned_amount?:boolean | `@${string}`, + recipient?:boolean | `@${string}`, + /** Имя получателя (ФИО пайщика или название организации) */ + recipient_name?:boolean | `@${string}`, + recipient_type?:boolean | `@${string}`, + status?:boolean | `@${string}`, + __typename?: boolean | `@${string}`, + ['...on CapitalProgramExpenseItem']?: Omit +}>; /** Проект в системе CAPITAL с компонентами */ ["CapitalProject"]: AliasType<{ /** Дата создания записи */ @@ -3593,6 +3645,10 @@ export type ValueTypes = { global_available_invest_pool?:boolean | `@${string}`, /** Флаг присутствия записи в блокчейне */ present?:boolean | `@${string}`, + /** Пул программных расходов — средства, переведённые под целевые расходы программы */ + program_expense_pool?:boolean | `@${string}`, + /** Зарезервировано в пуле программных расходов под активные расходы (служебные записки в работе) */ + program_expense_reserved?:boolean | `@${string}`, /** Доступная сумма членских взносов по программе */ program_membership_available?:boolean | `@${string}`, /** Накопительное вознаграждение на долю в членских взносах */ @@ -3758,6 +3814,12 @@ export type ValueTypes = { project_hash?: string | undefined | null | Variable, /** Имя пользователя (опционально) */ username?: string | undefined | null | Variable +}; + ["CapitalTopupProgramExpenseInput"]: { + /** Сумма пополнения (asset, eg "10000.0000 RUB"). */ + amount: string | Variable, + /** Имя кооператива. */ + coopname: string | Variable }; /** Голос в системе CAPITAL */ ["CapitalVote"]: AliasType<{ @@ -4439,6 +4501,22 @@ export type ValueTypes = { project_hash: string | Variable, /** Служебная записка о расходе */ statement: ValueTypes["SignedDigitalDocumentInput"] | Variable +}; + ["CreateExpenseProposalInput"]: { + /** Callback на финализацию closeexp (опционально). */ + callback?: ValueTypes["ExpenseCallbackInput"] | undefined | null | Variable, + /** Имя кооператива. */ + coopname: string | Variable, + /** Строки расхода (массив items). */ + items: Array | Variable, + /** Хеш сметы расхода (детерминированный, из UI). */ + proposal_hash: string | Variable, + /** Источник средств (eosio::name кошелька-источника, eg "w.cap.blago"). */ + source_wallet: string | Variable, + /** Подписанная СЗ-смета (document2, registry 2010). */ + statement: ValueTypes["ExpenseProposalStatementSignedDocumentInput"] | Variable, + /** Имя пайщика-создателя СЗ. */ + username: string | Variable }; ["CreateIndividualDataInput"]: { /** Дата рождения */ @@ -4517,16 +4595,6 @@ export type ValueTypes = { ["CreateMatrixAccountInputDTO"]: { password: string | Variable, username: string | Variable -}; - ["CreateMembershipExitInput"]: { - /** Имя аккаунта кооператива */ - coopname: string | Variable, - /** Хеш процесса выхода (генерируется на клиенте) */ - exit_hash: string | Variable, - /** Подписанное пайщиком заявление о выходе из кооператива */ - statement: ValueTypes["MembershipExitApplicationSignedDocumentInput"] | Variable, - /** Имя пайщика, выходящего из кооператива */ - username: string | Variable }; ["CreateOrganizationDataInput"]: { /** Банковский счет организации */ @@ -5137,6 +5205,47 @@ export type ValueTypes = { /** ОГРН */ ogrn: string | Variable }; + ["ExpenseCallbackInput"]: { + /** Action-метод */ + action?: string | undefined | null | Variable, + /** Контракт-целевой */ + contract?: string | undefined | null | Variable, + /** Payload (hex bytes) */ + data?: string | undefined | null | Variable +}; + /** Запись о первичном файле расхода (платёжка/чек/возврат). */ +["ExpenseFile"]: AliasType<{ + /** SHA-256 содержимого, hex-lowercase. */ + checksum_sha256?:boolean | `@${string}`, + /** Имя кооператива (scope). */ + coopname?:boolean | `@${string}`, + /** Внутренний ID записи. */ + id?:boolean | `@${string}`, + /** Хеш строки расхода (если файл уровня item). */ + item_hash?:boolean | `@${string}`, + /** Назначение файла. */ + kind?:boolean | `@${string}`, + /** MIME-тип содержимого. */ + mime_type?:boolean | `@${string}`, + /** Оригинальное имя загруженного файла. */ + original_filename?:boolean | `@${string}`, + /** Хеш сметы расхода. */ + proposal_hash?:boolean | `@${string}`, + /** Короткоживущий URL на скачивание (HMAC-signed). */ + read_url?:boolean | `@${string}`, + /** Размер файла в байтах. */ + size_bytes?:boolean | `@${string}`, + /** MinIO-ключ внутри бакета. */ + storage_key?:boolean | `@${string}`, + /** Когда загружено. */ + uploaded_at?:boolean | `@${string}`, + /** Кто загрузил (username). */ + uploaded_by_username?:boolean | `@${string}`, + __typename?: boolean | `@${string}`, + ['...on ExpenseFile']?: Omit +}>; + /** Тип первичного файла расхода. */ +["ExpenseFileKind"]:ExpenseFileKind; ["ExpenseFilter"]: { /** Фильтр по ID фонда */ fundId?: string | undefined | null | Variable, @@ -5147,6 +5256,307 @@ export type ValueTypes = { /** Фильтр по имени пользователя */ username?: string | undefined | null | Variable }; + /** Строка сметы расхода. */ +["ExpenseItem"]: AliasType<{ + /** Фактическая сумма (после оплаты/отчёта). */ + actual_amount?:boolean | `@${string}`, + /** Назначение/описание строки. */ + description?:boolean | `@${string}`, + /** Хеш строки расхода. */ + item_hash?:boolean | `@${string}`, + /** Способ оплаты. */ + mechanics?:boolean | `@${string}`, + /** Планируемая сумма. */ + planned_amount?:boolean | `@${string}`, + /** Идентификатор получателя. */ + recipient?:boolean | `@${string}`, + /** Тип получателя платежа. */ + recipient_type?:boolean | `@${string}`, + /** Статус строки. */ + status?:boolean | `@${string}`, + __typename?: boolean | `@${string}`, + ['...on ExpenseItem']?: Omit +}>; + ["ExpenseItemInput"]: { + /** Описание назначения расхода. */ + description: string | Variable, + /** Хеш строки расхода (детерминированный, из UI). */ + item_hash: string | Variable, + /** Способ оплаты (ADVANCE / DIRECT). */ + mechanics: ValueTypes["ExpenseMechanics"] | Variable, + /** Идентификатор сохранённых реквизитов получателя-пайщика — реквизиты снимаются в момент создания и прикладываются к платежу. */ + payment_method_id?: string | undefined | null | Variable, + /** Назначение платежа (для оплаты по счёту) — фиксируется в снимке для кассира. */ + payment_purpose?: string | undefined | null | Variable, + /** Планируемая сумма (asset, eg "1000.0000 RUB"). */ + planned_amount: string | Variable, + /** Получатель: username пайщика; для организации — пустая строка (аккаунта в кооперативе нет). */ + recipient: string | Variable, + /** Тип получателя. */ + recipient_type: ValueTypes["ExpenseRecipientType"] | Variable, + /** Реквизиты получателя-организации (вводятся вручную). */ + requisites?: string | undefined | null | Variable +}; + /** Статус строки расхода. */ +["ExpenseItemStatus"]:ExpenseItemStatus; + /** Способ оплаты строки расхода. */ +["ExpenseMechanics"]:ExpenseMechanics; + /** Смета расхода (СЗ). */ +["ExpenseProposal"]: AliasType<{ + /** Дата создания записи */ + _created_at?:boolean | `@${string}`, + /** Внутренний ID базы данных */ + _id?:boolean | `@${string}`, + /** Дата последнего обновления записи */ + _updated_at?:boolean | `@${string}`, + /** Номер блока крайней синхронизации с блокчейном */ + block_num?:boolean | `@${string}`, + /** Сырой статус из блокчейна (uint8). */ + blockchain_status?:boolean | `@${string}`, + /** Имя кооператива (scope). */ + coopname?:boolean | `@${string}`, + /** Время создания (chain). */ + created_at?:boolean | `@${string}`, + /** Решение совета о расходе (DocumentAggregate). */ + decision_doc?:ValueTypes["DocumentAggregate"], + /** ID в блокчейне. */ + id?:boolean | `@${string}`, + /** Строки сметы. */ + items?:ValueTypes["ExpenseItem"], + /** Флаг присутствия записи в блокчейне */ + present?:boolean | `@${string}`, + /** Хеш сметы расхода. */ + proposal_hash?:boolean | `@${string}`, + /** Кошелёк-источник средств. */ + source_wallet?:boolean | `@${string}`, + /** Заявление пайщика о расходе (DocumentAggregate). */ + statement_doc?:ValueTypes["DocumentAggregate"], + /** Доменный статус сметы. */ + status?:boolean | `@${string}`, + /** Сумма всех строк (факт). */ + total_actual?:boolean | `@${string}`, + /** Сумма всех строк (план). */ + total_planned?:boolean | `@${string}`, + /** Время последнего обновления (chain). */ + updated_at?:boolean | `@${string}`, + /** Создатель сметы (username). */ + username?:boolean | `@${string}`, + __typename?: boolean | `@${string}`, + ['...on ExpenseProposal']?: Omit +}>; + ["ExpenseProposalDecisionBodyInput"]: { + /** Род решения (approve / decline) */ + kind: string | Variable, + /** Причина отказа (для decline) */ + reason?: string | undefined | null | Variable +}; + ["ExpenseProposalDecisionGenerateDocumentInput"]: { + /** Номер блока, на котором был создан документ */ + block_num?: number | undefined | null | Variable, + /** Название кооператива, связанное с документом */ + coopname: string | Variable, + /** Дата и время создания документа */ + created_at?: string | undefined | null | Variable, + /** Идентификатор решения совета (повестка) — источник данных голосования */ + decision_id: number | Variable, + /** Имя генератора, использованного для создания документа */ + generator?: string | undefined | null | Variable, + /** Позиции расхода */ + items: Array | Variable, + /** Язык документа */ + lang?: string | undefined | null | Variable, + /** Ссылки, связанные с документом */ + links?: Array | undefined | null | Variable, + /** Шапка СЗ */ + proposal: ValueTypes["ExpenseProposalDecisionHeaderInput"] | Variable, + /** Хеш сметы расхода */ + proposal_hash: string | Variable, + /** Резолюция совета (утвердить / отказать) */ + resolution: ValueTypes["ExpenseProposalDecisionBodyInput"] | Variable, + /** Часовой пояс, в котором был создан документ */ + timezone?: string | undefined | null | Variable, + /** Название документа */ + title?: string | undefined | null | Variable, + /** Имя пользователя, создавшего документ */ + username: string | Variable, + /** Версия генератора, использованного для создания документа */ + version?: string | undefined | null | Variable +}; + ["ExpenseProposalDecisionHeaderInput"]: { + deadline?: string | undefined | null | Variable, + description: string | Variable, + fund_name?: string | undefined | null | Variable, + items_count: number | Variable, + source_wallet: string | Variable, + total_amount: string | Variable +}; + ["ExpenseProposalDecisionItemInput"]: { + amount: string | Variable, + description: string | Variable, + mechanics: string | Variable, + number: string | Variable, + recipient_type: string | Variable +}; + ["ExpenseProposalHeaderInput"]: { + /** Срок исполнения («в срок до»), формат DD.MM.YYYY */ + deadline?: string | undefined | null | Variable, + /** Описание цели расходов */ + description: string | Variable, + /** Фонд списания — подставляется сервером из параметров шасси расходов, передавать не нужно */ + fund_name?: string | undefined | null | Variable, + /** Количество позиций */ + items_count: number | Variable, + /** Кошелёк-источник */ + source_wallet: string | Variable, + /** Итоговая сумма расходов */ + total_amount: string | Variable +}; + ["ExpenseProposalItemInput"]: { + /** Сумма строки */ + amount: string | Variable, + /** Описание расхода */ + description: string | Variable, + /** Способ оплаты (ADVANCE / DIRECT) */ + mechanics: string | Variable, + /** Порядковый номер строки */ + number: string | Variable, + /** Идентификатор сохранённых реквизитов получателя-пайщика — сервер подставит полные реквизиты в документ. */ + payment_method_id?: string | undefined | null | Variable, + /** Назначение платежа — отдельной строкой после реквизитов */ + payment_purpose?: string | undefined | null | Variable, + /** Имя получателя */ + recipient_name?: string | undefined | null | Variable, + /** Тип получателя (SELF / MEMBER / ORG) */ + recipient_type: string | Variable, + /** Имя аккаунта получателя-пайщика (владелец реквизитов). */ + recipient_username?: string | undefined | null | Variable, + /** Реквизиты получателя */ + requisites?: string | undefined | null | Variable +}; + ["ExpenseProposalSignedItemInput"]: { + /** Сумма строки */ + amount: string | Variable, + /** Описание расхода */ + description: string | Variable, + /** Способ оплаты (ADVANCE / DIRECT) */ + mechanics: string | Variable, + /** Порядковый номер строки */ + number: string | Variable, + /** Тип получателя (SELF / MEMBER / ORG) */ + recipient_type: string | Variable +}; + ["ExpenseProposalStatementGenerateDocumentInput"]: { + /** Номер блока, на котором был создан документ */ + block_num?: number | undefined | null | Variable, + /** Название кооператива, связанное с документом */ + coopname: string | Variable, + /** Дата и время создания документа */ + created_at?: string | undefined | null | Variable, + /** Имя генератора, использованного для создания документа */ + generator?: string | undefined | null | Variable, + /** Позиции расхода */ + items: Array | Variable, + /** Язык документа */ + lang?: string | undefined | null | Variable, + /** Ссылки, связанные с документом */ + links?: Array | undefined | null | Variable, + /** Шапка СЗ */ + proposal: ValueTypes["ExpenseProposalHeaderInput"] | Variable, + /** Хеш сметы расхода (детерминированный) */ + proposal_hash: string | Variable, + /** Часовой пояс, в котором был создан документ */ + timezone?: string | undefined | null | Variable, + /** Название документа */ + title?: string | undefined | null | Variable, + /** Имя пользователя, создавшего документ */ + username: string | Variable, + /** Версия генератора, использованного для создания документа */ + version?: string | undefined | null | Variable +}; + ["ExpenseProposalStatementSignedDocumentInput"]: { + /** Хэш содержимого документа */ + doc_hash: string | Variable, + /** Общий хэш (doc_hash + meta_hash) */ + hash: string | Variable, + /** Метаинформация СЗ-заявления */ + meta: ValueTypes["ExpenseProposalStatementSignedMetaDocumentInput"] | Variable, + /** Хэш мета-данных */ + meta_hash: string | Variable, + /** Вектор подписей */ + signatures: Array | Variable, + /** Версия стандарта документа */ + version: string | Variable +}; + ["ExpenseProposalStatementSignedMetaDocumentInput"]: { + /** Номер блока, на котором был создан документ */ + block_num: number | Variable, + /** Название кооператива, связанное с документом */ + coopname: string | Variable, + /** Дата и время создания документа */ + created_at: string | Variable, + /** Идентификатор приватных данных документа off-chain (реквизиты/имя/назначение) */ + doc_data_hash: string | Variable, + /** Имя генератора, использованного для создания документа */ + generator: string | Variable, + /** Публичные позиции расхода (без реквизитов) */ + items: Array | Variable, + /** Язык документа */ + lang: string | Variable, + /** Ссылки, связанные с документом */ + links: Array | Variable, + /** Шапка СЗ */ + proposal: ValueTypes["ExpenseProposalHeaderInput"] | Variable, + /** Хеш сметы расхода (детерминированный) */ + proposal_hash: string | Variable, + /** ID документа в реестре */ + registry_id: number | Variable, + /** Часовой пояс, в котором был создан документ */ + timezone: string | Variable, + /** Название документа */ + title: string | Variable, + /** Имя пользователя, создавшего документ */ + username: string | Variable, + /** Версия генератора, использованного для создания документа */ + version: string | Variable +}; + /** Статус сметы расхода. */ +["ExpenseProposalStatus"]:ExpenseProposalStatus; + /** Тип получателя платежа. */ +["ExpenseRecipientType"]:ExpenseRecipientType; + /** Исход отчёта по строке-авансу: закрыто либо ожидается расчёт разницы (возврат/доплата). */ +["ExpenseReportOutcome"]:ExpenseReportOutcome; + ["ExpenseReportResult"]: AliasType<{ + /** Исход отчёта. */ + outcome?:boolean | `@${string}`, + /** Сумма разницы к расчёту (asset), при недо-/перерасходе. */ + settlement_amount?:boolean | `@${string}`, + /** Хэш заведённой платёжки расчёта (возврат/доплата). */ + settlement_payment_hash?:boolean | `@${string}`, + /** Транзакция закрытия позиции (только при CLOSED). */ + transaction?:ValueTypes["Transaction"], + __typename?: boolean | `@${string}`, + ['...on ExpenseReportResult']?: Omit +}>; + ["ExpenseRequisite"]: AliasType<{ + /** Имя кооператива. */ + coopname?:boolean | `@${string}`, + /** Снимок данных платёжного метода на момент подачи СЗ. */ + data?:boolean | `@${string}`, + /** Хеш строки расхода (item). */ + item_hash?:boolean | `@${string}`, + /** Тип платёжного метода пайщика (СБП / банковский перевод). */ + method_type?:boolean | `@${string}`, + /** Назначение платежа для поручения кассиру. */ + payment_purpose?:boolean | `@${string}`, + /** Хеш сметы расхода (proposal). */ + proposal_hash?:boolean | `@${string}`, + /** Получатель платежа (аккаунт пайщика или название организации). */ + recipient?:boolean | `@${string}`, + /** Реквизиты строкой — как в документе служебной записки. */ + requisites?:boolean | `@${string}`, + __typename?: boolean | `@${string}`, + ['...on ExpenseRequisite']?: Omit +}>; /** Статус расхода в системе CAPITAL */ ["ExpenseStatus"]:ExpenseStatus; /** Расширенное действие блокчейна с сертификатом пользователя, совершившего его. */ @@ -6422,125 +6832,6 @@ export type ValueTypes = { __typename?: boolean | `@${string}`, ['...on MeetQuestionResult']?: Omit }>; - ["MembershipExit"]: AliasType<{ - /** Дата подачи заявления на выход */ - created_at?:boolean | `@${string}`, - /** Хеш процесса выхода */ - exit_hash?:boolean | `@${string}`, - /** Сумма к возврату (фиксируется советом при одобрении; до одобрения — 0) */ - quantity?:boolean | `@${string}`, - /** Статус процесса выхода */ - status?:boolean | `@${string}`, - __typename?: boolean | `@${string}`, - ['...on MembershipExit']?: Omit -}>; - ["MembershipExitApplicationGenerateDocumentInput"]: { - /** Номер блока, на котором был создан документ */ - block_num?: number | undefined | null | Variable, - /** Название кооператива, связанное с документом */ - coopname: string | Variable, - /** Дата и время создания документа */ - created_at?: string | undefined | null | Variable, - /** Имя генератора, использованного для создания документа */ - generator?: string | undefined | null | Variable, - /** Язык документа */ - lang?: string | undefined | null | Variable, - /** Ссылки, связанные с документом */ - links?: Array | undefined | null | Variable, - /** Флаг пропуска сохранения документа (используется для предварительной генерации и демонстрации пользователю) */ - skip_save: boolean | Variable, - /** Часовой пояс, в котором был создан документ */ - timezone?: string | undefined | null | Variable, - /** Название документа */ - title?: string | undefined | null | Variable, - /** Имя пользователя, создавшего документ */ - username: string | Variable, - /** Версия генератора, использованного для создания документа */ - version?: string | undefined | null | Variable -}; - ["MembershipExitApplicationSignedDocumentInput"]: { - /** Хэш содержимого документа */ - doc_hash: string | Variable, - /** Общий хэш (doc_hash + meta_hash) */ - hash: string | Variable, - meta: ValueTypes["MembershipExitApplicationSignedMetaDocumentInput"] | Variable, - /** Хэш мета-данных */ - meta_hash: string | Variable, - /** Вектор подписей */ - signatures: Array | Variable, - /** Версия стандарта документа */ - version: string | Variable -}; - ["MembershipExitApplicationSignedMetaDocumentInput"]: { - /** Номер блока, на котором был создан документ */ - block_num: number | Variable, - /** Название кооператива, связанное с документом */ - coopname: string | Variable, - /** Дата и время создания документа */ - created_at: string | Variable, - /** Имя генератора, использованного для создания документа */ - generator: string | Variable, - /** Язык документа */ - lang: string | Variable, - /** Ссылки, связанные с документом */ - links: Array | Variable, - /** ID документа в реестре */ - registry_id: number | Variable, - /** Флаг пропуска сохранения документа (используется для предварительной генерации и демонстрации пользователю) */ - skip_save: boolean | Variable, - /** Часовой пояс, в котором был создан документ */ - timezone: string | Variable, - /** Название документа */ - title: string | Variable, - /** Имя пользователя, создавшего документ */ - username: string | Variable, - /** Версия генератора, использованного для создания документа */ - version: string | Variable -}; - ["MembershipExitDecisionGenerateDocumentInput"]: { - /** Номер блока, на котором был создан документ */ - block_num?: number | undefined | null | Variable, - /** Название кооператива, связанное с документом */ - coopname: string | Variable, - /** Дата и время создания документа */ - created_at?: string | undefined | null | Variable, - /** Идентификатор протокола решения собрания совета */ - decision_id: number | Variable, - /** Имя генератора, использованного для создания документа */ - generator?: string | undefined | null | Variable, - /** Язык документа */ - lang?: string | undefined | null | Variable, - /** Ссылки, связанные с документом */ - links?: Array | undefined | null | Variable, - /** Часовой пояс, в котором был создан документ */ - timezone?: string | undefined | null | Variable, - /** Название документа */ - title?: string | undefined | null | Variable, - /** Имя пользователя, создавшего документ */ - username: string | Variable, - /** Версия генератора, использованного для создания документа */ - version?: string | undefined | null | Variable -}; - ["MembershipExitResult"]: AliasType<{ - /** Хеш созданного процесса выхода */ - exit_hash?:boolean | `@${string}`, - /** Статус процесса выхода после подачи (ожидает подтверждения по ссылке из письма) */ - status?:boolean | `@${string}`, - __typename?: boolean | `@${string}`, - ['...on MembershipExitResult']?: Omit -}>; - ["MembershipExitReturnPreview"]: AliasType<{ - /** Минимальный паевой взнос пайщика */ - minimum_contribution?:boolean | `@${string}`, - /** Целевой паевой взнос пайщика */ - share_contribution?:boolean | `@${string}`, - /** Итоговая сумма к возврату (минимальный + целевой паевой) */ - total?:boolean | `@${string}`, - __typename?: boolean | `@${string}`, - ['...on MembershipExitReturnPreview']?: Omit -}>; - /** Статус процесса выхода пайщика из кооператива */ -["MembershipExitStatus"]:MembershipExitStatus; ["MissingRequisiteField"]: AliasType<{ key?:boolean | `@${string}`, label?:boolean | `@${string}`, @@ -6603,7 +6894,6 @@ addParticipant?: [{ data: ValueTypes["AddParticipantInput"] | Variable},ValueTypes["PaymentMethod"]], addTrustedAccount?: [{ data: ValueTypes["AddTrustedAccountInput"] | Variable},ValueTypes["Branch"]], authorizeDecision?: [{ data: ValueTypes["AuthorizeDecisionInput"] | Variable},ValueTypes["Transaction"]], -cancelMembershipExit?: [{ coopname: string | Variable, username: string | Variable},boolean | `@${string}`], cancelRequest?: [{ data: ValueTypes["CancelRequestInput"] | Variable},ValueTypes["Transaction"]], capitalAddAuthor?: [{ data: ValueTypes["AddAuthorInput"] | Variable},ValueTypes["CapitalProject"]], capitalApproveCommit?: [{ data: ValueTypes["CommitApproveInput"] | Variable},ValueTypes["CapitalCommit"]], @@ -6619,6 +6909,7 @@ capitalCreateDebt?: [{ data: ValueTypes["CreateDebtInput"] | Variable},ValueTypes["Transaction"]], capitalCreateIssue?: [{ data: ValueTypes["CreateIssueInput"] | Variable},ValueTypes["CapitalIssue"]], capitalCreateProcessTemplate?: [{ data: ValueTypes["CreateProcessTemplateInput"] | Variable},ValueTypes["ProcessTemplate"]], +capitalCreateProgramExpense?: [{ data: ValueTypes["CapitalCreateProgramExpenseInput"] | Variable},ValueTypes["Transaction"]], capitalCreateProgramInvest?: [{ data: ValueTypes["CreateProgramInvestInput"] | Variable},ValueTypes["Transaction"]], capitalCreateProgramProperty?: [{ data: ValueTypes["CreateProgramPropertyInput"] | Variable},ValueTypes["Transaction"]], capitalCreateProject?: [{ data: ValueTypes["CreateProjectInput"] | Variable},ValueTypes["Transaction"]], @@ -6676,6 +6967,7 @@ capitalStartProject?: [{ data: ValueTypes["StartProjectInput"] | Variable},ValueTypes["Transaction"]], capitalStopProject?: [{ data: ValueTypes["StopProjectInput"] | Variable},ValueTypes["CapitalProject"]], capitalSubmitVote?: [{ data: ValueTypes["SubmitVoteInput"] | Variable},ValueTypes["Transaction"]], +capitalTopupProgramExpensePool?: [{ data: ValueTypes["CapitalTopupProgramExpenseInput"] | Variable},ValueTypes["Transaction"]], capitalUpdateIssue?: [{ data: ValueTypes["UpdateIssueInput"] | Variable},ValueTypes["CapitalIssue"]], capitalUpdateProcessTemplate?: [{ data: ValueTypes["UpdateProcessTemplateInput"] | Variable},ValueTypes["ProcessTemplate"]], capitalUpdateStory?: [{ data: ValueTypes["UpdateStoryInput"] | Variable},ValueTypes["CapitalStory"]], @@ -6698,15 +6990,14 @@ completeChairmanGeneralMeetStep?: [{ data: ValueTypes["ChairmanOnboardingGeneral completeExtensionOnboardingStep?: [{ data: ValueTypes["CompleteExtensionOnboardingStepInput"] | Variable},ValueTypes["ExtensionOnboardingState"]], completeRequest?: [{ data: ValueTypes["CompleteRequestInput"] | Variable},ValueTypes["Transaction"]], confirmAgreement?: [{ data: ValueTypes["ConfirmAgreementInput"] | Variable},ValueTypes["Transaction"]], -confirmMembershipExit?: [{ token: string | Variable},ValueTypes["MembershipExitResult"]], confirmReceiveOnRequest?: [{ data: ValueTypes["ConfirmReceiveOnRequestInput"] | Variable},ValueTypes["Transaction"]], confirmSupplyOnRequest?: [{ data: ValueTypes["ConfirmSupplyOnRequestInput"] | Variable},ValueTypes["Transaction"]], createAnnualGeneralMeet?: [{ data: ValueTypes["CreateAnnualGeneralMeetInput"] | Variable},ValueTypes["MeetAggregate"]], createBranch?: [{ data: ValueTypes["CreateBranchInput"] | Variable},ValueTypes["Branch"]], createChildOrder?: [{ data: ValueTypes["CreateChildOrderInput"] | Variable},ValueTypes["Transaction"]], createDepositPayment?: [{ data: ValueTypes["CreateDepositPaymentInput"] | Variable},ValueTypes["GatewayPayment"]], +createExpenseProposal?: [{ data: ValueTypes["CreateExpenseProposalInput"] | Variable},ValueTypes["Transaction"]], createInitialPayment?: [{ data: ValueTypes["CreateInitialPaymentInput"] | Variable},ValueTypes["GatewayPayment"]], -createMembershipExit?: [{ data: ValueTypes["CreateMembershipExitInput"] | Variable},ValueTypes["MembershipExitResult"]], createParentOffer?: [{ data: ValueTypes["CreateParentOfferInput"] | Variable},ValueTypes["Transaction"]], createProjectOfFreeDecision?: [{ data: ValueTypes["CreateProjectFreeDecisionInput"] | Variable},ValueTypes["CreatedProjectFreeDecision"]], createWebPushSubscription?: [{ data: ValueTypes["CreateSubscriptionInput"] | Variable},ValueTypes["CreateSubscriptionResponse"]], @@ -6732,9 +7023,9 @@ generateAssetContributionStatement?: [{ data: ValueTypes["AssetContributionState generateBallotForAnnualGeneralMeetDocument?: [{ data: ValueTypes["AnnualGeneralMeetingVotingBallotGenerateDocumentInput"] | Variable, options?: ValueTypes["GenerateDocumentOptionsInput"] | undefined | null | Variable},ValueTypes["GeneratedDocument"]], generateConvertToAxonStatement?: [{ data: ValueTypes["ConvertToAxonStatementGenerateDocumentInput"] | Variable, options?: ValueTypes["GenerateDocumentOptionsInput"] | undefined | null | Variable},ValueTypes["GeneratedDocument"]], generateDocument?: [{ input: ValueTypes["GenerateAnyDocumentInput"] | Variable},ValueTypes["GeneratedDocument"]], +generateExpenseProposalDecisionDocument?: [{ data: ValueTypes["ExpenseProposalDecisionGenerateDocumentInput"] | Variable, options?: ValueTypes["GenerateDocumentOptionsInput"] | undefined | null | Variable},ValueTypes["GeneratedDocument"]], +generateExpenseProposalStatementDocument?: [{ data: ValueTypes["ExpenseProposalStatementGenerateDocumentInput"] | Variable, options?: ValueTypes["GenerateDocumentOptionsInput"] | undefined | null | Variable},ValueTypes["GeneratedDocument"]], generateFreeDecision?: [{ data: ValueTypes["FreeDecisionGenerateDocumentInput"] | Variable, options?: ValueTypes["GenerateDocumentOptionsInput"] | undefined | null | Variable},ValueTypes["GeneratedDocument"]], -generateMembershipExitApplication?: [{ data: ValueTypes["MembershipExitApplicationGenerateDocumentInput"] | Variable, options?: ValueTypes["GenerateDocumentOptionsInput"] | undefined | null | Variable},ValueTypes["GeneratedDocument"]], -generateMembershipExitDecision?: [{ data: ValueTypes["MembershipExitDecisionGenerateDocumentInput"] | Variable, options?: ValueTypes["GenerateDocumentOptionsInput"] | undefined | null | Variable},ValueTypes["GeneratedDocument"]], generateParticipantApplication?: [{ data: ValueTypes["ParticipantApplicationGenerateDocumentInput"] | Variable, options?: ValueTypes["GenerateDocumentOptionsInput"] | undefined | null | Variable},ValueTypes["GeneratedDocument"]], generateParticipantApplicationDecision?: [{ data: ValueTypes["ParticipantApplicationDecisionGenerateDocumentInput"] | Variable, options?: ValueTypes["GenerateDocumentOptionsInput"] | undefined | null | Variable},ValueTypes["GeneratedDocument"]], generatePrivacyAgreement?: [{ data: ValueTypes["GenerateDocumentInput"] | Variable, options?: ValueTypes["GenerateDocumentOptionsInput"] | undefined | null | Variable},ValueTypes["GeneratedDocument"]], @@ -6761,6 +7052,8 @@ markNotificationRead?: [{ id: string | Variable},ValueTypes["InboxN markReportPeriod?: [{ data: ValueTypes["MarkReportPeriodInput"] | Variable},boolean | `@${string}`], moderateRequest?: [{ data: ValueTypes["ModerateRequestInput"] | Variable},ValueTypes["Transaction"]], notifyOnAnnualGeneralMeet?: [{ data: ValueTypes["NotifyOnAnnualGeneralMeetInput"] | Variable},ValueTypes["MeetAggregate"]], +overspendExpenseItem?: [{ data: ValueTypes["OverspendExpenseItemInput"] | Variable},ValueTypes["Transaction"]], +payExpenseItem?: [{ data: ValueTypes["PayExpenseItemInput"] | Variable},ValueTypes["Transaction"]], processConvertToAxonStatement?: [{ data: ValueTypes["ProcessConvertToAxonStatementInput"] | Variable},boolean | `@${string}`], prohibitRequest?: [{ data: ValueTypes["ProhibitRequestInput"] | Variable},ValueTypes["Transaction"]], publishProjectOfFreeDecision?: [{ data: ValueTypes["PublishProjectFreeDecisionInput"] | Variable},ValueTypes["AgendaWithDocuments"]], @@ -6769,11 +7062,13 @@ receiveOnRequest?: [{ data: ValueTypes["ReceiveOnRequestInput"] | Variable},ValueTypes["RegisteredAccount"]], registerAccount?: [{ data: ValueTypes["RegisterAccountInput"] | Variable},ValueTypes["RegisteredAccount"]], registerParticipant?: [{ data: ValueTypes["RegisterParticipantInput"] | Variable},ValueTypes["Account"]], +reportExpenseItem?: [{ data: ValueTypes["ReportExpenseItemInput"] | Variable},ValueTypes["ExpenseReportResult"]], resendNotification?: [{ id: string | Variable},ValueTypes["Notification"]], resetKey?: [{ data: ValueTypes["ResetKeyInput"] | Variable},boolean | `@${string}`], /** Откатить собственную незавершённую регистрацию к редактированию данных: снимает заморозку профиля и e-mail, сбрасывает подписанное заявление и непринятую попытку вступительного платежа. Доступно только до отправки регистрации в блокчейн; если взнос уже принят — требуется возврат средств. */ resetRegistration?:ValueTypes["Account"], restartAnnualGeneralMeet?: [{ data: ValueTypes["RestartAnnualGeneralMeetInput"] | Variable},ValueTypes["MeetAggregate"]], +returnExpenseItem?: [{ data: ValueTypes["ReturnExpenseItemInput"] | Variable},ValueTypes["Transaction"]], saveReportDraft?: [{ input: ValueTypes["SaveReportDraftInput"] | Variable},ValueTypes["ReportDraft"]], selectBranch?: [{ data: ValueTypes["SelectBranchInput"] | Variable},boolean | `@${string}`], sendAgreement?: [{ data: ValueTypes["SendAgreementInput"] | Variable},ValueTypes["Transaction"]], @@ -6783,6 +7078,7 @@ signByPresiderOnAnnualGeneralMeet?: [{ data: ValueTypes["SignByPresiderOnAnnualG signBySecretaryOnAnnualGeneralMeet?: [{ data: ValueTypes["SignBySecretaryOnAnnualGeneralMeetInput"] | Variable},ValueTypes["MeetAggregate"]], startInstall?: [{ data: ValueTypes["StartInstallInput"] | Variable},ValueTypes["StartInstallResult"]], startResetKey?: [{ data: ValueTypes["StartResetKeyInput"] | Variable},boolean | `@${string}`], +submitExpenseReport?: [{ data: ValueTypes["SubmitExpenseReportInput"] | Variable},ValueTypes["Transaction"]], supplyOnRequest?: [{ data: ValueTypes["SupplyOnRequestInput"] | Variable},ValueTypes["Transaction"]], triggerNotificationWorkflow?: [{ data: ValueTypes["TriggerNotificationWorkflowInput"] | Variable},boolean | `@${string}`], uninstallExtension?: [{ data: ValueTypes["UninstallExtensionInput"] | Variable},boolean | `@${string}`], @@ -6794,6 +7090,7 @@ updateReportRequisites?: [{ input: ValueTypes["UpdateReportRequisitesInput"] | V updateRequest?: [{ data: ValueTypes["UpdateRequestInput"] | Variable},ValueTypes["Transaction"]], updateSettings?: [{ data: ValueTypes["UpdateSettingsInput"] | Variable},ValueTypes["Settings"]], updateSystem?: [{ data: ValueTypes["Update"] | Variable},ValueTypes["SystemInfo"]], +uploadExpenseFile?: [{ data: ValueTypes["UploadExpenseFileInput"] | Variable},ValueTypes["ExpenseFile"]], verifyEmail?: [{ data: ValueTypes["VerifyEmailInputDTO"] | Variable},boolean | `@${string}`], voteOnAnnualGeneralMeet?: [{ data: ValueTypes["VoteOnAnnualGeneralMeetInput"] | Variable},ValueTypes["MeetAggregate"]], walmoveWallets?: [{ input: ValueTypes["WalmoveInput"] | Variable},ValueTypes["Ledger2AdjustmentResult"]], @@ -7037,6 +7334,16 @@ walmoveWallets?: [{ input: ValueTypes["WalmoveInput"] | Variable},V __typename?: boolean | `@${string}`, ['...on OrganizationWithBankAccount']?: Omit }>; + ["OverspendExpenseItemInput"]: { + /** Имя кооператива. */ + coopname: string | Variable, + /** Хеш строки расхода (item). */ + item_hash: string | Variable, + /** Сумма доплаты сверх planned_amount (asset, например "200.0000 RUB"). */ + overspend_amount: string | Variable, + /** Хеш сметы расхода (proposal). */ + proposal_hash: string | Variable +}; ["PaginatedActionsPaginationResult"]: AliasType<{ /** Текущая страница */ currentPage?:boolean | `@${string}`, @@ -7180,6 +7487,18 @@ walmoveWallets?: [{ input: ValueTypes["WalmoveInput"] | Variable},V totalPages?:boolean | `@${string}`, __typename?: boolean | `@${string}`, ['...on PaginatedCapitalLogsPaginationResult']?: Omit +}>; + ["PaginatedCapitalProgramExpensesPaginationResult"]: AliasType<{ + /** Текущая страница */ + currentPage?:boolean | `@${string}`, + /** Элементы текущей страницы */ + items?:ValueTypes["CapitalProgramExpense"], + /** Общее количество элементов */ + totalCount?:boolean | `@${string}`, + /** Общее количество страниц */ + totalPages?:boolean | `@${string}`, + __typename?: boolean | `@${string}`, + ['...on PaginatedCapitalProgramExpensesPaginationResult']?: Omit }>; ["PaginatedCapitalProjectsPaginationResult"]: AliasType<{ /** Текущая страница */ @@ -7300,6 +7619,18 @@ walmoveWallets?: [{ input: ValueTypes["WalmoveInput"] | Variable},V totalPages?:boolean | `@${string}`, __typename?: boolean | `@${string}`, ['...on PaginatedDeltasPaginationResult']?: Omit +}>; + ["PaginatedExpenseProposalsPaginationResult"]: AliasType<{ + /** Текущая страница */ + currentPage?:boolean | `@${string}`, + /** Элементы текущей страницы */ + items?:ValueTypes["ExpenseProposal"], + /** Общее количество элементов */ + totalCount?:boolean | `@${string}`, + /** Общее количество страниц */ + totalPages?:boolean | `@${string}`, + __typename?: boolean | `@${string}`, + ['...on PaginatedExpenseProposalsPaginationResult']?: Omit }>; ["PaginatedGatewayPaymentsPaginationResult"]: AliasType<{ /** Текущая страница */ @@ -7466,6 +7797,16 @@ walmoveWallets?: [{ input: ValueTypes["WalmoveInput"] | Variable},V issued_by: string | Variable, number: number | Variable, series: number | Variable +}; + ["PayExpenseItemInput"]: { + /** Фактическая сумма оплаты (asset, например "100.0000 RUB"). */ + actual_amount: string | Variable, + /** Имя кооператива. */ + coopname: string | Variable, + /** Хеш строки расхода (item). */ + item_hash: string | Variable, + /** Хеш сметы расхода (proposal). */ + proposal_hash: string | Variable }; ["PaymentDetails"]: AliasType<{ /** Сумма платежа с учетом комиссии */ @@ -7494,6 +7835,8 @@ walmoveWallets?: [{ input: ValueTypes["WalmoveInput"] | Variable},V direction?: ValueTypes["PaymentDirection"] | undefined | null | Variable, /** Хэш платежа */ hash?: string | undefined | null | Variable, + /** Хэш расхода (служебной записки): вернёт все платежи, связанные с этим расходом */ + proposal_hash?: string | undefined | null | Variable, /** Провайдер платежа */ provider?: string | undefined | null | Variable, /** Статус платежа */ @@ -8076,6 +8419,8 @@ capitalInvest?: [{ data: ValueTypes["GetInvestInput"] | Variable},V capitalInvests?: [{ filter?: ValueTypes["CapitalInvestFilter"] | undefined | null | Variable, options?: ValueTypes["PaginationInput"] | undefined | null | Variable},ValueTypes["PaginatedCapitalInvestsPaginationResult"]], capitalIssue?: [{ data: ValueTypes["GetCapitalIssueByHashInput"] | Variable},ValueTypes["CapitalIssue"]], capitalIssues?: [{ filter?: ValueTypes["CapitalIssueFilter"] | undefined | null | Variable, options?: ValueTypes["PaginationInput"] | undefined | null | Variable},ValueTypes["PaginatedCapitalIssuesPaginationResult"]], +capitalProgramExpense?: [{ coopname: string | Variable, expense_hash: string | Variable},ValueTypes["CapitalProgramExpense"]], +capitalProgramExpenses?: [{ coopname: string | Variable, options?: ValueTypes["PaginationInput"] | undefined | null | Variable},ValueTypes["PaginatedCapitalProgramExpensesPaginationResult"]], capitalProject?: [{ data: ValueTypes["GetProjectInput"] | Variable},ValueTypes["CapitalProject"]], capitalProjectWithRelations?: [{ data: ValueTypes["GetProjectWithRelationsInput"] | Variable},ValueTypes["CapitalProject"]], capitalProjects?: [{ filter?: ValueTypes["CapitalProjectFilter"] | undefined | null | Variable, options?: ValueTypes["PaginationInput"] | undefined | null | Variable},ValueTypes["PaginatedCapitalProjectsPaginationResult"]], @@ -8123,6 +8468,13 @@ chatcoopListUtcDatesWithNewRoomMessages?: [{ data: ValueTypes["ListUtcDatesWithN checkReportReadiness?: [{ reportType: ValueTypes["ReportType"] | Variable},ValueTypes["ReportReadinessView"]], cooperativeAgreements?: [{ coopname: string | Variable},ValueTypes["CoopAgreement"]], cooperativePrograms?: [{ coopname: string | Variable},ValueTypes["CooperativeProgram"]], +expenseFile?: [{ id: number | Variable},ValueTypes["ExpenseFile"]], +expenseFilesByItem?: [{ coopname: string | Variable, item_hash: string | Variable, proposal_hash: string | Variable},ValueTypes["ExpenseFile"]], +expenseFilesByProposal?: [{ coopname: string | Variable, proposal_hash: string | Variable},ValueTypes["ExpenseFile"]], +expenseProposal?: [{ proposal_hash: string | Variable},ValueTypes["ExpenseProposal"]], +expenseProposalsByCooperative?: [{ coopname: string | Variable, options?: ValueTypes["PaginationInput"] | undefined | null | Variable},ValueTypes["PaginatedExpenseProposalsPaginationResult"]], +expenseProposalsByMember?: [{ coopname: string | Variable, options?: ValueTypes["PaginationInput"] | undefined | null | Variable, username: string | Variable},ValueTypes["PaginatedExpenseProposalsPaginationResult"]], +expenseRequisitesByProposal?: [{ coopname: string | Variable, proposal_hash: string | Variable},ValueTypes["ExpenseRequisite"]], getAccount?: [{ data: ValueTypes["GetAccountInput"] | Variable},ValueTypes["Account"]], getAccounts?: [{ data?: ValueTypes["GetAccountsInput"] | undefined | null | Variable, options?: ValueTypes["PaginationInput"] | undefined | null | Variable},ValueTypes["AccountsPaginationResult"]], getActions?: [{ filters?: ValueTypes["ActionFiltersInput"] | undefined | null | Variable, pagination?: ValueTypes["PaginationInput"] | undefined | null | Variable},ValueTypes["PaginatedActionsPaginationResult"]], @@ -8198,8 +8550,6 @@ getUserWebPushSubscriptions?: [{ data: ValueTypes["GetUserSubscriptionsInput"] | Требуемые роли: chairman. */ getWebPushSubscriptionStats?:ValueTypes["SubscriptionStatsDto"], listReportDrafts?: [{ filter?: ValueTypes["ListReportDraftsFilterInput"] | undefined | null | Variable},ValueTypes["ReportDraft"]], -membershipExit?: [{ coopname: string | Variable, username: string | Variable},ValueTypes["MembershipExit"]], -membershipExitReturnPreview?: [{ coopname: string | Variable, username: string | Variable},ValueTypes["MembershipExitReturnPreview"]], onecoopGetDocuments?: [{ data: ValueTypes["GetOneCoopDocumentsInput"] | Variable},ValueTypes["OneCoopDocumentsResponse"]], process?: [{ coopname: string | Variable, hash: string | Variable},ValueTypes["ProcessView"]], processes?: [{ filter: ValueTypes["ProcessesFilter"] | Variable, pagination: ValueTypes["PaginationInput"] | Variable},ValueTypes["ProcessSummaryPaginationResult"]], @@ -8446,6 +8796,16 @@ validateReportEdits?: [{ editsJson: string | Variable, reportType: __typename?: boolean | `@${string}`, ['...on ReportDraft']?: Omit }>; + ["ReportExpenseItemInput"]: { + /** Фактически потраченная сумма по чекам (asset, например "800.0000 RUB"). Не указана — равна выданному авансу. */ + actual_amount?: string | undefined | null | Variable, + /** Имя кооператива. */ + coopname: string | Variable, + /** Хеш строки расхода (item). */ + item_hash: string | Variable, + /** Хеш сметы расхода (proposal). */ + proposal_hash: string | Variable +}; ["ReportHistoryFilterInput"]: { /** Лимит (макс 100, по умолчанию 20) */ limit?: number | undefined | null | Variable, @@ -8914,6 +9274,16 @@ validateReportEdits?: [{ editsJson: string | Variable, reportType: username: string | Variable, /** Версия генератора, использованного для создания документа */ version: string | Variable +}; + ["ReturnExpenseItemInput"]: { + /** Имя кооператива. */ + coopname: string | Variable, + /** Хеш строки расхода (item). */ + item_hash: string | Variable, + /** Хеш сметы расхода (proposal). */ + proposal_hash: string | Variable, + /** Возвращаемая сумма (asset, например "50.0000 RUB"). */ + return_amount: string | Variable }; /** Тип сообщения в истории комнаты Matrix (текст или расшифрованное аудио) */ ["RoomMessageKind"]:RoomMessageKind; @@ -9301,6 +9671,16 @@ validateReportEdits?: [{ editsJson: string | Variable, reportType: }; /** Статус истории в системе CAPITAL */ ["StoryStatus"]:StoryStatus; + ["SubmitExpenseReportInput"]: { + /** Комментарий пайщика к финализации отчёта (свободный текст, до 1000 символов). */ + comment?: string | undefined | null | Variable, + /** Имя кооператива. */ + coopname: string | Variable, + /** Хеш сметы расхода. */ + proposal_hash: string | Variable, + /** Итоговая сумма фактических расходов (asset, например "1500.0000 RUB"). */ + total_actual_amount?: string | undefined | null | Variable +}; ["SubmitVoteInput"]: { /** Имя аккаунта кооператива */ coopname: string | Variable, @@ -9676,6 +10056,26 @@ validateReportEdits?: [{ editsJson: string | Variable, reportType: story_hash: string | Variable, /** Название истории */ title?: string | undefined | null | Variable +}; + ["UploadExpenseFileInput"]: { + /** SHA-256 содержимого, hex-lowercase (64 hex-символа). */ + checksum_sha256: string | Variable, + /** Содержимое файла, base64 без префикса data:. */ + content_base64: string | Variable, + /** Имя кооператива. */ + coopname: string | Variable, + /** Хеш строки расхода (пусто — файл уровня сметы). */ + item_hash?: string | undefined | null | Variable, + /** Назначение файла (платёжка/чек/возврат). */ + kind: ValueTypes["ExpenseFileKind"] | Variable, + /** MIME-тип содержимого. */ + mime_type: string | Variable, + /** Оригинальное имя файла — для отображения и поиска. */ + original_filename?: string | undefined | null | Variable, + /** Хеш сметы расхода. */ + proposal_hash: string | Variable, + /** Размер файла в байтах (для серверной валидации). */ + size_bytes: number | Variable }; ["UserAccount"]: AliasType<{ /** Метаинформация */ @@ -11388,6 +11788,20 @@ export type ResolverInputTypes = { status?: ResolverInputTypes["ContributorStatus"] | undefined | null, /** Фильтр по имени пользователя */ username?: string | undefined | null +}; + ["CapitalCreateProgramExpenseInput"]: { + /** Имя кооператива. */ + coopname: string, + /** Имя пайщика-создателя СЗ (председатель). */ + creator: string, + /** Описание программного расхода. */ + description: string, + /** Хэш СЗ-расхода (детерминированный, из UI). Он же станет proposal_hash в шасси. */ + expense_hash: string, + /** Строки расхода. Способ оплаты (аванс под отчёт / оплата по счёту) задаётся на каждой строке отдельно. */ + items: Array, + /** Подписанная СЗ-смета (document2, registry 2010). */ + statement: ResolverInputTypes["ExpenseProposalStatementSignedDocumentInput"] }; /** Цикл разработки в системе CAPITAL */ ["CapitalCycle"]: AliasType<{ @@ -11759,6 +12173,41 @@ export type ResolverInputTypes = { step: ResolverInputTypes["CapitalOnboardingStep"], title?: string | undefined | null }; + ["CapitalProgramExpense"]: AliasType<{ + callback?:ResolverInputTypes["CapitalProgramExpenseCallback"], + coopname?:boolean | `@${string}`, + created_at?:boolean | `@${string}`, + creator?:boolean | `@${string}`, + /** Имя инициатора (ФИО пайщика или название организации) */ + creator_name?:boolean | `@${string}`, + expense_hash?:boolean | `@${string}`, + items?:ResolverInputTypes["CapitalProgramExpenseItem"], + source_wallet?:boolean | `@${string}`, + status?:boolean | `@${string}`, + total_actual?:boolean | `@${string}`, + total_planned?:boolean | `@${string}`, + updated_at?:boolean | `@${string}`, + __typename?: boolean | `@${string}` +}>; + ["CapitalProgramExpenseCallback"]: AliasType<{ + action?:boolean | `@${string}`, + contract?:boolean | `@${string}`, + data?:boolean | `@${string}`, + __typename?: boolean | `@${string}` +}>; + ["CapitalProgramExpenseItem"]: AliasType<{ + actual_amount?:boolean | `@${string}`, + description?:boolean | `@${string}`, + item_hash?:boolean | `@${string}`, + mechanics?:boolean | `@${string}`, + planned_amount?:boolean | `@${string}`, + recipient?:boolean | `@${string}`, + /** Имя получателя (ФИО пайщика или название организации) */ + recipient_name?:boolean | `@${string}`, + recipient_type?:boolean | `@${string}`, + status?:boolean | `@${string}`, + __typename?: boolean | `@${string}` +}>; /** Проект в системе CAPITAL с компонентами */ ["CapitalProject"]: AliasType<{ /** Дата создания записи */ @@ -12325,6 +12774,10 @@ export type ResolverInputTypes = { global_available_invest_pool?:boolean | `@${string}`, /** Флаг присутствия записи в блокчейне */ present?:boolean | `@${string}`, + /** Пул программных расходов — средства, переведённые под целевые расходы программы */ + program_expense_pool?:boolean | `@${string}`, + /** Зарезервировано в пуле программных расходов под активные расходы (служебные записки в работе) */ + program_expense_reserved?:boolean | `@${string}`, /** Доступная сумма членских взносов по программе */ program_membership_available?:boolean | `@${string}`, /** Накопительное вознаграждение на долю в членских взносах */ @@ -12485,6 +12938,12 @@ export type ResolverInputTypes = { project_hash?: string | undefined | null, /** Имя пользователя (опционально) */ username?: string | undefined | null +}; + ["CapitalTopupProgramExpenseInput"]: { + /** Сумма пополнения (asset, eg "10000.0000 RUB"). */ + amount: string, + /** Имя кооператива. */ + coopname: string }; /** Голос в системе CAPITAL */ ["CapitalVote"]: AliasType<{ @@ -13151,6 +13610,22 @@ export type ResolverInputTypes = { project_hash: string, /** Служебная записка о расходе */ statement: ResolverInputTypes["SignedDigitalDocumentInput"] +}; + ["CreateExpenseProposalInput"]: { + /** Callback на финализацию closeexp (опционально). */ + callback?: ResolverInputTypes["ExpenseCallbackInput"] | undefined | null, + /** Имя кооператива. */ + coopname: string, + /** Строки расхода (массив items). */ + items: Array, + /** Хеш сметы расхода (детерминированный, из UI). */ + proposal_hash: string, + /** Источник средств (eosio::name кошелька-источника, eg "w.cap.blago"). */ + source_wallet: string, + /** Подписанная СЗ-смета (document2, registry 2010). */ + statement: ResolverInputTypes["ExpenseProposalStatementSignedDocumentInput"], + /** Имя пайщика-создателя СЗ. */ + username: string }; ["CreateIndividualDataInput"]: { /** Дата рождения */ @@ -13229,16 +13704,6 @@ export type ResolverInputTypes = { ["CreateMatrixAccountInputDTO"]: { password: string, username: string -}; - ["CreateMembershipExitInput"]: { - /** Имя аккаунта кооператива */ - coopname: string, - /** Хеш процесса выхода (генерируется на клиенте) */ - exit_hash: string, - /** Подписанное пайщиком заявление о выходе из кооператива */ - statement: ResolverInputTypes["MembershipExitApplicationSignedDocumentInput"], - /** Имя пайщика, выходящего из кооператива */ - username: string }; ["CreateOrganizationDataInput"]: { /** Банковский счет организации */ @@ -13833,6 +14298,46 @@ export type ResolverInputTypes = { /** ОГРН */ ogrn: string }; + ["ExpenseCallbackInput"]: { + /** Action-метод */ + action?: string | undefined | null, + /** Контракт-целевой */ + contract?: string | undefined | null, + /** Payload (hex bytes) */ + data?: string | undefined | null +}; + /** Запись о первичном файле расхода (платёжка/чек/возврат). */ +["ExpenseFile"]: AliasType<{ + /** SHA-256 содержимого, hex-lowercase. */ + checksum_sha256?:boolean | `@${string}`, + /** Имя кооператива (scope). */ + coopname?:boolean | `@${string}`, + /** Внутренний ID записи. */ + id?:boolean | `@${string}`, + /** Хеш строки расхода (если файл уровня item). */ + item_hash?:boolean | `@${string}`, + /** Назначение файла. */ + kind?:boolean | `@${string}`, + /** MIME-тип содержимого. */ + mime_type?:boolean | `@${string}`, + /** Оригинальное имя загруженного файла. */ + original_filename?:boolean | `@${string}`, + /** Хеш сметы расхода. */ + proposal_hash?:boolean | `@${string}`, + /** Короткоживущий URL на скачивание (HMAC-signed). */ + read_url?:boolean | `@${string}`, + /** Размер файла в байтах. */ + size_bytes?:boolean | `@${string}`, + /** MinIO-ключ внутри бакета. */ + storage_key?:boolean | `@${string}`, + /** Когда загружено. */ + uploaded_at?:boolean | `@${string}`, + /** Кто загрузил (username). */ + uploaded_by_username?:boolean | `@${string}`, + __typename?: boolean | `@${string}` +}>; + /** Тип первичного файла расхода. */ +["ExpenseFileKind"]:ExpenseFileKind; ["ExpenseFilter"]: { /** Фильтр по ID фонда */ fundId?: string | undefined | null, @@ -13843,6 +14348,303 @@ export type ResolverInputTypes = { /** Фильтр по имени пользователя */ username?: string | undefined | null }; + /** Строка сметы расхода. */ +["ExpenseItem"]: AliasType<{ + /** Фактическая сумма (после оплаты/отчёта). */ + actual_amount?:boolean | `@${string}`, + /** Назначение/описание строки. */ + description?:boolean | `@${string}`, + /** Хеш строки расхода. */ + item_hash?:boolean | `@${string}`, + /** Способ оплаты. */ + mechanics?:boolean | `@${string}`, + /** Планируемая сумма. */ + planned_amount?:boolean | `@${string}`, + /** Идентификатор получателя. */ + recipient?:boolean | `@${string}`, + /** Тип получателя платежа. */ + recipient_type?:boolean | `@${string}`, + /** Статус строки. */ + status?:boolean | `@${string}`, + __typename?: boolean | `@${string}` +}>; + ["ExpenseItemInput"]: { + /** Описание назначения расхода. */ + description: string, + /** Хеш строки расхода (детерминированный, из UI). */ + item_hash: string, + /** Способ оплаты (ADVANCE / DIRECT). */ + mechanics: ResolverInputTypes["ExpenseMechanics"], + /** Идентификатор сохранённых реквизитов получателя-пайщика — реквизиты снимаются в момент создания и прикладываются к платежу. */ + payment_method_id?: string | undefined | null, + /** Назначение платежа (для оплаты по счёту) — фиксируется в снимке для кассира. */ + payment_purpose?: string | undefined | null, + /** Планируемая сумма (asset, eg "1000.0000 RUB"). */ + planned_amount: string, + /** Получатель: username пайщика; для организации — пустая строка (аккаунта в кооперативе нет). */ + recipient: string, + /** Тип получателя. */ + recipient_type: ResolverInputTypes["ExpenseRecipientType"], + /** Реквизиты получателя-организации (вводятся вручную). */ + requisites?: string | undefined | null +}; + /** Статус строки расхода. */ +["ExpenseItemStatus"]:ExpenseItemStatus; + /** Способ оплаты строки расхода. */ +["ExpenseMechanics"]:ExpenseMechanics; + /** Смета расхода (СЗ). */ +["ExpenseProposal"]: AliasType<{ + /** Дата создания записи */ + _created_at?:boolean | `@${string}`, + /** Внутренний ID базы данных */ + _id?:boolean | `@${string}`, + /** Дата последнего обновления записи */ + _updated_at?:boolean | `@${string}`, + /** Номер блока крайней синхронизации с блокчейном */ + block_num?:boolean | `@${string}`, + /** Сырой статус из блокчейна (uint8). */ + blockchain_status?:boolean | `@${string}`, + /** Имя кооператива (scope). */ + coopname?:boolean | `@${string}`, + /** Время создания (chain). */ + created_at?:boolean | `@${string}`, + /** Решение совета о расходе (DocumentAggregate). */ + decision_doc?:ResolverInputTypes["DocumentAggregate"], + /** ID в блокчейне. */ + id?:boolean | `@${string}`, + /** Строки сметы. */ + items?:ResolverInputTypes["ExpenseItem"], + /** Флаг присутствия записи в блокчейне */ + present?:boolean | `@${string}`, + /** Хеш сметы расхода. */ + proposal_hash?:boolean | `@${string}`, + /** Кошелёк-источник средств. */ + source_wallet?:boolean | `@${string}`, + /** Заявление пайщика о расходе (DocumentAggregate). */ + statement_doc?:ResolverInputTypes["DocumentAggregate"], + /** Доменный статус сметы. */ + status?:boolean | `@${string}`, + /** Сумма всех строк (факт). */ + total_actual?:boolean | `@${string}`, + /** Сумма всех строк (план). */ + total_planned?:boolean | `@${string}`, + /** Время последнего обновления (chain). */ + updated_at?:boolean | `@${string}`, + /** Создатель сметы (username). */ + username?:boolean | `@${string}`, + __typename?: boolean | `@${string}` +}>; + ["ExpenseProposalDecisionBodyInput"]: { + /** Род решения (approve / decline) */ + kind: string, + /** Причина отказа (для decline) */ + reason?: string | undefined | null +}; + ["ExpenseProposalDecisionGenerateDocumentInput"]: { + /** Номер блока, на котором был создан документ */ + block_num?: number | undefined | null, + /** Название кооператива, связанное с документом */ + coopname: string, + /** Дата и время создания документа */ + created_at?: string | undefined | null, + /** Идентификатор решения совета (повестка) — источник данных голосования */ + decision_id: number, + /** Имя генератора, использованного для создания документа */ + generator?: string | undefined | null, + /** Позиции расхода */ + items: Array, + /** Язык документа */ + lang?: string | undefined | null, + /** Ссылки, связанные с документом */ + links?: Array | undefined | null, + /** Шапка СЗ */ + proposal: ResolverInputTypes["ExpenseProposalDecisionHeaderInput"], + /** Хеш сметы расхода */ + proposal_hash: string, + /** Резолюция совета (утвердить / отказать) */ + resolution: ResolverInputTypes["ExpenseProposalDecisionBodyInput"], + /** Часовой пояс, в котором был создан документ */ + timezone?: string | undefined | null, + /** Название документа */ + title?: string | undefined | null, + /** Имя пользователя, создавшего документ */ + username: string, + /** Версия генератора, использованного для создания документа */ + version?: string | undefined | null +}; + ["ExpenseProposalDecisionHeaderInput"]: { + deadline?: string | undefined | null, + description: string, + fund_name?: string | undefined | null, + items_count: number, + source_wallet: string, + total_amount: string +}; + ["ExpenseProposalDecisionItemInput"]: { + amount: string, + description: string, + mechanics: string, + number: string, + recipient_type: string +}; + ["ExpenseProposalHeaderInput"]: { + /** Срок исполнения («в срок до»), формат DD.MM.YYYY */ + deadline?: string | undefined | null, + /** Описание цели расходов */ + description: string, + /** Фонд списания — подставляется сервером из параметров шасси расходов, передавать не нужно */ + fund_name?: string | undefined | null, + /** Количество позиций */ + items_count: number, + /** Кошелёк-источник */ + source_wallet: string, + /** Итоговая сумма расходов */ + total_amount: string +}; + ["ExpenseProposalItemInput"]: { + /** Сумма строки */ + amount: string, + /** Описание расхода */ + description: string, + /** Способ оплаты (ADVANCE / DIRECT) */ + mechanics: string, + /** Порядковый номер строки */ + number: string, + /** Идентификатор сохранённых реквизитов получателя-пайщика — сервер подставит полные реквизиты в документ. */ + payment_method_id?: string | undefined | null, + /** Назначение платежа — отдельной строкой после реквизитов */ + payment_purpose?: string | undefined | null, + /** Имя получателя */ + recipient_name?: string | undefined | null, + /** Тип получателя (SELF / MEMBER / ORG) */ + recipient_type: string, + /** Имя аккаунта получателя-пайщика (владелец реквизитов). */ + recipient_username?: string | undefined | null, + /** Реквизиты получателя */ + requisites?: string | undefined | null +}; + ["ExpenseProposalSignedItemInput"]: { + /** Сумма строки */ + amount: string, + /** Описание расхода */ + description: string, + /** Способ оплаты (ADVANCE / DIRECT) */ + mechanics: string, + /** Порядковый номер строки */ + number: string, + /** Тип получателя (SELF / MEMBER / ORG) */ + recipient_type: string +}; + ["ExpenseProposalStatementGenerateDocumentInput"]: { + /** Номер блока, на котором был создан документ */ + block_num?: number | undefined | null, + /** Название кооператива, связанное с документом */ + coopname: string, + /** Дата и время создания документа */ + created_at?: string | undefined | null, + /** Имя генератора, использованного для создания документа */ + generator?: string | undefined | null, + /** Позиции расхода */ + items: Array, + /** Язык документа */ + lang?: string | undefined | null, + /** Ссылки, связанные с документом */ + links?: Array | undefined | null, + /** Шапка СЗ */ + proposal: ResolverInputTypes["ExpenseProposalHeaderInput"], + /** Хеш сметы расхода (детерминированный) */ + proposal_hash: string, + /** Часовой пояс, в котором был создан документ */ + timezone?: string | undefined | null, + /** Название документа */ + title?: string | undefined | null, + /** Имя пользователя, создавшего документ */ + username: string, + /** Версия генератора, использованного для создания документа */ + version?: string | undefined | null +}; + ["ExpenseProposalStatementSignedDocumentInput"]: { + /** Хэш содержимого документа */ + doc_hash: string, + /** Общий хэш (doc_hash + meta_hash) */ + hash: string, + /** Метаинформация СЗ-заявления */ + meta: ResolverInputTypes["ExpenseProposalStatementSignedMetaDocumentInput"], + /** Хэш мета-данных */ + meta_hash: string, + /** Вектор подписей */ + signatures: Array, + /** Версия стандарта документа */ + version: string +}; + ["ExpenseProposalStatementSignedMetaDocumentInput"]: { + /** Номер блока, на котором был создан документ */ + block_num: number, + /** Название кооператива, связанное с документом */ + coopname: string, + /** Дата и время создания документа */ + created_at: string, + /** Идентификатор приватных данных документа off-chain (реквизиты/имя/назначение) */ + doc_data_hash: string, + /** Имя генератора, использованного для создания документа */ + generator: string, + /** Публичные позиции расхода (без реквизитов) */ + items: Array, + /** Язык документа */ + lang: string, + /** Ссылки, связанные с документом */ + links: Array, + /** Шапка СЗ */ + proposal: ResolverInputTypes["ExpenseProposalHeaderInput"], + /** Хеш сметы расхода (детерминированный) */ + proposal_hash: string, + /** ID документа в реестре */ + registry_id: number, + /** Часовой пояс, в котором был создан документ */ + timezone: string, + /** Название документа */ + title: string, + /** Имя пользователя, создавшего документ */ + username: string, + /** Версия генератора, использованного для создания документа */ + version: string +}; + /** Статус сметы расхода. */ +["ExpenseProposalStatus"]:ExpenseProposalStatus; + /** Тип получателя платежа. */ +["ExpenseRecipientType"]:ExpenseRecipientType; + /** Исход отчёта по строке-авансу: закрыто либо ожидается расчёт разницы (возврат/доплата). */ +["ExpenseReportOutcome"]:ExpenseReportOutcome; + ["ExpenseReportResult"]: AliasType<{ + /** Исход отчёта. */ + outcome?:boolean | `@${string}`, + /** Сумма разницы к расчёту (asset), при недо-/перерасходе. */ + settlement_amount?:boolean | `@${string}`, + /** Хэш заведённой платёжки расчёта (возврат/доплата). */ + settlement_payment_hash?:boolean | `@${string}`, + /** Транзакция закрытия позиции (только при CLOSED). */ + transaction?:ResolverInputTypes["Transaction"], + __typename?: boolean | `@${string}` +}>; + ["ExpenseRequisite"]: AliasType<{ + /** Имя кооператива. */ + coopname?:boolean | `@${string}`, + /** Снимок данных платёжного метода на момент подачи СЗ. */ + data?:boolean | `@${string}`, + /** Хеш строки расхода (item). */ + item_hash?:boolean | `@${string}`, + /** Тип платёжного метода пайщика (СБП / банковский перевод). */ + method_type?:boolean | `@${string}`, + /** Назначение платежа для поручения кассиру. */ + payment_purpose?:boolean | `@${string}`, + /** Хеш сметы расхода (proposal). */ + proposal_hash?:boolean | `@${string}`, + /** Получатель платежа (аккаунт пайщика или название организации). */ + recipient?:boolean | `@${string}`, + /** Реквизиты строкой — как в документе служебной записки. */ + requisites?:boolean | `@${string}`, + __typename?: boolean | `@${string}` +}>; /** Статус расхода в системе CAPITAL */ ["ExpenseStatus"]:ExpenseStatus; /** Расширенное действие блокчейна с сертификатом пользователя, совершившего его. */ @@ -15081,122 +15883,6 @@ export type ResolverInputTypes = { votes_for?:boolean | `@${string}`, __typename?: boolean | `@${string}` }>; - ["MembershipExit"]: AliasType<{ - /** Дата подачи заявления на выход */ - created_at?:boolean | `@${string}`, - /** Хеш процесса выхода */ - exit_hash?:boolean | `@${string}`, - /** Сумма к возврату (фиксируется советом при одобрении; до одобрения — 0) */ - quantity?:boolean | `@${string}`, - /** Статус процесса выхода */ - status?:boolean | `@${string}`, - __typename?: boolean | `@${string}` -}>; - ["MembershipExitApplicationGenerateDocumentInput"]: { - /** Номер блока, на котором был создан документ */ - block_num?: number | undefined | null, - /** Название кооператива, связанное с документом */ - coopname: string, - /** Дата и время создания документа */ - created_at?: string | undefined | null, - /** Имя генератора, использованного для создания документа */ - generator?: string | undefined | null, - /** Язык документа */ - lang?: string | undefined | null, - /** Ссылки, связанные с документом */ - links?: Array | undefined | null, - /** Флаг пропуска сохранения документа (используется для предварительной генерации и демонстрации пользователю) */ - skip_save: boolean, - /** Часовой пояс, в котором был создан документ */ - timezone?: string | undefined | null, - /** Название документа */ - title?: string | undefined | null, - /** Имя пользователя, создавшего документ */ - username: string, - /** Версия генератора, использованного для создания документа */ - version?: string | undefined | null -}; - ["MembershipExitApplicationSignedDocumentInput"]: { - /** Хэш содержимого документа */ - doc_hash: string, - /** Общий хэш (doc_hash + meta_hash) */ - hash: string, - meta: ResolverInputTypes["MembershipExitApplicationSignedMetaDocumentInput"], - /** Хэш мета-данных */ - meta_hash: string, - /** Вектор подписей */ - signatures: Array, - /** Версия стандарта документа */ - version: string -}; - ["MembershipExitApplicationSignedMetaDocumentInput"]: { - /** Номер блока, на котором был создан документ */ - block_num: number, - /** Название кооператива, связанное с документом */ - coopname: string, - /** Дата и время создания документа */ - created_at: string, - /** Имя генератора, использованного для создания документа */ - generator: string, - /** Язык документа */ - lang: string, - /** Ссылки, связанные с документом */ - links: Array, - /** ID документа в реестре */ - registry_id: number, - /** Флаг пропуска сохранения документа (используется для предварительной генерации и демонстрации пользователю) */ - skip_save: boolean, - /** Часовой пояс, в котором был создан документ */ - timezone: string, - /** Название документа */ - title: string, - /** Имя пользователя, создавшего документ */ - username: string, - /** Версия генератора, использованного для создания документа */ - version: string -}; - ["MembershipExitDecisionGenerateDocumentInput"]: { - /** Номер блока, на котором был создан документ */ - block_num?: number | undefined | null, - /** Название кооператива, связанное с документом */ - coopname: string, - /** Дата и время создания документа */ - created_at?: string | undefined | null, - /** Идентификатор протокола решения собрания совета */ - decision_id: number, - /** Имя генератора, использованного для создания документа */ - generator?: string | undefined | null, - /** Язык документа */ - lang?: string | undefined | null, - /** Ссылки, связанные с документом */ - links?: Array | undefined | null, - /** Часовой пояс, в котором был создан документ */ - timezone?: string | undefined | null, - /** Название документа */ - title?: string | undefined | null, - /** Имя пользователя, создавшего документ */ - username: string, - /** Версия генератора, использованного для создания документа */ - version?: string | undefined | null -}; - ["MembershipExitResult"]: AliasType<{ - /** Хеш созданного процесса выхода */ - exit_hash?:boolean | `@${string}`, - /** Статус процесса выхода после подачи (ожидает подтверждения по ссылке из письма) */ - status?:boolean | `@${string}`, - __typename?: boolean | `@${string}` -}>; - ["MembershipExitReturnPreview"]: AliasType<{ - /** Минимальный паевой взнос пайщика */ - minimum_contribution?:boolean | `@${string}`, - /** Целевой паевой взнос пайщика */ - share_contribution?:boolean | `@${string}`, - /** Итоговая сумма к возврату (минимальный + целевой паевой) */ - total?:boolean | `@${string}`, - __typename?: boolean | `@${string}` -}>; - /** Статус процесса выхода пайщика из кооператива */ -["MembershipExitStatus"]:MembershipExitStatus; ["MissingRequisiteField"]: AliasType<{ key?:boolean | `@${string}`, label?:boolean | `@${string}`, @@ -15257,7 +15943,6 @@ addParticipant?: [{ data: ResolverInputTypes["AddParticipantInput"]},ResolverInp addPaymentMethod?: [{ data: ResolverInputTypes["AddPaymentMethodInput"]},ResolverInputTypes["PaymentMethod"]], addTrustedAccount?: [{ data: ResolverInputTypes["AddTrustedAccountInput"]},ResolverInputTypes["Branch"]], authorizeDecision?: [{ data: ResolverInputTypes["AuthorizeDecisionInput"]},ResolverInputTypes["Transaction"]], -cancelMembershipExit?: [{ coopname: string, username: string},boolean | `@${string}`], cancelRequest?: [{ data: ResolverInputTypes["CancelRequestInput"]},ResolverInputTypes["Transaction"]], capitalAddAuthor?: [{ data: ResolverInputTypes["AddAuthorInput"]},ResolverInputTypes["CapitalProject"]], capitalApproveCommit?: [{ data: ResolverInputTypes["CommitApproveInput"]},ResolverInputTypes["CapitalCommit"]], @@ -15273,6 +15958,7 @@ capitalCreateDebt?: [{ data: ResolverInputTypes["CreateDebtInput"]},ResolverInpu capitalCreateExpense?: [{ data: ResolverInputTypes["CreateExpenseInput"]},ResolverInputTypes["Transaction"]], capitalCreateIssue?: [{ data: ResolverInputTypes["CreateIssueInput"]},ResolverInputTypes["CapitalIssue"]], capitalCreateProcessTemplate?: [{ data: ResolverInputTypes["CreateProcessTemplateInput"]},ResolverInputTypes["ProcessTemplate"]], +capitalCreateProgramExpense?: [{ data: ResolverInputTypes["CapitalCreateProgramExpenseInput"]},ResolverInputTypes["Transaction"]], capitalCreateProgramInvest?: [{ data: ResolverInputTypes["CreateProgramInvestInput"]},ResolverInputTypes["Transaction"]], capitalCreateProgramProperty?: [{ data: ResolverInputTypes["CreateProgramPropertyInput"]},ResolverInputTypes["Transaction"]], capitalCreateProject?: [{ data: ResolverInputTypes["CreateProjectInput"]},ResolverInputTypes["Transaction"]], @@ -15330,6 +16016,7 @@ capitalStartProject?: [{ data: ResolverInputTypes["StartProjectInput"]},Resolver capitalStartVoting?: [{ data: ResolverInputTypes["StartVotingInput"]},ResolverInputTypes["Transaction"]], capitalStopProject?: [{ data: ResolverInputTypes["StopProjectInput"]},ResolverInputTypes["CapitalProject"]], capitalSubmitVote?: [{ data: ResolverInputTypes["SubmitVoteInput"]},ResolverInputTypes["Transaction"]], +capitalTopupProgramExpensePool?: [{ data: ResolverInputTypes["CapitalTopupProgramExpenseInput"]},ResolverInputTypes["Transaction"]], capitalUpdateIssue?: [{ data: ResolverInputTypes["UpdateIssueInput"]},ResolverInputTypes["CapitalIssue"]], capitalUpdateProcessTemplate?: [{ data: ResolverInputTypes["UpdateProcessTemplateInput"]},ResolverInputTypes["ProcessTemplate"]], capitalUpdateStory?: [{ data: ResolverInputTypes["UpdateStoryInput"]},ResolverInputTypes["CapitalStory"]], @@ -15352,15 +16039,14 @@ completeChairmanGeneralMeetStep?: [{ data: ResolverInputTypes["ChairmanOnboardin completeExtensionOnboardingStep?: [{ data: ResolverInputTypes["CompleteExtensionOnboardingStepInput"]},ResolverInputTypes["ExtensionOnboardingState"]], completeRequest?: [{ data: ResolverInputTypes["CompleteRequestInput"]},ResolverInputTypes["Transaction"]], confirmAgreement?: [{ data: ResolverInputTypes["ConfirmAgreementInput"]},ResolverInputTypes["Transaction"]], -confirmMembershipExit?: [{ token: string},ResolverInputTypes["MembershipExitResult"]], confirmReceiveOnRequest?: [{ data: ResolverInputTypes["ConfirmReceiveOnRequestInput"]},ResolverInputTypes["Transaction"]], confirmSupplyOnRequest?: [{ data: ResolverInputTypes["ConfirmSupplyOnRequestInput"]},ResolverInputTypes["Transaction"]], createAnnualGeneralMeet?: [{ data: ResolverInputTypes["CreateAnnualGeneralMeetInput"]},ResolverInputTypes["MeetAggregate"]], createBranch?: [{ data: ResolverInputTypes["CreateBranchInput"]},ResolverInputTypes["Branch"]], createChildOrder?: [{ data: ResolverInputTypes["CreateChildOrderInput"]},ResolverInputTypes["Transaction"]], createDepositPayment?: [{ data: ResolverInputTypes["CreateDepositPaymentInput"]},ResolverInputTypes["GatewayPayment"]], +createExpenseProposal?: [{ data: ResolverInputTypes["CreateExpenseProposalInput"]},ResolverInputTypes["Transaction"]], createInitialPayment?: [{ data: ResolverInputTypes["CreateInitialPaymentInput"]},ResolverInputTypes["GatewayPayment"]], -createMembershipExit?: [{ data: ResolverInputTypes["CreateMembershipExitInput"]},ResolverInputTypes["MembershipExitResult"]], createParentOffer?: [{ data: ResolverInputTypes["CreateParentOfferInput"]},ResolverInputTypes["Transaction"]], createProjectOfFreeDecision?: [{ data: ResolverInputTypes["CreateProjectFreeDecisionInput"]},ResolverInputTypes["CreatedProjectFreeDecision"]], createWebPushSubscription?: [{ data: ResolverInputTypes["CreateSubscriptionInput"]},ResolverInputTypes["CreateSubscriptionResponse"]], @@ -15386,9 +16072,9 @@ generateAssetContributionStatement?: [{ data: ResolverInputTypes["AssetContribut generateBallotForAnnualGeneralMeetDocument?: [{ data: ResolverInputTypes["AnnualGeneralMeetingVotingBallotGenerateDocumentInput"], options?: ResolverInputTypes["GenerateDocumentOptionsInput"] | undefined | null},ResolverInputTypes["GeneratedDocument"]], generateConvertToAxonStatement?: [{ data: ResolverInputTypes["ConvertToAxonStatementGenerateDocumentInput"], options?: ResolverInputTypes["GenerateDocumentOptionsInput"] | undefined | null},ResolverInputTypes["GeneratedDocument"]], generateDocument?: [{ input: ResolverInputTypes["GenerateAnyDocumentInput"]},ResolverInputTypes["GeneratedDocument"]], +generateExpenseProposalDecisionDocument?: [{ data: ResolverInputTypes["ExpenseProposalDecisionGenerateDocumentInput"], options?: ResolverInputTypes["GenerateDocumentOptionsInput"] | undefined | null},ResolverInputTypes["GeneratedDocument"]], +generateExpenseProposalStatementDocument?: [{ data: ResolverInputTypes["ExpenseProposalStatementGenerateDocumentInput"], options?: ResolverInputTypes["GenerateDocumentOptionsInput"] | undefined | null},ResolverInputTypes["GeneratedDocument"]], generateFreeDecision?: [{ data: ResolverInputTypes["FreeDecisionGenerateDocumentInput"], options?: ResolverInputTypes["GenerateDocumentOptionsInput"] | undefined | null},ResolverInputTypes["GeneratedDocument"]], -generateMembershipExitApplication?: [{ data: ResolverInputTypes["MembershipExitApplicationGenerateDocumentInput"], options?: ResolverInputTypes["GenerateDocumentOptionsInput"] | undefined | null},ResolverInputTypes["GeneratedDocument"]], -generateMembershipExitDecision?: [{ data: ResolverInputTypes["MembershipExitDecisionGenerateDocumentInput"], options?: ResolverInputTypes["GenerateDocumentOptionsInput"] | undefined | null},ResolverInputTypes["GeneratedDocument"]], generateParticipantApplication?: [{ data: ResolverInputTypes["ParticipantApplicationGenerateDocumentInput"], options?: ResolverInputTypes["GenerateDocumentOptionsInput"] | undefined | null},ResolverInputTypes["GeneratedDocument"]], generateParticipantApplicationDecision?: [{ data: ResolverInputTypes["ParticipantApplicationDecisionGenerateDocumentInput"], options?: ResolverInputTypes["GenerateDocumentOptionsInput"] | undefined | null},ResolverInputTypes["GeneratedDocument"]], generatePrivacyAgreement?: [{ data: ResolverInputTypes["GenerateDocumentInput"], options?: ResolverInputTypes["GenerateDocumentOptionsInput"] | undefined | null},ResolverInputTypes["GeneratedDocument"]], @@ -15415,6 +16101,8 @@ markNotificationRead?: [{ id: string},ResolverInputTypes["InboxNotification"]], markReportPeriod?: [{ data: ResolverInputTypes["MarkReportPeriodInput"]},boolean | `@${string}`], moderateRequest?: [{ data: ResolverInputTypes["ModerateRequestInput"]},ResolverInputTypes["Transaction"]], notifyOnAnnualGeneralMeet?: [{ data: ResolverInputTypes["NotifyOnAnnualGeneralMeetInput"]},ResolverInputTypes["MeetAggregate"]], +overspendExpenseItem?: [{ data: ResolverInputTypes["OverspendExpenseItemInput"]},ResolverInputTypes["Transaction"]], +payExpenseItem?: [{ data: ResolverInputTypes["PayExpenseItemInput"]},ResolverInputTypes["Transaction"]], processConvertToAxonStatement?: [{ data: ResolverInputTypes["ProcessConvertToAxonStatementInput"]},boolean | `@${string}`], prohibitRequest?: [{ data: ResolverInputTypes["ProhibitRequestInput"]},ResolverInputTypes["Transaction"]], publishProjectOfFreeDecision?: [{ data: ResolverInputTypes["PublishProjectFreeDecisionInput"]},ResolverInputTypes["AgendaWithDocuments"]], @@ -15423,11 +16111,13 @@ receiveOnRequest?: [{ data: ResolverInputTypes["ReceiveOnRequestInput"]},Resolve refresh?: [{ data: ResolverInputTypes["RefreshInput"]},ResolverInputTypes["RegisteredAccount"]], registerAccount?: [{ data: ResolverInputTypes["RegisterAccountInput"]},ResolverInputTypes["RegisteredAccount"]], registerParticipant?: [{ data: ResolverInputTypes["RegisterParticipantInput"]},ResolverInputTypes["Account"]], +reportExpenseItem?: [{ data: ResolverInputTypes["ReportExpenseItemInput"]},ResolverInputTypes["ExpenseReportResult"]], resendNotification?: [{ id: string},ResolverInputTypes["Notification"]], resetKey?: [{ data: ResolverInputTypes["ResetKeyInput"]},boolean | `@${string}`], /** Откатить собственную незавершённую регистрацию к редактированию данных: снимает заморозку профиля и e-mail, сбрасывает подписанное заявление и непринятую попытку вступительного платежа. Доступно только до отправки регистрации в блокчейн; если взнос уже принят — требуется возврат средств. */ resetRegistration?:ResolverInputTypes["Account"], restartAnnualGeneralMeet?: [{ data: ResolverInputTypes["RestartAnnualGeneralMeetInput"]},ResolverInputTypes["MeetAggregate"]], +returnExpenseItem?: [{ data: ResolverInputTypes["ReturnExpenseItemInput"]},ResolverInputTypes["Transaction"]], saveReportDraft?: [{ input: ResolverInputTypes["SaveReportDraftInput"]},ResolverInputTypes["ReportDraft"]], selectBranch?: [{ data: ResolverInputTypes["SelectBranchInput"]},boolean | `@${string}`], sendAgreement?: [{ data: ResolverInputTypes["SendAgreementInput"]},ResolverInputTypes["Transaction"]], @@ -15437,6 +16127,7 @@ signByPresiderOnAnnualGeneralMeet?: [{ data: ResolverInputTypes["SignByPresiderO signBySecretaryOnAnnualGeneralMeet?: [{ data: ResolverInputTypes["SignBySecretaryOnAnnualGeneralMeetInput"]},ResolverInputTypes["MeetAggregate"]], startInstall?: [{ data: ResolverInputTypes["StartInstallInput"]},ResolverInputTypes["StartInstallResult"]], startResetKey?: [{ data: ResolverInputTypes["StartResetKeyInput"]},boolean | `@${string}`], +submitExpenseReport?: [{ data: ResolverInputTypes["SubmitExpenseReportInput"]},ResolverInputTypes["Transaction"]], supplyOnRequest?: [{ data: ResolverInputTypes["SupplyOnRequestInput"]},ResolverInputTypes["Transaction"]], triggerNotificationWorkflow?: [{ data: ResolverInputTypes["TriggerNotificationWorkflowInput"]},boolean | `@${string}`], uninstallExtension?: [{ data: ResolverInputTypes["UninstallExtensionInput"]},boolean | `@${string}`], @@ -15448,6 +16139,7 @@ updateReportRequisites?: [{ input: ResolverInputTypes["UpdateReportRequisitesInp updateRequest?: [{ data: ResolverInputTypes["UpdateRequestInput"]},ResolverInputTypes["Transaction"]], updateSettings?: [{ data: ResolverInputTypes["UpdateSettingsInput"]},ResolverInputTypes["Settings"]], updateSystem?: [{ data: ResolverInputTypes["Update"]},ResolverInputTypes["SystemInfo"]], +uploadExpenseFile?: [{ data: ResolverInputTypes["UploadExpenseFileInput"]},ResolverInputTypes["ExpenseFile"]], verifyEmail?: [{ data: ResolverInputTypes["VerifyEmailInputDTO"]},boolean | `@${string}`], voteOnAnnualGeneralMeet?: [{ data: ResolverInputTypes["VoteOnAnnualGeneralMeetInput"]},ResolverInputTypes["MeetAggregate"]], walmoveWallets?: [{ input: ResolverInputTypes["WalmoveInput"]},ResolverInputTypes["Ledger2AdjustmentResult"]], @@ -15680,6 +16372,16 @@ walmoveWallets?: [{ input: ResolverInputTypes["WalmoveInput"]},ResolverInputType username?:boolean | `@${string}`, __typename?: boolean | `@${string}` }>; + ["OverspendExpenseItemInput"]: { + /** Имя кооператива. */ + coopname: string, + /** Хеш строки расхода (item). */ + item_hash: string, + /** Сумма доплаты сверх planned_amount (asset, например "200.0000 RUB"). */ + overspend_amount: string, + /** Хеш сметы расхода (proposal). */ + proposal_hash: string +}; ["PaginatedActionsPaginationResult"]: AliasType<{ /** Текущая страница */ currentPage?:boolean | `@${string}`, @@ -15811,6 +16513,17 @@ walmoveWallets?: [{ input: ResolverInputTypes["WalmoveInput"]},ResolverInputType /** Общее количество страниц */ totalPages?:boolean | `@${string}`, __typename?: boolean | `@${string}` +}>; + ["PaginatedCapitalProgramExpensesPaginationResult"]: AliasType<{ + /** Текущая страница */ + currentPage?:boolean | `@${string}`, + /** Элементы текущей страницы */ + items?:ResolverInputTypes["CapitalProgramExpense"], + /** Общее количество элементов */ + totalCount?:boolean | `@${string}`, + /** Общее количество страниц */ + totalPages?:boolean | `@${string}`, + __typename?: boolean | `@${string}` }>; ["PaginatedCapitalProjectsPaginationResult"]: AliasType<{ /** Текущая страница */ @@ -15921,6 +16634,17 @@ walmoveWallets?: [{ input: ResolverInputTypes["WalmoveInput"]},ResolverInputType /** Общее количество страниц */ totalPages?:boolean | `@${string}`, __typename?: boolean | `@${string}` +}>; + ["PaginatedExpenseProposalsPaginationResult"]: AliasType<{ + /** Текущая страница */ + currentPage?:boolean | `@${string}`, + /** Элементы текущей страницы */ + items?:ResolverInputTypes["ExpenseProposal"], + /** Общее количество элементов */ + totalCount?:boolean | `@${string}`, + /** Общее количество страниц */ + totalPages?:boolean | `@${string}`, + __typename?: boolean | `@${string}` }>; ["PaginatedGatewayPaymentsPaginationResult"]: AliasType<{ /** Текущая страница */ @@ -16084,6 +16808,16 @@ walmoveWallets?: [{ input: ResolverInputTypes["WalmoveInput"]},ResolverInputType issued_by: string, number: number, series: number +}; + ["PayExpenseItemInput"]: { + /** Фактическая сумма оплаты (asset, например "100.0000 RUB"). */ + actual_amount: string, + /** Имя кооператива. */ + coopname: string, + /** Хеш строки расхода (item). */ + item_hash: string, + /** Хеш сметы расхода (proposal). */ + proposal_hash: string }; ["PaymentDetails"]: AliasType<{ /** Сумма платежа с учетом комиссии */ @@ -16111,6 +16845,8 @@ walmoveWallets?: [{ input: ResolverInputTypes["WalmoveInput"]},ResolverInputType direction?: ResolverInputTypes["PaymentDirection"] | undefined | null, /** Хэш платежа */ hash?: string | undefined | null, + /** Хэш расхода (служебной записки): вернёт все платежи, связанные с этим расходом */ + proposal_hash?: string | undefined | null, /** Провайдер платежа */ provider?: string | undefined | null, /** Статус платежа */ @@ -16671,6 +17407,8 @@ capitalInvest?: [{ data: ResolverInputTypes["GetInvestInput"]},ResolverInputType capitalInvests?: [{ filter?: ResolverInputTypes["CapitalInvestFilter"] | undefined | null, options?: ResolverInputTypes["PaginationInput"] | undefined | null},ResolverInputTypes["PaginatedCapitalInvestsPaginationResult"]], capitalIssue?: [{ data: ResolverInputTypes["GetCapitalIssueByHashInput"]},ResolverInputTypes["CapitalIssue"]], capitalIssues?: [{ filter?: ResolverInputTypes["CapitalIssueFilter"] | undefined | null, options?: ResolverInputTypes["PaginationInput"] | undefined | null},ResolverInputTypes["PaginatedCapitalIssuesPaginationResult"]], +capitalProgramExpense?: [{ coopname: string, expense_hash: string},ResolverInputTypes["CapitalProgramExpense"]], +capitalProgramExpenses?: [{ coopname: string, options?: ResolverInputTypes["PaginationInput"] | undefined | null},ResolverInputTypes["PaginatedCapitalProgramExpensesPaginationResult"]], capitalProject?: [{ data: ResolverInputTypes["GetProjectInput"]},ResolverInputTypes["CapitalProject"]], capitalProjectWithRelations?: [{ data: ResolverInputTypes["GetProjectWithRelationsInput"]},ResolverInputTypes["CapitalProject"]], capitalProjects?: [{ filter?: ResolverInputTypes["CapitalProjectFilter"] | undefined | null, options?: ResolverInputTypes["PaginationInput"] | undefined | null},ResolverInputTypes["PaginatedCapitalProjectsPaginationResult"]], @@ -16718,6 +17456,13 @@ chatcoopListUtcDatesWithNewRoomMessages?: [{ data: ResolverInputTypes["ListUtcDa checkReportReadiness?: [{ reportType: ResolverInputTypes["ReportType"]},ResolverInputTypes["ReportReadinessView"]], cooperativeAgreements?: [{ coopname: string},ResolverInputTypes["CoopAgreement"]], cooperativePrograms?: [{ coopname: string},ResolverInputTypes["CooperativeProgram"]], +expenseFile?: [{ id: number},ResolverInputTypes["ExpenseFile"]], +expenseFilesByItem?: [{ coopname: string, item_hash: string, proposal_hash: string},ResolverInputTypes["ExpenseFile"]], +expenseFilesByProposal?: [{ coopname: string, proposal_hash: string},ResolverInputTypes["ExpenseFile"]], +expenseProposal?: [{ proposal_hash: string},ResolverInputTypes["ExpenseProposal"]], +expenseProposalsByCooperative?: [{ coopname: string, options?: ResolverInputTypes["PaginationInput"] | undefined | null},ResolverInputTypes["PaginatedExpenseProposalsPaginationResult"]], +expenseProposalsByMember?: [{ coopname: string, options?: ResolverInputTypes["PaginationInput"] | undefined | null, username: string},ResolverInputTypes["PaginatedExpenseProposalsPaginationResult"]], +expenseRequisitesByProposal?: [{ coopname: string, proposal_hash: string},ResolverInputTypes["ExpenseRequisite"]], getAccount?: [{ data: ResolverInputTypes["GetAccountInput"]},ResolverInputTypes["Account"]], getAccounts?: [{ data?: ResolverInputTypes["GetAccountsInput"] | undefined | null, options?: ResolverInputTypes["PaginationInput"] | undefined | null},ResolverInputTypes["AccountsPaginationResult"]], getActions?: [{ filters?: ResolverInputTypes["ActionFiltersInput"] | undefined | null, pagination?: ResolverInputTypes["PaginationInput"] | undefined | null},ResolverInputTypes["PaginatedActionsPaginationResult"]], @@ -16793,8 +17538,6 @@ getUserWebPushSubscriptions?: [{ data: ResolverInputTypes["GetUserSubscriptionsI Требуемые роли: chairman. */ getWebPushSubscriptionStats?:ResolverInputTypes["SubscriptionStatsDto"], listReportDrafts?: [{ filter?: ResolverInputTypes["ListReportDraftsFilterInput"] | undefined | null},ResolverInputTypes["ReportDraft"]], -membershipExit?: [{ coopname: string, username: string},ResolverInputTypes["MembershipExit"]], -membershipExitReturnPreview?: [{ coopname: string, username: string},ResolverInputTypes["MembershipExitReturnPreview"]], onecoopGetDocuments?: [{ data: ResolverInputTypes["GetOneCoopDocumentsInput"]},ResolverInputTypes["OneCoopDocumentsResponse"]], process?: [{ coopname: string, hash: string},ResolverInputTypes["ProcessView"]], processes?: [{ filter: ResolverInputTypes["ProcessesFilter"], pagination: ResolverInputTypes["PaginationInput"]},ResolverInputTypes["ProcessSummaryPaginationResult"]], @@ -17030,6 +17773,16 @@ validateReportEdits?: [{ editsJson: string, reportType: ResolverInputTypes["Repo year?:boolean | `@${string}`, __typename?: boolean | `@${string}` }>; + ["ReportExpenseItemInput"]: { + /** Фактически потраченная сумма по чекам (asset, например "800.0000 RUB"). Не указана — равна выданному авансу. */ + actual_amount?: string | undefined | null, + /** Имя кооператива. */ + coopname: string, + /** Хеш строки расхода (item). */ + item_hash: string, + /** Хеш сметы расхода (proposal). */ + proposal_hash: string +}; ["ReportHistoryFilterInput"]: { /** Лимит (макс 100, по умолчанию 20) */ limit?: number | undefined | null, @@ -17487,6 +18240,16 @@ validateReportEdits?: [{ editsJson: string, reportType: ResolverInputTypes["Repo username: string, /** Версия генератора, использованного для создания документа */ version: string +}; + ["ReturnExpenseItemInput"]: { + /** Имя кооператива. */ + coopname: string, + /** Хеш строки расхода (item). */ + item_hash: string, + /** Хеш сметы расхода (proposal). */ + proposal_hash: string, + /** Возвращаемая сумма (asset, например "50.0000 RUB"). */ + return_amount: string }; /** Тип сообщения в истории комнаты Matrix (текст или расшифрованное аудио) */ ["RoomMessageKind"]:RoomMessageKind; @@ -17866,6 +18629,16 @@ validateReportEdits?: [{ editsJson: string, reportType: ResolverInputTypes["Repo }; /** Статус истории в системе CAPITAL */ ["StoryStatus"]:StoryStatus; + ["SubmitExpenseReportInput"]: { + /** Комментарий пайщика к финализации отчёта (свободный текст, до 1000 символов). */ + comment?: string | undefined | null, + /** Имя кооператива. */ + coopname: string, + /** Хеш сметы расхода. */ + proposal_hash: string, + /** Итоговая сумма фактических расходов (asset, например "1500.0000 RUB"). */ + total_actual_amount?: string | undefined | null +}; ["SubmitVoteInput"]: { /** Имя аккаунта кооператива */ coopname: string, @@ -18232,6 +19005,26 @@ validateReportEdits?: [{ editsJson: string, reportType: ResolverInputTypes["Repo story_hash: string, /** Название истории */ title?: string | undefined | null +}; + ["UploadExpenseFileInput"]: { + /** SHA-256 содержимого, hex-lowercase (64 hex-символа). */ + checksum_sha256: string, + /** Содержимое файла, base64 без префикса data:. */ + content_base64: string, + /** Имя кооператива. */ + coopname: string, + /** Хеш строки расхода (пусто — файл уровня сметы). */ + item_hash?: string | undefined | null, + /** Назначение файла (платёжка/чек/возврат). */ + kind: ResolverInputTypes["ExpenseFileKind"], + /** MIME-тип содержимого. */ + mime_type: string, + /** Оригинальное имя файла — для отображения и поиска. */ + original_filename?: string | undefined | null, + /** Хеш сметы расхода. */ + proposal_hash: string, + /** Размер файла в байтах (для серверной валидации). */ + size_bytes: number }; ["UserAccount"]: AliasType<{ /** Метаинформация */ @@ -19896,6 +20689,20 @@ export type ModelTypes = { status?: ModelTypes["ContributorStatus"] | undefined | null, /** Фильтр по имени пользователя */ username?: string | undefined | null +}; + ["CapitalCreateProgramExpenseInput"]: { + /** Имя кооператива. */ + coopname: string, + /** Имя пайщика-создателя СЗ (председатель). */ + creator: string, + /** Описание программного расхода. */ + description: string, + /** Хэш СЗ-расхода (детерминированный, из UI). Он же станет proposal_hash в шасси. */ + expense_hash: string, + /** Строки расхода. Способ оплаты (аванс под отчёт / оплата по счёту) задаётся на каждой строке отдельно. */ + items: Array, + /** Подписанная СЗ-смета (document2, registry 2010). */ + statement: ModelTypes["ExpenseProposalStatementSignedDocumentInput"] }; /** Цикл разработки в системе CAPITAL */ ["CapitalCycle"]: { @@ -20256,6 +21063,38 @@ export type ModelTypes = { question: string, step: ModelTypes["CapitalOnboardingStep"], title?: string | undefined | null +}; + ["CapitalProgramExpense"]: { + callback?: ModelTypes["CapitalProgramExpenseCallback"] | undefined | null, + coopname: string, + created_at: string, + creator: string, + /** Имя инициатора (ФИО пайщика или название организации) */ + creator_name: string, + expense_hash: string, + items: Array, + source_wallet: string, + status: ModelTypes["ExpenseProposalStatus"], + total_actual: string, + total_planned: string, + updated_at: string +}; + ["CapitalProgramExpenseCallback"]: { + action: string, + contract: string, + data: string +}; + ["CapitalProgramExpenseItem"]: { + actual_amount: string, + description: string, + item_hash: string, + mechanics: ModelTypes["ExpenseMechanics"], + planned_amount: string, + recipient: string, + /** Имя получателя (ФИО пайщика или название организации) */ + recipient_name: string, + recipient_type: ModelTypes["ExpenseRecipientType"], + status: ModelTypes["ExpenseItemStatus"] }; /** Проект в системе CAPITAL с компонентами */ ["CapitalProject"]: { @@ -20811,6 +21650,10 @@ export type ModelTypes = { global_available_invest_pool: string, /** Флаг присутствия записи в блокчейне */ present: boolean, + /** Пул программных расходов — средства, переведённые под целевые расходы программы */ + program_expense_pool?: string | undefined | null, + /** Зарезервировано в пуле программных расходов под активные расходы (служебные записки в работе) */ + program_expense_reserved?: string | undefined | null, /** Доступная сумма членских взносов по программе */ program_membership_available: string, /** Накопительное вознаграждение на долю в членских взносах */ @@ -20965,6 +21808,12 @@ export type ModelTypes = { project_hash?: string | undefined | null, /** Имя пользователя (опционально) */ username?: string | undefined | null +}; + ["CapitalTopupProgramExpenseInput"]: { + /** Сумма пополнения (asset, eg "10000.0000 RUB"). */ + amount: string, + /** Имя кооператива. */ + coopname: string }; /** Голос в системе CAPITAL */ ["CapitalVote"]: { @@ -21613,6 +22462,22 @@ export type ModelTypes = { project_hash: string, /** Служебная записка о расходе */ statement: ModelTypes["SignedDigitalDocumentInput"] +}; + ["CreateExpenseProposalInput"]: { + /** Callback на финализацию closeexp (опционально). */ + callback?: ModelTypes["ExpenseCallbackInput"] | undefined | null, + /** Имя кооператива. */ + coopname: string, + /** Строки расхода (массив items). */ + items: Array, + /** Хеш сметы расхода (детерминированный, из UI). */ + proposal_hash: string, + /** Источник средств (eosio::name кошелька-источника, eg "w.cap.blago"). */ + source_wallet: string, + /** Подписанная СЗ-смета (document2, registry 2010). */ + statement: ModelTypes["ExpenseProposalStatementSignedDocumentInput"], + /** Имя пайщика-создателя СЗ. */ + username: string }; ["CreateIndividualDataInput"]: { /** Дата рождения */ @@ -21691,16 +22556,6 @@ export type ModelTypes = { ["CreateMatrixAccountInputDTO"]: { password: string, username: string -}; - ["CreateMembershipExitInput"]: { - /** Имя аккаунта кооператива */ - coopname: string, - /** Хеш процесса выхода (генерируется на клиенте) */ - exit_hash: string, - /** Подписанное пайщиком заявление о выходе из кооператива */ - statement: ModelTypes["MembershipExitApplicationSignedDocumentInput"], - /** Имя пайщика, выходящего из кооператива */ - username: string }; ["CreateOrganizationDataInput"]: { /** Банковский счет организации */ @@ -22276,6 +23131,44 @@ export type ModelTypes = { /** ОГРН */ ogrn: string }; + ["ExpenseCallbackInput"]: { + /** Action-метод */ + action?: string | undefined | null, + /** Контракт-целевой */ + contract?: string | undefined | null, + /** Payload (hex bytes) */ + data?: string | undefined | null +}; + /** Запись о первичном файле расхода (платёжка/чек/возврат). */ +["ExpenseFile"]: { + /** SHA-256 содержимого, hex-lowercase. */ + checksum_sha256: string, + /** Имя кооператива (scope). */ + coopname: string, + /** Внутренний ID записи. */ + id: number, + /** Хеш строки расхода (если файл уровня item). */ + item_hash?: string | undefined | null, + /** Назначение файла. */ + kind: ModelTypes["ExpenseFileKind"], + /** MIME-тип содержимого. */ + mime_type: string, + /** Оригинальное имя загруженного файла. */ + original_filename?: string | undefined | null, + /** Хеш сметы расхода. */ + proposal_hash: string, + /** Короткоживущий URL на скачивание (HMAC-signed). */ + read_url?: string | undefined | null, + /** Размер файла в байтах. */ + size_bytes: number, + /** MinIO-ключ внутри бакета. */ + storage_key: string, + /** Когда загружено. */ + uploaded_at: ModelTypes["DateTime"], + /** Кто загрузил (username). */ + uploaded_by_username: string +}; + ["ExpenseFileKind"]:ExpenseFileKind; ["ExpenseFilter"]: { /** Фильтр по ID фонда */ fundId?: string | undefined | null, @@ -22285,6 +23178,294 @@ export type ModelTypes = { status?: string | undefined | null, /** Фильтр по имени пользователя */ username?: string | undefined | null +}; + /** Строка сметы расхода. */ +["ExpenseItem"]: { + /** Фактическая сумма (после оплаты/отчёта). */ + actual_amount?: string | undefined | null, + /** Назначение/описание строки. */ + description: string, + /** Хеш строки расхода. */ + item_hash: string, + /** Способ оплаты. */ + mechanics: ModelTypes["ExpenseMechanics"], + /** Планируемая сумма. */ + planned_amount: string, + /** Идентификатор получателя. */ + recipient?: string | undefined | null, + /** Тип получателя платежа. */ + recipient_type: ModelTypes["ExpenseRecipientType"], + /** Статус строки. */ + status: ModelTypes["ExpenseItemStatus"] +}; + ["ExpenseItemInput"]: { + /** Описание назначения расхода. */ + description: string, + /** Хеш строки расхода (детерминированный, из UI). */ + item_hash: string, + /** Способ оплаты (ADVANCE / DIRECT). */ + mechanics: ModelTypes["ExpenseMechanics"], + /** Идентификатор сохранённых реквизитов получателя-пайщика — реквизиты снимаются в момент создания и прикладываются к платежу. */ + payment_method_id?: string | undefined | null, + /** Назначение платежа (для оплаты по счёту) — фиксируется в снимке для кассира. */ + payment_purpose?: string | undefined | null, + /** Планируемая сумма (asset, eg "1000.0000 RUB"). */ + planned_amount: string, + /** Получатель: username пайщика; для организации — пустая строка (аккаунта в кооперативе нет). */ + recipient: string, + /** Тип получателя. */ + recipient_type: ModelTypes["ExpenseRecipientType"], + /** Реквизиты получателя-организации (вводятся вручную). */ + requisites?: string | undefined | null +}; + ["ExpenseItemStatus"]:ExpenseItemStatus; + ["ExpenseMechanics"]:ExpenseMechanics; + /** Смета расхода (СЗ). */ +["ExpenseProposal"]: { + /** Дата создания записи */ + _created_at: ModelTypes["DateTime"], + /** Внутренний ID базы данных */ + _id: string, + /** Дата последнего обновления записи */ + _updated_at: ModelTypes["DateTime"], + /** Номер блока крайней синхронизации с блокчейном */ + block_num?: number | undefined | null, + /** Сырой статус из блокчейна (uint8). */ + blockchain_status?: number | undefined | null, + /** Имя кооператива (scope). */ + coopname: string, + /** Время создания (chain). */ + created_at?: string | undefined | null, + /** Решение совета о расходе (DocumentAggregate). */ + decision_doc?: ModelTypes["DocumentAggregate"] | undefined | null, + /** ID в блокчейне. */ + id?: number | undefined | null, + /** Строки сметы. */ + items: Array, + /** Флаг присутствия записи в блокчейне */ + present: boolean, + /** Хеш сметы расхода. */ + proposal_hash: string, + /** Кошелёк-источник средств. */ + source_wallet?: string | undefined | null, + /** Заявление пайщика о расходе (DocumentAggregate). */ + statement_doc?: ModelTypes["DocumentAggregate"] | undefined | null, + /** Доменный статус сметы. */ + status: ModelTypes["ExpenseProposalStatus"], + /** Сумма всех строк (факт). */ + total_actual?: string | undefined | null, + /** Сумма всех строк (план). */ + total_planned?: string | undefined | null, + /** Время последнего обновления (chain). */ + updated_at?: string | undefined | null, + /** Создатель сметы (username). */ + username?: string | undefined | null +}; + ["ExpenseProposalDecisionBodyInput"]: { + /** Род решения (approve / decline) */ + kind: string, + /** Причина отказа (для decline) */ + reason?: string | undefined | null +}; + ["ExpenseProposalDecisionGenerateDocumentInput"]: { + /** Номер блока, на котором был создан документ */ + block_num?: number | undefined | null, + /** Название кооператива, связанное с документом */ + coopname: string, + /** Дата и время создания документа */ + created_at?: string | undefined | null, + /** Идентификатор решения совета (повестка) — источник данных голосования */ + decision_id: number, + /** Имя генератора, использованного для создания документа */ + generator?: string | undefined | null, + /** Позиции расхода */ + items: Array, + /** Язык документа */ + lang?: string | undefined | null, + /** Ссылки, связанные с документом */ + links?: Array | undefined | null, + /** Шапка СЗ */ + proposal: ModelTypes["ExpenseProposalDecisionHeaderInput"], + /** Хеш сметы расхода */ + proposal_hash: string, + /** Резолюция совета (утвердить / отказать) */ + resolution: ModelTypes["ExpenseProposalDecisionBodyInput"], + /** Часовой пояс, в котором был создан документ */ + timezone?: string | undefined | null, + /** Название документа */ + title?: string | undefined | null, + /** Имя пользователя, создавшего документ */ + username: string, + /** Версия генератора, использованного для создания документа */ + version?: string | undefined | null +}; + ["ExpenseProposalDecisionHeaderInput"]: { + deadline?: string | undefined | null, + description: string, + fund_name?: string | undefined | null, + items_count: number, + source_wallet: string, + total_amount: string +}; + ["ExpenseProposalDecisionItemInput"]: { + amount: string, + description: string, + mechanics: string, + number: string, + recipient_type: string +}; + ["ExpenseProposalHeaderInput"]: { + /** Срок исполнения («в срок до»), формат DD.MM.YYYY */ + deadline?: string | undefined | null, + /** Описание цели расходов */ + description: string, + /** Фонд списания — подставляется сервером из параметров шасси расходов, передавать не нужно */ + fund_name?: string | undefined | null, + /** Количество позиций */ + items_count: number, + /** Кошелёк-источник */ + source_wallet: string, + /** Итоговая сумма расходов */ + total_amount: string +}; + ["ExpenseProposalItemInput"]: { + /** Сумма строки */ + amount: string, + /** Описание расхода */ + description: string, + /** Способ оплаты (ADVANCE / DIRECT) */ + mechanics: string, + /** Порядковый номер строки */ + number: string, + /** Идентификатор сохранённых реквизитов получателя-пайщика — сервер подставит полные реквизиты в документ. */ + payment_method_id?: string | undefined | null, + /** Назначение платежа — отдельной строкой после реквизитов */ + payment_purpose?: string | undefined | null, + /** Имя получателя */ + recipient_name?: string | undefined | null, + /** Тип получателя (SELF / MEMBER / ORG) */ + recipient_type: string, + /** Имя аккаунта получателя-пайщика (владелец реквизитов). */ + recipient_username?: string | undefined | null, + /** Реквизиты получателя */ + requisites?: string | undefined | null +}; + ["ExpenseProposalSignedItemInput"]: { + /** Сумма строки */ + amount: string, + /** Описание расхода */ + description: string, + /** Способ оплаты (ADVANCE / DIRECT) */ + mechanics: string, + /** Порядковый номер строки */ + number: string, + /** Тип получателя (SELF / MEMBER / ORG) */ + recipient_type: string +}; + ["ExpenseProposalStatementGenerateDocumentInput"]: { + /** Номер блока, на котором был создан документ */ + block_num?: number | undefined | null, + /** Название кооператива, связанное с документом */ + coopname: string, + /** Дата и время создания документа */ + created_at?: string | undefined | null, + /** Имя генератора, использованного для создания документа */ + generator?: string | undefined | null, + /** Позиции расхода */ + items: Array, + /** Язык документа */ + lang?: string | undefined | null, + /** Ссылки, связанные с документом */ + links?: Array | undefined | null, + /** Шапка СЗ */ + proposal: ModelTypes["ExpenseProposalHeaderInput"], + /** Хеш сметы расхода (детерминированный) */ + proposal_hash: string, + /** Часовой пояс, в котором был создан документ */ + timezone?: string | undefined | null, + /** Название документа */ + title?: string | undefined | null, + /** Имя пользователя, создавшего документ */ + username: string, + /** Версия генератора, использованного для создания документа */ + version?: string | undefined | null +}; + ["ExpenseProposalStatementSignedDocumentInput"]: { + /** Хэш содержимого документа */ + doc_hash: string, + /** Общий хэш (doc_hash + meta_hash) */ + hash: string, + /** Метаинформация СЗ-заявления */ + meta: ModelTypes["ExpenseProposalStatementSignedMetaDocumentInput"], + /** Хэш мета-данных */ + meta_hash: string, + /** Вектор подписей */ + signatures: Array, + /** Версия стандарта документа */ + version: string +}; + ["ExpenseProposalStatementSignedMetaDocumentInput"]: { + /** Номер блока, на котором был создан документ */ + block_num: number, + /** Название кооператива, связанное с документом */ + coopname: string, + /** Дата и время создания документа */ + created_at: string, + /** Идентификатор приватных данных документа off-chain (реквизиты/имя/назначение) */ + doc_data_hash: string, + /** Имя генератора, использованного для создания документа */ + generator: string, + /** Публичные позиции расхода (без реквизитов) */ + items: Array, + /** Язык документа */ + lang: string, + /** Ссылки, связанные с документом */ + links: Array, + /** Шапка СЗ */ + proposal: ModelTypes["ExpenseProposalHeaderInput"], + /** Хеш сметы расхода (детерминированный) */ + proposal_hash: string, + /** ID документа в реестре */ + registry_id: number, + /** Часовой пояс, в котором был создан документ */ + timezone: string, + /** Название документа */ + title: string, + /** Имя пользователя, создавшего документ */ + username: string, + /** Версия генератора, использованного для создания документа */ + version: string +}; + ["ExpenseProposalStatus"]:ExpenseProposalStatus; + ["ExpenseRecipientType"]:ExpenseRecipientType; + ["ExpenseReportOutcome"]:ExpenseReportOutcome; + ["ExpenseReportResult"]: { + /** Исход отчёта. */ + outcome: ModelTypes["ExpenseReportOutcome"], + /** Сумма разницы к расчёту (asset), при недо-/перерасходе. */ + settlement_amount?: string | undefined | null, + /** Хэш заведённой платёжки расчёта (возврат/доплата). */ + settlement_payment_hash?: string | undefined | null, + /** Транзакция закрытия позиции (только при CLOSED). */ + transaction?: ModelTypes["Transaction"] | undefined | null +}; + ["ExpenseRequisite"]: { + /** Имя кооператива. */ + coopname: string, + /** Снимок данных платёжного метода на момент подачи СЗ. */ + data?: ModelTypes["JSON"] | undefined | null, + /** Хеш строки расхода (item). */ + item_hash: string, + /** Тип платёжного метода пайщика (СБП / банковский перевод). */ + method_type?: string | undefined | null, + /** Назначение платежа для поручения кассиру. */ + payment_purpose?: string | undefined | null, + /** Хеш сметы расхода (proposal). */ + proposal_hash: string, + /** Получатель платежа (аккаунт пайщика или название организации). */ + recipient: string, + /** Реквизиты строкой — как в документе служебной записки. */ + requisites: string }; ["ExpenseStatus"]:ExpenseStatus; /** Расширенное действие блокчейна с сертификатом пользователя, совершившего его. */ @@ -23478,118 +24659,6 @@ export type ModelTypes = { /** Количество голосов за */ votes_for: number }; - ["MembershipExit"]: { - /** Дата подачи заявления на выход */ - created_at: string, - /** Хеш процесса выхода */ - exit_hash: string, - /** Сумма к возврату (фиксируется советом при одобрении; до одобрения — 0) */ - quantity: string, - /** Статус процесса выхода */ - status: ModelTypes["MembershipExitStatus"] -}; - ["MembershipExitApplicationGenerateDocumentInput"]: { - /** Номер блока, на котором был создан документ */ - block_num?: number | undefined | null, - /** Название кооператива, связанное с документом */ - coopname: string, - /** Дата и время создания документа */ - created_at?: string | undefined | null, - /** Имя генератора, использованного для создания документа */ - generator?: string | undefined | null, - /** Язык документа */ - lang?: string | undefined | null, - /** Ссылки, связанные с документом */ - links?: Array | undefined | null, - /** Флаг пропуска сохранения документа (используется для предварительной генерации и демонстрации пользователю) */ - skip_save: boolean, - /** Часовой пояс, в котором был создан документ */ - timezone?: string | undefined | null, - /** Название документа */ - title?: string | undefined | null, - /** Имя пользователя, создавшего документ */ - username: string, - /** Версия генератора, использованного для создания документа */ - version?: string | undefined | null -}; - ["MembershipExitApplicationSignedDocumentInput"]: { - /** Хэш содержимого документа */ - doc_hash: string, - /** Общий хэш (doc_hash + meta_hash) */ - hash: string, - meta: ModelTypes["MembershipExitApplicationSignedMetaDocumentInput"], - /** Хэш мета-данных */ - meta_hash: string, - /** Вектор подписей */ - signatures: Array, - /** Версия стандарта документа */ - version: string -}; - ["MembershipExitApplicationSignedMetaDocumentInput"]: { - /** Номер блока, на котором был создан документ */ - block_num: number, - /** Название кооператива, связанное с документом */ - coopname: string, - /** Дата и время создания документа */ - created_at: string, - /** Имя генератора, использованного для создания документа */ - generator: string, - /** Язык документа */ - lang: string, - /** Ссылки, связанные с документом */ - links: Array, - /** ID документа в реестре */ - registry_id: number, - /** Флаг пропуска сохранения документа (используется для предварительной генерации и демонстрации пользователю) */ - skip_save: boolean, - /** Часовой пояс, в котором был создан документ */ - timezone: string, - /** Название документа */ - title: string, - /** Имя пользователя, создавшего документ */ - username: string, - /** Версия генератора, использованного для создания документа */ - version: string -}; - ["MembershipExitDecisionGenerateDocumentInput"]: { - /** Номер блока, на котором был создан документ */ - block_num?: number | undefined | null, - /** Название кооператива, связанное с документом */ - coopname: string, - /** Дата и время создания документа */ - created_at?: string | undefined | null, - /** Идентификатор протокола решения собрания совета */ - decision_id: number, - /** Имя генератора, использованного для создания документа */ - generator?: string | undefined | null, - /** Язык документа */ - lang?: string | undefined | null, - /** Ссылки, связанные с документом */ - links?: Array | undefined | null, - /** Часовой пояс, в котором был создан документ */ - timezone?: string | undefined | null, - /** Название документа */ - title?: string | undefined | null, - /** Имя пользователя, создавшего документ */ - username: string, - /** Версия генератора, использованного для создания документа */ - version?: string | undefined | null -}; - ["MembershipExitResult"]: { - /** Хеш созданного процесса выхода */ - exit_hash: string, - /** Статус процесса выхода после подачи (ожидает подтверждения по ссылке из письма) */ - status: ModelTypes["MembershipExitStatus"] -}; - ["MembershipExitReturnPreview"]: { - /** Минимальный паевой взнос пайщика */ - minimum_contribution: string, - /** Целевой паевой взнос пайщика */ - share_contribution: string, - /** Итоговая сумма к возврату (минимальный + целевой паевой) */ - total: string -}; - ["MembershipExitStatus"]:MembershipExitStatus; ["MissingRequisiteField"]: { key: string, label: string, @@ -23659,8 +24728,6 @@ export type ModelTypes = { Требуемые роли: chairman. */ authorizeDecision: ModelTypes["Transaction"], - /** Отменить заявление на выход до подтверждения по email. */ - cancelMembershipExit: boolean, /** Отменить заявку */ cancelRequest: ModelTypes["Transaction"], /** Добавление автора проекта в CAPITAL контракте @@ -23719,6 +24786,10 @@ export type ModelTypes = { Требуемые роли: chairman, member. */ capitalCreateProcessTemplate: ModelTypes["ProcessTemplate"], + /** Создание программного расхода капитала через шасси expense. + +Требуемые роли: chairman, member. */ + capitalCreateProgramExpense: ModelTypes["Transaction"], /** Инвестирование в программу благорост (денежная программная инвестиция) Требуемые роли: participant. */ @@ -23943,6 +25014,10 @@ export type ModelTypes = { Требуемые роли: chairman, member, user. */ capitalSubmitVote: ModelTypes["Transaction"], + /** Пополнение пула программных расходов капитала из инвестиционного пула. + +Требуемые роли: chairman. */ + capitalTopupProgramExpensePool: ModelTypes["Transaction"], /** Обновление задачи в CAPITAL контракте Требуемые роли: chairman, member, user. */ @@ -24017,8 +25092,6 @@ export type ModelTypes = { Требуемые роли: chairman, member. */ confirmAgreement: ModelTypes["Transaction"], - /** Подтвердить выход из кооператива по ссылке из письма. Проверяет токен и отправляет ранее подписанное заявление в блокчейн. */ - confirmMembershipExit: ModelTypes["MembershipExitResult"], /** Подтвердить получение имущества Уполномоченным лицом от Заказчика по новации и акту приёмки-передачи */ confirmReceiveOnRequest: ModelTypes["Transaction"], /** Подтвердить поставку имущества Поставщиком по заявке Заказчика и акту приёма-передачи */ @@ -24037,12 +25110,14 @@ export type ModelTypes = { Требуемые роли: chairman, member. */ createDepositPayment: ModelTypes["GatewayPayment"], + /** Подать СЗ-расход (создать смету с подписью пайщика/председателя). + +Требуемые роли: chairman, member. */ + createExpenseProposal: ModelTypes["Transaction"], /** Создание объекта регистрационного платежа производится мутацией createInitialPayment. Выполнение мутации возвращает идентификатор платежа и данные для его совершения в зависимости от выбранного платежного провайдера. Требуемые роли: chairman, member. */ createInitialPayment: ModelTypes["GatewayPayment"], - /** Подать подписанное заявление на выход из кооператива. Запускает рассмотрение советом и последующий возврат паевого взноса. */ - createMembershipExit: ModelTypes["MembershipExitResult"], /** Создать предложение на поставку имущества */ createParentOffer: ModelTypes["Transaction"], /** Создать повестку дня и проект решения, и сохранить в хранилище для дальнейшей генерации документа и его публикации @@ -24131,16 +25206,18 @@ export type ModelTypes = { generateConvertToAxonStatement: ModelTypes["GeneratedDocument"], /** Универсальная генерация документа с произвольными данными (только для председателя) */ generateDocument: ModelTypes["GeneratedDocument"], + /** Сгенерировать документ-решение по СЗ (registry 2011) для последующей подписи. + +Требуемые роли: chairman. */ + generateExpenseProposalDecisionDocument: ModelTypes["GeneratedDocument"], + /** Сгенерировать документ СЗ-заявления (registry 2010) для последующей подписи. + +Требуемые роли: chairman, member. */ + generateExpenseProposalStatementDocument: ModelTypes["GeneratedDocument"], /** Сгенерировать протокол решения по предложенной повестке Требуемые роли: chairman, member. */ generateFreeDecision: ModelTypes["GeneratedDocument"], - /** Сгенерировать документ заявления о выходе из кооператива. */ - generateMembershipExitApplication: ModelTypes["GeneratedDocument"], - /** Сгенерировать документ решения собрания совета о выходе пайщика. - -Требуемые роли: chairman, member. */ - generateMembershipExitDecision: ModelTypes["GeneratedDocument"], /** Сгенерировать документ заявления о вступлении в кооператив. Требуемые роли: chairman, member. */ @@ -24235,6 +25312,14 @@ export type ModelTypes = { Требуемые роли: chairman, member. */ notifyOnAnnualGeneralMeet: ModelTypes["MeetAggregate"], + /** Доплатить сумму перерасхода по строке расхода (ADVANCE-механика). + +Требуемые роли: chairman. */ + overspendExpenseItem: ModelTypes["Transaction"], + /** Оплатить строку расхода (выдача аванса ADVANCE или прямая оплата DIRECT). + +Требуемые роли: chairman. */ + payExpenseItem: ModelTypes["Transaction"], /** Обрабатывает подписанное заявление на конвертацию и выполняет блокчейн-транзакцию Требуемые роли: member, chairman. */ @@ -24257,6 +25342,10 @@ export type ModelTypes = { Требуемые роли: chairman, member. */ registerParticipant: ModelTypes["Account"], + /** Отчитаться по строке-авансу: при совпадении факта с авансом — закрыть позицию; при недо-/перерасходе — завести платёжку расчёта разницы. + +Требуемые роли: chairman, member, user. */ + reportExpenseItem: ModelTypes["ExpenseReportResult"], /** Переотправить уведомление (force-постановка новой строки в очередь доставки) Требуемые роли: chairman. */ @@ -24269,6 +25358,10 @@ export type ModelTypes = { Требуемые роли: chairman. */ restartAnnualGeneralMeet: ModelTypes["MeetAggregate"], + /** Вернуть неиспользованный аванс по строке расхода (ADVANCE-остаток). + +Требуемые роли: chairman, member, user. */ + returnExpenseItem: ModelTypes["Transaction"], /** Сохранить/обновить черновик формы отчёта (upsert по owner+type+year+period) Требуемые роли: chairman. */ @@ -24297,6 +25390,10 @@ export type ModelTypes = { startInstall: ModelTypes["StartInstallResult"], /** Выслать токен для замены приватного ключа аккаунта на электронную почту */ startResetKey: boolean, + /** Финализировать СЗ-отчёт по смете расхода (все items закрыты — оплата/чек/возврат). + +Требуемые роли: chairman, member. */ + submitExpenseReport: ModelTypes["Transaction"], /** Подтвердить поставку имущества Поставщиком по заявке Заказчика и акту приёма-передачи */ supplyOnRequest: ModelTypes["Transaction"], /** Запустить воркфлоу уведомлений (только для председателя или server-secret) @@ -24333,6 +25430,10 @@ export type ModelTypes = { Требуемые роли: chairman. */ updateSystem: ModelTypes["SystemInfo"], + /** Загрузить первичный файл расхода (платёжка/чек/возврат) в бакет expenses:files. + +Требуемые роли: chairman, member, user. */ + uploadExpenseFile: ModelTypes["ExpenseFile"], /** Подтвердить email адрес пользователя */ verifyEmail: boolean, /** Голосование на общем собрании пайщиков @@ -24555,6 +25656,16 @@ export type ModelTypes = { type: string, /** Имя аккаунта организации */ username: string +}; + ["OverspendExpenseItemInput"]: { + /** Имя кооператива. */ + coopname: string, + /** Хеш строки расхода (item). */ + item_hash: string, + /** Сумма доплаты сверх planned_amount (asset, например "200.0000 RUB"). */ + overspend_amount: string, + /** Хеш сметы расхода (proposal). */ + proposal_hash: string }; ["PaginatedActionsPaginationResult"]: { /** Текущая страница */ @@ -24675,6 +25786,16 @@ export type ModelTypes = { totalCount: number, /** Общее количество страниц */ totalPages: number +}; + ["PaginatedCapitalProgramExpensesPaginationResult"]: { + /** Текущая страница */ + currentPage: number, + /** Элементы текущей страницы */ + items: Array, + /** Общее количество элементов */ + totalCount: number, + /** Общее количество страниц */ + totalPages: number }; ["PaginatedCapitalProjectsPaginationResult"]: { /** Текущая страница */ @@ -24775,6 +25896,16 @@ export type ModelTypes = { totalCount: number, /** Общее количество страниц */ totalPages: number +}; + ["PaginatedExpenseProposalsPaginationResult"]: { + /** Текущая страница */ + currentPage: number, + /** Элементы текущей страницы */ + items: Array, + /** Общее количество элементов */ + totalCount: number, + /** Общее количество страниц */ + totalPages: number }; ["PaginatedGatewayPaymentsPaginationResult"]: { /** Текущая страница */ @@ -24935,6 +26066,16 @@ export type ModelTypes = { issued_by: string, number: number, series: number +}; + ["PayExpenseItemInput"]: { + /** Фактическая сумма оплаты (asset, например "100.0000 RUB"). */ + actual_amount: string, + /** Имя кооператива. */ + coopname: string, + /** Хеш строки расхода (item). */ + item_hash: string, + /** Хеш сметы расхода (proposal). */ + proposal_hash: string }; ["PaymentDetails"]: { /** Сумма платежа с учетом комиссии */ @@ -24960,6 +26101,8 @@ export type ModelTypes = { direction?: ModelTypes["PaymentDirection"] | undefined | null, /** Хэш платежа */ hash?: string | undefined | null, + /** Хэш расхода (служебной записки): вернёт все платежи, связанные с этим расходом */ + proposal_hash?: string | undefined | null, /** Провайдер платежа */ provider?: string | undefined | null, /** Статус платежа */ @@ -25516,6 +26659,14 @@ export type ModelTypes = { Требуемые роли: chairman, member, user. */ capitalIssues: ModelTypes["PaginatedCapitalIssuesPaginationResult"], + /** Программный расход по expense_hash. + +Требуемые роли: chairman, member. */ + capitalProgramExpense?: ModelTypes["CapitalProgramExpense"] | undefined | null, + /** Список программных расходов капитала (через шасси expense). + +Требуемые роли: chairman, member. */ + capitalProgramExpenses: ModelTypes["PaginatedCapitalProgramExpensesPaginationResult"], /** Получение проекта по хешу с компонентами */ capitalProject?: ModelTypes["CapitalProject"] | undefined | null, /** Получение проекта с полными отношениями по хешу проекта */ @@ -25620,6 +26771,34 @@ export type ModelTypes = { cooperativeAgreements: Array, /** Целевые потребительские программы кооператива (id, тип, активность, draft_id) */ cooperativePrograms: Array, + /** Получить запись о файле + свежий короткоживущий read-URL. + +Требуемые роли: chairman, member, user. */ + expenseFile: ModelTypes["ExpenseFile"], + /** Список файлов строки расхода (без read-URL — запрос отдельно по id). + +Требуемые роли: chairman, member, user. */ + expenseFilesByItem: Array, + /** Список файлов сметы расхода (без read-URL — запрос отдельно по id). + +Требуемые роли: chairman, member, user. */ + expenseFilesByProposal: Array, + /** Получить смету расхода по хешу. + +Требуемые роли: chairman, member, user. */ + expenseProposal?: ModelTypes["ExpenseProposal"] | undefined | null, + /** Список смет расходов кооператива (paginated). + +Требуемые роли: chairman, member. */ + expenseProposalsByCooperative: ModelTypes["PaginatedExpenseProposalsPaginationResult"], + /** Список смет расходов пайщика (свои/созданные им, paginated). + +Требуемые роли: chairman, member, user. */ + expenseProposalsByMember: ModelTypes["PaginatedExpenseProposalsPaginationResult"], + /** Снимки реквизитов получателей по строкам СЗ (персональные данные — только совету; в блокчейн не пишутся). + +Требуемые роли: chairman, member. */ + expenseRequisitesByProposal: Array, /** Получить сводную информацию о аккаунте Требуемые роли: chairman, member. */ @@ -25800,10 +26979,6 @@ export type ModelTypes = { Требуемые роли: chairman. */ listReportDrafts: Array, - /** Текущий процесс выхода пайщика из кооператива (статус заявления и планируемая сумма возврата). null — активного выхода нет. */ - membershipExit?: ModelTypes["MembershipExit"] | undefined | null, - /** Предварительный расчёт суммы возврата паевого взноса при выходе пайщика (минимальный + целевой паевой). Ориентир для пайщика; итог фиксирует совет. */ - membershipExitReturnPreview: ModelTypes["MembershipExitReturnPreview"], /** Получение документов кооператива для синхронизации с 1С. Требует секретный ключ в заголовке x-onecoop-secret-key. */ onecoopGetDocuments: ModelTypes["OneCoopDocumentsResponse"], /** Получить полную картину процесса ledger2 по process_hash @@ -26041,6 +27216,16 @@ export type ModelTypes = { reportType: ModelTypes["ReportType"], updatedAt: ModelTypes["DateTime"], year: number +}; + ["ReportExpenseItemInput"]: { + /** Фактически потраченная сумма по чекам (asset, например "800.0000 RUB"). Не указана — равна выданному авансу. */ + actual_amount?: string | undefined | null, + /** Имя кооператива. */ + coopname: string, + /** Хеш строки расхода (item). */ + item_hash: string, + /** Хеш сметы расхода (proposal). */ + proposal_hash: string }; ["ReportHistoryFilterInput"]: { /** Лимит (макс 100, по умолчанию 20) */ @@ -26486,6 +27671,16 @@ export type ModelTypes = { username: string, /** Версия генератора, использованного для создания документа */ version: string +}; + ["ReturnExpenseItemInput"]: { + /** Имя кооператива. */ + coopname: string, + /** Хеш строки расхода (item). */ + item_hash: string, + /** Хеш сметы расхода (proposal). */ + proposal_hash: string, + /** Возвращаемая сумма (asset, например "50.0000 RUB"). */ + return_amount: string }; ["RoomMessageKind"]:RoomMessageKind; ["SaveReportDraftInput"]: { @@ -26854,6 +28049,16 @@ export type ModelTypes = { project_hash: string }; ["StoryStatus"]:StoryStatus; + ["SubmitExpenseReportInput"]: { + /** Комментарий пайщика к финализации отчёта (свободный текст, до 1000 символов). */ + comment?: string | undefined | null, + /** Имя кооператива. */ + coopname: string, + /** Хеш сметы расхода. */ + proposal_hash: string, + /** Итоговая сумма фактических расходов (asset, например "1500.0000 RUB"). */ + total_actual_amount?: string | undefined | null +}; ["SubmitVoteInput"]: { /** Имя аккаунта кооператива */ coopname: string, @@ -27209,6 +28414,26 @@ export type ModelTypes = { story_hash: string, /** Название истории */ title?: string | undefined | null +}; + ["UploadExpenseFileInput"]: { + /** SHA-256 содержимого, hex-lowercase (64 hex-символа). */ + checksum_sha256: string, + /** Содержимое файла, base64 без префикса data:. */ + content_base64: string, + /** Имя кооператива. */ + coopname: string, + /** Хеш строки расхода (пусто — файл уровня сметы). */ + item_hash?: string | undefined | null, + /** Назначение файла (платёжка/чек/возврат). */ + kind: ModelTypes["ExpenseFileKind"], + /** MIME-тип содержимого. */ + mime_type: string, + /** Оригинальное имя файла — для отображения и поиска. */ + original_filename?: string | undefined | null, + /** Хеш сметы расхода. */ + proposal_hash: string, + /** Размер файла в байтах (для серверной валидации). */ + size_bytes: number }; ["UserAccount"]: { /** Метаинформация */ @@ -27417,7 +28642,10 @@ export type ModelTypes = { } export type GraphQLTypes = { - ["AcceptChildOrderInput"]: { + // ------------------------------------------------------; + // THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY); + // ------------------------------------------------------; + ["AcceptChildOrderInput"]: { /** Имя аккаунта кооператива */ coopname: string, /** Подписанное заявление на имущественный паевый взнос */ @@ -28944,6 +30172,20 @@ export type GraphQLTypes = { status?: GraphQLTypes["ContributorStatus"] | undefined | null, /** Фильтр по имени пользователя */ username?: string | undefined | null +}; + ["CapitalCreateProgramExpenseInput"]: { + /** Имя кооператива. */ + coopname: string, + /** Имя пайщика-создателя СЗ (председатель). */ + creator: string, + /** Описание программного расхода. */ + description: string, + /** Хэш СЗ-расхода (детерминированный, из UI). Он же станет proposal_hash в шасси. */ + expense_hash: string, + /** Строки расхода. Способ оплаты (аванс под отчёт / оплата по счёту) задаётся на каждой строке отдельно. */ + items: Array, + /** Подписанная СЗ-смета (document2, registry 2010). */ + statement: GraphQLTypes["ExpenseProposalStatementSignedDocumentInput"] }; /** Цикл разработки в системе CAPITAL */ ["CapitalCycle"]: { @@ -29324,6 +30566,44 @@ export type GraphQLTypes = { question: string, step: GraphQLTypes["CapitalOnboardingStep"], title?: string | undefined | null +}; + ["CapitalProgramExpense"]: { + __typename: "CapitalProgramExpense", + callback?: GraphQLTypes["CapitalProgramExpenseCallback"] | undefined | null, + coopname: string, + created_at: string, + creator: string, + /** Имя инициатора (ФИО пайщика или название организации) */ + creator_name: string, + expense_hash: string, + items: Array, + source_wallet: string, + status: GraphQLTypes["ExpenseProposalStatus"], + total_actual: string, + total_planned: string, + updated_at: string, + ['...on CapitalProgramExpense']: Omit +}; + ["CapitalProgramExpenseCallback"]: { + __typename: "CapitalProgramExpenseCallback", + action: string, + contract: string, + data: string, + ['...on CapitalProgramExpenseCallback']: Omit +}; + ["CapitalProgramExpenseItem"]: { + __typename: "CapitalProgramExpenseItem", + actual_amount: string, + description: string, + item_hash: string, + mechanics: GraphQLTypes["ExpenseMechanics"], + planned_amount: string, + recipient: string, + /** Имя получателя (ФИО пайщика или название организации) */ + recipient_name: string, + recipient_type: GraphQLTypes["ExpenseRecipientType"], + status: GraphQLTypes["ExpenseItemStatus"], + ['...on CapitalProgramExpenseItem']: Omit }; /** Проект в системе CAPITAL с компонентами */ ["CapitalProject"]: { @@ -29904,6 +31184,10 @@ export type GraphQLTypes = { global_available_invest_pool: string, /** Флаг присутствия записи в блокчейне */ present: boolean, + /** Пул программных расходов — средства, переведённые под целевые расходы программы */ + program_expense_pool?: string | undefined | null, + /** Зарезервировано в пуле программных расходов под активные расходы (служебные записки в работе) */ + program_expense_reserved?: string | undefined | null, /** Доступная сумма членских взносов по программе */ program_membership_available: string, /** Накопительное вознаграждение на долю в членских взносах */ @@ -30068,6 +31352,12 @@ export type GraphQLTypes = { project_hash?: string | undefined | null, /** Имя пользователя (опционально) */ username?: string | undefined | null +}; + ["CapitalTopupProgramExpenseInput"]: { + /** Сумма пополнения (asset, eg "10000.0000 RUB"). */ + amount: string, + /** Имя кооператива. */ + coopname: string }; /** Голос в системе CAPITAL */ ["CapitalVote"]: { @@ -30749,6 +32039,22 @@ export type GraphQLTypes = { project_hash: string, /** Служебная записка о расходе */ statement: GraphQLTypes["SignedDigitalDocumentInput"] +}; + ["CreateExpenseProposalInput"]: { + /** Callback на финализацию closeexp (опционально). */ + callback?: GraphQLTypes["ExpenseCallbackInput"] | undefined | null, + /** Имя кооператива. */ + coopname: string, + /** Строки расхода (массив items). */ + items: Array, + /** Хеш сметы расхода (детерминированный, из UI). */ + proposal_hash: string, + /** Источник средств (eosio::name кошелька-источника, eg "w.cap.blago"). */ + source_wallet: string, + /** Подписанная СЗ-смета (document2, registry 2010). */ + statement: GraphQLTypes["ExpenseProposalStatementSignedDocumentInput"], + /** Имя пайщика-создателя СЗ. */ + username: string }; ["CreateIndividualDataInput"]: { /** Дата рождения */ @@ -30827,16 +32133,6 @@ export type GraphQLTypes = { ["CreateMatrixAccountInputDTO"]: { password: string, username: string -}; - ["CreateMembershipExitInput"]: { - /** Имя аккаунта кооператива */ - coopname: string, - /** Хеш процесса выхода (генерируется на клиенте) */ - exit_hash: string, - /** Подписанное пайщиком заявление о выходе из кооператива */ - statement: GraphQLTypes["MembershipExitApplicationSignedDocumentInput"], - /** Имя пайщика, выходящего из кооператива */ - username: string }; ["CreateOrganizationDataInput"]: { /** Банковский счет организации */ @@ -31447,6 +32743,47 @@ export type GraphQLTypes = { /** ОГРН */ ogrn: string }; + ["ExpenseCallbackInput"]: { + /** Action-метод */ + action?: string | undefined | null, + /** Контракт-целевой */ + contract?: string | undefined | null, + /** Payload (hex bytes) */ + data?: string | undefined | null +}; + /** Запись о первичном файле расхода (платёжка/чек/возврат). */ +["ExpenseFile"]: { + __typename: "ExpenseFile", + /** SHA-256 содержимого, hex-lowercase. */ + checksum_sha256: string, + /** Имя кооператива (scope). */ + coopname: string, + /** Внутренний ID записи. */ + id: number, + /** Хеш строки расхода (если файл уровня item). */ + item_hash?: string | undefined | null, + /** Назначение файла. */ + kind: GraphQLTypes["ExpenseFileKind"], + /** MIME-тип содержимого. */ + mime_type: string, + /** Оригинальное имя загруженного файла. */ + original_filename?: string | undefined | null, + /** Хеш сметы расхода. */ + proposal_hash: string, + /** Короткоживущий URL на скачивание (HMAC-signed). */ + read_url?: string | undefined | null, + /** Размер файла в байтах. */ + size_bytes: number, + /** MinIO-ключ внутри бакета. */ + storage_key: string, + /** Когда загружено. */ + uploaded_at: GraphQLTypes["DateTime"], + /** Кто загрузил (username). */ + uploaded_by_username: string, + ['...on ExpenseFile']: Omit +}; + /** Тип первичного файла расхода. */ +["ExpenseFileKind"]: ExpenseFileKind; ["ExpenseFilter"]: { /** Фильтр по ID фонда */ fundId?: string | undefined | null, @@ -31456,6 +32793,307 @@ export type GraphQLTypes = { status?: string | undefined | null, /** Фильтр по имени пользователя */ username?: string | undefined | null +}; + /** Строка сметы расхода. */ +["ExpenseItem"]: { + __typename: "ExpenseItem", + /** Фактическая сумма (после оплаты/отчёта). */ + actual_amount?: string | undefined | null, + /** Назначение/описание строки. */ + description: string, + /** Хеш строки расхода. */ + item_hash: string, + /** Способ оплаты. */ + mechanics: GraphQLTypes["ExpenseMechanics"], + /** Планируемая сумма. */ + planned_amount: string, + /** Идентификатор получателя. */ + recipient?: string | undefined | null, + /** Тип получателя платежа. */ + recipient_type: GraphQLTypes["ExpenseRecipientType"], + /** Статус строки. */ + status: GraphQLTypes["ExpenseItemStatus"], + ['...on ExpenseItem']: Omit +}; + ["ExpenseItemInput"]: { + /** Описание назначения расхода. */ + description: string, + /** Хеш строки расхода (детерминированный, из UI). */ + item_hash: string, + /** Способ оплаты (ADVANCE / DIRECT). */ + mechanics: GraphQLTypes["ExpenseMechanics"], + /** Идентификатор сохранённых реквизитов получателя-пайщика — реквизиты снимаются в момент создания и прикладываются к платежу. */ + payment_method_id?: string | undefined | null, + /** Назначение платежа (для оплаты по счёту) — фиксируется в снимке для кассира. */ + payment_purpose?: string | undefined | null, + /** Планируемая сумма (asset, eg "1000.0000 RUB"). */ + planned_amount: string, + /** Получатель: username пайщика; для организации — пустая строка (аккаунта в кооперативе нет). */ + recipient: string, + /** Тип получателя. */ + recipient_type: GraphQLTypes["ExpenseRecipientType"], + /** Реквизиты получателя-организации (вводятся вручную). */ + requisites?: string | undefined | null +}; + /** Статус строки расхода. */ +["ExpenseItemStatus"]: ExpenseItemStatus; + /** Способ оплаты строки расхода. */ +["ExpenseMechanics"]: ExpenseMechanics; + /** Смета расхода (СЗ). */ +["ExpenseProposal"]: { + __typename: "ExpenseProposal", + /** Дата создания записи */ + _created_at: GraphQLTypes["DateTime"], + /** Внутренний ID базы данных */ + _id: string, + /** Дата последнего обновления записи */ + _updated_at: GraphQLTypes["DateTime"], + /** Номер блока крайней синхронизации с блокчейном */ + block_num?: number | undefined | null, + /** Сырой статус из блокчейна (uint8). */ + blockchain_status?: number | undefined | null, + /** Имя кооператива (scope). */ + coopname: string, + /** Время создания (chain). */ + created_at?: string | undefined | null, + /** Решение совета о расходе (DocumentAggregate). */ + decision_doc?: GraphQLTypes["DocumentAggregate"] | undefined | null, + /** ID в блокчейне. */ + id?: number | undefined | null, + /** Строки сметы. */ + items: Array, + /** Флаг присутствия записи в блокчейне */ + present: boolean, + /** Хеш сметы расхода. */ + proposal_hash: string, + /** Кошелёк-источник средств. */ + source_wallet?: string | undefined | null, + /** Заявление пайщика о расходе (DocumentAggregate). */ + statement_doc?: GraphQLTypes["DocumentAggregate"] | undefined | null, + /** Доменный статус сметы. */ + status: GraphQLTypes["ExpenseProposalStatus"], + /** Сумма всех строк (факт). */ + total_actual?: string | undefined | null, + /** Сумма всех строк (план). */ + total_planned?: string | undefined | null, + /** Время последнего обновления (chain). */ + updated_at?: string | undefined | null, + /** Создатель сметы (username). */ + username?: string | undefined | null, + ['...on ExpenseProposal']: Omit +}; + ["ExpenseProposalDecisionBodyInput"]: { + /** Род решения (approve / decline) */ + kind: string, + /** Причина отказа (для decline) */ + reason?: string | undefined | null +}; + ["ExpenseProposalDecisionGenerateDocumentInput"]: { + /** Номер блока, на котором был создан документ */ + block_num?: number | undefined | null, + /** Название кооператива, связанное с документом */ + coopname: string, + /** Дата и время создания документа */ + created_at?: string | undefined | null, + /** Идентификатор решения совета (повестка) — источник данных голосования */ + decision_id: number, + /** Имя генератора, использованного для создания документа */ + generator?: string | undefined | null, + /** Позиции расхода */ + items: Array, + /** Язык документа */ + lang?: string | undefined | null, + /** Ссылки, связанные с документом */ + links?: Array | undefined | null, + /** Шапка СЗ */ + proposal: GraphQLTypes["ExpenseProposalDecisionHeaderInput"], + /** Хеш сметы расхода */ + proposal_hash: string, + /** Резолюция совета (утвердить / отказать) */ + resolution: GraphQLTypes["ExpenseProposalDecisionBodyInput"], + /** Часовой пояс, в котором был создан документ */ + timezone?: string | undefined | null, + /** Название документа */ + title?: string | undefined | null, + /** Имя пользователя, создавшего документ */ + username: string, + /** Версия генератора, использованного для создания документа */ + version?: string | undefined | null +}; + ["ExpenseProposalDecisionHeaderInput"]: { + deadline?: string | undefined | null, + description: string, + fund_name?: string | undefined | null, + items_count: number, + source_wallet: string, + total_amount: string +}; + ["ExpenseProposalDecisionItemInput"]: { + amount: string, + description: string, + mechanics: string, + number: string, + recipient_type: string +}; + ["ExpenseProposalHeaderInput"]: { + /** Срок исполнения («в срок до»), формат DD.MM.YYYY */ + deadline?: string | undefined | null, + /** Описание цели расходов */ + description: string, + /** Фонд списания — подставляется сервером из параметров шасси расходов, передавать не нужно */ + fund_name?: string | undefined | null, + /** Количество позиций */ + items_count: number, + /** Кошелёк-источник */ + source_wallet: string, + /** Итоговая сумма расходов */ + total_amount: string +}; + ["ExpenseProposalItemInput"]: { + /** Сумма строки */ + amount: string, + /** Описание расхода */ + description: string, + /** Способ оплаты (ADVANCE / DIRECT) */ + mechanics: string, + /** Порядковый номер строки */ + number: string, + /** Идентификатор сохранённых реквизитов получателя-пайщика — сервер подставит полные реквизиты в документ. */ + payment_method_id?: string | undefined | null, + /** Назначение платежа — отдельной строкой после реквизитов */ + payment_purpose?: string | undefined | null, + /** Имя получателя */ + recipient_name?: string | undefined | null, + /** Тип получателя (SELF / MEMBER / ORG) */ + recipient_type: string, + /** Имя аккаунта получателя-пайщика (владелец реквизитов). */ + recipient_username?: string | undefined | null, + /** Реквизиты получателя */ + requisites?: string | undefined | null +}; + ["ExpenseProposalSignedItemInput"]: { + /** Сумма строки */ + amount: string, + /** Описание расхода */ + description: string, + /** Способ оплаты (ADVANCE / DIRECT) */ + mechanics: string, + /** Порядковый номер строки */ + number: string, + /** Тип получателя (SELF / MEMBER / ORG) */ + recipient_type: string +}; + ["ExpenseProposalStatementGenerateDocumentInput"]: { + /** Номер блока, на котором был создан документ */ + block_num?: number | undefined | null, + /** Название кооператива, связанное с документом */ + coopname: string, + /** Дата и время создания документа */ + created_at?: string | undefined | null, + /** Имя генератора, использованного для создания документа */ + generator?: string | undefined | null, + /** Позиции расхода */ + items: Array, + /** Язык документа */ + lang?: string | undefined | null, + /** Ссылки, связанные с документом */ + links?: Array | undefined | null, + /** Шапка СЗ */ + proposal: GraphQLTypes["ExpenseProposalHeaderInput"], + /** Хеш сметы расхода (детерминированный) */ + proposal_hash: string, + /** Часовой пояс, в котором был создан документ */ + timezone?: string | undefined | null, + /** Название документа */ + title?: string | undefined | null, + /** Имя пользователя, создавшего документ */ + username: string, + /** Версия генератора, использованного для создания документа */ + version?: string | undefined | null +}; + ["ExpenseProposalStatementSignedDocumentInput"]: { + /** Хэш содержимого документа */ + doc_hash: string, + /** Общий хэш (doc_hash + meta_hash) */ + hash: string, + /** Метаинформация СЗ-заявления */ + meta: GraphQLTypes["ExpenseProposalStatementSignedMetaDocumentInput"], + /** Хэш мета-данных */ + meta_hash: string, + /** Вектор подписей */ + signatures: Array, + /** Версия стандарта документа */ + version: string +}; + ["ExpenseProposalStatementSignedMetaDocumentInput"]: { + /** Номер блока, на котором был создан документ */ + block_num: number, + /** Название кооператива, связанное с документом */ + coopname: string, + /** Дата и время создания документа */ + created_at: string, + /** Идентификатор приватных данных документа off-chain (реквизиты/имя/назначение) */ + doc_data_hash: string, + /** Имя генератора, использованного для создания документа */ + generator: string, + /** Публичные позиции расхода (без реквизитов) */ + items: Array, + /** Язык документа */ + lang: string, + /** Ссылки, связанные с документом */ + links: Array, + /** Шапка СЗ */ + proposal: GraphQLTypes["ExpenseProposalHeaderInput"], + /** Хеш сметы расхода (детерминированный) */ + proposal_hash: string, + /** ID документа в реестре */ + registry_id: number, + /** Часовой пояс, в котором был создан документ */ + timezone: string, + /** Название документа */ + title: string, + /** Имя пользователя, создавшего документ */ + username: string, + /** Версия генератора, использованного для создания документа */ + version: string +}; + /** Статус сметы расхода. */ +["ExpenseProposalStatus"]: ExpenseProposalStatus; + /** Тип получателя платежа. */ +["ExpenseRecipientType"]: ExpenseRecipientType; + /** Исход отчёта по строке-авансу: закрыто либо ожидается расчёт разницы (возврат/доплата). */ +["ExpenseReportOutcome"]: ExpenseReportOutcome; + ["ExpenseReportResult"]: { + __typename: "ExpenseReportResult", + /** Исход отчёта. */ + outcome: GraphQLTypes["ExpenseReportOutcome"], + /** Сумма разницы к расчёту (asset), при недо-/перерасходе. */ + settlement_amount?: string | undefined | null, + /** Хэш заведённой платёжки расчёта (возврат/доплата). */ + settlement_payment_hash?: string | undefined | null, + /** Транзакция закрытия позиции (только при CLOSED). */ + transaction?: GraphQLTypes["Transaction"] | undefined | null, + ['...on ExpenseReportResult']: Omit +}; + ["ExpenseRequisite"]: { + __typename: "ExpenseRequisite", + /** Имя кооператива. */ + coopname: string, + /** Снимок данных платёжного метода на момент подачи СЗ. */ + data?: GraphQLTypes["JSON"] | undefined | null, + /** Хеш строки расхода (item). */ + item_hash: string, + /** Тип платёжного метода пайщика (СБП / банковский перевод). */ + method_type?: string | undefined | null, + /** Назначение платежа для поручения кассиру. */ + payment_purpose?: string | undefined | null, + /** Хеш сметы расхода (proposal). */ + proposal_hash: string, + /** Получатель платежа (аккаунт пайщика или название организации). */ + recipient: string, + /** Реквизиты строкой — как в документе служебной записки. */ + requisites: string, + ['...on ExpenseRequisite']: Omit }; /** Статус расхода в системе CAPITAL */ ["ExpenseStatus"]: ExpenseStatus; @@ -32732,125 +34370,6 @@ export type GraphQLTypes = { votes_for: number, ['...on MeetQuestionResult']: Omit }; - ["MembershipExit"]: { - __typename: "MembershipExit", - /** Дата подачи заявления на выход */ - created_at: string, - /** Хеш процесса выхода */ - exit_hash: string, - /** Сумма к возврату (фиксируется советом при одобрении; до одобрения — 0) */ - quantity: string, - /** Статус процесса выхода */ - status: GraphQLTypes["MembershipExitStatus"], - ['...on MembershipExit']: Omit -}; - ["MembershipExitApplicationGenerateDocumentInput"]: { - /** Номер блока, на котором был создан документ */ - block_num?: number | undefined | null, - /** Название кооператива, связанное с документом */ - coopname: string, - /** Дата и время создания документа */ - created_at?: string | undefined | null, - /** Имя генератора, использованного для создания документа */ - generator?: string | undefined | null, - /** Язык документа */ - lang?: string | undefined | null, - /** Ссылки, связанные с документом */ - links?: Array | undefined | null, - /** Флаг пропуска сохранения документа (используется для предварительной генерации и демонстрации пользователю) */ - skip_save: boolean, - /** Часовой пояс, в котором был создан документ */ - timezone?: string | undefined | null, - /** Название документа */ - title?: string | undefined | null, - /** Имя пользователя, создавшего документ */ - username: string, - /** Версия генератора, использованного для создания документа */ - version?: string | undefined | null -}; - ["MembershipExitApplicationSignedDocumentInput"]: { - /** Хэш содержимого документа */ - doc_hash: string, - /** Общий хэш (doc_hash + meta_hash) */ - hash: string, - meta: GraphQLTypes["MembershipExitApplicationSignedMetaDocumentInput"], - /** Хэш мета-данных */ - meta_hash: string, - /** Вектор подписей */ - signatures: Array, - /** Версия стандарта документа */ - version: string -}; - ["MembershipExitApplicationSignedMetaDocumentInput"]: { - /** Номер блока, на котором был создан документ */ - block_num: number, - /** Название кооператива, связанное с документом */ - coopname: string, - /** Дата и время создания документа */ - created_at: string, - /** Имя генератора, использованного для создания документа */ - generator: string, - /** Язык документа */ - lang: string, - /** Ссылки, связанные с документом */ - links: Array, - /** ID документа в реестре */ - registry_id: number, - /** Флаг пропуска сохранения документа (используется для предварительной генерации и демонстрации пользователю) */ - skip_save: boolean, - /** Часовой пояс, в котором был создан документ */ - timezone: string, - /** Название документа */ - title: string, - /** Имя пользователя, создавшего документ */ - username: string, - /** Версия генератора, использованного для создания документа */ - version: string -}; - ["MembershipExitDecisionGenerateDocumentInput"]: { - /** Номер блока, на котором был создан документ */ - block_num?: number | undefined | null, - /** Название кооператива, связанное с документом */ - coopname: string, - /** Дата и время создания документа */ - created_at?: string | undefined | null, - /** Идентификатор протокола решения собрания совета */ - decision_id: number, - /** Имя генератора, использованного для создания документа */ - generator?: string | undefined | null, - /** Язык документа */ - lang?: string | undefined | null, - /** Ссылки, связанные с документом */ - links?: Array | undefined | null, - /** Часовой пояс, в котором был создан документ */ - timezone?: string | undefined | null, - /** Название документа */ - title?: string | undefined | null, - /** Имя пользователя, создавшего документ */ - username: string, - /** Версия генератора, использованного для создания документа */ - version?: string | undefined | null -}; - ["MembershipExitResult"]: { - __typename: "MembershipExitResult", - /** Хеш созданного процесса выхода */ - exit_hash: string, - /** Статус процесса выхода после подачи (ожидает подтверждения по ссылке из письма) */ - status: GraphQLTypes["MembershipExitStatus"], - ['...on MembershipExitResult']: Omit -}; - ["MembershipExitReturnPreview"]: { - __typename: "MembershipExitReturnPreview", - /** Минимальный паевой взнос пайщика */ - minimum_contribution: string, - /** Целевой паевой взнос пайщика */ - share_contribution: string, - /** Итоговая сумма к возврату (минимальный + целевой паевой) */ - total: string, - ['...on MembershipExitReturnPreview']: Omit -}; - /** Статус процесса выхода пайщика из кооператива */ -["MembershipExitStatus"]: MembershipExitStatus; ["MissingRequisiteField"]: { __typename: "MissingRequisiteField", key: string, @@ -32925,8 +34444,6 @@ export type GraphQLTypes = { Требуемые роли: chairman. */ authorizeDecision: GraphQLTypes["Transaction"], - /** Отменить заявление на выход до подтверждения по email. */ - cancelMembershipExit: boolean, /** Отменить заявку */ cancelRequest: GraphQLTypes["Transaction"], /** Добавление автора проекта в CAPITAL контракте @@ -32985,6 +34502,10 @@ export type GraphQLTypes = { Требуемые роли: chairman, member. */ capitalCreateProcessTemplate: GraphQLTypes["ProcessTemplate"], + /** Создание программного расхода капитала через шасси expense. + +Требуемые роли: chairman, member. */ + capitalCreateProgramExpense: GraphQLTypes["Transaction"], /** Инвестирование в программу благорост (денежная программная инвестиция) Требуемые роли: participant. */ @@ -33209,6 +34730,10 @@ export type GraphQLTypes = { Требуемые роли: chairman, member, user. */ capitalSubmitVote: GraphQLTypes["Transaction"], + /** Пополнение пула программных расходов капитала из инвестиционного пула. + +Требуемые роли: chairman. */ + capitalTopupProgramExpensePool: GraphQLTypes["Transaction"], /** Обновление задачи в CAPITAL контракте Требуемые роли: chairman, member, user. */ @@ -33283,8 +34808,6 @@ export type GraphQLTypes = { Требуемые роли: chairman, member. */ confirmAgreement: GraphQLTypes["Transaction"], - /** Подтвердить выход из кооператива по ссылке из письма. Проверяет токен и отправляет ранее подписанное заявление в блокчейн. */ - confirmMembershipExit: GraphQLTypes["MembershipExitResult"], /** Подтвердить получение имущества Уполномоченным лицом от Заказчика по новации и акту приёмки-передачи */ confirmReceiveOnRequest: GraphQLTypes["Transaction"], /** Подтвердить поставку имущества Поставщиком по заявке Заказчика и акту приёма-передачи */ @@ -33303,12 +34826,14 @@ export type GraphQLTypes = { Требуемые роли: chairman, member. */ createDepositPayment: GraphQLTypes["GatewayPayment"], + /** Подать СЗ-расход (создать смету с подписью пайщика/председателя). + +Требуемые роли: chairman, member. */ + createExpenseProposal: GraphQLTypes["Transaction"], /** Создание объекта регистрационного платежа производится мутацией createInitialPayment. Выполнение мутации возвращает идентификатор платежа и данные для его совершения в зависимости от выбранного платежного провайдера. Требуемые роли: chairman, member. */ createInitialPayment: GraphQLTypes["GatewayPayment"], - /** Подать подписанное заявление на выход из кооператива. Запускает рассмотрение советом и последующий возврат паевого взноса. */ - createMembershipExit: GraphQLTypes["MembershipExitResult"], /** Создать предложение на поставку имущества */ createParentOffer: GraphQLTypes["Transaction"], /** Создать повестку дня и проект решения, и сохранить в хранилище для дальнейшей генерации документа и его публикации @@ -33397,16 +34922,18 @@ export type GraphQLTypes = { generateConvertToAxonStatement: GraphQLTypes["GeneratedDocument"], /** Универсальная генерация документа с произвольными данными (только для председателя) */ generateDocument: GraphQLTypes["GeneratedDocument"], + /** Сгенерировать документ-решение по СЗ (registry 2011) для последующей подписи. + +Требуемые роли: chairman. */ + generateExpenseProposalDecisionDocument: GraphQLTypes["GeneratedDocument"], + /** Сгенерировать документ СЗ-заявления (registry 2010) для последующей подписи. + +Требуемые роли: chairman, member. */ + generateExpenseProposalStatementDocument: GraphQLTypes["GeneratedDocument"], /** Сгенерировать протокол решения по предложенной повестке Требуемые роли: chairman, member. */ generateFreeDecision: GraphQLTypes["GeneratedDocument"], - /** Сгенерировать документ заявления о выходе из кооператива. */ - generateMembershipExitApplication: GraphQLTypes["GeneratedDocument"], - /** Сгенерировать документ решения собрания совета о выходе пайщика. - -Требуемые роли: chairman, member. */ - generateMembershipExitDecision: GraphQLTypes["GeneratedDocument"], /** Сгенерировать документ заявления о вступлении в кооператив. Требуемые роли: chairman, member. */ @@ -33501,6 +35028,14 @@ export type GraphQLTypes = { Требуемые роли: chairman, member. */ notifyOnAnnualGeneralMeet: GraphQLTypes["MeetAggregate"], + /** Доплатить сумму перерасхода по строке расхода (ADVANCE-механика). + +Требуемые роли: chairman. */ + overspendExpenseItem: GraphQLTypes["Transaction"], + /** Оплатить строку расхода (выдача аванса ADVANCE или прямая оплата DIRECT). + +Требуемые роли: chairman. */ + payExpenseItem: GraphQLTypes["Transaction"], /** Обрабатывает подписанное заявление на конвертацию и выполняет блокчейн-транзакцию Требуемые роли: member, chairman. */ @@ -33523,6 +35058,10 @@ export type GraphQLTypes = { Требуемые роли: chairman, member. */ registerParticipant: GraphQLTypes["Account"], + /** Отчитаться по строке-авансу: при совпадении факта с авансом — закрыть позицию; при недо-/перерасходе — завести платёжку расчёта разницы. + +Требуемые роли: chairman, member, user. */ + reportExpenseItem: GraphQLTypes["ExpenseReportResult"], /** Переотправить уведомление (force-постановка новой строки в очередь доставки) Требуемые роли: chairman. */ @@ -33535,6 +35074,10 @@ export type GraphQLTypes = { Требуемые роли: chairman. */ restartAnnualGeneralMeet: GraphQLTypes["MeetAggregate"], + /** Вернуть неиспользованный аванс по строке расхода (ADVANCE-остаток). + +Требуемые роли: chairman, member, user. */ + returnExpenseItem: GraphQLTypes["Transaction"], /** Сохранить/обновить черновик формы отчёта (upsert по owner+type+year+period) Требуемые роли: chairman. */ @@ -33563,6 +35106,10 @@ export type GraphQLTypes = { startInstall: GraphQLTypes["StartInstallResult"], /** Выслать токен для замены приватного ключа аккаунта на электронную почту */ startResetKey: boolean, + /** Финализировать СЗ-отчёт по смете расхода (все items закрыты — оплата/чек/возврат). + +Требуемые роли: chairman, member. */ + submitExpenseReport: GraphQLTypes["Transaction"], /** Подтвердить поставку имущества Поставщиком по заявке Заказчика и акту приёма-передачи */ supplyOnRequest: GraphQLTypes["Transaction"], /** Запустить воркфлоу уведомлений (только для председателя или server-secret) @@ -33599,6 +35146,10 @@ export type GraphQLTypes = { Требуемые роли: chairman. */ updateSystem: GraphQLTypes["SystemInfo"], + /** Загрузить первичный файл расхода (платёжка/чек/возврат) в бакет expenses:files. + +Требуемые роли: chairman, member, user. */ + uploadExpenseFile: GraphQLTypes["ExpenseFile"], /** Подтвердить email адрес пользователя */ verifyEmail: boolean, /** Голосование на общем собрании пайщиков @@ -33847,6 +35398,16 @@ export type GraphQLTypes = { /** Имя аккаунта организации */ username: string, ['...on OrganizationWithBankAccount']: Omit +}; + ["OverspendExpenseItemInput"]: { + /** Имя кооператива. */ + coopname: string, + /** Хеш строки расхода (item). */ + item_hash: string, + /** Сумма доплаты сверх planned_amount (asset, например "200.0000 RUB"). */ + overspend_amount: string, + /** Хеш сметы расхода (proposal). */ + proposal_hash: string }; ["PaginatedActionsPaginationResult"]: { __typename: "PaginatedActionsPaginationResult", @@ -33991,6 +35552,18 @@ export type GraphQLTypes = { /** Общее количество страниц */ totalPages: number, ['...on PaginatedCapitalLogsPaginationResult']: Omit +}; + ["PaginatedCapitalProgramExpensesPaginationResult"]: { + __typename: "PaginatedCapitalProgramExpensesPaginationResult", + /** Текущая страница */ + currentPage: number, + /** Элементы текущей страницы */ + items: Array, + /** Общее количество элементов */ + totalCount: number, + /** Общее количество страниц */ + totalPages: number, + ['...on PaginatedCapitalProgramExpensesPaginationResult']: Omit }; ["PaginatedCapitalProjectsPaginationResult"]: { __typename: "PaginatedCapitalProjectsPaginationResult", @@ -34111,6 +35684,18 @@ export type GraphQLTypes = { /** Общее количество страниц */ totalPages: number, ['...on PaginatedDeltasPaginationResult']: Omit +}; + ["PaginatedExpenseProposalsPaginationResult"]: { + __typename: "PaginatedExpenseProposalsPaginationResult", + /** Текущая страница */ + currentPage: number, + /** Элементы текущей страницы */ + items: Array, + /** Общее количество элементов */ + totalCount: number, + /** Общее количество страниц */ + totalPages: number, + ['...on PaginatedExpenseProposalsPaginationResult']: Omit }; ["PaginatedGatewayPaymentsPaginationResult"]: { __typename: "PaginatedGatewayPaymentsPaginationResult", @@ -34277,6 +35862,16 @@ export type GraphQLTypes = { issued_by: string, number: number, series: number +}; + ["PayExpenseItemInput"]: { + /** Фактическая сумма оплаты (asset, например "100.0000 RUB"). */ + actual_amount: string, + /** Имя кооператива. */ + coopname: string, + /** Хеш строки расхода (item). */ + item_hash: string, + /** Хеш сметы расхода (proposal). */ + proposal_hash: string }; ["PaymentDetails"]: { __typename: "PaymentDetails", @@ -34305,6 +35900,8 @@ export type GraphQLTypes = { direction?: GraphQLTypes["PaymentDirection"] | undefined | null, /** Хэш платежа */ hash?: string | undefined | null, + /** Хэш расхода (служебной записки): вернёт все платежи, связанные с этим расходом */ + proposal_hash?: string | undefined | null, /** Провайдер платежа */ provider?: string | undefined | null, /** Статус платежа */ @@ -34924,6 +36521,14 @@ export type GraphQLTypes = { Требуемые роли: chairman, member, user. */ capitalIssues: GraphQLTypes["PaginatedCapitalIssuesPaginationResult"], + /** Программный расход по expense_hash. + +Требуемые роли: chairman, member. */ + capitalProgramExpense?: GraphQLTypes["CapitalProgramExpense"] | undefined | null, + /** Список программных расходов капитала (через шасси expense). + +Требуемые роли: chairman, member. */ + capitalProgramExpenses: GraphQLTypes["PaginatedCapitalProgramExpensesPaginationResult"], /** Получение проекта по хешу с компонентами */ capitalProject?: GraphQLTypes["CapitalProject"] | undefined | null, /** Получение проекта с полными отношениями по хешу проекта */ @@ -35028,6 +36633,34 @@ export type GraphQLTypes = { cooperativeAgreements: Array, /** Целевые потребительские программы кооператива (id, тип, активность, draft_id) */ cooperativePrograms: Array, + /** Получить запись о файле + свежий короткоживущий read-URL. + +Требуемые роли: chairman, member, user. */ + expenseFile: GraphQLTypes["ExpenseFile"], + /** Список файлов строки расхода (без read-URL — запрос отдельно по id). + +Требуемые роли: chairman, member, user. */ + expenseFilesByItem: Array, + /** Список файлов сметы расхода (без read-URL — запрос отдельно по id). + +Требуемые роли: chairman, member, user. */ + expenseFilesByProposal: Array, + /** Получить смету расхода по хешу. + +Требуемые роли: chairman, member, user. */ + expenseProposal?: GraphQLTypes["ExpenseProposal"] | undefined | null, + /** Список смет расходов кооператива (paginated). + +Требуемые роли: chairman, member. */ + expenseProposalsByCooperative: GraphQLTypes["PaginatedExpenseProposalsPaginationResult"], + /** Список смет расходов пайщика (свои/созданные им, paginated). + +Требуемые роли: chairman, member, user. */ + expenseProposalsByMember: GraphQLTypes["PaginatedExpenseProposalsPaginationResult"], + /** Снимки реквизитов получателей по строкам СЗ (персональные данные — только совету; в блокчейн не пишутся). + +Требуемые роли: chairman, member. */ + expenseRequisitesByProposal: Array, /** Получить сводную информацию о аккаунте Требуемые роли: chairman, member. */ @@ -35208,10 +36841,6 @@ export type GraphQLTypes = { Требуемые роли: chairman. */ listReportDrafts: Array, - /** Текущий процесс выхода пайщика из кооператива (статус заявления и планируемая сумма возврата). null — активного выхода нет. */ - membershipExit?: GraphQLTypes["MembershipExit"] | undefined | null, - /** Предварительный расчёт суммы возврата паевого взноса при выходе пайщика (минимальный + целевой паевой). Ориентир для пайщика; итог фиксирует совет. */ - membershipExitReturnPreview: GraphQLTypes["MembershipExitReturnPreview"], /** Получение документов кооператива для синхронизации с 1С. Требует секретный ключ в заголовке x-onecoop-secret-key. */ onecoopGetDocuments: GraphQLTypes["OneCoopDocumentsResponse"], /** Получить полную картину процесса ledger2 по process_hash @@ -35470,6 +37099,16 @@ export type GraphQLTypes = { updatedAt: GraphQLTypes["DateTime"], year: number, ['...on ReportDraft']: Omit +}; + ["ReportExpenseItemInput"]: { + /** Фактически потраченная сумма по чекам (asset, например "800.0000 RUB"). Не указана — равна выданному авансу. */ + actual_amount?: string | undefined | null, + /** Имя кооператива. */ + coopname: string, + /** Хеш строки расхода (item). */ + item_hash: string, + /** Хеш сметы расхода (proposal). */ + proposal_hash: string }; ["ReportHistoryFilterInput"]: { /** Лимит (макс 100, по умолчанию 20) */ @@ -35939,6 +37578,16 @@ export type GraphQLTypes = { username: string, /** Версия генератора, использованного для создания документа */ version: string +}; + ["ReturnExpenseItemInput"]: { + /** Имя кооператива. */ + coopname: string, + /** Хеш строки расхода (item). */ + item_hash: string, + /** Хеш сметы расхода (proposal). */ + proposal_hash: string, + /** Возвращаемая сумма (asset, например "50.0000 RUB"). */ + return_amount: string }; /** Тип сообщения в истории комнаты Matrix (текст или расшифрованное аудио) */ ["RoomMessageKind"]: RoomMessageKind; @@ -36326,6 +37975,16 @@ export type GraphQLTypes = { }; /** Статус истории в системе CAPITAL */ ["StoryStatus"]: StoryStatus; + ["SubmitExpenseReportInput"]: { + /** Комментарий пайщика к финализации отчёта (свободный текст, до 1000 символов). */ + comment?: string | undefined | null, + /** Имя кооператива. */ + coopname: string, + /** Хеш сметы расхода. */ + proposal_hash: string, + /** Итоговая сумма фактических расходов (asset, например "1500.0000 RUB"). */ + total_actual_amount?: string | undefined | null +}; ["SubmitVoteInput"]: { /** Имя аккаунта кооператива */ coopname: string, @@ -36701,6 +38360,26 @@ export type GraphQLTypes = { story_hash: string, /** Название истории */ title?: string | undefined | null +}; + ["UploadExpenseFileInput"]: { + /** SHA-256 содержимого, hex-lowercase (64 hex-символа). */ + checksum_sha256: string, + /** Содержимое файла, base64 без префикса data:. */ + content_base64: string, + /** Имя кооператива. */ + coopname: string, + /** Хеш строки расхода (пусто — файл уровня сметы). */ + item_hash?: string | undefined | null, + /** Назначение файла (платёжка/чек/возврат). */ + kind: GraphQLTypes["ExpenseFileKind"], + /** MIME-тип содержимого. */ + mime_type: string, + /** Оригинальное имя файла — для отображения и поиска. */ + original_filename?: string | undefined | null, + /** Хеш сметы расхода. */ + proposal_hash: string, + /** Размер файла в байтах (для серверной валидации). */ + size_bytes: number }; ["UserAccount"]: { __typename: "UserAccount", @@ -37028,6 +38707,49 @@ export enum DocumentAction { REGCOOP = "REGCOOP", WITHDRAW = "WITHDRAW" } +/** Тип первичного файла расхода. */ +export enum ExpenseFileKind { + CLOSING_DOC = "CLOSING_DOC", + PAYMENT_PROOF = "PAYMENT_PROOF", + REPORT_FILE = "REPORT_FILE", + RETURN_PROOF = "RETURN_PROOF" +} +/** Статус строки расхода. */ +export enum ExpenseItemStatus { + APPROVED = "APPROVED", + OVERSPENT = "OVERSPENT", + PAID = "PAID", + REPORTED = "REPORTED", + RETURNED = "RETURNED", + UNDEFINED = "UNDEFINED" +} +/** Способ оплаты строки расхода. */ +export enum ExpenseMechanics { + ADVANCE = "ADVANCE", + DIRECT = "DIRECT" +} +/** Статус сметы расхода. */ +export enum ExpenseProposalStatus { + AUTHORIZED = "AUTHORIZED", + CLOSED = "CLOSED", + CREATED = "CREATED", + DECLINED = "DECLINED", + PARTIALLY_PAID = "PARTIALLY_PAID", + REPORT_SUBMITTED = "REPORT_SUBMITTED", + UNDEFINED = "UNDEFINED" +} +/** Тип получателя платежа. */ +export enum ExpenseRecipientType { + MEMBER = "MEMBER", + ORG = "ORG", + SELF = "SELF" +} +/** Исход отчёта по строке-авансу: закрыто либо ожидается расчёт разницы (возврат/доплата). */ +export enum ExpenseReportOutcome { + CLOSED = "CLOSED", + OVERSPEND_PENDING = "OVERSPEND_PENDING", + RETURN_PENDING = "RETURN_PENDING" +} /** Статус расхода в системе CAPITAL */ export enum ExpenseStatus { APPROVED = "APPROVED", @@ -37150,12 +38872,6 @@ export enum ManagedRoomKind { MEMBERS = "MEMBERS", SECRETARY = "SECRETARY" } -/** Статус процесса выхода пайщика из кооператива */ -export enum MembershipExitStatus { - AUTHORIZED = "AUTHORIZED", - AWAITING_CONFIRMATION = "AWAITING_CONFIRMATION", - PENDING = "PENDING" -} /** Тип комнаты вне проекта: пайщики, совет, комната секретаря */ export enum NonProjectRoomKind { COUNCIL = "COUNCIL", @@ -37211,7 +38927,9 @@ export enum PaymentStatus { /** Тип платежа по назначению */ export enum PaymentType { DEPOSIT = "DEPOSIT", - MEMBERSHIP_EXIT = "MEMBERSHIP_EXIT", + EXPENSE = "EXPENSE", + EXPENSE_OVERSPEND = "EXPENSE_OVERSPEND", + EXPENSE_RETURN = "EXPENSE_RETURN", REGISTRATION = "REGISTRATION", REGISTRATION_REFUND = "REGISTRATION_REFUND", WITHDRAWAL = "WITHDRAWAL" @@ -37390,6 +39108,7 @@ type ZEUS_VARIABLES = { ["CandidateStatus"]: ValueTypes["CandidateStatus"]; ["CapitalCommitFilter"]: ValueTypes["CapitalCommitFilter"]; ["CapitalContributorFilter"]: ValueTypes["CapitalContributorFilter"]; + ["CapitalCreateProgramExpenseInput"]: ValueTypes["CapitalCreateProgramExpenseInput"]; ["CapitalCycleFilter"]: ValueTypes["CapitalCycleFilter"]; ["CapitalInvestFilter"]: ValueTypes["CapitalInvestFilter"]; ["CapitalIssueFilter"]: ValueTypes["CapitalIssueFilter"]; @@ -37402,6 +39121,7 @@ type ZEUS_VARIABLES = { ["CapitalStoryFilter"]: ValueTypes["CapitalStoryFilter"]; ["CapitalTimeEntriesFilter"]: ValueTypes["CapitalTimeEntriesFilter"]; ["CapitalTimeStatsInput"]: ValueTypes["CapitalTimeStatsInput"]; + ["CapitalTopupProgramExpenseInput"]: ValueTypes["CapitalTopupProgramExpenseInput"]; ["ChairmanOnboardingAgendaInput"]: ValueTypes["ChairmanOnboardingAgendaInput"]; ["ChairmanOnboardingAgendaStep"]: ValueTypes["ChairmanOnboardingAgendaStep"]; ["ChairmanOnboardingGeneralMeetInput"]: ValueTypes["ChairmanOnboardingGeneralMeetInput"]; @@ -37438,12 +39158,12 @@ type ZEUS_VARIABLES = { ["CreateDepositPaymentInput"]: ValueTypes["CreateDepositPaymentInput"]; ["CreateEntrepreneurDataInput"]: ValueTypes["CreateEntrepreneurDataInput"]; ["CreateExpenseInput"]: ValueTypes["CreateExpenseInput"]; + ["CreateExpenseProposalInput"]: ValueTypes["CreateExpenseProposalInput"]; ["CreateIndividualDataInput"]: ValueTypes["CreateIndividualDataInput"]; ["CreateInitOrganizationDataInput"]: ValueTypes["CreateInitOrganizationDataInput"]; ["CreateInitialPaymentInput"]: ValueTypes["CreateInitialPaymentInput"]; ["CreateIssueInput"]: ValueTypes["CreateIssueInput"]; ["CreateMatrixAccountInputDTO"]: ValueTypes["CreateMatrixAccountInputDTO"]; - ["CreateMembershipExitInput"]: ValueTypes["CreateMembershipExitInput"]; ["CreateOrganizationDataInput"]: ValueTypes["CreateOrganizationDataInput"]; ["CreateParentOfferInput"]: ValueTypes["CreateParentOfferInput"]; ["CreateProcessTemplateInput"]: ValueTypes["CreateProcessTemplateInput"]; @@ -37483,7 +39203,25 @@ type ZEUS_VARIABLES = { ["EditContributorInput"]: ValueTypes["EditContributorInput"]; ["EditProjectInput"]: ValueTypes["EditProjectInput"]; ["EntrepreneurDetailsInput"]: ValueTypes["EntrepreneurDetailsInput"]; + ["ExpenseCallbackInput"]: ValueTypes["ExpenseCallbackInput"]; + ["ExpenseFileKind"]: ValueTypes["ExpenseFileKind"]; ["ExpenseFilter"]: ValueTypes["ExpenseFilter"]; + ["ExpenseItemInput"]: ValueTypes["ExpenseItemInput"]; + ["ExpenseItemStatus"]: ValueTypes["ExpenseItemStatus"]; + ["ExpenseMechanics"]: ValueTypes["ExpenseMechanics"]; + ["ExpenseProposalDecisionBodyInput"]: ValueTypes["ExpenseProposalDecisionBodyInput"]; + ["ExpenseProposalDecisionGenerateDocumentInput"]: ValueTypes["ExpenseProposalDecisionGenerateDocumentInput"]; + ["ExpenseProposalDecisionHeaderInput"]: ValueTypes["ExpenseProposalDecisionHeaderInput"]; + ["ExpenseProposalDecisionItemInput"]: ValueTypes["ExpenseProposalDecisionItemInput"]; + ["ExpenseProposalHeaderInput"]: ValueTypes["ExpenseProposalHeaderInput"]; + ["ExpenseProposalItemInput"]: ValueTypes["ExpenseProposalItemInput"]; + ["ExpenseProposalSignedItemInput"]: ValueTypes["ExpenseProposalSignedItemInput"]; + ["ExpenseProposalStatementGenerateDocumentInput"]: ValueTypes["ExpenseProposalStatementGenerateDocumentInput"]; + ["ExpenseProposalStatementSignedDocumentInput"]: ValueTypes["ExpenseProposalStatementSignedDocumentInput"]; + ["ExpenseProposalStatementSignedMetaDocumentInput"]: ValueTypes["ExpenseProposalStatementSignedMetaDocumentInput"]; + ["ExpenseProposalStatus"]: ValueTypes["ExpenseProposalStatus"]; + ["ExpenseRecipientType"]: ValueTypes["ExpenseRecipientType"]; + ["ExpenseReportOutcome"]: ValueTypes["ExpenseReportOutcome"]; ["ExpenseStatus"]: ValueTypes["ExpenseStatus"]; ["ExtendedMeetStatus"]: ValueTypes["ExtendedMeetStatus"]; ["ExtensionInput"]: ValueTypes["ExtensionInput"]; @@ -37555,11 +39293,6 @@ type ZEUS_VARIABLES = { ["MakeClearanceInput"]: ValueTypes["MakeClearanceInput"]; ["ManagedRoomKind"]: ValueTypes["ManagedRoomKind"]; ["MarkReportPeriodInput"]: ValueTypes["MarkReportPeriodInput"]; - ["MembershipExitApplicationGenerateDocumentInput"]: ValueTypes["MembershipExitApplicationGenerateDocumentInput"]; - ["MembershipExitApplicationSignedDocumentInput"]: ValueTypes["MembershipExitApplicationSignedDocumentInput"]; - ["MembershipExitApplicationSignedMetaDocumentInput"]: ValueTypes["MembershipExitApplicationSignedMetaDocumentInput"]; - ["MembershipExitDecisionGenerateDocumentInput"]: ValueTypes["MembershipExitDecisionGenerateDocumentInput"]; - ["MembershipExitStatus"]: ValueTypes["MembershipExitStatus"]; ["ModerateRequestInput"]: ValueTypes["ModerateRequestInput"]; ["MoveCapitalIssueToComponentInput"]: ValueTypes["MoveCapitalIssueToComponentInput"]; ["NonProjectRoomKind"]: ValueTypes["NonProjectRoomKind"]; @@ -37572,12 +39305,14 @@ type ZEUS_VARIABLES = { ["OpenProjectInput"]: ValueTypes["OpenProjectInput"]; ["OrganizationDetailsInput"]: ValueTypes["OrganizationDetailsInput"]; ["OrganizationType"]: ValueTypes["OrganizationType"]; + ["OverspendExpenseItemInput"]: ValueTypes["OverspendExpenseItemInput"]; ["PaginationInput"]: ValueTypes["PaginationInput"]; ["ParticipantApplicationDecisionGenerateDocumentInput"]: ValueTypes["ParticipantApplicationDecisionGenerateDocumentInput"]; ["ParticipantApplicationGenerateDocumentInput"]: ValueTypes["ParticipantApplicationGenerateDocumentInput"]; ["ParticipantApplicationSignedDocumentInput"]: ValueTypes["ParticipantApplicationSignedDocumentInput"]; ["ParticipantApplicationSignedMetaDocumentInput"]: ValueTypes["ParticipantApplicationSignedMetaDocumentInput"]; ["PassportInput"]: ValueTypes["PassportInput"]; + ["PayExpenseItemInput"]: ValueTypes["PayExpenseItemInput"]; ["PaymentDirection"]: ValueTypes["PaymentDirection"]; ["PaymentFiltersInput"]: ValueTypes["PaymentFiltersInput"]; ["PaymentStatus"]: ValueTypes["PaymentStatus"]; @@ -37613,6 +39348,7 @@ type ZEUS_VARIABLES = { ["RegisterContributorInput"]: ValueTypes["RegisterContributorInput"]; ["RegisterParticipantInput"]: ValueTypes["RegisterParticipantInput"]; ["RemoveSecretaryRoomInput"]: ValueTypes["RemoveSecretaryRoomInput"]; + ["ReportExpenseItemInput"]: ValueTypes["ReportExpenseItemInput"]; ["ReportHistoryFilterInput"]: ValueTypes["ReportHistoryFilterInput"]; ["ReportPreviewInput"]: ValueTypes["ReportPreviewInput"]; ["ReportSubmissionMark"]: ValueTypes["ReportSubmissionMark"]; @@ -37637,6 +39373,7 @@ type ZEUS_VARIABLES = { ["ReturnByMoneyGenerateDocumentInput"]: ValueTypes["ReturnByMoneyGenerateDocumentInput"]; ["ReturnByMoneySignedDocumentInput"]: ValueTypes["ReturnByMoneySignedDocumentInput"]; ["ReturnByMoneySignedMetaDocumentInput"]: ValueTypes["ReturnByMoneySignedMetaDocumentInput"]; + ["ReturnExpenseItemInput"]: ValueTypes["ReturnExpenseItemInput"]; ["RoomMessageKind"]: ValueTypes["RoomMessageKind"]; ["SaveReportDraftInput"]: ValueTypes["SaveReportDraftInput"]; ["SbpDataInput"]: ValueTypes["SbpDataInput"]; @@ -37669,6 +39406,7 @@ type ZEUS_VARIABLES = { ["StartVotingInput"]: ValueTypes["StartVotingInput"]; ["StopProjectInput"]: ValueTypes["StopProjectInput"]; ["StoryStatus"]: ValueTypes["StoryStatus"]; + ["SubmitExpenseReportInput"]: ValueTypes["SubmitExpenseReportInput"]; ["SubmitVoteInput"]: ValueTypes["SubmitVoteInput"]; ["SupplyOnRequestInput"]: ValueTypes["SupplyOnRequestInput"]; ["SystemStatus"]: ValueTypes["SystemStatus"]; @@ -37690,6 +39428,7 @@ type ZEUS_VARIABLES = { ["UpdateRequestInput"]: ValueTypes["UpdateRequestInput"]; ["UpdateSettingsInput"]: ValueTypes["UpdateSettingsInput"]; ["UpdateStoryInput"]: ValueTypes["UpdateStoryInput"]; + ["UploadExpenseFileInput"]: ValueTypes["UploadExpenseFileInput"]; ["UserStatus"]: ValueTypes["UserStatus"]; ["VarsInput"]: ValueTypes["VarsInput"]; ["VerifyEmailInputDTO"]: ValueTypes["VerifyEmailInputDTO"]; diff --git a/package.json b/package.json index 42058f5fb73..523d2d5a637 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "reboot": "cd components/boot/scripts && bash reboot.sh", "reboot:clean": "cd components/boot/scripts && bash clean_reboot.sh", "reboot:extra": "cd components/boot/scripts && bash extra_reboot.sh", + "reboot:blago": "cd components/boot/scripts && bash blago_reboot.sh", "deploy": "lerna run deploy --scope @coopenomics/boot --", "clear": "lerna run clear --scope @coopenomics/boot", "enter": "lerna run run --scope @coopenomics/cleos",